qoro-divi 0.2.2b1__py3-none-any.whl → 0.3.1b0__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/qprog/_vqe.py CHANGED
@@ -3,8 +3,8 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  from enum import Enum
6
+ from warnings import warn
6
7
 
7
- import numpy as np
8
8
  import pennylane as qml
9
9
  import sympy as sp
10
10
 
@@ -45,10 +45,9 @@ class VQEAnsatz(Enum):
45
45
  class VQE(QuantumProgram):
46
46
  def __init__(
47
47
  self,
48
- symbols,
49
- bond_length: float,
50
- coordinate_structure: list[tuple[float, float, float]],
51
- charge: float = 0,
48
+ hamiltonian: qml.operation.Operator | None = None,
49
+ molecule: qml.qchem.Molecule | None = None,
50
+ n_electrons: int | None = None,
52
51
  n_layers: int = 1,
53
52
  ansatz=VQEAnsatz.HARTREE_FOCK,
54
53
  optimizer=Optimizer.MONTE_CARLO,
@@ -59,25 +58,16 @@ class VQE(QuantumProgram):
59
58
  Initialize the VQE problem.
60
59
 
61
60
  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.
61
+ hamiltonain (pennylane.operation.Operator, optional): A Hamiltonian representing the problem.
62
+ molecule (pennylane.qchem.Molecule, optional): The molecule representing the problem.
63
+ n_electrons (int, optional): Number of electrons associated with the Hamiltonian.
64
+ Only needs to be provided when a Hamiltonian is given.
66
65
  ansatz (VQEAnsatz): The ansatz to use for the VQE problem
67
66
  optimizer (Optimizers): The optimizer to use.
68
67
  max_iterations (int): Maximum number of iteration optimizers.
69
68
  """
70
69
 
71
70
  # 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
71
  self.n_layers = n_layers
82
72
  self.results = {}
83
73
  self.ansatz = ansatz
@@ -85,40 +75,59 @@ class VQE(QuantumProgram):
85
75
  self.max_iterations = max_iterations
86
76
  self.current_iteration = 0
87
77
 
88
- self.cost_hamiltonian = self._generate_hamiltonian_operations()
78
+ self._process_problem_input(
79
+ hamiltonian=hamiltonian, molecule=molecule, n_electrons=n_electrons
80
+ )
89
81
 
90
82
  super().__init__(**kwargs)
91
83
 
92
84
  self._meta_circuits = self._create_meta_circuits_dict()
93
85
 
94
- def _generate_hamiltonian_operations(self) -> qml.operation.Operator:
95
- """
96
- Generate the Hamiltonian operators for the given bond length.
86
+ def _process_problem_input(self, hamiltonian, molecule, n_electrons):
87
+ if hamiltonian is None and molecule is None:
88
+ raise ValueError(
89
+ "Either one of `molecule` and `hamiltonian` must be provided."
90
+ )
97
91
 
98
- Returns:
99
- The Hamiltonian corresponding to the VQE problem.
100
- """
92
+ if hamiltonian is not None:
93
+ if not isinstance(n_electrons, int) or n_electrons < 0:
94
+ raise ValueError(
95
+ f"`n_electrons` is expected to be a non-negative integer. Got {n_electrons}."
96
+ )
101
97
 
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
- ]
98
+ self.n_electrons = n_electrons
99
+ self.n_qubits = len(hamiltonian.wires)
110
100
 
111
- coordinates = np.array(coordinates)
112
- molecule = qml.qchem.Molecule(self.symbols, coordinates, charge=self.charge)
113
- hamiltonian, qubits = qml.qchem.molecular_hamiltonian(molecule)
101
+ if molecule is not None:
102
+ self.molecule = molecule
103
+ hamiltonian, self.n_qubits = qml.qchem.molecular_hamiltonian(molecule)
104
+ self.n_electrons = molecule.n_electrons
114
105
 
115
- self.n_qubits = qubits
116
- self.n_electrons = molecule.n_electrons
106
+ if (n_electrons is not None) and self.n_electrons != n_electrons:
107
+ warn(
108
+ "`n_electrons` is provided but not consistent with the molecule's. "
109
+ f"Got {n_electrons}, but molecule has {self.n_electrons}. "
110
+ "The molecular value will be used.",
111
+ UserWarning,
112
+ )
117
113
 
118
114
  self.n_params = self.ansatz.n_params(
119
115
  self.n_qubits, n_electrons=self.n_electrons
120
116
  )
121
117
 
118
+ self.cost_hamiltonian = self._clean_hamiltonian(hamiltonian)
119
+
120
+ def _clean_hamiltonian(
121
+ self, hamiltonian: qml.operation.Operator
122
+ ) -> qml.operation.Operator:
123
+ """
124
+ Extracts the scalar from the Hamiltonian, and stores it in
125
+ the `loss_constant` variable.
126
+
127
+ Returns:
128
+ The Hamiltonian without the scalar component.
129
+ """
130
+
122
131
  constant_terms_idx = list(
123
132
  filter(
124
133
  lambda x: all(
@@ -129,7 +138,7 @@ class VQE(QuantumProgram):
129
138
  )
130
139
 
131
140
  self.loss_constant = sum(
132
- map(lambda x: hamiltonian[x].scalar.item(), constant_terms_idx)
141
+ map(lambda x: hamiltonian[x].scalar, constant_terms_idx)
133
142
  )
134
143
 
135
144
  for idx in constant_terms_idx:
divi/qprog/_vqe_sweep.py CHANGED
@@ -2,17 +2,390 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ import inspect
6
+ from collections import deque
7
+ from collections.abc import Sequence
8
+ from dataclasses import dataclass
5
9
  from functools import partial
6
10
  from itertools import product
7
- from typing import Literal
11
+ from typing import Literal, NamedTuple
8
12
 
9
13
  import matplotlib.pyplot as plt
14
+ import numpy as np
15
+ import pennylane as qml
10
16
 
11
17
  from divi.qprog import VQE, ProgramBatch, VQEAnsatz
12
18
 
13
19
  from .optimizers import Optimizer
14
20
 
15
21
 
22
+ def _ctor_attrs(obj):
23
+ sig = inspect.signature(obj.__class__.__init__)
24
+ arg_names = list(sig.parameters.keys())[1:] # skip 'self'
25
+ return {name: getattr(obj, name) for name in arg_names if hasattr(obj, name)}
26
+
27
+
28
+ class _ZMatrixEntry(NamedTuple):
29
+ bond_ref: int | None
30
+ angle_ref: int | None
31
+ dihedral_ref: int | None
32
+ bond_length: float | None
33
+ angle: float | None
34
+ dihedral: float | None
35
+
36
+
37
+ # --- Helper functions ---
38
+ def _safe_normalize(v, fallback=None):
39
+ norm = np.linalg.norm(v)
40
+ if norm < 1e-6:
41
+ if fallback is None:
42
+ fallback = np.array([1.0, 0.0, 0.0])
43
+ return fallback / np.linalg.norm(fallback)
44
+ return v / norm
45
+
46
+
47
+ def _compute_angle(v1, v2):
48
+ dot = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
49
+ return np.degrees(np.arccos(np.clip(dot, -1.0, 1.0)))
50
+
51
+
52
+ def _compute_dihedral(b0, b1, b2):
53
+ n1 = np.cross(b0, b1)
54
+ n2 = np.cross(b0, b2)
55
+ if np.linalg.norm(n1) < 1e-6 or np.linalg.norm(n2) < 1e-6:
56
+ return 0.0
57
+ dot = np.dot(n1, n2) / (np.linalg.norm(n1) * np.linalg.norm(n2))
58
+ angle = np.degrees(np.arccos(np.clip(dot, -1.0, 1.0)))
59
+ if np.dot(b1, n2) < 0:
60
+ angle *= -1
61
+ return angle
62
+
63
+
64
+ def _find_refs(adj, placed, parent, child):
65
+ gp = next((n for n in adj[parent] if n != child and n in placed), -1)
66
+ ggp = -1
67
+ if gp != -1:
68
+ ggp = next((n for n in adj[gp] if n != parent and n in placed), -1)
69
+ return gp if gp != -1 else None, ggp if ggp != -1 else None
70
+
71
+
72
+ # --- Main functions ---
73
+ def _cartesian_to_zmatrix(
74
+ coords: np.ndarray, connectivity: list[tuple[int, int]]
75
+ ) -> list[_ZMatrixEntry]:
76
+ num_atoms = len(coords)
77
+ if num_atoms == 0:
78
+ raise ValueError(
79
+ "Cannot convert empty coordinate array to Z-matrix: molecule must have at least one atom."
80
+ )
81
+
82
+ adj = [[] for _ in range(num_atoms)]
83
+ for i, j in connectivity:
84
+ adj[i].append(j)
85
+ adj[j].append(i)
86
+
87
+ zmatrix_entries = {0: _ZMatrixEntry(None, None, None, None, None, None)}
88
+ q = deque([0])
89
+ placed_atoms = {0}
90
+
91
+ while q:
92
+ parent_idx = q.popleft()
93
+ for child_idx in adj[parent_idx]:
94
+ if child_idx in placed_atoms:
95
+ continue
96
+ placed_atoms.add(child_idx)
97
+ q.append(child_idx)
98
+
99
+ bond_len = np.linalg.norm(coords[child_idx] - coords[parent_idx])
100
+ gp, ggp = _find_refs(adj, placed_atoms, parent_idx, child_idx)
101
+
102
+ angle = None
103
+ if gp is not None:
104
+ angle = _compute_angle(
105
+ coords[child_idx] - coords[parent_idx],
106
+ coords[gp] - coords[parent_idx],
107
+ )
108
+
109
+ dihedral = None
110
+ if gp is not None and ggp is not None:
111
+ dihedral = _compute_dihedral(
112
+ coords[parent_idx] - coords[gp],
113
+ coords[child_idx] - coords[parent_idx],
114
+ coords[ggp] - coords[gp],
115
+ )
116
+
117
+ zmatrix_entries[child_idx] = _ZMatrixEntry(
118
+ parent_idx, gp, ggp, bond_len, angle, dihedral
119
+ )
120
+
121
+ return [zmatrix_entries[i] for i in range(num_atoms)]
122
+
123
+
124
+ def _zmatrix_to_cartesian(z_matrix: list[_ZMatrixEntry]) -> np.ndarray:
125
+ n_atoms = len(z_matrix)
126
+ coords = np.zeros((n_atoms, 3))
127
+
128
+ if n_atoms == 0:
129
+ return coords
130
+
131
+ # --- First atom at origin ---
132
+ coords[0] = np.array([0.0, 0.0, 0.0])
133
+
134
+ # --- Second atom along +X axis ---
135
+ if n_atoms > 1:
136
+ coords[1] = np.array([z_matrix[1].bond_length, 0.0, 0.0])
137
+
138
+ # --- Third atom in XY plane ---
139
+ if n_atoms > 2:
140
+ entry = z_matrix[2]
141
+ r = entry.bond_length
142
+ theta = np.radians(entry.angle) if entry.angle is not None else 0.0
143
+
144
+ a1 = coords[entry.bond_ref]
145
+ a2 = coords[entry.angle_ref]
146
+
147
+ v = a2 - a1
148
+ v /= np.linalg.norm(v)
149
+ # fixed perpendicular in XY plane, fallback handled inline
150
+ perp = np.array([-v[1], v[0], 0.0])
151
+ perp /= np.linalg.norm(perp) if np.linalg.norm(perp) > 1e-6 else 1.0
152
+
153
+ coords[2] = a1 + r * (np.cos(theta) * v + np.sin(theta) * perp)
154
+
155
+ for i, entry in enumerate(z_matrix[3:], start=3):
156
+ a1 = coords[entry.bond_ref] if entry.bond_ref is not None else np.zeros(3)
157
+ a2 = coords[entry.angle_ref] if entry.angle_ref is not None else np.zeros(3)
158
+ a3 = (
159
+ coords[entry.dihedral_ref]
160
+ if entry.dihedral_ref is not None
161
+ else np.zeros(3)
162
+ )
163
+
164
+ r = entry.bond_length
165
+
166
+ theta = np.radians(entry.angle) if entry.angle is not None else 0.0
167
+ phi = np.radians(entry.dihedral) if entry.dihedral is not None else 0.0
168
+
169
+ b1 = _safe_normalize(a1 - a2, fallback=np.array([1.0, 0.0, 0.0]))
170
+ b2 = a3 - a2
171
+ n = _safe_normalize(np.cross(b1, b2), fallback=np.array([0.0, 0.0, 1.0]))
172
+ nc = np.cross(n, b1)
173
+
174
+ coords[i] = a1 + r * (
175
+ -np.cos(theta) * b1 + np.sin(theta) * (np.cos(phi) * nc + np.sin(phi) * n)
176
+ )
177
+
178
+ return coords
179
+
180
+
181
+ def _transform_bonds(
182
+ zmatrix: list[_ZMatrixEntry],
183
+ bonds_to_transform: list[tuple[int, int]],
184
+ value: float,
185
+ transform_type: Literal["scale", "delta"] = "scale",
186
+ ) -> list[_ZMatrixEntry]:
187
+ """
188
+ Transform specified bonds in a Z-matrix.
189
+
190
+ Args:
191
+ zmatrix: List of _ZMatrixEntry.
192
+ bonds_to_transform: List of (atom1, atom2) tuples specifying bonds.
193
+ value: Multiplier or additive value.
194
+ transform_type: "scale" or "add".
195
+
196
+ Returns:
197
+ New Z-matrix with transformed bond lengths.
198
+ """
199
+ # Convert to set of sorted tuples for quick lookup
200
+ bonds_set = {tuple(sorted(b)) for b in bonds_to_transform}
201
+
202
+ new_zmatrix = []
203
+ for i, entry in enumerate(zmatrix):
204
+ if (
205
+ entry.bond_ref is not None
206
+ and tuple(sorted((i, entry.bond_ref))) in bonds_set
207
+ ):
208
+ old_length = entry.bond_length
209
+ new_length = (
210
+ old_length * value if transform_type == "scale" else old_length + value
211
+ )
212
+ if new_length == 0.0:
213
+ raise RuntimeError(
214
+ "New bond length can't be zero after transformation."
215
+ )
216
+ new_zmatrix.append(entry._replace(bond_length=new_length))
217
+ else:
218
+ new_zmatrix.append(entry)
219
+ return new_zmatrix
220
+
221
+
222
+ def _kabsch_align(P_in: np.ndarray, Q_in: np.ndarray, reference_atoms_idx=slice(None)):
223
+ """
224
+ Align point set P onto Q using the Kabsch algorithm.
225
+
226
+ Parameters
227
+ ----------
228
+ P : (N, D) ndarray. Source coordinates.
229
+ Q : (N, D) ndarray. Target coordinates.
230
+
231
+ Returns
232
+ -------
233
+ P_aligned : (N, D) ndarray
234
+ P rotated and translated onto Q.
235
+ """
236
+
237
+ P = P_in[reference_atoms_idx, :]
238
+ Q = Q_in[reference_atoms_idx, :]
239
+
240
+ P = np.asarray(P, dtype=float)
241
+ Q = np.asarray(Q, dtype=float)
242
+
243
+ # Centroids
244
+ Pc = np.mean(P, axis=0)
245
+ Qc = np.mean(Q, axis=0)
246
+
247
+ # Centered coordinates
248
+ P_centered = P - Pc
249
+ Q_centered = Q - Qc
250
+
251
+ # Covariance and SVD
252
+ H = P_centered.T @ Q_centered
253
+ U, _, Vt = np.linalg.svd(H)
254
+
255
+ # Reflection check
256
+ d = np.sign(np.linalg.det(Vt.T @ U.T))
257
+ D = np.diag([1] * (P.shape[1] - 1) + [d])
258
+
259
+ # Optimal rotation and translation
260
+ R = Vt.T @ D @ U.T
261
+ t = Qc - Pc @ R
262
+
263
+ # Apply transformation
264
+ P_aligned = P_in @ R + t
265
+
266
+ P_aligned[np.abs(P_aligned) < 1e-12] = 0.0
267
+
268
+ return P_aligned
269
+
270
+
271
+ @dataclass(frozen=True, eq=True)
272
+ class MoleculeTransformer:
273
+ """
274
+ base_molecule: qml.qchem.Molecule
275
+ The reference molecule used as a template for generating variants.
276
+ bond_modifiers: Sequence[float]
277
+ A list of values used to adjust bond lengths. The class will generate
278
+ **one new molecule for each modifier** in this list. The modification
279
+ mode is detected automatically:
280
+ - **Scale mode**: If all values are positive, they are used as scaling
281
+ factors (e.g., 1.1 for a 10% increase).
282
+ - **Delta mode**: If any value is zero or negative, all values are
283
+ treated as additive changes to the bond length, in Ångstroms.
284
+ atom_connectivity: Sequence[tuple[int, int]] | None
285
+ A sequence of atom index pairs specifying the bonds in the molecule.
286
+ If not provided, a chain structure will be assumed
287
+ e.g.: `[(0, 1), (1, 2), (2, 3), ...]`.
288
+ bonds_to_transform: Sequence[tuple[int, int]] | None
289
+ A subset of `atom_connectivity` that specifies the bonds to modify.
290
+ If None, all bonds will be transformed.
291
+ alignment_atoms: Sequence[int] | None
292
+ Indices of atoms onto which to align the orientation of the resulting
293
+ variants of the molecule. Only useful for visualization and debuggin.
294
+ If None, no alignment is carried out.
295
+ """
296
+
297
+ base_molecule: qml.qchem.Molecule
298
+ bond_modifiers: Sequence[float]
299
+ atom_connectivity: Sequence[tuple[int, int]] | None = None
300
+ bonds_to_transform: Sequence[tuple[int, int]] | None = None
301
+ alignment_atoms: Sequence[int] | None = None
302
+
303
+ def __post_init__(self):
304
+ if not isinstance(self.base_molecule, qml.qchem.Molecule):
305
+ raise ValueError(
306
+ "`base_molecule` is expected to be a Pennylane `Molecule` instance."
307
+ )
308
+
309
+ if not all(isinstance(x, (float, int)) for x in self.bond_modifiers):
310
+ raise ValueError("`bond_modifiers` should be a sequence of floats.")
311
+ if len(set(self.bond_modifiers)) < len(self.bond_modifiers):
312
+ raise ValueError("`bond_modifiers` contains duplicate values.")
313
+ object.__setattr__(
314
+ self,
315
+ "_mode",
316
+ "scale" if all(v > 0 for v in self.bond_modifiers) else "delta",
317
+ )
318
+
319
+ n_symbols = len(self.base_molecule.symbols)
320
+ if self.atom_connectivity is None:
321
+ object.__setattr__(
322
+ self,
323
+ "atom_connectivity",
324
+ tuple(zip(range(n_symbols), range(1, n_symbols))),
325
+ )
326
+ else:
327
+ if len(set(self.atom_connectivity)) < len(self.atom_connectivity):
328
+ raise ValueError("`atom_connectivity` contains duplicate values.")
329
+
330
+ if not all(
331
+ 0 <= a < n_symbols and 0 <= b < n_symbols
332
+ for a, b in self.atom_connectivity
333
+ ):
334
+ raise ValueError(
335
+ "`atom_connectivity` should be a sequence of tuples of"
336
+ " atom indices in (0, len(molecule.symbols))"
337
+ )
338
+
339
+ if self.bonds_to_transform is None:
340
+ object.__setattr__(self, "bonds_to_transform", self.atom_connectivity)
341
+ else:
342
+ if len(self.bonds_to_transform) == 0:
343
+ raise ValueError("`bonds_to_transform` cannot be empty.")
344
+ if not set(self.bonds_to_transform).issubset(self.atom_connectivity):
345
+ raise ValueError(
346
+ "`bonds_to_transform` is not a subset of `atom_connectivity`"
347
+ )
348
+
349
+ if self.alignment_atoms is not None and not all(
350
+ 0 <= idx < n_symbols for idx in self.alignment_atoms
351
+ ):
352
+ raise ValueError(
353
+ "`alignment_atoms` need to be in range (0, len(molecule.symbols))"
354
+ )
355
+
356
+ def generate(self) -> dict[float, qml.qchem.Molecule]:
357
+ base_attrs = _ctor_attrs(self.base_molecule)
358
+
359
+ variants = {}
360
+ original_coords = self.base_molecule.coordinates
361
+ mode = "scale" if all(v > 0 for v in self.bond_modifiers) else "delta"
362
+
363
+ # Convert to Z-matrix, with connectivity
364
+ z_matrix = _cartesian_to_zmatrix(original_coords, self.atom_connectivity)
365
+
366
+ for value in self.bond_modifiers:
367
+ if (value == 0 and mode == "delta") or (value == 1 and mode == "scale"):
368
+ transformed_coords = original_coords.copy()
369
+ else:
370
+ transformed_z_matrix = _transform_bonds(
371
+ z_matrix, self.bonds_to_transform, value, mode
372
+ )
373
+
374
+ transformed_coords = _zmatrix_to_cartesian(transformed_z_matrix)
375
+
376
+ if self.alignment_atoms is not None:
377
+ transformed_coords = _kabsch_align(
378
+ transformed_coords, original_coords, self.alignment_atoms
379
+ )
380
+
381
+ # A single molecule is created after all bonds have been modified
382
+ base_attrs["coordinates"] = transformed_coords
383
+ mol = qml.qchem.Molecule(**base_attrs)
384
+ variants[value] = mol
385
+
386
+ return variants
387
+
388
+
16
389
  class VQEHyperparameterSweep(ProgramBatch):
17
390
  """Allows user to carry out a grid search across different values
18
391
  for the ansatz and the bond length used in a VQE program.
@@ -20,36 +393,37 @@ class VQEHyperparameterSweep(ProgramBatch):
20
393
 
21
394
  def __init__(
22
395
  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,
396
+ ansatze: Sequence[VQEAnsatz],
397
+ molecule_transformer: MoleculeTransformer,
28
398
  optimizer: Optimizer = Optimizer.MONTE_CARLO,
29
399
  max_iterations: int = 10,
30
400
  **kwargs,
31
401
  ):
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.
402
+ """
403
+ Initialize a VQE hyperparameter sweep.
404
+
405
+ Parameters
406
+ ----------
407
+ ansatze: Sequence[VQEAnsatz]
408
+ A sequence of ansatz circuits to test.
409
+ molecule_transformer: MoleculeTransformer
410
+ A `MoleculeTransformer` object defining the configuration for
411
+ generating the molecule variants.
412
+ optimizer: Optimizer
413
+ The optimization algorithm for the VQE runs.
414
+ max_iterations: int
415
+ The maximum number of optimizer iterations for each VQE run.
416
+ **kwargs: Forwarded to parent class.
41
417
  """
42
418
  super().__init__(backend=kwargs.pop("backend"))
43
419
 
420
+ self.molecule_transformer = molecule_transformer
421
+
44
422
  self.ansatze = ansatze
45
- self.bond_lengths = [round(bnd, 9) for bnd in bond_lengths]
46
423
  self.max_iterations = max_iterations
47
424
 
48
425
  self._constructor = partial(
49
426
  VQE,
50
- symbols=symbols,
51
- coordinate_structure=coordinate_structure,
52
- charge=charge,
53
427
  optimizer=optimizer,
54
428
  max_iterations=self.max_iterations,
55
429
  backend=self.backend,
@@ -57,19 +431,17 @@ class VQEHyperparameterSweep(ProgramBatch):
57
431
  )
58
432
 
59
433
  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
434
  super().create_programs()
67
435
 
68
- for ansatz, bond_length in product(self.ansatze, self.bond_lengths):
69
- _job_id = (ansatz, bond_length)
436
+ self.molecule_variants = self.molecule_transformer.generate()
437
+
438
+ for ansatz, (modifier, molecule) in product(
439
+ self.ansatze, self.molecule_variants.items()
440
+ ):
441
+ _job_id = (ansatz, modifier)
70
442
  self.programs[_job_id] = self._constructor(
71
443
  job_id=_job_id,
72
- bond_length=bond_length,
444
+ molecule=molecule,
73
445
  ansatz=ansatz,
74
446
  losses=self._manager.list(),
75
447
  final_params=self._manager.list(),
@@ -77,16 +449,7 @@ class VQEHyperparameterSweep(ProgramBatch):
77
449
  )
78
450
 
79
451
  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
- )
452
+ super().aggregate_results()
90
453
 
