qoro-divi 0.3.3__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.

Files changed (74) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +7 -0
  3. divi/backends/_circuit_runner.py +46 -0
  4. divi/{parallel_simulator.py → backends/_parallel_simulator.py} +136 -53
  5. divi/backends/_qoro_service.py +531 -0
  6. divi/circuits/__init__.py +5 -0
  7. divi/circuits/_core.py +226 -0
  8. divi/{qasm.py → circuits/qasm.py} +21 -2
  9. divi/{exp → extern}/cirq/_validator.py +9 -7
  10. divi/qprog/__init__.py +18 -5
  11. divi/qprog/algorithms/__init__.py +14 -0
  12. divi/qprog/algorithms/_ansatze.py +311 -0
  13. divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +69 -41
  14. divi/qprog/{_vqe.py → algorithms/_vqe.py} +79 -135
  15. divi/qprog/batch.py +239 -55
  16. divi/qprog/exceptions.py +9 -0
  17. divi/qprog/optimizers.py +219 -18
  18. divi/qprog/quantum_program.py +389 -57
  19. divi/qprog/workflows/__init__.py +10 -0
  20. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +3 -34
  21. divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +42 -25
  22. divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +59 -26
  23. divi/reporting/__init__.py +7 -0
  24. divi/reporting/_pbar.py +112 -0
  25. divi/{qlogger.py → reporting/_qlogger.py} +37 -2
  26. divi/{reporter.py → reporting/_reporter.py} +8 -14
  27. divi/utils.py +49 -10
  28. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/METADATA +2 -1
  29. qoro_divi-0.3.5.dist-info/RECORD +69 -0
  30. divi/_pbar.py +0 -70
  31. divi/circuits.py +0 -139
  32. divi/interfaces.py +0 -25
  33. divi/qoro_service.py +0 -425
  34. qoro_divi-0.3.3.dist-info/RECORD +0 -62
  35. /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
  36. /divi/{qem.py → circuits/qem.py} +0 -0
  37. /divi/{exp → extern}/cirq/__init__.py +0 -0
  38. /divi/{exp → extern}/cirq/_lexer.py +0 -0
  39. /divi/{exp → extern}/cirq/_parser.py +0 -0
  40. /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
  41. /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
  42. /divi/{exp → extern}/cirq/exception.py +0 -0
  43. /divi/{exp → extern}/scipy/_cobyla.py +0 -0
  44. /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
  45. /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
  46. /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
  47. /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
  48. /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
  49. /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
  50. /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
  51. /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
  52. /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
  53. /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
  54. /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
  55. /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
  56. /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
  57. /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
  58. /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
  59. /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
  60. /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
  61. /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
  62. /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
  63. /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
  64. /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
  65. /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
  66. /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
  67. /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
  68. /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
  69. /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
  70. /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
  71. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSE +0 -0
  72. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/.license-header +0 -0
  73. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  74. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/WHEEL +0 -0
@@ -5,25 +5,179 @@
5
5
  import logging
6
6
  import pickle
7
7
  from abc import ABC, abstractmethod
8
- from functools import partial
8
+ from functools import lru_cache, partial
9
9
  from itertools import groupby
10
10
  from queue import Queue
11
+ from threading import Event
11
12
 
12
13
  import numpy as np
13
- from pennylane.measurements import ExpectationMP
14
+ import pennylane as qml
14
15
  from scipy.optimize import OptimizeResult
15
16
 
16
- from divi import QoroService
17
+ from divi.backends import CircuitRunner, JobStatus, QoroService
17
18
  from divi.circuits import Circuit, MetaCircuit
18
- from divi.interfaces import CircuitRunner
19
- from divi.qem import _NoMitigation
20
- from divi.qoro_service import JobStatus
19
+ from divi.circuits.qem import _NoMitigation
20
+ from divi.qprog.exceptions import _CancelledError
21
21
  from divi.qprog.optimizers import ScipyMethod, ScipyOptimizer
