|
| 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)) |
0 commit comments