91
454
  all_energies = {key: prog.losses[-1] for key, prog in self.programs.items()}
92
455
 
@@ -105,7 +468,7 @@ class VQEHyperparameterSweep(ProgramBatch):
105
468
  )
106
469
 
107
470
  if self._executor is not None:
108
- self.wait_for_all()
471
+ self.join()
109
472
 
110
473
  data = []
111
474
  colors = ["blue", "g", "r", "c", "m", "y", "k"]
@@ -113,13 +476,13 @@ class VQEHyperparameterSweep(ProgramBatch):
113
476
  ansatz_list = list(VQEAnsatz)
114
477
 
115
478
  if graph_type == "scatter":
116
- for ansatz, bond_length in self.programs.keys():
479
+ for ansatz, modifier in self.programs.keys():
117
480
  min_energies = []
118
481
 
119
- curr_energies = self.programs[(ansatz, bond_length)].losses[-1]
482
+ curr_energies = self.programs[(ansatz, modifier)].losses[-1]
120
483
  min_energies.append(
121
484
  (
122
- bond_length,
485
+ modifier,
123
486
  min(curr_energies.values()),
124
487
  colors[ansatz_list.index(ansatz)],
125
488
  )
@@ -132,13 +495,17 @@ class VQEHyperparameterSweep(ProgramBatch):
132
495
  elif graph_type == "line":
133
496
  for ansatz in self.ansatze:
134
497
  energies = []
135
- for bond_length in self.bond_lengths:
498
+ for modifier in self.molecule_transformer.bond_modifiers:
136
499
  energies.append(
137
- min(self.programs[(ansatz, bond_length)].losses[-1].values())
500
+ min(self.programs[(ansatz, modifier)].losses[-1].values())
138
501
  )
139
- plt.plot(self.bond_lengths, energies, label=ansatz)
502
+ plt.plot(
503
+ self.molecule_transformer.bond_modifiers, energies, label=ansatz
504
+ )
140
505
 
141
- plt.xlabel("Bond length")
506
+ plt.xlabel(
507
+ "Scale Factor" if self.molecule_transformer._mode == "scale" else "Bond Δ"
508
+ )
142
509
  plt.ylabel("Energy level")
143
510
  plt.legend()
144
511
  plt.show()