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.

@@ -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
- hamiltonain (pennylane.operation.Operator, optional): A Hamiltonian representing the problem.
33
- molecule (pennylane.qchem.Molecule, optional): The molecule representing the problem.
34
- n_electrons (int, optional): Number of electrons associated with the Hamiltonian.
35
- Only needs to be provided when a Hamiltonian is given.
36
- ansatz (Ansatz): The ansatz to use for the VQE problem
37
- optimizer (Optimizers): The optimizer to use.
38
- max_iterations (int): Maximum number of iteration optimizers.
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.cost_hamiltonian = self._clean_hamiltonian(hamiltonian)
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
- Extracts the scalar from the Hamiltonian, and stores it in
95
- 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.
96
165
 
97
166
  Returns:
98
- The Hamiltonian without the scalar component.
99
- """
167
+ qml.operation.Operator: The Hamiltonian without the constant (identity) component.
100
168
 
101
- constant_terms_idx = list(
102
- filter(
103
- lambda x: all(
104
- isinstance(term, qml.I) for term in hamiltonian[x].terms()[1]
105
- ),
106
- range(len(hamiltonian)),
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
- self.loss_constant = float(
111
- sum(map(lambda x: hamiltonian[x].scalar, constant_terms_idx))
112
- )
178
+ loss_constant = 0.0
179
+ non_constant_terms = []
113
180
 
114
- for idx in constant_terms_idx:
115
- hamiltonian -= hamiltonian[idx]
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
- return hamiltonian.simplify()
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
- 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
+
133
234
  Args:
134
- ansatz (Ansatze): The ansatz to use
135
- hamiltonian (qml.Hamiltonian): The Hamiltonian to use
136
- 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.
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.cost_hamiltonian, weights_syms
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
- Generate the circuits for the VQE problem.
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
- In this method, we generate bulk circuits based on the selected parameters.
165
- We generate circuits for each bond length and each ansatz and optimization choice.
291
+ def _post_process_results(self, results, **kwargs):
292
+ """Post-process the results of the VQE problem.
166
293
 
167
- The structure of the circuits is as follows:
168
- - For each bond length:
169
- - For each ansatz:
170
- - Generate the circuit
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
- for p, params_group in enumerate(self._curr_params):
174
- circuit = self._meta_circuits[
175
- "cost_circuit"
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
- self.circuits.append(circuit)
309
+ self._run_solution_measurement()
179
310
 
180
- def _run_optimization_circuits(self, store_data, data_file):
181
- if self.cost_hamiltonian is None or len(self.cost_hamiltonian) == 0:
182
- raise RuntimeError(
183
- "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
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 super()._run_optimization_circuits(store_data, data_file)
320
+ return self._total_circuit_count, self._total_run_time