Skip to content

Commit 0cf2cad

Browse files
committed
[XEB] Optimize/characterize by pair
1 parent 1590ff1 commit 0cf2cad

9 files changed

+629
-72
lines changed

cirq/experiments/random_quantum_circuit_generation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ def get_random_combinations_for_device(
344344
combinations_by_layer = []
345345
for layer in pattern:
346346
pairs = sorted(_get_active_pairs(device_graph, layer))
347+
if len(pairs) == 0:
348+
continue
349+
347350
combinations = rs.randint(0, n_library_circuits, size=(n_combinations, len(pairs)))
348351
combinations_by_layer.append(
349352
CircuitLibraryCombination(layer=layer, combinations=combinations, pairs=pairs)

cirq/experiments/random_quantum_circuit_generation_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,18 @@ def test_get_random_combinations_for_device():
136136
assert cirq.experiments.HALF_GRID_STAGGERED_PATTERN[i] == comb.layer
137137

138138

139+
def test_get_random_combinations_for_small_device():
140+
graph = _gridqubits_to_graph_device(cirq.GridQubit.rect(3, 1))
141+
n_combinations = 4
142+
combinations = get_random_combinations_for_device(
143+
n_library_circuits=3,
144+
n_combinations=n_combinations,
145+
device_graph=graph,
146+
random_state=99,
147+
)
148+
assert len(combinations) == 2 # 3x1 device only fits two layers
149+
150+
139151
def _cz_with_adjacent_z_rotations(
140152
a: cirq.GridQubit, b: cirq.GridQubit, prng: np.random.RandomState
141153
):

cirq/experiments/xeb_fitting.py

Lines changed: 236 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
Sequence,
2121
Tuple,
2222
TYPE_CHECKING,
23+
Dict,
2324
)
2425

2526
import numpy as np
2627
import pandas as pd
2728
import scipy.optimize
29+
import scipy.stats
2830
import sympy
2931

3032
from cirq import ops
@@ -71,20 +73,16 @@ def benchmark_2q_xeb_fidelities(
7173
)
7274
df = sampled_df.join(simulated_df)
7375

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
8886

8987
def per_cycle_depth(df):
9088
"""This function is applied per cycle_depth in the following groupby aggregation."""
@@ -105,9 +103,7 @@ def _try_keep(k):
105103
f"values for {k} were grouped together: {vals}"
106104
)
107105

108-
_try_keep('q0')
109-
_try_keep('q1')
110-
_try_keep('pair_name')
106+
_try_keep('pair')
111107
return pd.Series(ret)
112108

113109
if 'pair_i' in df.columns:
@@ -238,6 +234,26 @@ def parameterize_phased_fsim_circuit(
238234
)
239235

240236

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+
241257
def characterize_phased_fsim_parameters_with_xeb(
242258
sampled_df: pd.DataFrame,
243259
parameterized_circuits: List['cirq.Circuit'],
@@ -248,7 +264,7 @@ def characterize_phased_fsim_parameters_with_xeb(
248264
fatol: float = 1e-3,
249265
verbose: bool = True,
250266
pool: Optional['multiprocessing.pool.Pool'] = None,
251-
):
267+
) -> XEBCharacterizationResult:
252268
"""Run a classical optimization to fit phased fsim parameters to experimental data, and
253269
thereby characterize PhasedFSim-like gates.
254270
@@ -268,6 +284,7 @@ def characterize_phased_fsim_parameters_with_xeb(
268284
verbose: Whether to print progress updates.
269285
pool: An optional multiprocessing pool to execute circuit simulations in parallel.
270286
"""
287+
(pair,) = sampled_df['pair'].unique()
271288
initial_simplex, names = phased_fsim_options.get_initial_simplex_and_names(
272289
initial_simplex_step_size=initial_simplex_step_size
273290
)
@@ -289,10 +306,209 @@ def _mean_infidelity(angles):
289306
print("Loss: {:7.3g}".format(loss), flush=True)
290307
return loss
291308

292-
res = scipy.optimize.minimize(
309+
optimization_result = scipy.optimize.minimize(
293310
_mean_infidelity,
294311
x0=x0,
295312
options={'initial_simplex': initial_simplex, 'xatol': xatol, 'fatol': fatol},
296313
method='nelder-mead',
297314
)
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

Comments
 (0)