Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create workflow for Z-phase calibration #6728

Merged
merged 27 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ade51ee
checkpoint
NoureldinYosri Aug 2, 2024
145880e
checkpoint
NoureldinYosri Aug 2, 2024
9a9d06f
Create workflow for Z-phase calibration
NoureldinYosri Sep 13, 2024
d37ea60
update deps
NoureldinYosri Sep 13, 2024
717155c
increase error
NoureldinYosri Sep 13, 2024
b053a93
nit
NoureldinYosri Sep 13, 2024
eadda56
nit
NoureldinYosri Sep 13, 2024
07ba30a
Add test
NoureldinYosri Sep 13, 2024
8c244e7
Merge branch 'main' into z_cal2
NoureldinYosri Sep 19, 2024
552b7df
checkpoint
NoureldinYosri Sep 19, 2024
6f71abf
coverage
NoureldinYosri Sep 19, 2024
39b1537
Add plotting method and calculate the variance of estimated fidelity
NoureldinYosri Oct 2, 2024
e2cac7d
Merge branch 'main' into z_cal2
NoureldinYosri Oct 2, 2024
9f42000
docstring
NoureldinYosri Oct 2, 2024
26593a1
coverage
NoureldinYosri Oct 2, 2024
39e0a6d
change qubit names
NoureldinYosri Oct 2, 2024
d0a9583
Merge branch 'main' into z_cal2
NoureldinYosri Oct 24, 2024
530dc2d
checkpoint
NoureldinYosri Oct 30, 2024
55c47a5
address comments
NoureldinYosri Oct 31, 2024
0cb890d
Merge branch 'main' into z_cal2
NoureldinYosri Oct 31, 2024
5d668e2
address comments
NoureldinYosri Nov 7, 2024
a372c8e
nit
NoureldinYosri Nov 7, 2024
fcb2d2a
Merge branch 'main' into z_cal2
NoureldinYosri Nov 7, 2024
d9a8282
Update cirq-core/cirq/experiments/z_phase_calibration.py
NoureldinYosri Nov 7, 2024
b19cee9
Update cirq-core/cirq/experiments/z_phase_calibration.py
NoureldinYosri Nov 7, 2024
360aee1
Update cirq-core/cirq/experiments/z_phase_calibration.py
NoureldinYosri Nov 7, 2024
296922d
Update cirq-core/cirq/experiments/z_phase_calibration.py
NoureldinYosri Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cirq-core/cirq/experiments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,6 @@
parallel_two_qubit_xeb as parallel_two_qubit_xeb,
run_rb_and_xeb as run_rb_and_xeb,
)


