Skip to content

Commit 34a8e59

Browse files
Combine 2q parallel XEB into two methods, one for benchemarking and the other for visualization (#6443)
1 parent b9d2def commit 34a8e59

File tree

3 files changed

+326
-0
lines changed

3 files changed

+326
-0
lines changed

cirq-core/cirq/experiments/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,5 @@
6464
from cirq.experiments.t2_decay_experiment import t2_decay, T2DecayResult
6565

6666
from cirq.experiments.xeb_fitting import XEBPhasedFSimCharacterizationOptions
67+
68+
from cirq.experiments.two_qubit_xeb import TwoQubitXEBResult, parallel_two_qubit_xeb
+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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+
from typing import Sequence, TYPE_CHECKING, Optional, Tuple, Dict
15+
16+
from dataclasses import dataclass
17+
import itertools
18+
import functools
19+
20+
from matplotlib import pyplot as plt
21+
import networkx as nx
22+
import numpy as np
23+
import pandas as pd
24+
25+
from cirq import ops, devices, value, vis
26+
from cirq.experiments.xeb_sampling import sample_2q_xeb_circuits
27+
from cirq.experiments.xeb_fitting import benchmark_2q_xeb_fidelities
28+
from cirq.experiments.xeb_fitting import fit_exponential_decays, exponential_decay
29+
from cirq.experiments import random_quantum_circuit_generation as rqcg
30+
31+
if TYPE_CHECKING:
32+
import cirq
33+
34+
35+
def _grid_qubits_for_sampler(sampler: 'cirq.Sampler'):
36+
if hasattr(sampler, 'processor'):
37+
device = sampler.processor.get_device()
38+
return sorted(device.metadata.qubit_set)
39+
else:
40+
qubits = devices.GridQubit.rect(3, 2, 4, 3)
41+
# Delete one qubit from the rectangular arangement to
42+
# 1) make it irregular 2) simplify simulation.
43+
return qubits[:-1]
44+
45+
46+
def _manhattan_distance(qubit1: 'cirq.GridQubit', qubit2: 'cirq.GridQubit') -> int:
47+
return abs(qubit1.row - qubit2.row) + abs(qubit1.col - qubit2.col)
48+
49+
50+
@dataclass(frozen=True)
51+
class TwoQubitXEBResult:
52+
"""Results from an XEB experiment."""
53+
54+
fidelities: pd.DataFrame
55+
56+
@functools.cached_property
57+
def _qubit_pair_map(self) -> Dict[Tuple['cirq.GridQubit', 'cirq.GridQubit'], int]:
58+
return {
59+
(min(q0, q1), max(q0, q1)): i
60+
for i, (_, _, (q0, q1)) in enumerate(self.fidelities.index)
61+
}
62+
63+
@functools.cached_property
64+
def all_qubit_pairs(self) -> Tuple[Tuple['cirq.GridQubit', 'cirq.GridQubit'], ...]:
65+
return tuple(sorted(self._qubit_pair_map.keys()))
66+
67+
def plot_heatmap(self, ax: Optional[plt.Axes] = None, **plot_kwargs) -> plt.Axes:
68+
"""plot the heatmap for xeb error.
69+
70+
Args:
71+
ax: the plt.Axes to plot on. If not given, a new figure is created,
72+
plotted on, and shown.
73+
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
74+
"""
75+
show_plot = not ax
76+
if not isinstance(ax, plt.Axes):
77+
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
78+
79+
heatmap_data: Dict[Tuple['cirq.GridQubit', ...], float] = {
80+
pair: self.xeb_error(*pair) for pair in self.all_qubit_pairs
81+
}
82+
83+
ax.set_title('device xeb error heatmap')
84+
85+
vis.TwoQubitInteractionHeatmap(heatmap_data).plot(ax=ax, **plot_kwargs)
86+
if show_plot:
87+
fig.show()
88+
return ax
89+
90+
def plot_fitted_exponential(
91+
self,
92+
q0: 'cirq.GridQubit',
93+
q1: 'cirq.GridQubit',
94+
ax: Optional[plt.Axes] = None,
95+
**plot_kwargs,
96+
) -> plt.Axes:
97+
"""plot the fitted model to for xeb error of a qubit pair.
98+
99+
Args:
100+
q0: first qubit.
101+
q1: second qubit.
102+
ax: the plt.Axes to plot on. If not given, a new figure is created,
103+
plotted on, and shown.
104+
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
105+
"""
106+
show_plot = not ax
107+
if not isinstance(ax, plt.Axes):
108+
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
109+
110+
record = self._record(q0, q1)
111+
112+
ax.axhline(1, color='grey', ls='--')
113+
ax.plot(record['cycle_depths'], record['fidelities'], 'o')
114+
depths = np.linspace(0, np.max(record['cycle_depths']))
115+
ax.plot(
116+
depths,
117+
exponential_decay(depths, a=record['a'], layer_fid=record['layer_fid']),
118+
label='estimated exponential decay',
119+
**plot_kwargs,
120+
)
121+
ax.set_title(f'{q0}-{q1}')
122+
ax.set_ylabel('Circuit fidelity')
123+
ax.set_xlabel('Cycle Depth $d$')
124+
ax.legend(loc='best')
125+
if show_plot:
126+
fig.show()
127+
return ax
128+
129+
def _record(self, q0, q1) -> pd.Series:
130+
if q0 > q1:
131+
q0, q1 = q1, q0
132+
return self.fidelities.iloc[self._qubit_pair_map[(q0, q1)]]
133+
134+
def xeb_error(self, q0: 'cirq.GridQubit', q1: 'cirq.GridQubit') -> float:
135+
"""Return the XEB error of a qubit pair."""
136+
p = self._record(q0, q1).layer_fid
137+
return 1 - p
138+
139+
def all_errors(self) -> Dict[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]:
140+
"""Return the XEB error of all qubit pairs."""
141+
return {(q0, q1): self.xeb_error(q0, q1) for q0, q1 in self.all_qubit_pairs}
142+
143+
def plot_histogram(self, ax: Optional[plt.Axes] = None, **plot_kwargs) -> plt.Axes:
144+
"""plot a histogram of all xeb errors
145+
146+
Args:
147+
ax: the plt.Axes to plot on. If not given, a new figure is created,
148+
plotted on, and shown.
149+
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
150+
"""
151+
fig = None
152+
if ax is None:
153+
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
154+
vis.integrated_histogram(data=self.all_errors(), ax=ax, **plot_kwargs)
155+
if fig is not None:
156+
fig.show(**plot_kwargs)
157+
return ax
158+
159+
160+
def parallel_two_qubit_xeb(
161+
sampler: 'cirq.Sampler',
162+
entangling_gate: 'cirq.Gate' = ops.CZ,
163+
n_repetitions: int = 10**4,
164+
n_combinations: int = 10,
165+
n_circuits: int = 20,
166+
cycle_depths: Sequence[int] = tuple(np.arange(3, 100, 20)),
167+
random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = 42,
168+
ax: Optional[plt.Axes] = None,
169+
**plot_kwargs,
170+
) -> TwoQubitXEBResult:
171+
"""A convenience method that runs the full XEB workflow.
172+
173+
Args:
174+
sampler: The quantum engine or simulator to run the circuits.
175+
entangling_gate: The entangling gate to use.
176+
n_repetitions: The number of repetitions to use.
177+
n_combinations: The number of combinations to generate.
178+
n_circuits: The number of circuits to generate.
179+
cycle_depths: The cycle depths to use.
180+
random_state: The random state to use.
181+
ax: the plt.Axes to plot the device layout on. If not given,
182+
no plot is created.
183+
**plot_kwargs: Arguments to be passed to 'plt.Axes.plot'.
184+
185+
Returns:
186+
A TwoQubitXEBResult object representing the results of the experiment.
187+
"""
188+
rs = value.parse_random_state(random_state)
189+
190+
qubits = _grid_qubits_for_sampler(sampler)
191+
graph = nx.Graph(
192+
pair for pair in itertools.combinations(qubits, 2) if _manhattan_distance(*pair) == 1
193+
)
194+
195+
if ax is not None:
196+
nx.draw_networkx(graph, pos={q: (q.row, q.col) for q in qubits}, ax=ax)
197+
ax.set_title('device layout')
198+
ax.plot(**plot_kwargs)
199+
200+
circuit_library = rqcg.generate_library_of_2q_circuits(
201+
n_library_circuits=n_circuits, two_qubit_gate=entangling_gate, random_state=rs
202+
)
203+
204+
combs_by_layer = rqcg.get_random_combinations_for_device(
205+
n_library_circuits=len(circuit_library),
206+
n_combinations=n_combinations,
207+
device_graph=graph,
208+
random_state=rs,
209+
)
210+
211+
sampled_df = sample_2q_xeb_circuits(
212+
sampler=sampler,
213+
circuits=circuit_library,
214+
cycle_depths=cycle_depths,
215+
combinations_by_layer=combs_by_layer,
216+
shuffle=rs,
217+
repetitions=n_repetitions,
218+
)
219+
220+
fids = benchmark_2q_xeb_fidelities(
221+
sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths
222+
)
223+
224+
return TwoQubitXEBResult(fit_exponential_decays(fids))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
"""Wraps Parallel Two Qubit XEB into a few convenience methods."""
15+
from contextlib import redirect_stdout, redirect_stderr
16+
import itertools
17+
import io
18+
import random
19+
20+
import matplotlib.pyplot as plt
21+
22+
import numpy as np
23+
import networkx as nx
24+
import pytest
25+
26+
import cirq
27+
28+
29+
def _manhattan_distance(qubit1: 'cirq.GridQubit', qubit2: 'cirq.GridQubit') -> int:
30+
return abs(qubit1.row - qubit2.row) + abs(qubit1.col - qubit2.col)
31+
32+
33+
class MockDevice(cirq.Device):
34+
@property
35+
def metadata(self):
36+
qubits = cirq.GridQubit.rect(3, 2, 4, 3)
37+
graph = nx.Graph(
38+
pair for pair in itertools.combinations(qubits, 2) if _manhattan_distance(*pair) == 1
39+
)
40+
return cirq.DeviceMetadata(qubits, graph)
41+
42+
43+
class MockProcessor:
44+
def get_device(self):
45+
return MockDevice()
46+
47+
48+
class DensityMatrixSimulatorWithProcessor(cirq.DensityMatrixSimulator):
49+
@property
50+
def processor(self):
51+
return MockProcessor()
52+
53+
54+
@pytest.mark.parametrize(
55+
'sampler',
56+
[
57+
cirq.DensityMatrixSimulator(
58+
seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1))
59+
),
60+
DensityMatrixSimulatorWithProcessor(
61+
seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1))
62+
),
63+
],
64+
)
65+
def test_parallel_two_qubit_xeb(sampler: cirq.Sampler):
66+
np.random.seed(0)
67+
random.seed(0)
68+
69+
with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
70+
res = cirq.experiments.parallel_two_qubit_xeb(
71+
sampler=sampler,
72+
n_repetitions=100,
73+
n_combinations=1,
74+
n_circuits=1,
75+
cycle_depths=[3, 4, 5],
76+
random_state=0,
77+
)
78+
79+
got = [res.xeb_error(*reversed(pair)) for pair in res.all_qubit_pairs]
80+
np.testing.assert_allclose(got, 0.1, atol=1e-1)
81+
82+
83+
@pytest.mark.usefixtures('closefigures')
84+
@pytest.mark.parametrize(
85+
'sampler', [cirq.DensityMatrixSimulator(seed=0), DensityMatrixSimulatorWithProcessor(seed=0)]
86+
)
87+
@pytest.mark.parametrize('ax', [None, plt.subplots(1, 1, figsize=(8, 8))[1]])
88+
def test_plotting(sampler, ax):
89+
res = cirq.experiments.parallel_two_qubit_xeb(
90+
sampler=sampler,
91+
n_repetitions=1,
92+
n_combinations=1,
93+
n_circuits=1,
94+
cycle_depths=[3, 4, 5],
95+
random_state=0,
96+
ax=ax,
97+
)
98+
res.plot_heatmap(ax=ax)
99+
res.plot_fitted_exponential(cirq.GridQubit(4, 4), cirq.GridQubit(4, 3), ax=ax)
100+
res.plot_histogram(ax=ax)

0 commit comments

Comments
 (0)