qoro-divi 0.2.1b1__py3-none-any.whl → 0.3.0b1__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/_pbar.py +1 -3
- divi/circuits.py +3 -3
- divi/exp/cirq/__init__.py +1 -0
- divi/exp/cirq/_validator.py +645 -0
- divi/parallel_simulator.py +9 -9
- divi/qasm.py +2 -3
- divi/qoro_service.py +210 -141
- divi/qprog/__init__.py +2 -2
- divi/qprog/_graph_partitioning.py +103 -66
- divi/qprog/_qaoa.py +33 -8
- divi/qprog/_qubo_partitioning.py +199 -0
- divi/qprog/_vqe.py +48 -39
- divi/qprog/_vqe_sweep.py +413 -46
- divi/qprog/batch.py +61 -14
- divi/qprog/quantum_program.py +10 -11
- divi/qpu_system.py +20 -0
- qoro_divi-0.3.0b1.dist-info/LICENSES/.license-header +3 -0
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/METADATA +10 -3
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/RECORD +22 -19
- divi/qprog/_mlae.py +0 -182
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/LICENSE +0 -0
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.2.1b1.dist-info → qoro_divi-0.3.0b1.dist-info}/WHEEL +0 -0
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
479
|
+
for ansatz, modifier in self.programs.keys():
|
|
117
480
|
min_energies = []
|
|
118
481
|
|
|
119
|
-
curr_energies = self.programs[(ansatz,
|
|
482
|
+
curr_energies = self.programs[(ansatz, modifier)].losses[-1]
|
|
120
483
|
min_energies.append(
|
|
121
484
|
(
|
|
122
|
-
|
|
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
|
|
498
|
+
for modifier in self.molecule_transformer.bond_modifiers:
|
|
136
499
|
energies.append(
|
|
137
|
-
min(self.programs[(ansatz,
|
|
500
|
+
min(self.programs[(ansatz, modifier)].losses[-1].values())
|
|
138
501
|
)
|
|
139
|
-
plt.plot(
|
|
502
|
+
plt.plot(
|
|
503
|
+
self.molecule_transformer.bond_modifiers, energies, label=ansatz
|
|
504
|
+
)
|
|
140
505
|
|
|
141
|
-
plt.xlabel(
|
|
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()
|