qoro-divi 0.3.5__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 +21 -0
- divi/backends/_parallel_simulator.py +14 -0
- divi/backends/_qoro_service.py +232 -70
- divi/backends/_qpu_system.py +77 -3
- divi/circuits/_core.py +24 -5
- divi/circuits/qasm.py +1 -3
- divi/extern/cirq/_validator.py +12 -3
- divi/qprog/__init__.py +1 -0
- divi/qprog/algorithms/_ansatze.py +20 -16
- divi/qprog/algorithms/_qaoa.py +152 -111
- divi/qprog/algorithms/_vqe.py +170 -79
- divi/qprog/batch.py +34 -1
- divi/qprog/optimizers.py +133 -50
- divi/qprog/quantum_program.py +131 -633
- divi/qprog/variational_quantum_algorithm.py +786 -0
- divi/qprog/workflows/_graph_partitioning.py +42 -6
- divi/qprog/workflows/_qubo_partitioning.py +1 -1
- divi/qprog/workflows/_vqe_sweep.py +40 -33
- divi/reporting/_reporter.py +3 -6
- divi/utils.py +65 -0
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/METADATA +15 -1
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/RECORD +27 -26
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.5.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,24 +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
|
-
hamiltonian (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
n_layers (int, optional): Number of ansatz layers. Defaults to 1.
|
|
39
|
-
ansatz (Ansatz, optional): The ansatz to use for the VQE problem.
|
|
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.
|
|
40
62
|
Defaults to HartreeFockAnsatz.
|
|
41
|
-
optimizer (Optimizer
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Defaults to 10.
|
|
45
|
-
**kwargs: Additional keyword arguments passed to the parent QuantumProgram.
|
|
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.
|
|
46
66
|
"""
|
|
47
67
|
|
|
48
68
|
# Local Variables
|
|
@@ -54,6 +74,8 @@ class VQE(QuantumProgram):
|
|
|
54
74
|
|
|
55
75
|
self.optimizer = optimizer if optimizer is not None else MonteCarloOptimizer()
|
|
56
76
|
|
|
77
|
+
self._eigenstate = None
|
|
78
|
+
|
|
57
79
|
self._process_problem_input(
|
|
58
80
|
hamiltonian=hamiltonian, molecule=molecule, n_electrons=n_electrons
|
|
59
81
|
)
|
|
@@ -62,10 +84,14 @@ class VQE(QuantumProgram):
|
|
|
62
84
|
|
|
63
85
|
self._meta_circuits = self._create_meta_circuits_dict()
|
|
64
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
|
+
|
|
65
92
|
@property
|
|
66
93
|
def n_params(self):
|
|
67
|
-
"""
|
|
68
|
-
Get the total number of parameters for the VQE ansatz.
|
|
94
|
+
"""Get the total number of parameters for the VQE ansatz.
|
|
69
95
|
|
|
70
96
|
Returns:
|
|
71
97
|
int: Total number of parameters (n_params_per_layer * n_layers).
|
|
@@ -75,9 +101,18 @@ class VQE(QuantumProgram):
|
|
|
75
101
|
* self.n_layers
|
|
76
102
|
)
|
|
77
103
|
|
|
78
|
-
|
|
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.
|
|
79
111
|
"""
|
|
80
|
-
|
|
112
|
+
return self._eigenstate
|
|
113
|
+
|
|
114
|
+
def _process_problem_input(self, hamiltonian, molecule, n_electrons):
|
|
115
|
+
"""Process and validate the VQE problem input.
|
|
81
116
|
|
|
82
117
|
Handles both Hamiltonian-based and molecule-based problem specifications,
|
|
83
118
|
extracting the necessary information (n_qubits, n_electrons, hamiltonian).
|
|
@@ -113,38 +148,76 @@ class VQE(QuantumProgram):
|
|
|
113
148
|
UserWarning,
|
|
114
149
|
)
|
|
115
150
|
|
|
116
|
-
self.
|
|
151
|
+
self._cost_hamiltonian = self._clean_hamiltonian(hamiltonian)
|
|
117
152
|
|
|
118
153
|
def _clean_hamiltonian(
|
|
119
154
|
self, hamiltonian: qml.operation.Operator
|
|
120
155
|
) -> qml.operation.Operator:
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
124
165
|
|
|
125
166
|
Returns:
|
|
126
|
-
The Hamiltonian without the
|
|
127
|
-
"""
|
|
167
|
+
qml.operation.Operator: The Hamiltonian without the constant (identity) component.
|
|
128
168
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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]
|
|
136
176
|
)
|
|
137
177
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
loss_constant = 0.0
|
|
179
|
+
non_constant_terms = []
|
|
180
|
+
|
|
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
|
|
187
|
+
|
|
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)
|
|
141
201
|
|
|
142
|
-
|
|
143
|
-
hamiltonian -= hamiltonian[idx]
|
|
202
|
+
self.loss_constant = float(loss_constant)
|
|
144
203
|
|
|
145
|
-
|
|
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()
|
|
146
214
|
|
|
147
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
|
+
"""
|
|
148
221
|
weights_syms = sp.symarray(
|
|
149
222
|
"w",
|
|
150
223
|
(
|
|
@@ -155,13 +228,13 @@ class VQE(QuantumProgram):
|
|
|
155
228
|
),
|
|
156
229
|
)
|
|
157
230
|
|
|
158
|
-
def _prepare_circuit(hamiltonian, params):
|
|
159
|
-
"""
|
|
160
|
-
|
|
231
|
+
def _prepare_circuit(hamiltonian, params, final_measurement=False):
|
|
232
|
+
"""Prepare the circuit for the VQE problem.
|
|
233
|
+
|
|
161
234
|
Args:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
235
|
+
hamiltonian: The Hamiltonian to measure.
|
|
236
|
+
params: The parameters for the ansatz.
|
|
237
|
+
final_measurement (bool): Whether to perform final measurement.
|
|
165
238
|
"""
|
|
166
239
|
self.ansatz.build(
|
|
167
240
|
params,
|
|
@@ -170,6 +243,9 @@ class VQE(QuantumProgram):
|
|
|
170
243
|
n_electrons=self.n_electrons,
|
|
171
244
|
)
|
|
172
245
|
|
|
246
|
+
if final_measurement:
|
|
247
|
+
return qml.probs()
|
|
248
|
+
|
|
173
249
|
# Even though in principle we want to sample from a state,
|
|
174
250
|
# we are applying an `expval` operation here to make it compatible
|
|
175
251
|
# with the pennylane transforms down the line, which complain about
|
|
@@ -179,51 +255,66 @@ class VQE(QuantumProgram):
|
|
|
179
255
|
return {
|
|
180
256
|
"cost_circuit": self._meta_circuit_factory(
|
|
181
257
|
qml.tape.make_qscript(_prepare_circuit)(
|
|
182
|
-
self.
|
|
258
|
+
self._cost_hamiltonian, weights_syms, final_measurement=False
|
|
183
259
|
),
|
|
184
260
|
symbols=weights_syms.flatten(),
|
|
185
|
-
)
|
|
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
|
+
),
|
|
186
269
|
}
|
|
187
270
|
|
|
188
|
-
def _generate_circuits(self):
|
|
189
|
-
"""
|
|
190
|
-
Generate the circuits for the VQE problem.
|
|
271
|
+
def _generate_circuits(self) -> list[Circuit]:
|
|
272
|
+
"""Generate the circuits for the VQE problem.
|
|
191
273
|
|
|
192
|
-
|
|
193
|
-
|
|
274
|
+
Generates circuits for each parameter set in the current parameters.
|
|
275
|
+
Each circuit is tagged with its parameter index for result processing.
|
|
194
276
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
- For each ansatz:
|
|
198
|
-
- Generate the circuit
|
|
277
|
+
Returns:
|
|
278
|
+
list[Circuit]: List of Circuit objects for execution.
|
|
199
279
|
"""
|
|
280
|
+
circuit_type = (
|
|
281
|
+
"cost_circuit" if not self._is_compute_probabilites else "meas_circuit"
|
|
282
|
+
)
|
|
200
283
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"
|
|
204
|
-
|
|
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
|
+
]
|
|
205
290
|
|
|
206
|
-
|
|
291
|
+
def _post_process_results(self, results, **kwargs):
|
|
292
|
+
"""Post-process the results of the VQE problem.
|
|
207
293
|
|
|
208
|
-
|
|
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.
|
|
209
299
|
"""
|
|
210
|
-
|
|
300
|
+
if self._is_compute_probabilites:
|
|
301
|
+
return self._process_probability_results(results)
|
|
211
302
|
|
|
212
|
-
|
|
303
|
+
return super()._post_process_results(results, **kwargs)
|
|
213
304
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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")
|
|
217
308
|
|
|
218
|
-
|
|
219
|
-
dict: Loss values for each parameter set.
|
|
309
|
+
self._run_solution_measurement()
|
|
220
310
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
raise RuntimeError(
|
|
226
|
-
"Hamiltonian operators must be generated before running the VQE"
|
|
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
|
|
227
315
|
)
|
|
316
|
+
self._eigenstate = np.fromiter(eigenstate_bitstring, dtype=np.int32)
|
|
317
|
+
|
|
318
|
+
self.reporter.info(message="🏁 Computed Final Eigenstate! 🏁\r\n")
|
|
228
319
|
|
|
229
|
-
return
|
|
320
|
+
return self._total_circuit_count, self._total_run_time
|
divi/qprog/batch.py
CHANGED
|
@@ -139,6 +139,39 @@ class ProgramBatch(ABC):
|
|
|
139
139
|
|
|
140
140
|
@abstractmethod
|
|
141
141
|
def create_programs(self):
|
|
142
|
+
"""Generate and populate the programs dictionary for batch execution.
|
|
143
|
+
|
|
144
|
+
This method must be implemented by subclasses to create the quantum programs
|
|
145
|
+
that will be executed as part of the batch. The method operates via side effects:
|
|
146
|
+
it populates `self._programs` (or `self.programs`) with a dictionary mapping
|
|
147
|
+
program identifiers to `QuantumProgram` instances.
|
|
148
|
+
|
|
149
|
+
Implementation Notes:
|
|
150
|
+
- Subclasses should call `super().create_programs()` first to initialize
|
|
151
|
+
internal state (queue, events, etc.) and validate that no programs
|
|
152
|
+
already exist.
|
|
153
|
+
- After calling super(), subclasses should populate `self.programs` or
|
|
154
|
+
`self._programs` with their program instances.
|
|
155
|
+
- Program identifiers can be any hashable type (e.g., strings, tuples).
|
|
156
|
+
Common patterns include strings like "prog1", "prog2" or tuples like
|
|
157
|
+
('A', 5) for partitioned problems.
|
|
158
|
+
|
|
159
|
+
Side Effects:
|
|
160
|
+
- Populates `self._programs` with program instances.
|
|
161
|
+
- Initializes `self._queue` for progress reporting.
|
|
162
|
+
- Initializes `self._done_event` if `max_iterations` attribute exists.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
RuntimeError: If programs already exist (should call `reset()` first).
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
>>> def create_programs(self):
|
|
169
|
+
... super().create_programs()
|
|
170
|
+
... self.programs = {
|
|
171
|
+
... "prog1": QAOA(...),
|
|
172
|
+
... "prog2": QAOA(...),
|
|
173
|
+
... }
|
|
174
|
+
"""
|
|
142
175
|
if len(self._programs) > 0:
|
|
143
176
|
raise RuntimeError(
|
|
144
177
|
"Some programs already exist. "
|
|
@@ -468,7 +501,7 @@ class ProgramBatch(ABC):
|
|
|
468
501
|
if self._executor is not None:
|
|
469
502
|
self.join()
|
|
470
503
|
|
|
471
|
-
if any(len(program.
|
|
504
|
+
if any(len(program.losses_history) == 0 for program in self._programs.values()):
|
|
472
505
|
raise RuntimeError(
|
|
473
506
|
"Some/All programs have empty losses. Did you call run()?"
|
|
474
507
|
)
|