iqm-benchmarks 2.32__py3-none-any.whl → 2.33__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.

@@ -0,0 +1,19 @@
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
+ Error Per Layered Gate (EPLG) estimates the layer fidelity of 2Q gate layers.
17
+ """
18
+
19
+ from . import eplg
@@ -0,0 +1,409 @@
1
+ """
2
+ Error Per Layered Gate (EPLG).
3
+ """
4
+
5
+ from time import strftime
6
+ from typing import Dict, Optional, Sequence, Tuple, Type, cast
7
+
8
+ from matplotlib.figure import Figure
9
+ import matplotlib.pyplot as plt
10
+ import networkx as nx
11
+ import numpy as np
12
+ from qiskit.transpiler import CouplingMap
13
+ from uncertainties import ufloat
14
+ import xarray as xr
15
+
16
+ from iqm.benchmarks import (
17
+ Benchmark,
18
+ BenchmarkAnalysisResult,
19
+ BenchmarkCircuit,
20
+ BenchmarkObservation,
21
+ BenchmarkObservationIdentifier,
22
+ BenchmarkRunResult,
23
+ )
24
+ from iqm.benchmarks.benchmark import BenchmarkConfigurationBase
25
+ from iqm.benchmarks.logging_config import qcvv_logger
26
+ from iqm.benchmarks.randomized_benchmarking.direct_rb.direct_rb import (
27
+ DirectRandomizedBenchmarking,
28
+ DirectRBConfiguration,
29
+ direct_rb_analysis,
30
+ )
31
+ from iqm.benchmarks.utils import split_into_disjoint_pairs
32
+ from iqm.benchmarks.utils_plots import GraphPositions, draw_graph_edges, evaluate_hamiltonian_paths, rx_to_nx_graph
33
+ from iqm.qiskit_iqm.iqm_backend import IQMBackendBase
34
+
35
+
36
+ def plot_layered_fidelities_graph(
37
+ fidelities: Dict[str, Dict[str, float]],
38
+ backend_coupling_map: CouplingMap,
39
+ qubit_names: Dict[int, str],
40
+ timestamp: str,
41
+ station: Optional[str] = None,
42
+ eplg_estimate: Optional[Dict[str, float]] = None,
43
+ ) -> Tuple[str, Figure]:
44
+ """Plots the layered fidelity for each corresponding pair of qubits in a graph layout of the given backend.
45
+
46
+ Args:
47
+ fidelities (Dict[str, Dict[str, float]]): A dictionary (str qubit keys) of dictionaries (keys "value"/"uncertainty") of fidelities (float) to plot.
48
+ backend_coupling_map (CouplingMap): The CouplingMap instance.
49
+ qubit_names (Dict[int, str]): A dictionary of qubit names corresponding to qubit indices.
50
+ timestamp (str): The timestamp of the corresponding experiment.
51
+ station (str): The name of the station to use for the graph layout.
52
+ eplg_estimate (Optional[Dict[str, float]]): A dictionary with the EPLG estimate value and its uncertainty.
53
+
54
+ Returns:
55
+ Tuple[str, Figure]: The figure label and the layered fidelities plot figure.
56
+ """
57
+ num_qubits = len(qubit_names.keys())
58
+ fig_name = (
59
+ f"layered_fidelities_graph_{station}_{timestamp}"
60
+ if station is not None
61
+ else f"layered_fidelities_graph_{timestamp}"
62
+ )
63
+ # Sort the fidelities by value
64
+ sorted_fidelities = dict(sorted(fidelities.items(), key=lambda item: item[1]["value"]))
65
+
66
+ qubit_pairs = [
67
+ tuple(int(num) for num in x.replace("(", "").replace(")", "").replace("...", "").split(", "))
68
+ for x in sorted_fidelities.keys()
69
+ ]
70
+ fidelity_values = [100 * a["value"] for a in sorted_fidelities.values()]
71
+
72
+ fidelity_edges = dict(zip(qubit_pairs, fidelity_values))
73
+
74
+ cmap = plt.cm.get_cmap("winter")
75
+
76
+ fig = plt.figure()
77
+ ax = plt.axes()
78
+
79
+ if station is not None:
80
+ if station.lower() in GraphPositions.predefined_stations:
81
+ qubit_positions = GraphPositions.predefined_stations[station.lower()]
82
+ else:
83
+ if num_qubits in (20, 7):
84
+ station = "garnet" if num_qubits == 20 else "deneb"
85
+ qubit_positions = GraphPositions.predefined_stations[station]
86
+ else:
87
+ graph_backend = backend_coupling_map.graph.to_undirected(multigraph=False)
88
+ qubit_positions = GraphPositions.create_positions(graph_backend)
89
+ else:
90
+ graph_backend = backend_coupling_map.graph.to_undirected(multigraph=False)
91
+ if num_qubits in (20, 7):
92
+ station = "garnet" if num_qubits == 20 else "deneb"
93
+ qubit_positions = GraphPositions.predefined_stations[station]
94
+ else:
95
+ qubit_positions = GraphPositions.create_positions(graph_backend)
96
+
97
+ # Normalize fidelity values to the range [0, 1] for color mapping
98
+ norm = plt.Normalize(vmin=cast(float, min(fidelity_values)), vmax=cast(float, max(fidelity_values)))
99
+ edge_colors = [cmap(norm(fidelity_edges[edge])) for edge in qubit_pairs]
100
+
101
+ nx.draw_networkx(
102
+ rx_to_nx_graph(backend_coupling_map),
103
+ pos=qubit_positions,
104
+ nodelist=list(range(num_qubits)),
105
+ labels={x: qubit_names[x] for x in range(num_qubits)},
106
+ font_size=6.5,
107
+ edgelist=qubit_pairs,
108
+ width=4.0,
109
+ edge_color=edge_colors,
110
+ node_color="k",
111
+ font_color="w",
112
+ ax=ax,
113
+ )
114
+
115
+ # Add colorbar
116
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
117
+ sm.set_array([])
118
+ cbar = fig.colorbar(sm, ax=ax, shrink=0.5, label="Layered Fidelity (%)", format="%.2f")
119
+ cbar.set_ticks(np.linspace(min(fidelity_values), max(fidelity_values), 5, endpoint=True))
120
+
121
+ station_string = "IQM Backend" if station is None else station.capitalize()
122
+
123
+ eplg_string = (
124
+ f"EPLG estimate: {eplg_estimate['value']:.2e} +/- {eplg_estimate['uncertainty']:.2e}\n" if eplg_estimate else ""
125
+ )
126
+ plt.title(f"Layered fidelities for qubit pairs in {station_string}\n" f"{eplg_string}{timestamp}")
127
+ plt.close()
128
+
129
+ return fig_name, fig
130
+
131
+
132
+ def eplg_analysis(run: BenchmarkRunResult) -> BenchmarkAnalysisResult:
133
+ """EPLG analysis function
134
+
135
+ Args:
136
+ run (BenchmarkRunResult): The result of the benchmark run.
137
+
138
+ Returns:
139
+ AnalysisResult corresponding to DRB.
140
+ """
141
+
142
+ result_direct_rb = direct_rb_analysis(run)
143
+
144
+ dataset = result_direct_rb.dataset.copy(deep=True)
145
+ observations = result_direct_rb.observations
146
+ plots: Dict[str, Figure] = {}
147
+
148
+ timestamp = dataset.attrs["execution_timestamp"]
149
+
150
+ backend_configuration_name = dataset.attrs["backend_configuration_name"]
151
+ backend_coupling_map = dataset.attrs["backend_coupling_map"]
152
+ backend_num_qubits = dataset.attrs["backend_num_qubits"]
153
+
154
+ num_edges = len(observations)
155
+ num_qubits = dataset.attrs["num_qubits"]
156
+ edges = dataset.attrs["edges"]
157
+ disjoint_layers = dataset.attrs["disjoint_layers"]
158
+ qubit_names = dataset.attrs["qubit_names"]
159
+
160
+ fidelities = {}
161
+ fid_product = ufloat(1, 0)
162
+ for obs in observations:
163
+ fid_product *= ufloat(obs.value, obs.uncertainty)
164
+ fidelities[str(obs.identifier.qubit_indices)] = {"value": obs.value, "uncertainty": obs.uncertainty}
165
+
166
+ LF = fid_product
167
+ EPLG = 1 - LF ** (1 / num_edges)
168
+
169
+ observations.append(
170
+ BenchmarkObservation(
171
+ name="layer_fidelity",
172
+ identifier=BenchmarkObservationIdentifier(f"(n_qubits={num_qubits})"),
173
+ value=LF.nominal_value,
174
+ uncertainty=LF.std_dev,
175
+ )
176
+ )
177
+
178
+ observations.append(
179
+ BenchmarkObservation(
180
+ name="eplg",
181
+ identifier=BenchmarkObservationIdentifier(f"(n_qubits={num_qubits})"),
182
+ value=EPLG.nominal_value,
183
+ uncertainty=EPLG.std_dev,
184
+ )
185
+ )
186
+
187
+ # Plot the edges graph
188
+ fig_name, fig = draw_graph_edges(
189
+ backend_coupling_map,
190
+ backend_num_qubits=backend_num_qubits,
191
+ edge_list=edges,
192
+ timestamp=timestamp,
193
+ disjoint_layers=disjoint_layers,
194
+ station=backend_configuration_name,
195
+ qubit_names=qubit_names,
196
+ is_eplg=True,
197
+ )
198
+ plots[fig_name] = fig
199
+
200
+ # Plot the layered fidelities graph
201
+ fig_name, fig = plot_layered_fidelities_graph(
202
+ fidelities=fidelities,
203
+ backend_coupling_map=backend_coupling_map,
204
+ qubit_names=qubit_names,
205
+ timestamp=timestamp,
206
+ station=backend_configuration_name,
207
+ eplg_estimate={"value": EPLG.nominal_value, "uncertainty": EPLG.std_dev},
208
+ )
209
+ plots[fig_name] = fig
210
+
211
+ plots.update(result_direct_rb.plots)
212
+
213
+ return BenchmarkAnalysisResult(dataset=dataset, observations=observations, plots=plots)
214
+
215
+
216
+ class EPLGBenchmark(Benchmark):
217
+ """EPLG estimates the layer fidelity of native 2Q gate layers"""
218
+
219
+ analysis_function = staticmethod(eplg_analysis)
220
+
221
+ name: str = "EPLG"
222
+
223
+ def __init__(self, backend_arg: IQMBackendBase | str, configuration: "EPLGConfiguration"):
224
+ """Construct the EPLG class
225
+
226
+ Args:
227
+ backend_arg (IQMBackendBase | str): The backend to use for the benchmark,
228
+ either as a backend instance or a backend name string.
229
+ configuration (EPLGConfiguration): The configuration settings for the EPLG benchmark.
230
+ """
231
+ super().__init__(backend_arg, configuration)
232
+ # EXPERIMENT
233
+ self.backend_configuration_name = backend_arg if isinstance(backend_arg, str) else backend_arg.name
234
+ self.session_timestamp = strftime("%Y%m%d-%H%M%S")
235
+ self.execution_timestamp = ""
236
+
237
+ # Initialize the variable to contain the circuits for each layout
238
+ self.untranspiled_circuits = BenchmarkCircuit("untranspiled_circuits")
239
+ self.transpiled_circuits = BenchmarkCircuit("transpiled_circuits")
240
+
241
+ self.drb_depths = configuration.drb_depths
242
+ self.drb_circuit_samples = configuration.drb_circuit_samples
243
+
244
+ self.custom_qubits_array = configuration.custom_qubits_array
245
+
246
+ self.chain_length = configuration.chain_length
247
+ self.chain_path_samples = configuration.chain_path_samples
248
+ self.num_disjoint_layers = configuration.num_disjoint_layers
249
+ self.calibration_url = configuration.calibration_url
250
+ self.max_hamiltonian_path_tries = configuration.max_hamiltonian_path_tries
251
+
252
+ def add_all_meta_to_dataset(self, dataset: xr.Dataset):
253
+ """Adds all configuration metadata and circuits to the dataset variable
254
+
255
+ Args:
256
+ dataset (xr.Dataset): The xarray dataset
257
+ """
258
+ dataset.attrs["session_timestamp"] = self.session_timestamp
259
+ dataset.attrs["execution_timestamp"] = self.execution_timestamp
260
+ dataset.attrs["backend_configuration_name"] = self.backend_configuration_name
261
+ dataset.attrs["backend_name"] = self.backend.name
262
+ dataset.attrs["backend_coupling_map"] = self.backend.coupling_map
263
+ dataset.attrs["backend_num_qubits"] = self.backend.num_qubits
264
+
265
+ for key, value in self.configuration:
266
+ if key == "benchmark": # Avoid saving the class object
267
+ dataset.attrs[key] = value.name
268
+ else:
269
+ dataset.attrs[key] = value
270
+ # Defined outside configuration - if any
271
+
272
+ def validate_custom_qubits_array(self):
273
+ """Validates the custom qubits array input ."""
274
+ if self.custom_qubits_array is not None:
275
+ # Validate that the custom qubits array is a list of pairs
276
+ if not all(isinstance(pair, tuple) and len(pair) == 2 for pair in self.custom_qubits_array):
277
+ raise ValueError("The custom qubits array must be a Sequence of tuples.")
278
+ # Validate that the custom qubits array has no repeated qubits
279
+ if len(set(tuple(sorted(x)) for x in self.custom_qubits_array)) != len(self.custom_qubits_array):
280
+ raise ValueError("The custom qubits array must have unique qubit pairs.")
281
+
282
+ def validate_random_chain_inputs(self):
283
+ """Validates inputs for chain sampling.
284
+
285
+ Raises:
286
+ ValueError: If the chain inputs are beyond general or EPLG criteria.
287
+ """
288
+ # Check chain length
289
+ if self.chain_length is None:
290
+ qcvv_logger.warning("chain_length input was None: will assign backend.num_qubits!")
291
+ self.chain_length = self.backend.num_qubits
292
+ elif self.chain_length > self.backend.num_qubits:
293
+ raise ValueError("The chain length cannot exceed the number of qubits in the backend.")
294
+
295
+ # Check path samples
296
+ if self.chain_path_samples is None:
297
+ self.chain_path_samples = 20
298
+ elif self.chain_path_samples < 1:
299
+ raise ValueError("The number of chain path samples must be a positive integer.")
300
+
301
+ # Check calibration URL - this is a temporary solution, normally the backend should be enough to specify this
302
+ if self.calibration_url is None:
303
+ raise ValueError("The calibration URL must be specified if custom qubits array is not specified.")
304
+
305
+ if self.num_disjoint_layers is None:
306
+ self.num_disjoint_layers = 2
307
+ elif self.num_disjoint_layers < 1:
308
+ raise ValueError("The number of disjoint layers must be a positive integer.")
309
+
310
+ if self.max_hamiltonian_path_tries is None:
311
+ self.max_hamiltonian_path_tries = 10
312
+ elif self.max_hamiltonian_path_tries < 1:
313
+ raise ValueError("The maximum number of Hamiltonian path tries must be a positive integer.")
314
+
315
+ def execute(self, backend: IQMBackendBase) -> xr.Dataset:
316
+ """Execute the EPLG Benchmark"""
317
+
318
+ self.execution_timestamp = strftime("%Y%m%d-%H%M%S")
319
+
320
+ dataset_eplg = xr.Dataset()
321
+
322
+ if self.custom_qubits_array is not None:
323
+ self.validate_custom_qubits_array()
324
+ edges = self.custom_qubits_array
325
+ num_qubits = len(list(set(x for y in edges for x in y)))
326
+ all_disjoint = split_into_disjoint_pairs(self.custom_qubits_array)
327
+ self.num_disjoint_layers = len(all_disjoint)
328
+ qcvv_logger.info(
329
+ f"Using specified custom_qubits_array: will split into {self.num_disjoint_layers} disjoint layers."
330
+ )
331
+
332
+ else:
333
+ self.validate_random_chain_inputs()
334
+ num_qubits = cast(int, self.chain_length)
335
+ qcvv_logger.info("Generating linear chain path")
336
+ h_path_costs = evaluate_hamiltonian_paths(
337
+ self.chain_length,
338
+ self.chain_path_samples,
339
+ self.backend,
340
+ self.calibration_url,
341
+ self.max_hamiltonian_path_tries,
342
+ )
343
+ qcvv_logger.info("Extracting the path that maximizes total 2Q calibration fidelity")
344
+ max_cost_path = h_path_costs[max(h_path_costs.keys())]
345
+
346
+ all_disjoint = [
347
+ max_cost_path[i :: self.num_disjoint_layers] for i in range(cast(int, self.num_disjoint_layers))
348
+ ]
349
+ edges = max_cost_path
350
+
351
+ dataset_eplg.attrs["num_qubits"] = num_qubits
352
+ backend_qubits = list(range(backend.num_qubits))
353
+ dataset_eplg.attrs["qubit_names"] = {qubit: self.backend.index_to_qubit_name(qubit) for qubit in backend_qubits}
354
+
355
+ self.add_all_meta_to_dataset(dataset_eplg)
356
+
357
+ # Execute parallel DRB in all disjoint layers
358
+ drb_config = DirectRBConfiguration(
359
+ qubits_array=all_disjoint,
360
+ is_eplg=True,
361
+ depths=self.drb_depths,
362
+ num_circuit_samples=self.drb_circuit_samples,
363
+ shots=self.shots,
364
+ max_gates_per_batch=self.max_gates_per_batch,
365
+ max_circuits_per_batch=self.configuration.max_circuits_per_batch,
366
+ )
367
+
368
+ benchmarks_direct_rb = DirectRandomizedBenchmarking(backend, drb_config)
369
+ run_direct_rb = benchmarks_direct_rb.run()
370
+ dataset = run_direct_rb.dataset
371
+ self.circuits = benchmarks_direct_rb.circuits
372
+ dataset_eplg.attrs.update({"disjoint_layers": all_disjoint, "edges": edges})
373
+ dataset.attrs.update(dataset_eplg.attrs)
374
+
375
+ return dataset
376
+
377
+
378
+ class EPLGConfiguration(BenchmarkConfigurationBase):
379
+ """EPLG Configuration
380
+
381
+ Attributes:
382
+ drb_depths (Sequence[int]): The layer depths to consider for the parallel DRB.
383
+ drb_circuit_samples (int): The number of circuit samples to consider for the parallel DRB.
384
+ custom_qubits_array (Optional[Sequence[Tuple[int, int]]]): The custom qubits array to consider; this corresponds to a Sequence of Tuple pairs of qubits.
385
+ * If not specified, will proceed to generate linear chains at random, selecting the one with the highest total 2Q gate fidelity.
386
+ * Default is None.
387
+ chain_length (Optional[int]): The length of a linear chain of 2Q gates to consider, corresponding to the number of qubits, if custom_qubits_array not specified.
388
+ * Default is None: assigns the number of qubits in the backend minus one.
389
+ chain_path_samples (int): The number of chain path samples to consider, if custom_qubits_array not specified.
390
+ * Default is None: assigns 20 path samples (arbitrary).
391
+ num_disjoint_layers (Optional[int]): The number of disjoint layers to consider.
392
+ * Default is None: assigns 2 disjoint layers (arbitrary).
393
+ max_hamiltonian_path_tries (Optional[int]): The maximum number of tries to find a Hamiltonian path.
394
+ * Default is None: assigns 10 tries (arbitrary).
395
+ calibration_url (Optional[str]): The URL of the IQM station to retrieve calibration data from.
396
+ * It must be specified if custom_qubits_array is not specified.
397
+ * Default is None - raises an error if custom_qubits_array is not specified.
398
+
399
+ """
400
+
401
+ benchmark: Type[Benchmark] = EPLGBenchmark
402
+ drb_depths: Sequence[int]
403
+ drb_circuit_samples: int
404
+ custom_qubits_array: Optional[Sequence[Tuple[int, int]]] = None
405
+ chain_length: Optional[int] = None
406
+ chain_path_samples: Optional[int] = None
407
+ num_disjoint_layers: Optional[int] = None
408
+ max_hamiltonian_path_tries: Optional[int] = None
409
+ calibration_url: Optional[str] = None
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  """
16
- Mirror RB reflects the fidelity of mirror circuits
16
+ Mirror RB estimates the average layer fidelity of ensembles of gates
17
17
  """
18
18
 
19
19
  from . import mirror_rb