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.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +7 -0
- divi/backends/_circuit_runner.py +46 -0
- divi/{parallel_simulator.py → backends/_parallel_simulator.py} +136 -53
- divi/backends/_qoro_service.py +531 -0
- divi/circuits/__init__.py +5 -0
- divi/circuits/_core.py +226 -0
- divi/{qasm.py → circuits/qasm.py} +21 -2
- divi/{exp → extern}/cirq/_validator.py +9 -7
- divi/qprog/__init__.py +18 -5
- divi/qprog/algorithms/__init__.py +14 -0
- divi/qprog/algorithms/_ansatze.py +311 -0
- divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +69 -41
- divi/qprog/{_vqe.py → algorithms/_vqe.py} +79 -135
- divi/qprog/batch.py +239 -55
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +219 -18
- divi/qprog/quantum_program.py +389 -57
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +3 -34
- divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +42 -25
- divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +59 -26
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +112 -0
- divi/{qlogger.py → reporting/_qlogger.py} +37 -2
- divi/{reporter.py → reporting/_reporter.py} +8 -14
- divi/utils.py +49 -10
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/METADATA +2 -1
- qoro_divi-0.3.5.dist-info/RECORD +69 -0
- divi/_pbar.py +0 -70
- divi/circuits.py +0 -139
- divi/interfaces.py +0 -25
- divi/qoro_service.py +0 -425
- qoro_divi-0.3.3.dist-info/RECORD +0 -62
- /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
- /divi/{qem.py → circuits/qem.py} +0 -0
- /divi/{exp → extern}/cirq/__init__.py +0 -0
- /divi/{exp → extern}/cirq/_lexer.py +0 -0
- /divi/{exp → extern}/cirq/_parser.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
- /divi/{exp → extern}/cirq/exception.py +0 -0
- /divi/{exp → extern}/scipy/_cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
- /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/WHEEL +0 -0
divi/qprog/quantum_program.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
19
|
-
from divi.
|
|
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.
|
|
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
|
-
|
|
98
|
-
self.
|
|
99
|
-
self.final_params = kwargs.pop("final_params", [])
|
|
243
|
+
self._losses = []
|
|
244
|
+
self._final_params = []
|
|
100
245
|
|
|
101
|
-
self.
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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):
|
|
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
|
-
|
|
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):
|
|
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.
|
|
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.
|
|
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.
|