from cirq.experiments.z_phase_calibration import z_phase_calibration_workflow, calibrate_z_phases
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little redundant to have both calibrate_z_phases and z_phase_calibration_workflow. Can we just have one thing with the functionality of z_phase_calibration_workflow?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On an unrelated note, please use the from somewhere import foo as foo so that the new names can be imported from cirq.experiments without raising mypy error. (Ref: #6717)

5 changes: 4 additions & 1 deletion cirq-core/cirq/experiments/two_qubit_xeb.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from cirq._compat import cached_method

if TYPE_CHECKING:
import multiprocessing

Check warning on line 41 in cirq-core/cirq/experiments/two_qubit_xeb.py

View check run for this annotation

Codecov / codecov/patch

cirq-core/cirq/experiments/two_qubit_xeb.py#L41

Added line #L41 was not covered by tests
import cirq


Expand Down Expand Up @@ -358,6 +359,7 @@
cycle_depths: Sequence[int] = (5, 25, 50, 100, 200, 300),
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
ax: Optional[plt.Axes] = None,
pool: Optional['multiprocessing.pool.Pool'] = None,
**plot_kwargs,
) -> Tuple[pd.DataFrame, Sequence['cirq.Circuit'], pd.DataFrame]:
"""A utility method that runs the full XEB workflow.
Expand All @@ -373,6 +375,7 @@
random_state: The random state to use.
ax: the plt.Axes to plot the device layout on. If not given,
no plot is created.
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
pool: An optional multiprocessing pool.
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.

Returns:
Expand Down Expand Up @@ -426,7 +429,7 @@
)

fids = benchmark_2q_xeb_fidelities(
sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths
sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths, pool=pool
)

return fids, circuit_library, sampled_df
Expand Down
27 changes: 22 additions & 5 deletions cirq-core/cirq/experiments/xeb_fitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ def benchmark_2q_xeb_fidelities(
df['e_u'] = np.sum(pure_probs**2, axis=1)
df['u_u'] = np.sum(pure_probs, axis=1) / D
df['m_u'] = np.sum(pure_probs * sampled_probs, axis=1)
# Var[m_u] = Var[sum p(x) * p_sampled(x)]
# = sum p(x)^2 Var[p_sampled(x)]
# = sum p(x)^2 p(x) (1 - p(x))
# = sum p(x)^3 (1 - p(x))
df['var_m_u'] = np.sum(pure_probs**3 * (1 - pure_probs), axis=1)
df['y'] = df['m_u'] - df['u_u']
df['x'] = df['e_u'] - df['u_u']
df['numerator'] = df['x'] * df['y']
Expand All @@ -103,7 +108,11 @@ def benchmark_2q_xeb_fidelities(
def per_cycle_depth(df):
"""This function is applied per cycle_depth in the following groupby aggregation."""
fid_lsq = df['numerator'].sum() / df['denominator'].sum()
ret = {'fidelity': fid_lsq}
# Note: both df['denominator'] and df['x'] are constants.
# Var[f] = Var[df['numerator']] / (sum df['denominator'])^2
# = sum (df['x']^2 * df['var_m_u']) / (sum df['denominator'])^2
var_fid = (df['var_m_u'] * df['x'] ** 2).sum() / df['denominator'].sum() ** 2
ret = {'fidelity': fid_lsq, 'fidelity_variance': var_fid}

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


def parameterize_circuit(
circuit: 'cirq.Circuit', options: XEBCharacterizationOptions
circuit: 'cirq.Circuit',
options: XEBCharacterizationOptions,
target_gatefamily: Optional[ops.GateFamily] = None,
) -> 'cirq.Circuit':
"""Parameterize PhasedFSim-like gates in a given circuit according to
`phased_fsim_options`.
"""
if isinstance(target_gatefamily, ops.GateFamily):
should_parameterize = lambda op: op in target_gatefamily or options.should_parameterize(op)
else:
should_parameterize = options.should_parameterize
gate = options.get_parameterized_gate()
return circuits.Circuit(
circuits.Moment(
gate.on(*op.qubits) if options.should_parameterize(op) else op
for op in moment.operations
gate.on(*op.qubits) if should_parameterize(op) else op for op in moment.operations
)
for moment in circuit.moments
)
Expand Down Expand Up @@ -667,13 +681,16 @@ def _per_pair(f1):
a, layer_fid, a_std, layer_fid_std = _fit_exponential_decay(
f1['cycle_depth'], f1['fidelity']
)
fidelity_variance = 0
if 'fidelity_variance' in f1:
fidelity_variance = f1['fidelity_variance'].values
record = {
'a': a,
'layer_fid': layer_fid,
'cycle_depths': f1['cycle_depth'].values,
'fidelities': f1['fidelity'].values,
'a_std': a_std,
'layer_fid_std': layer_fid_std,
'layer_fid_std': np.sqrt(layer_fid_std**2 + fidelity_variance),
}
return pd.Series(record)

Expand Down
271 changes: 271 additions & 0 deletions cirq-core/cirq/experiments/z_phase_calibration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
# Copyright 2024 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Provides a method to do z-phase calibration for excitation-preserving gates."""
from typing import Union, Optional, Sequence, Tuple, Dict, TYPE_CHECKING

import multiprocessing
import multiprocessing.pool
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
import matplotlib.pyplot as plt
import numpy as np

