diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 8d70700eec2..5842452cc15 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -88,6 +88,7 @@ NO_NOISE, NOISE_MODEL_LIKE, NoiseModel, + OpIdentifier, SymmetricalQidPair, UNCONSTRAINED_DEVICE, NamedTopology, diff --git a/cirq-core/cirq/devices/__init__.py b/cirq-core/cirq/devices/__init__.py index 1060f7aea0e..72f793a7e46 100644 --- a/cirq-core/cirq/devices/__init__.py +++ b/cirq-core/cirq/devices/__init__.py @@ -47,3 +47,15 @@ get_placements, draw_placements, ) + +from cirq.devices.noise_utils import ( + OpIdentifier, + decay_constant_to_xeb_fidelity, + decay_constant_to_pauli_error, + pauli_error_to_decay_constant, + xeb_fidelity_to_decay_constant, + pauli_error_from_t1, + pauli_error_from_depolarization, + average_error, + decoherence_pauli_error, +) diff --git a/cirq-core/cirq/devices/noise_utils.py b/cirq-core/cirq/devices/noise_utils.py new file mode 100644 index 00000000000..fe59d94a9c1 --- /dev/null +++ b/cirq-core/cirq/devices/noise_utils.py @@ -0,0 +1,233 @@ +# 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 TYPE_CHECKING, Any, Dict, Tuple, Type, Union +import warnings +import numpy as np + +from cirq import ops, protocols, value + +if TYPE_CHECKING: + import cirq + + +# Tag for gates to which noise must be applied. +PHYSICAL_GATE_TAG = 'physical_gate' + + +@value.value_equality(distinct_child_types=True) +class OpIdentifier: + """Identifies an operation by gate and (optionally) target qubits.""" + + def __init__(self, gate_type: Type['cirq.Gate'], *qubits: 'cirq.Qid'): + self._gate_type = gate_type + self._gate_family = ops.GateFamily(gate_type) + self._qubits: Tuple['cirq.Qid', ...] = tuple(qubits) + + @property + def gate_type(self) -> Type['cirq.Gate']: + # set to a type during initialization, never modified + return self._gate_type + + @property + def qubits(self) -> Tuple['cirq.Qid', ...]: + return self._qubits + + def _predicate(self, *args, **kwargs): + return self._gate_family._predicate(*args, **kwargs) + + def swapped(self): + return OpIdentifier(self.gate_type, *self.qubits[::-1]) + + def is_proper_subtype_of(self, op_id: 'OpIdentifier'): + """Returns true if this is contained within op_id, but not equal to it. + + If this returns true, (x in self) implies (x in op_id), but the reverse + implication does not hold. op_id must be more general than self (either + by accepting any qubits or having a more general gate type) for this + to return true. + """ + more_specific_qubits = self.qubits and not op_id.qubits + more_specific_gate = self.gate_type != op_id.gate_type and issubclass( + self.gate_type, op_id.gate_type + ) + if more_specific_qubits: + return more_specific_gate or self.gate_type == op_id.gate_type + elif more_specific_gate: + return more_specific_qubits or self.qubits == op_id.qubits + else: + return False + + def __contains__(self, item: Union[ops.Gate, ops.Operation]) -> bool: + if isinstance(item, ops.Gate): + return (not self._qubits) and self._predicate(item) + return ( + (not self.qubits or (item.qubits == self._qubits)) + and item.gate is not None + and self._predicate(item.gate) + ) + + def __str__(self): + return f'{self.gate_type}{self.qubits}' + + def __repr__(self) -> str: + fullname = f'{self.gate_type.__module__}.{self.gate_type.__qualname__}' + qubits = ', '.join(map(repr, self.qubits)) + return f'cirq.devices.noise_utils.OpIdentifier({fullname}, {qubits})' + + def _value_equality_values_(self) -> Any: + return (self.gate_type, self.qubits) + + def _json_dict_(self) -> Dict[str, Any]: + gate_json = protocols.json_cirq_type(self._gate_type) + return { + 'gate_type': gate_json, + 'qubits': self._qubits, + } + + @classmethod + def _from_json_dict_(cls, gate_type, qubits, **kwargs) -> 'OpIdentifier': + gate_type = protocols.cirq_type_from_json(gate_type) + return cls(gate_type, *qubits) + + +# TODO: expose all from top-level cirq? +def decay_constant_to_xeb_fidelity(decay_constant: float, num_qubits: int = 2) -> float: + """Calculates the XEB fidelity from the depolarization decay constant. + + Args: + decay_constant: Depolarization decay constant. + num_qubits: Number of qubits. + + Returns: + Calculated XEB fidelity. + """ + N = 2 ** num_qubits + return 1 - ((1 - decay_constant) * (1 - 1 / N)) + + +def decay_constant_to_pauli_error(decay_constant: float, num_qubits: int = 1) -> float: + """Calculates pauli error from the depolarization decay constant. + + Args: + decay_constant: Depolarization decay constant. + num_qubits: Number of qubits. + + Returns: + Calculated Pauli error. + """ + N = 2 ** num_qubits + return (1 - decay_constant) * (1 - 1 / N / N) + + +def pauli_error_to_decay_constant(pauli_error: float, num_qubits: int = 1) -> float: + """Calculates depolarization decay constant from pauli error. + + Args: + pauli_error: The pauli error. + num_qubits: Number of qubits. + + Returns: + Calculated depolarization decay constant. + """ + N = 2 ** num_qubits + return 1 - (pauli_error / (1 - 1 / N / N)) + + +def xeb_fidelity_to_decay_constant(xeb_fidelity: float, num_qubits: int = 2) -> float: + """Calculates the depolarization decay constant from XEB fidelity. + + Args: + xeb_fidelity: The XEB fidelity. + num_qubits: Number of qubits. + + Returns: + Calculated depolarization decay constant. + """ + N = 2 ** num_qubits + return 1 - (1 - xeb_fidelity) / (1 - 1 / N) + + +def pauli_error_from_t1(t_ns: float, t1_ns: float) -> float: + """Calculates the pauli error from T1 decay constant. + + This computes error for a specific duration, `t`. + + Args: + t_ns: The duration of the gate in ns. + t1_ns: The T1 decay constant in ns. + + Returns: + Calculated Pauli error resulting from T1 decay. + """ + t2 = 2 * t1_ns + return (1 - np.exp(-t_ns / t2)) / 2 + (1 - np.exp(-t_ns / t1_ns)) / 4 + + +def pauli_error_from_depolarization(t_ns: float, t1_ns: float, pauli_error: float = 0) -> float: + """Calculates the amount of pauli error from depolarization. + + This computes non-T1 error for a specific duration, `t`. If pauli error + from T1 decay is more than total pauli error, this returns zero; otherwise, + it returns the portion of pauli error not attributable to T1 error. + + Args: + t_ns: The duration of the gate in ns. + t1_ns: The T1 decay constant in ns. + pauli_error: The total pauli error. + + Returns: + Calculated Pauli error resulting from depolarization. + """ + t1_pauli_error = pauli_error_from_t1(t_ns, t1_ns) + if pauli_error >= t1_pauli_error: + return pauli_error - t1_pauli_error + + warnings.warn("Pauli error from T1 decay is greater than total Pauli error", RuntimeWarning) + return 0 + + +def average_error(decay_constant: float, num_qubits: int = 1) -> float: + """Calculates the average error from the depolarization decay constant. + + Args: + decay_constant: Depolarization decay constant. + num_qubits: Number of qubits. + + Returns: + Calculated average error. + """ + N = 2 ** num_qubits + return (1 - decay_constant) * (1 - 1 / N) + + +def decoherence_pauli_error(t1_ns: float, tphi_ns: float, gate_time_ns: float) -> float: + """The component of Pauli error caused by decoherence. + + Args: + t1_ns: T1 time in nanoseconds. + tphi_ns: Tphi time in nanoseconds. + gate_time_ns: Duration in nanoseconds of the gate affected by this error. + + Returns: + Calculated Pauli error resulting from decoherence. + """ + gamma_2 = (1 / (2 * t1_ns)) + 1 / tphi_ns + + exp1 = np.exp(-gate_time_ns / t1_ns) + exp2 = np.exp(-gate_time_ns * gamma_2) + px = 0.25 * (1 - exp1) + py = px + pz = 0.5 * (1 - exp2) - px + return px + py + pz diff --git a/cirq-core/cirq/devices/noise_utils_test.py b/cirq-core/cirq/devices/noise_utils_test.py new file mode 100644 index 00000000000..d49d9e6508a --- /dev/null +++ b/cirq-core/cirq/devices/noise_utils_test.py @@ -0,0 +1,170 @@ +# 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. + +import numpy as np +import pytest + +import cirq +from cirq.devices.noise_utils import ( + OpIdentifier, + decay_constant_to_xeb_fidelity, + decay_constant_to_pauli_error, + pauli_error_to_decay_constant, + xeb_fidelity_to_decay_constant, + pauli_error_from_t1, + pauli_error_from_depolarization, + average_error, + decoherence_pauli_error, +) + + +def test_op_identifier(): + op_id = OpIdentifier(cirq.XPowGate) + assert cirq.X(cirq.LineQubit(1)) in op_id + assert cirq.Rx(rads=1) in op_id + + +def test_op_identifier_subtypes(): + gate_id = OpIdentifier(cirq.Gate) + xpow_id = OpIdentifier(cirq.XPowGate) + x_on_q0_id = OpIdentifier(cirq.XPowGate, cirq.LineQubit(0)) + assert xpow_id.is_proper_subtype_of(gate_id) + assert x_on_q0_id.is_proper_subtype_of(xpow_id) + assert x_on_q0_id.is_proper_subtype_of(gate_id) + assert not xpow_id.is_proper_subtype_of(xpow_id) + + +def test_op_id_str(): + op_id = OpIdentifier(cirq.XPowGate, cirq.LineQubit(0)) + print(op_id) + print(repr(op_id)) + assert str(op_id) == "(cirq.LineQubit(0),)" + assert repr(op_id) == ( + "cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_gates.XPowGate, cirq.LineQubit(0))" + ) + + +def test_op_id_swap(): + q0, q1 = cirq.LineQubit.range(2) + base_id = OpIdentifier(cirq.CZPowGate, q0, q1) + swap_id = base_id.swapped() + assert cirq.CZ(q0, q1) in base_id + assert cirq.CZ(q0, q1) not in swap_id + assert cirq.CZ(q1, q0) not in base_id + assert cirq.CZ(q1, q0) in swap_id + + +@pytest.mark.parametrize( + 'decay_constant,num_qubits,expected_output', + [ + (0.01, 1, 1 - (0.99 * 1 / 2)), + (0.05, 2, 1 - (0.95 * 3 / 4)), + ], +) +def test_decay_constant_to_xeb_fidelity(decay_constant, num_qubits, expected_output): + val = decay_constant_to_xeb_fidelity(decay_constant, num_qubits) + assert val == expected_output + + +@pytest.mark.parametrize( + 'decay_constant,num_qubits,expected_output', + [ + (0.01, 1, 0.99 * 3 / 4), + (0.05, 2, 0.95 * 15 / 16), + ], +) +def test_decay_constant_to_pauli_error(decay_constant, num_qubits, expected_output): + val = decay_constant_to_pauli_error(decay_constant, num_qubits) + assert val == expected_output + + +@pytest.mark.parametrize( + 'pauli_error,num_qubits,expected_output', + [ + (0.01, 1, 1 - (0.01 / (3 / 4))), + (0.05, 2, 1 - (0.05 / (15 / 16))), + ], +) +def test_pauli_error_to_decay_constant(pauli_error, num_qubits, expected_output): + val = pauli_error_to_decay_constant(pauli_error, num_qubits) + assert val == expected_output + + +@pytest.mark.parametrize( + 'xeb_fidelity,num_qubits,expected_output', + [ + (0.01, 1, 1 - 0.99 / (1 / 2)), + (0.05, 2, 1 - 0.95 / (3 / 4)), + ], +) +def test_xeb_fidelity_to_decay_constant(xeb_fidelity, num_qubits, expected_output): + val = xeb_fidelity_to_decay_constant(xeb_fidelity, num_qubits) + assert val == expected_output + + +@pytest.mark.parametrize( + 't,t1_ns,expected_output', + [ + (20, 1e5, (1 - np.exp(-20 / 2e5)) / 2 + (1 - np.exp(-20 / 1e5)) / 4), + (4000, 1e4, (1 - np.exp(-4000 / 2e4)) / 2 + (1 - np.exp(-4000 / 1e4)) / 4), + ], +) +def test_pauli_error_from_t1(t, t1_ns, expected_output): + val = pauli_error_from_t1(t, t1_ns) + assert val == expected_output + + +@pytest.mark.parametrize( + 't,t1_ns,pauli_error,expected_output', + [ + (20, 1e5, 0.01, 0.01 - ((1 - np.exp(-20 / 2e5)) / 2 + (1 - np.exp(-20 / 1e5)) / 4)), + # In this case, the formula produces a negative result. + (4000, 1e4, 0.01, 0), + ], +) +def test_pauli_error_from_depolarization(t, t1_ns, pauli_error, expected_output): + val = pauli_error_from_depolarization(t, t1_ns, pauli_error) + assert val == expected_output + + +@pytest.mark.parametrize( + 'decay_constant,num_qubits,expected_output', + [ + (0.01, 1, 0.99 * 1 / 2), + (0.05, 2, 0.95 * 3 / 4), + ], +) +def test_average_error(decay_constant, num_qubits, expected_output): + val = average_error(decay_constant, num_qubits) + assert val == expected_output + + +@pytest.mark.parametrize( + 'T1_ns,Tphi_ns,gate_time_ns', + [ + (1e4, 2e4, 25), + (1e5, 2e3, 25), + (1e4, 2e4, 4000), + ], +) +def test_decoherence_pauli_error(T1_ns, Tphi_ns, gate_time_ns): + val = decoherence_pauli_error(T1_ns, Tphi_ns, gate_time_ns) + # Expected value is of the form: + # + # (1/4) * [1 - e^(-t/T1)] + (1/2) * [1 - e^(-t/(2*T1) - t/Tphi] + # + expected_output = 0.25 * (1 - np.exp(-gate_time_ns / T1_ns)) + 0.5 * ( + 1 - np.exp(-gate_time_ns * ((1 / (2 * T1_ns)) + 1 / Tphi_ns)) + ) + assert val == expected_output diff --git a/cirq-core/cirq/json_resolver_cache.py b/cirq-core/cirq/json_resolver_cache.py index 7f28bb7ebf3..6f362dcd52b 100644 --- a/cirq-core/cirq/json_resolver_cache.py +++ b/cirq-core/cirq/json_resolver_cache.py @@ -116,6 +116,7 @@ def _parallel_gate_op(gate, qubits): 'NamedQubit': cirq.NamedQubit, 'NamedQid': cirq.NamedQid, 'NoIdentifierQubit': cirq.testing.NoIdentifierQubit, + 'OpIdentifier': cirq.OpIdentifier, '_PauliX': cirq.ops.pauli_gates._PauliX, '_PauliY': cirq.ops.pauli_gates._PauliY, '_PauliZ': cirq.ops.pauli_gates._PauliZ, diff --git a/cirq-core/cirq/protocols/json_test_data/OpIdentifier.json b/cirq-core/cirq/protocols/json_test_data/OpIdentifier.json new file mode 100644 index 00000000000..d33b909367d --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/OpIdentifier.json @@ -0,0 +1,10 @@ +{ + "cirq_type": "OpIdentifier", + "gate_type": "XPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + } + ] +} \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/OpIdentifier.repr b/cirq-core/cirq/protocols/json_test_data/OpIdentifier.repr new file mode 100644 index 00000000000..6b991bb0b2c --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/OpIdentifier.repr @@ -0,0 +1,4 @@ +cirq.devices.noise_utils.OpIdentifier( + cirq.ops.common_gates.XPowGate, + cirq.LineQubit(1) +) \ No newline at end of file