iqm-benchmarks 1.7__py3-none-any.whl → 1.9__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,851 @@
1
+ """
2
+ Data analysis code for compressive gate set tomography
3
+ """
4
+
5
+ from itertools import product
6
+ from time import perf_counter
7
+ from typing import Any, List, Tuple, Union
8
+
9
+ from matplotlib.figure import Figure
10
+ import matplotlib.pyplot as plt
11
+ from matplotlib.transforms import Bbox
12
+ from numpy import ndarray
13
+ import numpy as np
14
+ from pandas import DataFrame
15
+ from pygsti.models.model import Model
16
+ from pygsti.tools import change_basis
17
+ import xarray as xr
18
+
19
+ from iqm.benchmarks.benchmark_definition import (
20
+ BenchmarkAnalysisResult,
21
+ BenchmarkObservation,
22
+ BenchmarkObservationIdentifier,
23
+ BenchmarkRunResult,
24
+ )
25
+ from mGST import additional_fns, algorithm, compatibility
26
+ from mGST.low_level_jit import contract
27
+ from mGST.qiskit_interface import qiskit_gate_to_operator
28
+ from mGST.reporting import figure_gen, reporting
29
+
30
+
31
+ def dataframe_to_figure(
32
+ df: DataFrame, row_labels: Union[List[str], None] = None, col_width: float = 2, fontsize: int = 12
33
+ ) -> Figure:
34
+ """Turns a pandas DataFrame into a figure
35
+ This is needed to conform with the standard file saving routine of QCVV.
36
+
37
+ Args:
38
+ df: Pandas DataFrame
39
+ A dataframe table containing GST results
40
+ row_labels: List[str]
41
+ The row labels for the dataframe
42
+ col_width: int
43
+ Used to control cell width in the table
44
+ fontsize: int
45
+ Font size of text/numbers in table cells
46
+
47
+ Returns:
48
+ figure: Matplotlib figure object
49
+ A figure representing the dataframe.
50
+ """
51
+
52
+ if row_labels is None:
53
+ row_labels = list(np.arange(df.shape[0]))
54
+
55
+ row_height = fontsize / 70 * 2
56
+ n_cols = df.shape[1]
57
+ n_rows = df.shape[0]
58
+ figsize = np.array([n_cols + 1, n_rows + 1]) * np.array([col_width, row_height])
59
+
60
+ fig, ax = plt.subplots(figsize=figsize)
61
+
62
+ fig.patch.set_visible(False)
63
+ ax.axis("off")
64
+ ax.axis("tight")
65
+ data_array = (df.to_numpy(dtype="str")).copy()
66
+ column_names = df.columns.tolist()
67
+ table = ax.table(
68
+ cellText=data_array,
69
+ colLabels=column_names,
70
+ rowLabels=row_labels,
71
+ cellLoc="center",
72
+ colColours=["#7FA1C3" for _ in range(n_cols)],
73
+ bbox=Bbox([[0, 0], [1, 1]]),
74
+ )
75
+ table.set_fontsize(fontsize)
76
+ table.set_figure(fig)
77
+ return fig
78
+
79
+
80
+ def bootstrap_errors(
81
+ dataset: xr.Dataset,
82
+ y: ndarray,
83
+ K: ndarray,
84
+ X: ndarray,
85
+ E: ndarray,
86
+ rho: ndarray,
87
+ target_mdl: Model,
88
+ parametric: bool = False,
89
+ ) -> tuple[Any, Any, Any, Any, Any]:
90
+ """Resamples circuit outcomes a number of times and computes GST estimates for each repetition
91
+ All results are then returned in order to compute bootstrap-error bars for GST estimates.
92
+ Parametric bootstrapping uses the estimated gate set to create a newly sampled data set.
93
+ Non-parametric bootstrapping uses the initial dataset and resamples according to the
94
+ corresp. outcome probabilities.
95
+ Each bootstrap run is initialized with the estimated gate set in order to save processing time.
96
+
97
+ Parameters
98
+ ----------
99
+ dataset: xarray.Dataset
100
+ A dataset containing counts from the experiment and configurations
101
+ qubit_layout: List[int]
102
+ The list of qubits for the current GST experiment
103
+ y: ndarray
104
+ The circuit outcome probabilities as a num_povm x num_circuits array
105
+ K : ndarray
106
+ Each subarray along the first axis contains a set of Kraus operators.
107
+ The second axis enumerates Kraus operators for a gate specified by the first axis.
108
+ X : 3D ndarray
109
+ Array where reconstructed CPT superoperators in standard basis are stacked along the first axis.
110
+ E : ndarray
111
+ Current POVM estimate
112
+ rho : ndarray
113
+ Current initial state estimate
114
+ target_mdl : pygsti model object
115
+ The target gate set
116
+ parametric : bool
117
+ If set to True, parametric bootstrapping is used, else non-parametric bootstrapping. Default: False
118
+
119
+ Returns
120
+ -------
121
+ X_array : ndarray
122
+ Array containing all estimated gate tensors of different bootstrapping repetitions along first axis
123
+ E_array : ndarray
124
+ Array containing all estimated POVM tensors of different bootstrapping repetitions along first axis
125
+ rho_array : ndarray
126
+ Array containing all estimated initial states of different bootstrapping repetitions along first axis
127
+ df_g_array : ndarray
128
+ Contains gate quality measures of bootstrapping repetitions
129
+ df_o_array : ndarray
130
+ Contains SPAM and other quality measures of bootstrapping repetitions
131
+
132
+ """
133
+ if parametric:
134
+ y = np.real(
135
+ np.array(
136
+ [
137
+ [E[i].conj() @ contract(X, j) @ rho for j in dataset.attrs["J"]]
138
+ for i in range(dataset.attrs["num_povm"])
139
+ ]
140
+ )
141
+ )
142
+ X_array = np.zeros((dataset.attrs["bootstrap_samples"], *X.shape)).astype(complex)
143
+ E_array = np.zeros((dataset.attrs["bootstrap_samples"], *E.shape)).astype(complex)
144
+ rho_array = np.zeros((dataset.attrs["bootstrap_samples"], *rho.shape)).astype(complex)
145
+ df_g_list = []
146
+ df_o_list = []
147
+
148
+ for i in range(dataset.attrs["bootstrap_samples"]):
149
+ y_sampled = additional_fns.sampled_measurements(y, dataset.attrs["shots"]).copy()
150
+ _, X_, E_, rho_, _ = algorithm.run_mGST(
151
+ y_sampled,
152
+ dataset.attrs["J"],
153
+ dataset.attrs["seq_len_list"][-1],
154
+ dataset.attrs["num_gates"],
155
+ dataset.attrs["pdim"] ** 2,
156
+ dataset.attrs["rank"],
157
+ dataset.attrs["num_povm"],
158
+ dataset.attrs["batch_size"],
159
+ dataset.attrs["shots"],
160
+ method=dataset.attrs["opt_method"],
161
+ max_inits=dataset.attrs["max_inits"],
162
+ max_iter=0,
163
+ final_iter=dataset.attrs["max_iterations"][1],
164
+ threshold_multiplier=dataset.attrs["convergence_criteria"][0],
165
+ target_rel_prec=dataset.attrs["convergence_criteria"][1],
166
+ init=[K, E, rho],
167
+ testing=False,
168
+ )
169
+
170
+ X_opt, E_opt, rho_opt = reporting.gauge_opt(X_, E_, rho_, target_mdl, dataset.attrs[f"gauge_weights"])
171
+ df_g, df_o = reporting.report(
172
+ X_opt,
173
+ E_opt,
174
+ rho_opt,
175
+ dataset.attrs["J"],
176
+ y_sampled,
177
+ target_mdl,
178
+ dataset.attrs["gate_labels"],
179
+ )
180
+ df_g_list.append(df_g.values)
181
+ df_o_list.append(df_o.values)
182
+
183
+ X_opt_pp, E_opt_pp, rho_opt_pp = compatibility.std2pp(X_opt, E_opt, rho_opt)
184
+
185
+ X_array[i] = X_opt_pp
186
+ E_array[i] = E_opt_pp
187
+ rho_array[i] = rho_opt_pp
188
+
189
+ return X_array, E_array, rho_array, np.array(df_g_list), np.array(df_o_list)
190
+
191
+
192
+ def generate_non_gate_results(
193
+ dataset: xr.Dataset, qubit_layout: List[int], df_o: DataFrame
194
+ ) -> tuple[DataFrame, Figure]:
195
+ """
196
+ Creates error bars (if bootstrapping was used) and formats results for non-gate errors.
197
+ The resulting tables are also turned into figures, so that they can be saved automatically.
198
+
199
+ Args:
200
+ dataset: xr.Dataset
201
+ A dataset containing counts from the experiment and configurations
202
+ qubit_layout: List[int]
203
+ The list of qubits for the current GST experiment
204
+ df_o: Pandas DataFrame
205
+ A dataframe containing the non-gate quality metrics (SPAM errors and fit quality)
206
+
207
+ Returns:
208
+ df_o_final: Pandas DataFrame
209
+ The final formated results
210
+ """
211
+ identifier = BenchmarkObservationIdentifier(qubit_layout).string_identifier
212
+ if dataset.attrs["bootstrap_samples"] > 0:
213
+ _, _, _, _, df_o_array = dataset.attrs["results_layout_" + identifier]["bootstrap_data"]
214
+ df_o_array[df_o_array == -1] = np.nan
215
+ percentiles_o_low, percentiles_o_high = np.nanpercentile(df_o_array, [2.5, 97.5], axis=0)
216
+ df_o_final = DataFrame(
217
+ {
218
+ f"Mean TVD: estimate - data": reporting.number_to_str(
219
+ df_o.values[0, 1].copy(), [percentiles_o_high[0, 1], percentiles_o_low[0, 1]], precision=5
220
+ ),
221
+ f"Mean TVD: target - data": reporting.number_to_str(
222
+ df_o.values[0, 2].copy(), [percentiles_o_high[0, 2], percentiles_o_low[0, 2]], precision=5
223
+ ),
224
+ f"POVM - diamond dist.": reporting.number_to_str(
225
+ df_o.values[0, 3].copy(), [percentiles_o_high[0, 3], percentiles_o_low[0, 3]], precision=5
226
+ ),
227
+ f"State - trace dist.": reporting.number_to_str(
228
+ df_o.values[0, 4].copy(), [percentiles_o_high[0, 4], percentiles_o_low[0, 4]], precision=5
229
+ ),
230
+ },
231
+ index=[""],
232
+ )
233
+ else:
234
+ df_o_final = DataFrame(
235
+ {
236
+ f"Mean TVD: estimate - data": reporting.number_to_str(df_o.values[0, 1].copy(), precision=5),
237
+ f"Mean TVD: target - data": reporting.number_to_str(df_o.values[0, 2].copy(), precision=5),
238
+ f"POVM - diamond dist.": reporting.number_to_str(df_o.values[0, 3].copy(), precision=5),
239
+ f"State - trace dist.": reporting.number_to_str(df_o.values[0, 4].copy(), precision=5),
240
+ },
241
+ index=[""],
242
+ )
243
+ fig = dataframe_to_figure(df_o_final, [""]) # dataframe_to_figure(df_o_final, [""])
244
+ return df_o_final, fig
245
+
246
+
247
+ def generate_unit_rank_gate_results(
248
+ dataset: xr.Dataset, qubit_layout: List[int], df_g: DataFrame, X_opt: ndarray, K_target: ndarray
249
+ ) -> Tuple[DataFrame, DataFrame, Figure, Figure]:
250
+ """
251
+ Produces all result tables for Kraus rank 1 estimates and turns them into figures.
252
+
253
+ This includes parameters of the Hamiltonian generators in the Pauli basis for all gates,
254
+ as well as the usual performance metrics (Fidelities and Diamond distances). If bootstrapping
255
+ data is available, error bars will also be generated.
256
+
257
+ Args:
258
+ dataset: xarray.Dataset
259
+ A dataset containing counts from the experiment and configurations
260
+ qubit_layout: List[int]
261
+ The list of qubits for the current GST experiment
262
+ df_g: Pandas DataFrame
263
+ The dataframe with properly formated results
264
+ X_opt: 3D numpy array
265
+ The gate set after gauge optimization
266
+ K_target: 4D numpy array
267
+ The Kraus operators of all target gates, used to compute distance measures.
268
+
269
+ Returns:
270
+ df_g_final: Pandas DataFrame
271
+ The dataframe with properly formated results of standard gate errors
272
+ df_g_rotation Pandas DataFrame
273
+ A dataframe containing Hamiltonian (rotation) parameters
274
+ fig_g: Figure
275
+ A table in Figure format of gate results (fidelities etc.)
276
+ fig_rotation: Figure
277
+ A table in Figure format of gate Hamiltonian parameters
278
+
279
+
280
+ """
281
+ identifier = BenchmarkObservationIdentifier(qubit_layout).string_identifier
282
+ pauli_labels = generate_basis_labels(dataset.attrs["pdim"], basis="Pauli")
283
+ if dataset.attrs["bootstrap_samples"] > 0:
284
+ X_array, E_array, rho_array, df_g_array, _ = dataset.attrs["results_layout_" + identifier]["bootstrap_data"]
285
+ df_g_array[df_g_array == -1] = np.nan
286
+ percentiles_g_low, percentiles_g_high = np.nanpercentile(df_g_array, [2.5, 97.5], axis=0)
287
+
288
+ df_g_final = DataFrame(
289
+ {
290
+ r"Avg. gate fidelity": [
291
+ reporting.number_to_str(
292
+ df_g.values[i, 0], [percentiles_g_high[i, 0], percentiles_g_low[i, 0]], precision=5
293
+ )
294
+ for i in range(len(dataset.attrs["gate_labels"]))
295
+ ],
296
+ r"Diamond distance": [
297
+ reporting.number_to_str(
298
+ df_g.values[i, 1], [percentiles_g_high[i, 1], percentiles_g_low[i, 1]], precision=5
299
+ )
300
+ for i in range(dataset.attrs["num_gates"])
301
+ ],
302
+ }
303
+ )
304
+
305
+ U_opt = reporting.phase_opt(X_opt, K_target)
306
+ pauli_coeffs = reporting.compute_sparsest_Pauli_Hamiltonian(U_opt)
307
+
308
+ bootstrap_pauli_coeffs = np.zeros((len(X_array), dataset.attrs["num_gates"], dataset.attrs["pdim"] ** 2))
309
+ for i, X_ in enumerate(X_array):
310
+ X_std, _, _ = compatibility.pp2std(X_, E_array[i], rho_array[i])
311
+ U_opt_ = reporting.phase_opt(
312
+ np.array([change_basis(X_std[j], "pp", "std") for j in range(dataset.attrs["num_gates"])]), K_target
313
+ )
314
+ pauli_coeffs_ = reporting.compute_sparsest_Pauli_Hamiltonian(U_opt_)
315
+ bootstrap_pauli_coeffs[i, :, :] = pauli_coeffs_
316
+ pauli_coeffs_low, pauli_coeffs_high = np.nanpercentile(bootstrap_pauli_coeffs, [2.5, 97.5], axis=0)
317
+
318
+ df_g_rotation = DataFrame(
319
+ np.array(
320
+ [
321
+ [
322
+ reporting.number_to_str(
323
+ pauli_coeffs[i, j], [pauli_coeffs_high[i, j], pauli_coeffs_low[i, j]], precision=5
324
+ )
325
+ for i in range(dataset.attrs["num_gates"])
326
+ ]
327
+ for j in range(dataset.attrs["pdim"] ** 2)
328
+ ]
329
+ ).T
330
+ )
331
+
332
+ df_g_rotation.columns = [f"h_%s" % label for label in pauli_labels]
333
+ df_g_rotation.rename(index=dataset.attrs["gate_labels"], inplace=True)
334
+
335
+ else:
336
+ df_g_final = DataFrame(
337
+ {
338
+ "Avg. gate fidelity": [
339
+ reporting.number_to_str(df_g.values[i, 0], precision=5) for i in range(dataset.attrs["num_gates"])
340
+ ],
341
+ "Diamond distance": [
342
+ reporting.number_to_str(df_g.values[i, 1], precision=5) for i in range(dataset.attrs["num_gates"])
343
+ ],
344
+ }
345
+ )
346
+ U_opt = reporting.phase_opt(X_opt, K_target)
347
+ pauli_coeffs = reporting.compute_sparsest_Pauli_Hamiltonian(U_opt)
348
+
349
+ df_g_rotation = DataFrame(
350
+ np.array(
351
+ [
352
+ [
353
+ reporting.number_to_str(pauli_coeffs[i, j], precision=5)
354
+ for i in range(dataset.attrs["num_gates"])
355
+ ]
356
+ for j in range(dataset.attrs["pdim"] ** 2)
357
+ ]
358
+ ).T
359
+ )
360
+ df_g_rotation.columns = [f"h_%s" % label for label in pauli_labels]
361
+ df_g_final.rename(index=dataset.attrs["gate_labels"], inplace=True)
362
+
363
+ fig_g = dataframe_to_figure(df_g_final, dataset.attrs["gate_labels"])
364
+ fig_rotation = dataframe_to_figure(df_g_rotation, dataset.attrs["gate_labels"])
365
+ return df_g_final, df_g_rotation, fig_g, fig_rotation
366
+
367
+
368
+ def generate_gate_results(
369
+ dataset: xr.Dataset,
370
+ qubit_layout: List[int],
371
+ df_g: DataFrame,
372
+ X_opt: ndarray,
373
+ E_opt: ndarray,
374
+ rho_opt: ndarray,
375
+ max_evals: int = 6,
376
+ ) -> Tuple[DataFrame, DataFrame, Figure, Figure]:
377
+ """
378
+ Produces all result tables for arbitrary Kraus rank estimates and turns them into figures.
379
+
380
+ Args:
381
+ df_g: Pandas DataFrame
382
+ The dataframe with properly formated results
383
+ X_opt: 3D numpy array
384
+ The gate set after gauge optimization
385
+ E_opt: 3D numpy array
386
+ An array containg all the POVM elements as matrices after gauge optimization
387
+ rho_opt: 2D numpy array
388
+ The density matrix after gauge optmization
389
+ max_evals: int
390
+ The maximum number of eigenvalues of the Choi matrices which are returned.
391
+
392
+ Returns:
393
+ df_g_final: Pandas DataFrame
394
+ The dataframe with properly formated results of standard gate errors
395
+ df_g_evals_final Pandas DataFrame
396
+ A dataframe containing eigenvalues of the Choi matrices for all gates
397
+ fig_g: Figure
398
+ A table in Figure format of gate results (fidelities etc.)
399
+ fig_choi: Figure
400
+ A table in Figure format of eigenvalues of the Choi matrices of all gates
401
+
402
+ """
403
+ identifier = BenchmarkObservationIdentifier(qubit_layout).string_identifier
404
+ n_evals = np.min([max_evals, dataset.attrs["pdim"] ** 2])
405
+ X_opt_pp, _, _ = compatibility.std2pp(X_opt, E_opt, rho_opt)
406
+ df_g_evals = reporting.generate_Choi_EV_table(X_opt, n_evals, dataset.attrs["gate_labels"])
407
+
408
+ if dataset.attrs["bootstrap_samples"] > 0:
409
+ X_array, E_array, rho_array, df_g_array, _ = dataset.attrs["results_layout_" + identifier]["bootstrap_data"]
410
+ df_g_array[df_g_array == -1] = np.nan
411
+ percentiles_g_low, percentiles_g_high = np.nanpercentile(df_g_array, [2.5, 97.5], axis=0)
412
+ bootstrap_unitarities = np.array(
413
+ [reporting.unitarities(X_array[i]) for i in range(dataset.attrs["bootstrap_samples"])]
414
+ )
415
+ percentiles_u_low, percentiles_u_high = np.nanpercentile(bootstrap_unitarities, [2.5, 97.5], axis=0)
416
+ X_array_std = [
417
+ compatibility.pp2std(X_array[i], E_array[i], rho_array[i])[0]
418
+ for i in range(dataset.attrs["bootstrap_samples"])
419
+ ]
420
+ bootstrap_evals = np.array(
421
+ [
422
+ reporting.generate_Choi_EV_table(X_array_std[i], n_evals, dataset.attrs["gate_labels"])
423
+ for i in range(dataset.attrs["bootstrap_samples"])
424
+ ]
425
+ )
426
+ percentiles_evals_low, percentiles_evals_high = np.nanpercentile(bootstrap_evals, [2.5, 97.5], axis=0)
427
+ eval_strs = [
428
+ [
429
+ reporting.number_to_str(
430
+ df_g_evals.values[i, j],
431
+ [percentiles_evals_high[i, j], percentiles_evals_low[i, j]],
432
+ precision=5,
433
+ )
434
+ for i in range(dataset.attrs["num_gates"])
435
+ ]
436
+ for j in range(n_evals)
437
+ ]
438
+
439
+ df_g_final = DataFrame(
440
+ {
441
+ r"Avg. gate fidelity": [
442
+ reporting.number_to_str(
443
+ df_g.values[i, 0], [percentiles_g_high[i, 0], percentiles_g_low[i, 0]], precision=5
444
+ )
445
+ for i in range(dataset.attrs["num_gates"])
446
+ ],
447
+ r"Diamond distance": [
448
+ reporting.number_to_str(
449
+ df_g.values[i, 1], [percentiles_g_high[i, 1], percentiles_g_low[i, 1]], precision=5
450
+ )
451
+ for i in range(dataset.attrs["num_gates"])
452
+ ],
453
+ r"Unitarity": [
454
+ reporting.number_to_str(
455
+ reporting.unitarities(X_opt_pp)[i],
456
+ [percentiles_u_high[i], percentiles_u_low[i]],
457
+ precision=5,
458
+ )
459
+ for i in range(dataset.attrs["num_gates"])
460
+ ],
461
+ }
462
+ )
463
+
464
+ else:
465
+ df_g_final = DataFrame(
466
+ {
467
+ "Avg. gate fidelity": [
468
+ reporting.number_to_str(df_g.values[i, 0].copy(), precision=5)
469
+ for i in range(len(dataset.attrs["gate_labels"]))
470
+ ],
471
+ "Diamond distance": [
472
+ reporting.number_to_str(df_g.values[i, 1].copy(), precision=5)
473
+ for i in range(len(dataset.attrs["gate_labels"]))
474
+ ],
475
+ "Unitarity": [
476
+ reporting.number_to_str(reporting.unitarities(X_opt_pp)[i], precision=5)
477
+ for i in range(len(dataset.attrs["gate_labels"]))
478
+ ],
479
+ # "Entanglemen fidelity to depol. channel": [reporting.number_to_str(reporting.eff_depol_params(X_opt_pp)[i], precision=5)
480
+ # for i in range(len(gate_labels))],
481
+ # "Min. spectral distances": [number_to_str(df_g.values[i, 2], precision=5) for i in range(len(gate_labels))]
482
+ }
483
+ )
484
+ eval_strs = [
485
+ [
486
+ reporting.number_to_str(df_g_evals.values[i, j].copy(), precision=5)
487
+ for i in range(dataset.attrs["num_gates"])
488
+ ]
489
+ for j in range(n_evals)
490
+ ]
491
+
492
+ df_g_evals_final = DataFrame(eval_strs).T
493
+ df_g_evals_final.rename(index=dataset.attrs["gate_labels"], inplace=True)
494
+
495
+ fig_g = dataframe_to_figure(df_g_final, dataset.attrs["gate_labels"])
496
+ fig_choi = dataframe_to_figure(df_g_evals_final, dataset.attrs["gate_labels"])
497
+ return df_g_final, df_g_evals_final, fig_g, fig_choi
498
+
499
+
500
+ def generate_basis_labels(pdim: int, basis: Union[str, None] = None) -> List[str]:
501
+ """Generate a list of labels for the Pauli basis or the standard basis
502
+
503
+ Args:
504
+ pdim: int
505
+ Physical dimension
506
+ basis: str
507
+ Which basis the labels correspond to, currently default is standard basis and "Pauli" can be choose
508
+ for Pauli basis labels like "II", "IX", "XX", ...
509
+
510
+ Returns:
511
+ labels: List[str]
512
+ A list of all string combinations for the given dimension and basis
513
+ """
514
+ separator = ""
515
+ if basis == "Pauli":
516
+ pauli_labels_loc = ["I", "X", "Y", "Z"]
517
+ pauli_labels_rep = [pauli_labels_loc for _ in range(int(np.log2(pdim)))]
518
+ labels = [separator.join(map(str, x)) for x in product(*pauli_labels_rep)]
519
+ else:
520
+ std_labels_loc = ["0", "1"]
521
+ std_labels_rep = [std_labels_loc for _ in range(int(np.log2(pdim)))]
522
+ labels = [separator.join(map(str, x)) for x in product(*std_labels_rep)]
523
+
524
+ return labels
525
+
526
+
527
+ def result_str_to_floats(result_str: str, err: str) -> Tuple[float, float]:
528
+ """Converts formated string results from mgst to float (value, uncertainty) pairs
529
+
530
+ Args:
531
+ result_str: str
532
+ The value of a result parameter formated as str
533
+ err: str
534
+ The error interval of the parameters
535
+
536
+ Returns:
537
+ value: float
538
+ The parameter value as floar
539
+ uncertainty: float
540
+ A single uncertainty value
541
+ """
542
+ if err:
543
+ value = float(result_str.split("[")[0])
544
+ rest = result_str.split("[")[1].split(",")
545
+ uncertainty = float(rest[1][:-1]) - float(rest[0])
546
+ return value, uncertainty
547
+ return float(result_str), np.NaN
548
+
549
+
550
+ def pandas_results_to_observations(
551
+ dataset: xr.Dataset, df_g: DataFrame, df_o: DataFrame, identifier: BenchmarkObservationIdentifier
552
+ ) -> List[BenchmarkObservation]:
553
+ """Converts high level GST results from a pandas Dataframe to a simple observation dictionary
554
+
555
+ Args:
556
+ dataset: xarray.Dataset
557
+ A dataset containing counts from the experiment and configurations
558
+ qubit_layout: List[int]
559
+ The list of qubits for the current GST experiment
560
+ df_g: Pandas DataFrame
561
+ The dataframe with properly formated gate results
562
+ df_o: Pandas DataFrame
563
+ The dataframe with properly formated non-gate results like SPAM error measures or fit quality.
564
+ identifier: BenchmarkObservationIdentifier
565
+ An identifier object for the current GST run
566
+
567
+ Returns:
568
+ observation_list: List[BenchmarkObservation]
569
+ List of observations converted from the pandas dataframes
570
+ """
571
+ observation_list: list[BenchmarkObservation] = []
572
+ err = dataset.attrs["bootstrap_samples"] > 0
573
+ for idx, gate_label in enumerate(dataset.attrs["gate_labels"].values()):
574
+ observation_list.extend(
575
+ [
576
+ BenchmarkObservation(
577
+ name=f"{name} - {gate_label}",
578
+ identifier=identifier,
579
+ value=result_str_to_floats(df_g[name].iloc[idx], err)[0],
580
+ uncertainty=result_str_to_floats(df_g[name].iloc[idx], err)[1],
581
+ )
582
+ for name in df_g.columns.tolist()
583
+ ]
584
+ )
585
+ observation_list.extend(
586
+ [
587
+ BenchmarkObservation(
588
+ name=f"{name}",
589
+ identifier=identifier,
590
+ value=result_str_to_floats(df_o[name].iloc[0], err)[0],
591
+ uncertainty=result_str_to_floats(df_o[name].iloc[0], err)[1],
592
+ )
593
+ for name in df_o.columns.tolist()
594
+ ]
595
+ )
596
+ return observation_list
597
+
598
+
599
+ def dataset_counts_to_mgst_format(dataset: xr.Dataset, qubit_layout: List[int]) -> ndarray:
600
+ """Turns the dictionary of outcomes obtained from qiskit backend
601
+ into the format which is used in mGST
602
+
603
+ Args:
604
+ dataset: xarray.Dataset
605
+ A dataset containing counts from the experiment and configurations
606
+ qubit_layout: List[int]
607
+ The list of qubits for the current GST experiment
608
+
609
+ Returns
610
+ -------
611
+ y : numpy array
612
+ 2D array of measurement outcomes for sequences in J;
613
+ Each column contains the outcome probabilities for a fixed sequence
614
+
615
+ """
616
+ num_qubits = len(qubit_layout)
617
+ num_povm = dataset.attrs["num_povm"]
618
+ y_list = []
619
+ for run_index in range(dataset.attrs["num_circuits"]):
620
+ # result = dataset[f"{qubit_layout}_counts_{run_index}"].to_dict()
621
+ result = dataset[f"{qubit_layout}_state_{run_index}"].data.tolist()
622
+ basis_dict = {entry: int("".join([entry[::-1][i] for i in range(num_qubits)][::-1]), 2) for entry in result}
623
+ # Sort by index:
624
+ basis_dict = dict(sorted(basis_dict.items(), key=lambda item: item[1]))
625
+
626
+ counts_normalized = (
627
+ dataset[f"{qubit_layout}_counts_{run_index}"] / dataset[f"{qubit_layout}_counts_{run_index}"].sum()
628
+ )
629
+ row = [counts_normalized.loc[key].data for key in basis_dict]
630
+ # row = [result[key] for key in basis_dict]
631
+ if len(row) < num_povm:
632
+ missing_entries = list(np.arange(num_povm))
633
+ for given_entry in basis_dict.values():
634
+ missing_entries.remove(given_entry)
635
+ for missing_entry in missing_entries:
636
+ row.insert(missing_entry, 0) # 0 measurement outcomes in not recorded entry
637
+ y_list.append(row)
638
+ y = np.array(y_list).T
639
+ return y
640
+
641
+
642
+ def run_mGST(
643
+ dataset: xr.Dataset, y: ndarray
644
+ ) -> tuple[ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray, ndarray]:
645
+ """Wrapper function for mGST algorithm execution which prepares an initialization and sets the alg. parameters
646
+
647
+ Args:
648
+ dataset: xarray.Dataset
649
+ A dataset containing counts from the experiment and configurations
650
+ y: ndarray
651
+ The circuit outcome probabilities as a num_povm x num_circuits array
652
+
653
+ Returns:
654
+ K : ndarray
655
+ Kraus estimate array where each subarray along the first axis contains a set of Kraus operators.
656
+ The second axis enumerates Kraus operators for a gate specified by the first axis.
657
+ X : ndarray
658
+ Superoperator estimate array where reconstructed CPT superoperators in
659
+ standard basis are stacked along the first axis.
660
+ E : ndarray
661
+ Current POVM estimate
662
+ rho : ndarray
663
+ Current initial state estimate
664
+ K_target : ndarray
665
+ Target gate Kraus array where each subarray along the first axis contains a set of Kraus operators.
666
+ The second axis enumerates Kraus operators for a gate specified by the first axis.
667
+ X_target : ndarray
668
+ Target gate superoperator estimate array where reconstructed CPT superoperators in
669
+ standard basis are stacked along the first axis.
670
+ E_target : ndarray
671
+ Target POVM
672
+ rho_target : ndarray
673
+ Target initial state
674
+ """
675
+
676
+ K_target = qiskit_gate_to_operator(dataset.attrs["gate_set"])
677
+ X_target = np.einsum("ijkl,ijnm -> iknlm", K_target, K_target.conj()).reshape(
678
+ (dataset.attrs["num_gates"], dataset.attrs["pdim"] ** 2, dataset.attrs["pdim"] ** 2)
679
+ ) # tensor of superoperators
680
+
681
+ rho_target = (
682
+ np.kron(additional_fns.basis(dataset.attrs["pdim"], 0).T.conj(), additional_fns.basis(dataset.attrs["pdim"], 0))
683
+ .reshape(-1)
684
+ .astype(np.complex128)
685
+ )
686
+
687
+ # Computational basis measurement:
688
+ E_target = np.array(
689
+ [
690
+ np.kron(
691
+ additional_fns.basis(dataset.attrs["pdim"], i).T.conj(), additional_fns.basis(dataset.attrs["pdim"], i)
692
+ ).reshape(-1)
693
+ for i in range(dataset.attrs["pdim"])
694
+ ]
695
+ ).astype(np.complex128)
696
+
697
+ # Run mGST
698
+ if dataset.attrs["from_init"]:
699
+ K_init = additional_fns.perturbed_target_init(X_target, dataset.attrs["rank"])
700
+ init_params = [K_init, E_target, rho_target]
701
+ else:
702
+ init_params = None
703
+
704
+ K, X, E, rho, _ = algorithm.run_mGST(
705
+ y,
706
+ dataset.attrs["J"],
707
+ dataset.attrs["seq_len_list"][-1],
708
+ dataset.attrs["num_gates"],
709
+ dataset.attrs["pdim"] ** 2,
710
+ dataset.attrs["rank"],
711
+ dataset.attrs["num_povm"],
712
+ dataset.attrs["batch_size"],
713
+ dataset.attrs["shots"],
714
+ method=dataset.attrs["opt_method"],
715
+ max_inits=dataset.attrs["max_inits"],
716
+ max_iter=dataset.attrs["max_iterations"][0],
717
+ final_iter=dataset.attrs["max_iterations"][1],
718
+ threshold_multiplier=dataset.attrs["convergence_criteria"][0],
719
+ target_rel_prec=dataset.attrs["convergence_criteria"][1],
720
+ init=init_params,
721
+ testing=False,
722
+ )
723
+
724
+ return K, X, E, rho, K_target, X_target, E_target, rho_target
725
+
726
+
727
+ def mgst_analysis(run: BenchmarkRunResult) -> BenchmarkAnalysisResult:
728
+ """Analysis function for compressive GST
729
+
730
+ Args:
731
+ run: BenchmarkRunResult
732
+ A BenchmarkRunResult instance storing the dataset
733
+ Returns:
734
+ result: BenchmarkAnalysisResult
735
+ An BenchmarkAnalysisResult instance with the updated dataset, as well as plots and observations
736
+ """
737
+ dataset = run.dataset
738
+ pdim = dataset.attrs["pdim"]
739
+ plots = {}
740
+ for i, qubit_layout in enumerate(dataset.attrs["qubit_layouts"]):
741
+ identifier = BenchmarkObservationIdentifier(qubit_layout).string_identifier
742
+
743
+ # Computing circuit outcome probabilities from counts
744
+ y = dataset_counts_to_mgst_format(dataset, qubit_layout)
745
+
746
+ # Main GST reconstruction
747
+ start_timer = perf_counter()
748
+ K, X, E, rho, K_target, X_target, E_target, rho_target = run_mGST(dataset, y)
749
+ main_gst_time = perf_counter() - start_timer
750
+
751
+ # Gauge optimization
752
+ start_timer = perf_counter()
753
+ target_mdl = compatibility.arrays_to_pygsti_model(X_target, E_target, rho_target, basis="std")
754
+ X_opt, E_opt, rho_opt = reporting.gauge_opt(X, E, rho, target_mdl, dataset.attrs[f"gauge_weights"])
755
+ gauge_optimization_time = perf_counter() - start_timer
756
+
757
+ # Quick report
758
+ df_g, _ = reporting.quick_report(
759
+ X_opt, E_opt, rho_opt, dataset.attrs["J"], y, target_mdl, dataset.attrs["gate_labels"]
760
+ )
761
+
762
+ # Gate set in the Pauli basis
763
+ X_opt_pp, _, _ = compatibility.std2pp(X_opt, E_opt, rho_opt)
764
+ X_target_pp, _, _ = compatibility.std2pp(X_target, E_target, rho_target)
765
+
766
+ # Saving
767
+ dataset.attrs["results_layout_" + identifier] = {
768
+ "raw_Kraus_operators": K,
769
+ "raw_gates": X,
770
+ "raw_POVM": E.reshape((dataset.attrs["num_povm"], pdim, pdim)),
771
+ "raw_state": rho.reshape((pdim, pdim)),
772
+ "gauge_opt_gates": X_opt,
773
+ "gauge_opt_gates_Pauli_basis": X_opt_pp,
774
+ "gauge_opt_POVM": E_opt.reshape((dataset.attrs["num_povm"], pdim, pdim)),
775
+ "gauge_opt_state": rho_opt.reshape((pdim, pdim)),
776
+ "main_mGST_time": main_gst_time,
777
+ "gauge_optimization_time": gauge_optimization_time,
778
+ }
779
+
780
+ ### Bootstrap
781
+ if dataset.attrs["bootstrap_samples"] > 0:
782
+ bootstrap_results = bootstrap_errors(dataset, y, K, X, E, rho, target_mdl)
783
+ dataset.attrs["results_layout_" + identifier].update({"bootstrap_data": bootstrap_results})
784
+
785
+ _, df_o_full = reporting.report(
786
+ X_opt, E_opt, rho_opt, dataset.attrs["J"], y, target_mdl, dataset.attrs["gate_labels"]
787
+ )
788
+ df_o_final, fig_o = generate_non_gate_results(dataset, qubit_layout, df_o_full)
789
+
790
+ ### Result table generation and full report
791
+ if dataset.attrs["rank"] == 1:
792
+ df_g_final, df_g_rotation, fig_g, fig_rotation = generate_unit_rank_gate_results(
793
+ dataset, qubit_layout, df_g, X_opt, K_target
794
+ )
795
+ dataset.attrs["results_layout_" + identifier].update({"hamiltonian_parameters": df_g_rotation.to_dict()})
796
+ plots[f"layout_{qubit_layout}_hamiltonian_parameters"] = fig_rotation
797
+ else:
798
+ df_g_final, df_g_evals, fig_g, fig_choi = generate_gate_results(
799
+ dataset, qubit_layout, df_g, X_opt, E_opt, rho_opt
800
+ )
801
+ dataset.attrs["results_layout_" + identifier].update({"choi_evals": df_g_evals.to_dict()})
802
+ plots[f"layout_{qubit_layout}_choi_eigenvalues"] = fig_choi
803
+ plots[f"layout_{qubit_layout}_gate_metrics"] = fig_g
804
+ plots[f"layout_{qubit_layout}_other_metrics"] = fig_o
805
+
806
+ observation_list = pandas_results_to_observations(
807
+ dataset, df_g_final, df_o_final, BenchmarkObservationIdentifier(qubit_layout)
808
+ )
809
+
810
+ dataset.attrs["results_layout_" + identifier].update(
811
+ {"full_metrics": {"Gates": df_g_final.to_dict(), "Outcomes and SPAM": df_o_final.to_dict()}}
812
+ )
813
+
814
+ ### Process matrix plots
815
+ pauli_labels = generate_basis_labels(pdim, basis="Pauli")
816
+ std_labels = generate_basis_labels(pdim)
817
+
818
+ figures = figure_gen.generate_gate_err_pdf(
819
+ "",
820
+ X_opt_pp,
821
+ X_target_pp,
822
+ basis_labels=pauli_labels,
823
+ gate_labels=dataset.attrs["gate_labels"],
824
+ return_fig=True,
825
+ )
826
+ for i, figure in enumerate(figures):
827
+ plots[f"layout_{qubit_layout}_process_matrix_{i}"] = figure
828
+
829
+ plots[f"layout_{qubit_layout}_SPAM_matrices_real"] = figure_gen.generate_spam_err_std_pdf(
830
+ "",
831
+ E_opt.real,
832
+ rho_opt.real,
833
+ E_target.real,
834
+ rho_target.real,
835
+ basis_labels=std_labels,
836
+ title=f"Real part of state and measurement effects in the standard basis",
837
+ return_fig=True,
838
+ )
839
+ plots[f"layout_{qubit_layout}_SPAM_matrices_imag"] = figure_gen.generate_spam_err_std_pdf(
840
+ "",
841
+ E_opt.imag,
842
+ rho_opt.imag,
843
+ E_target.imag,
844
+ rho_target.imag,
845
+ basis_labels=std_labels,
846
+ title=f"Imaginary part of state and measurement effects in the standard basis",
847
+ return_fig=True,
848
+ )
849
+ plt.close("all")
850
+
851
+ return BenchmarkAnalysisResult(dataset=dataset, observations=observation_list, plots=plots)