from cirq.experiments import xeb_fitting
from cirq.experiments.two_qubit_xeb import parallel_xeb_workflow
from cirq import ops

if TYPE_CHECKING:
import cirq
import pandas as pd

Check warning on line 29 in cirq-core/cirq/experiments/z_phase_calibration.py

View check run for this annotation

Codecov / codecov/patch

cirq-core/cirq/experiments/z_phase_calibration.py#L28-L29

Added lines #L28 - L29 were not covered by tests


def z_phase_calibration_workflow(
sampler: 'cirq.Sampler',
qubits: Optional[Sequence['cirq.GridQubit']] = None,
two_qubit_gate: 'cirq.Gate' = ops.CZ,
options: Optional[xeb_fitting.XEBPhasedFSimCharacterizationOptions] = None,
n_repetitions: int = 10**4,
n_combinations: int = 10,
n_circuits: int = 20,
cycle_depths: Sequence[int] = tuple(np.arange(3, 100, 20)),
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
atol: float = 1e-3,
pool_or_num_workers: Optional[Union[int, 'multiprocessing.pool.Pool']] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make a single-process execution the default behavior of the pool argument.
Running via multiprocessing has previously caused unexpected problems in #6674.
It is better if users explicitly ask for multiprocessing so there is a better chance that
potential bugs appear then rather as a default behavior.

I would also recommend to split the pool_or_num_workers argument into two exclusive arguments, pool and num_workers: Optional[int].
The invocation via num_workers could then use a recursive call as

with multiprocessing.Pool(num_workers) as pool:
    return z_phase_calibration_workflow(sampler, ..., pool=pool)

which would be more robust for closing the pool in case of exception, you'd also won't need to track a local_pool flag.

Copy link
Collaborator Author

@NoureldinYosri NoureldinYosri Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make a single-process execution the default behavior of the pool argument

this was the initial behaviour but @eliottrosenberg requested the default be multiprocessing

I would also recommend to split the pool_or_num_workers argument into two exclusive arguments, pool and num_workers: Optional[int]

I condidered this but I thought the api would be a bit convoluted

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pavoljuhas I think that most users would use it with the default values without passing a parallel pool, and it is very slow that way.

Copy link
Collaborator

@pavoljuhas pavoljuhas Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is all other functions with pool-like argument interpret pool=None as run in series.
If reading z_phase_calibration_workflow(..., pool_or_num_workers=None) my expectation would be to have no pool and no workers, but in fact this starts a pool of ncpu size, which may cause memory problems on hosts with many CPUs.

Can we instead use some reasonable default, say 4, and not accept None, ie, make that argument of Union[int, 'multiprocessing.pool.Pool'] type?

Also renaming to num_workers_or_pool might give a better hint to pass integer and make distinction from pool-like arguments.

@NoureldinYosri

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

) -> Tuple[xeb_fitting.XEBCharacterizationResult, 'pd.DataFrame']:
"""Perform z-phase calibration for excitation-preserving gates.

For a given excitation-preserving two-qubit gate we assume an error model that can be described
using Z-rotations:
0: ───Rz(a)───two_qubit_gate───Rz(c)───
1: ───Rz(b)───two_qubit_gate───Rz(d)───
for some angles a, b, c, and d.

Since the two-qubit gate is a excitation-preserving-gate, it can be represented by an FSimGate
and the effect of rotations turns it into a PhasedFSimGate. Using XEB-data we find the
PhasedFSimGate parameters that minimize the infidelity of the gate.

References:
- https://arxiv.org/abs/2001.08343
- https://arxiv.org/abs/2010.07965
- https://arxiv.org/abs/1910.11333

Args:
sampler: The quantum engine or simulator to run the circuits.
qubits: Qubits to use. If none, use all qubits on the sampler's device.
two_qubit_gate: The entangling gate to use.
options: The XEB-fitting options. If None, calibrate all 5 PhasedFSimGate parameters,
using the representation of a two-qubit gate as an FSimGate for the initial guess.
n_repetitions: The number of repetitions to use.
n_combinations: The number of combinations to generate.
n_circuits: The number of circuits to generate.
cycle_depths: The cycle depths to use.
random_state: The random state to use.
atol: Absolute tolerance to be used by the minimizer.
pool_or_num_workers: An optional multi-processing pool or number of workers.
A zero value means no multiprocessing.
A positivie integer value will create a pool with the given number of workers.
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
A None value will create pool with maximum number of workers.
Returns:
- An `XEBCharacterizationResult` object that contains the calibration result.
- A `pd.DataFrame` comparing the before and after fidelities.
"""

