|
| 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.""" |
| 15 | +from abc import abstractmethod |
| 16 | +from dataclasses import dataclass |
| 17 | +from typing import ( |
| 18 | + List, |
| 19 | + Optional, |
| 20 | + Sequence, |
| 21 | + Tuple, |
| 22 | + TYPE_CHECKING, |
| 23 | +) |
| 24 | + |
| 25 | +import numpy as np |
| 26 | +import pandas as pd |
| 27 | +import scipy.optimize |
| 28 | +import sympy |
| 29 | + |
| 30 | +from cirq import ops |
| 31 | +from cirq.circuits import Circuit |
| 32 | +from cirq.experiments.xeb_simulation import simulate_2q_xeb_circuits |
| 33 | + |
| 34 | +if TYPE_CHECKING: |
| 35 | + import cirq |
| 36 | + import multiprocessing |
| 37 | + |
| 38 | +THETA_SYMBOL, ZETA_SYMBOL, CHI_SYMBOL, GAMMA_SYMBOL, PHI_SYMBOL = sympy.symbols( |
| 39 | + 'theta zeta chi gamma phi' |
| 40 | +) |
| 41 | +SQRT_ISWAP = ops.ISWAP ** 0.5 |
| 42 | + |
| 43 | + |
| 44 | +def benchmark_2q_xeb_fidelities( |
| 45 | + sampled_df: pd.DataFrame, |
| 46 | + circuits: Sequence['cirq.Circuit'], |
| 47 | + cycle_depths: Sequence[int], |
| 48 | + param_resolver: 'cirq.ParamResolverOrSimilarType' = None, |
| 49 | + pool: Optional['multiprocessing.pool.Pool'] = None, |
| 50 | +): |
| 51 | + """Simulate and benchmark two-qubit XEB circuits. |
| 52 | +
|
| 53 | + This uses the estimator from |
| 54 | + `cirq.experiments.fidelity_estimation.least_squares_xeb_fidelity_from_expectations`, but |
| 55 | + adapted for use on pandas DataFrames for efficient vectorized operation. |
| 56 | +
|
| 57 | + Args: |
| 58 | + sampled_df: The sampled results to benchmark. This is likely produced by a call to |
| 59 | + `sample_2q_xeb_circuits`. |
| 60 | + circuits: The library of circuits corresponding to the sampled results in `sampled_df`. |
| 61 | + cycle_depths: The sequence of cycle depths to simulate the circuits. |
| 62 | + param_resolver: If circuits contain parameters, resolve according to this ParamResolver |
| 63 | + prior to simulation |
| 64 | + pool: If provided, execute the simulations in parallel. |
| 65 | +
|
| 66 | + Returns: |
| 67 | + A DataFrame with columns 'cycle_depth' and 'fidelity'. |
| 68 | + """ |
| 69 | + simulated_df = simulate_2q_xeb_circuits( |
| 70 | + circuits=circuits, cycle_depths=cycle_depths, param_resolver=param_resolver, pool=pool |
| 71 | + ) |
| 72 | + df = sampled_df.join(simulated_df) |
| 73 | + |
| 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) |
| 88 | + |
| 89 | + def per_cycle_depth(df): |
| 90 | + """This function is applied per cycle_depth in the following groupby aggregation.""" |
| 91 | + fid_lsq = df['numerator'].sum() / df['denominator'].sum() |
| 92 | + ret = {'fidelity': fid_lsq} |
| 93 | + |
| 94 | + def _try_keep(k): |
| 95 | + """If all the values for a key `k` are the same in this group, we can keep it.""" |
| 96 | + if k not in df.columns: |
| 97 | + return # coverage: ignore |
| 98 | + vals = df[k].unique() |
| 99 | + if len(vals) == 1: |
| 100 | + ret[k] = vals[0] |
| 101 | + else: |
| 102 | + # coverage: ignore |
| 103 | + raise AssertionError( |
| 104 | + f"When computing per-cycle-depth fidelity, multiple " |
| 105 | + f"values for {k} were grouped together: {vals}" |
| 106 | + ) |
| 107 | + |
| 108 | + _try_keep('q0') |
| 109 | + _try_keep('q1') |
| 110 | + _try_keep('pair_name') |
| 111 | + return pd.Series(ret) |
| 112 | + |
| 113 | + if 'pair_i' in df.columns: |
| 114 | + groupby_names = ['layer_i', 'pair_i', 'cycle_depth'] |
| 115 | + else: |
| 116 | + groupby_names = ['cycle_depth'] |
| 117 | + |
| 118 | + return df.reset_index().groupby(groupby_names).apply(per_cycle_depth).reset_index() |
| 119 | + |
| 120 | + |
| 121 | +# mypy issue: https://github.com/python/mypy/issues/5374 |
| 122 | +@dataclass(frozen=True) # type: ignore |
| 123 | +class XEBPhasedFSimCharacterizationOptions: |
| 124 | + """Options for calibrating a PhasedFSim-like gate using XEB. |
| 125 | +
|
| 126 | + You may want to use more specific subclasses like `SqrtISwapXEBOptions` |
| 127 | + which have sensible defaults. |
| 128 | +
|
| 129 | + Attributes: |
| 130 | + characterize_theta: Whether to characterize θ angle. |
| 131 | + characterize_zeta: Whether to characterize ζ angle. |
| 132 | + characterize_chi: Whether to characterize χ angle. |
| 133 | + characterize_gamma: Whether to characterize γ angle. |
| 134 | + characterize_phi: Whether to characterize φ angle. |
| 135 | + theta_default: The initial or default value to assume for the θ angle. |
| 136 | + zeta_default: The initial or default value to assume for the ζ angle. |
| 137 | + chi_default: The initial or default value to assume for the χ angle. |
| 138 | + gamma_default: The initial or default value to assume for the γ angle. |
| 139 | + phi_default: The initial or default value to assume for the φ angle. |
| 140 | + """ |
| 141 | + |
| 142 | + characterize_theta: bool = True |
| 143 | + characterize_zeta: bool = True |
| 144 | + characterize_chi: bool = True |
| 145 | + characterize_gamma: bool = True |
| 146 | + characterize_phi: bool = True |
| 147 | + |
| 148 | + theta_default: float = 0 |
| 149 | + zeta_default: float = 0 |
| 150 | + chi_default: float = 0 |
| 151 | + gamma_default: float = 0 |
| 152 | + phi_default: float = 0 |
| 153 | + |
| 154 | + @staticmethod |
| 155 | + @abstractmethod |
| 156 | + def should_parameterize(op: 'cirq.Operation') -> bool: |
| 157 | + """Whether to replace `op` with a parameterized version.""" |
| 158 | + |
| 159 | + def get_initial_simplex_and_names( |
| 160 | + self, initial_simplex_step_size: float = 0.1 |
| 161 | + ) -> Tuple[np.ndarray, List[str]]: |
| 162 | + """Get an initial simplex and parameter names for the optimization implied by these options. |
| 163 | +
|
| 164 | + The initial simplex initiates the Nelder-Mead optimization parameter. We |
| 165 | + use the standard simplex of `x0 + s*basis_vec` where x0 is given by the |
| 166 | + `xxx_default` attributes, s is `initial_simplex_step_size` and `basis_vec` |
| 167 | + is a one-hot encoded vector for each parameter for which the `parameterize_xxx` |
| 168 | + attribute is True. |
| 169 | +
|
| 170 | + We also return a list of parameter names so the Cirq `param_resovler` |
| 171 | + can be accurately constructed during optimization. |
| 172 | + """ |
| 173 | + x0 = [] |
| 174 | + names = [] |
| 175 | + if self.characterize_theta: |
| 176 | + x0 += [self.theta_default] |
| 177 | + names += [THETA_SYMBOL.name] |
| 178 | + if self.characterize_zeta: |
| 179 | + x0 += [self.zeta_default] |
| 180 | + names += [ZETA_SYMBOL.name] |
| 181 | + if self.characterize_chi: |
| 182 | + x0 += [self.chi_default] |
| 183 | + names += [CHI_SYMBOL.name] |
| 184 | + if self.characterize_gamma: |
| 185 | + x0 += [self.gamma_default] |
| 186 | + names += [GAMMA_SYMBOL.name] |
| 187 | + if self.characterize_phi: |
| 188 | + x0 += [self.phi_default] |
| 189 | + names += [PHI_SYMBOL.name] |
| 190 | + |
| 191 | + x0 = np.asarray(x0) |
| 192 | + n_param = len(x0) |
| 193 | + initial_simplex = [x0] |
| 194 | + for i in range(n_param): |
| 195 | + basis_vec = np.eye(1, n_param, i)[0] |
| 196 | + initial_simplex += [x0 + initial_simplex_step_size * basis_vec] |
| 197 | + initial_simplex = np.asarray(initial_simplex) |
| 198 | + |
| 199 | + return initial_simplex, names |
| 200 | + |
| 201 | + |
| 202 | +@dataclass(frozen=True) |
| 203 | +class SqrtISwapXEBOptions(XEBPhasedFSimCharacterizationOptions): |
| 204 | + """Options for calibrating a sqrt(ISWAP) gate using XEB. |
| 205 | +
|
| 206 | + As such, the default for theta is changed to -pi/4 and the parameterization |
| 207 | + predicate seeks out sqrt(ISWAP) gates. |
| 208 | + """ |
| 209 | + |
| 210 | + theta_default: float = -np.pi / 4 |
| 211 | + |
| 212 | + @staticmethod |
| 213 | + def should_parameterize(op: 'cirq.Operation') -> bool: |
| 214 | + return op.gate == SQRT_ISWAP |
| 215 | + |
| 216 | + |
| 217 | +def parameterize_phased_fsim_circuit( |
| 218 | + circuit: 'cirq.Circuit', |
| 219 | + phased_fsim_options: XEBPhasedFSimCharacterizationOptions, |
| 220 | +) -> 'cirq.Circuit': |
| 221 | + """Parameterize PhasedFSim-like gates in a given circuit according to |
| 222 | + `phased_fsim_options`. |
| 223 | + """ |
| 224 | + options = phased_fsim_options |
| 225 | + theta = THETA_SYMBOL if options.characterize_theta else options.theta_default |
| 226 | + zeta = ZETA_SYMBOL if options.characterize_zeta else options.zeta_default |
| 227 | + chi = CHI_SYMBOL if options.characterize_chi else options.chi_default |
| 228 | + gamma = GAMMA_SYMBOL if options.characterize_gamma else options.gamma_default |
| 229 | + phi = PHI_SYMBOL if options.characterize_phi else options.phi_default |
| 230 | + |
| 231 | + fsim_gate = ops.PhasedFSimGate(theta=theta, zeta=zeta, chi=chi, gamma=gamma, phi=phi) |
| 232 | + return Circuit( |
| 233 | + ops.Moment( |
| 234 | + fsim_gate.on(*op.qubits) if options.should_parameterize(op) else op |
| 235 | + for op in moment.operations |
| 236 | + ) |
| 237 | + for moment in circuit.moments |
| 238 | + ) |
| 239 | + |
| 240 | + |
| 241 | +def characterize_phased_fsim_parameters_with_xeb( |
| 242 | + sampled_df: pd.DataFrame, |
| 243 | + parameterized_circuits: List['cirq.Circuit'], |
| 244 | + cycle_depths: Sequence[int], |
| 245 | + phased_fsim_options: XEBPhasedFSimCharacterizationOptions, |
| 246 | + initial_simplex_step_size: float = 0.1, |
| 247 | + xatol: float = 1e-3, |
| 248 | + fatol: float = 1e-3, |
| 249 | + verbose: bool = True, |
| 250 | + pool: Optional['multiprocessing.pool.Pool'] = None, |
| 251 | +): |
| 252 | + """Run a classical optimization to fit phased fsim parameters to experimental data, and |
| 253 | + thereby characterize PhasedFSim-like gates. |
| 254 | +
|
| 255 | + Args: |
| 256 | + sampled_df: The DataFrame of sampled two-qubit probability distributions returned |
| 257 | + from `sample_2q_xeb_circuits`. |
| 258 | + parameterized_circuits: The circuits corresponding to those sampled in `sampled_df`, |
| 259 | + but with some gates parameterized, likely by using `parameterize_phased_fsim_circuit`. |
| 260 | + cycle_depths: The depths at which circuits were truncated. |
| 261 | + phased_fsim_options: A set of options that controls the classical optimization loop |
| 262 | + for characterizing the parameterized gates. |
| 263 | + initial_simplex_step_size: Set the size of the initial simplex for Nelder-Mead. |
| 264 | + xatol: The `xatol` argument for Nelder-Mead. This is the absolute error for convergence |
| 265 | + in the parameters. |
| 266 | + fatol: The `fatol` argument for Nelder-Mead. This is the absolute error for convergence |
| 267 | + in the function evaluation. |
| 268 | + verbose: Whether to print progress updates. |
| 269 | + pool: An optional multiprocessing pool to execute circuit simulations in parallel. |
| 270 | + """ |
| 271 | + initial_simplex, names = phased_fsim_options.get_initial_simplex_and_names( |
| 272 | + initial_simplex_step_size=initial_simplex_step_size |
| 273 | + ) |
| 274 | + x0 = initial_simplex[0] |
| 275 | + |
| 276 | + def _mean_infidelity(angles): |
| 277 | + params = dict(zip(names, angles)) |
| 278 | + if verbose: |
| 279 | + params_str = '' |
| 280 | + for name, val in params.items(): |
| 281 | + params_str += f'{name:5s} = {val:7.3g} ' |
| 282 | + print("Simulating with {}".format(params_str)) |
| 283 | + fids = benchmark_2q_xeb_fidelities( |
| 284 | + sampled_df, parameterized_circuits, cycle_depths, param_resolver=params, pool=pool |
| 285 | + ) |
| 286 | + |
| 287 | + loss = 1 - fids['fidelity'].mean() |
| 288 | + if verbose: |
| 289 | + print("Loss: {:7.3g}".format(loss), flush=True) |
| 290 | + return loss |
| 291 | + |
| 292 | + res = scipy.optimize.minimize( |
| 293 | + _mean_infidelity, |
| 294 | + x0=x0, |
| 295 | + options={'initial_simplex': initial_simplex, 'xatol': xatol, 'fatol': fatol}, |
| 296 | + method='nelder-mead', |
| 297 | + ) |
| 298 | + return res |
0 commit comments