qoro-divi 0.2.0b1__py3-none-any.whl → 0.6.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 +10 -0
- divi/backends/_backend_properties_conversion.py +227 -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 +13 -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 +391 -0
- divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
- divi/circuits/_qasm_validation.py +694 -0
- divi/qprog/__init__.py +27 -8
- divi/qprog/_expectation.py +181 -0
- divi/qprog/_hamiltonians.py +281 -0
- divi/qprog/algorithms/__init__.py +16 -0
- divi/qprog/algorithms/_ansatze.py +368 -0
- divi/qprog/algorithms/_custom_vqa.py +263 -0
- divi/qprog/algorithms/_pce.py +262 -0
- divi/qprog/algorithms/_qaoa.py +579 -0
- divi/qprog/algorithms/_vqe.py +262 -0
- divi/qprog/batch.py +387 -74
- divi/qprog/checkpointing.py +556 -0
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +1014 -43
- divi/qprog/quantum_program.py +243 -412
- divi/qprog/typing.py +62 -0
- divi/qprog/variational_quantum_algorithm.py +1208 -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 +221 -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 +155 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/METADATA +43 -15
- qoro_divi-0.6.0.dist-info/RECORD +47 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/WHEEL +1 -1
- qoro_divi-0.6.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.6.0.dist-info/licenses}/LICENSE +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from itertools import tee
|
|
7
|
+
from typing import Literal, Sequence
|
|
8
|
+
from warnings import warn
|
|
9
|
+
|
|
10
|
+
import pennylane as qml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _require_trainable_params(n_params: int, ansatz_name: str) -> int:
|
|
14
|
+
if n_params <= 0:
|
|
15
|
+
raise ValueError(
|
|
16
|
+
f"{ansatz_name} must define at least one trainable parameter. "
|
|
17
|
+
"Parameter-free circuits are not supported."
|
|
18
|
+
)
|
|
19
|
+
return n_params
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Ansatz(ABC):
|
|
23
|
+
"""Abstract base class for all VQE ansätze."""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> str:
|
|
27
|
+
"""Returns the human-readable name of the ansatz."""
|
|
28
|
+
return self.__class__.__name__
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def n_params_per_layer(n_qubits: int, **kwargs) -> int:
|
|
33
|
+
"""Returns the number of parameters required by the ansatz for one layer."""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def build(
|
|
38
|
+
self, params, n_qubits: int, n_layers: int, **kwargs
|
|
39
|
+
) -> list[qml.operation.Operator]:
|
|
40
|
+
"""
|
|
41
|
+
Builds the ansatz circuit and returns a list of operations.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
params: Parameter array for the ansatz.
|
|
45
|
+
n_qubits (int): Number of qubits in the circuit.
|
|
46
|
+
n_layers (int): Number of ansatz layers.
|
|
47
|
+
**kwargs: Additional arguments specific to the ansatz.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
list[qml.operation.Operator]: List of PennyLane operations representing the ansatz.
|
|
51
|
+
"""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- Template Ansaetze ---
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GenericLayerAnsatz(Ansatz):
|
|
59
|
+
"""
|
|
60
|
+
A flexible ansatz alternating single-qubit gates with optional entanglers.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
gate_sequence: list[qml.operation.Operator],
|
|
66
|
+
entangler: qml.operation.Operator | None = None,
|
|
67
|
+
entangling_layout: (
|
|
68
|
+
Literal["linear", "brick", "circular", "all-to-all"]
|
|
69
|
+
| Sequence[tuple[int, int]]
|
|
70
|
+
| None
|
|
71
|
+
) = None,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Args:
|
|
75
|
+
gate_sequence (list[Callable]): List of one-qubit gate classes (e.g., qml.RY, qml.Rot).
|
|
76
|
+
entangler (Callable): Two-qubit entangling gate class (e.g., qml.CNOT, qml.CZ).
|
|
77
|
+
If None, no entanglement is applied.
|
|
78
|
+
entangling_layout (str): Layout for entangling layer ("linear", "all_to_all", etc.).
|
|
79
|
+
"""
|
|
80
|
+
if not all(
|
|
81
|
+
issubclass(g, qml.operation.Operator) and g.num_wires == 1
|
|
82
|
+
for g in gate_sequence
|
|
83
|
+
):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"All elements in gate_sequence must be PennyLane one-qubit gate classes."
|
|
86
|
+
)
|
|
87
|
+
self.gate_sequence = gate_sequence
|
|
88
|
+
|
|
89
|
+
if entangler not in (None, qml.CNOT, qml.CZ):
|
|
90
|
+
raise ValueError("Only qml.CNOT and qml.CZ are supported as entanglers.")
|
|
91
|
+
self.entangler = entangler
|
|
92
|
+
|
|
93
|
+
self.entangling_layout = entangling_layout
|
|
94
|
+
if entangler is None and self.entangling_layout is not None:
|
|
95
|
+
warn("`entangling_layout` provided but `entangler` is None.")
|
|
96
|
+
match self.entangling_layout:
|
|
97
|
+
case None | "linear":
|
|
98
|
+
self.entangling_layout = "linear"
|
|
99
|
+
|
|
100
|
+
self._layout_fn = lambda n_qubits: zip(
|
|
101
|
+
range(n_qubits), range(1, n_qubits)
|
|
102
|
+
)
|
|
103
|
+
case "brick":
|
|
104
|
+
self._layout_fn = lambda n_qubits: [
|
|
105
|
+
(i, i + 1) for r in range(2) for i in range(r, n_qubits - 1, 2)
|
|
106
|
+
]
|
|
107
|
+
case "circular":
|
|
108
|
+
self._layout_fn = lambda n_qubits: zip(
|
|
109
|
+
range(n_qubits), [(i + 1) % n_qubits for i in range(n_qubits)]
|
|
110
|
+
)
|
|
111
|
+
case "all-to-all":
|
|
112
|
+
self._layout_fn = lambda n_qubits: (
|
|
113
|
+
(i, j) for i in range(n_qubits) for j in range(i + 1, n_qubits)
|
|
114
|
+
)
|
|
115
|
+
case _:
|
|
116
|
+
if not all(
|
|
117
|
+
isinstance(ent, tuple)
|
|
118
|
+
and len(ent) == 2
|
|
119
|
+
and isinstance(ent[0], int)
|
|
120
|
+
and isinstance(ent[1], int)
|
|
121
|
+
for ent in entangling_layout
|
|
122
|
+
):
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"entangling_layout must be 'linear', 'circular', "
|
|
125
|
+
"'all_to_all', or a Sequence of tuples of integers."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self._layout_fn = lambda _: entangling_layout
|
|
129
|
+
|
|
130
|
+
def n_params_per_layer(self, n_qubits: int, **kwargs) -> int:
|
|
131
|
+
"""Total parameters = sum of gate.num_params per qubit per layer."""
|
|
132
|
+
per_qubit = sum(getattr(g, "num_params", 1) for g in self.gate_sequence)
|
|
133
|
+
return _require_trainable_params(per_qubit * n_qubits, self.name)
|
|
134
|
+
|
|
135
|
+
def build(
|
|
136
|
+
self, params, n_qubits: int, n_layers: int, **kwargs
|
|
137
|
+
) -> list[qml.operation.Operator]:
|
|
138
|
+
# calculate how many params each gate needs per qubit
|
|
139
|
+
gate_param_counts = [getattr(g, "num_params", 1) for g in self.gate_sequence]
|
|
140
|
+
per_qubit = sum(gate_param_counts)
|
|
141
|
+
|
|
142
|
+
# reshape into [layers, qubits, per_qubit]
|
|
143
|
+
params = params.reshape(n_layers, n_qubits, per_qubit)
|
|
144
|
+
layout_gen = iter(tee(self._layout_fn(n_qubits), n_layers))
|
|
145
|
+
|
|
146
|
+
operations = []
|
|
147
|
+
wires = list(range(n_qubits))
|
|
148
|
+
|
|
149
|
+
for layer_idx in range(n_layers):
|
|
150
|
+
layer_params = params[layer_idx]
|
|
151
|
+
# Single-qubit gates
|
|
152
|
+
for w, qubit_params in zip(wires, layer_params):
|
|
153
|
+
idx = 0
|
|
154
|
+
for gate, n_p in zip(self.gate_sequence, gate_param_counts):
|
|
155
|
+
theta = qubit_params[idx : idx + n_p]
|
|
156
|
+
if n_p == 0:
|
|
157
|
+
op = gate(wires=w)
|
|
158
|
+
elif n_p == 1:
|
|
159
|
+
op = gate(theta[0], wires=w)
|
|
160
|
+
else:
|
|
161
|
+
op = gate(*theta, wires=w)
|
|
162
|
+
operations.append(op)
|
|
163
|
+
idx += n_p
|
|
164
|
+
|
|
165
|
+
# Entangling gates
|
|
166
|
+
if self.entangler is not None:
|
|
167
|
+
for wire_a, wire_b in next(layout_gen):
|
|
168
|
+
op = self.entangler(wires=[wire_a, wire_b])
|
|
169
|
+
operations.append(op)
|
|
170
|
+
|
|
171
|
+
return operations
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class QAOAAnsatz(Ansatz):
|
|
175
|
+
"""
|
|
176
|
+
QAOA-style ansatz using PennyLane's QAOAEmbedding.
|
|
177
|
+
|
|
178
|
+
Implements a parameterized ansatz based on the Quantum Approximate Optimization
|
|
179
|
+
Algorithm structure, alternating between problem and mixer Hamiltonians.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def n_params_per_layer(n_qubits: int, **kwargs) -> int:
|
|
184
|
+
"""
|
|
185
|
+
Calculate the number of parameters per layer for QAOA ansatz.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
n_qubits (int): Number of qubits in the circuit.
|
|
189
|
+
**kwargs: Additional unused arguments.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
int: Number of parameters needed per layer.
|
|
193
|
+
"""
|
|
194
|
+
n_params = qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
|
|
195
|
+
return _require_trainable_params(n_params, QAOAAnsatz.__name__)
|
|
196
|
+
|
|
197
|
+
def build(
|
|
198
|
+
self, params, n_qubits: int, n_layers: int, **kwargs
|
|
199
|
+
) -> list[qml.operation.Operator]:
|
|
200
|
+
"""
|
|
201
|
+
Build the QAOA ansatz circuit.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
params: Parameter array to use for the ansatz.
|
|
205
|
+
n_qubits (int): Number of qubits.
|
|
206
|
+
n_layers (int): Number of QAOA layers.
|
|
207
|
+
**kwargs: Additional unused arguments.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
list[qml.operation.Operator]: List of operations representing the QAOA ansatz.
|
|
211
|
+
"""
|
|
212
|
+
return qml.QAOAEmbedding.compute_decomposition(
|
|
213
|
+
features=[],
|
|
214
|
+
weights=params.reshape(n_layers, -1),
|
|
215
|
+
wires=range(n_qubits),
|
|
216
|
+
local_field=qml.RY,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class HardwareEfficientAnsatz(Ansatz):
|
|
221
|
+
"""
|
|
222
|
+
Hardware-efficient ansatz (not yet implemented).
|
|
223
|
+
|
|
224
|
+
This ansatz is designed to be easily implementable on near-term quantum hardware,
|
|
225
|
+
typically using native gate sets and connectivity patterns.
|
|
226
|
+
|
|
227
|
+
Note:
|
|
228
|
+
This class is a placeholder for future implementation.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def n_params_per_layer(n_qubits: int, **kwargs) -> int:
|
|
233
|
+
"""Not yet implemented."""
|
|
234
|
+
raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
|
|
235
|
+
|
|
236
|
+
def build(
|
|
237
|
+
self, params, n_qubits: int, n_layers: int, **kwargs
|
|
238
|
+
) -> list[qml.operation.Operator]:
|
|
239
|
+
"""Not yet implemented."""
|
|
240
|
+
raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# --- Chemistry Ansaetze ---
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class UCCSDAnsatz(Ansatz):
|
|
247
|
+
"""
|
|
248
|
+
Unitary Coupled Cluster Singles and Doubles (UCCSD) ansatz.
|
|
249
|
+
|
|
250
|
+
This ansatz is specifically designed for quantum chemistry calculations,
|
|
251
|
+
implementing the UCCSD approximation which includes all single and double
|
|
252
|
+
electron excitations from a reference state.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
|
|
257
|
+
"""
|
|
258
|
+
Calculate the number of parameters per layer for UCCSD ansatz.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
n_qubits (int): Number of qubits in the circuit.
|
|
262
|
+
n_electrons (int): Number of electrons in the system.
|
|
263
|
+
**kwargs: Additional unused arguments.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
int: Number of parameters (number of single + double excitations).
|
|
267
|
+
"""
|
|
268
|
+
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
269
|
+
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
270
|
+
n_params = len(s_wires) + len(d_wires)
|
|
271
|
+
return _require_trainable_params(n_params, UCCSDAnsatz.__name__)
|
|
272
|
+
|
|
273
|
+
def build(
|
|
274
|
+
self, params, n_qubits: int, n_layers: int, **kwargs
|
|
275
|
+
) -> list[qml.operation.Operator]:
|
|
276
|
+
"""
|
|
277
|
+
Build the UCCSD ansatz circuit.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
params: Parameter array for excitation amplitudes.
|
|
281
|
+
n_qubits (int): Number of qubits.
|
|
282
|
+
n_layers (int): Number of UCCSD layers (repeats).
|
|
283
|
+
**kwargs: Additional arguments:
|
|
284
|
+
n_electrons (int): Number of electrons in the system (required).
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
list[qml.operation.Operator]: List of operations representing the UCCSD ansatz.
|
|
288
|
+
"""
|
|
289
|
+
n_electrons = kwargs.pop("n_electrons")
|
|
290
|
+
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
291
|
+
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
292
|
+
hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
|
|
293
|
+
|
|
294
|
+
return qml.UCCSD.compute_decomposition(
|
|
295
|
+
params.reshape(n_layers, -1),
|
|
296
|
+
wires=range(n_qubits),
|
|
297
|
+
s_wires=s_wires,
|
|
298
|
+
d_wires=d_wires,
|
|
299
|
+
init_state=hf_state,
|
|
300
|
+
n_repeats=n_layers,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class HartreeFockAnsatz(Ansatz):
|
|
305
|
+
"""
|
|
306
|
+
Hartree-Fock-based ansatz for quantum chemistry.
|
|
307
|
+
|
|
308
|
+
This ansatz prepares the Hartree-Fock reference state and applies
|
|
309
|
+
parameterized single and double excitation gates. It's a simplified
|
|
310
|
+
alternative to UCCSD, often used as a starting point for VQE calculations.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
|
|
315
|
+
"""
|
|
316
|
+
Calculate the number of parameters per layer for Hartree-Fock ansatz.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
n_qubits (int): Number of qubits in the circuit.
|
|
320
|
+
n_electrons (int): Number of electrons in the system.
|
|
321
|
+
**kwargs: Additional unused arguments.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
int: Number of parameters (number of single + double excitations).
|
|
325
|
+
"""
|
|
326
|
+
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
327
|
+
n_params = len(singles) + len(doubles)
|
|
328
|
+
return _require_trainable_params(n_params, HartreeFockAnsatz.__name__)
|
|
329
|
+
|
|
330
|
+
def build(
|
|
331
|
+
self, params, n_qubits: int, n_layers: int, **kwargs
|
|
332
|
+
) -> list[qml.operation.Operator]:
|
|
333
|
+
"""
|
|
334
|
+
Build the Hartree-Fock ansatz circuit.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
params: Parameter array for excitation amplitudes.
|
|
338
|
+
n_qubits (int): Number of qubits.
|
|
339
|
+
n_layers (int): Number of ansatz layers.
|
|
340
|
+
**kwargs: Additional arguments:
|
|
341
|
+
n_electrons (int): Number of electrons in the system (required).
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
list[qml.operation.Operator]: List of operations representing the Hartree-Fock ansatz.
|
|
345
|
+
"""
|
|
346
|
+
n_electrons = kwargs.pop("n_electrons")
|
|
347
|
+
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
348
|
+
hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
|
|
349
|
+
|
|
350
|
+
operations = []
|
|
351
|
+
for layer_params in params.reshape(n_layers, -1):
|
|
352
|
+
operations.extend(
|
|
353
|
+
qml.AllSinglesDoubles.compute_decomposition(
|
|
354
|
+
layer_params,
|
|
355
|
+
wires=range(n_qubits),
|
|
356
|
+
hf_state=hf_state,
|
|
357
|
+
singles=singles,
|
|
358
|
+
doubles=doubles,
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Reset the BasisState operations after the first layer
|
|
363
|
+
# for behaviour similar to UCCSD ansatz
|
|
364
|
+
for op in operations[len(operations) // 2 :]:
|
|
365
|
+
if hasattr(op, "_hyperparameters") and "hf_state" in op._hyperparameters:
|
|
366
|
+
op._hyperparameters["hf_state"] = 0
|
|
367
|
+
|
|
368
|
+
return operations
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from warnings import warn
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pennylane as qml
|
|
10
|
+
import sympy as sp
|
|
11
|
+
from qiskit import QuantumCircuit
|
|
12
|
+
|
|
13
|
+
from divi.circuits import CircuitBundle, MetaCircuit
|
|
14
|
+
from divi.qprog._hamiltonians import _clean_hamiltonian
|
|
15
|
+
from divi.qprog.variational_quantum_algorithm import VariationalQuantumAlgorithm
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CustomVQA(VariationalQuantumAlgorithm):
|
|
19
|
+
"""Custom variational algorithm for a parameterized QuantumScript.
|
|
20
|
+
|
|
21
|
+
This implementation wraps a PennyLane QuantumScript (or converts a Qiskit
|
|
22
|
+
QuantumCircuit into one) and optimizes its trainable parameters to minimize
|
|
23
|
+
a single expectation-value measurement. Qiskit measurements are converted
|
|
24
|
+
into a PauliZ expectation on the measured wires. Parameters are bound to sympy
|
|
25
|
+
symbols to enable QASM substitution and reuse of MetaCircuit templates
|
|
26
|
+
during optimization.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
qscript (qml.tape.QuantumScript): The parameterized QuantumScript.
|
|
30
|
+
param_shape (tuple[int, ...]): Shape of a single parameter set.
|
|
31
|
+
n_qubits (int): Number of qubits in the script.
|
|
32
|
+
n_layers (int): Layer count (fixed to 1 for this wrapper).
|
|
33
|
+
cost_hamiltonian (qml.operation.Operator): Observable being minimized.
|
|
34
|
+
loss_constant (float): Constant term extracted from the observable.
|
|
35
|
+
optimizer (Optimizer): Classical optimizer for parameter updates.
|
|
36
|
+
max_iterations (int): Maximum number of optimization iterations.
|
|
37
|
+
current_iteration (int): Current optimization iteration.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
qscript: qml.tape.QuantumScript | QuantumCircuit,
|
|
43
|
+
*,
|
|
44
|
+
param_shape: tuple[int, ...] | int | None = None,
|
|
45
|
+
max_iterations: int = 10,
|
|
46
|
+
**kwargs,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize a CustomVQA instance.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
qscript (qml.tape.QuantumScript | QuantumCircuit): A parameterized QuantumScript with a
|
|
52
|
+
single expectation-value measurement, or a Qiskit QuantumCircuit with
|
|
53
|
+
computational basis measurements.
|
|
54
|
+
param_shape (tuple[int, ...] | int | None): Shape of a single parameter
|
|
55
|
+
set. If None, uses a flat shape inferred from trainable parameters.
|
|
56
|
+
max_iterations (int): Maximum number of optimization iterations.
|
|
57
|
+
**kwargs: Additional keyword arguments passed to the parent class, including
|
|
58
|
+
backend and optimizer.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
TypeError: If qscript is not a supported PennyLane QuantumScript or Qiskit QuantumCircuit.
|
|
62
|
+
ValueError: If the script has an invalid measurement or no trainable parameters.
|
|
63
|
+
"""
|
|
64
|
+
super().__init__(**kwargs)
|
|
65
|
+
|
|
66
|
+
self._qiskit_param_names = (
|
|
67
|
+
[param.name for param in qscript.parameters]
|
|
68
|
+
if isinstance(qscript, QuantumCircuit)
|
|
69
|
+
else None
|
|
70
|
+
)
|
|
71
|
+
self.qscript = self._coerce_to_quantum_script(qscript)
|
|
72
|
+
|
|
73
|
+
if len(self.qscript.measurements) != 1:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"QuantumScript must contain exactly one measurement for optimization."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
measurement = self.qscript.measurements[0]
|
|
79
|
+
if not hasattr(measurement, "obs") or measurement.obs is None:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"QuantumScript must contain a single expectation-value measurement."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self._cost_hamiltonian, self.loss_constant = _clean_hamiltonian(measurement.obs)
|
|
85
|
+
if (
|
|
86
|
+
isinstance(self._cost_hamiltonian, qml.Hamiltonian)
|
|
87
|
+
and not self._cost_hamiltonian.operands
|
|
88
|
+
):
|
|
89
|
+
raise ValueError("Hamiltonian contains only constant terms.")
|
|
90
|
+
|
|
91
|
+
self.n_qubits = self.qscript.num_wires
|
|
92
|
+
self.n_layers = 1
|
|
93
|
+
self.max_iterations = max_iterations
|
|
94
|
+
self.current_iteration = 0
|
|
95
|
+
|
|
96
|
+
trainable_param_indices = (
|
|
97
|
+
list(self.qscript.trainable_params)
|
|
98
|
+
if self.qscript.trainable_params
|
|
99
|
+
else list(range(len(self.qscript.get_parameters())))
|
|
100
|
+
)
|
|
101
|
+
if not trainable_param_indices:
|
|
102
|
+
raise ValueError("QuantumScript does not contain any trainable parameters.")
|
|
103
|
+
|
|
104
|
+
self._param_shape = self._resolve_param_shape(
|
|
105
|
+
param_shape, len(trainable_param_indices)
|
|
106
|
+
)
|
|
107
|
+
self._n_params = int(np.prod(self._param_shape))
|
|
108
|
+
|
|
109
|
+
self._trainable_param_indices = trainable_param_indices
|
|
110
|
+
self._param_symbols = (
|
|
111
|
+
np.array(
|
|
112
|
+
[sp.Symbol(name) for name in self._qiskit_param_names], dtype=object
|
|
113
|
+
).reshape(self._param_shape)
|
|
114
|
+
if self._qiskit_param_names is not None
|
|
115
|
+
else sp.symarray("p", self._param_shape)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
flat_symbols = self._param_symbols.flatten().tolist()
|
|
119
|
+
self._qscript = self.qscript.bind_new_parameters(
|
|
120
|
+
flat_symbols, self._trainable_param_indices
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def cost_hamiltonian(self) -> qml.operation.Operator:
|
|
125
|
+
"""The cost Hamiltonian for the QuantumScript optimization."""
|
|
126
|
+
return self._cost_hamiltonian
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def param_shape(self) -> tuple[int, ...]:
|
|
130
|
+
"""Shape of a single parameter set."""
|
|
131
|
+
return self._param_shape
|
|
132
|
+
|
|
133
|
+
def _resolve_param_shape(
|
|
134
|
+
self, param_shape: tuple[int, ...] | int | None, n_params: int
|
|
135
|
+
) -> tuple[int, ...]:
|
|
136
|
+
"""Validate and normalize the parameter shape.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
param_shape (tuple[int, ...] | int | None): User-provided parameter shape.
|
|
140
|
+
n_params (int): Number of trainable parameters in the script.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
tuple[int, ...]: Normalized parameter shape.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If the shape is invalid or does not match n_params.
|
|
147
|
+
"""
|
|
148
|
+
if param_shape is None:
|
|
149
|
+
return (n_params,)
|
|
150
|
+
|
|
151
|
+
param_shape = (param_shape,) if isinstance(param_shape, int) else param_shape
|
|
152
|
+
|
|
153
|
+
if any(dim <= 0 for dim in param_shape):
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"param_shape entries must be positive, got {param_shape}."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if int(np.prod(param_shape)) != n_params:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"param_shape does not match the number of trainable parameters. "
|
|
161
|
+
f"Expected product {n_params}, got {int(np.prod(param_shape))}."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return tuple(param_shape)
|
|
165
|
+
|
|
166
|
+
def _coerce_to_quantum_script(
|
|
167
|
+
self,
|
|
168
|
+
qscript: qml.tape.QuantumScript | QuantumCircuit,
|
|
169
|
+
) -> qml.tape.QuantumScript:
|
|
170
|
+
"""Convert supported inputs into a PennyLane QuantumScript.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
qscript (qml.tape.QuantumScript): Input QuantumScript or Qiskit QuantumCircuit.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
qml.tape.QuantumScript: The converted QuantumScript.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
TypeError: If the input type is unsupported.
|
|
180
|
+
"""
|
|
181
|
+
if isinstance(qscript, qml.tape.QuantumScript):
|
|
182
|
+
return qscript
|
|
183
|
+
|
|
184
|
+
if isinstance(qscript, QuantumCircuit):
|
|
185
|
+
measured_wires = sorted(
|
|
186
|
+
{
|
|
187
|
+
qscript.qubits.index(qubit)
|
|
188
|
+
for instruction in qscript.data
|
|
189
|
+
if instruction.operation.name == "measure"
|
|
190
|
+
for qubit in instruction.qubits
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
if not measured_wires:
|
|
194
|
+
warn(
|
|
195
|
+
"Provided QuantumCircuit has no measurement operations. "
|
|
196
|
+
"Defaulting to measuring all wires with PauliZ.",
|
|
197
|
+
UserWarning,
|
|
198
|
+
)
|
|
199
|
+
measured_wires = list(range(len(qscript.qubits)))
|
|
200
|
+
|
|
201
|
+
obs = (
|
|
202
|
+
qml.Z(measured_wires[0])
|
|
203
|
+
if len(measured_wires) == 1
|
|
204
|
+
else qml.sum(*(qml.Z(wire) for wire in measured_wires))
|
|
205
|
+
)
|
|
206
|
+
# Remove measurements before conversion to avoid MidMeasureMP issues
|
|
207
|
+
qc_no_measure = QuantumCircuit(qscript.num_qubits)
|
|
208
|
+
for instruction in qscript.data:
|
|
209
|
+
if instruction.operation.name != "measure":
|
|
210
|
+
qc_no_measure.append(
|
|
211
|
+
instruction.operation, instruction.qubits, instruction.clbits
|
|
212
|
+
)
|
|
213
|
+
qfunc = qml.from_qiskit(qc_no_measure)
|
|
214
|
+
qiskit_params = [
|
|
215
|
+
qml.numpy.array(0.0, requires_grad=True) for _ in qscript.parameters
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
def qfunc_with_measurement(*params):
|
|
219
|
+
qfunc(*params)
|
|
220
|
+
return qml.expval(obs)
|
|
221
|
+
|
|
222
|
+
return qml.tape.make_qscript(qfunc_with_measurement)(*qiskit_params)
|
|
223
|
+
|
|
224
|
+
raise TypeError(
|
|
225
|
+
"qscript must be a PennyLane QuantumScript or a Qiskit QuantumCircuit."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
|
|
229
|
+
"""Create the meta-circuit dictionary for CustomVQA.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
dict[str, MetaCircuit]: Dictionary containing the cost circuit template.
|
|
233
|
+
"""
|
|
234
|
+
return {
|
|
235
|
+
"cost_circuit": self._meta_circuit_factory(
|
|
236
|
+
self._qscript, symbols=self._param_symbols.flatten()
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
def _generate_circuits(self) -> list[CircuitBundle]:
|
|
241
|
+
"""Generate circuits for the current parameter sets.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
list[CircuitBundle]: Circuit bundles tagged by parameter index.
|
|
245
|
+
"""
|
|
246
|
+
return [
|
|
247
|
+
self.meta_circuits["cost_circuit"].initialize_circuit_from_params(
|
|
248
|
+
params_group, param_idx=p
|
|
249
|
+
)
|
|
250
|
+
for p, params_group in enumerate(self._curr_params)
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
def _perform_final_computation(self, **kwargs) -> None:
|
|
254
|
+
"""No-op by default for custom QuantumScript optimization."""
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
def _save_subclass_state(self) -> dict[str, Any]:
|
|
258
|
+
"""Save subclass-specific state for checkpointing."""
|
|
259
|
+
return {}
|
|
260
|
+
|
|
261
|
+
def _load_subclass_state(self, state: dict[str, Any]) -> None:
|
|
262
|
+
"""Load subclass-specific state from a checkpoint."""
|
|
263
|
+
pass
|