diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 9f27af266c7..6bc4262f8c6 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -93,6 +93,8 @@ NO_NOISE, NOISE_MODEL_LIKE, NoiseModel, + NoiseModelFromNoiseProperties, + NoiseProperties, OpIdentifier, SymmetricalQidPair, UNCONSTRAINED_DEVICE, diff --git a/cirq-core/cirq/devices/__init__.py b/cirq-core/cirq/devices/__init__.py index 1af4f58c8a7..49b0708a5f0 100644 --- a/cirq-core/cirq/devices/__init__.py +++ b/cirq-core/cirq/devices/__init__.py @@ -62,6 +62,11 @@ ThermalNoiseModel, ) +from cirq.devices.noise_properties import ( + NoiseModelFromNoiseProperties, + NoiseProperties, +) + from cirq.devices.noise_utils import ( OpIdentifier, decay_constant_to_xeb_fidelity, diff --git a/cirq-core/cirq/devices/noise_properties.py b/cirq-core/cirq/devices/noise_properties.py index b708a24eea6..a2ee49ff0fd 100644 --- a/cirq-core/cirq/devices/noise_properties.py +++ b/cirq-core/cirq/devices/noise_properties.py @@ -1,227 +1,44 @@ -# pylint: disable=wrong-or-nonexistent-copyright-notice -import warnings -from typing import Sequence, TYPE_CHECKING, List -from itertools import product -from cirq import circuits, ops, protocols, devices -import numpy as np +# Copyright 2021 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. + +"""Classes for representing device noise. + +NoiseProperties is an abstract class for capturing metrics of a device that can +be translated into noise models. NoiseModelFromNoiseProperties consumes those +noise models to produce a single noise model which replicates device noise. +""" + +import abc +from typing import Iterable, Sequence, TYPE_CHECKING, List + +from cirq import _import, ops, protocols, devices +from cirq.devices.noise_utils import ( + PHYSICAL_GATE_TAG, +) + +circuits = _import.LazyLoader("circuits", globals(), "cirq.circuits.circuit") if TYPE_CHECKING: - from typing import Iterable import cirq -class NoiseProperties: - def __init__( - self, - *, - t1_ns: float = None, - decay_constant: float = None, - xeb_fidelity: float = None, - pauli_error: float = None, - p00: float = None, - p11: float = None, - ) -> None: - """Creates a NoiseProperties object using the provided metrics. +class NoiseProperties(abc.ABC): + """Noise-defining properties for a quantum device.""" - Only one of decay_constant, xeb_fidelity, and pauli_error should be specified. - - Args: - t1_ns: t1 decay constant in ns - decay_constant: depolarization decay constant - xeb_fidelity: 2-qubit XEB Fidelity - pauli_error: total Pauli error - p00: probability of qubit initialized as zero being measured as zero - p11: probability of qubit initialized as one being measured as one - - Raises: - ValueError: if no metrics are specified - ValueError: if xeb fidelity, pauli error, p00, or p00 are less than 0 or greater than 1 - ValueError: if more than one of pauli error, xeb fidelity, or decay constant is specified - """ - if not any([t1_ns, decay_constant, xeb_fidelity, pauli_error, p00, p11]): - raise ValueError('At least one metric must be specified') - - for metric in [xeb_fidelity, pauli_error, p00, p11]: - if metric is not None and not 0.0 <= metric <= 1.0: - raise ValueError('xeb, pauli error, p00, and p11 must be between 0 and 1') - - if ( - np.count_nonzero( - [metric is not None for metric in [xeb_fidelity, pauli_error, decay_constant]] - ) - > 1 - ): - raise ValueError( - 'Only one of xeb fidelity, pauli error, or decay constant should be defined' - ) - - self._t1_ns = t1_ns - self._p = decay_constant - self._p00 = p00 - self._p11 = p11 - - if pauli_error is not None: - self._p = self.pauli_error_to_decay_constant(pauli_error) - elif xeb_fidelity is not None: - self._p = self.xeb_fidelity_to_decay_constant(xeb_fidelity) - - @property - def decay_constant(self): - return self._p - - @property - def p00(self): - return self._p00 - - @property - def p11(self): - return self._p11 - - @property - def pauli_error(self): - return self.decay_constant_to_pauli_error() - - @property - def t1_ns(self): - return self._t1_ns - - @property - def xeb(self): - return self.decay_constant_to_xeb_fidelity() - - def decay_constant_to_xeb_fidelity(self, num_qubits: int = 2): - """Calculates the XEB fidelity from the depolarization decay constant. - - Args: - num_qubits: number of qubits - """ - if self._p is not None: - N = 2 ** num_qubits - return 1 - ((1 - self._p) * (1 - 1 / N)) - return None - - def decay_constant_to_pauli_error(self, num_qubits: int = 1): - """Calculates pauli error from the depolarization decay constant. - Args: - num_qubits: number of qubits - """ - if self._p is not None: - N = 2 ** num_qubits - return (1 - self._p) * (1 - 1 / N / N) - return None - - def pauli_error_to_decay_constant(self, pauli_error: float, num_qubits: int = 1): - """Calculates depolarization decay constant from pauli error. - - Args: - pauli_error: The pauli error - num_qubits: Number of qubits - """ - N = 2 ** num_qubits - return 1 - (pauli_error / (1 - 1 / N / N)) - - def xeb_fidelity_to_decay_constant(self, xeb_fidelity: float, num_qubits: int = 2): - """Calculates the depolarization decay constant from the XEB noise_properties. - - Args: - xeb_fidelity: The XEB noise_properties - num_qubits: Number of qubits - """ - N = 2 ** num_qubits - return 1 - (1 - xeb_fidelity) / (1 - 1 / N) - - def pauli_error_from_t1(self, t: float, t1_ns: float): - """Calculates the pauli error from amplitude damping. - Unlike the other methods, this computes a specific case (over time t). - - Args: - t: the duration of the gate - t1_ns: the t1 decay constant in ns - """ - t2 = 2 * t1_ns - return (1 - np.exp(-t / t2)) / 2 + (1 - np.exp(-t / t1_ns)) / 4 - - def pauli_error_from_depolarization(self, t: float): - """Calculates the amount of pauli error from depolarization. - Unlike the other methods, this computes a specific case (over time t). - - If pauli error from t1 decay is more than total pauli error, just return the pauli error. - - Args: - t: the duration of the gate - """ - if self.t1_ns is not None: - pauli_error_from_t1 = self.pauli_error_from_t1(t, self.t1_ns) - if self.pauli_error >= pauli_error_from_t1: - return self.pauli_error - pauli_error_from_t1 - else: - warnings.warn( - "Pauli error from T1 decay is greater than total Pauli error", RuntimeWarning - ) - return self.pauli_error - - def average_error(self, num_qubits: int = 1): - """Calculates the average error from the depolarization decay constant. - - Args: - num_qubits: the number of qubits - """ - if self._p is not None: - N = 2 ** num_qubits - return (1 - self._p) * (1 - 1 / N) - return None - - -def get_duration_ns(gate): - # Gate durations based on sycamore durations. - # TODO: pull the gate durations from cirq_google - # or allow users to pass them in - if isinstance(gate, ops.FSimGate): - theta, _ = gate._value_equality_values_() - if np.abs(theta) % (np.pi / 2) == 0: - return 12.0 - return 32.0 - elif isinstance(gate, ops.ISwapPowGate): - return 32.0 - elif isinstance(gate, ops.ZPowGate): - return 0.0 - elif isinstance(gate, ops.MeasurementGate): - return 4000.0 - elif isinstance(gate, ops.WaitGate): - return gate.duration.total_nanos() - return 25.0 - - -def _apply_readout_noise(p00, p11, moments, measurement_qubits): - if p00 is None: - p = 1.0 - gamma = p11 - elif p11 is None: - p = 0.0 - gamma = p00 - else: - p = p11 / (p00 + p11) - gamma = p11 / p - moments.append( - circuits.Moment( - ops.GeneralizedAmplitudeDampingChannel(p=p, gamma=gamma)(q) for q in measurement_qubits - ) - ) - - -def _apply_depol_noise(pauli_error, moments, system_qubits): - - _sq_inds = np.arange(4) - pauli_inds = np.array(list(product(_sq_inds, repeat=1))) - num_inds = len(pauli_inds) - p_other = pauli_error / (num_inds - 1) # probability of X, Y, Z gates - moments.append(circuits.Moment(ops.depolarize(p_other)(q) for q in system_qubits)) - - -def _apply_amplitude_damp_noise(duration, t1, moments, system_qubits): - moments.append( - circuits.Moment(ops.amplitude_damp(1 - np.exp(-duration / t1)).on_each(system_qubits)) - ) + @abc.abstractmethod + def build_noise_models(self) -> List['cirq.NoiseModel']: + """Construct all NoiseModels associated with this NoiseProperties.""" class NoiseModelFromNoiseProperties(devices.NoiseModel): @@ -234,37 +51,78 @@ def __init__(self, noise_properties: NoiseProperties) -> None: Raises: ValueError: if no NoiseProperties object is specified. """ - if noise_properties is not None: - self._noise_properties = noise_properties - else: - raise ValueError('A NoiseProperties object must be specified') + self._noise_properties = noise_properties + self.noise_models = self._noise_properties.build_noise_models() - def noisy_moment( - self, moment: circuits.Moment, system_qubits: Sequence['cirq.Qid'] - ) -> 'cirq.OP_TREE': - moments: List[circuits.Moment] = [] + def virtual_predicate(self, op: 'cirq.Operation') -> bool: + """Returns True if an operation is virtual. - if any( - [protocols.is_measurement(op.gate) for op in moment.operations] - ): # Add readout error before measurement gate - p00 = self._noise_properties.p00 - p11 = self._noise_properties.p11 - measurement_qubits = [ - list(op.qubits)[0] for op in moment.operations if protocols.is_measurement(op.gate) - ] - if p00 is not None or p11 is not None: - _apply_readout_noise(p00, p11, moments, measurement_qubits) - moments.append(moment) - else: - moments.append(moment) - if self._noise_properties.pauli_error is not None: # Add depolarization error# - duration = max([get_duration_ns(op.gate) for op in moment.operations]) - pauli_error = self._noise_properties.pauli_error_from_depolarization(duration) - _apply_depol_noise(pauli_error, moments, system_qubits) + Device-specific subclasses should implement this method to mark any + operations which their device handles outside the quantum hardware. - if self._noise_properties.t1_ns is not None: # Add amplitude damping noise - duration = max([get_duration_ns(op.gate) for op in moment.operations]) - _apply_amplitude_damp_noise( - duration, self._noise_properties.t1_ns, moments, system_qubits - ) - return moments + Args: + op: an operation to check for virtual indicators. + + Returns: + True if `op` is virtual. + """ + return False + + def noisy_moments( + self, moments: Iterable['cirq.Moment'], system_qubits: Sequence['cirq.Qid'] + ) -> Sequence['cirq.OP_TREE']: + # Split multi-qubit measurements into single-qubit measurements. + # These will be recombined after noise is applied. + split_measure_moments = [] + multi_measurements = {} + for moment in moments: + split_measure_ops = [] + for op in moment: + if not protocols.is_measurement(op): + split_measure_ops.append(op) + continue + m_key = protocols.measurement_key_obj(op) + multi_measurements[m_key] = op + for q in op.qubits: + split_measure_ops.append(ops.measure(q, key=m_key)) + split_measure_moments.append(circuits.Moment(split_measure_ops)) + + # Append PHYSICAL_GATE_TAG to non-virtual ops in the input circuit, + # using `self.virtual_predicate` to determine virtuality. + new_moments = [] + for moment in split_measure_moments: + virtual_ops = {op for op in moment if self.virtual_predicate(op)} + physical_ops = [ + op.with_tags(PHYSICAL_GATE_TAG) for op in moment if op not in virtual_ops + ] + # Both physical and virtual operations remain in the circuit, but + # only ops with PHYSICAL_GATE_TAG will receive noise. + if virtual_ops: + # Only subclasses will trigger this case. + new_moments.append(circuits.Moment(virtual_ops)) # coverage: ignore + if physical_ops: + new_moments.append(circuits.Moment(physical_ops)) + + split_measure_circuit = circuits.Circuit(new_moments) + + # Add noise from each noise model. The PHYSICAL_GATE_TAGs added + # previously allow noise models to distinguish physical gates from + # those added by other noise models. + noisy_circuit = split_measure_circuit.copy() + for model in self.noise_models: + noisy_circuit = noisy_circuit.with_noise(model) + + # Recombine measurements. + final_moments = [] + for moment in noisy_circuit: + combined_measure_ops = [] + restore_keys = set() + for op in moment: + if not protocols.is_measurement(op): + combined_measure_ops.append(op) + continue + restore_keys.add(protocols.measurement_key_obj(op)) + for key in restore_keys: + combined_measure_ops.append(multi_measurements[key]) + final_moments.append(circuits.Moment(combined_measure_ops)) + return final_moments diff --git a/cirq-core/cirq/devices/noise_properties_test.py b/cirq-core/cirq/devices/noise_properties_test.py index eea1ed4d8d8..c848af659b3 100644 --- a/cirq-core/cirq/devices/noise_properties_test.py +++ b/cirq-core/cirq/devices/noise_properties_test.py @@ -1,298 +1,62 @@ -# pylint: disable=wrong-or-nonexistent-copyright-notice -import pytest +# Copyright 2021 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 List, Tuple import cirq -from cirq.testing import assert_equivalent_op_tree + +from cirq.devices.insertion_noise_model import InsertionNoiseModel from cirq.devices.noise_properties import ( NoiseProperties, NoiseModelFromNoiseProperties, - get_duration_ns, ) -import numpy as np - - -def test_invalid_arguments(): - with pytest.raises(ValueError, match='At least one metric must be specified'): - NoiseProperties() - - with pytest.raises(ValueError, match='xeb, pauli error, p00, and p11 must be between 0 and 1'): - NoiseProperties(p00=1.2) - - with pytest.raises(ValueError, match='xeb, pauli error, p00, and p11 must be between 0 and 1'): - NoiseProperties(pauli_error=-0.2) - - with pytest.raises( - ValueError, - match='Only one of xeb fidelity, pauli error, or decay constant should be defined', - ): - NoiseProperties(pauli_error=0.2, xeb_fidelity=0.5) - - with pytest.raises(ValueError, match='A NoiseProperties object must be specified'): - NoiseModelFromNoiseProperties(None) - - -def test_constructor_and_metrics(): - prop = NoiseProperties(p00=0.2) - assert prop.xeb is None - assert prop.pauli_error is None - assert prop.decay_constant is None - assert prop.average_error() is None - - # These and other metrics in the file are purely for testing and - # do not necessarily represent actual hardware behavior - xeb_fidelity = 0.95 - p00 = 0.1 - t1_ns = 200.0 - - # Create fidelity object with a defined XEB fidelity - from_xeb = NoiseProperties(xeb_fidelity=xeb_fidelity, p00=p00, t1_ns=t1_ns) - - assert from_xeb.p00 == p00 - assert from_xeb.p11 is None - assert from_xeb.t1_ns == t1_ns - assert from_xeb.xeb == xeb_fidelity - - # Create another fidelity object with the decay constant from the first one - decay_constant_from_xeb = from_xeb.decay_constant - - from_decay = NoiseProperties(decay_constant=decay_constant_from_xeb) - - # Check that their depolarization metrics match - assert np.isclose(xeb_fidelity, from_decay.xeb) - assert np.isclose(from_xeb.pauli_error, from_decay.pauli_error) - assert np.isclose(from_xeb.average_error(), from_decay.average_error()) - - -def test_gate_durations(): - assert get_duration_ns(cirq.X) == 25.0 - assert get_duration_ns(cirq.FSimGate(3 * np.pi / 2, np.pi / 6)) == 12.0 - assert get_duration_ns(cirq.FSimGate(3 * np.pi / 4, np.pi / 6)) == 32.0 - assert get_duration_ns(cirq.ISWAP) == 32.0 - assert get_duration_ns(cirq.ZPowGate(exponent=5)) == 0.0 - assert get_duration_ns(cirq.MeasurementGate(1, 'a')) == 4000.0 - - wait_gate = cirq.WaitGate(cirq.Duration(nanos=4)) - assert get_duration_ns(wait_gate) == 4.0 - - assert get_duration_ns(cirq.CZ) == 25.0 - - -def test_readout_error(): - p00 = 0.05 - p11 = 0.1 - - p = p11 / (p00 + p11) - gamma = p11 / p - - # Create qubits and circuit - qubits = [cirq.LineQubit(0), cirq.LineQubit(1)] - circuit = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.H(qubits[1])]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - ) - - # Create noise model from NoiseProperties object with specified noise - prop = NoiseProperties(p00=p00, p11=p11) - noise_model = NoiseModelFromNoiseProperties(prop) - - noisy_circuit = cirq.Circuit(noise_model.noisy_moments(circuit, qubits)) - - # Insert expected channels to circuit - expected_circuit = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.H(qubits[1])]), - cirq.Moment([cirq.GeneralizedAmplitudeDampingChannel(p=p, gamma=gamma).on_each(qubits)]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - ) - - assert_equivalent_op_tree(expected_circuit, noisy_circuit) - - # Create Noise Model with just p00 - prop_p00 = NoiseProperties(p00=p00) - noise_model_p00 = NoiseModelFromNoiseProperties(prop_p00) - - noisy_circuit_p00 = cirq.Circuit(noise_model_p00.noisy_moments(circuit, qubits)) - - # Insert expected channels to circuit - expected_circuit_p00 = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.H(qubits[1])]), - cirq.Moment([cirq.GeneralizedAmplitudeDampingChannel(p=0.0, gamma=p00).on_each(qubits)]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - ) - - assert_equivalent_op_tree(expected_circuit_p00, noisy_circuit_p00) - - # Create Noise Model with just p11 - prop_p11 = NoiseProperties(p11=p11) - noise_model_p11 = NoiseModelFromNoiseProperties(prop_p11) - - noisy_circuit_p11 = cirq.Circuit(noise_model_p11.noisy_moments(circuit, qubits)) - - # Insert expected channels to circuit - expected_circuit_p11 = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.H(qubits[1])]), - cirq.Moment([cirq.GeneralizedAmplitudeDampingChannel(p=1.0, gamma=p11).on_each(qubits)]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - ) - - assert_equivalent_op_tree(expected_circuit_p11, noisy_circuit_p11) - - -def test_depolarization_error(): - # Account for floating point errors - # Needs Cirq issue 3965 to be resolved - pauli_error = 0.09999999999999998 - - # Create qubits and circuit - qubits = [cirq.LineQubit(0), cirq.LineQubit(1)] - circuit = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.H(qubits[1])]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - ) - - # Create noise model from NoiseProperties object with specified noise - prop = NoiseProperties(pauli_error=pauli_error) - noise_model = NoiseModelFromNoiseProperties(prop) - - noisy_circuit = cirq.Circuit(noise_model.noisy_moments(circuit, qubits)) - - # Insert expected channels to circuit - expected_circuit = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.depolarize(pauli_error / 3).on_each(qubits)]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.depolarize(pauli_error / 3).on_each(qubits)]), - cirq.Moment([cirq.H(qubits[1])]), - cirq.Moment([cirq.depolarize(pauli_error / 3).on_each(qubits)]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - cirq.Moment([cirq.depolarize(pauli_error / 3).on_each(qubits)]), - ) - assert_equivalent_op_tree(expected_circuit, noisy_circuit) - - -def test_ampl_damping_error(): - t1_ns = 200.0 - - # Create qubits and circuit - qubits = [cirq.LineQubit(0), cirq.LineQubit(1)] - circuit = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.FSimGate(5 * np.pi / 2, np.pi).on_each(qubits)]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - ) - - # Create noise model from NoiseProperties object with specified noise - prop = NoiseProperties(t1_ns=t1_ns) - noise_model = NoiseModelFromNoiseProperties(prop) - - noisy_circuit = cirq.Circuit(noise_model.noisy_moments(circuit, qubits)) - - # Insert expected channels to circuit - expected_circuit = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-25.0 / t1_ns)).on_each(qubits)]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-25.0 / t1_ns)).on_each(qubits)]), - cirq.Moment([cirq.FSimGate(np.pi / 2, np.pi).on_each(qubits)]), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-12.0 / t1_ns)).on_each(qubits)]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-4000.0 / t1_ns)).on_each(qubits)]), - ) - assert_equivalent_op_tree(expected_circuit, noisy_circuit) +from cirq.devices.noise_utils import ( + OpIdentifier, + PHYSICAL_GATE_TAG, +) -def test_combined_error(): - # Helper function to calculate pauli error from depolarization - def pauli_error_from_depolarization(pauli_error, t1_ns, duration): - t2 = 2 * t1_ns - pauli_error_from_t1 = (1 - np.exp(-duration / t2)) / 2 + (1 - np.exp(-duration / t1_ns)) / 4 - if pauli_error >= pauli_error_from_t1: - return pauli_error - pauli_error_from_t1 - return pauli_error +# These properties are for testing purposes only - they are not representative +# of device behavior for any existing hardware. +class SampleNoiseProperties(NoiseProperties): + def __init__(self, system_qubits: List[cirq.Qid], qubit_pairs: List[Tuple[cirq.Qid, cirq.Qid]]): + self.qubits = system_qubits + self.qubit_pairs = qubit_pairs - t1_ns = 2000.0 - p11 = 0.01 + def build_noise_models(self): + add_h = InsertionNoiseModel({OpIdentifier(cirq.Gate, q): cirq.H(q) for q in self.qubits}) + add_iswap = InsertionNoiseModel( + {OpIdentifier(cirq.Gate, *qs): cirq.ISWAP(*qs) for qs in self.qubit_pairs} + ) + return [add_h, add_iswap] - # Account for floating point errors - # Needs Cirq issue 3965 to be resolved - pauli_error = 0.019999999999999962 - # Create qubits and circuit - qubits = [cirq.LineQubit(0), cirq.LineQubit(1)] +def test_sample_model(): + q0, q1 = cirq.LineQubit.range(2) + props = SampleNoiseProperties([q0, q1], [(q0, q1), (q1, q0)]) + model = NoiseModelFromNoiseProperties(props) circuit = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment([cirq.measure(qubits[0], key='q0')]), - cirq.Moment([cirq.ISwapPowGate().on_each(qubits)]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), + cirq.X(q0), cirq.CNOT(q0, q1), cirq.Z(q1), cirq.measure(q0, q1, key='meas') ) - - # Create noise model from NoiseProperties object with specified noise - prop = NoiseProperties(t1_ns=t1_ns, p11=p11, pauli_error=pauli_error) - noise_model = NoiseModelFromNoiseProperties(prop) - - with pytest.warns( - RuntimeWarning, match='Pauli error from T1 decay is greater than total Pauli error' - ): - noisy_circuit = cirq.Circuit(noise_model.noisy_moments(circuit, qubits)) - - # Insert expected channels to circuit + noisy_circuit = circuit.with_noise(model) expected_circuit = cirq.Circuit( - cirq.Moment([cirq.X(qubits[0])]), - cirq.Moment( - [ - cirq.depolarize( - pauli_error_from_depolarization(pauli_error, t1_ns, 25.0) / 3 - ).on_each(qubits) - ] - ), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-25.0 / t1_ns)).on_each(qubits)]), - cirq.Moment([cirq.CNOT(qubits[0], qubits[1])]), - cirq.Moment( - [ - cirq.depolarize( - pauli_error_from_depolarization(pauli_error, t1_ns, 25.0) / 3 - ).on_each(qubits) - ] - ), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-25.0 / t1_ns)).on_each(qubits)]), - cirq.Moment([cirq.GeneralizedAmplitudeDampingChannel(p=1.0, gamma=p11).on(qubits[0])]), - cirq.Moment([cirq.measure(qubits[0], key='q0')]), - cirq.Moment( - [ - cirq.depolarize( - pauli_error_from_depolarization(pauli_error, t1_ns, 4000.0) / 3 - ).on_each(qubits) - ] - ), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-4000.0 / t1_ns)).on_each(qubits)]), - cirq.Moment([cirq.ISwapPowGate().on_each(qubits)]), - cirq.Moment( - [ - cirq.depolarize( - pauli_error_from_depolarization(pauli_error, t1_ns, 32.0) / 3 - ).on_each(qubits) - ] - ), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-32.0 / t1_ns)).on_each(qubits)]), - cirq.Moment([cirq.GeneralizedAmplitudeDampingChannel(p=1.0, gamma=p11).on_each(qubits)]), - cirq.Moment([cirq.measure(qubits[0], key='q0'), cirq.measure(qubits[1], key='q1')]), - cirq.Moment( - [ - cirq.depolarize( - pauli_error_from_depolarization(pauli_error, t1_ns, 4000.0) / 3 - ).on_each(qubits) - ] - ), - cirq.Moment([cirq.amplitude_damp(1 - np.exp(-4000.0 / t1_ns)).on_each(qubits)]), - ) - assert_equivalent_op_tree(expected_circuit, noisy_circuit) + cirq.Moment(cirq.X(q0).with_tags(PHYSICAL_GATE_TAG)), + cirq.Moment(cirq.H(q0)), + cirq.Moment(cirq.CNOT(q0, q1).with_tags(PHYSICAL_GATE_TAG)), + cirq.Moment(cirq.ISWAP(q0, q1)), + cirq.Moment(cirq.Z(q1).with_tags(PHYSICAL_GATE_TAG)), + cirq.Moment(cirq.H(q1)), + cirq.Moment(cirq.measure(q0, q1, key='meas')), + cirq.Moment(cirq.H(q0), cirq.H(q1)), + ) + assert noisy_circuit == expected_circuit diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py index 38cf7e0b558..9df089bc156 100644 --- a/cirq-core/cirq/protocols/json_test_data/spec.py +++ b/cirq-core/cirq/protocols/json_test_data/spec.py @@ -179,6 +179,7 @@ 'ParamDictType', # utility: 'CliffordSimulator', + 'NoiseModelFromNoiseProperties', 'Simulator', 'StabilizerSampler', 'Unique', diff --git a/cirq-google/cirq_google/experimental/noise_models/calibration_to_noise_properties.py b/cirq-google/cirq_google/experimental/noise_models/calibration_to_noise_properties.py deleted file mode 100644 index 06f202a1b7f..00000000000 --- a/cirq-google/cirq_google/experimental/noise_models/calibration_to_noise_properties.py +++ /dev/null @@ -1,117 +0,0 @@ -# pylint: disable=wrong-or-nonexistent-copyright-notice -import cirq_google -import numpy as np -from cirq.devices.noise_properties import NoiseProperties - - -def _xeb_fidelity_to_decay_constant(xeb_fidelity, num_qubits=2): - # Converts from XEB Fidelity to depolarization decay constant - if xeb_fidelity is not None: - N = 2 ** num_qubits # Dimension of Hilbert space - return 1 - (1 - xeb_fidelity) / (1 - 1 / N) - return None - - -def _rb_average_error_to_decay_constant(rb_average_error, num_qubits: int = 1): - # Converts from randomized benchmarking average error to depolarization decay constant - if rb_average_error is not None: - N = 2 ** num_qubits # Dimension of Hilbert space - return 1 - rb_average_error / (1 - 1 / N) - else: - return None - - -def _rb_pauli_error_to_decay_constant(rb_pauli_error, num_qubits: int = 1): - # Converts from randomized benchmarking pauli error to depolarization decay constant - if rb_pauli_error is not None: - N = 2 ** num_qubits # Dimension of Hilbert space - return 1 - rb_pauli_error / (1 - 1 / N ** 2) - else: - return None - - -def _within_tolerance(val_1, val_2, tolerance): - # Helper function to check if two values are within tolerance - if val_1 is None or val_2 is None: - return True - return abs(val_1 - val_2) <= tolerance - - -def _unpack_from_calibration(metric_name, calibration): - # Gets the average (over all qubits) of each metric - # TODO: Add support for per-qubit noise - if metric_name in calibration.keys(): - return np.mean([value for qubit, value in calibration[metric_name].items()]) - else: - return None - - -def noise_properties_from_calibration( - calibration: cirq_google.Calibration, validate: bool = True, tolerance: float = 0.01 -): - """Translates between a Calibration object and a NoiseProperties object. - The NoiseProperties object can then be used as input to the NoiseModelFromNoiseProperties - class (cirq.devices.noise_properties) to create a NoiseModel that can be used with a simulator. - - If the validate argument is set to false, the depolarization decay constant will be calculated - from the RB Pauli error if defined, the XEB Fidelity if RB Pauli error is not defined, or the - RB Average error if the others are not defined. - - Args: - calibration: a Calibration object with hardware metrics - validate: whether or not to check that the depolarization decay constants calculated from - RB Pauli error, RB average error, & XEB Fidelity agree to within a given tolerance - tolerance: threshold for validating decay constants frmo RB Pauli error, RB Average error, - and XEB fidelity. - - Raises: - ValueError: decay constants from RB Average Error and RB Pauli Error aren't within tolerance - - ValueError: decay constants from RB Pauli Error and XEB Fidelity aren't within tolerance - - ValueError: decay constant from RB Pauli Error and XEB Fidelity aren't within tolerance - """ - - # Unpack all values from Calibration object - t1_micros = _unpack_from_calibration('single_qubit_idle_t1_micros', calibration) - t1_nanos = t1_micros * 1000 if t1_micros is not None else None - xeb_error = _unpack_from_calibration('xeb', calibration) - xeb_fidelity = 1 - xeb_error if xeb_error is not None else None - rb_pauli_error = _unpack_from_calibration('single_qubit_rb_pauli_error_per_gate', calibration) - rb_average_error = _unpack_from_calibration( - 'single_qubit_rb_average_error_per_gate', calibration - ) - p00 = _unpack_from_calibration('single_qubit_p00_error', calibration) - p11 = _unpack_from_calibration('single_qubit_p11_error', calibration) - decay_constant_pauli = _rb_pauli_error_to_decay_constant(rb_pauli_error) - - decay_constant_average = _rb_average_error_to_decay_constant(rb_average_error) - - if validate: # Will throw error if metrics aren't compatible - if not _within_tolerance(decay_constant_pauli, decay_constant_average, tolerance): - raise ValueError( - f'Decay constant from RB Pauli error: {decay_constant_pauli}, ' - f'decay constant from RB Average error: {decay_constant_average}. ' - 'If validation is disabled, RB Pauli error will be used.' - ) - decay_constant_from_xeb = _xeb_fidelity_to_decay_constant(xeb_fidelity) - if not _within_tolerance(decay_constant_from_xeb, decay_constant_pauli, tolerance): - raise ValueError( - f'Decay constant from RB Pauli error: {decay_constant_pauli}, ' - f'decay constant from XEB Fidelity: {decay_constant_from_xeb}. ' - 'If validation is disabled, RB Pauli error will be used.' - ) - if not _within_tolerance(decay_constant_from_xeb, decay_constant_average, tolerance): - raise ValueError( - f'Decay constant from RB Average error: {decay_constant_average}, ' - f'decay constant from XEB Fidelity: {decay_constant_from_xeb}. ' - 'If validation is disabled, XEB Fidelity will be used.' - ) - - if decay_constant_pauli is not None: # can't define both decay constant and xeb - return NoiseProperties( - t1_ns=t1_nanos, decay_constant=decay_constant_pauli, p00=p00, p11=p11 - ) - if xeb_fidelity is not None: - return NoiseProperties(t1_ns=t1_nanos, xeb_fidelity=xeb_fidelity, p00=p00, p11=p11) - return NoiseProperties(t1_ns=t1_nanos, decay_constant=decay_constant_average, p00=p00, p11=p11) diff --git a/cirq-google/cirq_google/experimental/noise_models/calibration_to_noise_properties_test.py b/cirq-google/cirq_google/experimental/noise_models/calibration_to_noise_properties_test.py deleted file mode 100644 index 5412130a321..00000000000 --- a/cirq-google/cirq_google/experimental/noise_models/calibration_to_noise_properties_test.py +++ /dev/null @@ -1,284 +0,0 @@ -# pylint: disable=wrong-or-nonexistent-copyright-notice -import pytest -import cirq_google -from cirq_google.api import v2 -from cirq_google.experimental.noise_models.calibration_to_noise_properties import ( - noise_properties_from_calibration, -) -from google.protobuf.text_format import Merge -import numpy as np - - -def test_noise_properties_from_calibration(): - xeb_error_1 = 0.999 - xeb_error_2 = 0.996 - - p00_1 = 0.001 - p00_2 = 0.002 - p00_3 = 0.003 - - t1_1 = 0.005 - t1_2 = 0.007 - t1_3 = 0.003 - - _CALIBRATION_DATA = Merge( - f""" - timestamp_ms: 1579214873, - metrics: [{{ - name: 'xeb', - targets: ['0_0', '0_1'], - values: [{{ - double_val: {xeb_error_1} - }}] - }}, {{ - name: 'xeb', - targets: ['0_0', '1_0'], - values: [{{ - double_val:{xeb_error_2} - }}] - }}, {{ - name: 'single_qubit_p00_error', - targets: ['0_0'], - values: [{{ - double_val: {p00_1} - }}] - }}, {{ - name: 'single_qubit_p00_error', - targets: ['0_1'], - values: [{{ - double_val: {p00_2} - }}] - }}, {{ - name: 'single_qubit_p00_error', - targets: ['1_0'], - values: [{{ - double_val: {p00_3} - }}] - }}, {{ - name: 'single_qubit_readout_separation_error', - targets: ['0_0'], - values: [{{ - double_val: .004 - }}] - }}, {{ - name: 'single_qubit_readout_separation_error', - targets: ['0_1'], - values: [{{ - double_val: .005 - }}] - }},{{ - name: 'single_qubit_readout_separation_error', - targets: ['1_0'], - values: [{{ - double_val: .006 - }}] - }}, {{ - name: 'single_qubit_idle_t1_micros', - targets: ['0_0'], - values: [{{ - double_val: {t1_1} - }}] - }}, {{ - name: 'single_qubit_idle_t1_micros', - targets: ['0_1'], - values: [{{ - double_val: {t1_2} - }}] - }}, {{ - name: 'single_qubit_idle_t1_micros', - targets: ['1_0'], - values: [{{ - double_val: {t1_3} - }}] - }}] -""", - v2.metrics_pb2.MetricsSnapshot(), - ) - - # Create NoiseProperties object from Calibration - calibration = cirq_google.Calibration(_CALIBRATION_DATA) - prop = noise_properties_from_calibration(calibration) - - expected_t1_nanos = np.mean([t1_1, t1_2, t1_3]) * 1000 - expected_xeb_fidelity = 1 - np.mean([xeb_error_1, xeb_error_2]) - expected_p00 = np.mean([p00_1, p00_2, p00_3]) - - assert np.isclose(prop.t1_ns, expected_t1_nanos) - assert np.isclose(prop.xeb, expected_xeb_fidelity) - assert np.isclose(prop.p00, expected_p00) - - -def test_from_calibration_rb(): - rb_pauli_1 = 0.001 - rb_pauli_2 = 0.002 - rb_pauli_3 = 0.003 - - _CALIBRATION_DATA_RB = Merge( - f""" - timestamp_ms: 1579214873, - metrics: [{{ - - name: 'single_qubit_rb_pauli_error_per_gate', - targets: ['0_0'], - values: [{{ - double_val: {rb_pauli_1} - }}] - }}, {{ - name: 'single_qubit_rb_pauli_error_per_gate', - targets: ['0_1'], - values: [{{ - double_val: {rb_pauli_2} - }}] - }}, {{ - name: 'single_qubit_rb_pauli_error_per_gate', - targets: ['1_0'], - values: [{{ - double_val: {rb_pauli_3} - }}] - }}] - """, - v2.metrics_pb2.MetricsSnapshot(), - ) - - # Create NoiseProperties object from Calibration - rb_calibration = cirq_google.Calibration(_CALIBRATION_DATA_RB) - rb_noise_prop = noise_properties_from_calibration(rb_calibration) - - average_pauli_rb = np.mean([rb_pauli_1, rb_pauli_2, rb_pauli_3]) - assert np.isclose(average_pauli_rb, rb_noise_prop.pauli_error) - - -def test_validate_calibration(): - # RB Pauli error and RB Average Error disagree - rb_pauli_error = 0.05 - rb_average_error = 0.1 - - decay_constant_pauli = 1 - rb_pauli_error / (1 - 1 / 4) - decay_constant_average = 1 - rb_average_error / (1 - 1 / 2) - _CALIBRATION_DATA_PAULI_AVERAGE = Merge( - f""" - timestamp_ms: 1579214873, - metrics: [{{ - - name: 'single_qubit_rb_pauli_error_per_gate', - targets: ['0_0'], - values: [{{ - double_val: {rb_pauli_error} - }}] - }}, {{ - name: 'single_qubit_rb_average_error_per_gate', - targets: ['0_1'], - values: [{{ - double_val: {rb_average_error} - }}] - }}] - """, - v2.metrics_pb2.MetricsSnapshot(), - ) - bad_calibration_pauli_average = cirq_google.Calibration(_CALIBRATION_DATA_PAULI_AVERAGE) - with pytest.raises( - ValueError, - match=f'Decay constant from RB Pauli error: {decay_constant_pauli}, ' - f'decay constant from RB Average error: {decay_constant_average}. ' - 'If validation is disabled, RB Pauli error will be used.', - ): - noise_properties_from_calibration(bad_calibration_pauli_average) - - assert np.isclose( - noise_properties_from_calibration( - bad_calibration_pauli_average, validate=False - ).pauli_error, - rb_pauli_error, - ) - - # RB Pauli Error and XEB Fidelity disagree - xeb_fidelity = 0.99 - - decay_constant_from_xeb = 1 - (1 - xeb_fidelity) / (1 - 1 / 4) - - _CALIBRATION_DATA_PAULI_XEB = Merge( - f""" - timestamp_ms: 1579214873, - metrics: [{{ - - name: 'single_qubit_rb_pauli_error_per_gate', - targets: ['0_0'], - values: [{{ - double_val: {rb_pauli_error} - }}] - }}, {{ - name: 'xeb', - targets: ['0_0', '1_0'], - values: [{{ - double_val:{1 - xeb_fidelity} - }}] - }}] - """, - v2.metrics_pb2.MetricsSnapshot(), - ) - - bad_calibration_pauli_xeb = cirq_google.Calibration(_CALIBRATION_DATA_PAULI_XEB) - with pytest.raises( - ValueError, - match=f'Decay constant from RB Pauli error: {decay_constant_pauli}, ' - f'decay constant from XEB Fidelity: {decay_constant_from_xeb}. ' - 'If validation is disabled, RB Pauli error will be used.', - ): - noise_properties_from_calibration(bad_calibration_pauli_xeb) - - # RB Average Error and XEB Fidelity disagree - _CALIBRATION_DATA_AVERAGE_XEB = Merge( - f""" - timestamp_ms: 1579214873, - metrics: [{{ - - name: 'single_qubit_rb_average_error_per_gate', - targets: ['0_0'], - values: [{{ - double_val: {rb_average_error} - }}] - }}, {{ - name: 'xeb', - targets: ['0_0', '1_0'], - values: [{{ - double_val:{1 - xeb_fidelity} - }}] - }}] - """, - v2.metrics_pb2.MetricsSnapshot(), - ) - - bad_calibration_average_xeb = cirq_google.Calibration(_CALIBRATION_DATA_AVERAGE_XEB) - with pytest.raises( - ValueError, - match=f'Decay constant from RB Average error: {decay_constant_average}, ' - f'decay constant from XEB Fidelity: {decay_constant_from_xeb}. ' - 'If validation is disabled, XEB Fidelity will be used.', - ): - noise_properties_from_calibration(bad_calibration_average_xeb) - - assert np.isclose( - noise_properties_from_calibration(bad_calibration_average_xeb, validate=False).xeb, - xeb_fidelity, - ) - - # Calibration data with no RB error or XEB fidelity - t1 = 2.0 # microseconds - - _CALIBRATION_DATA_T1 = Merge( - f""" - timestamp_ms: 1579214873, - metrics: [{{ - name: 'single_qubit_idle_t1_micros', - targets: ['0_0'], - values: [{{ - double_val: {t1} - }}] - }}] - """, - v2.metrics_pb2.MetricsSnapshot(), - ) - - calibration_t1 = cirq_google.Calibration(_CALIBRATION_DATA_T1) - - assert np.isclose(noise_properties_from_calibration(calibration_t1).t1_ns, t1 * 1000) diff --git a/cirq-google/cirq_google/experimental/noise_models/compare_generated_noise_to_metrics.py b/cirq-google/cirq_google/experimental/noise_models/compare_generated_noise_to_metrics.py deleted file mode 100644 index 2996d5fb75d..00000000000 --- a/cirq-google/cirq_google/experimental/noise_models/compare_generated_noise_to_metrics.py +++ /dev/null @@ -1,83 +0,0 @@ -# pylint: disable=wrong-or-nonexistent-copyright-notice -# coverage: ignore -import cirq_google -import numpy as np -from cirq.devices.noise_properties import NoiseModelFromNoiseProperties -from cirq_google.experimental.noise_models.calibration_to_noise_properties import ( - noise_properties_from_calibration, -) -import cirq -import pandas as pd - - -def compare_generated_noise_to_metrics( - calibration: cirq_google.Calibration, validate: bool = True, tolerance: float = 0.01 -): - """Compares the metrics from a Calibration object to those measured from a Noise Model - created with cirq.devices.noise_properties_from_calibration. - - Args: - calibration: Calibration object to be turned into a Noise Model - validate: check calibration metrics are in agreement (arg for noise_properties_from_calibration) - tolerance: tolerance for calibration metrics (argument for noise_properties_from_calibration) - - Returns: - df: Pandas dataframe comparing input and measured values for each calibration metric - """ - # Create Noise Model from Calibration object - noise_prop = noise_properties_from_calibration(calibration, validate, tolerance) - noise_model = NoiseModelFromNoiseProperties(noise_prop) - - p00 = noise_prop.p00 - p11 = noise_prop.p11 - xeb_fidelity = noise_prop.xeb - pauli_error = noise_prop.pauli_error - t1_ns = noise_prop.t1_ns - average_error = noise_prop.average_error() - - qubits = [cirq.LineQubit(0), cirq.LineQubit(1)] - - # Create simulator for experiments with noise model - simulator = cirq.sim.DensityMatrixSimulator(noise=noise_model) - - # Experiments to measure metrics - estimate_readout = cirq.experiments.estimate_single_qubit_readout_errors( - simulator, qubits=[qubits[0]], repetitions=1000 - ) - - xeb_result = cirq.experiments.cross_entropy_benchmarking( - simulator, qubits, num_circuits=50, repetitions=1000 - ) - measured_xeb = np.mean([datum.xeb_fidelity for datum in xeb_result.data]) - decay_constant = xeb_result.depolarizing_model().cycle_depolarization - - output = [] - - if p00 is not None: - output.append(['p00', p00, estimate_readout.zero_state_errors[cirq.LineQubit(0)]]) - if p11 is not None: - output.append(['p11', p11, estimate_readout.one_state_errors[cirq.LineQubit(0)]]) - if xeb_fidelity is not None: - output.append(['XEB Fidelity', xeb_fidelity, measured_xeb]) - if t1_ns is not None: - t1_results = cirq.experiments.t1_decay( - simulator, - qubit=qubits[0], - num_points=100, - repetitions=100, - min_delay=cirq.Duration(nanos=10), - max_delay=cirq.Duration(micros=1), - ) - output.append(['T1', t1_ns, t1_results.constant]) - if pauli_error is not None: - N = 2 # Dimension of Hilbert Space - measured_pauli_error = (1 - decay_constant) * (1 - 1 / N / N) - output.append(['Pauli Error', pauli_error, measured_pauli_error]) - if average_error is not None: - N = 2 # Dimension of Hilbert Space - measured_average_error = (1 - decay_constant) * (1 - 1 / N) - output.append(['Average Error', average_error, measured_average_error]) - - columns = ["Metric", "Initial value", "Measured value"] - df = pd.DataFrame(output, columns=columns) - return df