Skip to content

Commit 1590ff1

Browse files
authored
[XEB] Split into three files (#3794)
This PR is purely moving things around (on top of #3760) with one exception noted inline. - `random_quantum_circuit_generation` -- left alone; this is a well-scoped file - `xeb_sampling` - functionality related to sampling random circuits efficiently (for xeb) - `xeb_simulation` - functionality related to simulating 2-qubit circuits at various depths (for xeb) efficiently using multiprocessing - `xeb_fitting` - functionality related to using xeb to characterize gates by fitting parameters.
1 parent bbf6245 commit 1590ff1

11 files changed

+1319
-1148
lines changed

cirq/experiments/fidelity_estimation.py

Lines changed: 1 addition & 705 deletions
Large diffs are not rendered by default.

cirq/experiments/fidelity_estimation_test.py

Lines changed: 1 addition & 436 deletions
Large diffs are not rendered by default.

cirq/experiments/xeb_fitting.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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

Comments
 (0)