22
- from divi.reporter import LoggingProgressReporter, QueueProgressReporter
22
+ from divi.reporting import LoggingProgressReporter, QueueProgressReporter
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
26
 
27
+ def _get_structural_key(obs: qml.operation.Operation) -> tuple[str, ...]:
28
+ """Generates a hashable, wire-independent key from an observable's structure.
29
+
30
+ This function is used to create a canonical representation of an observable
31
+ based on its constituent Pauli operators, ignoring the wires they act on.
32
+ This key is ideal for caching computed eigenvalues, as observables with the
33
+ same structure (e.g., PauliX(0) @ PauliZ(1) and PauliX(2) @ PauliZ(3))
34
+ share the same eigenvalues. It maps PauliX and PauliY to PauliZ because
35
+ they are all isospectral (have eigenvalues [1, -1]).
36
+
37
+ Args:
38
+ obs: A PennyLane observable (e.g., qml.PauliZ(0), qml.PauliX(0) @ qml.PauliY(1)).
39
+
40
+ Returns:
41
+ A tuple of strings representing the structure of the observable,
42
+ e.g., ('PauliZ',) or ('PauliZ', 'PauliZ').
43
+ """
44
+
45
+ # Pennylane returns the same eigenvalues for PauliX and PauliY
46
+ # since it handles diagonalizing gates internally anyway
47
+ name_map = {
48
+ "PauliY": "PauliZ",
49
+ "PauliX": "PauliZ",
50
+ "PauliZ": "PauliZ",
51
+ "Identity": "Identity",
52
+ }
53
+
54
+ if isinstance(obs, qml.ops.Prod):
55
+ # Recursively build a tuple of operator names
56
+ return tuple(name_map[o.name] for o in obs.operands)
57
+
58
+ # For single operators, return a single-element tuple
59
+ return (name_map[obs.name],)
60
+
61
+
62
+ @lru_cache(maxsize=512)
63
+ def _get_eigvals_from_key(key: tuple[str, ...]) -> np.ndarray:
64
+ """Computes and caches eigenvalues based on a structural key.
65
+
66
+ This function takes a key generated by `_get_structural_key` and computes
67
+ the eigenvalues of the corresponding tensor product of operators. The results
68
+ are memoized using @lru_cache to avoid redundant calculations.
69
+
70
+ Args:
71
+ key: A tuple of strings representing the observable's structure.
72
+
73
+ Returns:
74
+ A NumPy array containing the eigenvalues of the observable.
75
+ """
76
+
77
+ # Define a mapping from name to the base eigenvalue array
78
+ eigvals_map = {
79
+ "PauliZ": np.array([1, -1], dtype=np.int8),
80
+ "Identity": np.array([1, 1], dtype=np.int8),
81
+ }
82
+
83
+ # Start with the eigenvalues of the first operator in the key
84
+ final_eigvals = eigvals_map[key[0]]
85
+
86
+ # Iteratively compute the kronecker product for the rest
87
+ for op_name in key[1:]:
88
+ final_eigvals = np.kron(final_eigvals, eigvals_map[op_name])
89
+
90
+ return final_eigvals
91
+
92
+
93
+ def _batched_expectation(shots_dicts, observables, wire_order):
94
+ """Efficiently calculates expectation values for multiple observables across multiple shot histograms.
95
+
96
+ This function is optimized to compute expectation values in a fully vectorized
97
+ manner, minimizing Python loops. It operates in four main steps:
98
+ 1. Aggregates all unique bitstrings measured across all histograms.
99
+ 2. Builds a "reduced" eigenvalue matrix corresponding only to the unique states.
100
+ 3. Builds a "reduced" probability matrix from the shot counts for each histogram.
101
+ 4. Computes all expectation values with a single matrix multiplication.
102
+
103
+ Args:
104
+ shots_dicts (list[dict[str, int]]): A list of shot dictionaries (histograms),
105
+ where each dictionary maps a measured bitstring to its count.
106
+ observables (list[qml.operation.Operation]): A list of PennyLane observables
107
+ for which to calculate expectation values.
108
+ wire_order (tuple[int, ...]): A tuple defining the order of wires, which maps
109
+ the bitstring to the qubits. Note: This is typically the reverse of the
110
+ qubit indices (e.g., (2, 1, 0) for a 3-qubit system).
111
+
112
+ Returns:
113
+ np.ndarray: A 2D NumPy array of shape (n_observables, n_shots) where
114
+ result[i, j] is the expectation value of observables[i] for the
115
+ histogram in shots_dicts[j].
116
+ """
117
+
118
+ n_histograms = len(shots_dicts)
119
+ n_total_wires = len(wire_order)
120
+ n_observables = len(observables)
121
+
122
+ # --- 1. Aggregate all unique measured states across all shots ---
123
+ all_measured_bitstrings = set()
124
+ for sd in shots_dicts:
125
+ all_measured_bitstrings.update(sd.keys())
126
+
127
+ unique_bitstrings = sorted(list(all_measured_bitstrings))
128
+ n_unique_states = len(unique_bitstrings)
129
+
130
+ bitstring_to_idx_map = {bs: i for i, bs in enumerate(unique_bitstrings)}
131
+
132
+ # --- 2. Build REDUCED Eigenvalue Matrix: (n_observables, n_unique_states) ---
133
+ unique_states_int = np.array(
134
+ [int(bs, 2) for bs in unique_bitstrings], dtype=np.uint64
135
+ )
136
+ reduced_eigvals_matrix = np.zeros((n_observables, n_unique_states))
137
+ wire_map = {w: i for i, w in enumerate(wire_order)}
138
+
139
+ powers_cache = {}
140
+
141
+ for obs_idx, observable in enumerate(observables):
142
+ obs_wires = observable.wires
143
+ n_obs_wires = len(obs_wires)
144
+
145
+ if n_obs_wires in powers_cache:
146
+ powers = powers_cache[n_obs_wires]
147
+ else:
148
+ powers = 2 ** np.arange(n_obs_wires - 1, -1, -1, dtype=np.intp)
149
+ powers_cache[n_obs_wires] = powers
150
+
151
+ obs_wire_indices = np.array([wire_map[w] for w in obs_wires], dtype=np.uint32)
152
+ eigvals = _get_eigvals_from_key(_get_structural_key(observable))
153
+
154
+ # Vectorized mapping, but on the *reduced* set of states
155
+ shifts = n_total_wires - 1 - obs_wire_indices
156
+ bits = ((unique_states_int[:, np.newaxis] >> shifts) & 1).astype(np.intp)
157
+ # powers = 2 ** np.arange(n_obs_wires - 1, -1, -1)
158
+
159
+ # obs_state_indices = (bits * powers).sum(axis=1).astype(np.intp)
160
+ obs_state_indices = np.dot(bits, powers)
161
+
162
+ reduced_eigvals_matrix[obs_idx, :] = eigvals[obs_state_indices]
163
+
164
+ # --- 3. Build REDUCED Probability Matrix: (n_shots, n_unique_states) ---
165
+ reduced_prob_matrix = np.zeros((n_histograms, n_unique_states), dtype=np.float32)
166
+ for i, shots_dict in enumerate(shots_dicts):
167
+ total = sum(shots_dict.values())
168
+
169
+ for bitstring, count in shots_dict.items():
170
+ col_idx = bitstring_to_idx_map[bitstring]
171
+ reduced_prob_matrix[i, col_idx] = count / total
172
+
173
+ # --- 4. Compute Final Expectation Values ---
174
+ # (n_shots, n_unique_states) @ (n_unique_states, n_observables)
175
+ result = reduced_prob_matrix @ reduced_eigvals_matrix.T
176
+
177
+ # Transpose to (n_observables, n_shots) as expected by the calling code
178
+ return result.T
179
+
180
+
27
181
  def _compute_parameter_shift_mask(n_params):
