diff --git a/cirq-core/cirq/experiments/__init__.py b/cirq-core/cirq/experiments/__init__.py index a339000b677..24472cb97a6 100644 --- a/cirq-core/cirq/experiments/__init__.py +++ b/cirq-core/cirq/experiments/__init__.py @@ -65,4 +65,8 @@ from cirq.experiments.xeb_fitting import XEBPhasedFSimCharacterizationOptions -from cirq.experiments.two_qubit_xeb import TwoQubitXEBResult, parallel_two_qubit_xeb +from cirq.experiments.two_qubit_xeb import ( + InferredXEBResult, + TwoQubitXEBResult, + parallel_two_qubit_xeb, +) diff --git a/cirq-core/cirq/experiments/two_qubit_xeb.py b/cirq-core/cirq/experiments/two_qubit_xeb.py index 4d7e7151e96..39ebc134fef 100644 --- a/cirq-core/cirq/experiments/two_qubit_xeb.py +++ b/cirq-core/cirq/experiments/two_qubit_xeb.py @@ -11,9 +11,10 @@ # 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 typing import Sequence, TYPE_CHECKING, Optional, Tuple, Dict, cast, Mapping from dataclasses import dataclass +from types import MappingProxyType import itertools import functools @@ -22,25 +23,24 @@ import numpy as np import pandas as pd -from cirq import ops, devices, value, vis +from cirq import ops, 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 +from cirq.experiments.qubit_characterizations import ParallelRandomizedBenchmarkingResult +from cirq.qis import noise_utils +from cirq._compat import cached_method if TYPE_CHECKING: import cirq -def _grid_qubits_for_sampler(sampler: 'cirq.Sampler'): +def _grid_qubits_for_sampler(sampler: 'cirq.Sampler') -> Optional[Sequence['cirq.GridQubit']]: 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] + return None def _manhattan_distance(qubit1: 'cirq.GridQubit', qubit2: 'cirq.GridQubit') -> int: @@ -65,7 +65,7 @@ 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) -> plt.Axes: - """plot the heatmap for xeb error. + """plot the heatmap of XEB errors. Args: ax: the plt.Axes to plot on. If not given, a new figure is created, @@ -75,7 +75,6 @@ def plot_heatmap(self, ax: Optional[plt.Axes] = None, **plot_kwargs) -> plt.Axes show_plot = not ax if not isinstance(ax, plt.Axes): 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 } @@ -131,10 +130,13 @@ def _record(self, q0, q1) -> pd.Series: q0, q1 = q1, q0 return self.fidelities.iloc[self._qubit_pair_map[(q0, q1)]] + def xeb_fidelity(self, q0: 'cirq.GridQubit', q1: 'cirq.GridQubit') -> float: + """Return the XEB fidelity of a qubit pair.""" + return self._record(q0, q1).layer_fid + 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 + return 1 - self.xeb_fidelity(q0, q1) def all_errors(self) -> Dict[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]: """Return the XEB error of all qubit pairs.""" @@ -156,9 +158,163 @@ def plot_histogram(self, ax: Optional[plt.Axes] = None, **plot_kwargs) -> plt.Ax fig.show(**plot_kwargs) return ax + @cached_method + def pauli_error(self) -> Dict[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]: + """Return the Pauli error of all qubit pairs.""" + return { + pair: noise_utils.decay_constant_to_pauli_error( + noise_utils.xeb_fidelity_to_decay_constant(self.xeb_fidelity(*pair), num_qubits=2), + num_qubits=2, + ) + for pair in self.all_qubit_pairs + } + + +@dataclass(frozen=True) +class InferredXEBResult: + """Uses the results from XEB and RB to compute inferred two-qubit Pauli errors.""" + + rb_result: ParallelRandomizedBenchmarkingResult + xeb_result: TwoQubitXEBResult + + @property + def all_qubit_pairs(self) -> Sequence[Tuple['cirq.GridQubit', 'cirq.GridQubit']]: + return self.xeb_result.all_qubit_pairs + + @cached_method + def single_qubit_pauli_error(self) -> Mapping['cirq.Qid', float]: + """Return the single-qubit Pauli error for all qubits (RB results).""" + return self.rb_result.pauli_error() + + @cached_method + def two_qubit_pauli_error(self) -> Mapping[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]: + """Return the two-qubit Pauli error for all pairs.""" + return MappingProxyType(self.xeb_result.pauli_error()) + + @cached_method + def inferred_pauli_error(self) -> Mapping[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]: + """Return the inferred Pauli error for all pairs.""" + single_q_paulis = self.rb_result.pauli_error() + xeb = self.xeb_result.pauli_error() + + def _pauli_error(q0: 'cirq.GridQubit', q1: 'cirq.GridQubit') -> float: + q0, q1 = sorted([q0, q1]) + return xeb[(q0, q1)] - single_q_paulis[q0] - single_q_paulis[q1] + + return MappingProxyType({pair: _pauli_error(*pair) for pair in self.all_qubit_pairs}) + + @cached_method + def inferred_decay_constant(self) -> Mapping[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]: + """Return the inferred decay constant for all pairs.""" + return MappingProxyType( + { + pair: noise_utils.pauli_error_to_decay_constant(pauli, 2) + for pair, pauli in self.inferred_pauli_error().items() + } + ) + + @cached_method + def inferred_xeb_error(self) -> Mapping[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]: + """Return the inferred XEB error for all pairs.""" + return MappingProxyType( + { + pair: 1 - noise_utils.decay_constant_to_xeb_fidelity(decay, 2) + for pair, decay in self.inferred_decay_constant().items() + } + ) + + def _target_errors( + self, target_error: str + ) -> Mapping[Tuple['cirq.GridQubit', 'cirq.GridQubit'], float]: + error_funcs = { + 'pauli': self.inferred_pauli_error, + 'decay_constant': self.inferred_decay_constant, + 'xeb': self.inferred_xeb_error, + } + return error_funcs[target_error]() + + def plot_heatmap( + self, target_error: str = 'pauli', ax: Optional[plt.Axes] = None, **plot_kwargs + ) -> plt.Axes: + """plot the heatmap of the target errors. + + Args: + target_error: The error to draw. Must be one of 'xeb', 'pauli', or 'decay_constant' + 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 isinstance(ax, plt.Axes): + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) + heatmap_data = cast( + Mapping[Tuple['cirq.GridQubit', ...], float], self._target_errors(target_error) + ) + + name = f'{target_error} error' if target_error != 'decay_constant' else 'decay constant' + ax.set_title(f'device {name} heatmap') + + vis.TwoQubitInteractionHeatmap(heatmap_data).plot(ax=ax, **plot_kwargs) + if show_plot: + fig.show() + return ax + + def plot_histogram( + self, + target_error: str = 'pauli', + ax: Optional[plt.Axes] = None, + kind: str = 'two_qubit', + **plot_kwargs, + ) -> plt.Axes: + """plot a histogram of target error. + + Args: + target_error: The error to draw. Must be one of 'xeb', 'pauli', or 'decay_constant' + kind: Whether to plot the single-qubit RB errors ('single_qubit') or the + two-qubit inferred errors ('two_qubit') or both ('both'). + 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'. + + Raises: + ValueError: If + - `kind` is not one of 'single_qubit', 'two_qubit', or 'both'. + - `target_error` is not one of 'pauli', 'xeb', or 'decay_constant' + - single qubit error is requested and `target_error` is not 'pauli'. + """ + if kind not in ('single_qubit', 'two_qubit', 'both'): + raise ValueError( + f"kind must be one of 'single_qubit', 'two_qubit', or 'both', not {kind}" + ) + if kind != 'two_qubit' and target_error != 'pauli': + raise ValueError(f'{target_error} is not supported for single qubits') + fig = None + if ax is None: + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) + + alpha = 0.5 if kind == 'both' else 1.0 + if kind == 'single_qubit' or kind == 'both': + self.rb_result.plot_integrated_histogram( + ax=ax, alpha=alpha, label='single qubit', color='green', **plot_kwargs + ) + if kind == 'two_qubit' or kind == 'both': + vis.integrated_histogram( + data=self._target_errors(target_error), + ax=ax, + alpha=alpha, + label='two qubit', + color='blue', + **plot_kwargs, + ) + + if fig is not None: + fig.show(**plot_kwargs) + return ax + def parallel_two_qubit_xeb( sampler: 'cirq.Sampler', + qubits: Optional[Sequence['cirq.GridQubit']] = None, entangling_gate: 'cirq.Gate' = ops.CZ, n_repetitions: int = 10**4, n_combinations: int = 10, @@ -172,6 +328,7 @@ def parallel_two_qubit_xeb( Args: sampler: The quantum engine or simulator to run the circuits. + qubits: Qubits under test. If none, uses all qubits on the sampler's device. entangling_gate: The entangling gate to use. n_repetitions: The number of repetitions to use. n_combinations: The number of combinations to generate. @@ -184,10 +341,17 @@ def parallel_two_qubit_xeb( Returns: A TwoQubitXEBResult object representing the results of the experiment. + + Raises: + ValueError: If qubits are not specified and the sampler has no device. """ rs = value.parse_random_state(random_state) - qubits = _grid_qubits_for_sampler(sampler) + if qubits is None: + qubits = _grid_qubits_for_sampler(sampler) + if qubits is None: + raise ValueError("Couldn't determine qubits from sampler. Please specify them.") + graph = nx.Graph( pair for pair in itertools.combinations(qubits, 2) if _manhattan_distance(*pair) == 1 ) diff --git a/cirq-core/cirq/experiments/two_qubit_xeb_test.py b/cirq-core/cirq/experiments/two_qubit_xeb_test.py index 7bf05eed693..b5ac035e6bd 100644 --- a/cirq-core/cirq/experiments/two_qubit_xeb_test.py +++ b/cirq-core/cirq/experiments/two_qubit_xeb_test.py @@ -12,6 +12,7 @@ # 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 typing import Optional, Sequence, Dict from contextlib import redirect_stdout, redirect_stderr import itertools import io @@ -21,9 +22,11 @@ import numpy as np import networkx as nx +import pandas as pd import pytest import cirq +from cirq.experiments.qubit_characterizations import ParallelRandomizedBenchmarkingResult def _manhattan_distance(qubit1: 'cirq.GridQubit', qubit2: 'cirq.GridQubit') -> int: @@ -51,24 +54,49 @@ def processor(self): return MockProcessor() -@pytest.mark.parametrize( - 'sampler', - [ +def test_parallel_two_qubit_xeb_simulator_without_processor_fails(): + sampler = ( cirq.DensityMatrixSimulator( seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1)) ), - DensityMatrixSimulatorWithProcessor( - seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1)) + ) + + with pytest.raises(ValueError): + _ = 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, + ) + + +@pytest.mark.parametrize( + 'sampler,qubits', + [ + ( + cirq.DensityMatrixSimulator( + seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1)) + ), + cirq.GridQubit.rect(3, 2, 4, 3), + ), + ( + DensityMatrixSimulatorWithProcessor( + seed=0, noise=cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(0.1)) + ), + None, ), ], ) -def test_parallel_two_qubit_xeb(sampler: cirq.Sampler): +def test_parallel_two_qubit_xeb(sampler: cirq.Sampler, qubits: Optional[Sequence[cirq.GridQubit]]): 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, + qubits=qubits, n_repetitions=100, n_combinations=1, n_circuits=1, @@ -82,12 +110,17 @@ def test_parallel_two_qubit_xeb(sampler: cirq.Sampler): @pytest.mark.usefixtures('closefigures') @pytest.mark.parametrize( - 'sampler', [cirq.DensityMatrixSimulator(seed=0), DensityMatrixSimulatorWithProcessor(seed=0)] + 'sampler,qubits', + [ + (cirq.DensityMatrixSimulator(seed=0), cirq.GridQubit.rect(3, 2, 4, 3)), + (DensityMatrixSimulatorWithProcessor(seed=0), None), + ], ) @pytest.mark.parametrize('ax', [None, plt.subplots(1, 1, figsize=(8, 8))[1]]) -def test_plotting(sampler, ax): +def test_plotting(sampler, qubits, ax): res = cirq.experiments.parallel_two_qubit_xeb( sampler=sampler, + qubits=qubits, n_repetitions=1, n_combinations=1, n_circuits=1, @@ -98,3 +131,136 @@ def test_plotting(sampler, 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) + + +_TEST_RESULT = cirq.experiments.TwoQubitXEBResult( + pd.read_csv( + io.StringIO( + """layer_i,pair_i,pair,a,layer_fid,cycle_depths,fidelities,a_std,layer_fid_std +0,0,"(cirq.GridQubit(4, 4), cirq.GridQubit(5, 4))",,0.9,[],[],, +0,1,"(cirq.GridQubit(5, 3), cirq.GridQubit(6, 3))",,0.8,[],[],, +1,0,"(cirq.GridQubit(4, 3), cirq.GridQubit(5, 3))",,0.3,[],[],, +1,1,"(cirq.GridQubit(5, 4), cirq.GridQubit(6, 4))",,0.2,[],[],, +2,0,"(cirq.GridQubit(4, 3), cirq.GridQubit(4, 4))",,0.1,[],[],, +2,1,"(cirq.GridQubit(6, 3), cirq.GridQubit(6, 4))",,0.5,[],[],, +3,0,"(cirq.GridQubit(5, 3), cirq.GridQubit(5, 4))",,0.4,[],[],""" + ), + index_col=[0, 1, 2], + converters={2: lambda s: eval(s)}, + ) +) + + +@pytest.mark.parametrize( + 'q0,q1,pauli', + [ + (cirq.GridQubit(4, 4), cirq.GridQubit(5, 4), 1 / 8), + (cirq.GridQubit(5, 3), cirq.GridQubit(6, 3), 1 / 4), + (cirq.GridQubit(4, 3), cirq.GridQubit(5, 3), 0.8 + 3 / 40), + (cirq.GridQubit(6, 3), cirq.GridQubit(6, 4), 5 / 8), + ], +) +def test_pauli_error(q0: cirq.GridQubit, q1: cirq.GridQubit, pauli: float): + assert _TEST_RESULT.pauli_error()[(q0, q1)] == pytest.approx(pauli) + + +class MockParallelRandomizedBenchmarkingResult(ParallelRandomizedBenchmarkingResult): + def pauli_error(self) -> Dict[cirq.Qid, float]: + return { + cirq.GridQubit(4, 4): 0.01, + cirq.GridQubit(5, 4): 0.02, + cirq.GridQubit(5, 3): 0.03, + cirq.GridQubit(5, 6): 0.04, + cirq.GridQubit(4, 3): 0.05, + cirq.GridQubit(6, 3): 0.06, + cirq.GridQubit(6, 4): 0.07, + } + + +@pytest.mark.parametrize( + 'q0,q1,pauli', + [ + (cirq.GridQubit(4, 4), cirq.GridQubit(5, 4), 1 / 8 - 0.03), + (cirq.GridQubit(5, 3), cirq.GridQubit(6, 3), 1 / 4 - 0.09), + (cirq.GridQubit(4, 3), cirq.GridQubit(5, 3), 0.8 + 3 / 40 - 0.08), + (cirq.GridQubit(6, 3), cirq.GridQubit(6, 4), 5 / 8 - 0.13), + ], +) +def test_inferred_pauli_error(q0: cirq.GridQubit, q1: cirq.GridQubit, pauli: float): + combined_results = cirq.experiments.InferredXEBResult( + rb_result=MockParallelRandomizedBenchmarkingResult({}), xeb_result=_TEST_RESULT + ) + + assert combined_results.inferred_pauli_error()[(q0, q1)] == pytest.approx(pauli) + + +@pytest.mark.parametrize( + 'q0,q1,xeb', + [ + (cirq.GridQubit(4, 4), cirq.GridQubit(5, 4), 0.076), + (cirq.GridQubit(5, 3), cirq.GridQubit(6, 3), 0.128), + (cirq.GridQubit(4, 3), cirq.GridQubit(5, 3), 0.636), + (cirq.GridQubit(6, 3), cirq.GridQubit(6, 4), 0.396), + ], +) +def test_inferred_xeb_error(q0: cirq.GridQubit, q1: cirq.GridQubit, xeb: float): + combined_results = cirq.experiments.InferredXEBResult( + rb_result=MockParallelRandomizedBenchmarkingResult({}), xeb_result=_TEST_RESULT + ) + + assert combined_results.inferred_xeb_error()[(q0, q1)] == pytest.approx(xeb) + + +def test_inferred_single_qubit_pauli(): + combined_results = cirq.experiments.InferredXEBResult( + rb_result=MockParallelRandomizedBenchmarkingResult({}), xeb_result=_TEST_RESULT + ) + + assert combined_results.single_qubit_pauli_error() == { + cirq.GridQubit(4, 4): 0.01, + cirq.GridQubit(5, 4): 0.02, + cirq.GridQubit(5, 3): 0.03, + cirq.GridQubit(5, 6): 0.04, + cirq.GridQubit(4, 3): 0.05, + cirq.GridQubit(6, 3): 0.06, + cirq.GridQubit(6, 4): 0.07, + } + + +@pytest.mark.parametrize( + 'q0,q1,pauli', + [ + (cirq.GridQubit(4, 4), cirq.GridQubit(5, 4), 1 / 8), + (cirq.GridQubit(5, 3), cirq.GridQubit(6, 3), 1 / 4), + (cirq.GridQubit(4, 3), cirq.GridQubit(5, 3), 0.8 + 3 / 40), + (cirq.GridQubit(6, 3), cirq.GridQubit(6, 4), 5 / 8), + ], +) +def test_inferred_two_qubit_pauli(q0: cirq.GridQubit, q1: cirq.GridQubit, pauli: float): + combined_results = cirq.experiments.InferredXEBResult( + rb_result=MockParallelRandomizedBenchmarkingResult({}), xeb_result=_TEST_RESULT + ) + assert combined_results.two_qubit_pauli_error()[(q0, q1)] == pytest.approx(pauli) + + +@pytest.mark.parametrize('ax', [None, plt.subplots(1, 1, figsize=(8, 8))[1]]) +@pytest.mark.parametrize('target_error', ['pauli', 'xeb', 'decay_constant']) +@pytest.mark.parametrize('kind', ['single_qubit', 'two_qubit', 'both', '']) +def test_inferred_plots(ax, target_error, kind): + combined_results = cirq.experiments.InferredXEBResult( + rb_result=MockParallelRandomizedBenchmarkingResult({}), xeb_result=_TEST_RESULT + ) + + combined_results.plot_heatmap(target_error=target_error, ax=ax) + + raise_error = False + if kind not in ('single_qubit', 'two_qubit', 'both'): + raise_error = True + if kind != 'two_qubit' and target_error != 'pauli': + raise_error = True + + if raise_error: + with pytest.raises(ValueError): + combined_results.plot_histogram(target_error=target_error, kind=kind, ax=ax) + else: + combined_results.plot_histogram(target_error=target_error, kind=kind, ax=ax)