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,810 @@
1
+ """
2
+ Mirror Randomized Benchmarking.
3
+ """
4
+
5
+ from copy import deepcopy
6
+ import random
7
+ from time import strftime
8
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, cast
9
+ import warnings
10
+
11
+ import numpy as np
12
+ from qiskit import QuantumCircuit, transpile
13
+ from qiskit.quantum_info import random_clifford, random_pauli
14
+ from qiskit_aer import Aer
15
+ from scipy.spatial.distance import hamming
16
+ import xarray as xr
17
+
18
+ from iqm.benchmarks import AnalysisResult, Benchmark, RunResult
19
+ from iqm.benchmarks.benchmark import BenchmarkConfigurationBase
20
+ from iqm.benchmarks.benchmark_definition import add_counts_to_dataset
21
+ from iqm.benchmarks.logging_config import qcvv_logger
22
+ from iqm.benchmarks.randomized_benchmarking.randomized_benchmarking_common import (
23
+ exponential_rb,
24
+ fit_decay_lmfit,
25
+ lmfit_minimizer,
26
+ plot_rb_decay,
27
+ validate_irb_gate,
28
+ )
29
+ from iqm.benchmarks.utils import (
30
+ get_iqm_backend,
31
+ retrieve_all_counts,
32
+ retrieve_all_job_metadata,
33
+ submit_execute,
34
+ timeit,
35
+ xrvariable_to_counts,
36
+ )
37
+ from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
38
+
39
+
40
+ def compute_polarizations(
41
+ num_qubits: int,
42
+ noisy_counts: List[Dict[str, int]],
43
+ ideal_counts: List[Dict[str, int]],
44
+ num_circ_samples: int,
45
+ num_pauli_samples: int,
46
+ ) -> List[float]:
47
+ """Estimates the polarization for a list of noisy counts with respect to corresponding ideal counts
48
+ The polarization here is a rescaling of the average fidelity, that corrects for few-qubit effects
49
+
50
+ Arguments:
51
+ num_qubits (int): the number of qubits being benchmarked
52
+ noisy_counts (List[Dict[str, int]]): the list of counts coming from real execution
53
+ ideal_counts (List[Dict[str, int]]): the list of counts coming from simulated, ideal execution
54
+ num_circ_samples (int): the number circuit of samples used to estimate the polarization
55
+ num_pauli_samples (int): the number of pauli samples per circuit sample used to estimate the polarization
56
+ Returns:
57
+ List[float]: the polarizations for each circuit sample of the given sequence length
58
+ """
59
+ ideal_counts_matrix = list_to_numcircuit_times_numpauli_matrix(ideal_counts, num_circ_samples, num_pauli_samples)
60
+ noisy_counts_matrix = list_to_numcircuit_times_numpauli_matrix(noisy_counts, num_circ_samples, num_pauli_samples)
61
+
62
+ polarizations: List = []
63
+ for ideal_m, noisy_m in zip(ideal_counts_matrix, noisy_counts_matrix):
64
+ polarization_pauli = []
65
+ for ideal_pauli, noisy_pauli in zip(ideal_m, noisy_m):
66
+ ideal_bitstrings = list(ideal_pauli.keys())
67
+ noisy_bitstrings = list(noisy_pauli.keys())
68
+
69
+ # Get the counts for each Hamming distance
70
+ hamming_distances = {i: 0 for i in range(num_qubits + 1)}
71
+ for s_n in noisy_bitstrings:
72
+ for s_i in ideal_bitstrings:
73
+ k_index = int(hamming(list(s_n), list(s_i)) * num_qubits)
74
+ hamming_distances[k_index] += noisy_pauli[s_n]
75
+
76
+ # Compute polarizations
77
+ no_shots = sum(noisy_pauli.values())
78
+ weighted_hamming = sum(((-1 / 2) ** k) * (hamming_distances[k] / no_shots) for k in range(num_qubits + 1))
79
+ polarization_c = ((4**num_qubits) * weighted_hamming - 1) / (4**num_qubits - 1)
80
+ polarization_pauli.append(polarization_c)
81
+
82
+ # Average over the Pauli samples and add the circuit sample
83
+ polarizations.append(np.mean(polarization_pauli))
84
+
85
+ return polarizations
86
+
87
+
88
+ # TODO: Let edge_grab also admit a 1Q gate ensemble! Currently uniform Clifford by default # pylint: disable=fixme
89
+ # pylint: disable=too-many-branches
90
+ def edge_grab(
91
+ qubit_set: List[int],
92
+ n_layers: int,
93
+ backend_arg: IQMBackendBase | str,
94
+ density_2q_gates: float = 0.25,
95
+ two_qubit_gate_ensemble: Optional[Dict[str, float]] = None,
96
+ ) -> List[QuantumCircuit]:
97
+ """Generate a list of random layers containing single-qubit Cliffords and two-qubit gates,
98
+ sampled according to the edge-grab algorithm (see arXiv:2204.07568 [quant-ph]).
99
+
100
+ Args:
101
+ qubit_set (List[int]): The set of qubits of the backend.
102
+ n_layers (int): The number of layers.
103
+ backend_arg (IQMBackendBase | str): IQM backend.
104
+ density_2q_gates (float): The expected density of 2Q gates in a circuit formed by subsequent application of layers
105
+ two_qubit_gate_ensemble (Dict[str, float]): A dictionary with keys being str specifying 2Q gates, and values being corresponding probabilities
106
+ Raises:
107
+ ValueError: if the probabilities in the gate ensembles do not add up to unity.
108
+ Returns:
109
+ List[QuantumCircuit]: the list of gate layers, in the form of quantum circuits.
110
+ """
111
+ # Check the ensemble of 2Q gates, otherwise assign
112
+ if two_qubit_gate_ensemble is None:
113
+ two_qubit_gate_ensemble = cast(Dict[str, float], {"CZGate": 1.0})
114
+ elif sum(two_qubit_gate_ensemble.values()) != 1.0:
115
+ raise ValueError("The 2Q gate ensemble probabilities must sum to 1.0")
116
+
117
+ # Validate 2Q gates and get circuits
118
+ two_qubit_circuits = {}
119
+ for k in two_qubit_gate_ensemble.keys():
120
+ two_qubit_circuits[k] = validate_irb_gate(k, backend_arg, gate_params=None)
121
+ # TODO: Admit parametrized 2Q gates! # pylint: disable=fixme
122
+
123
+ # Check backend and retrieve if necessary
124
+ if isinstance(backend_arg, str):
125
+ backend = get_iqm_backend(backend_arg)
126
+ else:
127
+ backend = backend_arg
128
+
129
+ # Definitions
130
+ num_qubits = len(qubit_set)
131
+ physical_to_virtual_map = {q: i for i, q in enumerate(qubit_set)}
132
+
133
+ # Get the possible edges where to place 2Q gates given the backend connectivity
134
+ twoq_edges = []
135
+ for i, q0 in enumerate(qubit_set):
136
+ for q1 in qubit_set[i + 1 :]:
137
+ if (q0, q1) in list(backend.coupling_map):
138
+ twoq_edges.append([q0, q1])
139
+ twoq_edges = list(sorted(twoq_edges))
140
+
141
+ # Generate the layers
142
+ layer_list = []
143
+ for _ in range(n_layers):
144
+ # Pick edges at random and store them in a new list "edge_list"
145
+ aux = deepcopy(twoq_edges)
146
+ edge_list = []
147
+ layer = QuantumCircuit(num_qubits)
148
+ # Take (and remove) edges from "aux", then add to "edge_list"
149
+ edge_qubits = []
150
+ while aux:
151
+ new_edge = random.choice(aux)
152
+ edge_list.append(new_edge)
153
+ edge_qubits = list(np.array(edge_list).flatten())
154
+ # Removes all edges which include either of the qubits in new_edge
155
+ aux = [e for e in aux if ((new_edge[0] not in e) and (new_edge[1] not in e))]
156
+
157
+ # Define the probability for adding 2Q gates, given the input density
158
+ if len(edge_list) != 0:
159
+ prob_2qgate = num_qubits * density_2q_gates / len(edge_list)
160
+ else:
161
+ prob_2qgate = 0
162
+
163
+ # Add gates in selected edges
164
+ for e in edge_list:
165
+ # Sample the 2Q gate
166
+ two_qubit_gate = random.choices(
167
+ list(two_qubit_gate_ensemble.keys()),
168
+ weights=list(two_qubit_gate_ensemble.values()),
169
+ k=1,
170
+ )[0]
171
+
172
+ # Pick whether to place the sampled 2Q gate according to the probability above
173
+ is_gate_placed = random.choices(
174
+ [True, False],
175
+ weights=[prob_2qgate, 1 - prob_2qgate],
176
+ k=1,
177
+ )[0]
178
+
179
+ if is_gate_placed:
180
+ if two_qubit_gate == "clifford":
181
+ layer.compose(
182
+ random_clifford(2).to_instruction(),
183
+ qubits=[
184
+ physical_to_virtual_map[e[0]],
185
+ physical_to_virtual_map[e[1]],
186
+ ],
187
+ inplace=True,
188
+ )
189
+ else:
190
+ layer.append(
191
+ two_qubit_circuits[two_qubit_gate],
192
+ [
193
+ physical_to_virtual_map[e[0]],
194
+ physical_to_virtual_map[e[1]],
195
+ ],
196
+ )
197
+ else:
198
+ layer.compose(
199
+ random_clifford(1).to_instruction(),
200
+ qubits=[physical_to_virtual_map[e[0]]],
201
+ inplace=True,
202
+ )
203
+ layer.compose(
204
+ random_clifford(1).to_instruction(),
205
+ qubits=[physical_to_virtual_map[e[1]]],
206
+ inplace=True,
207
+ )
208
+
209
+ # Add 1Q gates in remaining qubits
210
+ remaining_qubits = [q for q in qubit_set if q not in edge_qubits]
211
+ while remaining_qubits:
212
+ for q in remaining_qubits:
213
+ layer.compose(
214
+ random_clifford(1).to_instruction(),
215
+ qubits=[physical_to_virtual_map[q]],
216
+ inplace=True,
217
+ )
218
+ remaining_qubits.remove(q)
219
+
220
+ layer_list.append(layer)
221
+
222
+ return layer_list
223
+
224
+
225
+ def generate_pauli_dressed_mrb_circuits(
226
+ qubits: List[int],
227
+ pauli_samples_per_circ: int,
228
+ depth: int,
229
+ backend_arg: IQMBackendBase | str,
230
+ density_2q_gates: float = 0.25,
231
+ two_qubit_gate_ensemble: Optional[Dict[str, float]] = None,
232
+ qiskit_optim_level: int = 1,
233
+ routing_method: str = "basic",
234
+ ) -> Dict[str, List[QuantumCircuit]]:
235
+ """Samples a mirror circuit and generates samples of "Pauli-dressed" circuits,
236
+ where for each circuit, random Pauli layers are interleaved between each layer of the circuit
237
+
238
+ Args:
239
+ qubits (List[int]): the qubits of the backend
240
+ pauli_samples_per_circ (int): the number of pauli samples per circuit
241
+ depth (int): the depth (number of canonical layers) of the circuit
242
+ backend_arg (IQMBackendBase | str): the backend
243
+ density_2q_gates (float): the expected density of 2Q gates
244
+ two_qubit_gate_ensemble (Optional[Dict[str, float]]):
245
+ qiskit_optim_level (int):
246
+ routing_method (str):
247
+ Returns:
248
+
249
+ """
250
+ num_qubits = len(qubits)
251
+
252
+ # Sample the layers using edge grab sampler - different samplers may be conditionally chosen here in the future
253
+ cycle_layers = edge_grab(qubits, depth, backend_arg, density_2q_gates, two_qubit_gate_ensemble)
254
+
255
+ # Sample the edge (initial/final) random Single-qubit Clifford layer
256
+ clifford_layer = [random_clifford(1) for _ in range(num_qubits)]
257
+
258
+ # Initialize the list of circuits
259
+ all_circuits = {}
260
+ pauli_dressed_circuits_untranspiled: List[QuantumCircuit] = []
261
+ pauli_dressed_circuits_transpiled: List[QuantumCircuit] = []
262
+
263
+ for _ in range(pauli_samples_per_circ):
264
+ # Initialize the quantum circuit object
265
+ circ = QuantumCircuit(num_qubits)
266
+ # Sample all the random Paulis
267
+ paulis = [random_pauli(num_qubits) for _ in range(depth + 1)]
268
+
269
+ # Add the edge product of Cliffords
270
+ for i in range(num_qubits):
271
+ circ.compose(clifford_layer[i].to_instruction(), qubits=[i], inplace=True)
272
+ circ.barrier()
273
+
274
+ # Add the cycle layers
275
+ for k in range(depth):
276
+ circ.compose(
277
+ paulis[k].to_instruction(),
278
+ qubits=list(range(num_qubits)),
279
+ inplace=True,
280
+ )
281
+ circ.barrier()
282
+ circ.compose(cycle_layers[k], inplace=True)
283
+ circ.barrier()
284
+
285
+ # Apply middle Pauli
286
+ circ.compose(
287
+ paulis[depth].to_instruction(),
288
+ qubits=list(range(num_qubits)),
289
+ inplace=True,
290
+ )
291
+ circ.barrier()
292
+
293
+ # Add the mirror layers
294
+ for k in range(depth):
295
+ circ.compose(cycle_layers[depth - k - 1].inverse(), inplace=True)
296
+ circ.barrier()
297
+ circ.compose(
298
+ paulis[depth - k - 1].to_instruction(),
299
+ qubits=list(range(num_qubits)),
300
+ inplace=True,
301
+ )
302
+ circ.barrier()
303
+
304
+ # Add the inverse edge product of Cliffords
305
+ for i in range(num_qubits):
306
+ circ.compose(clifford_layer[i].to_instruction().inverse(), qubits=[i], inplace=True)
307
+
308
+ # Add measurements
309
+ circ.measure_all()
310
+
311
+ # Transpile to backend - no optimize SQG should be used!
312
+ if isinstance(backend_arg, str):
313
+ retrieved_backend = get_iqm_backend(backend_arg)
314
+ else:
315
+ assert isinstance(backend_arg, IQMBackendBase)
316
+ retrieved_backend = backend_arg
317
+
318
+ circ_transpiled = transpile(
319
+ circ,
320
+ backend=retrieved_backend,
321
+ initial_layout=qubits,
322
+ optimization_level=qiskit_optim_level,
323
+ routing_method=routing_method,
324
+ )
325
+
326
+ pauli_dressed_circuits_untranspiled.append(circ)
327
+ pauli_dressed_circuits_transpiled.append(circ_transpiled)
328
+
329
+ # Store the circuit
330
+ all_circuits.update(
331
+ {
332
+ "untranspiled": pauli_dressed_circuits_untranspiled,
333
+ "transpiled": pauli_dressed_circuits_transpiled,
334
+ }
335
+ )
336
+
337
+ return all_circuits
338
+
339
+
340
+ @timeit
341
+ def generate_fixed_depth_mrb_circuits(
342
+ qubits: List[int],
343
+ circ_samples: int,
344
+ pauli_samples_per_circ: int,
345
+ depth: int,
346
+ backend_arg: IQMBackendBase | str,
347
+ density_2q_gates: float = 0.25,
348
+ two_qubit_gate_ensemble: Optional[Dict[str, float]] = None,
349
+ qiskit_optim_level: int = 1,
350
+ routing_method: str = "basic",
351
+ ) -> Dict[int, Dict[str, List[QuantumCircuit]]]:
352
+ """Generates a dictionary MRB circuits at fixed depth, indexed by sample number
353
+
354
+ Args:
355
+ qubits (List[int]): A list of integers specifying physical qubit labels
356
+ circ_samples (int): The number of sets of Pauli-dressed circuit samples
357
+ pauli_samples_per_circ (int): the number of pauli samples per circuit
358
+ depth (int): the depth (number of canonical layers) of the circuits
359
+ backend_arg (IQMBackendBase | str): the backend
360
+ density_2q_gates (float):
361
+ two_qubit_gate_ensemble (Optional[Dict[str, float]]):
362
+ qiskit_optim_level (int):
363
+ routing_method (str):
364
+ Returns:
365
+ A dictionary of lists of Pauli-dressed quantum circuits corresponding to the circuit sample index
366
+ """
367
+
368
+ circuits = {} # The dict with a Dict of random mirror circuits
369
+ for p_sample in range(circ_samples):
370
+ circuits[p_sample] = generate_pauli_dressed_mrb_circuits(
371
+ qubits,
372
+ pauli_samples_per_circ,
373
+ depth,
374
+ backend_arg,
375
+ density_2q_gates,
376
+ two_qubit_gate_ensemble,
377
+ qiskit_optim_level,
378
+ routing_method,
379
+ )
380
+
381
+ return circuits
382
+
383
+
384
+ def list_to_numcircuit_times_numpauli_matrix(
385
+ input_list: List[Any], num_circ_samples: int, num_pauli_samples: int
386
+ ) -> List[List[Any]]:
387
+ """Convert a flat list to a matrix of shape (num_circ_samples, num_pauli_samples).
388
+ Args:
389
+ input_list (List[Any]): the input flat list
390
+ num_circ_samples (int): the number of sets of Pauli-dressed circuit samples
391
+ num_pauli_samples (int): the number of Pauli samples per circuit
392
+ Raises:
393
+ ValueError: Length of passed list is not (num_circ_samples * num_pauli_samples).
394
+ Returns:
395
+ List[List[Any]]: the matrix
396
+ """
397
+ if len(input_list) != num_circ_samples * num_pauli_samples:
398
+ raise ValueError(
399
+ f"Length of passed list {len(input_list)} is not"
400
+ f" (num_circ_samples * num_pauli_samples) = {num_circ_samples * num_pauli_samples}"
401
+ )
402
+
403
+ return np.reshape(input_list, (num_circ_samples, num_pauli_samples)).tolist()
404
+
405
+
406
+ # pylint: disable=too-many-statements
407
+ def mrb_analysis(run: RunResult) -> AnalysisResult:
408
+ """Analysis function for a MRB experiment
409
+
410
+ Args:
411
+ run (RunResult): A MRB experiment run for which analysis result is created
412
+ Returns:
413
+ AnalysisResult corresponding to MRB
414
+ """
415
+ plots = {}
416
+ observations = {}
417
+ dataset = run.dataset
418
+
419
+ shots = dataset.attrs["shots"]
420
+ num_circuit_samples = dataset.attrs["num_circuit_samples"]
421
+ num_pauli_samples = dataset.attrs["num_pauli_samples"]
422
+
423
+ density_2q_gates = dataset.attrs["density_2q_gates"]
424
+ two_qubit_gate_ensemble = dataset.attrs["two_qubit_gate_ensemble"]
425
+
426
+ max_gates_per_batch = dataset.attrs["max_gates_per_batch"]
427
+
428
+ # Analyze the results for each qubit layout of the experiment dataset
429
+ qubits_array = dataset.attrs["qubits_array"]
430
+ depths_array = dataset.attrs["depths_array"]
431
+
432
+ assigned_mrb_depths = {}
433
+ if len(qubits_array) != len(depths_array):
434
+ # If user did not specify a list of depth for each list of qubits, assign the first
435
+ # If the len is not one, the input was incorrect
436
+ if len(depths_array) != 1:
437
+ qcvv_logger.info(
438
+ f"The amount of qubit layouts ({len(qubits_array)}) is not the same "
439
+ f"as the amount of depth configurations ({len(depths_array)}):\n\tWill assign to all the first "
440
+ f"configuration: {depths_array[0]} !"
441
+ )
442
+ assigned_mrb_depths = {str(q): [2 * m for m in depths_array[0]] for q in qubits_array}
443
+ else:
444
+ assigned_mrb_depths = {str(qubits_array[i]): [2 * m for m in depths_array[i]] for i in range(len(depths_array))}
445
+
446
+ transpiled_circuits = dataset.attrs["transpiled_circuits"]
447
+ simulator = Aer.get_backend("qasm_simulator")
448
+
449
+ all_noisy_counts: Dict[str, Dict[int, List[Dict[str, int]]]] = {}
450
+ all_noiseless_counts: Dict[str, Dict[int, List[Dict[str, int]]]] = {}
451
+ time_retrieve_noiseless: Dict[str, Dict[int, float]] = {}
452
+ # Need to loop over each set of qubits, and within, over each depth
453
+ for qubits_idx, qubits in enumerate(qubits_array):
454
+ polarizations = {}
455
+ num_qubits = len(qubits)
456
+ all_noisy_counts[str(qubits)] = {}
457
+ all_noiseless_counts[str(qubits)] = {}
458
+ time_retrieve_noiseless[str(qubits)] = {}
459
+ qcvv_logger.info(f"Post-processing MRB for qubits {qubits}")
460
+ for depth in assigned_mrb_depths[str(qubits)]:
461
+ # Retrieve counts
462
+ identifier = f"qubits_{str(qubits)}_depth_{str(depth)}"
463
+ all_noisy_counts[str(qubits)][depth] = xrvariable_to_counts(
464
+ dataset, identifier, num_circuit_samples * num_pauli_samples
465
+ )
466
+
467
+ qcvv_logger.info(f"Depth {depth}")
468
+ # Execute the quantum circuits on the simulated, ideal backend
469
+ # pylint: disable=unbalanced-tuple-unpacking
470
+ all_noiseless_jobs, _ = submit_execute(
471
+ {tuple(qubits): transpiled_circuits[str(qubits)][depth]},
472
+ simulator,
473
+ shots,
474
+ calset_id=None,
475
+ max_gates_per_batch=max_gates_per_batch,
476
+ )
477
+
478
+ # Retrieve counts
479
+ all_noiseless_counts[str(qubits)][depth], time_retrieve_noiseless[str(qubits)][depth] = retrieve_all_counts(
480
+ all_noiseless_jobs
481
+ )
482
+
483
+ # Compute polarizations for the current depth
484
+ polarizations[depth] = compute_polarizations(
485
+ num_qubits,
486
+ all_noisy_counts[str(qubits)][depth],
487
+ all_noiseless_counts[str(qubits)][depth],
488
+ num_circuit_samples,
489
+ num_pauli_samples,
490
+ )
491
+
492
+ # Fit decay and extract parameters
493
+ list_of_polarizations = list(polarizations.values())
494
+ fit_data, fit_parameters = fit_decay_lmfit(exponential_rb, qubits, list_of_polarizations, "mrb")
495
+ rb_fit_results = lmfit_minimizer(fit_parameters, fit_data, assigned_mrb_depths[str(qubits)], exponential_rb)
496
+
497
+ average_polarizations = {d: np.mean(polarizations[d]) for d in assigned_mrb_depths[str(qubits)]}
498
+ stddevs_from_mean = {
499
+ d: np.std(polarizations[d]) / np.sqrt(num_circuit_samples * num_pauli_samples)
500
+ for d in assigned_mrb_depths[str(qubits)]
501
+ }
502
+ popt = {
503
+ "amplitude": rb_fit_results.params["amplitude_1"],
504
+ "offset": rb_fit_results.params["offset_1"],
505
+ "decay_rate": rb_fit_results.params["p_mrb"],
506
+ }
507
+ fidelity = rb_fit_results.params["fidelity_mrb"]
508
+
509
+ processed_results = {
510
+ "avg_gate_fidelity": {"value": fidelity.value, "uncertainty": fidelity.stderr},
511
+ "decay_rate": {"value": popt["decay_rate"].value, "uncertainty": popt["decay_rate"].stderr},
512
+ "fit_amplitude": {"value": popt["amplitude"].value, "uncertainty": popt["amplitude"].stderr},
513
+ "fit_offset": {"value": popt["offset"].value, "uncertainty": popt["offset"].stderr},
514
+ }
515
+
516
+ dataset.attrs[qubits_idx].update(
517
+ {
518
+ "polarizations": polarizations,
519
+ "avg_polarization_nominal_values": average_polarizations,
520
+ "avg_polatization_stderr": stddevs_from_mean,
521
+ "fitting_method": str(rb_fit_results.method),
522
+ "num_function_evals": int(rb_fit_results.nfev),
523
+ "data_points": int(rb_fit_results.ndata),
524
+ "num_variables": int(rb_fit_results.nvarys),
525
+ "chi_square": float(rb_fit_results.chisqr),
526
+ "reduced_chi_square": float(rb_fit_results.redchi),
527
+ "Akaike_info_crit": float(rb_fit_results.aic),
528
+ "Bayesian_info_crit": float(rb_fit_results.bic),
529
+ }
530
+ )
531
+
532
+ # Update observations
533
+ observations.update({qubits_idx: processed_results})
534
+
535
+ # Generate plots
536
+ fig_name, fig = plot_rb_decay(
537
+ "mrb",
538
+ [qubits],
539
+ dataset,
540
+ observations,
541
+ mrb_2q_density=density_2q_gates,
542
+ mrb_2q_ensemble=two_qubit_gate_ensemble,
543
+ )
544
+ plots[fig_name] = fig
545
+
546
+ # Generate the combined plot
547
+ fig_name, fig = plot_rb_decay(
548
+ "mrb",
549
+ qubits_array,
550
+ dataset,
551
+ observations,
552
+ mrb_2q_density=density_2q_gates,
553
+ mrb_2q_ensemble=two_qubit_gate_ensemble,
554
+ )
555
+ plots[fig_name] = fig
556
+
557
+ return AnalysisResult(dataset=dataset, plots=plots, observations=observations)
558
+
559
+
560
+ class MirrorRandomizedBenchmarking(Benchmark):
561
+ """
562
+ Mirror RB estimates the fidelity of ensembles of n-qubit layers
563
+ """
564
+
565
+ analysis_function = staticmethod(mrb_analysis)
566
+
567
+ name: str = "mrb"
568
+
569
+ def __init__(self, backend_arg: IQMBackendBase | str, configuration: "MirrorRBConfiguration"):
570
+ """Construct the MirrorRandomizedBenchmarking class
571
+
572
+ Args:
573
+ backend_arg (IQMBackendBase | str): _description_
574
+ configuration (MirrorRBConfiguration): _description_
575
+ """
576
+ super().__init__(backend_arg, configuration)
577
+
578
+ # EXPERIMENT
579
+ self.backend_configuration_name = backend_arg if isinstance(backend_arg, str) else backend_arg.name
580
+
581
+ self.qubits_array = configuration.qubits_array
582
+ self.depths_array = configuration.depths_array
583
+ self.num_circuit_samples = configuration.num_circuit_samples
584
+ self.num_pauli_samples = configuration.num_pauli_samples
585
+ self.two_qubit_gate_ensemble = configuration.two_qubit_gate_ensemble
586
+ self.density_2q_gates = configuration.density_2q_gates
587
+
588
+ self.qiskit_optim_level = configuration.qiskit_optim_level
589
+
590
+ self.simulator = Aer.get_backend("qasm_simulator")
591
+
592
+ self.session_timestamp = strftime("%Y%m%d-%H%M%S")
593
+ self.execution_timestamp = ""
594
+
595
+ def add_all_meta_to_dataset(self, dataset: xr.Dataset):
596
+ """Adds all configuration metadata and circuits to the dataset variable
597
+
598
+ Args:
599
+ dataset (xr.Dataset): The xarray dataset
600
+ """
601
+ dataset.attrs["session_timestamp"] = self.session_timestamp
602
+ dataset.attrs["execution_timestamp"] = self.execution_timestamp
603
+ dataset.attrs["backend_configuration_name"] = self.backend_configuration_name
604
+ dataset.attrs["backend_name"] = self.backend.name
605
+
606
+ for key, value in self.configuration:
607
+ if key == "benchmark": # Avoid saving the class object
608
+ dataset.attrs[key] = value.name
609
+ else:
610
+ dataset.attrs[key] = value
611
+ # Defined outside configuration - if any
612
+
613
+ @timeit
614
+ def add_all_circuits_to_dataset(self, dataset: xr.Dataset):
615
+ """Adds all generated circuits during execution to the dataset variable
616
+ Args:
617
+ dataset (xr.Dataset): The xarray dataset
618
+ """
619
+ qcvv_logger.info(f"Adding all circuits to the dataset")
620
+ dataset.attrs["untranspiled_circuits"] = self.untranspiled_circuits
621
+ dataset.attrs["transpiled_circuits"] = self.transpiled_circuits
622
+
623
+ def submit_single_mrb_job(
624
+ self,
625
+ backend_arg: IQMBackendBase,
626
+ qubits: Sequence[int],
627
+ depth: int,
628
+ sorted_transpiled_circuit_dicts: Dict[Tuple[int, ...], List[QuantumCircuit]],
629
+ ) -> Dict[str, Any]:
630
+ """
631
+ Submit fixed-depth MRB jobs for execution in the specified IQMBackend
632
+ Args:
633
+ backend_arg (IQMBackendBase): the IQM backend to submit the job
634
+ qubits (Sequence[int]): the qubits to identify the submitted job
635
+ depth (int): the depth (number of canonical layers) of the circuits to identify the submitted job
636
+ sorted_transpiled_circuit_dicts (Dict[str, List[QuantumCircuit]]): A dictionary containing all MRB circuits
637
+ Returns:
638
+ Dict with qubit layout, submitted job objects, type (vanilla/DD) and submission time
639
+ """
640
+ # Submit
641
+ # Send to execute on backend
642
+ execution_jobs, time_submit = submit_execute(
643
+ sorted_transpiled_circuit_dicts,
644
+ backend_arg,
645
+ self.shots,
646
+ self.calset_id,
647
+ max_gates_per_batch=self.max_gates_per_batch,
648
+ )
649
+ mrb_submit_results = {
650
+ "qubits": qubits,
651
+ "depth": depth,
652
+ "jobs": execution_jobs,
653
+ "time_submit": time_submit,
654
+ }
655
+ return mrb_submit_results
656
+
657
+ def execute(self, backend: IQMBackendBase) -> xr.Dataset:
658
+ """Executes the benchmark"""
659
+
660
+ self.execution_timestamp = strftime("%Y%m%d-%H%M%S")
661
+
662
+ dataset = xr.Dataset()
663
+ self.add_all_meta_to_dataset(dataset)
664
+
665
+ # Submit jobs for all qubit layouts
666
+ all_mrb_jobs: List[Dict[str, Any]] = []
667
+ time_circuit_generation: Dict[str, float] = {}
668
+
669
+ # Initialize the variable to contain the circuits for each layout
670
+ self.untranspiled_circuits: Dict[str, Dict[int | str, List[QuantumCircuit]]] = {}
671
+ self.transpiled_circuits: Dict[str, Dict[int | str, List[QuantumCircuit]]] = {}
672
+
673
+ # The depths should be assigned to each set of qubits!
674
+ # The real final MRB depths are twice the originally specified, must be taken into account here!
675
+ assigned_mrb_depths = {}
676
+ if len(self.qubits_array) != len(self.depths_array):
677
+ # If user did not specify a list of depth for each list of qubits, assign the first
678
+ # If the len is not one, the input was incorrect
679
+ if len(self.depths_array) != 1:
680
+ warnings.warn(
681
+ f"The amount of qubit layouts ({len(self.qubits_array)}) is not the same "
682
+ f"as the amount of depth configurations ({len(self.depths_array)}):\n\tWill assign to all the first "
683
+ f"configuration: {self.depths_array[0]} !"
684
+ )
685
+ assigned_mrb_depths = {str(q): [2 * m for m in self.depths_array[0]] for q in self.qubits_array}
686
+ else:
687
+ assigned_mrb_depths = {
688
+ str(self.qubits_array[i]): [2 * m for m in self.depths_array[i]] for i in range(len(self.depths_array))
689
+ }
690
+
691
+ # Auxiliary dict from str(qubits) to indices
692
+ qubit_idx: Dict[str, Any] = {}
693
+ for qubits_idx, qubits in enumerate(self.qubits_array):
694
+ qubit_idx[str(qubits)] = qubits_idx
695
+ self.untranspiled_circuits[str(qubits)] = {}
696
+ self.transpiled_circuits[str(qubits)] = {}
697
+
698
+ qcvv_logger.info(
699
+ f"Executing MRB on qubits {qubits}."
700
+ f" Will generate and submit all {self.num_circuit_samples}x{self.num_pauli_samples} MRB circuits"
701
+ f" for each depth {assigned_mrb_depths[str(qubits)]}"
702
+ )
703
+ mrb_circuits = {}
704
+ mrb_transpiled_circuits_lists: Dict[int, List[QuantumCircuit]] = {}
705
+ mrb_untranspiled_circuits_lists: Dict[int, List[QuantumCircuit]] = {}
706
+ time_circuit_generation[str(qubits)] = 0
707
+ for depth in assigned_mrb_depths[str(qubits)]:
708
+ qcvv_logger.info(f"Depth {depth}")
709
+ mrb_circuits[depth], elapsed_time = generate_fixed_depth_mrb_circuits(
710
+ qubits,
711
+ self.num_circuit_samples,
712
+ self.num_pauli_samples,
713
+ int(depth / 2),
714
+ backend,
715
+ self.density_2q_gates,
716
+ self.two_qubit_gate_ensemble,
717
+ self.qiskit_optim_level,
718
+ self.routing_method,
719
+ )
720
+ time_circuit_generation[str(qubits)] += elapsed_time
721
+
722
+ # Generated circuits at fixed depth are (dict) indexed by Pauli sample number, turn into List
723
+ mrb_transpiled_circuits_lists[depth] = []
724
+ mrb_untranspiled_circuits_lists[depth] = []
725
+ for c_s in range(self.num_circuit_samples):
726
+ mrb_transpiled_circuits_lists[depth].extend(mrb_circuits[depth][c_s]["transpiled"])
727
+ for c_s in range(self.num_circuit_samples):
728
+ mrb_untranspiled_circuits_lists[depth].extend(mrb_circuits[depth][c_s]["untranspiled"])
729
+
730
+ # Submit
731
+ sorted_transpiled_qc_list = {tuple(qubits): mrb_transpiled_circuits_lists[depth]}
732
+ all_mrb_jobs.append(self.submit_single_mrb_job(backend, qubits, depth, sorted_transpiled_qc_list))
733
+ qcvv_logger.info(f"Job for layout {qubits} & depth {depth} submitted successfully!")
734
+
735
+ self.untranspiled_circuits[str(qubits)] = {
736
+ d: mrb_untranspiled_circuits_lists[d] for d in assigned_mrb_depths[str(qubits)]
737
+ }
738
+ self.transpiled_circuits[str(qubits)] = {
739
+ d: mrb_transpiled_circuits_lists[d] for d in assigned_mrb_depths[str(qubits)]
740
+ }
741
+
742
+ dataset.attrs[qubits_idx] = {"qubits": qubits}
743
+
744
+ # Retrieve counts of jobs for all qubit layouts
745
+ all_job_metadata = {}
746
+ for job_dict in all_mrb_jobs:
747
+ qubits = job_dict["qubits"]
748
+ depth = job_dict["depth"]
749
+ # Retrieve counts
750
+ execution_results, time_retrieve = retrieve_all_counts(
751
+ job_dict["jobs"], f"qubits_{str(qubits)}_depth_{str(depth)}"
752
+ )
753
+ # Retrieve all job meta data
754
+ all_job_metadata = retrieve_all_job_metadata(job_dict["jobs"])
755
+ # Export all to dataset
756
+ dataset.attrs[qubit_idx[str(qubits)]].update(
757
+ {
758
+ f"depth_{str(depth)}": {
759
+ "time_circuit_generation": time_circuit_generation[str(qubits)],
760
+ "time_submit": job_dict["time_submit"],
761
+ "time_retrieve": time_retrieve,
762
+ "all_job_metadata": all_job_metadata,
763
+ },
764
+ }
765
+ )
766
+
767
+ qcvv_logger.info(f"Adding counts of qubits {qubits} and depth {depth} run to the dataset")
768
+ dataset, _ = add_counts_to_dataset(execution_results, f"qubits_{str(qubits)}_depth_{str(depth)}", dataset)
769
+
770
+ self.add_all_circuits_to_dataset(dataset)
771
+
772
+ qcvv_logger.info(f"MRB experiment execution concluded !")
773
+
774
+ return dataset
775
+
776
+
777
+ class MirrorRBConfiguration(BenchmarkConfigurationBase):
778
+ """Mirror RB configuration.
779
+
780
+ Attributes:
781
+ benchmark (Type[Benchmark]): MirrorRandomizedBenchmarking.
782
+ qubits_array (Sequence[Sequence[int]]): The array of physical qubits in which to execute MRB.
783
+ depths_array (Sequence[Sequence[int]]): The array of physical depths in which to execute MRB for a corresponding qubit list.
784
+ * If len is the same as that of qubits_array, each Sequence[int] corresponds to the depths for the corresponding layout of qubits.
785
+ * If len is different from that of qubits_array, assigns the first Sequence[int].
786
+ num_circuit_samples (int): The number of random-layer mirror circuits to generate.
787
+ num_pauli_samples (int): The number of random Pauli layers to interleave per mirror circuit.
788
+ shots (int): The number of measurement shots to execute per circuit.
789
+ qiskit_optim_level (int): The Qiskit-level of optimization to use in transpilation.
790
+ * Default is 1.
791
+ routing_method (Literal["basic", "lookahead", "stochastic", "sabre", "none"]): The routing method to use in transpilation.
792
+ * Default is "sabre".
793
+ two_qubit_gate_ensemble (Dict[str, float]): The two-qubit gate ensemble to use in the random mirror circuits.
794
+ * Keys correspond to str names of qiskit circuit library gates, e.g., "CZGate" or "CXGate".
795
+ * Values correspond to the probability for the respective gate to be sampled.
796
+ * Default is {"CZGate": 1.0}.
797
+ density_2q_gates (float): The expected density of 2-qubit gates in the final circuits.
798
+ * Default is 0.25.
799
+ """
800
+
801
+ benchmark: Type[Benchmark] = MirrorRandomizedBenchmarking
802
+ qubits_array: Sequence[Sequence[int]]
803
+ depths_array: Sequence[Sequence[int]]
804
+ num_circuit_samples: int
805
+ num_pauli_samples: int
806
+ qiskit_optim_level: int = 1
807
+ two_qubit_gate_ensemble: Dict[str, float] = {
808
+ "CZGate": 1.0,
809
+ }
810
+ density_2q_gates: float = 0.25