pool: Optional['multiprocessing.pool.Pool'] = None
local_pool = False
if isinstance(pool_or_num_workers, multiprocessing.pool.Pool):
pool = pool_or_num_workers # pragma: no cover
elif pool_or_num_workers != 0:
pool = multiprocessing.Pool(pool_or_num_workers)
local_pool = True

fids_df_0, circuits, sampled_df = parallel_xeb_workflow(
sampler=sampler,
qubits=qubits,
entangling_gate=two_qubit_gate,
n_repetitions=n_repetitions,
cycle_depths=cycle_depths,
n_circuits=n_circuits,
n_combinations=n_combinations,
random_state=random_state,
pool=pool,
)

if options is None:
options = xeb_fitting.XEBPhasedFSimCharacterizationOptions(
characterize_chi=True,
characterize_gamma=True,
characterize_zeta=True,
characterize_theta=False,
characterize_phi=False,
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
).with_defaults_from_gate(two_qubit_gate)

p_circuits = [
xeb_fitting.parameterize_circuit(circuit, options, ops.GateFamily(two_qubit_gate))
for circuit in circuits
]

result = xeb_fitting.characterize_phased_fsim_parameters_with_xeb_by_pair(
sampled_df=sampled_df,
parameterized_circuits=p_circuits,
cycle_depths=cycle_depths,
options=options,
fatol=atol,
xatol=atol,
pool=pool,
)

before_after = xeb_fitting.before_and_after_characterization(
fids_df_0, characterization_result=result
)

if local_pool:
assert isinstance(pool, multiprocessing.pool.Pool)
pool.close()
return result, before_after


def calibrate_z_phases(
sampler: 'cirq.Sampler',
qubits: Optional[Sequence['cirq.GridQubit']] = None,
two_qubit_gate: 'cirq.Gate' = ops.CZ,
options: Optional[xeb_fitting.XEBPhasedFSimCharacterizationOptions] = None,
n_repetitions: int = 10**4,
n_combinations: int = 10,
n_circuits: int = 20,
cycle_depths: Sequence[int] = tuple(np.arange(3, 100, 20)),
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None,
atol: float = 1e-3,
pool_or_num_workers: Optional[Union[int, 'multiprocessing.pool.Pool']] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make single process the default and split to 2 arguments as suggested above for z_phase_calibration_workflow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) -> Dict[Tuple['cirq.Qid', 'cirq.Qid'], 'cirq.PhasedFSimGate']:
"""Perform z-phase calibration for excitation-preserving gates.

For a given excitation-preserving two-qubit gate we assume an error model that can be described
using Z-rotations:
0: ───Rz(a)───two_qubit_gate───Rz(c)───
1: ───Rz(b)───two_qubit_gate───Rz(d)───
for some angles a, b, c, and d.

Since the two-qubit gate is a excitation-preserving gate, it can be represented by an FSimGate
and the effect of rotations turns it into a PhasedFSimGate. Using XEB-data we find the
PhasedFSimGate parameters that minimize the infidelity of the gate.

References:
- https://arxiv.org/abs/2001.08343
- https://arxiv.org/abs/2010.07965
- https://arxiv.org/abs/1910.11333

Args:
sampler: The quantum engine or simulator to run the circuits.
qubits: Qubits to use. If none, use all qubits on the sampler's device.
two_qubit_gate: The entangling gate to use.
options: The XEB-fitting options. If None, calibrate all 5 PhasedFSimGate parameters,
using the representation of a two-qubit gate as an FSimGate for the initial guess.
n_repetitions: The number of repetitions to use.
n_combinations: The number of combinations to generate.
n_circuits: The number of circuits to generate.
cycle_depths: The cycle depths to use.
random_state: The random state to use.
atol: Absolute tolerance to be used by the minimizer.
pool_or_num_workers: An optional multi-processing pool or number of workers.
A zero value means no multiprocessing.
A positivie integer value will create a pool with the given number of workers.
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
A None value will create pool with maximum number of workers.

Returns:
- A dictionary mapping qubit pairs to the calibrated PhasedFSimGates.
"""

