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.

Files changed (42) hide show
  1. iqm/benchmarks/__init__.py +31 -0
  2. iqm/benchmarks/benchmark.py +109 -0
  3. iqm/benchmarks/benchmark_definition.py +264 -0
  4. iqm/benchmarks/benchmark_experiment.py +163 -0
  5. iqm/benchmarks/compressive_gst/__init__.py +20 -0
  6. iqm/benchmarks/compressive_gst/compressive_gst.py +1029 -0
  7. iqm/benchmarks/entanglement/__init__.py +18 -0
  8. iqm/benchmarks/entanglement/ghz.py +802 -0
  9. iqm/benchmarks/logging_config.py +29 -0
  10. iqm/benchmarks/optimization/__init__.py +18 -0
  11. iqm/benchmarks/optimization/qscore.py +719 -0
  12. iqm/benchmarks/quantum_volume/__init__.py +21 -0
  13. iqm/benchmarks/quantum_volume/clops.py +726 -0
  14. iqm/benchmarks/quantum_volume/quantum_volume.py +854 -0
  15. iqm/benchmarks/randomized_benchmarking/__init__.py +18 -0
  16. iqm/benchmarks/randomized_benchmarking/clifford_1q.pkl +0 -0
  17. iqm/benchmarks/randomized_benchmarking/clifford_2q.pkl +0 -0
  18. iqm/benchmarks/randomized_benchmarking/clifford_rb/__init__.py +19 -0
  19. iqm/benchmarks/randomized_benchmarking/clifford_rb/clifford_rb.py +386 -0
  20. iqm/benchmarks/randomized_benchmarking/interleaved_rb/__init__.py +19 -0
  21. iqm/benchmarks/randomized_benchmarking/interleaved_rb/interleaved_rb.py +555 -0
  22. iqm/benchmarks/randomized_benchmarking/mirror_rb/__init__.py +19 -0
  23. iqm/benchmarks/randomized_benchmarking/mirror_rb/mirror_rb.py +810 -0
  24. iqm/benchmarks/randomized_benchmarking/multi_lmfit.py +86 -0
  25. iqm/benchmarks/randomized_benchmarking/randomized_benchmarking_common.py +892 -0
  26. iqm/benchmarks/readout_mitigation.py +290 -0
  27. iqm/benchmarks/utils.py +521 -0
  28. iqm_benchmarks-1.3.dist-info/LICENSE +205 -0
  29. iqm_benchmarks-1.3.dist-info/METADATA +190 -0
  30. iqm_benchmarks-1.3.dist-info/RECORD +42 -0
  31. iqm_benchmarks-1.3.dist-info/WHEEL +5 -0
  32. iqm_benchmarks-1.3.dist-info/top_level.txt +2 -0
  33. mGST/LICENSE +21 -0
  34. mGST/README.md +54 -0
  35. mGST/additional_fns.py +962 -0
  36. mGST/algorithm.py +733 -0
  37. mGST/compatibility.py +238 -0
  38. mGST/low_level_jit.py +694 -0
  39. mGST/optimization.py +349 -0
  40. mGST/qiskit_interface.py +282 -0
  41. mGST/reporting/figure_gen.py +334 -0
  42. mGST/reporting/reporting.py +710 -0
@@ -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
+ ]