qoro-divi 0.3.3__py3-none-any.whl → 0.3.5__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/__init__.py +1 -2
- divi/backends/__init__.py +7 -0
- divi/backends/_circuit_runner.py +46 -0
- divi/{parallel_simulator.py → backends/_parallel_simulator.py} +136 -53
- divi/backends/_qoro_service.py +531 -0
- divi/circuits/__init__.py +5 -0
- divi/circuits/_core.py +226 -0
- divi/{qasm.py → circuits/qasm.py} +21 -2
- divi/{exp → extern}/cirq/_validator.py +9 -7
- divi/qprog/__init__.py +18 -5
- divi/qprog/algorithms/__init__.py +14 -0
- divi/qprog/algorithms/_ansatze.py +311 -0
- divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +69 -41
- divi/qprog/{_vqe.py → algorithms/_vqe.py} +79 -135
- divi/qprog/batch.py +239 -55
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +219 -18
- divi/qprog/quantum_program.py +389 -57
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +3 -34
- divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +42 -25
- divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +59 -26
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +112 -0
- divi/{qlogger.py → reporting/_qlogger.py} +37 -2
- divi/{reporter.py → reporting/_reporter.py} +8 -14
- divi/utils.py +49 -10
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/METADATA +2 -1
- qoro_divi-0.3.5.dist-info/RECORD +69 -0
- divi/_pbar.py +0 -70
- divi/circuits.py +0 -139
- divi/interfaces.py +0 -25
- divi/qoro_service.py +0 -425
- qoro_divi-0.3.3.dist-info/RECORD +0 -62
- /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
- /divi/{qem.py → circuits/qem.py} +0 -0
- /divi/{exp → extern}/cirq/__init__.py +0 -0
- /divi/{exp → extern}/cirq/_lexer.py +0 -0
- /divi/{exp → extern}/cirq/_parser.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
- /divi/{exp → extern}/cirq/exception.py +0 -0
- /divi/{exp → extern}/scipy/_cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
- /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/WHEEL +0 -0
divi/circuits/_core.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from itertools import product
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
import dill
|
|
11
|
+
import pennylane as qml
|
|
12
|
+
from pennylane.transforms.core.transform_program import TransformProgram
|
|
13
|
+
|
|
14
|
+
from divi.circuits.qasm import to_openqasm
|
|
15
|
+
from divi.circuits.qem import QEMProtocol
|
|
16
|
+
|
|
17
|
+
TRANSFORM_PROGRAM = TransformProgram()
|
|
18
|
+
TRANSFORM_PROGRAM.add_transform(qml.transforms.split_to_single_terms)
|
|
19
|
+
TRANSFORM_PROGRAM.add_transform(qml.transforms.split_non_commuting)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Circuit:
|
|
23
|
+
"""
|
|
24
|
+
Represents a quantum circuit with its QASM representation and metadata.
|
|
25
|
+
|
|
26
|
+
This class encapsulates a PennyLane quantum circuit along with its OpenQASM
|
|
27
|
+
serialization and associated tags for identification. Each circuit instance
|
|
28
|
+
is assigned a unique ID for tracking purposes.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
main_circuit: The PennyLane quantum circuit/tape object.
|
|
32
|
+
tags (list[str]): List of string tags for circuit identification.
|
|
33
|
+
qasm_circuits (list[str]): List of OpenQASM string representations.
|
|
34
|
+
circuit_id (int): Unique identifier for this circuit instance.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_id_counter = 0
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
main_circuit,
|
|
42
|
+
tags: list[str],
|
|
43
|
+
qasm_circuits: list[str] = None,
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Initialize a Circuit instance.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
main_circuit: A PennyLane quantum circuit or tape object to be wrapped.
|
|
50
|
+
tags (list[str]): List of string tags for identifying this circuit.
|
|
51
|
+
qasm_circuits (list[str], optional): Pre-computed OpenQASM string
|
|
52
|
+
representations. If None, they will be generated from main_circuit.
|
|
53
|
+
Defaults to None.
|
|
54
|
+
"""
|
|
55
|
+
self.main_circuit = main_circuit
|
|
56
|
+
self.tags = tags
|
|
57
|
+
|
|
58
|
+
self.qasm_circuits = qasm_circuits
|
|
59
|
+
|
|
60
|
+
if self.qasm_circuits is None:
|
|
61
|
+
self.qasm_circuits = to_openqasm(
|
|
62
|
+
self.main_circuit,
|
|
63
|
+
measurement_groups=[self.main_circuit.measurements],
|
|
64
|
+
return_measurements_separately=False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self.circuit_id = Circuit._id_counter
|
|
68
|
+
Circuit._id_counter += 1
|
|
69
|
+
|
|
70
|
+
def __str__(self):
|
|
71
|
+
"""
|
|
72
|
+
Return a string representation of the circuit.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: String in format "Circuit: {circuit_id}".
|
|
76
|
+
"""
|
|
77
|
+
return f"Circuit: {self.circuit_id}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MetaCircuit:
|
|
81
|
+
"""
|
|
82
|
+
A parameterized quantum circuit template for batch circuit generation.
|
|
83
|
+
|
|
84
|
+
MetaCircuit represents a symbolic quantum circuit that can be instantiated
|
|
85
|
+
multiple times with different parameter values. It handles circuit compilation,
|
|
86
|
+
observable grouping, and measurement decomposition for efficient execution.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
main_circuit: The PennyLane quantum circuit with symbolic parameters.
|
|
90
|
+
symbols: Array of sympy symbols used as circuit parameters.
|
|
91
|
+
qem_protocol (QEMProtocol): Quantum error mitigation protocol to apply.
|
|
92
|
+
compiled_circuits_bodies (list[str]): QASM bodies without measurements.
|
|
93
|
+
measurements (list[str]): QASM measurement strings.
|
|
94
|
+
measurement_groups (list[list]): Grouped observables for each circuit variant.
|
|
95
|
+
postprocessing_fn: Function to combine measurement results.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
main_circuit,
|
|
101
|
+
symbols,
|
|
102
|
+
grouping_strategy: Literal["wires", "default", "qwc"] | None = None,
|
|
103
|
+
qem_protocol: QEMProtocol | None = None,
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Initialize a MetaCircuit with symbolic parameters.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
main_circuit: A PennyLane quantum circuit/tape with symbolic parameters.
|
|
110
|
+
symbols: Array of sympy Symbol objects representing circuit parameters.
|
|
111
|
+
grouping_strategy (str, optional): Strategy for grouping commuting
|
|
112
|
+
observables. Options are "wires", "default", or "qwc" (qubit-wise
|
|
113
|
+
commuting). Defaults to None.
|
|
114
|
+
qem_protocol (QEMProtocol, optional): Quantum error mitigation protocol
|
|
115
|
+
to apply to the circuits. Defaults to None.
|
|
116
|
+
"""
|
|
117
|
+
self.main_circuit = main_circuit
|
|
118
|
+
self.symbols = symbols
|
|
119
|
+
self.qem_protocol = qem_protocol
|
|
120
|
+
|
|
121
|
+
transform_program = deepcopy(TRANSFORM_PROGRAM)
|
|
122
|
+
transform_program[1].kwargs["grouping_strategy"] = grouping_strategy
|
|
123
|
+
|
|
124
|
+
qscripts, self.postprocessing_fn = transform_program((main_circuit,))
|
|
125
|
+
|
|
126
|
+
self.compiled_circuits_bodies, self.measurements = to_openqasm(
|
|
127
|
+
main_circuit,
|
|
128
|
+
measurement_groups=[qsc.measurements for qsc in qscripts],
|
|
129
|
+
return_measurements_separately=True,
|
|
130
|
+
# TODO: optimize later
|
|
131
|
+
measure_all=True,
|
|
132
|
+
symbols=self.symbols,
|
|
133
|
+
qem_protocol=qem_protocol,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Need to store the measurement groups for computing
|
|
137
|
+
# expectation values later on, stripped of the `qml.expval` wrapper
|
|
138
|
+
self.measurement_groups = [
|
|
139
|
+
[meas.obs for meas in qsc.measurements] for qsc in qscripts
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
def __getstate__(self):
|
|
143
|
+
"""
|
|
144
|
+
Prepare the MetaCircuit for pickling.
|
|
145
|
+
|
|
146
|
+
Serializes the postprocessing function using dill since regular pickle
|
|
147
|
+
cannot handle certain PennyLane function objects.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
dict: State dictionary with serialized postprocessing function.
|
|
151
|
+
"""
|
|
152
|
+
state = self.__dict__.copy()
|
|
153
|
+
state["postprocessing_fn"] = dill.dumps(self.postprocessing_fn)
|
|
154
|
+
return state
|
|
155
|
+
|
|
156
|
+
def __setstate__(self, state):
|
|
157
|
+
"""
|
|
158
|
+
Restore the MetaCircuit from a pickled state.
|
|
159
|
+
|
|
160
|
+
Deserializes the postprocessing function that was serialized with dill
|
|
161
|
+
during pickling.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
state (dict): State dictionary from pickling with serialized
|
|
165
|
+
postprocessing function.
|
|
166
|
+
"""
|
|
167
|
+
state["postprocessing_fn"] = dill.loads(state["postprocessing_fn"])
|
|
168
|
+
|
|
169
|
+
self.__dict__.update(state)
|
|
170
|
+
|
|
171
|
+
def initialize_circuit_from_params(
|
|
172
|
+
self, param_list, tag_prefix: str = "", precision: int = 8
|
|
173
|
+
) -> Circuit:
|
|
174
|
+
"""
|
|
175
|
+
Instantiate a concrete Circuit by substituting symbolic parameters with values.
|
|
176
|
+
|
|
177
|
+
Takes a list of parameter values and creates a fully instantiated Circuit
|
|
178
|
+
by replacing all symbolic parameters in the QASM representations with their
|
|
179
|
+
concrete numerical values.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
param_list: Array of numerical parameter values to substitute for symbols.
|
|
183
|
+
Must match the length and order of self.symbols.
|
|
184
|
+
tag_prefix (str, optional): Prefix to prepend to circuit tags for
|
|
185
|
+
identification. Defaults to "".
|
|
186
|
+
precision (int, optional): Number of decimal places for parameter values
|
|
187
|
+
in the QASM output. Defaults to 8.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Circuit: A new Circuit instance with parameters substituted and proper
|
|
191
|
+
tags for identification.
|
|
192
|
+
|
|
193
|
+
Note:
|
|
194
|
+
The main_circuit attribute in the returned Circuit still contains
|
|
195
|
+
symbolic parameters. Only the QASM representations have concrete values.
|
|
196
|
+
"""
|
|
197
|
+
mapping = dict(
|
|
198
|
+
zip(
|
|
199
|
+
map(lambda x: re.escape(str(x)), self.symbols),
|
|
200
|
+
map(lambda x: f"{x:.{precision}f}", param_list),
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
pattern = re.compile("|".join(k for k in mapping.keys()))
|
|
204
|
+
|
|
205
|
+
final_qasm_strs = []
|
|
206
|
+
for circuit_body in self.compiled_circuits_bodies:
|
|
207
|
+
final_qasm_strs.append(
|
|
208
|
+
pattern.sub(lambda match: mapping[match.group(0)], circuit_body)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
tags = []
|
|
212
|
+
qasm_circuits = []
|
|
213
|
+
for (i, body_str), (j, meas_str) in product(
|
|
214
|
+
enumerate(final_qasm_strs), enumerate(self.measurements)
|
|
215
|
+
):
|
|
216
|
+
qasm_circuits.append(body_str + meas_str)
|
|
217
|
+
|
|
218
|
+
nonempty_subtags = filter(
|
|
219
|
+
None,
|
|
220
|
+
[tag_prefix, f"{self.qem_protocol.name}:{i}", str(j)],
|
|
221
|
+
)
|
|
222
|
+
tags.append("_".join(nonempty_subtags))
|
|
223
|
+
|
|
224
|
+
# Note: The main circuit's parameters are still in symbol form.
|
|
225
|
+
# Not sure if it is necessary for any useful application to parameterize them.
|
|
226
|
+
return Circuit(self.main_circuit, qasm_circuits=qasm_circuits, tags=tags)
|
|
@@ -14,8 +14,8 @@ from pennylane.tape import QuantumScript
|
|
|
14
14
|
from pennylane.wires import Wires
|
|
15
15
|
from sympy import Symbol
|
|
16
16
|
|
|
17
|
-
from divi.
|
|
18
|
-
from divi.
|
|
17
|
+
from divi.circuits.qem import QEMProtocol
|
|
18
|
+
from divi.extern.cirq import cirq_circuit_from_qasm
|
|
19
19
|
|
|
20
20
|
OPENQASM_GATES = {
|
|
21
21
|
"CNOT": "cx",
|
|
@@ -46,6 +46,25 @@ OPENQASM_GATES = {
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def _ops_to_qasm(operations, precision, wires):
|
|
49
|
+
"""
|
|
50
|
+
Convert PennyLane operations to OpenQASM instruction strings.
|
|
51
|
+
|
|
52
|
+
Translates a sequence of PennyLane quantum operations into their OpenQASM
|
|
53
|
+
2.0 equivalent representations. Each operation is mapped to its corresponding
|
|
54
|
+
QASM gate with appropriate parameters and wire labels.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
operations: Sequence of PennyLane operation objects to convert.
|
|
58
|
+
precision (int | None): Number of decimal places for parameter values.
|
|
59
|
+
If None, uses default Python string formatting.
|
|
60
|
+
wires: Wire labels used in the circuit for indexing.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: OpenQASM instruction string with each operation on a new line.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If an operation is not supported by the QASM serializer.
|
|
67
|
+
"""
|
|
49
68
|
# create the QASM code representing the operations
|
|
50
69
|
qasm_str = ""
|
|
51
70
|
|
|
@@ -289,7 +289,12 @@ class Parser:
|
|
|
289
289
|
# ---- gate definitions ----
|
|
290
290
|
def gate_def(self):
|
|
291
291
|
self.match("GATE")
|
|
292
|
-
|
|
292
|
+
gname_tok = self.match("ID")
|
|
293
|
+
gname = gname_tok.value
|
|
294
|
+
if gname in BUILTINS:
|
|
295
|
+
raise SyntaxError(
|
|
296
|
+
f"Cannot redefine built-in gate '{gname}' at {gname_tok.line}:{gname_tok.col}"
|
|
297
|
+
)
|
|
293
298
|
if gname in self.user_gates:
|
|
294
299
|
self._dupe(gname)
|
|
295
300
|
params: tuple[str, ...] = ()
|
|
@@ -570,7 +575,6 @@ class Parser:
|
|
|
570
575
|
if t.type == "PI":
|
|
571
576
|
self.match("PI")
|
|
572
577
|
return
|
|
573
|
-
# ---- NEW BLOCK TO HANDLE MATH FUNCTIONS ----
|
|
574
578
|
if t.type in _MATH_FUNCS:
|
|
575
579
|
self.match(t.type) # Consume the function name (e.g., COS)
|
|
576
580
|
self.match("LPAREN")
|
|
@@ -578,13 +582,11 @@ class Parser:
|
|
|
578
582
|
# Note: QASM 2.0 math functions only take one argument
|
|
579
583
|
self.match("RPAREN")
|
|
580
584
|
return
|
|
581
|
-
# --------------------------------------------
|
|
582
585
|
if t.type == "ID":
|
|
583
586
|
# function call or plain ID
|
|
584
587
|
id_tok = self.match("ID")
|
|
585
588
|
ident = id_tok.value
|
|
586
589
|
if self.accept("LPAREN"):
|
|
587
|
-
# This now correctly handles user-defined functions (if any)
|
|
588
590
|
if self.peek().type != "RPAREN":
|
|
589
591
|
self._expr(allow_id)
|
|
590
592
|
while self.accept("COMMA"):
|
|
@@ -637,9 +639,9 @@ def validate_qasm_raise(src: str) -> None:
|
|
|
637
639
|
Parser(toks).parse()
|
|
638
640
|
|
|
639
641
|
|
|
640
|
-
def is_valid_qasm(src: str) -> bool:
|
|
642
|
+
def is_valid_qasm(src: str) -> bool | str:
|
|
641
643
|
try:
|
|
642
644
|
validate_qasm_raise(src)
|
|
643
645
|
return True
|
|
644
|
-
except SyntaxError:
|
|
645
|
-
return
|
|
646
|
+
except SyntaxError as e:
|
|
647
|
+
return str(e)
|
divi/qprog/__init__.py
CHANGED
|
@@ -5,9 +5,22 @@
|
|
|
5
5
|
# isort: skip_file
|
|
6
6
|
from .quantum_program import QuantumProgram
|
|
7
7
|
from .batch import ProgramBatch
|
|
8
|
-
from .
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
from .algorithms import (
|
|
9
|
+
QAOA,
|
|
10
|
+
GraphProblem,
|
|
11
|
+
VQE,
|
|
12
|
+
Ansatz,
|
|
13
|
+
UCCSDAnsatz,
|
|
14
|
+
QAOAAnsatz,
|
|
15
|
+
HardwareEfficientAnsatz,
|
|
16
|
+
HartreeFockAnsatz,
|
|
17
|
+
GenericLayerAnsatz,
|
|
18
|
+
)
|
|
19
|
+
from .workflows import (
|
|
20
|
+
GraphPartitioningQAOA,
|
|
21
|
+
PartitioningConfig,
|
|
22
|
+
QUBOPartitioningQAOA,
|
|
23
|
+
VQEHyperparameterSweep,
|
|
24
|
+
MoleculeTransformer,
|
|
25
|
+
)
|
|
13
26
|
from .optimizers import ScipyOptimizer, ScipyMethod, MonteCarloOptimizer
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from ._ansatze import (
|
|
6
|
+
Ansatz,
|
|
7
|
+
GenericLayerAnsatz,
|
|
8
|
+
HardwareEfficientAnsatz,
|
|
9
|
+
HartreeFockAnsatz,
|
|
10
|
+
QAOAAnsatz,
|
|
11
|
+
UCCSDAnsatz,
|
|
12
|
+
)
|
|
13
|
+
from ._qaoa import QAOA, GraphProblem, GraphProblemTypes, QUBOProblemTypes
|
|
14
|
+
from ._vqe import VQE
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 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
|
+
class Ansatz(ABC):
|
|
14
|
+
"""Abstract base class for all VQE ansaetze."""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def name(self) -> str:
|
|
18
|
+
"""Returns the human-readable name of the ansatz."""
|
|
19
|
+
return self.__class__.__name__
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def n_params_per_layer(n_qubits: int, **kwargs) -> int:
|
|
24
|
+
"""Returns the number of parameters required by the ansatz for one layer."""
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs):
|
|
29
|
+
"""
|
|
30
|
+
Builds the ansatz circuit.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
params (array): The parameters (weights) for the ansatz.
|
|
34
|
+
n_qubits (int): The number of qubits.
|
|
35
|
+
n_layers (int): The number of layers.
|
|
36
|
+
**kwargs: Additional arguments like n_electrons for chemistry ansaetze.
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- Template Ansaetze ---
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GenericLayerAnsatz(Ansatz):
|
|
45
|
+
"""
|
|
46
|
+
A flexible ansatz alternating single-qubit gates with optional entanglers.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
gate_sequence: list[qml.operation.Operator],
|
|
52
|
+
entangler: qml.operation.Operator | None = None,
|
|
53
|
+
entangling_layout: (
|
|
54
|
+
Literal["linear", "brick", "circular", "all-to-all"]
|
|
55
|
+
| Sequence[tuple[int, int]]
|
|
56
|
+
| None
|
|
57
|
+
) = None,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Args:
|
|
61
|
+
gate_sequence (list[Callable]): List of one-qubit gate classes (e.g., qml.RY, qml.Rot).
|
|
62
|
+
entangler (Callable): Two-qubit entangling gate class (e.g., qml.CNOT, qml.CZ).
|
|
63
|
+
If None, no entanglement is applied.
|
|
64
|
+
entangling_layout (str): Layout for entangling layer ("linear", "all_to_all", etc.).
|
|
65
|
+
"""
|
|
66
|
+
if not all(
|
|
67
|
+
issubclass(g, qml.operation.Operator) and g.num_wires == 1
|
|
68
|
+
for g in gate_sequence
|
|
69
|
+
):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"All elements in gate_sequence must be PennyLane one-qubit gate classes."
|
|
72
|
+
)
|
|
73
|
+
self.gate_sequence = gate_sequence
|
|
74
|
+
|
|
75
|
+
if entangler not in (None, qml.CNOT, qml.CZ):
|
|
76
|
+
raise ValueError("Only qml.CNOT and qml.CZ are supported as entanglers.")
|
|
77
|
+
self.entangler = entangler
|
|
78
|
+
|
|
79
|
+
self.entangling_layout = entangling_layout
|
|
80
|
+
if entangler is None and self.entangling_layout is not None:
|
|
81
|
+
warn("`entangling_layout` provided but `entangler` is None.")
|
|
82
|
+
match self.entangling_layout:
|
|
83
|
+
case None | "linear":
|
|
84
|
+
self.entangling_layout = "linear"
|
|
85
|
+
|
|
86
|
+
self._layout_fn = lambda n_qubits: zip(
|
|
87
|
+
range(n_qubits), range(1, n_qubits)
|
|
88
|
+
)
|
|
89
|
+
case "brick":
|
|
90
|
+
self._layout_fn = lambda n_qubits: [
|
|
91
|
+
(i, i + 1) for r in range(2) for i in range(r, n_qubits - 1, 2)
|
|
92
|
+
]
|
|
93
|
+
case "circular":
|
|
94
|
+
self._layout_fn = lambda n_qubits: zip(
|
|
95
|
+
range(n_qubits), [(i + 1) % n_qubits for i in range(n_qubits)]
|
|
96
|
+
)
|
|
97
|
+
case "all_to_all":
|
|
98
|
+
self._layout_fn = lambda n_qubits: (
|
|
99
|
+
(i, j) for i in range(n_qubits) for j in range(i + 1, n_qubits)
|
|
100
|
+
)
|
|
101
|
+
case _:
|
|
102
|
+
if not all(
|
|
103
|
+
isinstance(ent, tuple)
|
|
104
|
+
and len(ent) == 2
|
|
105
|
+
and isinstance(ent[0], int)
|
|
106
|
+
and isinstance(ent[1], int)
|
|
107
|
+
for ent in entangling_layout
|
|
108
|
+
):
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"entangling_layout must be 'linear', 'circular', "
|
|
111
|
+
"'all_to_all', or a Sequence of tuples of integers."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
self._layout_fn = lambda _: entangling_layout
|
|
115
|
+
|
|
116
|
+
def n_params_per_layer(self, n_qubits: int, **kwargs) -> int:
|
|
117
|
+
"""Total parameters = sum of gate.num_params per qubit per layer."""
|
|
118
|
+
per_qubit = sum(getattr(g, "num_params", 1) for g in self.gate_sequence)
|
|
119
|
+
return per_qubit * n_qubits
|
|
120
|
+
|
|
121
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs):
|
|
122
|
+
# calculate how many params each gate needs per qubit
|
|
123
|
+
gate_param_counts = [getattr(g, "num_params", 1) for g in self.gate_sequence]
|
|
124
|
+
per_qubit = sum(gate_param_counts)
|
|
125
|
+
|
|
126
|
+
# reshape into [layers, qubits, per_qubit]
|
|
127
|
+
params = params.reshape(n_layers, n_qubits, per_qubit)
|
|
128
|
+
layout_gen = iter(tee(self._layout_fn(n_qubits), n_layers))
|
|
129
|
+
|
|
130
|
+
def _layer(layer_params, wires):
|
|
131
|
+
for w, qubit_params in zip(wires, layer_params):
|
|
132
|
+
idx = 0
|
|
133
|
+
for gate, n_p in zip(self.gate_sequence, gate_param_counts):
|
|
134
|
+
theta = qubit_params[idx : idx + n_p]
|
|
135
|
+
gate(*theta, wires=w)
|
|
136
|
+
idx += n_p
|
|
137
|
+
|
|
138
|
+
if self.entangler is not None:
|
|
139
|
+
for wire_a, wire_b in next(layout_gen):
|
|
140
|
+
self.entangler(wires=[wire_a, wire_b])
|
|
141
|
+
|
|
142
|
+
qml.layer(_layer, n_layers, params, wires=range(n_qubits))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class QAOAAnsatz(Ansatz):
|
|
146
|
+
"""
|
|
147
|
+
QAOA-style ansatz using PennyLane's QAOAEmbedding.
|
|
148
|
+
|
|
149
|
+
Implements a parameterized ansatz based on the Quantum Approximate Optimization
|
|
150
|
+
Algorithm structure, alternating between problem and mixer Hamiltonians.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def n_params_per_layer(n_qubits: int, **kwargs) -> int:
|
|
155
|
+
"""
|
|
156
|
+
Calculate the number of parameters per layer for QAOA ansatz.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
n_qubits (int): Number of qubits in the circuit.
|
|
160
|
+
**kwargs: Additional unused arguments.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
int: Number of parameters needed per layer.
|
|
164
|
+
"""
|
|
165
|
+
return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
|
|
166
|
+
|
|
167
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs):
|
|
168
|
+
"""
|
|
169
|
+
Build the QAOA ansatz circuit.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
params: Parameter array to use for the ansatz.
|
|
173
|
+
n_qubits (int): Number of qubits.
|
|
174
|
+
n_layers (int): Number of QAOA layers.
|
|
175
|
+
**kwargs: Additional unused arguments.
|
|
176
|
+
"""
|
|
177
|
+
qml.QAOAEmbedding(
|
|
178
|
+
features=[],
|
|
179
|
+
weights=params.reshape(n_layers, -1),
|
|
180
|
+
wires=range(n_qubits),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class HardwareEfficientAnsatz(Ansatz):
|
|
185
|
+
"""
|
|
186
|
+
Hardware-efficient ansatz (not yet implemented).
|
|
187
|
+
|
|
188
|
+
This ansatz is designed to be easily implementable on near-term quantum hardware,
|
|
189
|
+
typically using native gate sets and connectivity patterns.
|
|
190
|
+
|
|
191
|
+
Note:
|
|
192
|
+
This class is a placeholder for future implementation.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def n_params_per_layer(n_qubits: int, **kwargs) -> int:
|
|
197
|
+
"""Not yet implemented."""
|
|
198
|
+
raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
|
|
199
|
+
|
|
200
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
|
|
201
|
+
"""Not yet implemented."""
|
|
202
|
+
raise NotImplementedError("HardwareEfficientAnsatz is not yet implemented.")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# --- Chemistry Ansaetze ---
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class UCCSDAnsatz(Ansatz):
|
|
209
|
+
"""
|
|
210
|
+
Unitary Coupled Cluster Singles and Doubles (UCCSD) ansatz.
|
|
211
|
+
|
|
212
|
+
This ansatz is specifically designed for quantum chemistry calculations,
|
|
213
|
+
implementing the UCCSD approximation which includes all single and double
|
|
214
|
+
electron excitations from a reference state.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
|
|
219
|
+
"""
|
|
220
|
+
Calculate the number of parameters per layer for UCCSD ansatz.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
n_qubits (int): Number of qubits in the circuit.
|
|
224
|
+
n_electrons (int): Number of electrons in the system.
|
|
225
|
+
**kwargs: Additional unused arguments.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
int: Number of parameters (number of single + double excitations).
|
|
229
|
+
"""
|
|
230
|
+
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
231
|
+
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
232
|
+
return len(s_wires) + len(d_wires)
|
|
233
|
+
|
|
234
|
+
def build(self, params, n_qubits: int, n_layers: int, n_electrons: int, **kwargs):
|
|
235
|
+
"""
|
|
236
|
+
Build the UCCSD ansatz circuit.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
params: Parameter array for excitation amplitudes.
|
|
240
|
+
n_qubits (int): Number of qubits.
|
|
241
|
+
n_layers (int): Number of UCCSD layers (repeats).
|
|
242
|
+
n_electrons (int): Number of electrons in the system.
|
|
243
|
+
**kwargs: Additional unused arguments.
|
|
244
|
+
"""
|
|
245
|
+
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
246
|
+
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
247
|
+
hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
|
|
248
|
+
|
|
249
|
+
qml.UCCSD(
|
|
250
|
+
params.reshape(n_layers, -1),
|
|
251
|
+
wires=range(n_qubits),
|
|
252
|
+
s_wires=s_wires,
|
|
253
|
+
d_wires=d_wires,
|
|
254
|
+
init_state=hf_state,
|
|
255
|
+
n_repeats=n_layers,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class HartreeFockAnsatz(Ansatz):
|
|
260
|
+
"""
|
|
261
|
+
Hartree-Fock-based ansatz for quantum chemistry.
|
|
262
|
+
|
|
263
|
+
This ansatz prepares the Hartree-Fock reference state and applies
|
|
264
|
+
parameterized single and double excitation gates. It's a simplified
|
|
265
|
+
alternative to UCCSD, often used as a starting point for VQE calculations.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def n_params_per_layer(n_qubits: int, n_electrons: int, **kwargs) -> int:
|
|
270
|
+
"""
|
|
271
|
+
Calculate the number of parameters per layer for Hartree-Fock ansatz.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
n_qubits (int): Number of qubits in the circuit.
|
|
275
|
+
n_electrons (int): Number of electrons in the system.
|
|
276
|
+
**kwargs: Additional unused arguments.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
int: Number of parameters (number of single + double excitations).
|
|
280
|
+
"""
|
|
281
|
+
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
282
|
+
return len(singles) + len(doubles)
|
|
283
|
+
|
|
284
|
+
def build(self, params, n_qubits: int, n_layers: int, n_electrons: int, **kwargs):
|
|
285
|
+
"""
|
|
286
|
+
Build the Hartree-Fock ansatz circuit.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
params: Parameter array for excitation amplitudes.
|
|
290
|
+
n_qubits (int): Number of qubits.
|
|
291
|
+
n_layers (int): Number of ansatz layers.
|
|
292
|
+
n_electrons (int): Number of electrons in the system.
|
|
293
|
+
**kwargs: Additional unused arguments.
|
|
294
|
+
"""
|
|
295
|
+
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
296
|
+
hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
|
|
297
|
+
|
|
298
|
+
qml.layer(
|
|
299
|
+
qml.AllSinglesDoubles,
|
|
300
|
+
n_layers,
|
|
301
|
+
params.reshape(n_layers, -1),
|
|
302
|
+
wires=range(n_qubits),
|
|
303
|
+
hf_state=hf_state,
|
|
304
|
+
singles=singles,
|
|
305
|
+
doubles=doubles,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Reset the BasisState operations after the first layer
|
|
309
|
+
# for behaviour similar to UCCSD ansatz
|
|
310
|
+
for op in qml.QueuingManager.active_context().queue[1:]:
|
|
311
|
+
op._hyperparameters["hf_state"] = 0
|