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
divi/circuits/_core.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from itertools import product
|
|
9
|
+
from typing import Literal, NamedTuple
|
|
10
|
+
|
|
11
|
+
import dill
|
|
12
|
+
import numpy as np
|
|
13
|
+
import numpy.typing as npt
|
|
14
|
+
import pennylane as qml
|
|
15
|
+
from pennylane.transforms.core.transform_program import TransformProgram
|
|
16
|
+
|
|
17
|
+
from divi.circuits import to_openqasm
|
|
18
|
+
from divi.circuits.qem import QEMProtocol
|
|
19
|
+
|
|
20
|
+
TRANSFORM_PROGRAM = TransformProgram()
|
|
21
|
+
TRANSFORM_PROGRAM.add_transform(qml.transforms.split_to_single_terms)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _wire_grouping(measurements: list[qml.measurements.MeasurementProcess]):
|
|
25
|
+
"""
|
|
26
|
+
Groups a list of PennyLane MeasurementProcess objects by mutually non-overlapping wires.
|
|
27
|
+
|
|
28
|
+
Each group contains measurements whose wires do not overlap with those of any other
|
|
29
|
+
measurement in the same group. This enables parallel measurement of compatible observables,
|
|
30
|
+
e.g., for grouped execution or more efficient sampling.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
partition_indices (list[list[int]]): Indices of the original measurements in each group.
|
|
34
|
+
mp_groups (list[list[MeasurementProcess]]): Grouped MeasurementProcess objects.
|
|
35
|
+
"""
|
|
36
|
+
mp_groups = []
|
|
37
|
+
wires_for_each_group = []
|
|
38
|
+
group_mapping = {} # original_index -> (group_idx, pos_in_group)
|
|
39
|
+
|
|
40
|
+
for i, mp in enumerate(measurements):
|
|
41
|
+
added = False
|
|
42
|
+
for group_idx, wires in enumerate(wires_for_each_group):
|
|
43
|
+
if not qml.wires.Wires.shared_wires([wires, mp.wires]):
|
|
44
|
+
mp_groups[group_idx].append(mp)
|
|
45
|
+
wires_for_each_group[group_idx] += mp.wires
|
|
46
|
+
group_mapping[i] = (group_idx, len(mp_groups[group_idx]) - 1)
|
|
47
|
+
added = True
|
|
48
|
+
break
|
|
49
|
+
if not added:
|
|
50
|
+
mp_groups.append([mp])
|
|
51
|
+
wires_for_each_group.append(mp.wires)
|
|
52
|
+
group_mapping[i] = (len(mp_groups) - 1, 0)
|
|
53
|
+
|
|
54
|
+
partition_indices = [[] for _ in range(len(mp_groups))]
|
|
55
|
+
for original_idx, (group_idx, _) in group_mapping.items():
|
|
56
|
+
partition_indices[group_idx].append(original_idx)
|
|
57
|
+
|
|
58
|
+
return partition_indices, mp_groups
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _create_final_postprocessing_fn(coefficients, partition_indices, num_total_obs):
|
|
62
|
+
"""Create a wrapper fn that reconstructs the flat results list and computes the final energy."""
|
|
63
|
+
reverse_map = [None] * num_total_obs
|
|
64
|
+
for group_idx, indices_in_group in enumerate(partition_indices):
|
|
65
|
+
for idx_within_group, original_flat_idx in enumerate(indices_in_group):
|
|
66
|
+
reverse_map[original_flat_idx] = (group_idx, idx_within_group)
|
|
67
|
+
|
|
68
|
+
missing_indices = [i for i, v in enumerate(reverse_map) if v is None]
|
|
69
|
+
if missing_indices:
|
|
70
|
+
raise RuntimeError(
|
|
71
|
+
f"partition_indices does not cover all observable indices. Missing indices: {missing_indices}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def final_postprocessing_fn(grouped_results):
|
|
75
|
+
"""
|
|
76
|
+
Takes grouped results, flattens them to the original order,
|
|
77
|
+
multiplies by coefficients, and sums to get the final energy.
|
|
78
|
+
"""
|
|
79
|
+
if len(grouped_results) != len(partition_indices):
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
f"Expected {len(partition_indices)} grouped results, but got {len(grouped_results)}."
|
|
82
|
+
)
|
|
83
|
+
flat_results = np.zeros(num_total_obs, dtype=np.float64)
|
|
84
|
+
for original_flat_idx in range(num_total_obs):
|
|
85
|
+
group_idx, idx_within_group = reverse_map[original_flat_idx]
|
|
86
|
+
|
|
87
|
+
group_result = grouped_results[group_idx]
|
|
88
|
+
# When a group has one measurement, the result is a scalar.
|
|
89
|
+
if len(partition_indices[group_idx]) == 1:
|
|
90
|
+
flat_results[original_flat_idx] = group_result
|
|
91
|
+
else:
|
|
92
|
+
flat_results[original_flat_idx] = group_result[idx_within_group]
|
|
93
|
+
|
|
94
|
+
# Perform the final summation using the efficient dot product method.
|
|
95
|
+
return np.dot(coefficients, flat_results)
|
|
96
|
+
|
|
97
|
+
return final_postprocessing_fn
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CircuitTag(NamedTuple):
|
|
101
|
+
"""Structured tag for identifying circuit executions."""
|
|
102
|
+
|
|
103
|
+
param_id: int
|
|
104
|
+
qem_name: str
|
|
105
|
+
qem_id: int
|
|
106
|
+
meas_id: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def format_circuit_tag(tag: CircuitTag) -> str:
|
|
110
|
+
"""Format a CircuitTag into its wire-safe string representation."""
|
|
111
|
+
return f"{tag.param_id}_{tag.qem_name}:{tag.qem_id}_{tag.meas_id}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class ExecutableQASMCircuit:
|
|
116
|
+
"""Represents a single, executable QASM circuit with its associated tag."""
|
|
117
|
+
|
|
118
|
+
tag: CircuitTag
|
|
119
|
+
qasm: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class CircuitBundle:
|
|
124
|
+
"""
|
|
125
|
+
Represents a bundle of logically related quantum circuits.
|
|
126
|
+
|
|
127
|
+
A CircuitBundle is typically generated from a single `MetaCircuit` by
|
|
128
|
+
instantiating it with concrete parameters. It may contain multiple
|
|
129
|
+
executable circuits due to measurement grouping or error mitigation
|
|
130
|
+
protocols. Each executable circuit has a QASM representation and a
|
|
131
|
+
unique tag for identification.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
executables: tuple[ExecutableQASMCircuit, ...]
|
|
135
|
+
"""Tuple of executable circuits."""
|
|
136
|
+
|
|
137
|
+
def __str__(self):
|
|
138
|
+
"""
|
|
139
|
+
Return a string representation of the circuit bundle.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
str: String in format "CircuitBundle ({num_executables} executables)".
|
|
143
|
+
"""
|
|
144
|
+
return f"CircuitBundle ({len(self.executables)} executables)"
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def tags(self) -> list[CircuitTag]:
|
|
148
|
+
"""A list of tags for all executables in the bundle."""
|
|
149
|
+
return [e.tag for e in self.executables]
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def qasm_circuits(self) -> list[str]:
|
|
153
|
+
"""A list of QASM strings for all executables in the bundle."""
|
|
154
|
+
return [e.qasm for e in self.executables]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class MetaCircuit:
|
|
159
|
+
"""
|
|
160
|
+
A parameterized quantum circuit template for batch circuit generation.
|
|
161
|
+
|
|
162
|
+
MetaCircuit represents a symbolic quantum circuit that can be instantiated
|
|
163
|
+
multiple times with different parameter values. It handles circuit compilation,
|
|
164
|
+
observable grouping, and measurement decomposition for efficient execution.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
source_circuit: qml.tape.QuantumScript
|
|
168
|
+
"""The PennyLane quantum circuit with symbolic parameters."""
|
|
169
|
+
symbols: npt.NDArray[np.object_]
|
|
170
|
+
"""Array of sympy symbols used as circuit parameters."""
|
|
171
|
+
grouping_strategy: Literal["wires", "default", "qwc", "_backend_expval"] | None = (
|
|
172
|
+
None
|
|
173
|
+
)
|
|
174
|
+
"""Strategy for grouping commuting observables."""
|
|
175
|
+
qem_protocol: QEMProtocol | None = None
|
|
176
|
+
"""Quantum error mitigation protocol to apply."""
|
|
177
|
+
precision: int = 8
|
|
178
|
+
"""Number of decimal places for parameter values in QASM conversion."""
|
|
179
|
+
|
|
180
|
+
# --- Compiled artifacts ---
|
|
181
|
+
_compiled_circuit_bodies: tuple[str, ...] = field(init=False)
|
|
182
|
+
_measurements: tuple[str, ...] = field(init=False)
|
|
183
|
+
measurement_groups: tuple[tuple[qml.operation.Operator, ...], ...] = field(
|
|
184
|
+
init=False
|
|
185
|
+
)
|
|
186
|
+
postprocessing_fn: Callable = field(init=False)
|
|
187
|
+
|
|
188
|
+
def __post_init__(self):
|
|
189
|
+
"""
|
|
190
|
+
Compiles the circuit template after initialization.
|
|
191
|
+
|
|
192
|
+
This method performs several steps:
|
|
193
|
+
1. Decomposes the source circuit's measurement into single-term observables.
|
|
194
|
+
2. Groups commuting observables according to the specified strategy.
|
|
195
|
+
3. Generates a post-processing function to correctly combine measurement results.
|
|
196
|
+
4. Compiles the circuit body and measurement instructions into QASM strings.
|
|
197
|
+
"""
|
|
198
|
+
# Validate that the circuit has exactly one valid observable measurement.
|
|
199
|
+
if len(self.source_circuit.measurements) != 1:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"MetaCircuit requires a circuit with exactly one measurement, "
|
|
202
|
+
f"but {len(self.source_circuit.measurements)} were found."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
measurement = self.source_circuit.measurements[0]
|
|
206
|
+
# If the measurement is not an expectation value, we assume it is for sampling
|
|
207
|
+
# and does not require special post-processing.
|
|
208
|
+
if not hasattr(measurement, "obs") or measurement.obs is None:
|
|
209
|
+
postprocessing_fn = lambda x: x
|
|
210
|
+
measurement_groups = ((),)
|
|
211
|
+
(
|
|
212
|
+
compiled_circuit_bodies,
|
|
213
|
+
measurements,
|
|
214
|
+
) = to_openqasm(
|
|
215
|
+
self.source_circuit,
|
|
216
|
+
measurement_groups=measurement_groups,
|
|
217
|
+
return_measurements_separately=True,
|
|
218
|
+
symbols=self.symbols,
|
|
219
|
+
qem_protocol=self.qem_protocol,
|
|
220
|
+
precision=self.precision,
|
|
221
|
+
)
|
|
222
|
+
# Use object.__setattr__ because the class is frozen
|
|
223
|
+
object.__setattr__(self, "postprocessing_fn", postprocessing_fn)
|
|
224
|
+
object.__setattr__(self, "measurement_groups", measurement_groups)
|
|
225
|
+
object.__setattr__(
|
|
226
|
+
self, "_compiled_circuit_bodies", tuple(compiled_circuit_bodies)
|
|
227
|
+
)
|
|
228
|
+
object.__setattr__(self, "_measurements", tuple(measurements))
|
|
229
|
+
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
# Step 1: Use split_to_single_terms to get a flat list of measurement
|
|
233
|
+
# processes. We no longer need its post-processing function.
|
|
234
|
+
measurements_only_tape = qml.tape.QuantumScript(
|
|
235
|
+
measurements=self.source_circuit.measurements
|
|
236
|
+
)
|
|
237
|
+
s_tapes, _ = TRANSFORM_PROGRAM((measurements_only_tape,))
|
|
238
|
+
single_term_mps = s_tapes[0].measurements
|
|
239
|
+
|
|
240
|
+
# Extract the coefficients, which we will now use in our own post-processing.
|
|
241
|
+
obs = self.source_circuit.measurements[0].obs
|
|
242
|
+
if isinstance(obs, (qml.Hamiltonian, qml.ops.Sum)):
|
|
243
|
+
coeffs, _ = obs.terms()
|
|
244
|
+
else:
|
|
245
|
+
# For single observables, the coefficient is implicitly 1.0
|
|
246
|
+
coeffs = [1.0]
|
|
247
|
+
|
|
248
|
+
# Step 2: Manually group the flat list of measurements based on the strategy.
|
|
249
|
+
if self.grouping_strategy in ("qwc", "default"):
|
|
250
|
+
obs_list = [m.obs for m in single_term_mps]
|
|
251
|
+
# This computes the grouping indices for the flat list of observables
|
|
252
|
+
partition_indices = qml.pauli.compute_partition_indices(obs_list)
|
|
253
|
+
measurement_groups = tuple(
|
|
254
|
+
tuple(single_term_mps[i].obs for i in group)
|
|
255
|
+
for group in partition_indices
|
|
256
|
+
)
|
|
257
|
+
elif self.grouping_strategy == "wires":
|
|
258
|
+
partition_indices, grouped_mps = _wire_grouping(single_term_mps)
|
|
259
|
+
measurement_groups = tuple(
|
|
260
|
+
tuple(m.obs for m in group) for group in grouped_mps
|
|
261
|
+
)
|
|
262
|
+
elif self.grouping_strategy is None:
|
|
263
|
+
# Each measurement is its own group
|
|
264
|
+
measurement_groups = tuple(tuple([m.obs]) for m in single_term_mps)
|
|
265
|
+
partition_indices = [[i] for i in range(len(single_term_mps))]
|
|
266
|
+
elif self.grouping_strategy == "_backend_expval":
|
|
267
|
+
measurement_groups = ((),)
|
|
268
|
+
# For backends that compute expectation values directly, no explicit
|
|
269
|
+
# measurement basis rotations (diagonalizing gates) are needed in the QASM.
|
|
270
|
+
# The `to_openqasm` function interprets an empty measurement group `()`
|
|
271
|
+
# as a signal to skip adding these gates.
|
|
272
|
+
# All observables are still tracked in a single group for post-processing.
|
|
273
|
+
partition_indices = [list(range(len(single_term_mps)))]
|
|
274
|
+
else:
|
|
275
|
+
raise ValueError(f"Unknown grouping strategy: {self.grouping_strategy}")
|
|
276
|
+
|
|
277
|
+
# Step 3: Create our own post-processing function that handles the final summation.
|
|
278
|
+
postprocessing_fn = _create_final_postprocessing_fn(
|
|
279
|
+
coeffs, partition_indices, len(single_term_mps)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
compiled_circuit_bodies, measurements = to_openqasm(
|
|
283
|
+
self.source_circuit,
|
|
284
|
+
measurement_groups=measurement_groups,
|
|
285
|
+
return_measurements_separately=True,
|
|
286
|
+
# TODO: optimize later
|
|
287
|
+
measure_all=True,
|
|
288
|
+
symbols=self.symbols,
|
|
289
|
+
qem_protocol=self.qem_protocol,
|
|
290
|
+
precision=self.precision,
|
|
291
|
+
)
|
|
292
|
+
# Use object.__setattr__ because the class is frozen
|
|
293
|
+
object.__setattr__(self, "postprocessing_fn", postprocessing_fn)
|
|
294
|
+
object.__setattr__(self, "measurement_groups", measurement_groups)
|
|
295
|
+
object.__setattr__(
|
|
296
|
+
self, "_compiled_circuit_bodies", tuple(compiled_circuit_bodies)
|
|
297
|
+
)
|
|
298
|
+
object.__setattr__(self, "_measurements", tuple(measurements))
|
|
299
|
+
|
|
300
|
+
def __getstate__(self):
|
|
301
|
+
"""
|
|
302
|
+
Prepare the MetaCircuit for pickling.
|
|
303
|
+
|
|
304
|
+
Serializes the postprocessing function using dill since regular pickle
|
|
305
|
+
cannot handle certain PennyLane function objects.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
dict: State dictionary with serialized postprocessing function.
|
|
309
|
+
"""
|
|
310
|
+
state = self.__dict__.copy()
|
|
311
|
+
state["postprocessing_fn"] = dill.dumps(self.postprocessing_fn)
|
|
312
|
+
return state
|
|
313
|
+
|
|
314
|
+
def __setstate__(self, state):
|
|
315
|
+
"""
|
|
316
|
+
Restore the MetaCircuit from a pickled state.
|
|
317
|
+
|
|
318
|
+
Deserializes the postprocessing function that was serialized with dill
|
|
319
|
+
during pickling.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
state (dict): State dictionary from pickling with serialized
|
|
323
|
+
postprocessing function.
|
|
324
|
+
"""
|
|
325
|
+
state["postprocessing_fn"] = dill.loads(state["postprocessing_fn"])
|
|
326
|
+
|
|
327
|
+
self.__dict__.update(state)
|
|
328
|
+
|
|
329
|
+
def initialize_circuit_from_params(
|
|
330
|
+
self,
|
|
331
|
+
param_list: npt.NDArray[np.floating] | list[float],
|
|
332
|
+
param_idx: int = 0,
|
|
333
|
+
precision: int | None = None,
|
|
334
|
+
) -> CircuitBundle:
|
|
335
|
+
"""
|
|
336
|
+
Instantiate a concrete CircuitBundle by substituting symbolic parameters with values.
|
|
337
|
+
|
|
338
|
+
Takes a list of parameter values and creates a fully instantiated CircuitBundle
|
|
339
|
+
by replacing all symbolic parameters in the QASM representations with their
|
|
340
|
+
concrete numerical values.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
param_list (npt.NDArray[np.floating] | list[float]): Array of numerical
|
|
344
|
+
parameter values to substitute for symbols.
|
|
345
|
+
Must match the length and order of self.symbols.
|
|
346
|
+
param_idx (int, optional): Parameter set index used for structured tags.
|
|
347
|
+
Defaults to 0.
|
|
348
|
+
precision (int | None, optional): Number of decimal places for parameter values
|
|
349
|
+
in the QASM output. If None, uses the precision set on this MetaCircuit instance.
|
|
350
|
+
Defaults to None.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
CircuitBundle: A new CircuitBundle instance with parameters substituted and proper
|
|
354
|
+
tags for identification.
|
|
355
|
+
|
|
356
|
+
Note:
|
|
357
|
+
The main circuit's parameters are still in symbol form.
|
|
358
|
+
Not sure if it is necessary for any useful application to parameterize them.
|
|
359
|
+
"""
|
|
360
|
+
if precision is None:
|
|
361
|
+
precision = self.precision
|
|
362
|
+
mapping = dict(
|
|
363
|
+
zip(
|
|
364
|
+
map(lambda x: re.escape(str(x)), self.symbols),
|
|
365
|
+
map(lambda x: f"{x:.{precision}f}", param_list),
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
pattern = re.compile("|".join(k for k in mapping.keys()))
|
|
369
|
+
|
|
370
|
+
final_qasm_bodies = [
|
|
371
|
+
pattern.sub(lambda match: mapping[match.group(0)], body)
|
|
372
|
+
for body in self._compiled_circuit_bodies
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
executables = []
|
|
376
|
+
param_id = param_idx
|
|
377
|
+
for (i, body_str), (j, meas_str) in product(
|
|
378
|
+
enumerate(final_qasm_bodies), enumerate(self._measurements)
|
|
379
|
+
):
|
|
380
|
+
qasm_circuit = body_str + meas_str
|
|
381
|
+
tag = CircuitTag(
|
|
382
|
+
param_id=param_id,
|
|
383
|
+
qem_name=(
|
|
384
|
+
self.qem_protocol.name if self.qem_protocol else "NoMitigation"
|
|
385
|
+
),
|
|
386
|
+
qem_id=i,
|
|
387
|
+
meas_id=j,
|
|
388
|
+
)
|
|
389
|
+
executables.append(ExecutableQASMCircuit(tag=tag, qasm=qasm_circuit))
|
|
390
|
+
|
|
391
|
+
return CircuitBundle(executables=tuple(executables))
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import re
|
|
6
6
|
from functools import partial
|
|
7
7
|
from itertools import product
|
|
8
|
-
from typing import Optional
|
|
9
8
|
from warnings import warn
|
|
10
9
|
|
|
11
10
|
import cirq
|
|
@@ -13,10 +12,11 @@ import numpy as np
|
|
|
13
12
|
import pennylane as qml
|
|
14
13
|
from pennylane.tape import QuantumScript
|
|
15
14
|
from pennylane.wires import Wires
|
|
16
|
-
from sympy import Symbol
|
|
15
|
+
from sympy import Expr, Symbol
|
|
17
16
|
|
|
18
|
-
from divi.
|
|
19
|
-
|
|
17
|
+
from divi.circuits.qem import QEMProtocol
|
|
18
|
+
|
|
19
|
+
from ._cirq import ExtendedQasmParser as QasmParser
|
|
20
20
|
|
|
21
21
|
OPENQASM_GATES = {
|
|
22
22
|
"CNOT": "cx",
|
|
@@ -46,7 +46,39 @@ OPENQASM_GATES = {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def _cirq_circuit_from_qasm(qasm: str) -> cirq.Circuit:
|
|
50
|
+
"""Parses an OpenQASM string to `cirq.Circuit`.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
qasm: The OpenQASM string
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The parsed circuit
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
return QasmParser().parse(qasm).circuit
|
|
60
|
+
|
|
61
|
+
|
|
49
62
|
def _ops_to_qasm(operations, precision, wires):
|
|
63
|
+
"""
|
|
64
|
+
Convert PennyLane operations to OpenQASM instruction strings.
|
|
65
|
+
|
|
66
|
+
Translates a sequence of PennyLane quantum operations into their OpenQASM
|
|
67
|
+
2.0 equivalent representations. Each operation is mapped to its corresponding
|
|
68
|
+
QASM gate with appropriate parameters and wire labels.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
operations: Sequence of PennyLane operation objects to convert.
|
|
72
|
+
precision (int | None): Number of decimal places for parameter values.
|
|
73
|
+
If None, uses default Python string formatting.
|
|
74
|
+
wires: Wire labels used in the circuit for indexing.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
str: OpenQASM instruction string with each operation on a new line.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: If an operation is not supported by the QASM serializer.
|
|
81
|
+
"""
|
|
50
82
|
# create the QASM code representing the operations
|
|
51
83
|
qasm_str = ""
|
|
52
84
|
|
|
@@ -65,9 +97,17 @@ def _ops_to_qasm(operations, precision, wires):
|
|
|
65
97
|
# If the operation takes parameters, construct a string
|
|
66
98
|
# with parameter values.
|
|
67
99
|
if precision is not None:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
100
|
+
# Format parameters with precision, but use str() for sympy expressions
|
|
101
|
+
param_strs = []
|
|
102
|
+
for p in op.parameters:
|
|
103
|
+
if isinstance(p, Expr):
|
|
104
|
+
# Sympy expressions (Symbol, Mul, Add, etc.) should be kept as-is
|
|
105
|
+
# (will be replaced later during parameter substitution)
|
|
106
|
+
param_strs.append(str(p))
|
|
107
|
+
else:
|
|
108
|
+
# Numeric parameters can be formatted with precision
|
|
109
|
+
param_strs.append(f"{p:.{precision}}")
|
|
110
|
+
params = "(" + ",".join(param_strs) + ")"
|
|
71
111
|
else:
|
|
72
112
|
# use default precision
|
|
73
113
|
params = "(" + ",".join([str(p) for p in op.parameters]) + ")"
|
|
@@ -81,10 +121,10 @@ def to_openqasm(
|
|
|
81
121
|
main_qscript,
|
|
82
122
|
measurement_groups: list[list[qml.measurements.ExpectationMP]],
|
|
83
123
|
measure_all: bool = True,
|
|
84
|
-
precision:
|
|
124
|
+
precision: int | None = None,
|
|
85
125
|
return_measurements_separately: bool = False,
|
|
86
126
|
symbols: list[Symbol] = None,
|
|
87
|
-
qem_protocol:
|
|
127
|
+
qem_protocol: QEMProtocol | None = None,
|
|
88
128
|
) -> list[str] | tuple[str, list[str]]:
|
|
89
129
|
"""
|
|
90
130
|
Serialize the circuit as an OpenQASM 2.0 program.
|
|
@@ -137,7 +177,18 @@ def to_openqasm(
|
|
|
137
177
|
return main_qasm_str
|
|
138
178
|
|
|
139
179
|
if qem_protocol:
|
|
180
|
+
# Flatten symbols list to handle both individual symbols and arrays of symbols
|
|
181
|
+
flat_symbols = []
|
|
140
182
|
for symbol in symbols:
|
|
183
|
+
if isinstance(symbol, np.ndarray):
|
|
184
|
+
# If it's a numpy array of symbols, flatten it
|
|
185
|
+
flat_symbols.extend(symbol.flatten())
|
|
186
|
+
else:
|
|
187
|
+
# Individual symbol
|
|
188
|
+
flat_symbols.append(symbol)
|
|
189
|
+
|
|
190
|
+
# Declare each symbol individually in QASM 3.0
|
|
191
|
+
for symbol in flat_symbols:
|
|
141
192
|
main_qasm_str += f"input angle[32] {str(symbol)};\n"
|
|
142
193
|
|
|
143
194
|
# create the quantum and classical registers
|
|
@@ -167,7 +218,7 @@ def to_openqasm(
|
|
|
167
218
|
|
|
168
219
|
main_qasm_strs = []
|
|
169
220
|
if qem_protocol:
|
|
170
|
-
for circ in qem_protocol.modify_circuit(
|
|
221
|
+
for circ in qem_protocol.modify_circuit(_cirq_circuit_from_qasm(main_qasm_str)):
|
|
171
222
|
# Convert back to QASM2.0 code, with the symbolic parameters
|
|
172
223
|
qasm_str = cirq.qasm(circ)
|
|
173
224
|
# Remove redundant newlines
|
|
@@ -186,9 +237,19 @@ def to_openqasm(
|
|
|
186
237
|
|
|
187
238
|
# Create a copy of the program for every measurement that we have
|
|
188
239
|
for meas_group in measurement_groups:
|
|
240
|
+
# Ensure all items in measurement group are MeasurementProcess instances
|
|
241
|
+
wrapped_group = [
|
|
242
|
+
m if isinstance(m, qml.measurements.MeasurementProcess) else qml.expval(m)
|
|
243
|
+
for m in meas_group
|
|
244
|
+
]
|
|
245
|
+
|
|
189
246
|
curr_diag_qasm_str = (
|
|
190
247
|
_to_qasm(diag_ops)
|
|
191
|
-
if (
|
|
248
|
+
if (
|
|
249
|
+
diag_ops := QuantumScript(
|
|
250
|
+
measurements=wrapped_group
|
|
251
|
+
).diagonalizing_gates
|
|
252
|
+
)
|
|
192
253
|
else ""
|
|
193
254
|
)
|
|
194
255
|
|
|
@@ -197,9 +258,7 @@ def to_openqasm(
|
|
|
197
258
|
for wire in range(len(wires)):
|
|
198
259
|
measure_qasm_str += f"measure q[{wire}] -> c[{wire}];\n"
|
|
199
260
|
else:
|
|
200
|
-
measured_wires = Wires.all_wires(
|
|
201
|
-
[m.wires for m in main_qscript.measurements]
|
|
202
|
-
)
|
|
261
|
+
measured_wires = Wires.all_wires([m.wires for m in meas_group])
|
|
203
262
|
|
|
204
263
|
for w in measured_wires:
|
|
205
264
|
wire_indx = main_qscript.wires.index(w)
|