- 
                Notifications
    
You must be signed in to change notification settings  - Fork 1.1k
 
Combine 2q parallel XEB into one methods to simplify the XEB workflow #6443
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
Changes from 13 commits
f3f6727
              cc8aa10
              d616efe
              d06cc93
              592ecfc
              9435cff
              76e5a8d
              2a2f8b8
              a94730c
              9aa9d4e
              e6151f0
              a91e175
              8e47acd
              64effd8
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| # 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. | ||
| from typing import Sequence, TYPE_CHECKING, Optional, Tuple, Dict | ||
| 
     | 
||
| from dataclasses import dataclass | ||
| import itertools | ||
| import functools | ||
| 
     | 
||
| from matplotlib import pyplot as plt | ||
| import networkx as nx | ||
| import numpy as np | ||
| import pandas as pd | ||
| 
     | 
||
| from cirq import ops, devices, value, vis | ||
| from cirq.experiments.xeb_sampling import sample_2q_xeb_circuits | ||
| from cirq.experiments.xeb_fitting import benchmark_2q_xeb_fidelities | ||
| from cirq.experiments.xeb_fitting import fit_exponential_decays, exponential_decay | ||
| from cirq.experiments import random_quantum_circuit_generation as rqcg | ||
| 
     | 
||
| if TYPE_CHECKING: | ||
| import cirq | ||
| 
     | 
||
| 
     | 
||
| def _grid_qubits_for_sampler(sampler: 'cirq.Sampler'): | ||
| if hasattr(sampler, 'processor'): | ||
| device = sampler.processor.get_device() | ||
| return sorted(device.metadata.qubit_set) | ||
| else: | ||
| qubits = devices.GridQubit.rect(3, 2, 4, 3) | ||
| # Delete one qubit from the rectangular arangement to | ||
| # 1) make it irregular 2) simplify simulation. | ||
| return qubits[:-1] | ||
| 
     | 
||
| 
     | 
||
| def _manhattan_distance(qubit1: 'cirq.GridQubit', qubit2: 'cirq.GridQubit') -> int: | ||
| return abs(qubit1.row - qubit2.row) + abs(qubit1.col - qubit2.col) | ||
| 
     | 
||
| 
     | 
||
| @dataclass(frozen=True) | ||
| class TwoQubitXEBResult: | ||
| """Results from an XEB experiment.""" | ||
| 
     | 
||
| fidelities: pd.DataFrame | ||
| 
     | 
||
| @functools.cached_property | ||
| def _qubit_pair_map(self) -> Dict[Tuple['cirq.GridQubit', 'cirq.GridQubit'], int]: | ||
| return { | ||
| (min(q0, q1), max(q0, q1)): i | ||
| for i, (_, _, (q0, q1)) in enumerate(self.fidelities.index) | ||
| } | ||
| 
     | 
||
| @functools.cached_property | ||
| def all_qubit_pairs(self) -> Tuple[Tuple['cirq.GridQubit', 'cirq.GridQubit'], ...]: | ||
| return tuple(sorted(self._qubit_pair_map.keys())) | ||
| 
     | 
||
| def plot_heatmap(self, ax: Optional[plt.Axes] = None, **plot_kwargs): | ||
| """plot the heatmap for xeb error. | ||
| 
     | 
||
| Args: | ||
| ax: the plt.Axes to plot on. If not given, a new figure is created, | ||
| plotted on, and shown. | ||
| **plot_kwargs: Arguments to be passed to 'plt.Axes.plot'. | ||
| """ | ||
| show_plot = not ax | ||
| if ax is None: | ||
| fig, ax = plt.subplots(1, 1, figsize=(8, 8)) | ||
| 
     | 
||
| heatmap_data: Dict[Tuple['cirq.GridQubit', ...], float] = { | ||
| pair: self.xeb_error(*pair) for pair in self.all_qubit_pairs | ||
| } | ||
| 
     | 
||
| if ax is not None: | ||
| ax.set_title('device xeb error heatmap') | ||
| 
     | 
||
| vis.TwoQubitInteractionHeatmap(heatmap_data).plot(ax=ax, **plot_kwargs) | ||
| if show_plot: | ||
| fig.show() | ||
| 
     | 
||
| def plot_fitted_exponential( | ||
| self, | ||
| q0: 'cirq.GridQubit', | ||
| q1: 'cirq.GridQubit', | ||
| ax: Optional[plt.Axes] = None, | ||
| **plot_kwargs, | ||
| ): | ||
| """plot the fitted model to for xeb error of a qubit pair. | ||
| 
     | 
||
| Args: | ||
| q0: first qubit. | ||
| q1: second qubit. | ||
| ax: the plt.Axes to plot on. If not given, a new figure is created, | ||
| plotted on, and shown. | ||
| **plot_kwargs: Arguments to be passed to 'plt.Axes.plot'. | ||
| """ | ||
| show_plot = not ax | ||
| if not ax: | ||
| fig, ax = plt.subplots(1, 1, figsize=(8, 8)) | ||
| record = self._record(q0, q1) | ||
| 
     | 
