20
20
Sequence ,
21
21
Tuple ,
22
22
TYPE_CHECKING ,
23
+ Dict ,
23
24
)
24
25
25
26
import numpy as np
26
27
import pandas as pd
27
28
import scipy .optimize
29
+ import scipy .stats
28
30
import sympy
29
31
30
32
from cirq import ops
@@ -71,20 +73,16 @@ def benchmark_2q_xeb_fidelities(
71
73
)
72
74
df = sampled_df .join (simulated_df )
73
75
74
- def _summary_stats (row ):
75
- D = 4 # Two qubits
76
- row ['e_u' ] = np .sum (row ['pure_probs' ] ** 2 )
77
- row ['u_u' ] = np .sum (row ['pure_probs' ]) / D
78
- row ['m_u' ] = np .sum (row ['pure_probs' ] * row ['sampled_probs' ])
79
-
80
- row ['y' ] = row ['m_u' ] - row ['u_u' ]
81
- row ['x' ] = row ['e_u' ] - row ['u_u' ]
82
-
83
- row ['numerator' ] = row ['x' ] * row ['y' ]
84
- row ['denominator' ] = row ['x' ] ** 2
85
- return row
86
-
87
- df = df .apply (_summary_stats , axis = 1 )
76
+ D = 4 # two qubits
77
+ pure_probs = np .array (df ['pure_probs' ].to_list ())
78
+ sampled_probs = np .array (df ['sampled_probs' ].to_list ())
79
+ df ['e_u' ] = np .sum (pure_probs ** 2 , axis = 1 )
80
+ df ['u_u' ] = np .sum (pure_probs , axis = 1 ) / D
81
+ df ['m_u' ] = np .sum (pure_probs * sampled_probs , axis = 1 )
82
+ df ['y' ] = df ['m_u' ] - df ['u_u' ]
83
+ df ['x' ] = df ['e_u' ] - df ['u_u' ]
84
+ df ['numerator' ] = df ['x' ] * df ['y' ]
85
+ df ['denominator' ] = df ['x' ] ** 2
88
86
89
87
def per_cycle_depth (df ):
90
88
"""This function is applied per cycle_depth in the following groupby aggregation."""
@@ -105,9 +103,7 @@ def _try_keep(k):
105
103
f"values for { k } were grouped together: { vals } "
106
104
)
107
105
108
- _try_keep ('q0' )
109
- _try_keep ('q1' )
110
- _try_keep ('pair_name' )
106
+ _try_keep ('pair' )
111
107
return pd .Series (ret )
112
108
113
109
if 'pair_i' in df .columns :
@@ -238,6 +234,26 @@ def parameterize_phased_fsim_circuit(
238
234
)
239
235
240
236
237
+ QPair_T = Tuple ['cirq.Qid' , 'cirq.Qid' ]
238
+
239
+
240
+ @dataclass (frozen = True )
241
+ class XEBCharacterizationResult :
242
+ """The result of `characterize_phased_fsim_parameters_with_xeb`.
243
+
244
+ Attributes:
245
+ optimization_results: A mapping from qubit pair to the raw scipy OptimizeResult object
246
+ final_params: A mapping from qubit pair to a dictionary of (angle_name, angle_value)
247
+ key-value pairs
248
+ fidelities_df: A dataframe containing per-cycle_depth and per-pair fidelities after
249
+ fitting the characterization.
250
+ """
251
+
252
+ optimization_results : Dict [QPair_T , scipy .optimize .OptimizeResult ]
253
+ final_params : Dict [QPair_T , Dict [str , float ]]
254
+ fidelities_df : pd .DataFrame
255
+
256
+
241
257
def characterize_phased_fsim_parameters_with_xeb (
242
258
sampled_df : pd .DataFrame ,
243
259
parameterized_circuits : List ['cirq.Circuit' ],
@@ -248,7 +264,7 @@ def characterize_phased_fsim_parameters_with_xeb(
248
264
fatol : float = 1e-3 ,
249
265
verbose : bool = True ,
250
266
pool : Optional ['multiprocessing.pool.Pool' ] = None ,
251
- ):
267
+ ) -> XEBCharacterizationResult :
252
268
"""Run a classical optimization to fit phased fsim parameters to experimental data, and
253
269
thereby characterize PhasedFSim-like gates.
254
270
@@ -268,6 +284,7 @@ def characterize_phased_fsim_parameters_with_xeb(
268
284
verbose: Whether to print progress updates.
269
285
pool: An optional multiprocessing pool to execute circuit simulations in parallel.
270
286
"""
287
+ (pair ,) = sampled_df ['pair' ].unique ()
271
288
initial_simplex , names = phased_fsim_options .get_initial_simplex_and_names (
272
289
initial_simplex_step_size = initial_simplex_step_size
273
290
)
@@ -289,10 +306,209 @@ def _mean_infidelity(angles):
289
306
print ("Loss: {:7.3g}" .format (loss ), flush = True )
290
307
return loss
291
308
292
- res = scipy .optimize .minimize (
309
+ optimization_result = scipy .optimize .minimize (
293
310
_mean_infidelity ,
294
311
x0 = x0 ,
295
312
options = {'initial_simplex' : initial_simplex , 'xatol' : xatol , 'fatol' : fatol },
296
313
method = 'nelder-mead' ,
297
314
)
298
- return res
315
+
316
+ final_params = dict (zip (names , optimization_result .x ))
317
+ fidelities_df = benchmark_2q_xeb_fidelities (
318
+ sampled_df , parameterized_circuits , cycle_depths , param_resolver = final_params
319
+ )
320
+ return XEBCharacterizationResult (
321
+ optimization_results = {pair : optimization_result },
322
+ final_params = {pair : final_params },
323
+ fidelities_df = fidelities_df ,
324
+ )
325
+
326
+
327
+ class _CharacterizePhasedFsimParametersWithXebClosure :
328
+ """A closure object to wrap `characterize_phased_fsim_parameters_with_xeb` for use in
329
+ multiprocessing."""
330
+
331
+ def __init__ (
332
+ self ,
333
+ parameterized_circuits : List ['cirq.Circuit' ],
334
+ cycle_depths : Sequence [int ],
335
+ phased_fsim_options : XEBPhasedFSimCharacterizationOptions ,
336
+ initial_simplex_step_size : float = 0.1 ,
337
+ xatol : float = 1e-3 ,
338
+ fatol : float = 1e-3 ,
339
+ ):
340
+ self .parameterized_circuits = parameterized_circuits
341
+ self .cycle_depths = cycle_depths
342
+ self .phased_fsim_options = phased_fsim_options
343
+ self .initial_simplex_step_size = initial_simplex_step_size
344
+ self .xatol = xatol
345
+ self .fatol = fatol
346
+
347
+ def __call__ (self , sampled_df ) -> XEBCharacterizationResult :
348
+ return characterize_phased_fsim_parameters_with_xeb (
349
+ sampled_df = sampled_df ,
350
+ parameterized_circuits = self .parameterized_circuits ,
351
+ cycle_depths = self .cycle_depths ,
352
+ phased_fsim_options = self .phased_fsim_options ,
353
+ initial_simplex_step_size = self .initial_simplex_step_size ,
354
+ xatol = self .xatol ,
355
+ fatol = self .fatol ,
356
+ verbose = False ,
357
+ pool = None ,
358
+ )
359
+
360
+
361
+ def characterize_phased_fsim_parameters_with_xeb_by_pair (
362
+ sampled_df : pd .DataFrame ,
363
+ parameterized_circuits : List ['cirq.Circuit' ],
364
+ cycle_depths : Sequence [int ],
365
+ phased_fsim_options : XEBPhasedFSimCharacterizationOptions ,
366
+ initial_simplex_step_size : float = 0.1 ,
367
+ xatol : float = 1e-3 ,
368
+ fatol : float = 1e-3 ,
369
+ pool : Optional ['multiprocessing.pool.Pool' ] = None ,
370
+ ) -> XEBCharacterizationResult :
371
+ """Run a classical optimization to fit phased fsim parameters to experimental data, and
372
+ thereby characterize PhasedFSim-like gates grouped by pairs.
373
+
374
+ This is appropriate if you have run parallel XEB on multiple pairs of qubits.
375
+
376
+ Args:
377
+ sampled_df: The DataFrame of sampled two-qubit probability distributions returned
378
+ from `sample_2q_xeb_circuits`.
379
+ parameterized_circuits: The circuits corresponding to those sampled in `sampled_df`,
380
+ but with some gates parameterized, likely by using `parameterize_phased_fsim_circuit`.
381
+ cycle_depths: The depths at which circuits were truncated.
382
+ phased_fsim_options: A set of options that controls the classical optimization loop
383
+ for characterizing the parameterized gates.
384
+ initial_simplex_step_size: Set the size of the initial simplex for Nelder-Mead.
385
+ xatol: The `xatol` argument for Nelder-Mead. This is the absolute error for convergence
386
+ in the parameters.
387
+ fatol: The `fatol` argument for Nelder-Mead. This is the absolute error for convergence
388
+ in the function evaluation.
389
+ pool: An optional multiprocessing pool to execute pair optimization in parallel. Each
390
+ optimization (and the simulations therein) runs serially.
391
+ """
392
+ pairs = sampled_df ['pair' ].unique ()
393
+ closure = _CharacterizePhasedFsimParametersWithXebClosure (
394
+ parameterized_circuits = parameterized_circuits ,
395
+ cycle_depths = cycle_depths ,
396
+ phased_fsim_options = phased_fsim_options ,
397
+ initial_simplex_step_size = initial_simplex_step_size ,
398
+ xatol = xatol ,
399
+ fatol = fatol ,
400
+ )
401
+ subselected_dfs = [sampled_df [sampled_df ['pair' ] == pair ] for pair in pairs ]
402
+ if pool is not None :
403
+ results = pool .map (closure , subselected_dfs )
404
+ else :
405
+ results = [closure (df ) for df in subselected_dfs ]
406
+
407
+ optimization_results = {}
408
+ all_final_params = {}
409
+ fid_dfs = []
410
+ for result in results :
411
+ optimization_results .update (result .optimization_results )
412
+ all_final_params .update (result .final_params )
413
+ fid_dfs .append (result .fidelities_df )
414
+
415
+ return XEBCharacterizationResult (
416
+ optimization_results = optimization_results ,
417
+ final_params = all_final_params ,
418
+ fidelities_df = pd .concat (fid_dfs ),
419
+ )
420
+
421
+
422
+ def exponential_decay (cycle_depths : np .ndarray , A : float , layer_fid : float ) -> np .ndarray :
423
+ """An exponential decay for fitting."""
424
+ return A * layer_fid ** cycle_depths
425
+
426
+
427
+ def _fit_exponential_decay (cycle_depths : np .ndarray , fidelities : np .ndarray ) -> Tuple [float , float ]:
428
+ """Fit an exponential model fidelities = A * layer_fid**x using nonlinear least squares."""
429
+ cycle_depths = np .asarray (cycle_depths )
430
+ fidelities = np .asarray (fidelities )
431
+
432
+ # Get initial guess by linear least squares with logarithm of model
433
+ positives = fidelities > 0
434
+ cycle_depths_pos = cycle_depths [positives ]
435
+ log_fidelities = np .log (fidelities [positives ])
436
+ slope , intercept , _ , _ , _ = scipy .stats .linregress (cycle_depths_pos , log_fidelities )
437
+ layer_fid_0 = np .clip (np .exp (slope ), 0 , 1 )
438
+ A_0 = np .clip (np .exp (intercept ), 0 , 1 )
439
+
440
+ (A , layer_fid ), _ = scipy .optimize .curve_fit (
441
+ exponential_decay , cycle_depths , fidelities , p0 = (A_0 , layer_fid_0 ), bounds = ((0 , 0 ), (1 , 1 ))
442
+ )
443
+ return A , layer_fid
444
+
445
+
446
+ def _one_unique (df , name , default ):
447
+ """Helper function to assert that there's one unique value in a column and return it."""
448
+ if name not in df .columns :
449
+ return default
450
+ vals = df [name ].unique ()
451
+ assert len (vals ) == 1 , name
452
+ return vals [0 ]
453
+
454
+
455
+ def fit_exponential_decays (fidelities_df : pd .DataFrame ) -> pd .DataFrame :
456
+ """Fit exponential decay curves to a fidelities DataFrame.
457
+
458
+ Args:
459
+ fidelities_df: A DataFrame that is the result of `benchmark_2q_xeb_fidelities`. It
460
+ may contain results for multiple pairs of qubits identified by the "pair" column.
461
+ Each pair will be fit separately. At minimum, this dataframe must contain
462
+ "cycle_depth", "fidelity", and "pair" columns.
463
+
464
+ Returns:
465
+ A new, aggregated dataframe with index given by (pair, layer_i, pair_i); columns
466
+ for the fit parameters "A" and "layer_fid"; and nested "cycles_depths" and "fidelities"
467
+ lists (now grouped by pair).
468
+ """
469
+ records = []
470
+ for pair in fidelities_df ['pair' ].unique ():
471
+ f1 = fidelities_df [fidelities_df ['pair' ] == pair ]
472
+ A , layer_fid = _fit_exponential_decay (f1 ['cycle_depth' ], f1 ['fidelity' ])
473
+ record = {
474
+ 'pair' : pair ,
475
+ 'A' : A ,
476
+ 'layer_fid' : layer_fid ,
477
+ 'cycle_depths' : f1 ['cycle_depth' ].values ,
478
+ 'fidelities' : f1 ['fidelity' ].values ,
479
+ 'layer_i' : _one_unique (f1 , 'layer_i' , default = 0 ),
480
+ 'pair_i' : _one_unique (f1 , 'pair_i' , default = 0 ),
481
+ }
482
+ records .append (record )
483
+ return pd .DataFrame (records ).set_index (['pair' , 'layer_i' , 'pair_i' ])
484
+
485
+
486
+ def before_and_after_characterization (
487
+ fidelities_df_0 : pd .DataFrame , characterization_result : XEBCharacterizationResult
488
+ ) -> pd .DataFrame :
489
+ """A convenience function for horizontally stacking results pre- and post- characterization
490
+ optimization.
491
+
492
+ Args:
493
+ fidelities_df_0: A dataframe (before fitting), likely resulting from
494
+ `benchmark_2q_xeb_fidelities`.
495
+ characterization_result: The result of running a characterization. This contains the
496
+ second fidelities dataframe as well as the new parameters.
497
+
498
+ Returns:
499
+ A joined dataframe with original column names suffixed by "_0" and characterized
500
+ column names suffixed by "_c".
501
+ """
502
+ fit_decay_df_0 = fit_exponential_decays (fidelities_df_0 )
503
+ fit_decay_df_c = fit_exponential_decays (characterization_result .fidelities_df )
504
+
505
+ joined_df = fit_decay_df_0 .join (fit_decay_df_c , how = 'outer' , lsuffix = '_0' , rsuffix = '_c' )
506
+ joined_df ['characterized_angles' ] = [
507
+ characterization_result .final_params [pair ] for pair , _ , _ in joined_df .index
508
+ ]
509
+ angle_names = list (list (characterization_result .final_params .values ())[0 ].keys ())
510
+ for angle_name in angle_names :
511
+ joined_df [angle_name ] = [
512
+ characterization_result .final_params [pair ][angle_name ] for pair , _ , _ in joined_df .index
513
+ ]
514
+ return joined_df
0 commit comments