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.
Files changed (92) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +10 -0
  3. divi/backends/_backend_properties_conversion.py +227 -0
  4. divi/backends/_circuit_runner.py +70 -0
  5. divi/backends/_execution_result.py +70 -0
  6. divi/backends/_parallel_simulator.py +486 -0
  7. divi/backends/_qoro_service.py +663 -0
  8. divi/backends/_qpu_system.py +101 -0
  9. divi/backends/_results_processing.py +133 -0
  10. divi/circuits/__init__.py +13 -0
  11. divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
  12. divi/circuits/_cirq/_parser.py +110 -0
  13. divi/circuits/_cirq/_qasm_export.py +78 -0
  14. divi/circuits/_core.py +391 -0
  15. divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
  16. divi/circuits/_qasm_validation.py +694 -0
  17. divi/qprog/__init__.py +27 -8
  18. divi/qprog/_expectation.py +181 -0
  19. divi/qprog/_hamiltonians.py +281 -0
  20. divi/qprog/algorithms/__init__.py +16 -0
  21. divi/qprog/algorithms/_ansatze.py +368 -0
  22. divi/qprog/algorithms/_custom_vqa.py +263 -0
  23. divi/qprog/algorithms/_pce.py +262 -0
  24. divi/qprog/algorithms/_qaoa.py +579 -0
  25. divi/qprog/algorithms/_vqe.py +262 -0
  26. divi/qprog/batch.py +387 -74
  27. divi/qprog/checkpointing.py +556 -0
  28. divi/qprog/exceptions.py +9 -0
  29. divi/qprog/optimizers.py +1014 -43
  30. divi/qprog/quantum_program.py +243 -412
  31. divi/qprog/typing.py +62 -0
  32. divi/qprog/variational_quantum_algorithm.py +1208 -0
  33. divi/qprog/workflows/__init__.py +10 -0
  34. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
  35. divi/qprog/workflows/_qubo_partitioning.py +221 -0
  36. divi/qprog/workflows/_vqe_sweep.py +560 -0
  37. divi/reporting/__init__.py +7 -0
  38. divi/reporting/_pbar.py +127 -0
  39. divi/reporting/_qlogger.py +68 -0
  40. divi/reporting/_reporter.py +155 -0
  41. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/METADATA +43 -15
  42. qoro_divi-0.6.0.dist-info/RECORD +47 -0
  43. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/WHEEL +1 -1
  44. qoro_divi-0.6.0.dist-info/licenses/LICENSES/.license-header +3 -0
  45. divi/_pbar.py +0 -73
  46. divi/circuits.py +0 -139
  47. divi/exp/cirq/_lexer.py +0 -126
  48. divi/exp/cirq/_parser.py +0 -889
  49. divi/exp/cirq/_qasm_export.py +0 -37
  50. divi/exp/cirq/_qasm_import.py +0 -35
  51. divi/exp/cirq/exception.py +0 -21
  52. divi/exp/scipy/_cobyla.py +0 -342
  53. divi/exp/scipy/pyprima/LICENCE.txt +0 -28
  54. divi/exp/scipy/pyprima/__init__.py +0 -263
  55. divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
  56. divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
  57. divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
  58. divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
  59. divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
  60. divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
  61. divi/exp/scipy/pyprima/cobyla/update.py +0 -331
  62. divi/exp/scipy/pyprima/common/__init__.py +0 -0
  63. divi/exp/scipy/pyprima/common/_bounds.py +0 -41
  64. divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
  65. divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
  66. divi/exp/scipy/pyprima/common/_project.py +0 -224
  67. divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
  68. divi/exp/scipy/pyprima/common/consts.py +0 -48
  69. divi/exp/scipy/pyprima/common/evaluate.py +0 -101
  70. divi/exp/scipy/pyprima/common/history.py +0 -39
  71. divi/exp/scipy/pyprima/common/infos.py +0 -30
  72. divi/exp/scipy/pyprima/common/linalg.py +0 -452
  73. divi/exp/scipy/pyprima/common/message.py +0 -336
  74. divi/exp/scipy/pyprima/common/powalg.py +0 -131
  75. divi/exp/scipy/pyprima/common/preproc.py +0 -393
  76. divi/exp/scipy/pyprima/common/present.py +0 -5
  77. divi/exp/scipy/pyprima/common/ratio.py +0 -56
  78. divi/exp/scipy/pyprima/common/redrho.py +0 -49
  79. divi/exp/scipy/pyprima/common/selectx.py +0 -346
  80. divi/interfaces.py +0 -25
  81. divi/parallel_simulator.py +0 -258
  82. divi/qlogger.py +0 -119
  83. divi/qoro_service.py +0 -343
  84. divi/qprog/_mlae.py +0 -182
  85. divi/qprog/_qaoa.py +0 -440
  86. divi/qprog/_vqe.py +0 -275
  87. divi/qprog/_vqe_sweep.py +0 -144
  88. divi/utils.py +0 -116
  89. qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
  90. /divi/{qem.py → circuits/qem.py} +0 -0
  91. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSE +0 -0
  92. {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.exp.cirq import cirq_circuit_from_qasm
19
- from divi.qem import QEMProtocol
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
- params = (
69
- "(" + ",".join([f"{p:.{precision}}" for p in op.parameters]) + ")"
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: Optional[int] = None,
124
+ precision: int | None = None,
85
125
  return_measurements_separately: bool = False,
86
126
  symbols: list[Symbol] = None,
87
- qem_protocol: Optional[QEMProtocol] = None,
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(cirq_circuit_from_qasm(main_qasm_str)):
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 (diag_ops := QuantumScript(measurements=meas_group).diagonalizing_gates)
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)