qoro-divi 0.2.0b1__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.
- divi/__init__.py +8 -0
- divi/_pbar.py +73 -0
- divi/circuits.py +139 -0
- divi/exp/cirq/__init__.py +7 -0
- divi/exp/cirq/_lexer.py +126 -0
- divi/exp/cirq/_parser.py +889 -0
- divi/exp/cirq/_qasm_export.py +37 -0
- divi/exp/cirq/_qasm_import.py +35 -0
- divi/exp/cirq/exception.py +21 -0
- divi/exp/scipy/_cobyla.py +342 -0
- divi/exp/scipy/pyprima/LICENCE.txt +28 -0
- divi/exp/scipy/pyprima/__init__.py +263 -0
- divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
- divi/exp/scipy/pyprima/cobyla/cobyla.py +599 -0
- divi/exp/scipy/pyprima/cobyla/cobylb.py +849 -0
- divi/exp/scipy/pyprima/cobyla/geometry.py +240 -0
- divi/exp/scipy/pyprima/cobyla/initialize.py +269 -0
- divi/exp/scipy/pyprima/cobyla/trustregion.py +540 -0
- divi/exp/scipy/pyprima/cobyla/update.py +331 -0
- divi/exp/scipy/pyprima/common/__init__.py +0 -0
- divi/exp/scipy/pyprima/common/_bounds.py +41 -0
- divi/exp/scipy/pyprima/common/_linear_constraints.py +46 -0
- divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +64 -0
- divi/exp/scipy/pyprima/common/_project.py +224 -0
- divi/exp/scipy/pyprima/common/checkbreak.py +107 -0
- divi/exp/scipy/pyprima/common/consts.py +48 -0
- divi/exp/scipy/pyprima/common/evaluate.py +101 -0
- divi/exp/scipy/pyprima/common/history.py +39 -0
- divi/exp/scipy/pyprima/common/infos.py +30 -0
- divi/exp/scipy/pyprima/common/linalg.py +452 -0
- divi/exp/scipy/pyprima/common/message.py +336 -0
- divi/exp/scipy/pyprima/common/powalg.py +131 -0
- divi/exp/scipy/pyprima/common/preproc.py +393 -0
- divi/exp/scipy/pyprima/common/present.py +5 -0
- divi/exp/scipy/pyprima/common/ratio.py +56 -0
- divi/exp/scipy/pyprima/common/redrho.py +49 -0
- divi/exp/scipy/pyprima/common/selectx.py +346 -0
- divi/interfaces.py +25 -0
- divi/parallel_simulator.py +258 -0
- divi/qasm.py +220 -0
- divi/qem.py +191 -0
- divi/qlogger.py +119 -0
- divi/qoro_service.py +343 -0
- divi/qprog/__init__.py +13 -0
- divi/qprog/_graph_partitioning.py +619 -0
- divi/qprog/_mlae.py +182 -0
- divi/qprog/_qaoa.py +440 -0
- divi/qprog/_vqe.py +275 -0
- divi/qprog/_vqe_sweep.py +144 -0
- divi/qprog/batch.py +235 -0
- divi/qprog/optimizers.py +75 -0
- divi/qprog/quantum_program.py +493 -0
- divi/utils.py +116 -0
- qoro_divi-0.2.0b1.dist-info/LICENSE +190 -0
- qoro_divi-0.2.0b1.dist-info/LICENSES/Apache-2.0.txt +73 -0
- qoro_divi-0.2.0b1.dist-info/METADATA +57 -0
- qoro_divi-0.2.0b1.dist-info/RECORD +58 -0
- qoro_divi-0.2.0b1.dist-info/WHEEL +4 -0
divi/qprog/_vqe.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pennylane as qml
|
|
9
|
+
import sympy as sp
|
|
10
|
+
|
|
11
|
+
from divi.circuits import MetaCircuit
|
|
12
|
+
from divi.qprog import QuantumProgram
|
|
13
|
+
from divi.qprog.optimizers import Optimizer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VQEAnsatz(Enum):
|
|
17
|
+
UCCSD = "UCCSD"
|
|
18
|
+
RY = "RY"
|
|
19
|
+
RYRZ = "RYRZ"
|
|
20
|
+
HW_EFFICIENT = "HW_EFFICIENT"
|
|
21
|
+
QAOA = "QAOA"
|
|
22
|
+
HARTREE_FOCK = "HF"
|
|
23
|
+
|
|
24
|
+
def describe(self):
|
|
25
|
+
return self.name, self.value
|
|
26
|
+
|
|
27
|
+
def n_params(self, n_qubits, **kwargs):
|
|
28
|
+
if self in (VQEAnsatz.UCCSD, VQEAnsatz.HARTREE_FOCK):
|
|
29
|
+
singles, doubles = qml.qchem.excitations(
|
|
30
|
+
kwargs.pop("n_electrons"), n_qubits
|
|
31
|
+
)
|
|
32
|
+
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
33
|
+
|
|
34
|
+
return len(s_wires) + len(d_wires)
|
|
35
|
+
elif self == VQEAnsatz.RY:
|
|
36
|
+
return n_qubits
|
|
37
|
+
elif self == VQEAnsatz.RYRZ:
|
|
38
|
+
return 2 * n_qubits
|
|
39
|
+
elif self == VQEAnsatz.HW_EFFICIENT:
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
elif self == VQEAnsatz.QAOA:
|
|
42
|
+
return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VQE(QuantumProgram):
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
symbols,
|
|
49
|
+
bond_length: float,
|
|
50
|
+
coordinate_structure: list[tuple[float, float, float]],
|
|
51
|
+
charge: float = 0,
|
|
52
|
+
n_layers: int = 1,
|
|
53
|
+
ansatz=VQEAnsatz.HARTREE_FOCK,
|
|
54
|
+
optimizer=Optimizer.MONTE_CARLO,
|
|
55
|
+
max_iterations=10,
|
|
56
|
+
**kwargs,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Initialize the VQE problem.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
symbols (list): The symbols of the atoms in the molecule
|
|
63
|
+
bond_length (float): The bond length to consider
|
|
64
|
+
coordinate_structure (list): The coordinate structure of the molecule, represented in unit lengths
|
|
65
|
+
charge (float): the charge of the molecule. Defaults to 0.
|
|
66
|
+
ansatz (VQEAnsatz): The ansatz to use for the VQE problem
|
|
67
|
+
optimizer (Optimizers): The optimizer to use.
|
|
68
|
+
max_iterations (int): Maximum number of iteration optimizers.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
# Local Variables
|
|
72
|
+
self.symbols = symbols
|
|
73
|
+
self.coordinate_structure = coordinate_structure
|
|
74
|
+
self.bond_length = bond_length
|
|
75
|
+
self.charge = charge
|
|
76
|
+
if len(self.coordinate_structure) != len(self.symbols):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"The number of symbols must match the number of coordinates"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self.n_layers = n_layers
|
|
82
|
+
self.results = {}
|
|
83
|
+
self.ansatz = ansatz
|
|
84
|
+
self.optimizer = optimizer
|
|
85
|
+
self.max_iterations = max_iterations
|
|
86
|
+
self.current_iteration = 0
|
|
87
|
+
|
|
88
|
+
self.cost_hamiltonian = self._generate_hamiltonian_operations()
|
|
89
|
+
|
|
90
|
+
super().__init__(**kwargs)
|
|
91
|
+
|
|
92
|
+
self._meta_circuits = self._create_meta_circuits_dict()
|
|
93
|
+
|
|
94
|
+
def _generate_hamiltonian_operations(self) -> qml.operation.Operator:
|
|
95
|
+
"""
|
|
96
|
+
Generate the Hamiltonian operators for the given bond length.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The Hamiltonian corresponding to the VQE problem.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
coordinates = [
|
|
103
|
+
(
|
|
104
|
+
coord_0 * self.bond_length,
|
|
105
|
+
coord_1 * self.bond_length,
|
|
106
|
+
coord_2 * self.bond_length,
|
|
107
|
+
)
|
|
108
|
+
for (coord_0, coord_1, coord_2) in self.coordinate_structure
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
coordinates = np.array(coordinates)
|
|
112
|
+
molecule = qml.qchem.Molecule(self.symbols, coordinates, charge=self.charge)
|
|
113
|
+
hamiltonian, qubits = qml.qchem.molecular_hamiltonian(molecule)
|
|
114
|
+
|
|
115
|
+
self.n_qubits = qubits
|
|
116
|
+
self.n_electrons = molecule.n_electrons
|
|
117
|
+
|
|
118
|
+
self.n_params = self.ansatz.n_params(
|
|
119
|
+
self.n_qubits, n_electrons=self.n_electrons
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
constant_terms_idx = list(
|
|
123
|
+
filter(
|
|
124
|
+
lambda x: all(
|
|
125
|
+
isinstance(term, qml.I) for term in hamiltonian[x].terms()[1]
|
|
126
|
+
),
|
|
127
|
+
range(len(hamiltonian)),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self.loss_constant = sum(
|
|
132
|
+
map(lambda x: hamiltonian[x].scalar.item(), constant_terms_idx)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
for idx in constant_terms_idx:
|
|
136
|
+
hamiltonian -= hamiltonian[idx]
|
|
137
|
+
|
|
138
|
+
return hamiltonian.simplify()
|
|
139
|
+
|
|
140
|
+
def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
|
|
141
|
+
weights_syms = sp.symarray("w", (self.n_layers, self.n_params))
|
|
142
|
+
|
|
143
|
+
def _prepare_circuit(ansatz, hamiltonian, params):
|
|
144
|
+
"""
|
|
145
|
+
Prepare the circuit for the VQE problem.
|
|
146
|
+
Args:
|
|
147
|
+
ansatz (Ansatze): The ansatz to use
|
|
148
|
+
hamiltonian (qml.Hamiltonian): The Hamiltonian to use
|
|
149
|
+
params (list): The parameters to use for the ansatz
|
|
150
|
+
"""
|
|
151
|
+
self._set_ansatz(ansatz, params)
|
|
152
|
+
|
|
153
|
+
# Even though in principle we want to sample from a state,
|
|
154
|
+
# we are applying an `expval` operation here to make it compatible
|
|
155
|
+
# with the pennylane transforms down the line, which complain about
|
|
156
|
+
# the `sample` operation.
|
|
157
|
+
return qml.expval(hamiltonian)
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"cost_circuit": self._meta_circuit_factory(
|
|
161
|
+
qml.tape.make_qscript(_prepare_circuit)(
|
|
162
|
+
self.ansatz, self.cost_hamiltonian, weights_syms
|
|
163
|
+
),
|
|
164
|
+
symbols=weights_syms.flatten(),
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def _set_ansatz(self, ansatz: VQEAnsatz, params):
|
|
169
|
+
"""
|
|
170
|
+
Set the ansatz for the VQE problem.
|
|
171
|
+
Args:
|
|
172
|
+
ansatz (Ansatze): The ansatz to use
|
|
173
|
+
params (list): The parameters to use for the ansatz
|
|
174
|
+
n_layers (int): The number of layers to use for the ansatz
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def _add_hw_efficient_ansatz(params):
|
|
178
|
+
raise NotImplementedError
|
|
179
|
+
|
|
180
|
+
def _add_qaoa_ansatz(params):
|
|
181
|
+
# This infers layers automatically from the parameters shape
|
|
182
|
+
qml.QAOAEmbedding(
|
|
183
|
+
features=[],
|
|
184
|
+
weights=params.reshape(self.n_layers, -1),
|
|
185
|
+
wires=range(self.n_qubits),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _add_ry_ansatz(params):
|
|
189
|
+
qml.layer(
|
|
190
|
+
qml.AngleEmbedding,
|
|
191
|
+
self.n_layers,
|
|
192
|
+
params.reshape(self.n_layers, -1),
|
|
193
|
+
wires=range(self.n_qubits),
|
|
194
|
+
rotation="Y",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def _add_ryrz_ansatz(params):
|
|
198
|
+
def _ryrz(params, wires):
|
|
199
|
+
ry_rots, rz_rots = params.reshape(2, -1)
|
|
200
|
+
qml.AngleEmbedding(ry_rots, wires=wires, rotation="Y")
|
|
201
|
+
qml.AngleEmbedding(rz_rots, wires=wires, rotation="Z")
|
|
202
|
+
|
|
203
|
+
qml.layer(
|
|
204
|
+
_ryrz,
|
|
205
|
+
self.n_layers,
|
|
206
|
+
params.reshape(self.n_layers, -1),
|
|
207
|
+
wires=range(self.n_qubits),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _add_uccsd_ansatz(params):
|
|
211
|
+
hf_state = qml.qchem.hf_state(self.n_electrons, self.n_qubits)
|
|
212
|
+
|
|
213
|
+
singles, doubles = qml.qchem.excitations(self.n_electrons, self.n_qubits)
|
|
214
|
+
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
215
|
+
|
|
216
|
+
qml.UCCSD(
|
|
217
|
+
params.reshape(self.n_layers, -1),
|
|
218
|
+
wires=range(self.n_qubits),
|
|
219
|
+
s_wires=s_wires,
|
|
220
|
+
d_wires=d_wires,
|
|
221
|
+
init_state=hf_state,
|
|
222
|
+
n_repeats=self.n_layers,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def _add_hartree_fock_ansatz(params):
|
|
226
|
+
singles, doubles = qml.qchem.excitations(self.n_electrons, self.n_qubits)
|
|
227
|
+
hf_state = qml.qchem.hf_state(self.n_electrons, self.n_qubits)
|
|
228
|
+
|
|
229
|
+
qml.layer(
|
|
230
|
+
qml.AllSinglesDoubles,
|
|
231
|
+
self.n_layers,
|
|
232
|
+
params.reshape(self.n_layers, -1),
|
|
233
|
+
wires=range(self.n_qubits),
|
|
234
|
+
hf_state=hf_state,
|
|
235
|
+
singles=singles,
|
|
236
|
+
doubles=doubles,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Reset the BasisState operations after the first layer
|
|
240
|
+
# for behaviour similar to UCCSD ansatz
|
|
241
|
+
for op in qml.QueuingManager.active_context().queue[1:]:
|
|
242
|
+
op._hyperparameters["hf_state"] = 0
|
|
243
|
+
|
|
244
|
+
if ansatz in VQEAnsatz:
|
|
245
|
+
locals()[f"_add_{ansatz.name.lower()}_ansatz"](params)
|
|
246
|
+
else:
|
|
247
|
+
raise ValueError(f"Invalid Ansatz Value. Got {ansatz}.")
|
|
248
|
+
|
|
249
|
+
def _generate_circuits(self):
|
|
250
|
+
"""
|
|
251
|
+
Generate the circuits for the VQE problem.
|
|
252
|
+
|
|
253
|
+
In this method, we generate bulk circuits based on the selected parameters.
|
|
254
|
+
We generate circuits for each bond length and each ansatz and optimization choice.
|
|
255
|
+
|
|
256
|
+
The structure of the circuits is as follows:
|
|
257
|
+
- For each bond length:
|
|
258
|
+
- For each ansatz:
|
|
259
|
+
- Generate the circuit
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
for p, params_group in enumerate(self._curr_params):
|
|
263
|
+
circuit = self._meta_circuits[
|
|
264
|
+
"cost_circuit"
|
|
265
|
+
].initialize_circuit_from_params(params_group, tag_prefix=f"{p}")
|
|
266
|
+
|
|
267
|
+
self.circuits.append(circuit)
|
|
268
|
+
|
|
269
|
+
def _run_optimization_circuits(self, store_data, data_file):
|
|
270
|
+
if self.cost_hamiltonian is None or len(self.cost_hamiltonian) == 0:
|
|
271
|
+
raise RuntimeError(
|
|
272
|
+
"Hamiltonian operators must be generated before running the VQE"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return super()._run_optimization_circuits(store_data, data_file)
|
divi/qprog/_vqe_sweep.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from functools import partial
|
|
6
|
+
from itertools import product
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
|
|
11
|
+
from divi.qprog import VQE, ProgramBatch, VQEAnsatz
|
|
12
|
+
|
|
13
|
+
from .optimizers import Optimizer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VQEHyperparameterSweep(ProgramBatch):
|
|
17
|
+
"""Allows user to carry out a grid search across different values
|
|
18
|
+
for the ansatz and the bond length used in a VQE program.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
bond_lengths: list[float],
|
|
24
|
+
ansatze: list[VQEAnsatz],
|
|
25
|
+
symbols: list[str],
|
|
26
|
+
coordinate_structure: list[tuple[float, float, float]],
|
|
27
|
+
charge: float = 0,
|
|
28
|
+
optimizer: Optimizer = Optimizer.MONTE_CARLO,
|
|
29
|
+
max_iterations: int = 10,
|
|
30
|
+
**kwargs,
|
|
31
|
+
):
|
|
32
|
+
"""Initiates the class.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
bond_lengths (list): The bond lengths to consider.
|
|
36
|
+
ansatze (list): The ansatze to use for the VQE problem.
|
|
37
|
+
symbols (list): The symbols of the atoms in the molecule.
|
|
38
|
+
coordinate_structure (list): The coordinate structure of the molecule.
|
|
39
|
+
optimizer (Optimizers): The optimizer to use.
|
|
40
|
+
max_iterations (int): Maximum number of iteration optimizers.
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(backend=kwargs.pop("backend"))
|
|
43
|
+
|
|
44
|
+
self.ansatze = ansatze
|
|
45
|
+
self.bond_lengths = [round(bnd, 9) for bnd in bond_lengths]
|
|
46
|
+
self.max_iterations = max_iterations
|
|
47
|
+
|
|
48
|
+
self._constructor = partial(
|
|
49
|
+
VQE,
|
|
50
|
+
symbols=symbols,
|
|
51
|
+
coordinate_structure=coordinate_structure,
|
|
52
|
+
charge=charge,
|
|
53
|
+
optimizer=optimizer,
|
|
54
|
+
max_iterations=self.max_iterations,
|
|
55
|
+
backend=self.backend,
|
|
56
|
+
**kwargs,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def create_programs(self):
|
|
60
|
+
if len(self.programs) > 0:
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
"Some programs already exist. "
|
|
63
|
+
"Clear the program dictionary before creating new ones by using batch.reset()."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
super().create_programs()
|
|
67
|
+
|
|
68
|
+
for ansatz, bond_length in product(self.ansatze, self.bond_lengths):
|
|
69
|
+
_job_id = (ansatz, bond_length)
|
|
70
|
+
self.programs[_job_id] = self._constructor(
|
|
71
|
+
job_id=_job_id,
|
|
72
|
+
bond_length=bond_length,
|
|
73
|
+
ansatz=ansatz,
|
|
74
|
+
losses=self._manager.list(),
|
|
75
|
+
final_params=self._manager.list(),
|
|
76
|
+
progress_queue=self._queue,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def aggregate_results(self):
|
|
80
|
+
if len(self.programs) == 0:
|
|
81
|
+
raise RuntimeError("No programs to aggregate. Run create_programs() first.")
|
|
82
|
+
|
|
83
|
+
if self._executor is not None:
|
|
84
|
+
self.wait_for_all()
|
|
85
|
+
|
|
86
|
+
if any(len(program.losses) == 0 for program in self.programs.values()):
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
"Some/All programs have empty losses. Did you call run()?"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
all_energies = {key: prog.losses[-1] for key, prog in self.programs.items()}
|
|
92
|
+
|
|
93
|
+
smallest_key = min(all_energies, key=lambda k: min(all_energies[k].values()))
|
|
94
|
+
smallest_value = min(all_energies[smallest_key].values())
|
|
95
|
+
|
|
96
|
+
return smallest_key, smallest_value
|
|
97
|
+
|
|
98
|
+
def visualize_results(self, graph_type: Literal["line", "scatter"] = "line"):
|
|
99
|
+
"""
|
|
100
|
+
Visualize the results of the VQE problem.
|
|
101
|
+
"""
|
|
102
|
+
if graph_type not in ["line", "scatter"]:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Invalid graph type: {graph_type}. Choose between 'line' and 'scatter'."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if self._executor is not None:
|
|
108
|
+
self.wait_for_all()
|
|
109
|
+
|
|
110
|
+
data = []
|
|
111
|
+
colors = ["blue", "g", "r", "c", "m", "y", "k"]
|
|
112
|
+
|
|
113
|
+
ansatz_list = list(VQEAnsatz)
|
|
114
|
+
|
|
115
|
+
if graph_type == "scatter":
|
|
116
|
+
for ansatz, bond_length in self.programs.keys():
|
|
117
|
+
min_energies = []
|
|
118
|
+
|
|
119
|
+
curr_energies = self.programs[(ansatz, bond_length)].losses[-1]
|
|
120
|
+
min_energies.append(
|
|
121
|
+
(
|
|
122
|
+
bond_length,
|
|
123
|
+
min(curr_energies.values()),
|
|
124
|
+
colors[ansatz_list.index(ansatz)],
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
data.extend(min_energies)
|
|
128
|
+
|
|
129
|
+
x, y, z = zip(*data)
|
|
130
|
+
plt.scatter(x, y, color=z, label=ansatz)
|
|
131
|
+
|
|
132
|
+
elif graph_type == "line":
|
|
133
|
+
for ansatz in self.ansatze:
|
|
134
|
+
energies = []
|
|
135
|
+
for bond_length in self.bond_lengths:
|
|
136
|
+
energies.append(
|
|
137
|
+
min(self.programs[(ansatz, bond_length)].losses[-1].values())
|
|
138
|
+
)
|
|
139
|
+
plt.plot(self.bond_lengths, energies, label=ansatz)
|
|
140
|
+
|
|
141
|
+
plt.xlabel("Bond length")
|
|
142
|
+
plt.ylabel("Energy level")
|
|
143
|
+
plt.legend()
|
|
144
|
+
plt.show()
|
divi/qprog/batch.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import traceback
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
8
|
+
from multiprocessing import Event, Manager
|
|
9
|
+
from multiprocessing.synchronize import Event as EventClass
|
|
10
|
+
from queue import Empty, Queue
|
|
11
|
+
from threading import Lock, Thread
|
|
12
|
+
from warnings import warn
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.progress import Progress, TaskID
|
|
16
|
+
|
|
17
|
+
from divi._pbar import make_progress_bar
|
|
18
|
+
from divi.interfaces import CircuitRunner
|
|
19
|
+
from divi.parallel_simulator import ParallelSimulator
|
|
20
|
+
from divi.qlogger import disable_logging
|
|
21
|
+
from divi.qprog.quantum_program import QuantumProgram
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def queue_listener(
|
|
25
|
+
queue: Queue,
|
|
26
|
+
progress_bar: Progress,
|
|
27
|
+
pb_task_map: dict[QuantumProgram, TaskID],
|
|
28
|
+
done_event: EventClass,
|
|
29
|
+
is_jupyter: bool,
|
|
30
|
+
lock: Lock,
|
|
31
|
+
):
|
|
32
|
+
while not done_event.is_set():
|
|
33
|
+
try:
|
|
34
|
+
msg = queue.get(timeout=0.1)
|
|
35
|
+
except Empty:
|
|
36
|
+
continue
|
|
37
|
+
except Exception as e:
|
|
38
|
+
progress_bar.console.log(f"[queue_listener] Unexpected exception: {e}")
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
with lock:
|
|
42
|
+
task_id = pb_task_map[msg["job_id"]]
|
|
43
|
+
|
|
44
|
+
progress_bar.update(
|
|
45
|
+
task_id,
|
|
46
|
+
advance=msg["progress"],
|
|
47
|
+
poll_attempt=msg.get("poll_attempt", 0),
|
|
48
|
+
message=msg.get("message", ""),
|
|
49
|
+
final_status=msg.get("final_status", ""),
|
|
50
|
+
refresh=is_jupyter,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ProgramBatch(ABC):
|
|
55
|
+
"""This abstract class provides the basic scaffolding for higher-order
|
|
56
|
+
computations that require more than one quantum program to achieve its goal.
|
|
57
|
+
|
|
58
|
+
Each implementation of this class has to have an implementation of two functions:
|
|
59
|
+
1. `create_programs`: This function generates the independent programs that
|
|
60
|
+
are needed to achieve the objective of the job. The creation of those
|
|
61
|
+
programs can utilize the instance variables of the class to initialize
|
|
62
|
+
their parameters. The programs should be stored in a key-value store
|
|
63
|
+
where the keys represent the identifier of the program, whether random
|
|
64
|
+
or identificatory.
|
|
65
|
+
|
|
66
|
+
2. `aggregate_results`: This function aggregates the results of the programs
|
|
67
|
+
after they are done executing. This function should be aware of the different
|
|
68
|
+
formats the programs might have (counts dictionary, expectation value, etc) and
|
|
69
|
+
handle such cases accordingly.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, backend: CircuitRunner):
|
|
73
|
+
super().__init__()
|
|
74
|
+
|
|
75
|
+
self.backend = backend
|
|
76
|
+
self._executor = None
|
|
77
|
+
self.programs = {}
|
|
78
|
+
|
|
79
|
+
self._total_circuit_count = 0
|
|
80
|
+
self._total_run_time = 0.0
|
|
81
|
+
|
|
82
|
+
self._is_local = isinstance(backend, ParallelSimulator)
|
|
83
|
+
self._is_jupyter = Console().is_jupyter
|
|
84
|
+
|
|
85
|
+
# Disable logging since we already have the bars to track progress
|
|
86
|
+
disable_logging()
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def total_circuit_count(self):
|
|
90
|
+
return self._total_circuit_count
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def total_run_time(self):
|
|
94
|
+
return self._total_run_time
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def create_programs(self):
|
|
98
|
+
self._manager = Manager()
|
|
99
|
+
self._queue = self._manager.Queue()
|
|
100
|
+
|
|
101
|
+
if hasattr(self, "max_iterations"):
|
|
102
|
+
self._done_event = Event()
|
|
103
|
+
|
|
104
|
+
def reset(self):
|
|
105
|
+
self.programs.clear()
|
|
106
|
+
|
|
107
|
+
# Stop any active executor
|
|
108
|
+
if self._executor is not None:
|
|
109
|
+
self._executor.shutdown(wait=False)
|
|
110
|
+
self._executor = None
|
|
111
|
+
self.futures = None
|
|
112
|
+
|
|
113
|
+
# Signal and wait for listener thread to stop
|
|
114
|
+
if hasattr(self, "_done_event") and self._done_event is not None:
|
|
115
|
+
self._done_event.set()
|
|
116
|
+
self._done_event = None
|
|
117
|
+
|
|
118
|
+
if getattr(self, "_listener_thread", None) is not None:
|
|
119
|
+
self._listener_thread.join(timeout=1)
|
|
120
|
+
if self._listener_thread.is_alive():
|
|
121
|
+
warn(
|
|
122
|
+
"Listener thread did not terminate within timeout and may still be running."
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
self._listener_thread = None
|
|
126
|
+
self._queue.close()
|
|
127
|
+
self._queue.join_thread()
|
|
128
|
+
self._queue = None
|
|
129
|
+
|
|
130
|
+
# Stop the progress bar if it's still active
|
|
131
|
+
if getattr(self, "_progress_bar", None) is not None:
|
|
132
|
+
try:
|
|
133
|
+
self._progress_bar.stop()
|
|
134
|
+
self._progress_bar = None
|
|
135
|
+
except Exception:
|
|
136
|
+
pass # Already stopped or not running
|
|
137
|
+
|
|
138
|
+
self._pb_task_map.clear()
|
|
139
|
+
|
|
140
|
+
def add_program_to_executor(self, program):
|
|
141
|
+
self.futures.append(self._executor.submit(program.run))
|
|
142
|
+
|
|
143
|
+
if self._progress_bar is not None:
|
|
144
|
+
with self._pb_lock:
|
|
145
|
+
self._pb_task_map[program.job_id] = self._progress_bar.add_task(
|
|
146
|
+
"",
|
|
147
|
+
job_name=f"Job {program.job_id}",
|
|
148
|
+
total=self.max_iterations,
|
|
149
|
+
completed=0,
|
|
150
|
+
poll_attempt=0,
|
|
151
|
+
message="",
|
|
152
|
+
final_status="",
|
|
153
|
+
mode=("simulation" if self._is_local else "network"),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def run(self):
|
|
157
|
+
if self._executor is not None:
|
|
158
|
+
raise RuntimeError("A batch is already being run.")
|
|
159
|
+
|
|
160
|
+
if len(self.programs) == 0:
|
|
161
|
+
raise RuntimeError("No programs to run.")
|
|
162
|
+
|
|
163
|
+
self._progress_bar = (
|
|
164
|
+
make_progress_bar(
|
|
165
|
+
max_retries=None if self._is_local else self.backend.max_retries,
|
|
166
|
+
is_jupyter=self._is_jupyter,
|
|
167
|
+
)
|
|
168
|
+
if hasattr(self, "max_iterations")
|
|
169
|
+
else None
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self._executor = ProcessPoolExecutor()
|
|
173
|
+
self.futures = []
|
|
174
|
+
self._pb_task_map = {}
|
|
175
|
+
self._pb_lock = Lock()
|
|
176
|
+
|
|
177
|
+
if self._progress_bar is not None:
|
|
178
|
+
self._progress_bar.start()
|
|
179
|
+
self._listener_thread = Thread(
|
|
180
|
+
target=queue_listener,
|
|
181
|
+
args=(
|
|
182
|
+
self._queue,
|
|
183
|
+
self._progress_bar,
|
|
184
|
+
self._pb_task_map,
|
|
185
|
+
self._done_event,
|
|
186
|
+
self._is_jupyter,
|
|
187
|
+
self._pb_lock,
|
|
188
|
+
),
|
|
189
|
+
daemon=True,
|
|
190
|
+
)
|
|
191
|
+
self._listener_thread.start()
|
|
192
|
+
|
|
193
|
+
for program in self.programs.values():
|
|
194
|
+
self.add_program_to_executor(program)
|
|
195
|
+
|
|
196
|
+
def check_all_done(self):
|
|
197
|
+
return all(future.done() for future in self.futures)
|
|
198
|
+
|
|
199
|
+
def wait_for_all(self):
|
|
200
|
+
if self._executor is None:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
exceptions = []
|
|
204
|
+
try:
|
|
205
|
+
# Ensure all futures are completed and handle exceptions.
|
|
206
|
+
for future in as_completed(self.futures):
|
|
207
|
+
try:
|
|
208
|
+
future.result() # Raises an exception if the task failed.
|
|
209
|
+
except Exception as e:
|
|
210
|
+
exceptions.append(e)
|
|
211
|
+
|
|
212
|
+
finally:
|
|
213
|
+
self._executor.shutdown(wait=True, cancel_futures=False)
|
|
214
|
+
self._executor = None
|
|
215
|
+
|
|
216
|
+
if self._progress_bar is not None:
|
|
217
|
+
self._done_event.set()
|
|
218
|
+
self._listener_thread.join()
|
|
219
|
+
|
|
220
|
+
if exceptions:
|
|
221
|
+
for i, exc in enumerate(exceptions, 1):
|
|
222
|
+
print(f"Task {i} failed with exception:")
|
|
223
|
+
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
|
224
|
+
raise RuntimeError("One or more tasks failed. Check logs for details.")
|
|
225
|
+
|
|
226
|
+
if self._progress_bar is not None:
|
|
227
|
+
self._progress_bar.stop()
|
|
228
|
+
|
|
229
|
+
self._total_circuit_count += sum(future.result()[0] for future in self.futures)
|
|
230
|
+
self._total_run_time += sum(future.result()[1] for future in self.futures)
|
|
231
|
+
self.futures = []
|
|
232
|
+
|
|
233
|
+
@abstractmethod
|
|
234
|
+
def aggregate_results(self):
|
|
235
|
+
raise NotImplementedError
|