qoro-divi 0.3.4__py3-none-any.whl → 0.4.0__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/__init__.py +1 -1
- divi/backends/_circuit_runner.py +42 -0
- divi/backends/_parallel_simulator.py +145 -49
- divi/backends/_qoro_service.py +451 -182
- divi/backends/_qpu_system.py +77 -3
- divi/circuits/_core.py +124 -4
- divi/circuits/qasm.py +20 -3
- divi/extern/cirq/_validator.py +12 -3
- divi/qprog/__init__.py +1 -0
- divi/qprog/algorithms/_ansatze.py +112 -12
- divi/qprog/algorithms/_qaoa.py +179 -110
- divi/qprog/algorithms/_vqe.py +192 -58
- divi/qprog/batch.py +270 -51
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +336 -51
- divi/qprog/quantum_program.py +162 -339
- divi/qprog/variational_quantum_algorithm.py +786 -0
- divi/qprog/workflows/_graph_partitioning.py +43 -38
- divi/qprog/workflows/_qubo_partitioning.py +41 -24
- divi/qprog/workflows/_vqe_sweep.py +67 -39
- divi/reporting/_pbar.py +51 -9
- divi/reporting/_qlogger.py +35 -1
- divi/reporting/_reporter.py +11 -20
- divi/utils.py +100 -4
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.4.0.dist-info}/METADATA +16 -1
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.4.0.dist-info}/RECORD +30 -28
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.4.0.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.4.0.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.4.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.4.0.dist-info}/WHEEL +0 -0
divi/qprog/algorithms/_vqe.py
CHANGED
|
@@ -4,16 +4,41 @@
|
|
|
4
4
|
|
|
5
5
|
from warnings import warn
|
|
6
6
|
|
|
7
|
+
import numpy as np
|
|
7
8
|
import pennylane as qml
|
|
8
9
|
import sympy as sp
|
|
9
10
|
|
|
10
|
-
from divi.circuits import MetaCircuit
|
|
11
|
-
from divi.qprog import QuantumProgram
|
|
11
|
+
from divi.circuits import Circuit, MetaCircuit
|
|
12
12
|
from divi.qprog.algorithms._ansatze import Ansatz, HartreeFockAnsatz
|
|
13
13
|
from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer
|
|
14
|
+
from divi.qprog.variational_quantum_algorithm import VariationalQuantumAlgorithm
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VQE(VariationalQuantumAlgorithm):
|
|
18
|
+
"""Variational Quantum Eigensolver (VQE) implementation.
|
|
19
|
+
|
|
20
|
+
VQE is a hybrid quantum-classical algorithm used to find the ground state
|
|
21
|
+
energy of a given Hamiltonian. It works by preparing a parameterized quantum
|
|
22
|
+
state (ansatz) and optimizing the parameters to minimize the expectation
|
|
23
|
+
value of the Hamiltonian.
|
|
24
|
+
|
|
25
|
+
The algorithm can work with either:
|
|
26
|
+
- A molecular Hamiltonian (for quantum chemistry problems)
|
|
27
|
+
- A custom Hamiltonian operator
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
ansatz (Ansatz): The parameterized quantum circuit ansatz.
|
|
31
|
+
n_layers (int): Number of ansatz layers.
|
|
32
|
+
n_qubits (int): Number of qubits in the system.
|
|
33
|
+
n_electrons (int): Number of electrons (for molecular systems).
|
|
34
|
+
cost_hamiltonian (qml.operation.Operator): The Hamiltonian to minimize.
|
|
35
|
+
loss_constant (float): Constant term extracted from the Hamiltonian.
|
|
36
|
+
molecule (qml.qchem.Molecule): The molecule object (if applicable).
|
|
37
|
+
optimizer (Optimizer): Classical optimizer for parameter updates.
|
|
38
|
+
max_iterations (int): Maximum number of optimization iterations.
|
|
39
|
+
current_iteration (int): Current optimization iteration.
|
|
40
|
+
"""
|
|
14
41
|
|
|
15
|
-
|
|
16
|
-
class VQE(QuantumProgram):
|
|
17
42
|
def __init__(
|
|
18
43
|
self,
|
|
19
44
|
hamiltonian: qml.operation.Operator | None = None,
|
|
@@ -25,17 +50,19 @@ class VQE(QuantumProgram):
|
|
|
25
50
|
max_iterations=10,
|
|
26
51
|
**kwargs,
|
|
27
52
|
) -> None:
|
|
28
|
-
"""
|
|
29
|
-
Initialize the VQE problem.
|
|
53
|
+
"""Initialize the VQE problem.
|
|
30
54
|
|
|
31
55
|
Args:
|
|
32
|
-
|
|
33
|
-
molecule (
|
|
34
|
-
n_electrons (int
|
|
35
|
-
Only
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
hamiltonian (qml.operation.Operator | None): A Hamiltonian representing the problem. Defaults to None.
|
|
57
|
+
molecule (qml.qchem.Molecule | None): The molecule representing the problem. Defaults to None.
|
|
58
|
+
n_electrons (int | None): Number of electrons associated with the Hamiltonian.
|
|
59
|
+
Only needed when a Hamiltonian is given. Defaults to None.
|
|
60
|
+
n_layers (int): Number of ansatz layers. Defaults to 1.
|
|
61
|
+
ansatz (Ansatz | None): The ansatz to use for the VQE problem.
|
|
62
|
+
Defaults to HartreeFockAnsatz.
|
|
63
|
+
optimizer (Optimizer | None): The optimizer to use. Defaults to MonteCarloOptimizer.
|
|
64
|
+
max_iterations (int): Maximum number of optimization iterations. Defaults to 10.
|
|
65
|
+
**kwargs: Additional keyword arguments passed to the parent class.
|
|
39
66
|
"""
|
|
40
67
|
|
|
41
68
|
# Local Variables
|
|
@@ -47,6 +74,8 @@ class VQE(QuantumProgram):
|
|
|
47
74
|
|
|
48
75
|
self.optimizer = optimizer if optimizer is not None else MonteCarloOptimizer()
|
|
49
76
|
|
|
77
|
+
self._eigenstate = None
|
|
78
|
+
|
|
50
79
|
self._process_problem_input(
|
|
51
80
|
hamiltonian=hamiltonian, molecule=molecule, n_electrons=n_electrons
|
|
52
81
|
)
|
|
@@ -55,14 +84,48 @@ class VQE(QuantumProgram):
|
|
|
55
84
|
|
|
56
85
|
self._meta_circuits = self._create_meta_circuits_dict()
|
|
57
86
|
|
|
87
|
+
@property
|
|
88
|
+
def cost_hamiltonian(self) -> qml.operation.Operator:
|
|
89
|
+
"""The cost Hamiltonian for the VQE problem."""
|
|
90
|
+
return self._cost_hamiltonian
|
|
91
|
+
|
|
58
92
|
@property
|
|
59
93
|
def n_params(self):
|
|
94
|
+
"""Get the total number of parameters for the VQE ansatz.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
int: Total number of parameters (n_params_per_layer * n_layers).
|
|
98
|
+
"""
|
|
60
99
|
return (
|
|
61
100
|
self.ansatz.n_params_per_layer(self.n_qubits, n_electrons=self.n_electrons)
|
|
62
101
|
* self.n_layers
|
|
63
102
|
)
|
|
64
103
|
|
|
104
|
+
@property
|
|
105
|
+
def eigenstate(self) -> np.ndarray | None:
|
|
106
|
+
"""Get the computed eigenstate as a NumPy array.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
np.ndarray | None: The array of bits of the lowest energy eigenstate,
|
|
110
|
+
or None if not computed.
|
|
111
|
+
"""
|
|
112
|
+
return self._eigenstate
|
|
113
|
+
|
|
65
114
|
def _process_problem_input(self, hamiltonian, molecule, n_electrons):
|
|
115
|
+
"""Process and validate the VQE problem input.
|
|
116
|
+
|
|
117
|
+
Handles both Hamiltonian-based and molecule-based problem specifications,
|
|
118
|
+
extracting the necessary information (n_qubits, n_electrons, hamiltonian).
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
hamiltonian: PennyLane Hamiltonian operator or None.
|
|
122
|
+
molecule: PennyLane Molecule object or None.
|
|
123
|
+
n_electrons: Number of electrons or None.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If neither hamiltonian nor molecule is provided.
|
|
127
|
+
UserWarning: If n_electrons conflicts with the molecule's electron count.
|
|
128
|
+
"""
|
|
66
129
|
if hamiltonian is None and molecule is None:
|
|
67
130
|
raise ValueError(
|
|
68
131
|
"Either one of `molecule` and `hamiltonian` must be provided."
|
|
@@ -85,38 +148,76 @@ class VQE(QuantumProgram):
|
|
|
85
148
|
UserWarning,
|
|
86
149
|
)
|
|
87
150
|
|
|
88
|
-
self.
|
|
151
|
+
self._cost_hamiltonian = self._clean_hamiltonian(hamiltonian)
|
|
89
152
|
|
|
90
153
|
def _clean_hamiltonian(
|
|
91
154
|
self, hamiltonian: qml.operation.Operator
|
|
92
155
|
) -> qml.operation.Operator:
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
|
|
156
|
+
"""Separate constant and non-constant terms in a Hamiltonian.
|
|
157
|
+
|
|
158
|
+
This function processes a PennyLane Hamiltonian to separate out any terms
|
|
159
|
+
that are constant (i.e., proportional to the identity operator). The sum
|
|
160
|
+
of these constant terms is stored in `self.loss_constant`, and a new
|
|
161
|
+
Hamiltonian containing only the non-constant terms is returned.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
hamiltonian: The Hamiltonian operator to process.
|
|
96
165
|
|
|
97
166
|
Returns:
|
|
98
|
-
The Hamiltonian without the
|
|
99
|
-
"""
|
|
167
|
+
qml.operation.Operator: The Hamiltonian without the constant (identity) component.
|
|
100
168
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
169
|
+
Raises:
|
|
170
|
+
ValueError: If the Hamiltonian is found to contain only constant terms.
|
|
171
|
+
"""
|
|
172
|
+
terms = (
|
|
173
|
+
hamiltonian.operands
|
|
174
|
+
if isinstance(hamiltonian, qml.ops.Sum)
|
|
175
|
+
else [hamiltonian]
|
|
108
176
|
)
|
|
109
177
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
178
|
+
loss_constant = 0.0
|
|
179
|
+
non_constant_terms = []
|
|
113
180
|
|
|
114
|
-
for
|
|
115
|
-
|
|
181
|
+
for term in terms:
|
|
182
|
+
coeff = 1.0
|
|
183
|
+
base_op = term
|
|
184
|
+
if isinstance(term, qml.ops.SProd):
|
|
185
|
+
coeff = term.scalar
|
|
186
|
+
base_op = term.base
|
|
116
187
|
|
|
117
|
-
|
|
188
|
+
# Check for Identity term
|
|
189
|
+
is_constant = False
|
|
190
|
+
if isinstance(base_op, qml.Identity):
|
|
191
|
+
is_constant = True
|
|
192
|
+
elif isinstance(base_op, qml.ops.Prod) and all(
|
|
193
|
+
isinstance(op, qml.Identity) for op in base_op.operands
|
|
194
|
+
):
|
|
195
|
+
is_constant = True
|
|
196
|
+
|
|
197
|
+
if is_constant:
|
|
198
|
+
loss_constant += coeff
|
|
199
|
+
else:
|
|
200
|
+
non_constant_terms.append(term)
|
|
201
|
+
|
|
202
|
+
self.loss_constant = float(loss_constant)
|
|
203
|
+
|
|
204
|
+
if not non_constant_terms:
|
|
205
|
+
raise ValueError("Hamiltonian contains only constant terms.")
|
|
206
|
+
|
|
207
|
+
# Reconstruct the Hamiltonian from non-constant terms
|
|
208
|
+
if len(non_constant_terms) > 1:
|
|
209
|
+
new_hamiltonian = qml.sum(*non_constant_terms)
|
|
210
|
+
else:
|
|
211
|
+
new_hamiltonian = non_constant_terms[0]
|
|
212
|
+
|
|
213
|
+
return new_hamiltonian.simplify()
|
|
118
214
|
|
|
119
215
|
def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
|
|
216
|
+
"""Create the meta-circuit dictionary for VQE.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
dict[str, MetaCircuit]: Dictionary containing the cost circuit template.
|
|
220
|
+
"""
|
|
120
221
|
weights_syms = sp.symarray(
|
|
121
222
|
"w",
|
|
122
223
|
(
|
|
@@ -127,13 +228,13 @@ class VQE(QuantumProgram):
|
|
|
127
228
|
),
|
|
128
229
|
)
|
|
129
230
|
|
|
130
|
-
def _prepare_circuit(hamiltonian, params):
|
|
131
|
-
"""
|
|
132
|
-
|
|
231
|
+
def _prepare_circuit(hamiltonian, params, final_measurement=False):
|
|
232
|
+
"""Prepare the circuit for the VQE problem.
|
|
233
|
+
|
|
133
234
|
Args:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
235
|
+
hamiltonian: The Hamiltonian to measure.
|
|
236
|
+
params: The parameters for the ansatz.
|
|
237
|
+
final_measurement (bool): Whether to perform final measurement.
|
|
137
238
|
"""
|
|
138
239
|
self.ansatz.build(
|
|
139
240
|
params,
|
|
@@ -142,6 +243,9 @@ class VQE(QuantumProgram):
|
|
|
142
243
|
n_electrons=self.n_electrons,
|
|
143
244
|
)
|
|
144
245
|
|
|
246
|
+
if final_measurement:
|
|
247
|
+
return qml.probs()
|
|
248
|
+
|
|
145
249
|
# Even though in principle we want to sample from a state,
|
|
146
250
|
# we are applying an `expval` operation here to make it compatible
|
|
147
251
|
# with the pennylane transforms down the line, which complain about
|
|
@@ -151,36 +255,66 @@ class VQE(QuantumProgram):
|
|
|
151
255
|
return {
|
|
152
256
|
"cost_circuit": self._meta_circuit_factory(
|
|
153
257
|
qml.tape.make_qscript(_prepare_circuit)(
|
|
154
|
-
self.
|
|
258
|
+
self._cost_hamiltonian, weights_syms, final_measurement=False
|
|
155
259
|
),
|
|
156
260
|
symbols=weights_syms.flatten(),
|
|
157
|
-
)
|
|
261
|
+
),
|
|
262
|
+
"meas_circuit": self._meta_circuit_factory(
|
|
263
|
+
qml.tape.make_qscript(_prepare_circuit)(
|
|
264
|
+
self._cost_hamiltonian, weights_syms, final_measurement=True
|
|
265
|
+
),
|
|
266
|
+
symbols=weights_syms.flatten(),
|
|
267
|
+
grouping_strategy="wires",
|
|
268
|
+
),
|
|
158
269
|
}
|
|
159
270
|
|
|
160
|
-
def _generate_circuits(self):
|
|
271
|
+
def _generate_circuits(self) -> list[Circuit]:
|
|
272
|
+
"""Generate the circuits for the VQE problem.
|
|
273
|
+
|
|
274
|
+
Generates circuits for each parameter set in the current parameters.
|
|
275
|
+
Each circuit is tagged with its parameter index for result processing.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
list[Circuit]: List of Circuit objects for execution.
|
|
161
279
|
"""
|
|
162
|
-
|
|
280
|
+
circuit_type = (
|
|
281
|
+
"cost_circuit" if not self._is_compute_probabilites else "meas_circuit"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return [
|
|
285
|
+
self._meta_circuits[circuit_type].initialize_circuit_from_params(
|
|
286
|
+
params_group, tag_prefix=f"{p}"
|
|
287
|
+
)
|
|
288
|
+
for p, params_group in enumerate(self._curr_params)
|
|
289
|
+
]
|
|
163
290
|
|
|
164
|
-
|
|
165
|
-
|
|
291
|
+
def _post_process_results(self, results, **kwargs):
|
|
292
|
+
"""Post-process the results of the VQE problem.
|
|
166
293
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
294
|
+
Args:
|
|
295
|
+
results (dict[str, dict[str, int]]): The shot histograms of the quantum execution step.
|
|
296
|
+
**kwargs: Additional keyword arguments.
|
|
297
|
+
ham_ops (str): The Hamiltonian operators to measure, semicolon-separated.
|
|
298
|
+
Only needed when the backend supports expval.
|
|
171
299
|
"""
|
|
300
|
+
if self._is_compute_probabilites:
|
|
301
|
+
return self._process_probability_results(results)
|
|
302
|
+
|
|
303
|
+
return super()._post_process_results(results, **kwargs)
|
|
172
304
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
].initialize_circuit_from_params(params_group, tag_prefix=f"{p}")
|
|
305
|
+
def _perform_final_computation(self, **kwargs):
|
|
306
|
+
"""Extract the eigenstate corresponding to the lowest energy found."""
|
|
307
|
+
self.reporter.info(message="🏁 Computing Final Eigenstate 🏁\r")
|
|
177
308
|
|
|
178
|
-
|
|
309
|
+
self._run_solution_measurement()
|
|
179
310
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
311
|
+
if self._best_probs:
|
|
312
|
+
best_measurement_probs = next(iter(self._best_probs.values()))
|
|
313
|
+
eigenstate_bitstring = max(
|
|
314
|
+
best_measurement_probs, key=best_measurement_probs.get
|
|
184
315
|
)
|
|
316
|
+
self._eigenstate = np.fromiter(eigenstate_bitstring, dtype=np.int32)
|
|
317
|
+
|
|
318
|
+
self.reporter.info(message="🏁 Computed Final Eigenstate! 🏁\r\n")
|
|
185
319
|
|
|
186
|
-
return
|
|
320
|
+
return self._total_circuit_count, self._total_run_time
|