1# Copyright 2021 The Cirq 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# https://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"""Estimation of fidelity associated with experimental circuit executions.""" 15import dataclasses 16from abc import abstractmethod, ABC 17from typing import ( 18 List, 19 Optional, 20 Sequence, 21 Tuple, 22 TYPE_CHECKING, 23 Dict, 24 Iterable, 25) 26 27import numpy as np 28import pandas as pd 29import scipy.optimize 30import scipy.stats 31import sympy 32from cirq import ops, protocols 33from cirq.circuits import Circuit 34from cirq.experiments.xeb_simulation import simulate_2q_xeb_circuits 35 36if TYPE_CHECKING: 37 import cirq 38 import multiprocessing 39 40THETA_SYMBOL, ZETA_SYMBOL, CHI_SYMBOL, GAMMA_SYMBOL, PHI_SYMBOL = sympy.symbols( 41 'theta zeta chi gamma phi' 42) 43 44# TODO(#3388) Add documentation for Raises. 45# pylint: disable=missing-raises-doc 46def benchmark_2q_xeb_fidelities( 47 sampled_df: pd.DataFrame, 48 circuits: Sequence['cirq.Circuit'], 49 cycle_depths: Optional[Sequence[int]] = None, 50 param_resolver: 'cirq.ParamResolverOrSimilarType' = None, 51 pool: Optional['multiprocessing.pool.Pool'] = None, 52) -> pd.DataFrame: 53 """Simulate and benchmark two-qubit XEB circuits. 54 55 This uses the estimator from 56 `cirq.experiments.fidelity_estimation.least_squares_xeb_fidelity_from_expectations`, but 57 adapted for use on pandas DataFrames for efficient vectorized operation. 58 59 Args: 60 sampled_df: The sampled results to benchmark. This is likely produced by a call to 61 `sample_2q_xeb_circuits`. 62 circuits: The library of circuits corresponding to the sampled results in `sampled_df`. 63 cycle_depths: The sequence of cycle depths to benchmark the circuits. If not provided, 64 we use the cycle depths found in `sampled_df`. All requested `cycle_depths` must be 65 present in `sampled_df`. 66 param_resolver: If circuits contain parameters, resolve according to this ParamResolver 67 prior to simulation 68 pool: If provided, execute the simulations in parallel. 69 70 Returns: 71 A DataFrame with columns 'cycle_depth' and 'fidelity'. 72 """ 73 sampled_cycle_depths = ( 74 sampled_df.index.get_level_values('cycle_depth').drop_duplicates().sort_values() 75 ) 76 if cycle_depths is not None: 77 if len(cycle_depths) == 0: 78 raise ValueError("`cycle_depths` should be a non-empty array_like") 79 not_in_sampled = np.setdiff1d(cycle_depths, sampled_cycle_depths) 80 if len(not_in_sampled) > 0: 81 raise ValueError( 82 f"The `cycle_depths` provided include some not " 83 f"available in `sampled_df`: {not_in_sampled}" 84 ) 85 sim_cycle_depths = cycle_depths 86 else: 87 sim_cycle_depths = sampled_cycle_depths 88 simulated_df = simulate_2q_xeb_circuits( 89 circuits=circuits, cycle_depths=sim_cycle_depths, param_resolver=param_resolver, pool=pool 90 ) 91 # Join the `pure_probs` onto `sampled_df`. By using 'inner', we let 92 # the `cycle_depths` argument to this function control what cycle depths are benchmarked. 93 df = sampled_df.join(simulated_df, how='inner').reset_index() 94 95 D = 4 # two qubits 96 pure_probs = np.array(df['pure_probs'].to_list()) 97 sampled_probs = np.array(df['sampled_probs'].to_list()) 98 df['e_u'] = np.sum(pure_probs ** 2, axis=1) 99 df['u_u'] = np.sum(pure_probs, axis=1) / D 100 df['m_u'] = np.sum(pure_probs * sampled_probs, axis=1) 101 df['y'] = df['m_u'] - df['u_u'] 102 df['x'] = df['e_u'] - df['u_u'] 103 df['numerator'] = df['x'] * df['y'] 104 df['denominator'] = df['x'] ** 2 105 106 def per_cycle_depth(df): 107 """This function is applied per cycle_depth in the following groupby aggregation.""" 108 fid_lsq = df['numerator'].sum() / df['denominator'].sum() 109 ret = {'fidelity': fid_lsq} 110 111 def _try_keep(k): 112 """If all the values for a key `k` are the same in this group, we can keep it.""" 113 if k not in df.columns: 114 return # coverage: ignore 115 vals = df[k].unique() 116 if len(vals) == 1: 117 ret[k] = vals[0] 118 else: 119 # coverage: ignore 120 raise AssertionError( 121 f"When computing per-cycle-depth fidelity, multiple " 122 f"values for {k} were grouped together: {vals}" 123 ) 124 125 _try_keep('pair') 126 return pd.Series(ret) 127 128 if 'pair_i' in df.columns: 129 groupby_names = ['layer_i', 'pair_i', 'cycle_depth'] 130 else: 131 groupby_names = ['cycle_depth'] 132 133 return df.groupby(groupby_names).apply(per_cycle_depth).reset_index() 134 135 136# pylint: enable=missing-raises-doc 137class XEBCharacterizationOptions(ABC): 138 @staticmethod 139 @abstractmethod 140 def should_parameterize(op: 'cirq.Operation') -> bool: 141 """Whether to replace `op` with a parameterized version.""" 142 143 @abstractmethod 144 def get_parameterized_gate(self) -> 'cirq.Gate': 145 """The parameterized gate to use.""" 146 147 @abstractmethod 148 def get_initial_simplex_and_names( 149 self, initial_simplex_step_size: float = 0.1 150 ) -> Tuple[np.ndarray, List[str]]: 151 """Return an initial Nelder-Mead simplex and the names for each parameter.""" 152 153 154def phased_fsim_angles_from_gate(gate: 'cirq.Gate') -> Dict[str, float]: 155 """For a given gate, return a dictionary mapping '{angle}_default' to its noiseless value 156 for the five PhasedFSim angles.""" 157 defaults = { 158 'theta_default': 0.0, 159 'zeta_default': 0.0, 160 'chi_default': 0.0, 161 'gamma_default': 0.0, 162 'phi_default': 0.0, 163 } 164 if gate == ops.SQRT_ISWAP: 165 defaults['theta_default'] = -np.pi / 4 166 return defaults 167 if gate == ops.SQRT_ISWAP_INV: 168 defaults['theta_default'] = np.pi / 4 169 return defaults 170 if isinstance(gate, ops.FSimGate): 171 defaults['theta_default'] = gate.theta 172 defaults['phi_default'] = gate.phi 173 return defaults 174 if isinstance(gate, ops.PhasedFSimGate): 175 return { 176 'theta_default': gate.theta, 177 'zeta_default': gate.zeta, 178 'chi_default': gate.chi, 179 'gamma_default': gate.gamma, 180 'phi_default': gate.phi, 181 } 182 183 raise ValueError(f"Unknown default angles for {gate}.") 184 185 186@dataclasses.dataclass(frozen=True) 187class XEBPhasedFSimCharacterizationOptions(XEBCharacterizationOptions): 188 """Options for calibrating a PhasedFSim-like gate using XEB. 189 190 You may want to use more specific subclasses like `SqrtISwapXEBOptions` 191 which have sensible defaults. 192 193 Attributes: 194 characterize_theta: Whether to characterize θ angle. 195 characterize_zeta: Whether to characterize ζ angle. 196 characterize_chi: Whether to characterize χ angle. 197 characterize_gamma: Whether to characterize γ angle. 198 characterize_phi: Whether to characterize φ angle. 199 theta_default: The initial or default value to assume for the θ angle. 200 zeta_default: The initial or default value to assume for the ζ angle. 201 chi_default: The initial or default value to assume for the χ angle. 202 gamma_default: The initial or default value to assume for the γ angle. 203 phi_default: The initial or default value to assume for the φ angle. 204 """ 205 206 characterize_theta: bool = True 207 characterize_zeta: bool = True 208 characterize_chi: bool = True 209 characterize_gamma: bool = True 210 characterize_phi: bool = True 211 212 theta_default: Optional[float] = None 213 zeta_default: Optional[float] = None 214 chi_default: Optional[float] = None 215 gamma_default: Optional[float] = None 216 phi_default: Optional[float] = None 217 218 def _iter_angles(self) -> Iterable[Tuple[bool, Optional[float], 'sympy.Symbol']]: 219 yield from ( 220 (self.characterize_theta, self.theta_default, THETA_SYMBOL), 221 (self.characterize_zeta, self.zeta_default, ZETA_SYMBOL), 222 (self.characterize_chi, self.chi_default, CHI_SYMBOL), 223 (self.characterize_gamma, self.gamma_default, GAMMA_SYMBOL), 224 (self.characterize_phi, self.phi_default, PHI_SYMBOL), 225 ) 226 227 def _iter_angles_for_characterization(self) -> Iterable[Tuple[Optional[float], 'sympy.Symbol']]: 228 yield from ( 229 (default, symbol) 230 for characterize, default, symbol in self._iter_angles() 231 if characterize 232 ) 233 234 def get_initial_simplex_and_names( 235 self, initial_simplex_step_size: float = 0.1 236 ) -> Tuple[np.ndarray, List[str]]: 237 """Get an initial simplex and parameter names for the optimization implied by these options. 238 239 The initial simplex initiates the Nelder-Mead optimization parameter. We 240 use the standard simplex of `x0 + s*basis_vec` where x0 is given by the 241 `xxx_default` attributes, s is `initial_simplex_step_size` and `basis_vec` 242 is a one-hot encoded vector for each parameter for which the `parameterize_xxx` 243 attribute is True. 244 245 We also return a list of parameter names so the Cirq `param_resovler` 246 can be accurately constructed during optimization. 247 """ 248 x0 = [] 249 names = [] 250 251 for default, symbol in self._iter_angles_for_characterization(): 252 if default is None: 253 raise ValueError(f'{symbol.name}_default was not set.') 254 x0.append(default) 255 names.append(symbol.name) 256 257 x0 = np.asarray(x0) 258 n_param = len(x0) 259 initial_simplex = [x0] 260 for i in range(n_param): 261 basis_vec = np.eye(1, n_param, i)[0] 262 initial_simplex += [x0 + initial_simplex_step_size * basis_vec] 263 initial_simplex = np.asarray(initial_simplex) 264 265 return initial_simplex, names 266 267 def get_parameterized_gate(self): 268 theta = THETA_SYMBOL if self.characterize_theta else self.theta_default 269 zeta = ZETA_SYMBOL if self.characterize_zeta else self.zeta_default 270 chi = CHI_SYMBOL if self.characterize_chi else self.chi_default 271 gamma = GAMMA_SYMBOL if self.characterize_gamma else self.gamma_default 272 phi = PHI_SYMBOL if self.characterize_phi else self.phi_default 273 return ops.PhasedFSimGate(theta=theta, zeta=zeta, chi=chi, gamma=gamma, phi=phi) 274 275 @staticmethod 276 def should_parameterize(op: 'cirq.Operation') -> bool: 277 return isinstance(op.gate, (ops.PhasedFSimGate, ops.ISwapPowGate, ops.FSimGate)) 278 279 def defaults_set(self) -> bool: 280 """Whether the default angles are set. 281 282 This only considers angles where characterize_{angle} is True. If all such angles have 283 {angle}_default set to a value, this returns True. If none of the defaults are set, 284 this returns False. If some defaults are set, we raise an exception. 285 """ 286 defaults_set = [default is not None for _, default, _ in self._iter_angles()] 287 if any(defaults_set): 288 if all(defaults_set): 289 return True 290 291 problems = [ 292 symbol.name for _, default, symbol in self._iter_angles() if default is None 293 ] 294 raise ValueError(f"Some angles are set, but values for {problems} are not.") 295 return False 296 297 def with_defaults_from_gate( 298 self, gate: 'cirq.Gate', gate_to_angles_func=phased_fsim_angles_from_gate 299 ): 300 """A new Options class with {angle}_defaults inferred from `gate`. 301 302 This keeps the same settings for the characterize_{angle} booleans, but will disregard 303 any current {angle}_default values. 304 """ 305 return XEBPhasedFSimCharacterizationOptions( 306 characterize_theta=self.characterize_theta, 307 characterize_zeta=self.characterize_zeta, 308 characterize_chi=self.characterize_chi, 309 characterize_gamma=self.characterize_gamma, 310 characterize_phi=self.characterize_phi, 311 **gate_to_angles_func(gate), 312 ) 313 314 def _json_dict_(self): 315 return protocols.dataclass_json_dict(self) 316 317 318def SqrtISwapXEBOptions(*args, **kwargs): 319 """Options for calibrating a sqrt(ISWAP) gate using XEB.""" 320 return XEBPhasedFSimCharacterizationOptions(*args, **kwargs).with_defaults_from_gate( 321 ops.SQRT_ISWAP 322 ) 323 324 325def parameterize_circuit( 326 circuit: 'cirq.Circuit', 327 options: XEBCharacterizationOptions, 328) -> 'cirq.Circuit': 329 """Parameterize PhasedFSim-like gates in a given circuit according to 330 `phased_fsim_options`. 331 """ 332 gate = options.get_parameterized_gate() 333 return Circuit( 334 ops.Moment( 335 gate.on(*op.qubits) if options.should_parameterize(op) else op 336 for op in moment.operations 337 ) 338 for moment in circuit.moments 339 ) 340 341 342QPair_T = Tuple['cirq.Qid', 'cirq.Qid'] 343 344 345@dataclasses.dataclass(frozen=True) 346class XEBCharacterizationResult: 347 """The result of `characterize_phased_fsim_parameters_with_xeb`. 348 349 Attributes: 350 optimization_results: A mapping from qubit pair to the raw scipy OptimizeResult object 351 final_params: A mapping from qubit pair to a dictionary of (angle_name, angle_value) 352 key-value pairs 353 fidelities_df: A dataframe containing per-cycle_depth and per-pair fidelities after 354 fitting the characterization. 355 """ 356 357 optimization_results: Dict[QPair_T, scipy.optimize.OptimizeResult] 358 final_params: Dict[QPair_T, Dict[str, float]] 359 fidelities_df: pd.DataFrame 360 361 362def characterize_phased_fsim_parameters_with_xeb( 363 sampled_df: pd.DataFrame, 364 parameterized_circuits: List['cirq.Circuit'], 365 cycle_depths: Sequence[int], 366 options: XEBCharacterizationOptions, 367 initial_simplex_step_size: float = 0.1, 368 xatol: float = 1e-3, 369 fatol: float = 1e-3, 370 verbose: bool = True, 371 pool: Optional['multiprocessing.pool.Pool'] = None, 372) -> XEBCharacterizationResult: 373 """Run a classical optimization to fit phased fsim parameters to experimental data, and 374 thereby characterize PhasedFSim-like gates. 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_circuit`. 381 cycle_depths: The depths at which circuits were truncated. 382 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 verbose: Whether to print progress updates. 390 pool: An optional multiprocessing pool to execute circuit simulations in parallel. 391 """ 392 (pair,) = sampled_df['pair'].unique() 393 initial_simplex, names = options.get_initial_simplex_and_names( 394 initial_simplex_step_size=initial_simplex_step_size 395 ) 396 x0 = initial_simplex[0] 397 398 def _mean_infidelity(angles): 399 params = dict(zip(names, angles)) 400 if verbose: 401 params_str = '' 402 for name, val in params.items(): 403 params_str += f'{name:5s} = {val:7.3g} ' 404 print(f"Simulating with {params_str}") 405 fids = benchmark_2q_xeb_fidelities( 406 sampled_df, parameterized_circuits, cycle_depths, param_resolver=params, pool=pool 407 ) 408 409 loss = 1 - fids['fidelity'].mean() 410 if verbose: 411 print(f"Loss: {loss:7.3g}", flush=True) 412 return loss 413 414 optimization_result = scipy.optimize.minimize( 415 _mean_infidelity, 416 x0=x0, 417 options={ 418 'initial_simplex': initial_simplex, 419 'xatol': xatol, 420 'fatol': fatol, 421 }, 422 method='nelder-mead', 423 ) 424 425 final_params = dict(zip(names, optimization_result.x)) 426 fidelities_df = benchmark_2q_xeb_fidelities( 427 sampled_df, parameterized_circuits, cycle_depths, param_resolver=final_params 428 ) 429 return XEBCharacterizationResult( 430 optimization_results={pair: optimization_result}, 431 final_params={pair: final_params}, 432 fidelities_df=fidelities_df, 433 ) 434 435 436@dataclasses.dataclass(frozen=True) 437class _CharacterizePhasedFsimParametersWithXebClosure: 438 """A closure object to wrap `characterize_phased_fsim_parameters_with_xeb` for use in 439 multiprocessing.""" 440 441 parameterized_circuits: List['cirq.Circuit'] 442 cycle_depths: Sequence[int] 443 options: XEBCharacterizationOptions 444 initial_simplex_step_size: float = 0.1 445 xatol: float = 1e-3 446 fatol: float = 1e-3 447 448 def __call__(self, sampled_df) -> XEBCharacterizationResult: 449 return characterize_phased_fsim_parameters_with_xeb( 450 sampled_df=sampled_df, 451 parameterized_circuits=self.parameterized_circuits, 452 cycle_depths=self.cycle_depths, 453 options=self.options, 454 initial_simplex_step_size=self.initial_simplex_step_size, 455 xatol=self.xatol, 456 fatol=self.fatol, 457 verbose=False, 458 pool=None, 459 ) 460 461 462def characterize_phased_fsim_parameters_with_xeb_by_pair( 463 sampled_df: pd.DataFrame, 464 parameterized_circuits: List['cirq.Circuit'], 465 cycle_depths: Sequence[int], 466 options: XEBCharacterizationOptions, 467 initial_simplex_step_size: float = 0.1, 468 xatol: float = 1e-3, 469 fatol: float = 1e-3, 470 pool: Optional['multiprocessing.pool.Pool'] = None, 471) -> XEBCharacterizationResult: 472 """Run a classical optimization to fit phased fsim parameters to experimental data, and 473 thereby characterize PhasedFSim-like gates grouped by pairs. 474 475 This is appropriate if you have run parallel XEB on multiple pairs of qubits. 476 477 The optimization is done per-pair. If you have the same pair in e.g. two different 478 layers the characterization optimization will lump the data together. This is in contrast 479 with the benchmarking functionality, which will always index on `(layer_i, pair_i, pair)`. 480 481 Args: 482 sampled_df: The DataFrame of sampled two-qubit probability distributions returned 483 from `sample_2q_xeb_circuits`. 484 parameterized_circuits: The circuits corresponding to those sampled in `sampled_df`, 485 but with some gates parameterized, likely by using `parameterize_circuit`. 486 cycle_depths: The depths at which circuits were truncated. 487 options: A set of options that controls the classical optimization loop 488 for characterizing the parameterized gates. 489 initial_simplex_step_size: Set the size of the initial simplex for Nelder-Mead. 490 xatol: The `xatol` argument for Nelder-Mead. This is the absolute error for convergence 491 in the parameters. 492 fatol: The `fatol` argument for Nelder-Mead. This is the absolute error for convergence 493 in the function evaluation. 494 pool: An optional multiprocessing pool to execute pair optimization in parallel. Each 495 optimization (and the simulations therein) runs serially. 496 """ 497 pairs = sampled_df['pair'].unique() 498 closure = _CharacterizePhasedFsimParametersWithXebClosure( 499 parameterized_circuits=parameterized_circuits, 500 cycle_depths=cycle_depths, 501 options=options, 502 initial_simplex_step_size=initial_simplex_step_size, 503 xatol=xatol, 504 fatol=fatol, 505 ) 506 subselected_dfs = [sampled_df[sampled_df['pair'] == pair] for pair in pairs] 507 if pool is not None: 508 results = pool.map(closure, subselected_dfs) 509 else: 510 results = [closure(df) for df in subselected_dfs] 511 512 optimization_results = {} 513 all_final_params = {} 514 fid_dfs = [] 515 for result in results: 516 optimization_results.update(result.optimization_results) 517 all_final_params.update(result.final_params) 518 fid_dfs.append(result.fidelities_df) 519 520 return XEBCharacterizationResult( 521 optimization_results=optimization_results, 522 final_params=all_final_params, 523 fidelities_df=pd.concat(fid_dfs), 524 ) 525 526 527def exponential_decay(cycle_depths: np.ndarray, a: float, layer_fid: float) -> np.ndarray: 528 """An exponential decay for fitting. 529 530 This computes `a * layer_fid**cycle_depths` 531 532 Args: 533 cycle_depths: The various depths at which fidelity was estimated. This is the independent 534 variable in the exponential function. 535 a: A scale parameter in the exponential function. 536 layer_fid: The base of the exponent in the exponential function. 537 """ 538 return a * layer_fid ** cycle_depths 539 540 541def _fit_exponential_decay( 542 cycle_depths: np.ndarray, fidelities: np.ndarray 543) -> Tuple[float, float, float, float]: 544 """Fit an exponential model fidelity = a * layer_fid**x using nonlinear least squares. 545 546 This uses `exponential_decay` as the function to fit with parameters `a` and `layer_fid`. 547 548 Args: 549 cycle_depths: The various depths at which fidelity was estimated. Each element is `x` 550 in the fit expression. 551 fidelities: The estimated fidelities for each cycle depth. Each element is `fidelity` 552 in the fit expression. 553 554 Returns: 555 a: The first fit parameter that scales the exponential function, perhaps accounting for 556 state prep and measurement (SPAM) error. 557 layer_fid: The second fit parameters which serves as the base of the exponential. 558 a_std: The standard deviation of the `a` parameter estimate. 559 layer_fid_std: The standard deviation of the `layer_fid` parameter estimate. 560 """ 561 cycle_depths = np.asarray(cycle_depths) 562 fidelities = np.asarray(fidelities) 563 564 # Get initial guess by linear least squares with logarithm of model. 565 # This only works for positive fidelities. We use numpy fancy indexing 566 # with `positives` (an ndarray of bools). 567 positives = fidelities > 0 568 if np.sum(positives) <= 1: 569 # The sum of the boolean array is the number of `True` entries. 570 # For one or fewer positive values, we cannot perform the linear fit. 571 return 0, 0, np.inf, np.inf 572 cycle_depths_pos = cycle_depths[positives] 573 log_fidelities = np.log(fidelities[positives]) 574 slope, intercept, _, _, _ = scipy.stats.linregress(cycle_depths_pos, log_fidelities) 575 layer_fid_0 = np.clip(np.exp(slope), 0, 1) 576 a_0 = np.clip(np.exp(intercept), 0, 1) 577 578 try: 579 (a, layer_fid), pcov = scipy.optimize.curve_fit( 580 exponential_decay, 581 cycle_depths, 582 fidelities, 583 p0=(a_0, layer_fid_0), 584 bounds=((0, 0), (1, 1)), 585 ) 586 except ValueError: # coverage: ignore 587 # coverage: ignore 588 return 0, 0, np.inf, np.inf 589 590 a_std, layer_fid_std = np.sqrt(np.diag(pcov)) 591 return a, layer_fid, a_std, layer_fid_std 592 593 594def _one_unique(df, name, default): 595 """Helper function to assert that there's one unique value in a column and return it.""" 596 if name not in df.columns: 597 return default 598 vals = df[name].unique() 599 assert len(vals) == 1, name 600 return vals[0] 601 602 603def fit_exponential_decays(fidelities_df: pd.DataFrame) -> pd.DataFrame: 604 """Fit exponential decay curves to a fidelities DataFrame. 605 606 Args: 607 fidelities_df: A DataFrame that is the result of `benchmark_2q_xeb_fidelities`. It 608 may contain results for multiple pairs of qubits identified by the "pair" column. 609 Each pair will be fit separately. At minimum, this dataframe must contain 610 "cycle_depth", "fidelity", and "pair" columns. 611 612 Returns: 613 A new, aggregated dataframe with index given by (pair, layer_i, pair_i); columns 614 for the fit parameters "a" and "layer_fid"; and nested "cycles_depths" and "fidelities" 615 lists (now grouped by pair). 616 """ 617 618 def _per_pair(f1): 619 a, layer_fid, a_std, layer_fid_std = _fit_exponential_decay( 620 f1['cycle_depth'], f1['fidelity'] 621 ) 622 record = { 623 'a': a, 624 'layer_fid': layer_fid, 625 'cycle_depths': f1['cycle_depth'].values, 626 'fidelities': f1['fidelity'].values, 627 'a_std': a_std, 628 'layer_fid_std': layer_fid_std, 629 } 630 return pd.Series(record) 631 632 if 'layer_i' in fidelities_df.columns: 633 groupby = ['layer_i', 'pair_i', 'pair'] 634 else: 635 groupby = ['pair'] 636 return fidelities_df.groupby(groupby).apply(_per_pair) 637 638 639def before_and_after_characterization( 640 fidelities_df_0: pd.DataFrame, characterization_result: XEBCharacterizationResult 641) -> pd.DataFrame: 642 """A convenience function for horizontally stacking results pre- and post- characterization 643 optimization. 644 645 Args: 646 fidelities_df_0: A dataframe (before fitting), likely resulting from 647 `benchmark_2q_xeb_fidelities`. 648 characterization_result: The result of running a characterization. This contains the 649 second fidelities dataframe as well as the new parameters. 650 651 Returns: 652 A joined dataframe with original column names suffixed by "_0" and characterized 653 column names suffixed by "_c". 654 """ 655 fit_decay_df_0 = fit_exponential_decays(fidelities_df_0) 656 fit_decay_df_c = fit_exponential_decays(characterization_result.fidelities_df) 657 658 joined_df = fit_decay_df_0.join(fit_decay_df_c, how='outer', lsuffix='_0', rsuffix='_c') 659 # Remove (layer_i, pair_i) from the index. While we keep this for `fit_exponential_decays` 660 # so the same pair can be benchmarked in different contexts, the multi-pair characterization 661 # function only keys on the pair identity. This can be seen acutely by the 662 # `characterization_result.final_params` dictionary being keyed only by the pair. 663 joined_df = joined_df.reset_index().set_index('pair') 664 665 joined_df['characterized_angles'] = [ 666 characterization_result.final_params[pair] for pair in joined_df.index 667 ] 668 # Take any `final_params` (for any pair). We just need the angle names. 669 fp, *_ = characterization_result.final_params.values() 670 for angle_name in fp.keys(): 671 joined_df[angle_name] = [ 672 characterization_result.final_params[pair][angle_name] for pair in joined_df.index 673 ] 674 return joined_df 675