28
182
  """
29
183
  Generate a binary matrix mask for the parameter shift rule.
@@ -58,7 +212,6 @@ class QuantumProgram(ABC):
58
212
  backend: CircuitRunner,
59
213
  seed: int | None = None,
60
214
  progress_queue: Queue | None = None,
61
- has_final_computation: bool = False,
62
215
  **kwargs,
63
216
  ):
64
217
  """
@@ -79,30 +232,22 @@ class QuantumProgram(ABC):
79
232
  be used for the parameter initialization.
80
233
  Defaults to None.
81
234
  progress_queue (Queue): a queue for progress bar updates.
82
- has_final_computation (bool): Whether the program includes a final
83
- computation step after optimization. This affects progress reporting.
84
235
 
85
236
  **kwargs: Additional keyword arguments that influence behaviour.
86
237
  - grouping_strategy (Literal["default", "wires", "qwc"]): A strategy for grouping operations, used in Pennylane's transforms.
87
238
  Defaults to None.
88
239
  - qem_protocol (QEMProtocol, optional): the quantum error mitigation protocol to apply.
89
240
  Must be of type QEMProtocol. Defaults to None.
90
-
91
- The following key values are reserved for internal use and should not be set by the user:
92
- - losses (list, optional): A list to initialize the `losses` attribute. Defaults to an empty list.
93
- - final_params (list, optional): A list to initialize the `final_params` attribute. Defaults to an empty list.
94
-
95
241
  """
96
242
 
97
- # Shared Variables
98
- self.losses = kwargs.pop("losses", [])
99
- self.final_params = kwargs.pop("final_params", [])
243
+ self._losses = []
244
+ self._final_params = []
100
245
 
101
- self.circuits: list[Circuit] = []
246
+ self._circuits: list[Circuit] = []
102
247
 
103
248
  self._total_circuit_count = 0
104
249
  self._total_run_time = 0.0
105
- self._curr_params = []
250
+ self._curr_params = None
106
251
 
107
252
  self._seed = seed
108
253
  self._rng = np.random.default_rng(self._seed)
@@ -116,9 +261,7 @@ class QuantumProgram(ABC):
116
261
  self.job_id = kwargs.get("job_id", None)
117
262
  self._progress_queue = progress_queue
118
263
  if progress_queue and self.job_id:
119
- self.reporter = QueueProgressReporter(
120
- self.job_id, progress_queue, has_final_computation=has_final_computation
121
- )
264
+ self.reporter = QueueProgressReporter(self.job_id, progress_queue)
122
265
  else:
123
266
  self.reporter = LoggingProgressReporter()
124
267
 
@@ -127,6 +270,8 @@ class QuantumProgram(ABC):
127
270
 
128
271
  self._qem_protocol = kwargs.pop("qem_protocol", None) or _NoMitigation()
129
272
 
273
+ self._cancellation_event = None
274
+
130
275
  self._meta_circuit_factory = partial(
131
276
  MetaCircuit,
132
277
  grouping_strategy=self._grouping_strategy,
@@ -135,16 +280,113 @@ class QuantumProgram(ABC):
135
280
 
136
281
  @property
137
282
  def total_circuit_count(self):
283
+ """
284
+ Get the total number of circuits executed so far.
285
+
286
+ Returns:
287
+ int: Cumulative count of circuits submitted for execution.
288
+ """
138
289
  return self._total_circuit_count
139
290
 
140
291
  @property
141
292
  def total_run_time(self):
293
+ """
294
+ Get the total runtime across all circuit executions.
295
+
296
+ Returns:
297
+ float: Cumulative execution time in seconds.
298
+ """
142
299
  return self._total_run_time
143
300
 
144
301
  @property
145
302
  def meta_circuits(self):
303
+ """
304
+ Get the meta-circuit templates used by this program.
305
+
306
+ Returns:
307
+ dict[str, MetaCircuit]: Dictionary mapping circuit names to their
308
+ MetaCircuit templates.
309
+ """
146
310
  return self._meta_circuits
147
311
 
312
+ @property
313
+ def n_params(self):
314
+ """
315
+ Get the total number of parameters in the quantum circuit.
316
+
317
+ Returns:
318
+ int: Total number of trainable parameters (n_layers * n_params_per_layer).
319
+ """
320
+ return self._n_params
321
+
322
+ @property
323
+ def circuits(self) -> list[Circuit]:
324
+ """
325
+ Get a copy of the generated circuits list.
326
+
327
+ Returns:
328
+ list[Circuit]: Copy of the circuits list. Modifications to this list
329
+ will not affect the internal state.
330
+ """
331
+ return self._circuits.copy()
332
+
333
+ @property
334
+ def losses(self) -> list[dict]:
335
+ """
336
+ Get a copy of the optimization loss history.
337
+
338
+ Each entry is a dictionary mapping parameter indices to loss values.
339
+
340
+ Returns:
341
+ list[dict]: Copy of the loss history. Modifications to this list
342
+ will not affect the internal state.
343
+ """
344
+ return self._losses.copy()
345
+
346
+ @property
347
+ def final_params(self) -> list:
348
+ """
349
+ Get a copy of the final optimized parameters.
350
+
351
+ Returns:
352
+ list: Copy of the final parameters. Modifications to this list
353
+ will not affect the internal state.
354
+ """
355
+ return self._final_params.copy()
356
+
357
+ @property
358
+ def initial_params(self) -> np.ndarray:
359
+ """
360
+ Get the current initial parameters.
361
+
362
+ Returns:
363
+ np.ndarray: Current initial parameters. If not yet initialized,
364
+ they will be generated automatically.
365
+ """
366
+ if self._curr_params is None:
367
+ self._initialize_params()
368
+ return self._curr_params.copy()
369
+
370
+ @initial_params.setter
371
+ def initial_params(self, value: np.ndarray | None):
372
+ """
373
+ Set initial parameters.
374
+
375
+ Args:
376
+ value (np.ndarray | None): Initial parameters with shape
377
+ (n_param_sets, n_layers * n_params), or None to reset
378
+ to uninitialized state.
379
+
380
+ Raises:
381
+ ValueError: If parameters have incorrect shape.
382
+ """
383
+ if value is not None:
384
+ self._validate_initial_params(value)
385
+ self._curr_params = value.copy()
386
+ else:
387
+ # Reset to uninitialized state
388
+ self._curr_params = None
389
+
148
390
  @abstractmethod
149
391
  def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
150
392
  pass
@@ -153,16 +395,60 @@ class QuantumProgram(ABC):
153
395
  def _generate_circuits(self, **kwargs):
154
396
  pass
155
397
 
398
+ def _set_cancellation_event(self, event: Event):
399
+ """
400
+ Set a cancellation event for graceful program termination.
401
+
402
+ This internal method is called by a batch runner to provide a mechanism
403
+ for stopping the optimization loop cleanly when requested.
404
+
405
+ Args:
406
+ event (Event): Threading Event object that signals cancellation when set.
407
+ """
408
+ self._cancellation_event = event
409
+
410
+ def get_expected_param_shape(self) -> tuple[int, int]:
411
+ """
412
+ Get the expected shape for initial parameters.
413
+
414
+ Returns:
415
+ tuple[int, int]: Shape (n_param_sets, n_layers * n_params) that
416
+ initial parameters should have for this quantum program.
417
+ """
418
+ return (self.optimizer.n_param_sets, self.n_layers * self.n_params)
419
+
420
+ def _validate_initial_params(self, params: np.ndarray):
421
+ """
422
+ Validate user-provided initial parameters.
423
+
424
+ Args:
425
+ params (np.ndarray): Parameters to validate.
426
+
427
+ Raises:
428
+ ValueError: If parameters have incorrect shape.
429
+ """
430
+ expected_shape = self.get_expected_param_shape()
431
+
432
+ if params.shape != expected_shape:
433
+ raise ValueError(
434
+ f"Initial parameters must have shape {expected_shape}, "
435
+ f"got {params.shape}"
436
+ )
437
+
156
438
  def _initialize_params(self):
157
- self._curr_params = np.array(
158
- [
159
- self._rng.uniform(0, 2 * np.pi, self.n_layers * self.n_params)
160
- for _ in range(self.optimizer.n_param_sets)
161
- ]
439
+ """
440
+ Initialize the circuit parameters randomly.
441
+
442
+ Generates random parameters with values uniformly distributed between
443
+ 0 and 2π. The number of parameter sets depends on the optimizer being used.
444
+ """
445
+ total_params = self.n_layers * self.n_params
446
+ self._curr_params = self._rng.uniform(
447
+ 0, 2 * np.pi, (self.optimizer.n_param_sets, total_params)
162
448
  )
163
449
 
164
450
  def _run_optimization_circuits(self, store_data, data_file):
165
- self.circuits[:] = []
451
+ self._circuits[:] = []
166
452
 
167
453
  self._generate_circuits()
168
454
 
@@ -175,7 +461,7 @@ class QuantumProgram(ABC):
175
461
  def _prepare_and_send_circuits(self):
176
462
  job_circuits = {}
177
463
 
178
- for circuit in self.circuits:
464
+ for circuit in self._circuits:
179
465
  for tag, qasm_circuit in zip(circuit.tags, circuit.qasm_circuits):
180
466
  job_circuits[tag] = qasm_circuit
181
467
 
@@ -259,6 +545,10 @@ class QuantumProgram(ABC):
259
545
  (dict) The energies for each parameter set grouping, where the dict keys
260
546
  correspond to the parameter indices.
261
547
  """
