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,802 @@
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
+ GHZ state benchmark
17
+ """
18
+
19
+ from io import BytesIO
20
+ from itertools import chain
21
+ import json
22
+ from time import strftime, time
23
+ from typing import Dict, List, Optional, Tuple, Type, cast
24
+
25
+ from networkx import Graph, all_pairs_shortest_path, is_connected, minimum_spanning_tree
26
+ import numpy as np
27
+ import pycurl
28
+ from qiskit import QuantumCircuit, transpile
29
+ from qiskit.quantum_info import random_clifford
30
+ from qiskit_aer import Aer
31
+ from scipy.spatial.distance import hamming
32
+ import xarray as xr
33
+
34
+ from iqm.benchmarks import Benchmark
35
+ from iqm.benchmarks.benchmark import BenchmarkConfigurationBase
36
+ from iqm.benchmarks.benchmark_definition import AnalysisResult, RunResult, add_counts_to_dataset
37
+ from iqm.benchmarks.logging_config import qcvv_logger
38
+ from iqm.benchmarks.readout_mitigation import apply_readout_error_mitigation
39
+ from iqm.benchmarks.utils import (
40
+ perform_backend_transpilation,
41
+ reduce_to_active_qubits,
42
+ set_coupling_map,
43
+ timeit,
44
+ xrvariable_to_counts,
45
+ )
46
+ from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
47
+
48
+
49
+ @timeit
50
+ def append_rms(
51
+ circuit: QuantumCircuit,
52
+ num_rms: int,
53
+ backend: IQMBackendBase,
54
+ # optimize_sqg: bool = False,
55
+ ) -> List[QuantumCircuit]:
56
+ """
57
+ Appends 1Q Clifford gates sampled uniformly at random to all qubits in the given circuit.
58
+ Args:
59
+ circuit (QuantumCircuit):
60
+ num_rms (int):
61
+ backend (Optional[IQMBackendBase]): whether Cliffords are decomposed for the given backend
62
+ Returns:
63
+ List[QuantumCircuit] of the original circuit with 1Q Clifford gates appended to it
64
+ """
65
+ rm_circuits = []
66
+ for _ in range(num_rms):
67
+ rm_circ = circuit.copy()
68
+ # It shouldn't matter if measurement bits get scrambled
69
+ rm_circ.remove_final_measurements()
70
+
71
+ active_qubits = set()
72
+ for instruction in rm_circ.data:
73
+ for qubit in instruction[1]:
74
+ active_qubits.add(qubit.index)
75
+
76
+ for q in active_qubits:
77
+ if backend is not None:
78
+ rand_clifford = random_clifford(1).to_circuit()
79
+ else:
80
+ rand_clifford = random_clifford(1).to_instruction()
81
+ rm_circ.compose(rand_clifford, qubits=[q], inplace=True)
82
+
83
+ rm_circ.measure_active()
84
+ # if backend is not None and backend.name != "IQMNdonisBackend" and optimize_sqg:
85
+ # rm_circuits.append(optimize_single_qubit_gates(rm_circ))
86
+ # else:
87
+ rm_circuits.append(transpile(rm_circ, basis_gates=backend.operation_names))
88
+
89
+ return rm_circuits
90
+
91
+
92
+ def fidelity_ghz_randomized_measurements(
93
+ dataset: xr.Dataset, qubit_layout, ideal_probabilities: List[Dict[str, int]], num_qubits: int
94
+ ) -> Dict[str, float]:
95
+ """
96
+ Estimates GHZ state fidelity through cross-correlations of RMs.
97
+ Implementation of Eq. (34) in https://arxiv.org/abs/1812.02624
98
+
99
+ Arguments:
100
+ dataset (xr.Dataset):
101
+ qubit_layout: List[int]: The subset of system-qubits used in the protocol
102
+ ideal_probabilities (List[Dict[str, int]]):
103
+ num_qubits (int):
104
+ Returns:
105
+ Dict[str, float]
106
+ """
107
+
108
+ # List for each RM contribution to the fidelity
109
+ fid_rm = []
110
+
111
+ # Loop through RMs and add each contribution
112
+ num_rms = len(dataset.attrs["transpiled_circuits"][f"{str(qubit_layout)}"][tuple(qubit_layout)])
113
+ for u in range(num_rms):
114
+ # Probability estimates for noisy measurements
115
+ probabilities_sample = {}
116
+ c_keys = dataset[f"{str(qubit_layout)}_state_{u}"].data # measurements[u].keys()
117
+ num_shots_noisy = sum(dataset[f"{str(qubit_layout)}_counts_{u}"].data)
118
+ for k, key in enumerate(c_keys):
119
+ probabilities_sample[key] = dataset[f"{str(qubit_layout)}_counts_{u}"].data[k] / num_shots_noisy
120
+ # Keys for corresponding ideal probabilities
121
+ c_id_keys = ideal_probabilities[u].keys()
122
+
123
+ p_sum = []
124
+ for sb in c_id_keys:
125
+ for sa in c_keys:
126
+ exponent = hamming(list(sa), list(sb)) * num_qubits
127
+ p_sum.append(np.power(-2, -exponent) * probabilities_sample[sa] * ideal_probabilities[u][sb])
128
+ fid_rm.append((2**num_qubits) * sum(p_sum))
129
+ fidelities = {"mean": np.mean(fid_rm), "std": np.std(fid_rm) / np.sqrt(num_rms)}
130
+
131
+ if dataset.attrs["rem"]:
132
+ fid_rm_rem = []
133
+ for u in range(num_rms):
134
+ # Probability estimates for noisy measurements
135
+ probabilities_sample = {}
136
+ c_keys = dataset[f"{str(qubit_layout)}_rem_state_{u}"].data # measurements[u].keys()
137
+ num_shots_noisy = sum(dataset[f"{str(qubit_layout)}_rem_counts_{u}"].data)
138
+ for k, key in enumerate(c_keys):
139
+ probabilities_sample[key] = dataset[f"{str(qubit_layout)}_rem_counts_{u}"].data[k] / num_shots_noisy
140
+ # Keys for corresponding ideal probabilities
141
+ c_id_keys = ideal_probabilities[u].keys()
142
+
143
+ p_sum = []
144
+ for sb in c_id_keys:
145
+ for sa in c_keys:
146
+ exponent = hamming(list(sa), list(sb)) * num_qubits
147
+ p_sum.append(np.power(-2, -exponent) * probabilities_sample[sa] * ideal_probabilities[u][sb])
148
+ fid_rm_rem.append((2**num_qubits) * sum(p_sum))
149
+ fidelities = fidelities | {"mean_rem": np.mean(fid_rm_rem), "std_rem": np.std(fid_rm_rem) / np.sqrt(num_rms)}
150
+ return fidelities
151
+
152
+
153
+ def fidelity_ghz_coherences(dataset: xr.Dataset, qubit_layout: List[int]) -> List[float]:
154
+ """
155
+ Estimates the GHZ state fidelity based on the multiple quantum coherences method based on [Mooney, 2021]
156
+
157
+ Args:
158
+ dataset: xr.Dataset
159
+ An xarray dataset containing the measurement data
160
+ qubit_layout: List[int]
161
+ The subset of system-qubits used in the protocol
162
+
163
+ Returns:
164
+ List[int]: The ghz fidelity or, if rem=True, fidelity and readout error mitigated fidelity
165
+ """
166
+
167
+ num_qubits = len(qubit_layout)
168
+ phases = [np.pi * i / (num_qubits + 1) for i in range(2 * num_qubits + 2)]
169
+ transpiled_circuits = dataset.attrs["transpiled_circuits"][f"{str(qubit_layout)}"][tuple(qubit_layout)]
170
+ num_shots = dataset.attrs["shots"]
171
+ num_circuits = len(transpiled_circuits)
172
+
173
+ # Computing the phase acquired by the |11...1> component for each interval
174
+ complex_coefficients = np.exp(1j * num_qubits * np.array(phases))
175
+
176
+ # Loading the counts from the dataset
177
+ counts = xrvariable_to_counts(dataset, str(qubit_layout), num_circuits)
178
+ # for u in range(num_circuits):
179
+ # counts.append(
180
+ # dict(
181
+ # zip(
182
+ # list(dataset[f"{str(qubit_layout)}_state_{u}"].data),
183
+ # dataset[f"{str(qubit_layout)}_counts_{u}"].data,
184
+ # )
185
+ # )
186
+ # )
187
+ all_zero_probability_list = [] # An ordered list for storing the probabilities of returning to the |00..0> state
188
+ for count in counts[1:]:
189
+ if "0" * num_qubits in count.keys():
190
+ probability = count["0" * num_qubits] / num_shots
191
+ else:
192
+ probability = 0
193
+ all_zero_probability_list.append(probability)
194
+
195
+ # Extracting coherence parameter i_n using the fourier transform
196
+ i_n = np.abs(np.dot(complex_coefficients, np.array(all_zero_probability_list))) / (len(phases))
197
+
198
+ # Extracting the probabilities of the 00...0 and 11...1 bit strings
199
+ probs_direct = {label: count / num_shots for label, count in counts[0].items()}
200
+
201
+ # Computing GHZ state fidelity from i_n and the probabilities according to the method in [Mooney, 2021]
202
+ p0 = probs_direct["0" * num_qubits] if "0" * num_qubits in probs_direct.keys() else 0
203
+ p1 = probs_direct["1" * num_qubits] if "1" * num_qubits in probs_direct.keys() else 0
204
+ fidelity = (p0 + p1) / 2 + np.sqrt(i_n)
205
+
206
+ # Same procedure for error mitigated data
207
+ if dataset.attrs["rem"]:
208
+ probs_mit = xrvariable_to_counts(dataset, f"{str(qubit_layout)}_rem", num_circuits)
209
+ # for u in range(num_circuits):
210
+ # probs_mit.append(
211
+ # dict(
212
+ # zip(
213
+ # list(dataset[f"{str(qubit_layout)}_rem_state_{u}"].data),
214
+ # dataset[f"{str(qubit_layout)}_rem_counts_{u}"].data,
215
+ # )
216
+ # )
217
+ # )
218
+ all_zero_probability_list_mit = []
219
+ for prob in probs_mit[1:]:
220
+ if "0" * num_qubits in prob.keys():
221
+ probability = prob["0" * num_qubits]
222
+ else:
223
+ probability = 0
224
+ all_zero_probability_list_mit.append(probability)
225
+ i_n_mit = np.abs(np.dot(complex_coefficients, np.array(all_zero_probability_list_mit))) / (len(phases))
226
+ probs_direct_mit = dict(probs_mit[0].items())
227
+ p0_mit = probs_direct_mit["0" * num_qubits] if "0" * num_qubits in probs_direct_mit.keys() else 0
228
+ p1_mit = probs_direct_mit["1" * num_qubits] if "1" * num_qubits in probs_direct_mit.keys() else 0
229
+ fidelity_mit = (p0_mit + p1_mit) / 2 + np.sqrt(i_n_mit)
230
+ return [fidelity, fidelity_mit]
231
+ return [fidelity]
232
+
233
+
234
+ def fidelity_analysis(run: RunResult) -> AnalysisResult:
235
+ """Analyze counts and compute the state fidelity
236
+
237
+ Args:
238
+ run: RunResult
239
+ The RunResult object containing a dataset with counts and benchmark parameters
240
+
241
+ Returns:
242
+ AnalysisResult
243
+ An object containing the dataset, plots, and observations
244
+ """
245
+ observations = {}
246
+ dataset = run.dataset
247
+ routine = dataset.attrs["fidelity_routine"]
248
+ qubit_layouts = dataset.attrs["custom_qubits_array"]
249
+ backend_name = dataset.attrs["backend_name"]
250
+
251
+ for qubit_layout in qubit_layouts:
252
+ if routine == "randomized_measurements":
253
+ ideal_simulator = Aer.get_backend("statevector_simulator")
254
+ for qubit_layout in qubit_layouts:
255
+ ideal_probabilities = []
256
+ all_circuits = run.dataset.attrs["transpiled_circuits"][str(qubit_layout)][tuple(qubit_layout)]
257
+ for qc in all_circuits:
258
+ qc_copy = qc.copy()
259
+ qc_copy.remove_final_measurements()
260
+ deflated_qc = reduce_to_active_qubits(qc_copy, backend_name)
261
+ ideal_probabilities.append(
262
+ dict(sorted(ideal_simulator.run(deflated_qc).result().get_counts().items()))
263
+ )
264
+ result_dict = fidelity_ghz_randomized_measurements(
265
+ dataset, qubit_layout, ideal_probabilities, len(qubit_layout)
266
+ )
267
+ else: # default routine == "coherences":
268
+ fidelity = fidelity_ghz_coherences(dataset, qubit_layout)
269
+ result_dict = {"fidelity": fidelity[0]}
270
+ if len(fidelity) > 1:
271
+ result_dict.update({"fidelity_rem": fidelity[1]})
272
+ observations[str(qubit_layout)] = result_dict
273
+ return AnalysisResult(dataset=dataset, observations=observations)
274
+
275
+
276
+ def generate_ghz_linear(num_qubits: int) -> QuantumCircuit:
277
+ """
278
+ Generates a GHZ state by applying a Hadamard and a series of CX gates in a linear fashion.
279
+ The construction is symmetrized to halve the circuit depth.
280
+ Args:
281
+ num_qubits: the number of qubits of the GHZ state
282
+
283
+ Returns:
284
+ A quantum circuit generating a GHZ state of n qubits
285
+ """
286
+ s = int(num_qubits / 2)
287
+ qc = QuantumCircuit(num_qubits)
288
+ qc.h(s)
289
+
290
+ for m in range(s, 0, -1):
291
+ qc.cx(m, m - 1)
292
+ if num_qubits % 2 == 0 and m == s:
293
+ continue
294
+ qc.cx(num_qubits - m - 1, num_qubits - m)
295
+ qc.measure_all()
296
+ return qc
297
+
298
+
299
+ def generate_ghz_log_cruz(num_qubits: int) -> QuantumCircuit:
300
+ """
301
+ Generates a GHZ state in log-depth according to https://arxiv.org/abs/1807.05572
302
+ Args:
303
+ num_qubits: the number of qubits of the GHZ state
304
+
305
+ Returns:
306
+ A quantum circuit generating a GHZ state of n qubits
307
+ """
308
+ qc = QuantumCircuit(num_qubits)
309
+ qc.h(0)
310
+
311
+ for m in range(num_qubits):
312
+ for k in range(2**m):
313
+ if ((2**m) + k) >= num_qubits:
314
+ break
315
+ qc.cx(k, 2**m + k)
316
+ qc.measure_all()
317
+ return qc
318
+
319
+
320
+ def generate_ghz_log_mooney(num_qubits: int) -> QuantumCircuit:
321
+ """
322
+ Generates a GHZ state in log-depth according to https://arxiv.org/abs/2101.08946
323
+ Args:
324
+ num_qubits: the number of qubits of the GHZ state
325
+
326
+ Returns:
327
+ A quantum circuit generating a GHZ state of n qubits
328
+ """
329
+ qc = QuantumCircuit(num_qubits)
330
+ qc.h(0)
331
+
332
+ aux_n = int(np.ceil(np.log2(num_qubits)))
333
+ for m in range(aux_n, 0, -1):
334
+ for k in range(0, num_qubits, 2**m):
335
+ if k + 2 ** (m - 1) >= num_qubits:
336
+ continue
337
+ qc.cx(k, k + 2 ** (m - 1))
338
+ qc.measure_all()
339
+ return qc
340
+
341
+
342
+ def generate_ghz_spanning_tree(
343
+ graph: Graph,
344
+ qubit_layout: List[int],
345
+ n_state: int | None = None,
346
+ ) -> Tuple[QuantumCircuit, List[int]]:
347
+ """
348
+ Generates a GHZ state in log-depth by computing a minimal spanning tree for a given coupling map
349
+ Args:
350
+ graph: networkx.Graph
351
+ A graph of the backend coupling map
352
+ qubit_layout: List[int]
353
+ The subset of system-qubits used in the protocol, indexed from 0
354
+ n_state: int
355
+ The number of qubits for which a GHZ state should be created. This values should be smaller or equal to
356
+ the number of qubits in qubit_layout
357
+
358
+ Returns:
359
+ qc: QuantumCircuit
360
+ A quantum circuit generating a GHZ state of n qubits
361
+ participating_qubits: List[int]
362
+ The list of qubits on which the GHZ state is defined. This is a subset of qubit_layout with size n_state
363
+ """
364
+
365
+ cx_map = get_cx_map(qubit_layout, graph)
366
+ if n_state is None:
367
+ n_state = len(cx_map) + 1
368
+ participating_qubits = set(qubit for pair in cx_map[: n_state - 1] for qubit in pair)
369
+
370
+ relabeling = {idx_old: idx_new for idx_new, idx_old in enumerate(participating_qubits)}
371
+ qc = QuantumCircuit(n_state, name="ghz")
372
+ qc.h([relabeling[cx_map[0][0]]])
373
+ for _, pair in zip(np.arange(n_state - 1), cx_map):
374
+ relabeled_pair = [relabeling[pair[0]], relabeling[pair[1]]]
375
+ qc.barrier(relabeled_pair)
376
+ qc.cx(*relabeled_pair)
377
+ qc.measure_active()
378
+ return qc, list(participating_qubits)
379
+
380
+
381
+ def extract_fidelities(cal_url: str, qubit_layout: List[int]) -> Tuple[List[List[int]], List[float]]:
382
+ """Returns couplings and CZ-fidelities from calibration data URL
383
+
384
+ Args:
385
+ cal_url: str
386
+ The url under which the calibration data for the backend can be found
387
+ qubit_layout: List[int]
388
+ The subset of system-qubits used in the protocol, indexed from 0
389
+ Returns:
390
+ list_couplings: List[List[int]]
391
+ A list of pairs, each of which is a qubit coupling for which the calibration
392
+ data contains a fidelity.
393
+ list_fids: List[float]
394
+ A list of CZ fidelities from the calibration url, ordered in the same way as list_couplings
395
+ """
396
+
397
+ byteobj = BytesIO() # buffer creation
398
+ curlobj = pycurl.Curl() # pylint: disable=c-extension-no-member
399
+ curlobj.setopt(curlobj.URL, f"{cal_url}") # type: ignore
400
+ curlobj.setopt(curlobj.WRITEDATA, byteobj) # type: ignore
401
+ curlobj.perform() # perform file transfer
402
+ curlobj.close() # end of session
403
+ body = byteobj.getvalue()
404
+ res = json.loads(body.decode())
405
+
406
+ qubit_mapping = {qubit: idx for idx, qubit in enumerate(qubit_layout)}
407
+ list_couplings = []
408
+ list_fids = []
409
+ for key in res["metrics"]:
410
+ if "irb.cz" in key:
411
+ idx_1 = key.index(".QB")
412
+ idx_2 = key.index("__QB")
413
+ idx_3 = key.index(".fidelity")
414
+ qb1 = int(key[idx_1 + 3 : idx_2]) - 1
415
+ qb2 = int(key[idx_2 + 4 : idx_3]) - 1
416
+ if all([qb1 in qubit_layout, qb2 in qubit_layout]):
417
+ list_couplings.append([qubit_mapping[qb1], qubit_mapping[qb2]])
418
+ list_fids.append(float(res["metrics"][key]["value"]))
419
+ return list_couplings, list_fids
420
+
421
+
422
+ def get_edges(coupling_map, qubit_layout, edges_cal=None, fidelities_cal=None):
423
+ """Produces a networkx.Graph from coupling map fidelity information, with edges given by couplings
424
+ and edge weights given by fidelities
425
+
426
+ Args:
427
+ coupling_map: List[int]
428
+ The list pairs on which 2-qubit gates are natively supported
429
+ qubit_layout: List[int]
430
+ The subset of system-qubits used in the protocol, indexed from 0
431
+ edges_cal: List[int]
432
+ Same as the coupling map, but only with connections that have CZ fidelities in the calibration data
433
+ fidelities_cal: List[float]
434
+ A list of CZ fidelities ordered in the same way as edges_cal
435
+
436
+ Returns:
437
+ graph: networkx.Graph
438
+ The final weighted graph for the given calibration or coupling map
439
+ """
440
+ edges_coupling = list(coupling_map.get_edges())[::2]
441
+ edges_patch = []
442
+ for idx, edge in enumerate(edges_coupling):
443
+ if edge[0] in qubit_layout and edge[1] in qubit_layout:
444
+ edges_patch.append([edge[0], edge[1]])
445
+
446
+ if fidelities_cal is None:
447
+ weights = np.ones(len(edges_patch))
448
+ else:
449
+ fidelities_cal = np.minimum(np.array(fidelities_cal), np.ones(len(fidelities_cal))) # get rid of > 1 fidelities
450
+ fidelities_patch = []
451
+ for edge in edges_patch:
452
+ for idx, edge_2 in enumerate(edges_cal):
453
+ if edge == edge_2:
454
+ fidelities_patch.append(fidelities_cal[idx])
455
+ weights = -np.log(np.array(fidelities_patch))
456
+ graph = Graph()
457
+ for idx, edge in enumerate(edges_patch):
458
+ graph.add_edge(*edge, weight=weights[idx])
459
+ if not is_connected(graph):
460
+ print("Warning: The subgraph of selected qubit_layout is not connected.")
461
+ return graph
462
+
463
+
464
+ def get_cx_map(qubit_layout, graph) -> list[list[int]]:
465
+ """Calculate the cx_map based on participating qubits and the 2QB gate fidelities between them.
466
+
467
+ Uses networkx graph algorithms to calculate the minimal spanning tree of the subgraph defined by qubit_layout.
468
+ The weights are -log(CZ fidelity) for each edge. Then, finds the qubit in the most central position
469
+ by calculating the distances between all qubits. Next, adds CX applications to the list, starting
470
+ from the central qubit, such that the smallest number of layers is executed (most parallel).
471
+
472
+ Args:
473
+ qubit_layout: List[int]
474
+ The subset of system-qubits used in the protocol, indexed from 0
475
+ graph: networkx.Graph
476
+ The connectivity graph with edge weight given by CZ fidelities
477
+ Returns:
478
+ cx_map: List[List[int]]
479
+ A list of CX gates for the GHZ generation circuit, starting from the first gate to be applied
480
+ """
481
+
482
+ rev_mapping = {component: component for i, component in enumerate(qubit_layout)}
483
+ span = minimum_spanning_tree(graph)
484
+ path = dict(all_pairs_shortest_path(span))
485
+ all_distances = [max(len(path[target_qubit][qubit]) for qubit in qubit_layout) for target_qubit in qubit_layout]
486
+ shortest_distance = min(all_distances)
487
+ central_qubit = qubit_layout[np.argmin(all_distances)]
488
+ all_paths = list(path[central_qubit].values())
489
+ all_paths.sort(key=lambda x: -len(x))
490
+ already_entangled = [central_qubit]
491
+ cx_map = []
492
+ for i in range(1, shortest_distance):
493
+ for qubit_path in all_paths:
494
+ if i < len(qubit_path):
495
+ new_qubit = qubit_path[i]
496
+ else:
497
+ continue
498
+ if new_qubit in already_entangled:
499
+ continue
500
+ cx_map.append([rev_mapping[qubit_path[i - 1]], rev_mapping[new_qubit]])
501
+ already_entangled.append(new_qubit)
502
+ return cx_map
503
+
504
+
505
+ class GHZBenchmark(Benchmark):
506
+ """The GHZ Benchmark estimates the quality of generated Greenberger-Horne-Zeilinger states"""
507
+
508
+ analysis_function = staticmethod(fidelity_analysis)
509
+
510
+ def __init__(self, backend: IQMBackendBase, configuration: "GHZConfiguration"):
511
+ """Construct the GHZBenchmark class.
512
+
513
+ Args:
514
+ backend (IQMBackendBase): the backend to execute the benchmark on
515
+ configuration (QuantumVolumeConfiguration): the configuration of the benchmark
516
+ """
517
+ super().__init__(backend, configuration)
518
+
519
+ self.state_generation_routine = configuration.state_generation_routine
520
+ self.choose_qubits_routine = configuration.choose_qubits_routine
521
+ if configuration.custom_qubits_array:
522
+ self.custom_qubits_array = configuration.custom_qubits_array
523
+ else:
524
+ self.custom_qubits_array = list(set(chain(*backend.coupling_map)))
525
+ if not configuration.qubit_counts:
526
+ self.qubit_counts = [len(layout) for layout in self.custom_qubits_array]
527
+ else:
528
+ if any(np.max(configuration.qubit_counts) > [len(layout) for layout in self.custom_qubits_array]):
529
+ raise ValueError("The maximum given qubit count is larger than the size of the smallest qubit layout.")
530
+ self.qubit_counts = configuration.qubit_counts
531
+
532
+ self.qiskit_optim_level = configuration.qiskit_optim_level
533
+ self.optimize_sqg = configuration.optimize_sqg
534
+
535
+ self.fidelity_routine = configuration.fidelity_routine
536
+ self.num_RMs = configuration.num_RMs
537
+
538
+ self.rem = configuration.rem
539
+ self.mit_shots = configuration.mit_shots
540
+ self.cal_url = configuration.cal_url
541
+
542
+ self.timestamp = strftime("%Y%m%d-%H%M%S")
543
+
544
+ @staticmethod
545
+ def name() -> str:
546
+ return "ghz"
547
+
548
+ def generate_native_ghz(self, qubit_layout: List[int], qubit_count: int, routine: str) -> QuantumCircuit:
549
+ """
550
+ Generate a circuit preparing a GHZ state,
551
+ according to a given routine and transpiled to the native gate set and topology
552
+ Args:
553
+ qubit_layout: List[int]
554
+ The subset of system-qubits used in the protocol, indexed from 0
555
+ qubit_count: int
556
+ The number of qubits for which a GHZ state should be created. This values should be smaller or equal to
557
+ the number of qubits in qubit_layout
558
+ routine: str
559
+ The routine to generate the GHZ circuit
560
+
561
+ Returns:
562
+ QuantumCircuit implementing GHZ native state
563
+ """
564
+ # num_qubits = len(qubit_layout)
565
+ fixed_coupling_map = set_coupling_map(qubit_layout, self.backend, "fixed")
566
+
567
+ if routine == "naive":
568
+ ghz = generate_ghz_linear(qubit_count)
569
+ self.untranspiled_circuits[str(qubit_layout)].update({qubit_count: ghz})
570
+ ghz_native_transpiled, _ = perform_backend_transpilation(
571
+ [ghz],
572
+ self.backend,
573
+ qubit_layout,
574
+ fixed_coupling_map,
575
+ qiskit_optim_level=self.qiskit_optim_level,
576
+ optimize_sqg=self.optimize_sqg,
577
+ )
578
+ final_ghz = ghz_native_transpiled
579
+ elif routine == "tree":
580
+ if self.cal_url:
581
+ edges_cal, fidelities_cal = extract_fidelities(self.cal_url, qubit_layout)
582
+ graph = get_edges(self.backend.coupling_map, qubit_layout, edges_cal, fidelities_cal)
583
+ else:
584
+ graph = get_edges(self.backend.coupling_map, qubit_layout)
585
+ ghz, _ = generate_ghz_spanning_tree(graph, qubit_layout, qubit_count)
586
+ self.untranspiled_circuits[str(qubit_layout)].update({qubit_count: ghz})
587
+ ghz_native_transpiled, _ = perform_backend_transpilation(
588
+ [ghz],
589
+ self.backend,
590
+ qubit_layout,
591
+ fixed_coupling_map,
592
+ qiskit_optim_level=self.qiskit_optim_level,
593
+ optimize_sqg=self.optimize_sqg,
594
+ )
595
+ final_ghz = ghz_native_transpiled
596
+ else:
597
+ ghz_log = [generate_ghz_log_cruz(qubit_count), generate_ghz_log_mooney(qubit_count)]
598
+ ghz_native_transpiled, _ = perform_backend_transpilation(
599
+ ghz_log,
600
+ self.backend,
601
+ qubit_layout,
602
+ fixed_coupling_map,
603
+ qiskit_optim_level=self.qiskit_optim_level,
604
+ optimize_sqg=self.optimize_sqg,
605
+ )
606
+ # Use either the circuit with min depth after transpilation or min #2q gates
607
+ if ghz_native_transpiled[0].depth() == ghz_native_transpiled[1].depth():
608
+ index_min_2q = np.argmin([c.count_ops()["cz"] for c in ghz_native_transpiled])
609
+ final_ghz = ghz_native_transpiled[index_min_2q]
610
+ self.untranspiled_circuits[str(qubit_layout)].update({qubit_count: ghz_log[index_min_2q]})
611
+ else:
612
+ index_min_depth = np.argmin([c.depth() for c in ghz_native_transpiled])
613
+ final_ghz = ghz_native_transpiled[index_min_depth]
614
+ self.untranspiled_circuits[str(qubit_layout)].update({qubit_count: ghz_log[index_min_depth]})
615
+ return final_ghz[0]
616
+
617
+ def generate_coherence_meas_circuits(self, qubit_layout: List[int], qubit_count: int) -> List[QuantumCircuit]:
618
+ """
619
+ Takes a given GHZ circuit and outputs circuits needed to measure fidelity via mult. q. coherences method
620
+
621
+ Args:
622
+ qubit_layout: List[int]
623
+ The subset of system-qubits used in the protocol, indexed from 0
624
+ qubit_count: int
625
+ The number of qubits for which a GHZ state should be created. This values should be smaller or equal to
626
+ the number of qubits in qubit_layout
627
+ Returns:
628
+ qc_list_transpiled: List[QuantumCircuit]
629
+ A list of transpiled quantum circuits to be measured
630
+ """
631
+
632
+ qc = self.untranspiled_circuits[str(qubit_layout)][qubit_count]
633
+ qc_list = [qc.copy()]
634
+
635
+ qc.remove_final_measurements()
636
+ qc_inv = qc.inverse()
637
+ phases = [np.pi * i / (qubit_count + 1) for i in range(2 * qubit_count + 2)]
638
+ for phase in phases:
639
+ qc_phase = qc.copy()
640
+ qc_phase.barrier()
641
+ for qubit, _ in enumerate(qubit_layout):
642
+ qc_phase.p(phase, qubit)
643
+ qc_phase.barrier()
644
+ qc_phase.compose(qc_inv, inplace=True)
645
+ qc_phase.measure_active()
646
+ qc_list.append(qc_phase)
647
+
648
+ fixed_coupling_map = set_coupling_map(qubit_layout, self.backend, "fixed")
649
+ qc_list_transpiled, _ = perform_backend_transpilation(
650
+ qc_list,
651
+ self.backend,
652
+ qubit_layout,
653
+ fixed_coupling_map,
654
+ qiskit_optim_level=self.qiskit_optim_level,
655
+ optimize_sqg=self.optimize_sqg,
656
+ )
657
+ self.untranspiled_circuits[str(qubit_layout)].update({qubit_count: qc_list})
658
+ return qc_list_transpiled
659
+
660
+ def generate_readout_circuit(self, qubit_layout, qubit_count):
661
+ """
662
+ A wrapper for the creation of different circuits to estimate the fidelity
663
+
664
+ Args:
665
+ qubit_layout: List[int]
666
+ The subset of system-qubits used in the protocol, indexed from 0
667
+ qubit_count: int
668
+ The number of qubits for which a GHZ state should be created. This values should be smaller or equal to
669
+ the number of qubits in qubit_layout
670
+
671
+ Returns:
672
+ all_circuits_list: List[QuantumCircuit]
673
+ A list of transpiled quantum circuits to be measured
674
+ """
675
+ # Generate the list of circuits
676
+ self.untranspiled_circuits[str(qubit_layout)] = {}
677
+ self.transpiled_circuits[str(qubit_layout)] = {}
678
+
679
+ qcvv_logger.info(f"Now generating a {len(qubit_layout)}-qubit GHZ state on qubits {qubit_layout}")
680
+ transpiled_ghz = self.generate_native_ghz(qubit_layout, qubit_count, self.state_generation_routine)
681
+
682
+ if self.fidelity_routine == "randomized_measurements":
683
+ all_circuits_list, _ = append_rms(transpiled_ghz, cast(int, self.num_RMs), self.backend)
684
+ all_circuits_dict = {tuple(qubit_layout): all_circuits_list}
685
+ elif self.fidelity_routine == "coherences":
686
+ all_circuits_list = self.generate_coherence_meas_circuits(qubit_layout, qubit_count)
687
+ all_circuits_dict = {tuple(qubit_layout): all_circuits_list}
688
+ else:
689
+ all_circuits_list = transpiled_ghz
690
+ all_circuits_dict = {tuple(qubit_layout): all_circuits_list}
691
+
692
+ self.transpiled_circuits[str(qubit_layout)].update(all_circuits_dict)
693
+ return all_circuits_list
694
+
695
+ def add_configuration_to_dataset(self, dataset): # CHECK
696
+ """
697
+ Creates an xarray.Dataset and adds the circuits and configuration metadata to it
698
+
699
+ Args:
700
+ self: Source class
701
+ Returns:
702
+ dataset: xarray.Dataset to be used for further data storage
703
+ """
704
+
705
+ for key, value in self.configuration:
706
+ if key == "benchmark": # Avoid saving the class object
707
+ dataset.attrs[key] = value.name()
708
+ else:
709
+ dataset.attrs[key] = value
710
+ dataset.attrs[f"backend_name"] = self.backend.name
711
+ dataset.attrs[f"untranspiled_circuits"] = self.untranspiled_circuits
712
+ dataset.attrs[f"transpiled_circuits"] = self.transpiled_circuits
713
+
714
+ def execute(self, backend) -> xr.Dataset:
715
+ """
716
+ Executes the benchmark.
717
+ """
718
+ aux_custom_qubits_array = cast(List[List[int]], self.custom_qubits_array).copy()
719
+ dataset = xr.Dataset()
720
+
721
+ for qubit_layout in aux_custom_qubits_array:
722
+ qubit_count = len(qubit_layout)
723
+ circuits = self.generate_readout_circuit(qubit_layout, qubit_count)
724
+
725
+ qcvv_logger.info(f"Retrieving results")
726
+ t_start = time()
727
+ job = backend.run(circuits, shots=self.shots)
728
+ counts = job.result().get_counts()
729
+ print(f"\t Getting counts took {time()-t_start:.2f} sec")
730
+
731
+ # coordinates = [(f"qubit_layout", [str(qubit_layout)])]
732
+ identifier = str(qubit_layout)
733
+ qcvv_logger.info(f"Adding counts to dataset")
734
+ dataset, _ = add_counts_to_dataset(counts, identifier, dataset)
735
+ if self.rem:
736
+ qcvv_logger.info(f"Applying readout error mitigation")
737
+ rem_results, _ = apply_readout_error_mitigation(
738
+ backend, circuits, job.result().get_counts(), self.mit_shots
739
+ )
740
+ rem_results_dist = [counts_mit.nearest_probability_distribution() for counts_mit in rem_results]
741
+ qcvv_logger.info(f"Adding REM results to dataset")
742
+ dataset, _ = add_counts_to_dataset(rem_results_dist, f"{identifier}_rem", dataset)
743
+ self.add_configuration_to_dataset(dataset)
744
+ return dataset
745
+
746
+
747
+ class GHZConfiguration(BenchmarkConfigurationBase):
748
+ """GHZ state configuration
749
+
750
+ Attributes:
751
+ benchmark (Type[Benchmark]): GHZBenchmark
752
+ state_generation_routine (str): The routine to construct circuits generating a GHZ state. Possible values:
753
+ - "tree" (default): Optimized GHZ state generation circuit in log depth that
754
+ takes the qubit coupling and CZ fidelities into account. The algorithm creates
755
+ a minimal spanning tree for the qubit layout and chooses an initial qubit
756
+ that minimizes largest weighted distance to all other qubits.
757
+ - "log": Optimized circuit with parallel application of CX gates such that the
758
+ number of CX gates scales logarithmically in the system size. This
759
+ implementation currently does not take connectivity on the backend into account.
760
+ - "naive": Applies the naive textbook circuit with #CX gates scaling linearly in
761
+ the system size.
762
+ * If other is specified, assumes "log".
763
+ custom_qubits_array (Optional[Sequence[Sequence[int]]]): A sequence (e.g., Tuple or List) of sequences of
764
+ physical qubit layouts, as specified by integer labels, where the benchmark is meant to be run.
765
+ * If None, takes all qubits specified in the backend coupling map.
766
+ qubit_counts (Optional[Sequence[int]]): A sequence (e.g., Tuple or List) of integers denoting number of qubits
767
+ for which the benchmark is meant to be run. The largest qubit count provided here has to be smaller than the
768
+ smallest given qubit layout.
769
+ qiskit_optim_level (int): The optimization level used for transpilation to backend architecture.
770
+ * Default: 3
771
+ optimize_sqg (bool): Whether consecutive single qubit gates are optimized for reduced gate count via
772
+ iqm.qiskit_iqm.iqm_transpilation.optimize_single_qubit_gates
773
+ * Default: True
774
+ fidelity_routine (str): The method with which the fidelity is estimated. Possible values:
775
+ - "coherences": The multiple quantum coherences method as in [Mooney, 2021]
776
+ - "randomized_measurements": Fidelity estimation via randomized measurements outlined in
777
+ https://arxiv.org/abs/1812.02624
778
+ * Default is "coherences"
779
+ num_RMs (Optional[int]): The number of randomized measurements used if the respective fidelity routine is chosen
780
+ * Default: 100
781
+ rem (bool): Boolean flag determining if readout error mitigation is used
782
+ * Default: True
783
+ mit_shots (int): Total number of shots for readout error mitigation
784
+ * Default: 1000
785
+ cal_url (Optional[str]): Optional URL where the calibration data for the selected backend can be retrieved from
786
+ The calibration data is used for the "tree" state generation routine to prioritize couplings with high
787
+ CZ fidelity.
788
+ * Default: None
789
+ """
790
+
791
+ benchmark: Type[Benchmark] = GHZBenchmark
792
+ state_generation_routine: str = "tree"
793
+ choose_qubits_routine: str = "custom"
794
+ custom_qubits_array: Optional[List[List[int]]] = None
795
+ qubit_counts: Optional[List[int]] = None
796
+ qiskit_optim_level: int = 3
797
+ optimize_sqg: bool = True
798
+ fidelity_routine: str = "coherences"
799
+ num_RMs: Optional[int] = 24
800
+ rem: bool = True
801
+ mit_shots: int = 1_000
802
+ cal_url: Optional[str] = None