diff --git a/cirq-core/cirq/devices/noise_properties.py b/cirq-core/cirq/devices/noise_properties.py index a2ee49ff0fdc..1c10d31bb52e 100644 --- a/cirq-core/cirq/devices/noise_properties.py +++ b/cirq-core/cirq/devices/noise_properties.py @@ -22,7 +22,7 @@ import abc from typing import Iterable, Sequence, TYPE_CHECKING, List -from cirq import _import, ops, protocols, devices +from cirq import _compat, _import, ops, protocols, devices from cirq.devices.noise_utils import ( PHYSICAL_GATE_TAG, ) @@ -54,7 +54,7 @@ def __init__(self, noise_properties: NoiseProperties) -> None: self._noise_properties = noise_properties self.noise_models = self._noise_properties.build_noise_models() - def virtual_predicate(self, op: 'cirq.Operation') -> bool: + def is_virtual(self, op: 'cirq.Operation') -> bool: """Returns True if an operation is virtual. Device-specific subclasses should implement this method to mark any @@ -68,6 +68,10 @@ def virtual_predicate(self, op: 'cirq.Operation') -> bool: """ return False + @_compat.deprecated(deadline='v0.16', fix='Use is_virtual instead.') + def virtual_predicate(self, op: 'cirq.Operation') -> bool: + return self.is_virtual(op) + def noisy_moments( self, moments: Iterable['cirq.Moment'], system_qubits: Sequence['cirq.Qid'] ) -> Sequence['cirq.OP_TREE']: @@ -91,7 +95,7 @@ def noisy_moments( # 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)} + virtual_ops = {op for op in moment if self.is_virtual(op)} physical_ops = [ op.with_tags(PHYSICAL_GATE_TAG) for op in moment if op not in virtual_ops ] diff --git a/cirq-core/cirq/devices/noise_properties_test.py b/cirq-core/cirq/devices/noise_properties_test.py index c848af659b3a..582e5754e89e 100644 --- a/cirq-core/cirq/devices/noise_properties_test.py +++ b/cirq-core/cirq/devices/noise_properties_test.py @@ -60,3 +60,11 @@ def test_sample_model(): cirq.Moment(cirq.H(q0), cirq.H(q1)), ) assert noisy_circuit == expected_circuit + + +def test_deprecated_virtual_predicate(): + q0, q1 = cirq.LineQubit.range(2) + props = SampleNoiseProperties([q0, q1], [(q0, q1), (q1, q0)]) + model = NoiseModelFromNoiseProperties(props) + with cirq.testing.assert_deprecated("Use is_virtual", deadline="v0.16"): + _ = model.virtual_predicate(cirq.X(q0)) diff --git a/cirq-core/cirq/devices/superconducting_qubits_noise_properties.py b/cirq-core/cirq/devices/superconducting_qubits_noise_properties.py index 43a165e89f9d..64e307d46239 100644 --- a/cirq-core/cirq/devices/superconducting_qubits_noise_properties.py +++ b/cirq-core/cirq/devices/superconducting_qubits_noise_properties.py @@ -35,14 +35,15 @@ class SuperconductingQubitsNoiseProperties(devices.NoiseProperties, abc.ABC): Args: gate_times_ns: Dict[type, float] of gate types to their duration on - quantum hardware. + quantum hardware. Used with t(1|phi)_ns to specify thermal noise. t1_ns: Dict[cirq.Qid, float] of qubits to their T_1 time, in ns. tphi_ns: Dict[cirq.Qid, float] of qubits to their T_phi time, in ns. readout_errors: Dict[cirq.Qid, np.ndarray] of qubits to their readout errors in matrix form: [P(read |1> from |0>), P(read |0> from |1>)]. + Used to prepend amplitude damping errors to measurements. gate_pauli_errors: dict of noise_utils.OpIdentifiers (a gate and the qubits it - targets) to the Pauli error for that operation. Keys in this dict - must have defined qubits. + targets) to the Pauli error for that operation. Used to construct + depolarizing error. Keys in this dict must have defined qubits. validate: If True, verifies that t1 and tphi qubits sets match, and that all symmetric two-qubit gates have errors which are symmetric over the qubits they affect. Defaults to True. diff --git a/cirq-google/cirq_google/devices/google_noise_properties.py b/cirq-google/cirq_google/devices/google_noise_properties.py new file mode 100644 index 000000000000..6d71d61ffaec --- /dev/null +++ b/cirq-google/cirq_google/devices/google_noise_properties.py @@ -0,0 +1,139 @@ +# Copyright 2022 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. + + +"""Class for representing noise on a Google device.""" + +from dataclasses import dataclass, field +from typing import Dict, List, Set, Type +import numpy as np + +import cirq, cirq_google +from cirq import _compat, devices +from cirq.devices import noise_utils +from cirq.transformers.heuristic_decompositions import gate_tabulation_math_utils + + +SINGLE_QUBIT_GATES: Set[Type['cirq.Gate']] = { + cirq.ZPowGate, + cirq.PhasedXZGate, + cirq.MeasurementGate, + cirq.ResetChannel, +} +SYMMETRIC_TWO_QUBIT_GATES: Set[Type['cirq.Gate']] = { + cirq_google.SycamoreGate, + cirq.FSimGate, + cirq.PhasedFSimGate, + cirq.ISwapPowGate, + cirq.CZPowGate, +} +ASYMMETRIC_TWO_QUBIT_GATES: Set[Type['cirq.Gate']] = set() + + +@dataclass +class GoogleNoiseProperties(devices.SuperconductingQubitsNoiseProperties): + """Noise-defining properties for a Google device. + + Inherited args: + gate_times_ns: Dict[type, float] of gate types to their duration on + quantum hardware. Used with t(1|phi)_ns to specify thermal noise. + t1_ns: Dict[cirq.Qid, float] of qubits to their T_1 time, in ns. + tphi_ns: Dict[cirq.Qid, float] of qubits to their T_phi time, in ns. + readout_errors: Dict[cirq.Qid, np.ndarray] of qubits to their readout + errors in matrix form: [P(read |1> from |0>), P(read |0> from |1>)]. + Used to prepend amplitude damping errors to measurements. + gate_pauli_errors: dict of noise_utils.OpIdentifiers (a gate and the qubits it + targets) to the Pauli error for that operation. Used to construct + depolarizing error. Keys in this dict must have defined qubits. + validate: If True, verifies that t1 and tphi qubits sets match, and + that all symmetric two-qubit gates have errors which are + symmetric over the qubits they affect. Defaults to True. + + Additional args: + fsim_errors: Dict[noise_utils.OpIdentifier, cirq.PhasedFSimGate] of gate types + (potentially on specific qubits) to the PhasedFSim fix-up operation + for that gate. Defaults to no-op for all gates. + """ + + fsim_errors: Dict[noise_utils.OpIdentifier, cirq.PhasedFSimGate] = field(default_factory=dict) + + def __post_init__(self): + super().__post_init__() + + # validate two qubit gate errors. + self._validate_symmetric_errors('fsim_errors') + + @classmethod + def single_qubit_gates(cls) -> Set[type]: + return SINGLE_QUBIT_GATES + + @classmethod + def symmetric_two_qubit_gates(cls) -> Set[type]: + return SYMMETRIC_TWO_QUBIT_GATES + + @classmethod + def asymmetric_two_qubit_gates(cls) -> Set[type]: + return ASYMMETRIC_TWO_QUBIT_GATES + + @_compat.cached_property + def _depolarizing_error(self) -> Dict[noise_utils.OpIdentifier, float]: + depol_errors = super()._depolarizing_error + + def extract_entangling_error(match_id: noise_utils.OpIdentifier): + """Gets the entangling error component of depol_errors[match_id].""" + unitary_err = cirq.unitary(self.fsim_errors[match_id]) + fid = gate_tabulation_math_utils.unitary_entanglement_fidelity(unitary_err, np.eye(4)) + return 1 - fid + + # Subtract entangling angle error. + for op_id in depol_errors: + if op_id.gate_type not in self.two_qubit_gates(): + continue + if op_id in self.fsim_errors: + depol_errors[op_id] -= extract_entangling_error(op_id) + continue + # Find the closest matching supertype, if one is provided. + # Gateset has similar behavior, but cannot be used here + # because depol_errors is a dict, not a set. + match_id = None + candidate_parents = [ + parent_id for parent_id in self.fsim_errors if op_id.is_proper_subtype_of(parent_id) + ] + for parent_id in candidate_parents: + if match_id is None or parent_id.is_proper_subtype_of(match_id): + match_id = parent_id + if match_id is not None: + depol_errors[op_id] -= extract_entangling_error(match_id) + + return depol_errors + + def build_noise_models(self) -> List['cirq.NoiseModel']: + """Construct all NoiseModels associated with NoiseProperties.""" + noise_models = super().build_noise_models() + + # Insert entangling gate coherent errors after thermal error. + if self.fsim_errors: + fsim_ops = {op_id: gate.on(*op_id.qubits) for op_id, gate in self.fsim_errors.items()} + noise_models.insert(1, devices.InsertionNoiseModel(ops_added=fsim_ops)) + + return noise_models + + +class NoiseModelFromGoogleNoiseProperties(devices.NoiseModelFromNoiseProperties): + """A noise model defined from noise properties of a Google device.""" + + def is_virtual(self, op: cirq.Operation) -> bool: + return isinstance(op.gate, cirq.ZPowGate) and cirq_google.PhysicalZTag not in op.tags + + # noisy_moments is implemented by the superclass. diff --git a/cirq-google/cirq_google/devices/google_noise_properties_test.py b/cirq-google/cirq_google/devices/google_noise_properties_test.py new file mode 100644 index 000000000000..aa627b6ddc6f --- /dev/null +++ b/cirq-google/cirq_google/devices/google_noise_properties_test.py @@ -0,0 +1,276 @@ +# Copyright 2022 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 Dict, List, Tuple +from cirq.ops.fsim_gate import PhasedFSimGate +import numpy as np +import pytest +import cirq, cirq_google + +from cirq_google.devices.google_noise_properties import ( + SYMMETRIC_TWO_QUBIT_GATES, + SINGLE_QUBIT_GATES, +) +from cirq.devices.noise_utils import ( + OpIdentifier, + PHYSICAL_GATE_TAG, +) + +from cirq_google.devices.google_noise_properties import ( + GoogleNoiseProperties, + NoiseModelFromGoogleNoiseProperties, +) + + +DEFAULT_GATE_NS: Dict[type, float] = { + cirq.ZPowGate: 25.0, + cirq.MeasurementGate: 4000.0, + cirq.ResetChannel: 250.0, + cirq.PhasedXZGate: 25.0, + cirq.FSimGate: 32.0, + # SYC is normally 12ns, but setting it equal to other two-qubit gates + # simplifies the tests. + cirq_google.SycamoreGate: 32.0, + cirq.PhasedFSimGate: 32.0, + cirq.ISwapPowGate: 32.0, + cirq.CZPowGate: 32.0, + # cirq.WaitGate is a special case. +} + +# Mock pauli error rates for 1- and 2-qubit gates. +SINGLE_QUBIT_ERROR = 0.001 +TWO_QUBIT_ERROR = 0.01 + + +# These properties are for testing purposes only - they are not representative +# of device behavior for any existing hardware. +def sample_noise_properties( + system_qubits: List[cirq.Qid], qubit_pairs: List[Tuple[cirq.Qid, cirq.Qid]] +): + # Known false positive: https://github.com/PyCQA/pylint/issues/5857 + return GoogleNoiseProperties( # pylint: disable=unexpected-keyword-arg + gate_times_ns=DEFAULT_GATE_NS, + t1_ns={q: 1e5 for q in system_qubits}, + tphi_ns={q: 2e5 for q in system_qubits}, + readout_errors={q: np.array([SINGLE_QUBIT_ERROR, TWO_QUBIT_ERROR]) for q in system_qubits}, + gate_pauli_errors={ + **{OpIdentifier(g, q): 0.001 for g in SINGLE_QUBIT_GATES for q in system_qubits}, + **{ + OpIdentifier(g, q0, q1): 0.01 + for g in SYMMETRIC_TWO_QUBIT_GATES + for q0, q1 in qubit_pairs + }, + }, + fsim_errors={ + OpIdentifier(g, q0, q1): cirq.PhasedFSimGate(0.01, 0.03, 0.04, 0.05, 0.02) + for g in SYMMETRIC_TWO_QUBIT_GATES + for q0, q1 in qubit_pairs + }, + ) + + +def test_zphase_gates(): + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + model = NoiseModelFromGoogleNoiseProperties(props) + circuit = cirq.Circuit(cirq.Z(q0) ** 0.3) + noisy_circuit = circuit.with_noise(model) + assert noisy_circuit == circuit + + +@pytest.mark.parametrize( + 'op', + [ + (cirq.Z(cirq.LineQubit(0)) ** 0.3).with_tags(cirq_google.PhysicalZTag), + cirq.PhasedXZGate(x_exponent=0.8, z_exponent=0.2, axis_phase_exponent=0.1).on( + cirq.LineQubit(0) + ), + ], +) +def test_single_qubit_gates(op): + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + model = NoiseModelFromGoogleNoiseProperties(props) + circuit = cirq.Circuit(op) + noisy_circuit = circuit.with_noise(model) + assert len(noisy_circuit.moments) == 3 + assert len(noisy_circuit.moments[0].operations) == 1 + assert noisy_circuit.moments[0].operations[0] == op.with_tags(PHYSICAL_GATE_TAG) + + # Depolarizing noise + assert len(noisy_circuit.moments[1].operations) == 1 + depol_op = noisy_circuit.moments[1].operations[0] + assert isinstance(depol_op.gate, cirq.DepolarizingChannel) + assert np.isclose(depol_op.gate.p, 0.00081252) + + # Thermal noise + assert len(noisy_circuit.moments[2].operations) == 1 + thermal_op = noisy_circuit.moments[2].operations[0] + assert isinstance(thermal_op.gate, cirq.KrausChannel) + thermal_choi = cirq.kraus_to_choi(cirq.kraus(thermal_op)) + assert np.allclose( + thermal_choi, + [ + [1, 0, 0, 9.99750031e-01], + [0, 2.49968753e-04, 0, 0], + [0, 0, 0, 0], + [9.99750031e-01, 0, 0, 9.99750031e-01], + ], + ) + + # Pauli error for depol_op + thermal_op == total (0.001) + depol_pauli_err = 1 - cirq.qis.measures.entanglement_fidelity(depol_op) + thermal_pauli_err = 1 - cirq.qis.measures.entanglement_fidelity(thermal_op) + total_err = depol_pauli_err + thermal_pauli_err + assert np.isclose(total_err, SINGLE_QUBIT_ERROR) + + +@pytest.mark.parametrize( + 'op', + [ + cirq.ISWAP(*cirq.LineQubit.range(2)) ** 0.6, + cirq.CZ(*cirq.LineQubit.range(2)) ** 0.3, + cirq_google.SYC(*cirq.LineQubit.range(2)), + ], +) +def test_two_qubit_gates(op): + q0, q1 = cirq.LineQubit.range(2) + props = sample_noise_properties([q0, q1], [(q0, q1), (q1, q0)]) + model = NoiseModelFromGoogleNoiseProperties(props) + circuit = cirq.Circuit(op) + noisy_circuit = circuit.with_noise(model) + assert len(noisy_circuit.moments) == 4 + assert len(noisy_circuit.moments[0].operations) == 1 + assert noisy_circuit.moments[0].operations[0] == op.with_tags(PHYSICAL_GATE_TAG) + + # Depolarizing noise + assert len(noisy_circuit.moments[1].operations) == 1 + depol_op = noisy_circuit.moments[1].operations[0] + assert isinstance(depol_op.gate, cirq.DepolarizingChannel) + assert np.isclose(depol_op.gate.p, 0.00719705) + + # FSim angle corrections + assert len(noisy_circuit.moments[2].operations) == 1 + fsim_op = noisy_circuit.moments[2].operations[0] + assert isinstance(fsim_op.gate, cirq.PhasedFSimGate) + assert fsim_op == PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02).on( + q0, q1 + ) + + # Thermal noise + assert len(noisy_circuit.moments[3].operations) == 2 + thermal_op_0 = noisy_circuit.moments[3].operation_at(q0) + thermal_op_1 = noisy_circuit.moments[3].operation_at(q1) + assert isinstance(thermal_op_0.gate, cirq.KrausChannel) + assert isinstance(thermal_op_1.gate, cirq.KrausChannel) + thermal_choi_0 = cirq.kraus_to_choi(cirq.kraus(thermal_op_0)) + thermal_choi_1 = cirq.kraus_to_choi(cirq.kraus(thermal_op_1)) + expected_thermal_choi = np.array( + [ + [1, 0, 0, 9.99680051e-01], + [0, 3.19948805e-04, 0, 0], + [0, 0, 0, 0], + [9.99680051e-01, 0, 0, 9.99680051e-01], + ] + ) + assert np.allclose(thermal_choi_0, expected_thermal_choi) + assert np.allclose(thermal_choi_1, expected_thermal_choi) + + # Pauli error for depol_op + fsim_op + thermal_op_(0|1) == total (0.01) + depol_pauli_err = 1 - cirq.qis.measures.entanglement_fidelity(depol_op) + fsim_pauli_err = 1 - cirq.qis.measures.entanglement_fidelity(fsim_op) + thermal0_pauli_err = 1 - cirq.qis.measures.entanglement_fidelity(thermal_op_0) + thermal1_pauli_err = 1 - cirq.qis.measures.entanglement_fidelity(thermal_op_1) + total_err = depol_pauli_err + thermal0_pauli_err + thermal1_pauli_err + fsim_pauli_err + assert np.isclose(total_err, TWO_QUBIT_ERROR) + + +def test_supertype_match(): + # Verifies that ops in gate_pauli_errors which only appear as their + # supertypes in fsim_errors are properly accounted for. + q0, q1 = cirq.LineQubit.range(2) + op_id = OpIdentifier(cirq_google.SycamoreGate, q0, q1) + test_props = sample_noise_properties([q0, q1], [(q0, q1), (q1, q0)]) + expected_err = test_props._depolarizing_error[op_id] + + props = sample_noise_properties([q0, q1], [(q0, q1), (q1, q0)]) + props.fsim_errors = { + k: cirq.PhasedFSimGate(0.5, 0.4, 0.3, 0.2, 0.1) + for k in [OpIdentifier(cirq.FSimGate, q0, q1), OpIdentifier(cirq.FSimGate, q1, q0)] + } + assert props._depolarizing_error[op_id] != expected_err + + +def test_measure_gates(): + q00, q01, q10, q11 = cirq.GridQubit.rect(2, 2) + qubits = [q00, q01, q10, q11] + props = sample_noise_properties( + qubits, + [ + (q00, q01), + (q01, q00), + (q10, q11), + (q11, q10), + (q00, q10), + (q10, q00), + (q01, q11), + (q11, q01), + ], + ) + model = NoiseModelFromGoogleNoiseProperties(props) + op = cirq.measure(*qubits, key='m') + circuit = cirq.Circuit(cirq.measure(*qubits, key='m')) + noisy_circuit = circuit.with_noise(model) + # Measurement gates are prepended by amplitude damping, and nothing else. + assert len(noisy_circuit.moments) == 2 + + # Amplitude damping before measurement + assert len(noisy_circuit.moments[0].operations) == 4 + for q in qubits: + op = noisy_circuit.moments[0].operation_at(q) + assert isinstance(op.gate, cirq.GeneralizedAmplitudeDampingChannel), q + assert np.isclose(op.gate.p, 0.90909090), q + assert np.isclose(op.gate.gamma, 0.011), q + + # Original measurement is after the noise. + assert len(noisy_circuit.moments[1].operations) == 1 + # Measurements are untagged during reconstruction. + assert noisy_circuit.moments[1] == circuit.moments[0] + + +def test_wait_gates(): + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + model = NoiseModelFromGoogleNoiseProperties(props) + op = cirq.wait(q0, nanos=100) + circuit = cirq.Circuit(op) + noisy_circuit = circuit.with_noise(model) + assert len(noisy_circuit.moments) == 2 + assert noisy_circuit.moments[0].operations[0] == op.with_tags(PHYSICAL_GATE_TAG) + + # No depolarizing noise because WaitGate has none. + + assert len(noisy_circuit.moments[1].operations) == 1 + thermal_op = noisy_circuit.moments[1].operations[0] + assert isinstance(thermal_op.gate, cirq.KrausChannel) + thermal_choi = cirq.kraus_to_choi(cirq.kraus(thermal_op)) + assert np.allclose( + thermal_choi, + [ + [1, 0, 0, 9.990005e-01], + [0, 9.99500167e-04, 0, 0], + [0, 0, 0, 0], + [9.990005e-01, 0, 0, 9.990005e-01], + ], + )