548
+ if not (self._cancellation_event and self._cancellation_event.is_set()):
549
+ self.reporter.info(
550
+ message="Post-processing output", iteration=self.current_iteration
551
+ )
262
552
 
263
553
  losses = {}
264
554
  measurement_groups = self._meta_circuits["cost_circuit"].measurement_groups
@@ -282,20 +572,24 @@ class QuantumProgram(ABC):
282
572
  for shots_dicts, curr_measurement_group in zip(
283
573
  shots_by_qem_idx, measurement_groups
284
574
  ):
285
- curr_marginal_results = []
286
- for observable in curr_measurement_group:
287
- intermediate_exp_values = [
288
- ExpectationMP(observable).process_counts(
289
- shots_dict,
290
- tuple(reversed(range(len(next(iter(shots_dict.keys())))))),
291
- )
292
- for shots_dict in shots_dicts
293
- ]
575
+ if hasattr(self, "cost_hamiltonian"):
576
+ wire_order = tuple(reversed(self.cost_hamiltonian.wires))
577
+ else:
578
+ wire_order = tuple(
579
+ reversed(range(len(next(iter(shots_dicts[0].keys())))))
580
+ )
294
581
 
582
+ expectation_matrix = _batched_expectation(
583
+ shots_dicts, curr_measurement_group, wire_order
584
+ )
585
+
586
+ # expectation_matrix[i, j] = expectation value for observable i, histogram j
587
+ curr_marginal_results = []
588
+ for obs_idx in range(len(curr_measurement_group)):
589
+ intermediate_exp_values = expectation_matrix[obs_idx, :]
295
590
  mitigated_exp_value = self._qem_protocol.postprocess_results(
296
591
  intermediate_exp_values
297
592
  )
