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,892 @@
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
+ Common functions for Randomized Benchmarking-based techniques
17
+ """
18
+
19
+ from importlib import import_module
20
+ from itertools import chain
21
+ import os
22
+ import pickle
23
+ import random
24
+ from typing import Any, Callable, Dict, List, Optional, Tuple, cast
25
+
26
+ from lmfit import Parameters, minimize
27
+ from lmfit.minimizer import MinimizerResult
28
+ from matplotlib.collections import PolyCollection
29
+ from matplotlib.figure import Figure
30
+ import matplotlib.pyplot as plt
31
+ import numpy as np
32
+ from qiskit import QuantumCircuit, transpile
33
+ from qiskit.quantum_info import Clifford
34
+ import xarray as xr
35
+
36
+ from iqm.benchmarks.logging_config import qcvv_logger
37
+ from iqm.benchmarks.randomized_benchmarking.multi_lmfit import create_multi_dataset_params, multi_dataset_residual
38
+ from iqm.benchmarks.utils import get_iqm_backend, marginal_distribution, submit_execute, timeit
39
+ from iqm.qiskit_iqm import optimize_single_qubit_gates
40
+ from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
41
+
42
+
43
+ def compute_inverse_clifford(qc_inv: QuantumCircuit, clifford_dictionary: Dict) -> Optional[QuantumCircuit]:
44
+ """Function to compute the inverse Clifford of a circuit
45
+ Args:
46
+ qc_inv (QuantumCircuit): The Clifford circuit to be inverted
47
+ clifford_dictionary (Dict): A dictionary of Clifford gates labeled by (de)stabilizers
48
+ Returns:
49
+ Optional[QuantumCircuit]: A Clifford circuit
50
+ """
51
+ label_inv = str(Clifford(qc_inv).adjoint().to_labels(mode="B"))
52
+ compiled_inverse = cast(dict, clifford_dictionary)[label_inv]
53
+ return compiled_inverse
54
+
55
+
56
+ def estimate_survival_probabilities(num_qubits: int, counts: List[Dict[str, int]]) -> List[float]:
57
+ """Compute a result's probability of being on the ground state.
58
+ Args:
59
+ num_qubits (int): the number of qubits
60
+ counts (List[Dict[str, int]]): the result of the execution of a list of quantum circuits (counts)
61
+
62
+ Returns:
63
+ List[float]: the ground state probabilities of the RB sequence
64
+ """
65
+ return [c["0" * num_qubits] / sum(c.values()) if "0" * num_qubits in c.keys() else 0 for c in counts]
66
+
67
+
68
+ def exponential_rb(
69
+ depths: np.ndarray, depolarization_probability: float, offset: float, amplitude: float
70
+ ) -> np.ndarray:
71
+ """Fit function for interleaved/non-interleaved RB
72
+
73
+ Args:
74
+ depths (np.ndarray): the depths of the RB experiment
75
+ depolarization_probability (float): the depolarization value (1-p) of the RB decay
76
+ offset (float): the offset of the RB decay
77
+ amplitude (float): the amplitude of the RB decay
78
+
79
+ Returns:
80
+ np.ndarray: the exponential fit function
81
+ """
82
+ return (amplitude - offset) * (1 - depolarization_probability) ** depths + offset
83
+
84
+
85
+ def fit_decay_lmfit(
86
+ func: Callable,
87
+ qubit_set: List[int],
88
+ data: List[List[float]] | List[List[List[float]]],
89
+ rb_identifier: str,
90
+ simultaneous_fit_vars: Optional[List[str]] = None,
91
+ ) -> Tuple[np.ndarray, Parameters]:
92
+ """Perform a fitting routine for 0th-order (Ap^m+B) RB using lmfit
93
+
94
+ Args:
95
+ func (Callable): the model function for fitting
96
+ qubit_set (List[int]): the qubits entering the model
97
+ data (List[List[float]] | List[List[List[float]]]): the data to be fitted
98
+ rb_identifier (str): the RB identifier, either "stdrb", "irb" or "mrb"
99
+ simultaneous_fit_vars (List[str], optional): the list of variables used to fit simultaneously
100
+ Returns:
101
+ A tuple of fitting data (list of lists of average fidelities or polarizations) and MRB fit parameters
102
+ """
103
+ n_qubits = len(qubit_set)
104
+
105
+ fidelity_guess = 0.988**n_qubits
106
+ offset_guess = 0.2 if n_qubits == 2 else 0.65
107
+ amplitude_guess = 0.8 if n_qubits == 2 else 0.35
108
+
109
+ estimates = {
110
+ "depolarization_probability": 2 * (1 - fidelity_guess),
111
+ "offset": offset_guess,
112
+ "amplitude": amplitude_guess + offset_guess,
113
+ }
114
+ constraints = {
115
+ "depolarization_probability": {"min": 0, "max": 1},
116
+ "offset": {"min": 0, "max": 1},
117
+ "amplitude": {"min": 0, "max": 1},
118
+ }
119
+ if rb_identifier in ("clifford", "mrb"):
120
+ fit_data = np.array([np.mean(data, axis=1)])
121
+ if rb_identifier == "clifford":
122
+ params = create_multi_dataset_params(
123
+ func, fit_data, initial_guesses=estimates, constraints=None, simultaneously_fit_vars=None
124
+ )
125
+ params.add(f"p_rb", expr=f"1-depolarization_probability_{1}")
126
+ params.add(f"fidelity_per_clifford", expr=f"p_rb + (1 - p_rb) / (2**{n_qubits})")
127
+ else:
128
+ params = create_multi_dataset_params(
129
+ func, fit_data, initial_guesses=None, constraints=constraints, simultaneously_fit_vars=None
130
+ )
131
+ params.add(f"p_mrb", expr=f"1-depolarization_probability_{1}")
132
+ params.add(f"fidelity_mrb", expr=f"1 - (1 - p_mrb) * (1 - 1 / (4 ** {n_qubits}))")
133
+ else:
134
+ fit_data = np.array([np.mean(data[0], axis=1), np.mean(data[1], axis=1)])
135
+ params = create_multi_dataset_params(
136
+ func,
137
+ fit_data,
138
+ initial_guesses=estimates,
139
+ constraints=None,
140
+ simultaneously_fit_vars=simultaneous_fit_vars,
141
+ )
142
+ params.add(f"p_rb", expr=f"1-depolarization_probability_{1}")
143
+ params.add(f"fidelity_per_clifford", expr=f"p_rb + (1 - p_rb) / (2**{n_qubits})")
144
+ params.add(f"p_irb", expr=f"1-depolarization_probability_{2}")
145
+ params.add(f"interleaved_fidelity", expr=f"p_irb / p_rb + (1 - p_irb / p_rb) / (2**{n_qubits})")
146
+
147
+ return fit_data, params
148
+
149
+
150
+ @timeit
151
+ def generate_all_rb_circuits(
152
+ qubits: List[int],
153
+ sequence_lengths: List[int],
154
+ clifford_dict: Dict[str, QuantumCircuit],
155
+ num_circuit_samples: int,
156
+ backend_arg: str | IQMBackendBase,
157
+ interleaved_gate: Optional[QuantumCircuit],
158
+ ) -> Tuple[Dict[int, List[QuantumCircuit]], Dict[int, List[QuantumCircuit]]]:
159
+ """
160
+ Args:
161
+ qubits (List[int]): List of qubits
162
+ sequence_lengths (List[int]): List of sequence lengths
163
+ clifford_dict (Dict[str, QuantumCircuit]): the dictionary of Clifford circuits
164
+ num_circuit_samples (int): the number of circuits samples
165
+ backend_arg (str | IQMBackendBase): the backend fir which to generate the circuits.
166
+ interleaved_gate (str): the name of the interleaved gate
167
+ Returns:
168
+ Tuple of untranspiled and transpiled circuits for all class-defined sequence lengths
169
+ """
170
+ untranspiled = {}
171
+ transpiled = {}
172
+ for s in sequence_lengths:
173
+ qcvv_logger.info(f"Now at sequence length {s}")
174
+ untranspiled[s], transpiled[s] = generate_random_clifford_seq_circuits(
175
+ qubits, clifford_dict, s, num_circuit_samples, backend_arg, interleaved_gate
176
+ )
177
+ return untranspiled, transpiled
178
+
179
+
180
+ # pylint: disable=too-many-branches, disable=too-many-statements
181
+ @timeit
182
+ def generate_fixed_depth_parallel_rb_circuits(
183
+ qubits_array: List[List[int]],
184
+ cliffords_1q: Dict[str, QuantumCircuit],
185
+ cliffords_2q: Dict[str, QuantumCircuit],
186
+ sequence_length: int,
187
+ num_samples: int,
188
+ backend_arg: IQMBackendBase | str,
189
+ interleaved_gate: Optional[QuantumCircuit] = None,
190
+ ) -> Tuple[List[QuantumCircuit], List[QuantumCircuit]]:
191
+ """Generates parallel RB circuits, before and after transpilation, at fixed depth
192
+
193
+ Args:
194
+ qubits_array (List[List[int]]): the qubits entering the quantum circuits
195
+ cliffords_1q (Dict[str, QuantumCircuit]): dictionary of 1-qubit Cliffords in terms of IQM-native r and CZ gates
196
+ cliffords_2q (Dict[str, QuantumCircuit]): dictionary of 2-qubit Cliffords in terms of IQM-native r and CZ gates
197
+ sequence_length (int): the number of random Cliffords in the circuits
198
+ num_samples (int): the number of circuit samples
199
+ backend_arg (IQMBackendBase | str): the backend to transpile the circuits to
200
+ interleaved_gate (Optional[QuantumCircuit]): whether the circuits should have interleaved gates
201
+ Returns:
202
+ A list of QuantumCircuits of given RB sequence length for parallel RB
203
+ """
204
+
205
+ if isinstance(backend_arg, str):
206
+ backend = get_iqm_backend(backend_arg)
207
+ else:
208
+ backend = backend_arg
209
+
210
+ # Identify total amount of qubits
211
+ qubit_counts = [len(x) for x in qubits_array]
212
+
213
+ # Shuffle qubits_array: we don't want unnecessary qubit registers
214
+ shuffled_qubits_array = relabel_qubits_array_from_zero(qubits_array)
215
+ # The total amount of qubits the circuits will have
216
+ n_qubits = sum(qubit_counts)
217
+
218
+ # Get the keys of the Clifford dictionaries
219
+ clifford_1q_keys = list(cliffords_1q.keys())
220
+ clifford_2q_keys = list(cliffords_2q.keys())
221
+
222
+ # Generate the circuit samples
223
+ circuits_list: List[QuantumCircuit] = []
224
+ circuits_transpiled_list: List[QuantumCircuit] = []
225
+ for _ in range(num_samples):
226
+ circuit = QuantumCircuit(n_qubits)
227
+ # Need to track inverses on each qubit, or each pair of qubits, separately
228
+ circ_inverses = [QuantumCircuit(n) for n in qubit_counts]
229
+ # Place Clifford sequences on each
230
+ for _ in range(sequence_length):
231
+ # Sample the random Cliffords for each qubit or qubits
232
+ cliffords = []
233
+ for n in qubit_counts:
234
+ if n == 1:
235
+ rand_key = random.choice(clifford_1q_keys)
236
+ c_1q = cast(dict, cliffords_1q)[rand_key]
237
+ cliffords.append(c_1q)
238
+ else:
239
+ rand_key = random.choice(clifford_2q_keys)
240
+ c_2q = cast(dict, cliffords_2q)[rand_key]
241
+ cliffords.append(c_2q)
242
+ # Compose the sampled Cliffords in the circuit and add barrier
243
+ for n, shuffled_qubits in enumerate(shuffled_qubits_array):
244
+ circuit.compose(cliffords[n], qubits=shuffled_qubits, inplace=True)
245
+ circ_inverses[n].compose(cliffords[n], qubits=list(range(qubit_counts[n])), inplace=True)
246
+ circuit.barrier() # NB: mind the barriers!
247
+
248
+ if interleaved_gate is not None:
249
+ for n, shuffled_qubits in enumerate(shuffled_qubits_array):
250
+ circuit.compose(interleaved_gate, qubits=shuffled_qubits, inplace=True)
251
+ circ_inverses[n].compose(interleaved_gate, qubits=list(range(qubit_counts[n])), inplace=True)
252
+ circuit.barrier()
253
+
254
+ # Compile and compose the inverse
255
+ # compiled_inverse_clifford = []
256
+ for n, shuffled_qubits in enumerate(shuffled_qubits_array):
257
+ clifford_dict = cliffords_1q if qubit_counts[n] == 1 else cliffords_2q
258
+ circuit.compose(
259
+ compute_inverse_clifford(circ_inverses[n], clifford_dict),
260
+ qubits=shuffled_qubits,
261
+ inplace=True,
262
+ )
263
+ # Add measurement
264
+ circuit.measure_all()
265
+
266
+ circuits_list.append(circuit)
267
+ # Transpilation here only means to the layout - avoid using automated transpilers!
268
+ circuit_transpiled = QuantumCircuit(backend.num_qubits)
269
+ qubits_array_flat = [x for y in qubits_array for x in y]
270
+ circuit_transpiled.compose(circuit, qubits=qubits_array_flat, inplace=True)
271
+ circuits_transpiled_list.append(circuit_transpiled)
272
+
273
+ return circuits_list, circuits_transpiled_list
274
+
275
+
276
+ def generate_random_clifford_seq_circuits(
277
+ qubits: List[int],
278
+ clifford_dict: Dict[str, QuantumCircuit],
279
+ seq_length: int,
280
+ num_circ_samples: int,
281
+ backend_arg: str | IQMBackendBase,
282
+ interleaved_gate: Optional[QuantumCircuit] = None,
283
+ ) -> Tuple[List[QuantumCircuit], List[QuantumCircuit]]:
284
+ """Generate random Clifford circuits in native gates for a given sequence length.
285
+
286
+ Args:
287
+ qubits (List[int]): the list of qubits
288
+ clifford_dict (Dict[str, QuantumCircuit]): A dictionary of Clifford gates labeled by (de)stabilizers
289
+ seq_length (int): the sequence length
290
+ num_circ_samples (int): the number of samples
291
+ backend_arg (str | IQMBackendBase):
292
+ interleaved_gate (Optional[QuantumCircuit]): Clifford native gate to be interleaved - None by default
293
+ Returns:
294
+ List[QuantumCircuit]: the list of `self.num_samples` random Clifford quantum circuits
295
+ """
296
+ if isinstance(backend_arg, str):
297
+ backend = get_iqm_backend(backend_arg)
298
+ else:
299
+ backend = backend_arg
300
+
301
+ qc_list = [] # List of random Clifford circuits
302
+ qc_list_transpiled = []
303
+ num_qubits = len(qubits)
304
+ logic_qubits = list(range(num_qubits))
305
+
306
+ if num_qubits > 2:
307
+ raise ValueError("Please specify qubit layouts with only n=1 or n=2 qubits. Run MRB for n>2 instead.")
308
+
309
+ clifford_keys = []
310
+ if clifford_dict is not None:
311
+ clifford_keys = list(clifford_dict.keys())
312
+
313
+ for _ in range(num_circ_samples):
314
+ qc = QuantumCircuit(num_qubits)
315
+ qc_inv = QuantumCircuit(num_qubits)
316
+ # Compose circuit with random Clifford gates
317
+ for _ in range(seq_length):
318
+ rand_key = random.choice(clifford_keys)
319
+ clifford = cast(dict, clifford_dict)[rand_key]
320
+ # Append clifford
321
+ qc.compose(clifford, qubits=logic_qubits, inplace=True)
322
+ # Append clifford gate to circuit to invert
323
+ qc_inv.compose(clifford, qubits=logic_qubits, inplace=True)
324
+ qc.barrier()
325
+ # Append interleaved if not None
326
+ if interleaved_gate is not None:
327
+ qc.compose(interleaved_gate, qubits=logic_qubits, inplace=True)
328
+ qc_inv.compose(interleaved_gate, qubits=logic_qubits, inplace=True)
329
+ qc.barrier()
330
+ # Append the inverse
331
+ qc.compose(
332
+ compute_inverse_clifford(qc_inv, clifford_dict),
333
+ qubits=logic_qubits,
334
+ inplace=True,
335
+ )
336
+ qc.measure_all()
337
+
338
+ qc_list.append(qc)
339
+ qc_transpiled = QuantumCircuit(backend.num_qubits)
340
+ qc_transpiled.compose(qc, qubits=qubits, inplace=True)
341
+ qc_list_transpiled.append(qc_transpiled)
342
+
343
+ return qc_list, qc_list_transpiled
344
+
345
+
346
+ def get_survival_probabilities(num_qubits: int, counts: List[Dict[str, int]]) -> List[float]:
347
+ """Compute a result's probability of being on the ground state.
348
+
349
+ Args:
350
+ num_qubits (int): the number of qubits
351
+ counts (List[Dict[str, int]]): the result of the execution of a list of quantum circuits (counts)
352
+
353
+ Returns:
354
+ List[float]: the ground state probabilities of the RB sequence
355
+ """
356
+ return [c["0" * num_qubits] / sum(c.values()) if "0" * num_qubits in c.keys() else 0 for c in counts]
357
+
358
+
359
+ def import_native_gate_cliffords() -> Tuple[Dict[str, QuantumCircuit], Dict[str, QuantumCircuit]]:
360
+ """Import native gate Clifford dictionaries
361
+ Returns:
362
+ Dictionaries of 1Q and 2Q Clifford gates
363
+ """
364
+ # Import the native-gate Cliffords
365
+ with open(os.path.join(os.path.dirname(__file__), "clifford_1q.pkl"), "rb") as f1q:
366
+ clifford_1q_dict = pickle.load(f1q)
367
+ with open(os.path.join(os.path.dirname(__file__), "clifford_2q.pkl"), "rb") as f2q:
368
+ clifford_2q_dict = pickle.load(f2q)
369
+ qcvv_logger.info("Clifford dictionaries imported successfully !")
370
+ return clifford_1q_dict, clifford_2q_dict
371
+
372
+
373
+ def lmfit_minimizer(
374
+ fit_parameters: Parameters, fit_data: np.ndarray, depths: List[int], func: Callable
375
+ ) -> MinimizerResult:
376
+ """
377
+ Args:
378
+ fit_parameters (Parameters): the parameters to fit
379
+ fit_data (np.ndarray): the data to fit
380
+ depths (List[int]): the depths of the RB experiment
381
+ func (Callable): the model function for fitting
382
+ Returns:
383
+ MinimizerResult: the result of the minimization
384
+ """
385
+ return minimize(
386
+ fcn=multi_dataset_residual,
387
+ params=fit_parameters,
388
+ args=(depths, fit_data, func),
389
+ )
390
+
391
+
392
+ def relabel_qubits_array_from_zero(arr: List[List[int]]) -> List[List[int]]:
393
+ """Helper function to relabel a qubits array to an increasingly ordered one starting from zero
394
+ e.g., [[2,3], [5], [7,8]] -> [[0,1], [2], [3,4]]
395
+ Note: this assumes the input array is sorted in increasing order!
396
+ """
397
+ # Flatten the original array
398
+ flat_list = [item for sublist in arr for item in sublist]
399
+ # Generate a list of ordered numbers with the same length as the flattened array
400
+ ordered_indices = list(range(len(flat_list)))
401
+ # Reconstruct the list of lists structure
402
+ result = []
403
+ index = 0
404
+ for sublist in arr:
405
+ result.append(ordered_indices[index : index + len(sublist)])
406
+ index += len(sublist)
407
+ return result
408
+
409
+
410
+ def submit_parallel_rb_job(
411
+ backend_arg: IQMBackendBase,
412
+ qubits_array: List[List[int]],
413
+ depth: int,
414
+ sorted_transpiled_circuit_dicts: Dict[Tuple[int, ...], List[QuantumCircuit]],
415
+ shots: int,
416
+ calset_id: Optional[str],
417
+ max_gates_per_batch: Optional[str],
418
+ ) -> Dict[str, Any]:
419
+ """Submit fixed-depth parallel MRB jobs for execution in the specified IQMBackend
420
+ Args:
421
+ backend_arg (IQMBackendBase): the IQM backend to submit the job
422
+ qubits_array (List[int]): the qubits to identify the submitted job
423
+ depth (int): the depth (number of canonical layers) of the circuits to identify the submitted job
424
+ sorted_transpiled_circuit_dicts (Dict[Tuple[int,...], List[QuantumCircuit]]): A dictionary containing all MRB circuits
425
+ shots (int): the number of shots to submit the job
426
+ calset_id (Optional[str]): the calibration identifier
427
+ max_gates_per_batch (Optional[str]): the maximum number of gates per batch to submit the job
428
+ Returns:
429
+ Dict with qubit layout, submitted job objects, type (vanilla/DD) and submission time
430
+ """
431
+ # Submit
432
+ # Send to execute on backend
433
+ # pylint: disable=unbalanced-tuple-unpacking
434
+ execution_jobs, time_submit = submit_execute(
435
+ sorted_transpiled_circuit_dicts, backend_arg, shots, calset_id, max_gates_per_batch=max_gates_per_batch
436
+ )
437
+ rb_submit_results = {
438
+ "qubits": qubits_array,
439
+ "depth": depth,
440
+ "jobs": execution_jobs,
441
+ "time_submit": time_submit,
442
+ }
443
+ return rb_submit_results
444
+
445
+
446
+ def submit_sequential_rb_jobs(
447
+ qubits: List[int],
448
+ transpiled_circuits: Dict[int, List[QuantumCircuit]],
449
+ shots: int,
450
+ backend_arg: str | IQMBackendBase,
451
+ calset_id: Optional[str] = None,
452
+ max_gates_per_batch: Optional[int] = None,
453
+ ) -> List[Dict[str, Any]]:
454
+ """Submit sequential RB jobs for execution in the specified IQMBackend
455
+ Args:
456
+ qubits (List[int]): the qubits to identify the submitted job
457
+ transpiled_circuits (Dict[str, List[QuantumCircuit]]): A dictionary containing all MRB circuits
458
+ shots (int): the number of shots to submit per job
459
+ backend_arg (IQMBackendBase): the IQM backend to submit the job
460
+ calset_id (Optional[str]): the calibration identifier
461
+ max_gates_per_batch (Optional[int]): the maximum number of gates per batch
462
+ Returns:
463
+ Dict with qubit layout, submitted job objects, type (vanilla/DD) and submission time
464
+ """
465
+ if isinstance(backend_arg, str):
466
+ backend = get_iqm_backend(backend_arg)
467
+ else:
468
+ backend = backend_arg
469
+
470
+ all_submit_results = []
471
+ rb_submit_results = {}
472
+ for depth in transpiled_circuits.keys():
473
+ # Submit - send to execute on backend
474
+ # pylint: disable=unbalanced-tuple-unpacking
475
+ execution_jobs, time_submit = submit_execute(
476
+ {tuple(qubits): transpiled_circuits[depth]}, backend, shots, calset_id, max_gates_per_batch
477
+ )
478
+ rb_submit_results[depth] = {
479
+ "qubits": qubits,
480
+ "depth": depth,
481
+ "jobs": execution_jobs,
482
+ "time_submit": time_submit,
483
+ }
484
+ all_submit_results.append(rb_submit_results[depth])
485
+
486
+ return all_submit_results
487
+
488
+
489
+ def survival_probabilities_parallel(
490
+ qubits_array: List[List[int]], counts: List[Dict[str, int]]
491
+ ) -> Dict[str, List[float]]:
492
+ """Estimates marginalized survival probabilities from a parallel RB execution (at fixed depth)
493
+ Args:
494
+ qubits_array (List[int]): List of qubits in which the experiment was performed
495
+ counts (Dict[str, int]): The measurement counts for corresponding bitstrings
496
+ Returns:
497
+ Dict[str, List[float]]: The survival probabilities for each qubit
498
+ """
499
+ # Global probability estimations
500
+ global_probabilities = [{k: v / sum(c.values()) for k, v in c.items()} for c in counts]
501
+
502
+ # The bit indices
503
+ all_bit_indices = relabel_qubits_array_from_zero(qubits_array)
504
+
505
+ # Estimate all marginal probabilities
506
+ marginal_probabilities: Dict[str, List[Dict[str, float]]] = {str(q): [] for q in qubits_array}
507
+ for position, indices in enumerate(all_bit_indices):
508
+ marginal_probabilities[str(qubits_array[position])] = [
509
+ marginal_distribution(global_probability, indices) for global_probability in global_probabilities
510
+ ]
511
+
512
+ # Estimate the survival probabilities in the marginal distributions
513
+ marginal_survival_probabilities: Dict[str, List[float]] = {str(q): [] for q in qubits_array}
514
+ for q in qubits_array:
515
+ n_qubits = len(q)
516
+ marginal_survival_probabilities[str(q)] = [
517
+ c["0" * n_qubits] / sum(c.values()) if "0" * n_qubits in c.keys() else 0
518
+ for c in marginal_probabilities[str(q)]
519
+ ]
520
+
521
+ return marginal_survival_probabilities
522
+
523
+
524
+ # pylint: disable=too-many-statements
525
+ def plot_rb_decay(
526
+ identifier: str,
527
+ qubits_array: List[List[int]],
528
+ dataset: xr.Dataset,
529
+ observations: Dict[int, Dict[str, Any]],
530
+ violin: bool = True,
531
+ scatter: bool = True,
532
+ bars: bool = False,
533
+ shade_stdev: bool = False,
534
+ shade_meanerror: bool = False,
535
+ logscale: bool = True,
536
+ interleaved_gate: Optional[str] = None,
537
+ mrb_2q_density: Optional[float] = None,
538
+ mrb_2q_ensemble: Optional[Dict[str, float]] = None,
539
+ ) -> Tuple[str, Figure]:
540
+ """Plot the fidelity decay and the fit to the model.
541
+
542
+ Args:
543
+ identifier (str): the type of RB experiment
544
+ qubits_array (List[List[int]]): Array of sets of qubits for which to plot decays
545
+ dataset (xr.dataset): the dataset from the experiment
546
+ observations (Dict[str, Dict[str, Any]]): the corresponding observations from the experiment
547
+ bars (bool, optional): Whether error bars are plotted or not. Defaults to False
548
+ violin (bool, optional): Whether violins are plotted or not. Defaults to True
549
+ scatter (bool, optional): Whether all individual points are plotted or not. Defaults to True
550
+ shade_stdev (bool, optional): Whether standard deviations are shaded or not. Defaults to False
551
+ shade_meanerror (bool, optional): Whether to shade standard deviations. Defaults to False
552
+ logscale (bool, optional): Whether x-axis uses logscale. Defaults to True
553
+ interleaved_gate (Optional[str]):
554
+ mrb_2q_density (Optional[float], optional): Density of MRB 2Q gates. Defaults to None.
555
+ mrb_2q_ensemble (Optional[Dict[str, float]], optional): MRB ensemble of 2Q gates. Defaults to None.
556
+
557
+ Returns:
558
+ Tuple[str, Figure]: the plot title and the figure
559
+ """
560
+ fig, ax = plt.subplots()
561
+
562
+ cmap = plt.get_cmap("winter")
563
+
564
+ num_circuit_samples = dataset.attrs["num_circuit_samples"]
565
+ timestamp = dataset.attrs["execution_timestamp"]
566
+ backend_name = dataset.attrs["backend_name"]
567
+
568
+ # Fetch the relevant observations indexed by qubit layouts
569
+ depths = {}
570
+ polarizations = {}
571
+ average_polarizations = {}
572
+ stddevs_from_mean = {}
573
+ fidelity_value = {}
574
+ fidelity_stderr = {}
575
+ decay_rate = {}
576
+ offset = {}
577
+ amplitude = {}
578
+ rb_type_keys = []
579
+ colors = []
580
+ if identifier != "irb":
581
+ rb_type_keys = [identifier]
582
+ colors = [cmap(i) for i in np.linspace(start=1, stop=0, num=len(qubits_array)).tolist()]
583
+ if identifier == "mrb":
584
+ depths[identifier] = {
585
+ str(q): list(dataset.attrs[q_idx]["polarizations"].keys()) for q_idx, q in enumerate(qubits_array)
586
+ }
587
+ polarizations[identifier] = {
588
+ str(q): dataset.attrs[q_idx]["polarizations"] for q_idx, q in enumerate(qubits_array)
589
+ }
590
+ average_polarizations[identifier] = {
591
+ str(q): dataset.attrs[q_idx]["avg_polarization_nominal_values"] for q_idx, q in enumerate(qubits_array)
592
+ }
593
+ stddevs_from_mean[identifier] = {
594
+ str(q): dataset.attrs[q_idx]["avg_polatization_stderr"] for q_idx, q in enumerate(qubits_array)
595
+ }
596
+ else:
597
+ depths[identifier] = {
598
+ str(q): list(dataset.attrs[q_idx]["fidelities"].keys()) for q_idx, q in enumerate(qubits_array)
599
+ }
600
+ polarizations[identifier] = {
601
+ str(q): dataset.attrs[q_idx]["fidelities"] for q_idx, q in enumerate(qubits_array)
602
+ }
603
+ average_polarizations[identifier] = {
604
+ str(q): dataset.attrs[q_idx]["avg_fidelities_nominal_values"] for q_idx, q in enumerate(qubits_array)
605
+ }
606
+ stddevs_from_mean[identifier] = {
607
+ str(q): dataset.attrs[q_idx]["avg_fidelities_stderr"] for q_idx, q in enumerate(qubits_array)
608
+ }
609
+ # These are common to both MRB and standard Clifford
610
+ fidelity_value[identifier] = {
611
+ str(q): observations[q_idx]["avg_gate_fidelity"]["value"] for q_idx, q in enumerate(qubits_array)
612
+ }
613
+ fidelity_stderr[identifier] = {
614
+ str(q): observations[q_idx]["avg_gate_fidelity"]["uncertainty"] for q_idx, q in enumerate(qubits_array)
615
+ }
616
+ decay_rate[identifier] = {
617
+ str(q): observations[q_idx]["decay_rate"]["value"] for q_idx, q in enumerate(qubits_array)
618
+ }
619
+ offset[identifier] = {
620
+ str(q): observations[q_idx]["fit_offset"]["value"] for q_idx, q in enumerate(qubits_array)
621
+ }
622
+ amplitude[identifier] = {
623
+ str(q): observations[q_idx]["fit_amplitude"]["value"] for q_idx, q in enumerate(qubits_array)
624
+ }
625
+ else:
626
+ rb_type_keys = list(observations[0].keys())
627
+ colors = [cmap(i) for i in np.linspace(start=1, stop=0, num=len(rb_type_keys)).tolist()]
628
+ for rb_type in rb_type_keys:
629
+ depths[rb_type] = {
630
+ str(q): list(dataset.attrs[q_idx][rb_type]["fidelities"].keys()) for q_idx, q in enumerate(qubits_array)
631
+ }
632
+ polarizations[rb_type] = {
633
+ str(q): dataset.attrs[q_idx][rb_type]["fidelities"] for q_idx, q in enumerate(qubits_array)
634
+ }
635
+ average_polarizations[rb_type] = {
636
+ str(q): dataset.attrs[q_idx][rb_type]["avg_fidelities_nominal_values"]
637
+ for q_idx, q in enumerate(qubits_array)
638
+ }
639
+ stddevs_from_mean[rb_type] = {
640
+ str(q): dataset.attrs[q_idx][rb_type]["avg_fidelities_stderr"] for q_idx, q in enumerate(qubits_array)
641
+ }
642
+ fidelity_value[rb_type] = {
643
+ str(q): observations[q_idx][rb_type]["avg_gate_fidelity"]["value"]
644
+ for q_idx, q in enumerate(qubits_array)
645
+ }
646
+ fidelity_stderr[rb_type] = {
647
+ str(q): observations[q_idx][rb_type]["avg_gate_fidelity"]["uncertainty"]
648
+ for q_idx, q in enumerate(qubits_array)
649
+ }
650
+ decay_rate[rb_type] = {
651
+ str(q): observations[q_idx][rb_type]["decay_rate"]["value"] for q_idx, q in enumerate(qubits_array)
652
+ }
653
+ offset[rb_type] = {
654
+ str(q): observations[q_idx][rb_type]["fit_offset"]["value"] for q_idx, q in enumerate(qubits_array)
655
+ }
656
+ amplitude[rb_type] = {
657
+ str(q): observations[q_idx][rb_type]["fit_amplitude"]["value"] for q_idx, q in enumerate(qubits_array)
658
+ }
659
+
660
+ for index_irb, key in enumerate(rb_type_keys):
661
+ for index_qubits, qubits in enumerate(qubits_array):
662
+ # Index for colors
663
+ index = index_irb if identifier == "irb" else index_qubits
664
+ # Plot Averages
665
+ # Draw the averages (w or w/o error bars)
666
+ if bars:
667
+ y_err_f = stddevs_from_mean[key][str(qubits)]
668
+ else:
669
+ y_err_f = None
670
+ ax.errorbar(
671
+ depths[key][str(qubits)],
672
+ average_polarizations[key][str(qubits)].values(),
673
+ yerr=y_err_f,
674
+ # label=f"Avg with {num_circ_samples} samples",
675
+ capsize=4,
676
+ color=colors[index],
677
+ fmt="o",
678
+ mec="black",
679
+ alpha=1,
680
+ markersize=5,
681
+ )
682
+ if shade_stdev:
683
+ # Shade the standard deviation
684
+ ax.fill_between(
685
+ depths[key][str(qubits)],
686
+ [
687
+ a - np.sqrt(num_circuit_samples) * b
688
+ for a, b in zip(
689
+ average_polarizations[key][str(qubits)].values(),
690
+ stddevs_from_mean[key][str(qubits)].values(),
691
+ )
692
+ ],
693
+ [
694
+ a + np.sqrt(num_circuit_samples) * b
695
+ for a, b in zip(
696
+ average_polarizations[key][str(qubits)].values(),
697
+ stddevs_from_mean[key][str(qubits)].values(),
698
+ )
699
+ ],
700
+ color=colors[index],
701
+ alpha=0.2,
702
+ interpolate=True,
703
+ )
704
+ if shade_meanerror:
705
+ # Shade the error from the mean
706
+ ax.fill_between(
707
+ depths[key][str(qubits)],
708
+ [
709
+ a - b
710
+ for a, b in zip(
711
+ average_polarizations[key][str(qubits)].values(),
712
+ stddevs_from_mean[key][str(qubits)].values(),
713
+ )
714
+ ],
715
+ [
716
+ a + b
717
+ for a, b in zip(
718
+ average_polarizations[key][str(qubits)].values(),
719
+ stddevs_from_mean[key][str(qubits)].values(),
720
+ )
721
+ ],
722
+ color=colors[index],
723
+ alpha=0.1,
724
+ interpolate=True,
725
+ )
726
+ if violin:
727
+ widths = [0.5 * m for m in depths[key][str(qubits)]]
728
+ violin_parts_f = ax.violinplot(
729
+ polarizations[key][str(qubits)].values(),
730
+ positions=depths[key][str(qubits)],
731
+ showmeans=False,
732
+ showextrema=False,
733
+ widths=widths,
734
+ )
735
+ assert isinstance(violin_parts_f["bodies"], list)
736
+ for pc in violin_parts_f["bodies"]:
737
+ assert isinstance(pc, PolyCollection)
738
+ pc.set_facecolor(colors[index])
739
+ pc.set_edgecolor("g")
740
+ pc.set_alpha(0.1)
741
+ if scatter:
742
+ # Plot the individual outcomes
743
+ flat_fidelity_data = list(chain.from_iterable(polarizations[key][str(qubits)].values()))
744
+ scatter_x = [
745
+ [depth] * len(polarizations[key][str(qubits)][depth]) for depth in depths[key][str(qubits)]
746
+ ]
747
+ flat_scatter_x = list(chain.from_iterable(scatter_x))
748
+ ax.scatter(
749
+ flat_scatter_x,
750
+ flat_fidelity_data,
751
+ s=3,
752
+ color=colors[index],
753
+ alpha=0.35,
754
+ )
755
+ # Define the points of the x-axis
756
+ x_linspace = np.linspace(np.min(depths[key][str(qubits)]), np.max(depths[key][str(qubits)]), 300)
757
+ # Calculate the fit points of the y-axis
758
+ y_fit = exponential_rb(
759
+ x_linspace,
760
+ 1 - decay_rate[key][str(qubits)],
761
+ offset[key][str(qubits)],
762
+ amplitude[key][str(qubits)],
763
+ )
764
+ # Plot
765
+ # Handle None types in instances where fidelity or stderr were not set
766
+ if fidelity_value[key][str(qubits)] is None:
767
+ fidelity_value[key][str(qubits)] = np.nan
768
+ if fidelity_stderr[key][str(qubits)] is None:
769
+ fidelity_stderr[key][str(qubits)] = np.nan
770
+
771
+ if identifier == "mrb":
772
+ plot_label = fr"$\overline{{F}}_\text{{MRB}} (n={len(qubits)})$ = {100.0 * fidelity_value[key][str(qubits)]:.2f} +/- {100.0 * fidelity_stderr[key][str(qubits)]:.2f} (%)"
773
+ elif key == "interleaved":
774
+ plot_label = fr"$\overline{{F}}_\text{{{interleaved_gate}}} ({qubits})$ = {100.0 * fidelity_value[key][str(qubits)]:.2f} +/- {100.0 * fidelity_stderr[key][str(qubits)]:.2f} (%)"
775
+ else:
776
+ plot_label = fr"$\overline{{F}}_\text{{Clifford}} ({qubits})$ = {100.0 * fidelity_value[key][str(qubits)]:.2f} +/- {100.0 * fidelity_stderr[key][str(qubits)]:.2f} (%)"
777
+
778
+ ax.plot(
779
+ x_linspace,
780
+ y_fit,
781
+ color=colors[index],
782
+ alpha=0.5,
783
+ label=plot_label,
784
+ )
785
+
786
+ # Labels
787
+ on_qubits = "on all qubit layouts"
788
+ fig_name = "all_qubit_layouts"
789
+ if len(qubits_array) == 1:
790
+ on_qubits = f"on qubits {qubits_array[0]}"
791
+ fig_name = f"{str(qubits_array[0])}"
792
+
793
+ if identifier == "irb":
794
+ ax.set_title(
795
+ f"{identifier.upper()} experiment for {interleaved_gate} {on_qubits}\nbackend: {backend_name} --- {timestamp}"
796
+ )
797
+ elif identifier == "clifford":
798
+ ax.set_title(f"{identifier.capitalize()} experiment {on_qubits}\nbackend: {backend_name} --- {timestamp}")
799
+ else:
800
+ ax.set_title(
801
+ f"{identifier.upper()} experiment {on_qubits}\n"
802
+ f"2Q gate density: {mrb_2q_density}, ensemble {mrb_2q_ensemble}\n"
803
+ f"backend: {backend_name} --- {timestamp}"
804
+ )
805
+ if identifier == "mrb":
806
+ ax.set_ylabel("Polarization")
807
+ ax.set_xlabel("Layer Depth")
808
+ else:
809
+ ax.set_ylabel("Fidelity")
810
+ ax.set_xlabel("Sequence Length")
811
+ if logscale:
812
+ ax.set_xscale("log")
813
+
814
+ # Show the relevant depths!
815
+ # Gather and flatten all the possible depths and then pick unique elements
816
+ all_depths = [x for d in depths.values() for w in d.values() for x in w]
817
+
818
+ xticks = sorted(list(set(all_depths)))
819
+ ax.set_xticks(xticks, labels=xticks)
820
+
821
+ plt.legend(fontsize=8)
822
+ ax.grid()
823
+
824
+ plt.gcf().set_dpi(250)
825
+
826
+ plt.close()
827
+
828
+ return fig_name, fig
829
+
830
+
831
+ def validate_rb_qubits(qubits_array: List[List[int]], backend_arg: str | IQMBackendBase):
832
+ """Validate qubit inputs for a Clifford RB experiment
833
+ Args:
834
+ qubits_array (List[List[int]]): the array of qubits
835
+ backend_arg (IQMBackendBase): the IQM backend
836
+ Raises:
837
+ ValueError if specified pairs of qubits are not connected
838
+ """
839
+ if isinstance(backend_arg, str):
840
+ backend = get_iqm_backend(backend_arg)
841
+ else:
842
+ backend = backend_arg
843
+ # Identify total amount of qubits
844
+ qubit_counts = [len(x) for x in qubits_array]
845
+
846
+ # Validations
847
+ # Suggest MRB for n>2 qubits
848
+ if any(n > 2 for n in qubit_counts):
849
+ raise ValueError("Please specify qubit layouts with only n=1 or n=2 qubits. Run MRB for n>2 instead.")
850
+ pairs_qubits = [qubits_array[i] for i, n in enumerate(qubit_counts) if n == 2]
851
+ # Qubits should be connected
852
+ if any(tuple(x) not in backend.coupling_map for x in pairs_qubits):
853
+ raise ValueError("Some specified pairs of qubits are not connected")
854
+
855
+
856
+ def validate_irb_gate(
857
+ gate_id: str,
858
+ backend_arg: str | IQMBackendBase,
859
+ gate_params: Optional[List[float]] = None,
860
+ ) -> QuantumCircuit:
861
+ """Validate that an input gate is Clifford and transpiled to IQM's native basis
862
+
863
+ Args:
864
+ gate_id (str): the gate identifier as a Qiskit circuit library operator
865
+ backend_arg (IQMBackendBase): the IQM backend to verify transpilation
866
+ gate_params (Optional[List[float]]): the gate parameters
867
+ Returns:
868
+ Transpiled circuit
869
+
870
+ """
871
+ if gate_params is not None:
872
+ gate = getattr(import_module("qiskit.circuit.library"), gate_id)(*gate_params)
873
+ else:
874
+ gate = getattr(import_module("qiskit.circuit.library"), gate_id)()
875
+
876
+ if isinstance(backend_arg, str):
877
+ backend = get_iqm_backend(backend_arg)
878
+ else:
879
+ backend = backend_arg
880
+
881
+ qc = QuantumCircuit(gate.num_qubits)
882
+ if gate in backend.operation_names:
883
+ qc.compose(gate, qubits=range(gate.num_qubits), inplace=True)
884
+ return qc
885
+
886
+ # else transpile
887
+ qc.compose(gate, qubits=range(gate.num_qubits), inplace=True)
888
+ # Use Optimize SQG but retain final r gate - want the unitary to be preserved!
889
+ qc_t = optimize_single_qubit_gates(
890
+ transpile(qc, basis_gates=backend.operation_names, optimization_level=3), drop_final_rz=False
891
+ )
892
+ return qc_t