Skip to content

Commit 59be462

Browse files
Create workflow for Z-phase calibration (#6728)
This calibration workflow is created for excitation preserving 2-qubit gates and assumes an error model that can be described with small random z-rotations ``` 0: ───Rz(a)───two_qubit_gate───Rz(c)─── │ 1: ───Rz(b)───two_qubit_gate───Rz(d)─── ``` for some small angles a, b, c, and d. --- when the error model doesn't apply the workflow may give absured numbers (e.g. fidilities $\notin [0, 1]$). The fidilities can be slightly outside the $[0, 1]$ interval because it's a statistical estimate as can be seen in the following figure which compares the estimated fidelity of a CZ surrounded by random z rotations before and after calibration ![image](https://github.com/user-attachments/assets/40e7cedb-179b-416b-8796-94d1da44012b) test notebook: https://colab.sandbox.google.com/drive/10gZ5dggYKH_xSsCJFpg__GakxvoaZDIi
1 parent 2c914ce commit 59be462

File tree

5 files changed

+512
-6
lines changed

5 files changed

+512
-6
lines changed

cirq-core/cirq/experiments/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,9 @@
8282
parallel_two_qubit_xeb as parallel_two_qubit_xeb,
8383
run_rb_and_xeb as run_rb_and_xeb,
8484
)
85+
86+
87+
from cirq.experiments.z_phase_calibration import (
88+
z_phase_calibration_workflow as z_phase_calibration_workflow,
89+
calibrate_z_phases as calibrate_z_phases,
90+
)

cirq-core/cirq/experiments/two_qubit_xeb.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from cirq._compat import cached_method
3939

4040
if TYPE_CHECKING:
41+
import multiprocessing
4142
import cirq
4243

4344

@@ -358,6 +359,7 @@ def parallel_xeb_workflow(
358359
cycle_depths: Sequence[int] = (5, 25, 50, 100, 200, 300),
359360
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
360361
ax: Optional[plt.Axes] = None,
362+
pool: Optional['multiprocessing.pool.Pool'] = None,
361363
**plot_kwargs,
362364
) -> Tuple[pd.DataFrame, Sequence['cirq.Circuit'], pd.DataFrame]:
363365
"""A utility method that runs the full XEB workflow.
@@ -373,6 +375,7 @@ def parallel_xeb_workflow(
373375
random_state: The random state to use.
374376
ax: the plt.Axes to plot the device layout on. If not given,
375377
no plot is created.
378+
pool: An optional multiprocessing pool.
376379
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
377380
378381
Returns:
@@ -426,7 +429,7 @@ def parallel_xeb_workflow(
426429
)
427430

428431
fids = benchmark_2q_xeb_fidelities(
429-
sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths
432+
sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths, pool=pool
430433
)
431434

432435
return fids, circuit_library, sampled_df

cirq-core/cirq/experiments/xeb_fitting.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ def benchmark_2q_xeb_fidelities(
9595
df['e_u'] = np.sum(pure_probs**2, axis=1)
9696
df['u_u'] = np.sum(pure_probs, axis=1) / D
9797
df['m_u'] = np.sum(pure_probs * sampled_probs, axis=1)
98+
# Var[m_u] = Var[sum p(x) * p_sampled(x)]
99+
# = sum p(x)^2 Var[p_sampled(x)]
100+
# = sum p(x)^2 p(x) (1 - p(x))
101+
# = sum p(x)^3 (1 - p(x))
102+
df['var_m_u'] = np.sum(pure_probs**3 * (1 - pure_probs), axis=1)
98103
df['y'] = df['m_u'] - df['u_u']
99104
df['x'] = df['e_u'] - df['u_u']
100105
df['numerator'] = df['x'] * df['y']
@@ -103,7 +108,11 @@ def benchmark_2q_xeb_fidelities(
103108
def per_cycle_depth(df):
104109
"""This function is applied per cycle_depth in the following groupby aggregation."""
105110
fid_lsq = df['numerator'].sum() / df['denominator'].sum()
106-
ret = {'fidelity': fid_lsq}
111+
# Note: both df['denominator'] and df['x'] are constants.
112+
# Var[f] = Var[df['numerator']] / (sum df['denominator'])^2
113+
# = sum (df['x']^2 * df['var_m_u']) / (sum df['denominator'])^2
114+
var_fid = (df['var_m_u'] * df['x'] ** 2).sum() / df['denominator'].sum() ** 2
115+
ret = {'fidelity': fid_lsq, 'fidelity_variance': var_fid}
107116

108117
def _try_keep(k):
109118
"""If all the values for a key `k` are the same in this group, we can keep it."""
@@ -385,16 +394,21 @@ def SqrtISwapXEBOptions(*args, **kwargs):
385394

386395

387396
def parameterize_circuit(
388-
circuit: 'cirq.Circuit', options: XEBCharacterizationOptions
397+
circuit: 'cirq.Circuit',
398+
options: XEBCharacterizationOptions,
399+
target_gatefamily: Optional[ops.GateFamily] = None,
389400
) -> 'cirq.Circuit':
390401
"""Parameterize PhasedFSim-like gates in a given circuit according to
391402
`phased_fsim_options`.
392403
"""
404+
if isinstance(target_gatefamily, ops.GateFamily):
405+
should_parameterize = lambda op: op in target_gatefamily or options.should_parameterize(op)
406+
else:
407+
should_parameterize = options.should_parameterize
393408
gate = options.get_parameterized_gate()
394409
return circuits.Circuit(
395410
circuits.Moment(
396-
gate.on(*op.qubits) if options.should_parameterize(op) else op
397-
for op in moment.operations
411+
gate.on(*op.qubits) if should_parameterize(op) else op for op in moment.operations
398412
)
399413
for moment in circuit.moments
400414
)
@@ -667,13 +681,16 @@ def _per_pair(f1):
667681
a, layer_fid, a_std, layer_fid_std = _fit_exponential_decay(
668682
f1['cycle_depth'], f1['fidelity']
669683
)
684+
fidelity_variance = 0
685+
if 'fidelity_variance' in f1:
686+
fidelity_variance = f1['fidelity_variance'].values
670687
record = {
671688
'a': a,
672689
'layer_fid': layer_fid,
673690
'cycle_depths': f1['cycle_depth'].values,
674691
'fidelities': f1['fidelity'].values,
675692
'a_std': a_std,
676-
'layer_fid_std': layer_fid_std,
693+
'layer_fid_std': np.sqrt(layer_fid_std**2 + fidelity_variance),
677694
}
678695
return pd.Series(record)
679696

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# Copyright 2024 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+
15+
"""Provides a method to do z-phase calibration for excitation-preserving gates."""
16+
from typing import Union, Optional, Sequence, Tuple, Dict, TYPE_CHECKING
17+
import multiprocessing
18+
import multiprocessing.pool
19+
20+
import matplotlib.pyplot as plt
21+
import numpy as np
22+
23+
from cirq.experiments import xeb_fitting
24+
from cirq.experiments.two_qubit_xeb import parallel_xeb_workflow
25+
from cirq import ops
26+
27+
if TYPE_CHECKING:
28+
import cirq
29+
import pandas as pd
30+
31+
32+
def z_phase_calibration_workflow(
33+
sampler: 'cirq.Sampler',
34+
qubits: Optional[Sequence['cirq.GridQubit']] = None,
35+
two_qubit_gate: 'cirq.Gate' = ops.CZ,
36+
options: Optional[xeb_fitting.XEBPhasedFSimCharacterizationOptions] = None,
37+
n_repetitions: int = 10**4,
38+
n_combinations: int = 10,
39+
n_circuits: int = 20,
40+
cycle_depths: Sequence[int] = tuple(np.arange(3, 100, 20)),
41+
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
42+
atol: float = 1e-3,
43+
num_workers_or_pool: Union[int, 'multiprocessing.pool.Pool'] = -1,
44+
) -> Tuple[xeb_fitting.XEBCharacterizationResult, 'pd.DataFrame']:
45+
"""Perform z-phase calibration for excitation-preserving gates.
46+
47+
For a given excitation-preserving two-qubit gate we assume an error model that can be described
48+
using Z-rotations:
49+
0: ───Rz(a)───two_qubit_gate───Rz(c)───
50+
51+
1: ───Rz(b)───two_qubit_gate───Rz(d)───
52+
for some angles a, b, c, and d.
53+
54+
Since the two-qubit gate is a excitation-preserving-gate, it can be represented by an FSimGate
55+
and the effect of rotations turns it into a PhasedFSimGate. Using XEB-data we find the
56+
PhasedFSimGate parameters that minimize the infidelity of the gate.
57+
58+
References:
59+
- https://arxiv.org/abs/2001.08343
60+
- https://arxiv.org/abs/2010.07965
61+
- https://arxiv.org/abs/1910.11333
62+
63+
Args:
64+
sampler: The quantum engine or simulator to run the circuits.
65+
qubits: Qubits to use. If none, use all qubits on the sampler's device.
66+
two_qubit_gate: The entangling gate to use.
67+
options: The XEB-fitting options. If None, calibrate only the three phase angles
68+
(chi, gamma, zeta) using the representation of a two-qubit gate as an FSimGate
69+
for the initial guess.
70+
n_repetitions: The number of repetitions to use.
71+
n_combinations: The number of combinations to generate.
72+
n_circuits: The number of circuits to generate.
73+
cycle_depths: The cycle depths to use.
74+
random_state: The random state to use.
75+
atol: Absolute tolerance to be used by the minimizer.
76+
num_workers_or_pool: An optional multi-processing pool or number of workers.
77+
A zero value means no multiprocessing.
78+
A positive integer value will create a pool with the given number of workers.
79+
A negative value will create pool with maximum number of workers.
80+
Returns:
81+
- An `XEBCharacterizationResult` object that contains the calibration result.
82+
- A `pd.DataFrame` comparing the before and after fidelities.
83+
"""
84+
85+
pool: Optional['multiprocessing.pool.Pool'] = None
86+
local_pool = False
87+
if isinstance(num_workers_or_pool, multiprocessing.pool.Pool):
88+
pool = num_workers_or_pool # pragma: no cover
89+
elif num_workers_or_pool != 0:
90+
pool = multiprocessing.Pool(num_workers_or_pool if num_workers_or_pool > 0 else None)
91+
local_pool = True
92+
93+
fids_df_0, circuits, sampled_df = parallel_xeb_workflow(
94+
sampler=sampler,
95+
qubits=qubits,
96+
entangling_gate=two_qubit_gate,
97+
n_repetitions=n_repetitions,
98+
cycle_depths=cycle_depths,
99+
n_circuits=n_circuits,
100+
n_combinations=n_combinations,
101+
random_state=random_state,
102+
pool=pool,
103+
)
104+
105+
if options is None:
106+
options = xeb_fitting.XEBPhasedFSimCharacterizationOptions(
107+
characterize_chi=True,
108+
characterize_gamma=True,
109+
characterize_zeta=True,
110+
characterize_theta=False,
111+
characterize_phi=False,
112+
).with_defaults_from_gate(two_qubit_gate)
113+
114+
p_circuits = [
115+
xeb_fitting.parameterize_circuit(circuit, options, ops.GateFamily(two_qubit_gate))
116+
for circuit in circuits
117+
]
118+
119+
result = xeb_fitting.characterize_phased_fsim_parameters_with_xeb_by_pair(
120+
sampled_df=sampled_df,
121+
parameterized_circuits=p_circuits,
122+
cycle_depths=cycle_depths,
123+
options=options,
124+
fatol=atol,
125+
xatol=atol,
126+
pool=pool,
127+
)
128+
129+
before_after = xeb_fitting.before_and_after_characterization(
130+
fids_df_0, characterization_result=result
131+
)
132+
133+
if local_pool:
134+
assert isinstance(pool, multiprocessing.pool.Pool)
135+
pool.close()
136+
return result, before_after
137+
138+
139+
def calibrate_z_phases(
140+
sampler: 'cirq.Sampler',
141+
qubits: Optional[Sequence['cirq.GridQubit']] = None,
142+
two_qubit_gate: 'cirq.Gate' = ops.CZ,
143+
options: Optional[xeb_fitting.XEBPhasedFSimCharacterizationOptions] = None,
144+
n_repetitions: int = 10**4,
145+
n_combinations: int = 10,
146+
n_circuits: int = 20,
147+
cycle_depths: Sequence[int] = tuple(np.arange(3, 100, 20)),
148+
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
149+
atol: float = 1e-3,
150+
num_workers_or_pool: Union[int, 'multiprocessing.pool.Pool'] = -1,
151+
) -> Dict[Tuple['cirq.Qid', 'cirq.Qid'], 'cirq.PhasedFSimGate']:
152+
"""Perform z-phase calibration for excitation-preserving gates.
153+
154+
For a given excitation-preserving two-qubit gate we assume an error model that can be described
155+
using Z-rotations:
156+
0: ───Rz(a)───two_qubit_gate───Rz(c)───
157+
158+
1: ───Rz(b)───two_qubit_gate───Rz(d)───
159+
for some angles a, b, c, and d.
160+
161+
Since the two-qubit gate is a excitation-preserving gate, it can be represented by an FSimGate
162+
and the effect of rotations turns it into a PhasedFSimGate. Using XEB-data we find the
163+
PhasedFSimGate parameters that minimize the infidelity of the gate.
164+
165+
References:
166+
- https://arxiv.org/abs/2001.08343
167+
- https://arxiv.org/abs/2010.07965
168+
- https://arxiv.org/abs/1910.11333
169+
170+
Args:
171+
sampler: The quantum engine or simulator to run the circuits.
172+
qubits: Qubits to use. If none, use all qubits on the sampler's device.
173+
two_qubit_gate: The entangling gate to use.
174+
options: The XEB-fitting options. If None, calibrate only the three phase angles
175+
(chi, gamma, zeta) using the representation of a two-qubit gate as an FSimGate
176+
for the initial guess.
177+
n_repetitions: The number of repetitions to use.
178+
n_combinations: The number of combinations to generate.
179+
n_circuits: The number of circuits to generate.
180+
cycle_depths: The cycle depths to use.
181+
random_state: The random state to use.
182+
atol: Absolute tolerance to be used by the minimizer.
183+
num_workers_or_pool: An optional multi-processing pool or number of workers.
184+
A zero value means no multiprocessing.
185+
A positive integer value will create a pool with the given number of workers.
186+
A negative value will create pool with maximum number of workers.
187+
188+
Returns:
189+
- A dictionary mapping qubit pairs to the calibrated PhasedFSimGates.
190+
"""
191+
192+
if options is None:
193+
options = xeb_fitting.XEBPhasedFSimCharacterizationOptions(
194+
characterize_chi=True,
195+
characterize_gamma=True,
196+
characterize_zeta=True,
197+
characterize_theta=False,
198+
characterize_phi=False,
199+
).with_defaults_from_gate(two_qubit_gate)
200+
201+
result, _ = z_phase_calibration_workflow(
202+
sampler=sampler,
203+
qubits=qubits,
204+
two_qubit_gate=two_qubit_gate,
205+
options=options,
206+
n_repetitions=n_repetitions,
207+
n_combinations=n_combinations,
208+
n_circuits=n_circuits,
209+
cycle_depths=cycle_depths,
210+
random_state=random_state,
211+
atol=atol,
212+
num_workers_or_pool=num_workers_or_pool,
213+
)
214+
215+
gates = {}
216+
for pair, params in result.final_params.items():
217+
params['theta'] = params.get('theta', options.theta_default or 0)
218+
params['phi'] = params.get('phi', options.phi_default or 0)
219+
params['zeta'] = params.get('zeta', options.zeta_default or 0)
220+
params['chi'] = params.get('chi', options.chi_default or 0)
221+
params['gamma'] = params.get('gamma', options.gamma_default or 0)
222+
gates[pair] = ops.PhasedFSimGate(**params)
223+
return gates
224+
225+
226+
def plot_z_phase_calibration_result(
227+
before_after_df: 'pd.DataFrame',
228+
axes: Optional[np.ndarray[Sequence[Sequence['plt.Axes']], np.dtype[np.object_]]] = None,
229+
pairs: Optional[Sequence[Tuple['cirq.Qid', 'cirq.Qid']]] = None,
230+
*,
231+
with_error_bars: bool = False,
232+
) -> np.ndarray[Sequence[Sequence['plt.Axes']], np.dtype[np.object_]]:
233+
"""A helper method to plot the result of running z-phase calibration.
234+
235+
Note that the plotted fidelity is a statistical estimate of the true fidelity and as a result
236+
may be outside the [0, 1] range.
237+
238+
Args:
239+
before_after_df: The second return object of running `z_phase_calibration_workflow`.
240+
axes: And ndarray of the axes to plot on.
241+
The number of axes is expected to be >= number of qubit pairs.
242+
pairs: If provided, only the given pairs are plotted.
243+
with_error_bars: Whether to add error bars or not.
244+
The width of the bar is an upper bound on standard variation of the estimated fidelity.
245+
"""
246+
if pairs is None:
247+
pairs = before_after_df.index
248+
if axes is None:
249+
# Create a 16x9 rectangle.
250+
ncols = int(np.ceil(np.sqrt(9 / 16 * len(pairs))))
251+
nrows = (len(pairs) + ncols - 1) // ncols
252+
_, axes = plt.subplots(nrows=nrows, ncols=ncols)
253+
axes = axes if isinstance(axes, np.ndarray) else np.array(axes)
254+
for pair, ax in zip(pairs, axes.flatten()):
255+
row = before_after_df.loc[[pair]].iloc[0]
256+
ax.errorbar(
257+
row.cycle_depths_0,
258+
row.fidelities_0,
259+
yerr=row.layer_fid_std_0 * with_error_bars,
260+
label='original',
261+
)
262+
ax.errorbar(
263+
row.cycle_depths_0,
264+
row.fidelities_c,
265+
yerr=row.layer_fid_std_c * with_error_bars,
266+
label='calibrated',
267+
)
268+
ax.axhline(1, linestyle='--')
269+
ax.set_xlabel('cycle depth')
270+
ax.set_ylabel('fidelity estimate')
271+
ax.set_title('-'.join(str(q) for q in pair))
272+
ax.legend()
273+
return axes

0 commit comments

Comments
 (0)