||
| plt.axhline(1, color='grey', ls='--') | ||
| plt.plot(record['cycle_depths'], record['fidelities'], 'o') | ||
| xx = np.linspace(0, np.max(record['cycle_depths'])) | ||
| 
     | 
||
| if ax is not None: | ||
| ax.plot( | ||
| xx, | ||
| exponential_decay(xx, a=record['a'], layer_fid=record['layer_fid']), | ||
| label='estimated exponential decay', | ||
| **plot_kwargs, | ||
| ) | ||
| ax.set_title(f'{q0}-{q1}') | ||
| ax.set_ylabel('Circuit fidelity') | ||
| ax.set_xlabel('Cycle Depth $d$') | ||
| ax.legend(loc='best') | ||
| if show_plot: | ||
| fig.show() | ||
| 
     | 
||
| def _record(self, q0, q1) -> pd.Series: | ||
| if q0 > q1: | ||
| q0, q1 = q1, q0 | ||
| return self.fidelities.iloc[self._qubit_pair_map[(q0, q1)]] | ||
| 
     | 
||
| def xeb_error(self, q0: 'cirq.GridQubit', q1: 'cirq.GridQubit') -> float: | ||
| """Return the XEB error of a qubit pair.""" | ||
| p = self._record(q0, q1).layer_fid | ||
| return 1 - p | ||
| 
     | 
||
| def all_errors(self) -> Dict[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]: | ||
| """Return the XEB error of all qubit pairs.""" | ||
| return {(q0, q1): self.xeb_error(q0, q1) for q0, q1 in self.all_qubit_pairs} | ||
| 
     | 
||
| def plot_histogram(self, ax: Optional[plt.Axes] = None, **plot_kwargs): | ||
| """plot a histogram of all xeb errors | ||
| 
     | 
||
| Args: | ||
| ax: the plt.Axes to plot on. If not given, a new figure is created, | ||
| plotted on, and shown. | ||
| **plot_kwargs: Arguments to be passed to 'plt.Axes.plot'. | ||
| """ | ||
| fig = None | ||
| if ax is None: | ||
| fig, ax = plt.subplots(1, 1, figsize=(8, 8)) | ||
| vis.integrated_histogram(data=self.all_errors(), ax=ax, **plot_kwargs) | ||
| if fig is not None: | ||
| fig.show(**plot_kwargs) | ||
| 
     | 
||
| 
     | 
||
| def parallel_two_qubit_xeb( | ||
| sampler: 'cirq.Sampler', | ||
| entangling_gate: 'cirq.Gate' = ops.CZ, | ||
| 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' = 42, | ||
| ax: Optional[plt.Axes] = None, | ||
| **plot_kwargs, | ||
| ) -> TwoQubitXEBResult: | ||
| """A convenience method that runs the full XEB workflow. | ||
| 
     | 
||
| Args: | ||
| sampler: The quantum engine or simulator to run the circuits. | ||
| entangling_gate: The entangling gate to use. | ||
| 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. | ||
| ax: the plt.Axes to plot the device layout on. If not given, | ||
| no plot is created. | ||
| **plot_kwargs: Arguments to be passed to 'plt.Axes.plot'. | ||
| 
     | 
||
| Returns: | ||
| A TwoQubitXEBResult object representing the results of the experiment. | ||
| """ | ||
| rs = value.parse_random_state(random_state) | ||
| 
     | 
||
| qubits = _grid_qubits_for_sampler(sampler) | ||
| graph = nx.Graph( | ||
| pair for pair in itertools.combinations(qubits, 2) if _manhattan_distance(*pair) == 1 | ||
| ) | ||
| 
     | 
||
| if ax is not None: | ||
| nx.draw_networkx(graph, pos={q: (q.row, q.col) for q in qubits}, ax=ax) | ||
| ax.set_title('device layout') | ||
| ax.plot(**plot_kwargs) | ||
| 
     | 
||
| circuit_library = rqcg.generate_library_of_2q_circuits( | ||
| n_library_circuits=n_circuits, two_qubit_gate=entangling_gate, random_state=rs | ||
| ) | ||
| 
     | 
||
| combs_by_layer = rqcg.get_random_combinations_for_device( | ||
| n_library_circuits=len(circuit_library), | ||
| n_combinations=n_combinations, | ||
| device_graph=graph, | ||
| random_state=rs, | ||
| ) | ||
| 
     | 
