iqm-benchmarks 1.3__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 iqm-benchmarks might be problematic. Click here for more details.
- iqm/benchmarks/__init__.py +31 -0
- iqm/benchmarks/benchmark.py +109 -0
- iqm/benchmarks/benchmark_definition.py +264 -0
- iqm/benchmarks/benchmark_experiment.py +163 -0
- iqm/benchmarks/compressive_gst/__init__.py +20 -0
- iqm/benchmarks/compressive_gst/compressive_gst.py +1029 -0
- iqm/benchmarks/entanglement/__init__.py +18 -0
- iqm/benchmarks/entanglement/ghz.py +802 -0
- iqm/benchmarks/logging_config.py +29 -0
- iqm/benchmarks/optimization/__init__.py +18 -0
- iqm/benchmarks/optimization/qscore.py +719 -0
- iqm/benchmarks/quantum_volume/__init__.py +21 -0
- iqm/benchmarks/quantum_volume/clops.py +726 -0
- iqm/benchmarks/quantum_volume/quantum_volume.py +854 -0
- iqm/benchmarks/randomized_benchmarking/__init__.py +18 -0
- iqm/benchmarks/randomized_benchmarking/clifford_1q.pkl +0 -0
- iqm/benchmarks/randomized_benchmarking/clifford_2q.pkl +0 -0
- iqm/benchmarks/randomized_benchmarking/clifford_rb/__init__.py +19 -0
- iqm/benchmarks/randomized_benchmarking/clifford_rb/clifford_rb.py +386 -0
- iqm/benchmarks/randomized_benchmarking/interleaved_rb/__init__.py +19 -0
- iqm/benchmarks/randomized_benchmarking/interleaved_rb/interleaved_rb.py +555 -0
- iqm/benchmarks/randomized_benchmarking/mirror_rb/__init__.py +19 -0
- iqm/benchmarks/randomized_benchmarking/mirror_rb/mirror_rb.py +810 -0
- iqm/benchmarks/randomized_benchmarking/multi_lmfit.py +86 -0
- iqm/benchmarks/randomized_benchmarking/randomized_benchmarking_common.py +892 -0
- iqm/benchmarks/readout_mitigation.py +290 -0
- iqm/benchmarks/utils.py +521 -0
- iqm_benchmarks-1.3.dist-info/LICENSE +205 -0
- iqm_benchmarks-1.3.dist-info/METADATA +190 -0
- iqm_benchmarks-1.3.dist-info/RECORD +42 -0
- iqm_benchmarks-1.3.dist-info/WHEEL +5 -0
- iqm_benchmarks-1.3.dist-info/top_level.txt +2 -0
- mGST/LICENSE +21 -0
- mGST/README.md +54 -0
- mGST/additional_fns.py +962 -0
- mGST/algorithm.py +733 -0
- mGST/compatibility.py +238 -0
- mGST/low_level_jit.py +694 -0
- mGST/optimization.py +349 -0
- mGST/qiskit_interface.py +282 -0
- mGST/reporting/figure_gen.py +334 -0
- mGST/reporting/reporting.py +710 -0
iqm/benchmarks/utils.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# Copyright 2024 IQM Benchmarks developers
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
General utility functions
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
from functools import wraps
|
|
21
|
+
from math import floor
|
|
22
|
+
from time import time
|
|
23
|
+
from typing import Any, Dict, Iterable, List, Literal, Optional, Sequence, Tuple, Union, cast
|
|
24
|
+
|
|
25
|
+
from more_itertools import chunked
|
|
26
|
+
from mthree.utils import final_measurement_mapping
|
|
27
|
+
import numpy as np
|
|
28
|
+
from qiskit import ClassicalRegister, QuantumCircuit, transpile
|
|
29
|
+
from qiskit.converters import circuit_to_dag
|
|
30
|
+
from qiskit.transpiler import CouplingMap
|
|
31
|
+
import xarray as xr
|
|
32
|
+
|
|
33
|
+
from iqm.benchmarks.logging_config import qcvv_logger
|
|
34
|
+
from iqm.qiskit_iqm import transpile_to_IQM
|
|
35
|
+
from iqm.qiskit_iqm.fake_backends.fake_adonis import IQMFakeAdonis
|
|
36
|
+
from iqm.qiskit_iqm.fake_backends.fake_apollo import IQMFakeApollo
|
|
37
|
+
from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
|
|
38
|
+
from iqm.qiskit_iqm.iqm_job import IQMJob
|
|
39
|
+
from iqm.qiskit_iqm.iqm_provider import IQMProvider
|
|
40
|
+
from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def timeit(f):
|
|
44
|
+
"""Calculates the amount of time a function takes to execute
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
f: The function to add the timing attribute to
|
|
48
|
+
Returns:
|
|
49
|
+
The decorated function execution with logger statement of elapsed time in execution
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@wraps(f)
|
|
53
|
+
def wrap(*args, **kw):
|
|
54
|
+
ts = time()
|
|
55
|
+
result = f(*args, **kw)
|
|
56
|
+
te = time()
|
|
57
|
+
elapsed = te - ts
|
|
58
|
+
if 1.0 <= elapsed <= 60.0:
|
|
59
|
+
qcvv_logger.debug(f'\t"{f.__name__}" took {elapsed:.2f} sec')
|
|
60
|
+
else:
|
|
61
|
+
qcvv_logger.debug(f'\t"{f.__name__}" took {elapsed/60.0:.2f} min')
|
|
62
|
+
return result, elapsed
|
|
63
|
+
|
|
64
|
+
return wrap
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@timeit
|
|
68
|
+
def count_2q_layers(circuit_list: List[QuantumCircuit]) -> List[int]:
|
|
69
|
+
"""Calculate the number of layers of parallel 2-qubit gates in a list of circuits.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
circuit_list (List[QuantumCircuit]): the list of quantum circuits to analyze.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List[int]: the number of layers of parallel 2-qubit gates in the list of circuits.
|
|
76
|
+
"""
|
|
77
|
+
all_number_2q_layers = []
|
|
78
|
+
for circuit in circuit_list:
|
|
79
|
+
dag = circuit_to_dag(circuit)
|
|
80
|
+
layers = list(dag.layers()) # Call the method and convert the result to a list
|
|
81
|
+
parallel_2q_layers = 0
|
|
82
|
+
|
|
83
|
+
for layer in layers:
|
|
84
|
+
two_qubit_gates_in_layer = [
|
|
85
|
+
node
|
|
86
|
+
for node in layer["graph"].op_nodes() # Use op_nodes to get only operation nodes
|
|
87
|
+
if node.op.num_qubits == 2
|
|
88
|
+
]
|
|
89
|
+
if two_qubit_gates_in_layer:
|
|
90
|
+
parallel_2q_layers += 1
|
|
91
|
+
all_number_2q_layers.append(parallel_2q_layers)
|
|
92
|
+
|
|
93
|
+
return all_number_2q_layers
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def count_native_gates(
|
|
97
|
+
backend_arg: Union[str, IQMBackendBase], transpiled_qc_list: List[QuantumCircuit]
|
|
98
|
+
) -> Dict[str, Dict[str, float]]:
|
|
99
|
+
"""Count the number of IQM native gates of each quantum circuit in a list.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
backend_arg (str | IQMBackendBase): The backend, either specified as str or as IQMBackendBase.
|
|
103
|
+
transpiled_qc_list: a list of quantum circuits transpiled to ['r','cz','barrier','measure'] gate set.
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary with
|
|
106
|
+
- outermost keys being native operations.
|
|
107
|
+
- values being Dict[str, float] with mean and standard deviation values of native operation counts.
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(backend_arg, str):
|
|
111
|
+
backend = get_iqm_backend(backend_arg)
|
|
112
|
+
else:
|
|
113
|
+
backend = backend_arg
|
|
114
|
+
|
|
115
|
+
native_operations = backend.operation_names
|
|
116
|
+
# Some backends may not include "barrier" in the operation_names attribute
|
|
117
|
+
if "barrier" not in native_operations:
|
|
118
|
+
native_operations.append("barrier")
|
|
119
|
+
|
|
120
|
+
num_native_operations: Dict[str, List[int]] = {x: [0] for x in native_operations}
|
|
121
|
+
avg_native_operations: Dict[str, Dict[str, float]] = {x: {} for x in native_operations}
|
|
122
|
+
|
|
123
|
+
for q in transpiled_qc_list:
|
|
124
|
+
for k in q.count_ops().keys():
|
|
125
|
+
if k not in native_operations:
|
|
126
|
+
raise ValueError(f"Count # of gates: '{k}' is not in the backend's native gate set")
|
|
127
|
+
for op in native_operations:
|
|
128
|
+
if op in q.count_ops().keys():
|
|
129
|
+
num_native_operations[op].append(q.count_ops()[op])
|
|
130
|
+
|
|
131
|
+
avg_native_operations.update(
|
|
132
|
+
{
|
|
133
|
+
x: {"Mean": np.mean(num_native_operations[x]), "Std": np.std(num_native_operations[x])}
|
|
134
|
+
for x in native_operations
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return avg_native_operations
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# DD code to be adapted to Pulla version once released
|
|
142
|
+
# @timeit
|
|
143
|
+
# def execute_with_dd(
|
|
144
|
+
# backend: IQMBackendBase, transpiled_circuits: List[QuantumCircuit], shots: int, dd_strategy: DDStrategy
|
|
145
|
+
# ) -> List[Dict[str, int]]:
|
|
146
|
+
# """Executes a list of transpiled quantum circuits with dynamical decoupling according to a specified strategy
|
|
147
|
+
# Args:
|
|
148
|
+
# backend (IQMBackendBase):
|
|
149
|
+
# transpiled_circuits (List[QuantumCircuit]):
|
|
150
|
+
# shots (int):
|
|
151
|
+
# dd_strategy (DDStrategy):
|
|
152
|
+
#
|
|
153
|
+
# Returns:
|
|
154
|
+
# List[Dict[str, int]]: The counts of the execution with dynamical decoupling
|
|
155
|
+
# """
|
|
156
|
+
# warnings.warn("Suppressing INFO messages from Pulla with logging.disable(sys.maxsize) - update if problematic!")
|
|
157
|
+
# logging.disable(sys.maxsize)
|
|
158
|
+
#
|
|
159
|
+
# pulla_obj = Pulla(cocos_url=iqm_url)
|
|
160
|
+
#
|
|
161
|
+
# execution_results = dd.execute_with_dd(
|
|
162
|
+
# pulla_obj,
|
|
163
|
+
# backend=backend,
|
|
164
|
+
# circuits=transpiled_circuits,
|
|
165
|
+
# shots=shots,
|
|
166
|
+
# dd_strategy=dd_strategy,
|
|
167
|
+
# )
|
|
168
|
+
#
|
|
169
|
+
# return execution_results
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# pylint: disable=too-many-branches
|
|
173
|
+
def get_iqm_backend(backend_label: str) -> IQMBackendBase:
|
|
174
|
+
"""Get the IQM backend object from a backend name (str).
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
backend_label (str): The name of the IQM backend.
|
|
178
|
+
Returns:
|
|
179
|
+
IQMBackendBase.
|
|
180
|
+
"""
|
|
181
|
+
# ****** 5Q star ******
|
|
182
|
+
# FakeAdonis
|
|
183
|
+
if backend_label.lower() in ("iqmfakeadonis", "fakeadonis"):
|
|
184
|
+
backend_object = IQMFakeAdonis()
|
|
185
|
+
|
|
186
|
+
# ****** 20Q grid ******
|
|
187
|
+
# Garnet
|
|
188
|
+
elif backend_label.lower() == "garnet":
|
|
189
|
+
iqm_server_url = "https://cocos.resonance.meetiqm.com/garnet"
|
|
190
|
+
provider = IQMProvider(iqm_server_url)
|
|
191
|
+
backend_object = provider.get_backend()
|
|
192
|
+
# FakeApollo
|
|
193
|
+
elif backend_label.lower() in ("iqmfakeapollo", "fakeapollo"):
|
|
194
|
+
backend_object = IQMFakeApollo()
|
|
195
|
+
|
|
196
|
+
# ****** 6Q Resonator Star ******
|
|
197
|
+
# Deneb
|
|
198
|
+
elif backend_label.lower() == "deneb":
|
|
199
|
+
iqm_server_url = "https://cocos.resonance.meetiqm.com/deneb"
|
|
200
|
+
provider = IQMProvider(iqm_server_url)
|
|
201
|
+
backend_object = provider.get_backend()
|
|
202
|
+
|
|
203
|
+
else:
|
|
204
|
+
raise ValueError(f"Backend {backend_label} not supported. Try 'garnet', 'deneb', 'fakeadonis' or 'fakeapollo'.")
|
|
205
|
+
|
|
206
|
+
return backend_object
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def marginal_distribution(prob_dist: Dict[str, float], indices: Iterable[int]) -> Dict[str, float]:
|
|
210
|
+
"""Compute the marginal distribution over specified bits (indices)
|
|
211
|
+
|
|
212
|
+
Params:
|
|
213
|
+
- prob_dist (dict): A dictionary with keys being bitstrings and values are their probabilities
|
|
214
|
+
- indices (list): List of bit indices to marginalize over
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
- dict: A dictionary representing the marginal distribution over the specified bits.
|
|
218
|
+
"""
|
|
219
|
+
marginal_dist: Dict[str, float] = defaultdict(float)
|
|
220
|
+
|
|
221
|
+
for bitstring, prob in prob_dist.items():
|
|
222
|
+
# Extract the bits at the specified indices and form the marginalized bitstring
|
|
223
|
+
marginalized_bitstring = "".join(bitstring[i] for i in indices)
|
|
224
|
+
# Sum up probabilities for each marginalized bitstring
|
|
225
|
+
marginal_dist[marginalized_bitstring] += prob
|
|
226
|
+
|
|
227
|
+
return dict(marginal_dist)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@timeit
|
|
231
|
+
def perform_backend_transpilation(
|
|
232
|
+
qc_list: List[QuantumCircuit],
|
|
233
|
+
backend: IQMBackendBase,
|
|
234
|
+
qubits: Sequence[int],
|
|
235
|
+
coupling_map: List[List[int]],
|
|
236
|
+
basis_gates: Tuple[str, ...] = ("r", "cz"),
|
|
237
|
+
qiskit_optim_level: int = 1,
|
|
238
|
+
optimize_sqg: bool = False,
|
|
239
|
+
drop_final_rz: bool = True,
|
|
240
|
+
routing_method: Optional[str] = "sabre",
|
|
241
|
+
) -> List[QuantumCircuit]:
|
|
242
|
+
"""
|
|
243
|
+
Transpile a list of circuits to backend specifications.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
qc_list (List[QuantumCircuit]): The original (untranspiled) list of quantum circuits.
|
|
247
|
+
backend (IQMBackendBase ): The backend to execute the benchmark on.
|
|
248
|
+
qubits (Sequence[int]): The qubits to target in the transpilation.
|
|
249
|
+
coupling_map (List[List[int]]): The target coupling map to transpile to.
|
|
250
|
+
basis_gates (Tuple[str, ...]): The basis gates.
|
|
251
|
+
qiskit_optim_level (int): Qiskit "optimization_level" value.
|
|
252
|
+
optimize_sqg (bool): Whether SQG optimization is performed taking into account virtual Z.
|
|
253
|
+
drop_final_rz (bool): Whether the SQG optimizer drops a final RZ gate.
|
|
254
|
+
routing_method (Optional[str]): The routing method employed by Qiskit's transpilation pass.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List[QuantumCircuit]: A list of transpiled quantum circuits.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
# Helper function considering whether optimize_sqg is done,
|
|
261
|
+
# and whether the coupling map is reduced (whether final physical layout must be fixed onto an auxiliary QC)
|
|
262
|
+
def transpile_and_optimize(qc, aux_qc=None):
|
|
263
|
+
transpiled = transpile(
|
|
264
|
+
qc,
|
|
265
|
+
basis_gates=basis_gates,
|
|
266
|
+
coupling_map=coupling_map,
|
|
267
|
+
optimization_level=qiskit_optim_level,
|
|
268
|
+
initial_layout=qubits if aux_qc is None else None,
|
|
269
|
+
routing_method=routing_method,
|
|
270
|
+
)
|
|
271
|
+
if optimize_sqg:
|
|
272
|
+
transpiled = optimize_single_qubit_gates(transpiled, drop_final_rz=drop_final_rz)
|
|
273
|
+
if backend.name == "IQMNdonisBackend":
|
|
274
|
+
transpiled = transpile_to_IQM(
|
|
275
|
+
transpiled, backend=backend, optimize_single_qubits=optimize_sqg, remove_final_rzs=drop_final_rz
|
|
276
|
+
)
|
|
277
|
+
if aux_qc is not None:
|
|
278
|
+
if backend.name == "IQMNdonisBackend":
|
|
279
|
+
transpiled = reduce_to_active_qubits(transpiled, backend.name)
|
|
280
|
+
transpiled = aux_qc.compose(transpiled, qubits=[0] + qubits, clbits=list(range(qc.num_clbits)))
|
|
281
|
+
else:
|
|
282
|
+
transpiled = aux_qc.compose(transpiled, qubits=qubits, clbits=list(range(qc.num_clbits)))
|
|
283
|
+
|
|
284
|
+
return transpiled
|
|
285
|
+
|
|
286
|
+
qcvv_logger.info(
|
|
287
|
+
f"Transpiling for backend {backend.name} with optimization level {qiskit_optim_level}, "
|
|
288
|
+
f"{routing_method} routing method{' and SQG optimization' if optimize_sqg else ''} all circuits"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if coupling_map == backend.coupling_map:
|
|
292
|
+
transpiled_qc_list = [transpile_and_optimize(qc) for qc in qc_list]
|
|
293
|
+
else: # The coupling map will be reduced if the physical layout is to be fixed
|
|
294
|
+
aux_qc_list = [QuantumCircuit(backend.num_qubits, q.num_clbits) for q in qc_list]
|
|
295
|
+
transpiled_qc_list = [transpile_and_optimize(qc, aux_qc=aux_qc_list[idx]) for idx, qc in enumerate(qc_list)]
|
|
296
|
+
|
|
297
|
+
return transpiled_qc_list
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def reduce_to_active_qubits(circuit: QuantumCircuit, backend_name: Optional[str] = None) -> QuantumCircuit:
|
|
301
|
+
"""
|
|
302
|
+
Reduces a quantum circuit to only its active qubits.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
backend_name (Optional[str]): The backend name, if any, in which the circuits are defined.
|
|
306
|
+
circuit (QuantumCircuit): The original quantum circuit.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
QuantumCircuit: A new quantum circuit containing only active qubits.
|
|
310
|
+
"""
|
|
311
|
+
# Identify active qubits
|
|
312
|
+
active_qubits = set()
|
|
313
|
+
for instruction in circuit.data:
|
|
314
|
+
for qubit in instruction.qubits:
|
|
315
|
+
active_qubits.add(circuit.find_bit(qubit).index)
|
|
316
|
+
if backend_name is not None and backend_name == "IQMNdonisBackend" and 0 not in active_qubits:
|
|
317
|
+
# For star systems, the resonator must always be there, regardless of whether it MOVE gates on it or not
|
|
318
|
+
active_qubits.add(0)
|
|
319
|
+
|
|
320
|
+
# Create a mapping from old qubits to new qubits
|
|
321
|
+
active_qubits = set(sorted(active_qubits))
|
|
322
|
+
qubit_map = {old_idx: new_idx for new_idx, old_idx in enumerate(active_qubits)}
|
|
323
|
+
|
|
324
|
+
# Create a new quantum circuit with the reduced number of qubits
|
|
325
|
+
reduced_circuit = QuantumCircuit(len(active_qubits))
|
|
326
|
+
|
|
327
|
+
# Add classical registers if they exist
|
|
328
|
+
if circuit.num_clbits > 0:
|
|
329
|
+
creg = ClassicalRegister(circuit.num_clbits)
|
|
330
|
+
reduced_circuit.add_register(creg)
|
|
331
|
+
|
|
332
|
+
# Copy operations to the new circuit, remapping qubits and classical bits
|
|
333
|
+
for instruction in circuit.data:
|
|
334
|
+
new_qubits = [reduced_circuit.qubits[qubit_map[circuit.find_bit(qubit).index]] for qubit in instruction.qubits]
|
|
335
|
+
new_clbits = [reduced_circuit.clbits[circuit.find_bit(clbit).index] for clbit in instruction.clbits]
|
|
336
|
+
reduced_circuit.append(instruction.operation, new_qubits, new_clbits)
|
|
337
|
+
|
|
338
|
+
return reduced_circuit
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@timeit
|
|
342
|
+
def retrieve_all_counts(iqm_jobs: List[IQMJob], identifier: Optional[str] = None) -> List[Dict[str, int]]:
|
|
343
|
+
"""Retrieve the counts from a list of IQMJob objects.
|
|
344
|
+
Args:
|
|
345
|
+
iqm_jobs (List[IQMJob]): The list of IQMJob objects.
|
|
346
|
+
identifier (Optional[str]): a string identifying the job.
|
|
347
|
+
Returns:
|
|
348
|
+
List[Dict[str, int]]: The counts of all the IQMJob objects.
|
|
349
|
+
"""
|
|
350
|
+
if identifier is None:
|
|
351
|
+
qcvv_logger.info(f"Retrieving all counts")
|
|
352
|
+
else:
|
|
353
|
+
qcvv_logger.info(f"Retrieving all counts for {identifier}")
|
|
354
|
+
final_counts = []
|
|
355
|
+
for j in iqm_jobs:
|
|
356
|
+
counts = j.result().get_counts()
|
|
357
|
+
if isinstance(counts, list):
|
|
358
|
+
final_counts.extend(counts)
|
|
359
|
+
elif isinstance(counts, dict):
|
|
360
|
+
final_counts.append(counts)
|
|
361
|
+
|
|
362
|
+
return final_counts
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def retrieve_all_job_metadata(
|
|
366
|
+
iqm_jobs: List[IQMJob],
|
|
367
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
368
|
+
"""Retrieve the counts from a list of IQMJob objects.
|
|
369
|
+
Args:
|
|
370
|
+
iqm_jobs List[IQMJob]: The list of IQMJob objects.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Dict[str, Dict[str, Any]]: Relevant metadata of all the IQMJob objects.
|
|
374
|
+
"""
|
|
375
|
+
all_meta = {}
|
|
376
|
+
|
|
377
|
+
for index, j in enumerate(iqm_jobs):
|
|
378
|
+
all_attributes_j = dir(j)
|
|
379
|
+
all_meta.update(
|
|
380
|
+
{
|
|
381
|
+
"batch_job_"
|
|
382
|
+
+ str(index + 1): {
|
|
383
|
+
"job_id": j.job_id() if "job_id" in all_attributes_j else None,
|
|
384
|
+
"backend": j.backend().name if "backend" in all_attributes_j else None,
|
|
385
|
+
"status": j.status().value if "status" in all_attributes_j else None,
|
|
386
|
+
"circuits_in_batch": (
|
|
387
|
+
len(cast(List, j.circuit_metadata)) if "circuit_metadata" in all_attributes_j else None
|
|
388
|
+
),
|
|
389
|
+
"shots": j.metadata["shots"] if "shots" in j.metadata.keys() else None,
|
|
390
|
+
"timestamps": j.metadata["timestamps"] if "timestamps" in j.metadata.keys() else None,
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
return all_meta
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def set_coupling_map(
|
|
399
|
+
qubits: Sequence[int], backend: IQMBackendBase, physical_layout: Literal["fixed", "batching"] = "fixed"
|
|
400
|
+
) -> CouplingMap:
|
|
401
|
+
"""Set a coupling map according to the specified physical layout.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
qubits (Sequence[int]): the list of physical qubits to consider.
|
|
405
|
+
backend (IQMBackendBase): the backend from IQM.
|
|
406
|
+
physical_layout (Literal["fixed", "batching"]): the physical layout type to consider.
|
|
407
|
+
- "fixed" sets a coupling map restricted to the input qubits -> results will be constrained to measure those qubits.
|
|
408
|
+
- "batching" sets the coupling map of the backend -> results in a benchmark will be "batched" according to final layouts.
|
|
409
|
+
* Default is "fixed".
|
|
410
|
+
Returns:
|
|
411
|
+
A coupling map according to the specified physical layout.
|
|
412
|
+
"""
|
|
413
|
+
if physical_layout == "fixed":
|
|
414
|
+
if backend.name == "IQMNdonisBackend":
|
|
415
|
+
return backend.coupling_map.reduce(mapping=[0] + list(qubits))
|
|
416
|
+
return backend.coupling_map.reduce(mapping=qubits)
|
|
417
|
+
if physical_layout == "batching":
|
|
418
|
+
return backend.coupling_map
|
|
419
|
+
raise ValueError('physical_layout must either be "fixed" or "batching"')
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@timeit
|
|
423
|
+
def sort_batches_by_final_layout(
|
|
424
|
+
transpiled_circuit_list: List[QuantumCircuit],
|
|
425
|
+
) -> Tuple[Dict[Tuple, List[QuantumCircuit]], Dict[Tuple, List[int]]]:
|
|
426
|
+
"""Sort batches of circuits according to the final measurement mapping in their corresponding backend.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
transpiled_circuit_list (List[QuantumCircuit]): the list of circuits transpiled to a given backend.
|
|
430
|
+
Returns:
|
|
431
|
+
sorted_circuits (Dict[Tuple, List[QuantumCircuit]]): dictionary, keys: final measured qubits, values: corresponding circuits.
|
|
432
|
+
sorted_indices (Dict[Tuple, List[int]]): dictionary, keys: final measured qubits, values: corresponding circuit indices.
|
|
433
|
+
"""
|
|
434
|
+
qcvv_logger.info("Now getting the final measurement maps of all circuits")
|
|
435
|
+
all_measurement_maps = [tuple(final_measurement_mapping(qc).values()) for qc in transpiled_circuit_list]
|
|
436
|
+
unique_measurement_maps = set(tuple(sorted(x)) for x in all_measurement_maps)
|
|
437
|
+
sorted_circuits: Dict[Tuple, List[QuantumCircuit]] = {u: [] for u in unique_measurement_maps}
|
|
438
|
+
sorted_indices: Dict[Tuple, List[int]] = {i: [] for i in unique_measurement_maps}
|
|
439
|
+
for index, qc in enumerate(transpiled_circuit_list):
|
|
440
|
+
final_measurement = all_measurement_maps[index]
|
|
441
|
+
final_measurement = tuple(sorted(final_measurement))
|
|
442
|
+
sorted_circuits[final_measurement].append(qc)
|
|
443
|
+
sorted_indices[final_measurement].append(index)
|
|
444
|
+
|
|
445
|
+
if len(sorted_circuits) == 1:
|
|
446
|
+
qcvv_logger.info(f"The routing method generated a single batch of circuits to be measured")
|
|
447
|
+
else:
|
|
448
|
+
qcvv_logger.info(f"The routing method generated {len(sorted_circuits)} batches of circuits to be measured")
|
|
449
|
+
|
|
450
|
+
return sorted_circuits, sorted_indices
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@timeit
|
|
454
|
+
def submit_execute(
|
|
455
|
+
sorted_transpiled_qc_list: Dict[Tuple, List[QuantumCircuit]],
|
|
456
|
+
backend: IQMBackendBase,
|
|
457
|
+
shots: int,
|
|
458
|
+
calset_id: Optional[str],
|
|
459
|
+
max_gates_per_batch: Optional[int],
|
|
460
|
+
) -> List[IQMJob]:
|
|
461
|
+
"""Submit for execute a list of quantum circuits on the specified Backend.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
sorted_transpiled_qc_list (Dict[Tuple, List[QuantumCircuit]]): the list of quantum circuits to be executed.
|
|
465
|
+
backend (IQMBackendBase): the backend to execute the circuits on.
|
|
466
|
+
shots (int): the number of shots per circuit.
|
|
467
|
+
calset_id (Optional[str]): the calibration set ID, uses the latest one if None.
|
|
468
|
+
max_gates_per_batch (int): the maximum number of gates per batch sent to the backend, used to make manageable batches.
|
|
469
|
+
Returns:
|
|
470
|
+
List[IQMJob]: the IQMJob objects of the executed circuits.
|
|
471
|
+
"""
|
|
472
|
+
final_jobs = []
|
|
473
|
+
for k in sorted(
|
|
474
|
+
sorted_transpiled_qc_list.keys(),
|
|
475
|
+
key=lambda x: len(sorted_transpiled_qc_list[x]),
|
|
476
|
+
reverse=True,
|
|
477
|
+
):
|
|
478
|
+
# sorted is so batches are looped from larger to smaller
|
|
479
|
+
qcvv_logger.info(
|
|
480
|
+
f"Submitting batch with {len(sorted_transpiled_qc_list[k])} circuits corresponding to qubits {list(k)}"
|
|
481
|
+
)
|
|
482
|
+
# Divide into batches according to maximum gate count per batch
|
|
483
|
+
if max_gates_per_batch is None:
|
|
484
|
+
jobs = backend.run(sorted_transpiled_qc_list[k], shots=shots, calibration_set_id=calset_id)
|
|
485
|
+
final_jobs.append(jobs)
|
|
486
|
+
else:
|
|
487
|
+
# Calculate average gate count per quantum circuit
|
|
488
|
+
avg_gates_per_qc = sum(sum(qc.count_ops().values()) for qc in sorted_transpiled_qc_list[k]) / len(
|
|
489
|
+
sorted_transpiled_qc_list[k]
|
|
490
|
+
)
|
|
491
|
+
final_batch_jobs = []
|
|
492
|
+
for index, qc_batch in enumerate(
|
|
493
|
+
chunked(
|
|
494
|
+
sorted_transpiled_qc_list[k],
|
|
495
|
+
max(1, floor(max_gates_per_batch / avg_gates_per_qc)),
|
|
496
|
+
)
|
|
497
|
+
):
|
|
498
|
+
qcvv_logger.info(
|
|
499
|
+
f"max_gates_per_batch restriction: submitting subbatch #{index+1} with {len(qc_batch)} circuits corresponding to qubits {list(k)}"
|
|
500
|
+
)
|
|
501
|
+
batch_jobs = backend.run(qc_batch, shots=shots, calibration_set_id=calset_id)
|
|
502
|
+
final_batch_jobs.append(batch_jobs)
|
|
503
|
+
final_jobs.extend(final_batch_jobs)
|
|
504
|
+
|
|
505
|
+
return final_jobs
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def xrvariable_to_counts(dataset: xr.Dataset, identifier: str, counts_range: int) -> List[Dict[str, int]]:
|
|
509
|
+
"""Retrieve counts from xarray dataset.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
dataset (xr.Dataset): the dataset to extract counts from.
|
|
513
|
+
identifier (str): the identifier for the dataset counts.
|
|
514
|
+
counts_range (int): the range of counts to extract (e.g., the amount of circuits that were executed).
|
|
515
|
+
Returns:
|
|
516
|
+
List[Dict[str, int]]: A list of counts dictionaries from the dataset.
|
|
517
|
+
"""
|
|
518
|
+
return [
|
|
519
|
+
dict(zip(list(dataset[f"{identifier}_state_{u}"].data), dataset[f"{identifier}_counts_{u}"].data))
|
|
520
|
+
for u in range(counts_range)
|
|
521
|
+
]
|