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/qprog/quantum_program.py
CHANGED
|
@@ -5,23 +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
|
-
|
|
14
|
+
import pennylane as qml
|
|
14
15
|
from scipy.optimize import OptimizeResult
|
|
15
16
|
|
|
16
17
|
from divi.backends import CircuitRunner, JobStatus, QoroService
|
|
17
18
|
from divi.circuits import Circuit, MetaCircuit
|
|
18
19
|
from divi.circuits.qem import _NoMitigation
|
|
20
|
+
from divi.qprog.exceptions import _CancelledError
|
|
19
21
|
from divi.qprog.optimizers import ScipyMethod, ScipyOptimizer
|
|
20
22
|
from divi.reporting import LoggingProgressReporter, QueueProgressReporter
|
|
21
23
|
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
23
25
|
|
|
24
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
|
+
|
|
25
181
|
def _compute_parameter_shift_mask(n_params):
|
|
26
182
|
"""
|
|
27
183
|
Generate a binary matrix mask for the parameter shift rule.
|
|
@@ -56,7 +212,6 @@ class QuantumProgram(ABC):
|
|
|
56
212
|
backend: CircuitRunner,
|
|
57
213
|
seed: int | None = None,
|
|
58
214
|
progress_queue: Queue | None = None,
|
|
59
|
-
has_final_computation: bool = False,
|
|
60
215
|
**kwargs,
|
|
61
216
|
):
|
|
62
217
|
"""
|
|
@@ -77,30 +232,22 @@ class QuantumProgram(ABC):
|
|
|
77
232
|
be used for the parameter initialization.
|
|
78
233
|
Defaults to None.
|
|
79
234
|
progress_queue (Queue): a queue for progress bar updates.
|
|
80
|
-
has_final_computation (bool): Whether the program includes a final
|
|
81
|
-
computation step after optimization. This affects progress reporting.
|
|
82
235
|
|
|
83
236
|
**kwargs: Additional keyword arguments that influence behaviour.
|
|
84
237
|
- grouping_strategy (Literal["default", "wires", "qwc"]): A strategy for grouping operations, used in Pennylane's transforms.
|
|
85
238
|
Defaults to None.
|
|
86
239
|
- qem_protocol (QEMProtocol, optional): the quantum error mitigation protocol to apply.
|
|
87
240
|
Must be of type QEMProtocol. Defaults to None.
|
|
88
|
-
|
|
89
|
-
The following key values are reserved for internal use and should not be set by the user:
|
|
90
|
-
- losses (list, optional): A list to initialize the `losses` attribute. Defaults to an empty list.
|
|
91
|
-
- final_params (list, optional): A list to initialize the `final_params` attribute. Defaults to an empty list.
|
|
92
|
-
|
|
93
241
|
"""
|
|
94
242
|
|
|
95
|
-
|
|
96
|
-
self.
|
|
97
|
-
self.final_params = kwargs.pop("final_params", [])
|
|
243
|
+
self._losses = []
|
|
244
|
+
self._final_params = []
|
|
98
245
|
|
|
99
|
-
self.
|
|
246
|
+
self._circuits: list[Circuit] = []
|
|
100
247
|
|
|
101
248
|
self._total_circuit_count = 0
|
|
102
249
|
self._total_run_time = 0.0
|
|
103
|
-
self._curr_params =
|
|
250
|
+
self._curr_params = None
|
|
104
251
|
|
|
105
252
|
self._seed = seed
|
|
106
253
|
self._rng = np.random.default_rng(self._seed)
|
|
@@ -114,9 +261,7 @@ class QuantumProgram(ABC):
|
|
|
114
261
|
self.job_id = kwargs.get("job_id", None)
|
|
115
262
|
self._progress_queue = progress_queue
|
|
116
263
|
if progress_queue and self.job_id:
|
|
117
|
-
self.reporter = QueueProgressReporter(
|
|
118
|
-
self.job_id, progress_queue, has_final_computation=has_final_computation
|
|
119
|
-
)
|
|
264
|
+
self.reporter = QueueProgressReporter(self.job_id, progress_queue)
|
|
120
265
|
else:
|
|
121
266
|
self.reporter = LoggingProgressReporter()
|
|
122
267
|
|
|
@@ -125,6 +270,8 @@ class QuantumProgram(ABC):
|
|
|
125
270
|
|
|
126
271
|
self._qem_protocol = kwargs.pop("qem_protocol", None) or _NoMitigation()
|
|
127
272
|
|
|
273
|
+
self._cancellation_event = None
|
|
274
|
+
|
|
128
275
|
self._meta_circuit_factory = partial(
|
|
129
276
|
MetaCircuit,
|
|
130
277
|
grouping_strategy=self._grouping_strategy,
|
|
@@ -133,20 +280,113 @@ class QuantumProgram(ABC):
|
|
|
133
280
|
|
|
134
281
|
@property
|
|
135
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
|
+
"""
|
|
136
289
|
return self._total_circuit_count
|
|
137
290
|
|
|
138
291
|
@property
|
|
139
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
|
+
"""
|
|
140
299
|
return self._total_run_time
|
|
141
300
|
|
|
142
301
|
@property
|
|
143
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
|
+
"""
|
|
144
310
|
return self._meta_circuits
|
|
145
311
|
|
|
146
312
|
@property
|
|
147
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
|
+
"""
|
|
148
320
|
return self._n_params
|
|
149
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
|
+
|
|
150
390
|
@abstractmethod
|
|
151
391
|
def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
|
|
152
392
|
pass
|
|
@@ -155,16 +395,60 @@ class QuantumProgram(ABC):
|
|
|
155
395
|
def _generate_circuits(self, **kwargs):
|
|
156
396
|
pass
|
|
157
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
|
+
|
|
158
438
|
def _initialize_params(self):
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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)
|
|
164
448
|
)
|
|
165
449
|
|
|
166
450
|
def _run_optimization_circuits(self, store_data, data_file):
|
|
167
|
-
self.
|
|
451
|
+
self._circuits[:] = []
|
|
168
452
|
|
|
169
453
|
self._generate_circuits()
|
|
170
454
|
|
|
@@ -177,7 +461,7 @@ class QuantumProgram(ABC):
|
|
|
177
461
|
def _prepare_and_send_circuits(self):
|
|
178
462
|
job_circuits = {}
|
|
179
463
|
|
|
180
|
-
for circuit in self.
|
|
464
|
+
for circuit in self._circuits:
|
|
181
465
|
for tag, qasm_circuit in zip(circuit.tags, circuit.qasm_circuits):
|
|
182
466
|
job_circuits[tag] = qasm_circuit
|
|
183
467
|
|
|
@@ -261,6 +545,10 @@ class QuantumProgram(ABC):
|
|
|
261
545
|
(dict) The energies for each parameter set grouping, where the dict keys
|
|
262
546
|
correspond to the parameter indices.
|
|
263
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
|
+
)
|
|
264
552
|
|
|
265
553
|
losses = {}
|
|
266
554
|
measurement_groups = self._meta_circuits["cost_circuit"].measurement_groups
|
|
@@ -291,18 +579,17 @@ class QuantumProgram(ABC):
|
|
|
291
579
|
reversed(range(len(next(iter(shots_dicts[0].keys())))))
|
|
292
580
|
)
|
|
293
581
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
intermediate_exp_values = [
|
|
298
|
-
ExpectationMP(observable).process_counts(shots_dict, wire_order)
|
|
299
|
-
for shots_dict in shots_dicts
|
|
300
|
-
]
|
|
582
|
+
expectation_matrix = _batched_expectation(
|
|
583
|
+
shots_dicts, curr_measurement_group, wire_order
|
|
584
|
+
)
|
|
301
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, :]
|
|
302
590
|
mitigated_exp_value = self._qem_protocol.postprocess_results(
|
|
303
591
|
intermediate_exp_values
|
|
304
592
|
)
|
|
305
|
-
|
|
306
593
|
curr_marginal_results.append(mitigated_exp_value)
|
|
307
594
|
|
|
308
595
|
marginal_results.append(
|
|
@@ -321,6 +608,20 @@ class QuantumProgram(ABC):
|
|
|
321
608
|
|
|
322
609
|
return losses
|
|
323
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
|
+
|
|
324
625
|
def run(self, store_data=False, data_file=None):
|
|
325
626
|
"""
|
|
326
627
|
Run the QAOA problem. The outputs are stored in the QAOA object. Optionally, the data can be stored in a file.
|
|
@@ -371,7 +672,8 @@ class QuantumProgram(ABC):
|
|
|
371
672
|
return grads
|
|
372
673
|
|
|
373
674
|
def _iteration_counter(intermediate_result: OptimizeResult):
|
|
374
|
-
|
|
675
|
+
|
|
676
|
+
self._losses.append(
|
|
375
677
|
dict(
|
|
376
678
|
zip(
|
|
377
679
|
range(len(intermediate_result.x)),
|
|
@@ -380,12 +682,13 @@ class QuantumProgram(ABC):
|
|
|
380
682
|
)
|
|
381
683
|
)
|
|
382
684
|
|
|
383
|
-
self.final_params[:] = np.atleast_2d(intermediate_result.x)
|
|
384
|
-
|
|
385
685
|
self.current_iteration += 1
|
|
386
686
|
|
|
387
687
|
self.reporter.update(iteration=self.current_iteration)
|
|
388
688
|
|
|
689
|
+
if self._cancellation_event and self._cancellation_event.is_set():
|
|
690
|
+
raise _CancelledError("Cancellation requested by batch.")
|
|
691
|
+
|
|
389
692
|
if (
|
|
390
693
|
isinstance(self.optimizer, ScipyOptimizer)
|
|
391
694
|
and self.optimizer.method == ScipyMethod.COBYLA
|
|
@@ -396,26 +699,42 @@ class QuantumProgram(ABC):
|
|
|
396
699
|
self.reporter.info(message="Finished Setup")
|
|
397
700
|
|
|
398
701
|
self._initialize_params()
|
|
399
|
-
self._minimize_res = self.optimizer.optimize(
|
|
400
|
-
cost_fn=cost_fn,
|
|
401
|
-
initial_params=self._curr_params,
|
|
402
|
-
callback_fn=_iteration_counter,
|
|
403
|
-
jac=grad_fn,
|
|
404
|
-
maxiter=self.max_iterations,
|
|
405
|
-
rng=self._rng,
|
|
406
|
-
)
|
|
407
|
-
self.final_params[:] = np.atleast_2d(self._minimize_res.x)
|
|
408
702
|
|
|
409
|
-
|
|
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!")
|
|
410
722
|
|
|
411
723
|
return self._total_circuit_count, self._total_run_time
|
|
412
724
|
|
|
413
725
|
def save_iteration(self, data_file):
|
|
414
726
|
"""
|
|
415
|
-
Save the current
|
|
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.
|
|
416
731
|
|
|
417
732
|
Args:
|
|
418
|
-
data_file (str):
|
|
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()`.
|
|
419
738
|
"""
|
|
420
739
|
|
|
421
740
|
with open(data_file, "wb") as f:
|
|
@@ -424,10 +743,16 @@ class QuantumProgram(ABC):
|
|
|
424
743
|
@staticmethod
|
|
425
744
|
def import_iteration(data_file):
|
|
426
745
|
"""
|
|
427
|
-
|
|
746
|
+
Load a previously saved quantum program state from a file.
|
|
747
|
+
|
|
748
|
+
Deserializes a QuantumProgram instance that was saved using `save_iteration()`.
|
|
428
749
|
|
|
429
750
|
Args:
|
|
430
|
-
data_file (str):
|
|
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.
|
|
431
756
|
"""
|
|
432
757
|
|
|
433
758
|
with open(data_file, "rb") as f:
|
|
@@ -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.
|
|
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.
|