if options is None:
options = xeb_fitting.XEBPhasedFSimCharacterizationOptions(
characterize_chi=True,
characterize_gamma=True,
characterize_zeta=True,
characterize_theta=False,
characterize_phi=False,
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
).with_defaults_from_gate(two_qubit_gate)

result, _ = z_phase_calibration_workflow(
sampler=sampler,
qubits=qubits,
two_qubit_gate=two_qubit_gate,
options=options,
n_repetitions=n_repetitions,
n_combinations=n_combinations,
n_circuits=n_circuits,
cycle_depths=cycle_depths,
random_state=random_state,
atol=atol,
pool_or_num_workers=pool_or_num_workers,
)

gates = {}
for pair, params in result.final_params.items():
params['theta'] = params.get('theta', options.theta_default or 0)
params['phi'] = params.get('phi', options.phi_default or 0)
params['zeta'] = params.get('zeta', options.zeta_default or 0)
params['chi'] = params.get('chi', options.chi_default or 0)
params['gamma'] = params.get('gamma', options.gamma_default or 0)
gates[pair] = ops.PhasedFSimGate(**params)
return gates


def plot_z_phase_calibration_result(
before_after_df: 'pd.DataFrame',
axes: Optional[np.ndarray[Sequence[Sequence['plt.Axes']], np.dtype[np.object_]]] = None,
pairs: Optional[Sequence[Tuple['cirq.Qid', 'cirq.Qid']]] = None,
*,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eliottrosenberg - Would it be useful to provide an optional pairs argument if only specific qubit pairs are desired for plotting?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added that option

with_error_bars: bool = False,
) -> np.ndarray[Sequence[Sequence['plt.Axes']], np.dtype[np.object_]]:
"""A helper method to plot the result of running z-phase calibration.

Note that the plotted fidelity is a statistical estimate of the true fidelity and as a result
may be outside the [0, 1] range.

Args:
before_after_df: The second return object of running `z_phase_calibration_workflow`.
axes: And ndarray of the axes to plot on.
The number of axes is expected to be >= number of qubit pairs.
pairs: If provided, only the given pairs are plotted.
with_error_bars: Whether to add error bars or not.
The width of the bar is an upper bound on standard variation of the estimated fidelity.
"""
if pairs is None:
pairs = before_after_df.index
if axes is None:
# Create a 16x9 rectangle.
ncols = int(np.ceil(np.sqrt(9 / 16 * len(pairs))))
nrows = (len(pairs) + ncols - 1) // ncols
_, axes = plt.subplots(nrows=nrows, ncols=ncols)
axes = axes if isinstance(axes, np.ndarray) else np.array(axes)
for pair, ax in zip(pairs, axes.flatten()):
row = before_after_df.loc[[pair]].iloc[0]
ax.errorbar(
row.cycle_depths_0,
row.fidelities_0,
yerr=row.layer_fid_std_0 * with_error_bars,
label='original',
)
ax.errorbar(
row.cycle_depths_0,
row.fidelities_c,
yerr=row.layer_fid_std_c * with_error_bars,
label='calibrated',
)
ax.axhline(1, linestyle='--')
ax.set_xlabel('cycle depth')
ax.set_ylabel('fidelity estimate')
ax.set_title('-'.join(str(q) for q in pair))
ax.legend()
return axes
Loading