298
-
299
593
  curr_marginal_results.append(mitigated_exp_value)
300
594
 
301
595
  marginal_results.append(
@@ -314,6 +608,20 @@ class QuantumProgram(ABC):
314
608
 
315
609
  return losses
316
610
 
611
+ def _perform_final_computation(self):
612
+ """
613
+ Perform final computations after optimization completes.
614
+
615
+ This is an optional hook method that subclasses can override to perform
616
+ any post-optimization processing, such as extracting solutions, running
617
+ final measurements, or computing additional metrics.
618
+
619
+ Note:
620
+ The default implementation does nothing. Subclasses should override
621
+ this method if they need post-optimization processing.
622
+ """
623
+ pass
624
+
317
625
  def run(self, store_data=False, data_file=None):
318
626
  """
319
627
  Run the QAOA problem. The outputs are stored in the QAOA object. Optionally, the data can be stored in a file.
@@ -364,7 +672,8 @@ class QuantumProgram(ABC):
364
672
  return grads
365
673
 
366
674
  def _iteration_counter(intermediate_result: OptimizeResult):
367
- self.losses.append(
675
+
676
+ self._losses.append(
368
677
  dict(
369
678
  zip(
370
679
  range(len(intermediate_result.x)),
@@ -373,12 +682,13 @@ class QuantumProgram(ABC):
373
682
  )
374
683
  )
375
684
 
376
- self.final_params[:] = np.atleast_2d(intermediate_result.x)
377
-
378
685
  self.current_iteration += 1
379
686
 
380
687
  self.reporter.update(iteration=self.current_iteration)
381
688
 
689
+ if self._cancellation_event and self._cancellation_event.is_set():
690
+ raise _CancelledError("Cancellation requested by batch.")
691
+
382
692
  if (
383
693
  isinstance(self.optimizer, ScipyOptimizer)
384
694
  and self.optimizer.method == ScipyMethod.COBYLA
@@ -389,26 +699,42 @@ class QuantumProgram(ABC):
389
699
  self.reporter.info(message="Finished Setup")
390
700
 
391
701
  self._initialize_params()
392
- self._minimize_res = self.optimizer.optimize(
393
- cost_fn=cost_fn,
394
- initial_params=self._curr_params,
395
- callback_fn=_iteration_counter,
396
- jac=grad_fn,
397
- maxiter=self.max_iterations,
398
- rng=self._rng,
399
- )
400
- self.final_params[:] = np.atleast_2d(self._minimize_res.x)
401
702
 
402
- self.reporter.info(message="Finished Optimization!")
703
+ try:
704
+ self._minimize_res = self.optimizer.optimize(
705
+ cost_fn=cost_fn,
706
+ initial_params=self._curr_params,
707
+ callback_fn=_iteration_counter,
708
+ jac=grad_fn,
709
+ maxiter=self.max_iterations,
710
+ rng=self._rng,
711
+ )
712
+ except _CancelledError:
713
+ # The optimizer was stopped by our callback. This is not a real
714
+ # error, just a signal to exit this task cleanly.
715
+ return self._total_circuit_count, self._total_run_time
716
+
717
+ self._final_params[:] = np.atleast_2d(self._minimize_res.x)
718
+
719
+ self._perform_final_computation()
720
+
721
+ self.reporter.info(message="Finished successfully!")
403
722
 
404
723
  return self._total_circuit_count, self._total_run_time
405
724
 
406
725
  def save_iteration(self, data_file):
407
726
  """
408
- Save the current iteration of the program to a file.
727
+ Save the current state of the quantum program to a file.
728
+
729
+ Serializes the entire QuantumProgram instance including parameters,
730
+ losses, and circuit history using pickle.
409
731
 
410
732
  Args:
411
- data_file (str): The file to save the iteration to.
733
+ data_file (str): Path to the file where the program state will be saved.
734
+
735
+ Note:
736
+ The file is written in binary mode and can be restored using
737
+ `import_iteration()`.
412
738
  """
413
739
 
414
740
  with open(data_file, "wb") as f:
@@ -417,10 +743,16 @@ class QuantumProgram(ABC):
417
743
  @staticmethod
418
744
  def import_iteration(data_file):
419
745
  """
420
- Import an iteration of the program from a file.
746
+ Load a previously saved quantum program state from a file.
747
+
748
+ Deserializes a QuantumProgram instance that was saved using `save_iteration()`.
421
749
 
422
750
  Args:
423
- data_file (str): The file to import the iteration from.
751
+ data_file (str): Path to the file containing the saved program state.
752
+
753
+ Returns:
754
+ QuantumProgram: The restored QuantumProgram instance with all its state,
755
+ including parameters, losses, and circuit history.
424
756
  """
425
757
 
426
758
  with open(data_file, "rb") as f:
@@ -0,0 +1,10 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from ._graph_partitioning import (
6
+ GraphPartitioningQAOA,
7
+ PartitioningConfig,
8
+ )
9
+ from ._qubo_partitioning import QUBOPartitioningQAOA
10
+ from ._vqe_sweep import MoleculeTransformer, VQEHyperparameterSweep
@@ -20,9 +20,9 @@ import scipy.sparse.linalg as spla
20
20
  from pymetis import part_graph
21
21
  from sklearn.cluster import SpectralClustering
22
22
 
23
- from divi.interfaces import CircuitRunner
23
+ from divi.backends import CircuitRunner
24
24
  from divi.qprog import QAOA, ProgramBatch, QuantumProgram
25
- from divi.qprog._qaoa import (
25
+ from divi.qprog.algorithms._qaoa import (
26
26
  _SUPPORTED_INITIAL_STATES_LITERAL,
27
27
  GraphProblem,
28
28
  GraphProblemTypes,
@@ -387,14 +387,6 @@ def dominance_aggregation(
387
387
  return curr_solution
388
388
 
389
389
 
390
- def _run_and_compute_solution(program: QuantumProgram):
391
- program.run()
392
-
393
- final_sol_circuit_count, final_sol_run_time = program.compute_final_solution()
394
-
395
- return final_sol_circuit_count, final_sol_run_time
396
-
397
-
398
390
  class GraphPartitioningQAOA(ProgramBatch):
399
391
  def __init__(
400
392
  self,
@@ -443,8 +435,6 @@ class GraphPartitioningQAOA(ProgramBatch):
443
435
  self.solution = None
444
436
  self.aggregate_fn = aggregate_fn
445
437
 
446
- self._task_fn = _run_and_compute_solution
447
-
448
438
  self._constructor = partial(
449
439
  QAOA,
450
440
  initial_state=initial_state,
@@ -499,33 +489,12 @@ class GraphPartitioningQAOA(ProgramBatch):
499
489
  self.reverse_index_maps[prog_id] = {v: k for k, v in index_map.items()}
500
490
 
501
491
  _subgraph = nx.relabel_nodes(subgraph, index_map)
502
- self.programs[prog_id] = self._constructor(
492
+ self._programs[prog_id] = self._constructor(
503
493
  job_id=prog_id,
504
494
  problem=_subgraph,
505
- losses=self._manager.list(),
506
- probs=self._manager.dict(),
507
- final_params=self._manager.list(),
508
- solution_nodes=self._manager.list(),
509
495
  progress_queue=self._queue,
510
496
  )
511
497
 
512
- def compute_final_solutions(self):
513
- if self._executor is not None:
514
- self.join()
515
-
516
- if self._executor is not None:
517
- raise RuntimeError("A batch is already being run.")
518
-
519
- if len(self.programs) == 0:
520
- raise RuntimeError("No programs to run.")
521
-
522
- self._executor = ProcessPoolExecutor()
523
-
524
- self.futures = [
525
- self._executor.submit(program.compute_final_solution)
526
- for program in self.programs.values()
527
- ]
528
-
529
498
  def aggregate_results(self):
530
499
  """
531
500
  Aggregates the results from all QAOA subprograms to form a global solution.