||
| sampled_df = sample_2q_xeb_circuits( | ||
| sampler=sampler, | ||
| circuits=circuit_library, | ||
| cycle_depths=cycle_depths, | ||
| combinations_by_layer=combs_by_layer, | ||
| shuffle=rs, | ||
| repetitions=n_repetitions, | ||
| ) | ||
| 
     | 
||
| fids = benchmark_2q_xeb_fidelities( | ||
| sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths | ||
| ) | ||
| 
     | 
||
| return TwoQubitXEBResult(fit_exponential_decays(fids)) | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # 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. | ||
| """Wraps Parallel Two Qubit XEB into a few convenience methods.""" | ||
| from contextlib import redirect_stdout, redirect_stderr | ||
| import itertools | ||
| import io | ||
| import random | ||
| 
     | 
||
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import networkx as nx | ||
| import pytest | ||
| 
     | 
||
| import cirq | ||
| 
     | 
||
| 
     | 
||
| def _manhattan_distance(qubit1: 'cirq.GridQubit', qubit2: 'cirq.GridQubit') -> int: | ||
| return abs(qubit1.row - qubit2.row) + abs(qubit1.col - qubit2.col) | ||
| 
     | 
||
| 
     | 
||
| class MockDevice(cirq.Device): | ||
| @property | ||
| def metadata(self): | ||
| qubits = cirq.GridQubit.rect(3, 2, 4, 3) | ||
| graph = nx.Graph( | ||
| pair for pair in itertools.combinations(qubits, 2) if _manhattan_distance(*pair) == 1 | ||
| ) | ||
| return cirq.DeviceMetadata(qubits, graph) | ||
| 
     | 
||
| 
     | 
||
| class MockProcessor: | ||
| def get_device(self): | ||
| return MockDevice() | ||
| 
     | 
||
| 
     | 
||
| class DensityMatrixSimulatorWithProcessor(cirq.DensityMatrixSimulator): | ||
| @property | ||
| def processor(self): | ||
| return MockProcessor() | ||
| 
     | 
||
| 
     | 
||
| @pytest.mark.parametrize( | ||
| 'sampler', | ||
| [ | ||
| cirq.DensityMatrixSimulator( | ||
| seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1)) | ||
| ), | ||
| DensityMatrixSimulatorWithProcessor( | ||
| seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1)) | ||
| ), | ||
| ], | ||
| ) | ||
| def test_parallel_two_qubit_xeb(sampler: cirq.Sampler): | ||
| np.random.seed(0) | ||
| random.seed(0) | ||
| 
     | 
||
| with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): | ||
| res = cirq.experiments.parallel_two_qubit_xeb( | ||
| sampler=sampler, | ||
| n_repetitions=100, | ||
| n_combinations=1, | ||
| n_circuits=1, | ||
| cycle_depths=[3, 4, 5], | ||
| random_state=0, | ||
| ) | ||
| 
     | 
||
| got = [res.xeb_error(*reversed(pair)) for pair in res.all_qubit_pairs] | ||
| np.testing.assert_allclose(got, 0.1, atol=1e-1) | ||
| 
     | 
||
| 
     | 
||
| @pytest.mark.parametrize( | ||
| 'sampler', [cirq.DensityMatrixSimulator(seed=0), DensityMatrixSimulatorWithProcessor(seed=0)] | ||
| ) | ||
| @pytest.mark.parametrize('ax', [None, plt.subplots(1, 1, figsize=(8, 8))[1]]) | ||
| def test_plotting(sampler, ax): | ||
                
      
                  NoureldinYosri marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| res = cirq.experiments.parallel_two_qubit_xeb( | ||
| sampler=sampler, | ||
| n_repetitions=1, | ||
| n_combinations=1, | ||
| n_circuits=1, | ||
| cycle_depths=[3, 4, 5], | ||
| random_state=0, | ||
| ax=ax, | ||
| ) | ||
| res.plot_heatmap(ax=ax) | ||
| res.plot_fitted_exponential(cirq.GridQubit(4, 4), cirq.GridQubit(4, 3), ax=ax) | ||
| res.plot_histogram(ax=ax) | ||
| 
         
      Comment on lines
    
      +98
     to 
      +100
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not seem to check if plot functions have any effect - perhaps we can add simple assertions once they return axes? I am also getting some UserWarning-s in the pytest output such as below which we could avoid - Let's use  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this doesn't work. that method doesn't seem to exist. I can't find this being done in other places in Cirq. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I will take a look later if warnings can be stopped in a separate PR.  | 
||
Uh oh!
There was an error while loading. Please reload this page.