qoro-divi 0.2.0b1__py3-none-any.whl → 0.5.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.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +9 -0
- divi/backends/_circuit_runner.py +70 -0
- divi/backends/_execution_result.py +70 -0
- divi/backends/_parallel_simulator.py +486 -0
- divi/backends/_qoro_service.py +663 -0
- divi/backends/_qpu_system.py +101 -0
- divi/backends/_results_processing.py +133 -0
- divi/circuits/__init__.py +8 -0
- divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
- divi/circuits/_cirq/_parser.py +110 -0
- divi/circuits/_cirq/_qasm_export.py +78 -0
- divi/circuits/_core.py +369 -0
- divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
- divi/circuits/_qasm_validation.py +694 -0
- divi/qprog/__init__.py +24 -6
- divi/qprog/_expectation.py +181 -0
- divi/qprog/_hamiltonians.py +281 -0
- divi/qprog/algorithms/__init__.py +14 -0
- divi/qprog/algorithms/_ansatze.py +356 -0
- divi/qprog/algorithms/_qaoa.py +572 -0
- divi/qprog/algorithms/_vqe.py +249 -0
- divi/qprog/batch.py +383 -73
- divi/qprog/checkpointing.py +556 -0
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +1014 -43
- divi/qprog/quantum_program.py +231 -413
- divi/qprog/variational_quantum_algorithm.py +995 -0
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
- divi/qprog/workflows/_qubo_partitioning.py +220 -0
- divi/qprog/workflows/_vqe_sweep.py +560 -0
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +127 -0
- divi/reporting/_qlogger.py +68 -0
- divi/reporting/_reporter.py +133 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info}/METADATA +43 -15
- qoro_divi-0.5.0.dist-info/RECORD +43 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info}/WHEEL +1 -1
- qoro_divi-0.5.0.dist-info/licenses/LICENSES/.license-header +3 -0
- divi/_pbar.py +0 -73
- divi/circuits.py +0 -139
- divi/exp/cirq/_lexer.py +0 -126
- divi/exp/cirq/_parser.py +0 -889
- divi/exp/cirq/_qasm_export.py +0 -37
- divi/exp/cirq/_qasm_import.py +0 -35
- divi/exp/cirq/exception.py +0 -21
- divi/exp/scipy/_cobyla.py +0 -342
- divi/exp/scipy/pyprima/LICENCE.txt +0 -28
- divi/exp/scipy/pyprima/__init__.py +0 -263
- divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
- divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
- divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
- divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
- divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
- divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
- divi/exp/scipy/pyprima/cobyla/update.py +0 -331
- divi/exp/scipy/pyprima/common/__init__.py +0 -0
- divi/exp/scipy/pyprima/common/_bounds.py +0 -41
- divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
- divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
- divi/exp/scipy/pyprima/common/_project.py +0 -224
- divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
- divi/exp/scipy/pyprima/common/consts.py +0 -48
- divi/exp/scipy/pyprima/common/evaluate.py +0 -101
- divi/exp/scipy/pyprima/common/history.py +0 -39
- divi/exp/scipy/pyprima/common/infos.py +0 -30
- divi/exp/scipy/pyprima/common/linalg.py +0 -452
- divi/exp/scipy/pyprima/common/message.py +0 -336
- divi/exp/scipy/pyprima/common/powalg.py +0 -131
- divi/exp/scipy/pyprima/common/preproc.py +0 -393
- divi/exp/scipy/pyprima/common/present.py +0 -5
- divi/exp/scipy/pyprima/common/ratio.py +0 -56
- divi/exp/scipy/pyprima/common/redrho.py +0 -49
- divi/exp/scipy/pyprima/common/selectx.py +0 -346
- divi/interfaces.py +0 -25
- divi/parallel_simulator.py +0 -258
- divi/qlogger.py +0 -119
- divi/qoro_service.py +0 -343
- divi/qprog/_mlae.py +0 -182
- divi/qprog/_qaoa.py +0 -440
- divi/qprog/_vqe.py +0 -275
- divi/qprog/_vqe_sweep.py +0 -144
- divi/utils.py +0 -116
- qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
- /divi/{qem.py → circuits/qem.py} +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info/licenses}/LICENSE +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections import deque
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from functools import partial
|
|
10
|
+
from itertools import product
|
|
11
|
+
from typing import Literal, NamedTuple
|
|
12
|
+
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
import pennylane as qml
|
|
17
|
+
|
|
18
|
+
from divi.qprog import VQE, Ansatz, ProgramBatch
|
|
19
|
+
from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer, copy_optimizer
|
|
20
|
+
|
|
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: npt.NDArray[np.float64], 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]) -> npt.NDArray[np.float64]:
|
|
125
|
+
n_atoms = len(z_matrix)
|
|
126
|
+
coords = np.zeros((n_atoms, 3))
|
|
127
|
+
|
|
128
|
+
if n_atoms == 0:
|
|
129
|
+
return coords
|
|
130
|
+
|
|
131
|
+
# Validate bond lengths are positive
|
|
132
|
+
for i, entry in enumerate(z_matrix[1:], start=1):
|
|
133
|
+
if entry.bond_length is not None and entry.bond_length <= 0:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Bond length for atom {i} must be positive, got {entry.bond_length}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# --- First atom at origin ---
|
|
139
|
+
coords[0] = np.array([0.0, 0.0, 0.0])
|
|
140
|
+
|
|
141
|
+
# --- Second atom along +X axis ---
|
|
142
|
+
if n_atoms > 1:
|
|
143
|
+
coords[1] = np.array([z_matrix[1].bond_length, 0.0, 0.0])
|
|
144
|
+
|
|
145
|
+
# --- Third atom in XY plane ---
|
|
146
|
+
if n_atoms > 2:
|
|
147
|
+
entry = z_matrix[2]
|
|
148
|
+
r = entry.bond_length
|
|
149
|
+
theta = np.radians(entry.angle) if entry.angle is not None else 0.0
|
|
150
|
+
|
|
151
|
+
a1 = coords[entry.bond_ref]
|
|
152
|
+
a2 = coords[entry.angle_ref]
|
|
153
|
+
|
|
154
|
+
v = a2 - a1
|
|
155
|
+
v /= np.linalg.norm(v)
|
|
156
|
+
# fixed perpendicular in XY plane, fallback handled inline
|
|
157
|
+
perp = np.array([-v[1], v[0], 0.0])
|
|
158
|
+
perp /= np.linalg.norm(perp) if np.linalg.norm(perp) > 1e-6 else 1.0
|
|
159
|
+
|
|
160
|
+
coords[2] = a1 + r * (np.cos(theta) * v + np.sin(theta) * perp)
|
|
161
|
+
|
|
162
|
+
for i, entry in enumerate(z_matrix[3:], start=3):
|
|
163
|
+
a1 = coords[entry.bond_ref] if entry.bond_ref is not None else np.zeros(3)
|
|
164
|
+
a2 = coords[entry.angle_ref] if entry.angle_ref is not None else np.zeros(3)
|
|
165
|
+
a3 = (
|
|
166
|
+
coords[entry.dihedral_ref]
|
|
167
|
+
if entry.dihedral_ref is not None
|
|
168
|
+
else np.zeros(3)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
r = entry.bond_length
|
|
172
|
+
|
|
173
|
+
theta = np.radians(entry.angle) if entry.angle is not None else 0.0
|
|
174
|
+
phi = np.radians(entry.dihedral) if entry.dihedral is not None else 0.0
|
|
175
|
+
|
|
176
|
+
b1 = _safe_normalize(a1 - a2, fallback=np.array([1.0, 0.0, 0.0]))
|
|
177
|
+
b2 = a3 - a2
|
|
178
|
+
n = _safe_normalize(np.cross(b1, b2), fallback=np.array([0.0, 0.0, 1.0]))
|
|
179
|
+
nc = np.cross(n, b1)
|
|
180
|
+
|
|
181
|
+
coords[i] = a1 + r * (
|
|
182
|
+
-np.cos(theta) * b1 + np.sin(theta) * (np.cos(phi) * nc + np.sin(phi) * n)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return coords
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _transform_bonds(
|
|
189
|
+
zmatrix: list[_ZMatrixEntry],
|
|
190
|
+
bonds_to_transform: list[tuple[int, int]],
|
|
191
|
+
value: float,
|
|
192
|
+
transform_type: Literal["scale", "delta"] = "scale",
|
|
193
|
+
) -> list[_ZMatrixEntry]:
|
|
194
|
+
"""
|
|
195
|
+
Transform specified bonds in a Z-matrix.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
zmatrix: List of _ZMatrixEntry.
|
|
199
|
+
bonds_to_transform: List of (atom1, atom2) tuples specifying bonds.
|
|
200
|
+
value: Multiplier or additive value.
|
|
201
|
+
transform_type: "scale" or "add".
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
New Z-matrix with transformed bond lengths.
|
|
205
|
+
"""
|
|
206
|
+
# Convert to set of sorted tuples for quick lookup
|
|
207
|
+
bonds_set = {tuple(sorted(b)) for b in bonds_to_transform}
|
|
208
|
+
|
|
209
|
+
new_zmatrix = []
|
|
210
|
+
for i, entry in enumerate(zmatrix):
|
|
211
|
+
if (
|
|
212
|
+
entry.bond_ref is not None
|
|
213
|
+
and tuple(sorted((i, entry.bond_ref))) in bonds_set
|
|
214
|
+
):
|
|
215
|
+
old_length = entry.bond_length
|
|
216
|
+
new_length = (
|
|
217
|
+
old_length * value if transform_type == "scale" else old_length + value
|
|
218
|
+
)
|
|
219
|
+
if new_length == 0.0:
|
|
220
|
+
raise RuntimeError(
|
|
221
|
+
"New bond length can't be zero after transformation."
|
|
222
|
+
)
|
|
223
|
+
new_zmatrix.append(entry._replace(bond_length=new_length))
|
|
224
|
+
else:
|
|
225
|
+
new_zmatrix.append(entry)
|
|
226
|
+
return new_zmatrix
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _kabsch_align(
|
|
230
|
+
P_in: npt.NDArray[np.float64],
|
|
231
|
+
Q_in: npt.NDArray[np.float64],
|
|
232
|
+
reference_atoms_idx=slice(None),
|
|
233
|
+
) -> npt.NDArray[np.float64]:
|
|
234
|
+
"""
|
|
235
|
+
Align point set P onto Q using the Kabsch algorithm.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
P : (N, D) npt.NDArray[np.float64]. Source coordinates.
|
|
240
|
+
Q : (N, D) npt.NDArray[np.float64]. Target coordinates.
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
P_aligned : (N, D) npt.NDArray[np.float64]
|
|
245
|
+
P rotated and translated onto Q.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
P = P_in[reference_atoms_idx, :]
|
|
249
|
+
Q = Q_in[reference_atoms_idx, :]
|
|
250
|
+
|
|
251
|
+
P = np.asarray(P, dtype=float)
|
|
252
|
+
Q = np.asarray(Q, dtype=float)
|
|
253
|
+
|
|
254
|
+
# Centroids
|
|
255
|
+
Pc = np.mean(P, axis=0)
|
|
256
|
+
Qc = np.mean(Q, axis=0)
|
|
257
|
+
|
|
258
|
+
# Centered coordinates
|
|
259
|
+
P_centered = P - Pc
|
|
260
|
+
Q_centered = Q - Qc
|
|
261
|
+
|
|
262
|
+
# Covariance and SVD
|
|
263
|
+
H = P_centered.T @ Q_centered
|
|
264
|
+
U, _, Vt = np.linalg.svd(H)
|
|
265
|
+
|
|
266
|
+
# Compute rotation matrix
|
|
267
|
+
R = Vt.T @ U.T
|
|
268
|
+
|
|
269
|
+
# Ensure proper rotation (det = +1) by handling reflections
|
|
270
|
+
if np.linalg.det(R) < 0:
|
|
271
|
+
# Flip the last column of Vt to ensure proper rotation
|
|
272
|
+
Vt[-1, :] *= -1
|
|
273
|
+
R = Vt.T @ U.T
|
|
274
|
+
t = Qc - Pc @ R
|
|
275
|
+
|
|
276
|
+
# Apply transformation
|
|
277
|
+
P_aligned = P_in @ R + t
|
|
278
|
+
|
|
279
|
+
P_aligned[np.abs(P_aligned) < 1e-12] = 0.0
|
|
280
|
+
|
|
281
|
+
return P_aligned
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@dataclass(frozen=True, eq=True)
|
|
285
|
+
class MoleculeTransformer:
|
|
286
|
+
"""
|
|
287
|
+
A class for transforming molecular structures by modifying bond lengths.
|
|
288
|
+
|
|
289
|
+
This class generates variants of a base molecule by adjusting bond lengths
|
|
290
|
+
according to specified modifiers. The modification mode is detected automatically.
|
|
291
|
+
|
|
292
|
+
Attributes:
|
|
293
|
+
base_molecule (qml.qchem.Molecule): The reference molecule used as a template for generating variants.
|
|
294
|
+
bond_modifiers (Sequence[float]): A list of values used to adjust bond lengths. The class will generate
|
|
295
|
+
**one new molecule for each modifier** in this list. The modification
|
|
296
|
+
mode is detected automatically:
|
|
297
|
+
- **Scale mode**: If all values are positive, they are used as scaling
|
|
298
|
+
factors (e.g., 1.1 for a 10% increase).
|
|
299
|
+
- **Delta mode**: If any value is zero or negative, all values are
|
|
300
|
+
treated as additive changes to the bond length, in Ångstroms.
|
|
301
|
+
atom_connectivity (Sequence[tuple[int, int]] | None): A sequence of atom index pairs specifying the bonds in the molecule.
|
|
302
|
+
If not provided, a chain structure will be assumed
|
|
303
|
+
e.g.: `[(0, 1), (1, 2), (2, 3), ...]`.
|
|
304
|
+
bonds_to_transform (Sequence[tuple[int, int]] | None): A subset of `atom_connectivity` that specifies the bonds to modify.
|
|
305
|
+
If None, all bonds will be transformed.
|
|
306
|
+
alignment_atoms (Sequence[int] | None): Indices of atoms onto which to align the orientation of the resulting
|
|
307
|
+
variants of the molecule. Only useful for visualization and debugging.
|
|
308
|
+
If None, no alignment is carried out.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
base_molecule: qml.qchem.Molecule
|
|
312
|
+
bond_modifiers: Sequence[float]
|
|
313
|
+
atom_connectivity: Sequence[tuple[int, int]] | None = None
|
|
314
|
+
bonds_to_transform: Sequence[tuple[int, int]] | None = None
|
|
315
|
+
alignment_atoms: Sequence[int] | None = None
|
|
316
|
+
|
|
317
|
+
def __post_init__(self):
|
|
318
|
+
if not isinstance(self.base_molecule, qml.qchem.Molecule):
|
|
319
|
+
raise ValueError(
|
|
320
|
+
"`base_molecule` is expected to be a Pennylane `Molecule` instance."
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if not all(isinstance(x, (float, int)) for x in self.bond_modifiers):
|
|
324
|
+
raise ValueError("`bond_modifiers` should be a sequence of floats.")
|
|
325
|
+
if len(set(self.bond_modifiers)) < len(self.bond_modifiers):
|
|
326
|
+
raise ValueError("`bond_modifiers` contains duplicate values.")
|
|
327
|
+
object.__setattr__(
|
|
328
|
+
self,
|
|
329
|
+
"_mode",
|
|
330
|
+
"scale" if all(v > 0 for v in self.bond_modifiers) else "delta",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
n_symbols = len(self.base_molecule.symbols)
|
|
334
|
+
if self.atom_connectivity is None:
|
|
335
|
+
object.__setattr__(
|
|
336
|
+
self,
|
|
337
|
+
"atom_connectivity",
|
|
338
|
+
tuple(zip(range(n_symbols), range(1, n_symbols))),
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
if len(set(self.atom_connectivity)) < len(self.atom_connectivity):
|
|
342
|
+
raise ValueError("`atom_connectivity` contains duplicate values.")
|
|
343
|
+
|
|
344
|
+
if not all(
|
|
345
|
+
0 <= a < n_symbols and 0 <= b < n_symbols
|
|
346
|
+
for a, b in self.atom_connectivity
|
|
347
|
+
):
|
|
348
|
+
raise ValueError(
|
|
349
|
+
"`atom_connectivity` should be a sequence of tuples of"
|
|
350
|
+
" atom indices in (0, len(molecule.symbols))"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if self.bonds_to_transform is None:
|
|
354
|
+
object.__setattr__(self, "bonds_to_transform", self.atom_connectivity)
|
|
355
|
+
else:
|
|
356
|
+
if len(self.bonds_to_transform) == 0:
|
|
357
|
+
raise ValueError("`bonds_to_transform` cannot be empty.")
|
|
358
|
+
if not set(self.bonds_to_transform).issubset(self.atom_connectivity):
|
|
359
|
+
raise ValueError(
|
|
360
|
+
"`bonds_to_transform` is not a subset of `atom_connectivity`"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if self.alignment_atoms is not None and not all(
|
|
364
|
+
0 <= idx < n_symbols for idx in self.alignment_atoms
|
|
365
|
+
):
|
|
366
|
+
raise ValueError(
|
|
367
|
+
"`alignment_atoms` need to be in range (0, len(molecule.symbols))"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def generate(self) -> dict[float, qml.qchem.Molecule]:
|
|
371
|
+
base_attrs = _ctor_attrs(self.base_molecule)
|
|
372
|
+
|
|
373
|
+
variants = {}
|
|
374
|
+
original_coords = self.base_molecule.coordinates
|
|
375
|
+
mode = "scale" if all(v > 0 for v in self.bond_modifiers) else "delta"
|
|
376
|
+
|
|
377
|
+
# Convert to Z-matrix, with connectivity
|
|
378
|
+
z_matrix = _cartesian_to_zmatrix(original_coords, self.atom_connectivity)
|
|
379
|
+
|
|
380
|
+
for value in self.bond_modifiers:
|
|
381
|
+
if (value == 0 and mode == "delta") or (value == 1 and mode == "scale"):
|
|
382
|
+
transformed_coords = original_coords.copy()
|
|
383
|
+
else:
|
|
384
|
+
transformed_z_matrix = _transform_bonds(
|
|
385
|
+
z_matrix, self.bonds_to_transform, value, mode
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
transformed_coords = _zmatrix_to_cartesian(transformed_z_matrix)
|
|
389
|
+
|
|
390
|
+
if self.alignment_atoms is not None:
|
|
391
|
+
transformed_coords = _kabsch_align(
|
|
392
|
+
transformed_coords, original_coords, self.alignment_atoms
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# A single molecule is created after all bonds have been modified
|
|
396
|
+
base_attrs["coordinates"] = transformed_coords
|
|
397
|
+
mol = qml.qchem.Molecule(**base_attrs)
|
|
398
|
+
variants[value] = mol
|
|
399
|
+
|
|
400
|
+
return variants
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class VQEHyperparameterSweep(ProgramBatch):
|
|
404
|
+
"""Allows user to carry out a grid search across different values
|
|
405
|
+
for the ansatz and the bond length used in a VQE program.
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
def __init__(
|
|
409
|
+
self,
|
|
410
|
+
ansatze: Sequence[Ansatz],
|
|
411
|
+
molecule_transformer: MoleculeTransformer,
|
|
412
|
+
optimizer: Optimizer | None = None,
|
|
413
|
+
max_iterations: int = 10,
|
|
414
|
+
**kwargs,
|
|
415
|
+
):
|
|
416
|
+
"""
|
|
417
|
+
Initialize a VQE hyperparameter sweep.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
ansatze: Sequence[Ansatz]
|
|
422
|
+
A sequence of ansatz circuits to test.
|
|
423
|
+
molecule_transformer: MoleculeTransformer
|
|
424
|
+
A `MoleculeTransformer` object defining the configuration for
|
|
425
|
+
generating the molecule variants.
|
|
426
|
+
optimizer: Optimizer
|
|
427
|
+
The optimization algorithm for the VQE runs.
|
|
428
|
+
max_iterations: int
|
|
429
|
+
The maximum number of optimizer iterations for each VQE run.
|
|
430
|
+
**kwargs: Forwarded to parent class.
|
|
431
|
+
"""
|
|
432
|
+
super().__init__(backend=kwargs.pop("backend"))
|
|
433
|
+
|
|
434
|
+
self.molecule_transformer = molecule_transformer
|
|
435
|
+
|
|
436
|
+
self.ansatze = ansatze
|
|
437
|
+
self.max_iterations = max_iterations
|
|
438
|
+
|
|
439
|
+
# Store the optimizer template (will be copied for each program)
|
|
440
|
+
self._optimizer_template = (
|
|
441
|
+
optimizer if optimizer is not None else MonteCarloOptimizer()
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
self._constructor = partial(
|
|
445
|
+
VQE,
|
|
446
|
+
max_iterations=self.max_iterations,
|
|
447
|
+
backend=self.backend,
|
|
448
|
+
**kwargs,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def create_programs(self):
|
|
452
|
+
"""
|
|
453
|
+
Create VQE programs for all combinations of ansätze and molecule variants.
|
|
454
|
+
|
|
455
|
+
Generates molecule variants using the configured MoleculeTransformer, then
|
|
456
|
+
creates a VQE program for each (ansatz, molecule_variant) pair.
|
|
457
|
+
|
|
458
|
+
Note:
|
|
459
|
+
Program IDs are tuples of (ansatz_name, bond_modifier_value).
|
|
460
|
+
"""
|
|
461
|
+
super().create_programs()
|
|
462
|
+
|
|
463
|
+
self.molecule_variants = self.molecule_transformer.generate()
|
|
464
|
+
|
|
465
|
+
for ansatz, (modifier, molecule) in product(
|
|
466
|
+
self.ansatze, self.molecule_variants.items()
|
|
467
|
+
):
|
|
468
|
+
_job_id = (ansatz.name, modifier)
|
|
469
|
+
self._programs[_job_id] = self._constructor(
|
|
470
|
+
program_id=_job_id,
|
|
471
|
+
molecule=molecule,
|
|
472
|
+
ansatz=ansatz,
|
|
473
|
+
optimizer=copy_optimizer(self._optimizer_template),
|
|
474
|
+
progress_queue=self._queue,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def aggregate_results(self):
|
|
478
|
+
"""
|
|
479
|
+
Find the best ansatz and bond configuration from all VQE runs.
|
|
480
|
+
|
|
481
|
+
Compares the final energies across all ansatz/molecule combinations
|
|
482
|
+
and returns the configuration that achieved the lowest ground state energy.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
tuple: A tuple containing:
|
|
486
|
+
- best_config (tuple): (ansatz_name, bond_modifier) of the best result.
|
|
487
|
+
- best_energy (float): The lowest energy achieved.
|
|
488
|
+
|
|
489
|
+
Raises:
|
|
490
|
+
RuntimeError: If programs haven't been run or have empty losses.
|
|
491
|
+
"""
|
|
492
|
+
super().aggregate_results()
|
|
493
|
+
|
|
494
|
+
all_energies = {key: prog.best_loss for key, prog in self.programs.items()}
|
|
495
|
+
|
|
496
|
+
smallest_key = min(all_energies, key=lambda k: all_energies[k])
|
|
497
|
+
smallest_value = all_energies[smallest_key]
|
|
498
|
+
|
|
499
|
+
return smallest_key, smallest_value
|
|
500
|
+
|
|
501
|
+
def visualize_results(self, graph_type: Literal["line", "scatter"] = "line"):
|
|
502
|
+
"""
|
|
503
|
+
Visualize the results of the VQE problem.
|
|
504
|
+
"""
|
|
505
|
+
if graph_type not in ["line", "scatter"]:
|
|
506
|
+
raise ValueError(
|
|
507
|
+
f"Invalid graph type: {graph_type}. Choose between 'line' and 'scatter'."
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if self._executor is not None:
|
|
511
|
+
self.join()
|
|
512
|
+
|
|
513
|
+
# Get the unique ansatz objects that were actually run
|
|
514
|
+
# Assumes `self.ansatze` is a list of the ansatz instances used.
|
|
515
|
+
unique_ansatze = self.ansatze
|
|
516
|
+
|
|
517
|
+
# Create a stable color mapping for each unique ansatz object
|
|
518
|
+
colors = ["blue", "g", "r", "c", "m", "y", "k"]
|
|
519
|
+
color_map = {
|
|
520
|
+
ansatz: colors[i % len(colors)] for i, ansatz in enumerate(unique_ansatze)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if graph_type == "scatter":
|
|
524
|
+
# Plot each ansatz's results as a separate series for clarity
|
|
525
|
+
for ansatz in unique_ansatze:
|
|
526
|
+
modifiers = []
|
|
527
|
+
energies = []
|
|
528
|
+
for modifier in self.molecule_transformer.bond_modifiers:
|
|
529
|
+
program_key = (ansatz.name, modifier)
|
|
530
|
+
if program_key in self._programs:
|
|
531
|
+
modifiers.append(modifier)
|
|
532
|
+
energies.append(self._programs[program_key].best_loss)
|
|
533
|
+
|
|
534
|
+
# Use the new .name property for the label and the color_map
|
|
535
|
+
plt.scatter(
|
|
536
|
+
modifiers,
|
|
537
|
+
energies,
|
|
538
|
+
color=color_map[ansatz],
|
|
539
|
+
label=ansatz.name,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
elif graph_type == "line":
|
|
543
|
+
for ansatz in unique_ansatze:
|
|
544
|
+
energies = []
|
|
545
|
+
for modifier in self.molecule_transformer.bond_modifiers:
|
|
546
|
+
energies.append(self._programs[(ansatz.name, modifier)].best_loss)
|
|
547
|
+
|
|
548
|
+
plt.plot(
|
|
549
|
+
self.molecule_transformer.bond_modifiers,
|
|
550
|
+
energies,
|
|
551
|
+
label=ansatz.name,
|
|
552
|
+
color=color_map[ansatz],
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
plt.xlabel(
|
|
556
|
+
"Scale Factor" if self.molecule_transformer._mode == "scale" else "Bond Δ"
|
|
557
|
+
)
|
|
558
|
+
plt.ylabel("Energy level")
|
|
559
|
+
plt.legend()
|
|
560
|
+
plt.show()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from ._pbar import make_progress_bar
|
|
6
|
+
from ._qlogger import disable_logging, enable_logging
|
|
7
|
+
from ._reporter import LoggingProgressReporter, ProgressReporter, QueueProgressReporter
|
divi/reporting/_pbar.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from rich.progress import (
|
|
6
|
+
BarColumn,
|
|
7
|
+
MofNCompleteColumn,
|
|
8
|
+
Progress,
|
|
9
|
+
ProgressColumn,
|
|
10
|
+
SpinnerColumn,
|
|
11
|
+
TextColumn,
|
|
12
|
+
TimeElapsedColumn,
|
|
13
|
+
)
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _UnfinishedTaskWrapper:
|
|
18
|
+
"""Wrapper that forces a task to appear unfinished for spinner animation."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, task):
|
|
21
|
+
self._task = task
|
|
22
|
+
|
|
23
|
+
def __getattr__(self, name):
|
|
24
|
+
if name == "finished":
|
|
25
|
+
return False
|
|
26
|
+
return getattr(self._task, name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConditionalSpinnerColumn(ProgressColumn):
|
|
30
|
+
_FINAL_STATUSES = ("Success", "Failed", "Cancelled", "Aborted")
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.spinner = SpinnerColumn("point")
|
|
35
|
+
|
|
36
|
+
def render(self, task):
|
|
37
|
+
status = task.fields.get("final_status")
|
|
38
|
+
|
|
39
|
+
if status in self._FINAL_STATUSES:
|
|
40
|
+
return Text("")
|
|
41
|
+
|
|
42
|
+
# Force the task to appear unfinished for spinner animation
|
|
43
|
+
return self.spinner.render(_UnfinishedTaskWrapper(task))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PhaseStatusColumn(ProgressColumn):
|
|
47
|
+
_STATUS_MESSAGES = {
|
|
48
|
+
"Success": ("• Success! ✅", "bold green"),
|
|
49
|
+
"Failed": ("• Failed! ❌", "bold red"),
|
|
50
|
+
"Cancelled": ("• Cancelled ⏹️", "bold yellow"),
|
|
51
|
+
"Aborted": ("• Aborted ⚠️", "dim magenta"),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def __init__(self, table_column=None):
|
|
55
|
+
super().__init__(table_column)
|
|
56
|
+
|
|
57
|
+
def _build_polling_string(
|
|
58
|
+
self, split_job_id: str, job_status: str, poll_attempt: int, max_retries: int
|
|
59
|
+
) -> str:
|
|
60
|
+
"""Build the polling status string for service job tracking."""
|
|
61
|
+
if job_status == "COMPLETED":
|
|
62
|
+
return f" [Job {split_job_id} is complete.]"
|
|
63
|
+
elif poll_attempt > 0:
|
|
64
|
+
return f" [Job {split_job_id} is {job_status}. Polling attempt {poll_attempt} / {max_retries}]"
|
|
65
|
+
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
def render(self, task):
|
|
69
|
+
final_status = task.fields.get("final_status")
|
|
70
|
+
|
|
71
|
+
# Early return for final statuses
|
|
72
|
+
if final_status in self._STATUS_MESSAGES:
|
|
73
|
+
message, style = self._STATUS_MESSAGES[final_status]
|
|
74
|
+
return Text(message, style=style)
|
|
75
|
+
|
|
76
|
+
# Build message with polling information
|
|
77
|
+
message = task.fields.get("message")
|
|
78
|
+
service_job_id = task.fields.get("service_job_id")
|
|
79
|
+
job_status = task.fields.get("job_status")
|
|
80
|
+
poll_attempt = task.fields.get("poll_attempt", 0)
|
|
81
|
+
max_retries = task.fields.get("max_retries")
|
|
82
|
+
|
|
83
|
+
polling_str = ""
|
|
84
|
+
split_job_id = None
|
|
85
|
+
if service_job_id is not None:
|
|
86
|
+
split_job_id = service_job_id.split("-")[0]
|
|
87
|
+
polling_str = self._build_polling_string(
|
|
88
|
+
split_job_id, job_status, poll_attempt, max_retries
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
final_text = Text(f"[{message}]{polling_str}")
|
|
92
|
+
|
|
93
|
+
# Highlight job ID if present
|
|
94
|
+
if split_job_id is not None:
|
|
95
|
+
final_text.highlight_words([split_job_id], "blue")
|
|
96
|
+
|
|
97
|
+
return final_text
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def make_progress_bar(is_jupyter: bool = False) -> Progress:
|
|
101
|
+
"""
|
|
102
|
+
Create a customized Rich progress bar for tracking quantum program execution.
|
|
103
|
+
|
|
104
|
+
Builds a progress bar with custom columns including job name, completion status,
|
|
105
|
+
elapsed time, spinner, and phase status indicators. Automatically adapts refresh
|
|
106
|
+
behavior for Jupyter notebook environments.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
is_jupyter (bool, optional): Whether the progress bar is being displayed in
|
|
110
|
+
a Jupyter notebook environment. Affects refresh behavior. Defaults to False.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Progress: A configured Rich Progress instance with custom columns for
|
|
114
|
+
quantum program tracking.
|
|
115
|
+
"""
|
|
116
|
+
return Progress(
|
|
117
|
+
TextColumn("[bold blue]{task.fields[job_name]}"),
|
|
118
|
+
BarColumn(),
|
|
119
|
+
MofNCompleteColumn(),
|
|
120
|
+
TimeElapsedColumn(),
|
|
121
|
+
ConditionalSpinnerColumn(),
|
|
122
|
+
PhaseStatusColumn(),
|
|
123
|
+
# For jupyter notebooks, refresh manually instead
|
|
124
|
+
auto_refresh=not is_jupyter,
|
|
125
|
+
# Give a dummy positive value if is_jupyter
|
|
126
|
+
refresh_per_second=10 if not is_jupyter else 999,
|
|
127
|
+
)
|