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.

@@ -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 (pennylane.operation.Operator, optional): A Hamiltonian
33
- representing the problem.
34
- molecule (pennylane.qchem.Molecule, optional): The molecule representing
35
- the problem.
36
- n_electrons (int, optional): Number of electrons associated with the
37
- Hamiltonian. Only needs to be provided when a Hamiltonian is given.
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, optional): The optimizer to use. Defaults to
42
- MonteCarloOptimizer.
43
- max_iterations (int, optional): Maximum number of optimization iterations.
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
- def _process_problem_input(self, hamiltonian, molecule, n_electrons):
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
- Process and validate the VQE problem input.
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.cost_hamiltonian = self._clean_hamiltonian(hamiltonian)
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
- Extracts the scalar from the Hamiltonian, and stores it in
123
- the `loss_constant` variable.
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 scalar component.
127
- """
167
+ qml.operation.Operator: The Hamiltonian without the constant (identity) component.
128
168
 
129
- constant_terms_idx = list(
130
- filter(
131
- lambda x: all(
132
- isinstance(term, qml.I) for term in hamiltonian[x].terms()[1]
133
- ),
134
- range(len(hamiltonian)),
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
- self.loss_constant = float(
139
- sum(map(lambda x: hamiltonian[x].scalar, constant_terms_idx))
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
- for idx in constant_terms_idx:
143
- hamiltonian -= hamiltonian[idx]
202
+ self.loss_constant = float(loss_constant)
144
203
 
145
- return hamiltonian.simplify()
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
- Prepare the circuit for the VQE problem.
231
+ def _prepare_circuit(hamiltonian, params, final_measurement=False):
232
+ """Prepare the circuit for the VQE problem.
233
+
161
234
  Args:
162
- ansatz (Ansatze): The ansatz to use
163
- hamiltonian (qml.Hamiltonian): The Hamiltonian to use
164
- params (list): The parameters to use for the ansatz
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.cost_hamiltonian, weights_syms
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
- In this method, we generate bulk circuits based on the selected parameters.
193
- We generate circuits for each bond length and each ansatz and optimization choice.
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
- The structure of the circuits is as follows:
196
- - For each bond length:
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
- for p, params_group in enumerate(self._curr_params):
202
- circuit = self._meta_circuits[
203
- "cost_circuit"
204
- ].initialize_circuit_from_params(params_group, tag_prefix=f"{p}")
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
- self._circuits.append(circuit)
291
+ def _post_process_results(self, results, **kwargs):
292
+ """Post-process the results of the VQE problem.
207
293
 
208
- def _run_optimization_circuits(self, store_data, data_file):
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
- Execute the circuits for the current optimization iteration.
300
+ if self._is_compute_probabilites:
301
+ return self._process_probability_results(results)
211
302
 
212
- Validates that the Hamiltonian is properly set before running circuits.
303
+ return super()._post_process_results(results, **kwargs)
213
304
 
214
- Args:
215
- store_data (bool): Whether to save iteration data.
216
- data_file (str): Path to file for saving data.
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
- Returns:
219
- dict: Loss values for each parameter set.
309
+ self._run_solution_measurement()
220
310
 
221
- Raises:
222
- RuntimeError: If the cost Hamiltonian is not set or empty.
223
- """
224
- if self.cost_hamiltonian is None or len(self.cost_hamiltonian) == 0:
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 super()._run_optimization_circuits(store_data, data_file)
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.losses) == 0 for program in self._programs.values()):
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
  )