diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 35b84ca21da..4a2d0f355ce 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -88,6 +88,9 @@ NO_NOISE, NOISE_MODEL_LIKE, NoiseModel, + NoiseProperties, + NoiseModelFromNoiseProperties, + OpIdentifier, SymmetricalQidPair, UNCONSTRAINED_DEVICE, NamedTopology, diff --git a/cirq-core/cirq/devices/__init__.py b/cirq-core/cirq/devices/__init__.py index 1060f7aea0e..7c303725331 100644 --- a/cirq-core/cirq/devices/__init__.py +++ b/cirq-core/cirq/devices/__init__.py @@ -39,6 +39,11 @@ ConstantQubitNoiseModel, ) +from cirq.devices.noise_properties import ( + NoiseProperties, + NoiseModelFromNoiseProperties, +) + from cirq.devices.named_topologies import ( NamedTopology, draw_gridlike, @@ -47,3 +52,23 @@ get_placements, draw_placements, ) + +from cirq.devices.insertion_noise_model import ( + InsertionNoiseModel, +) + +from cirq.devices.thermal_noise_model import ( + ThermalNoiseModel, +) + +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/insertion_noise_model.py b/cirq-core/cirq/devices/insertion_noise_model.py new file mode 100644 index 00000000000..ed8409c3ca6 --- /dev/null +++ b/cirq-core/cirq/devices/insertion_noise_model.py @@ -0,0 +1,71 @@ +# 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 dataclasses import dataclass, field +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence + +from cirq import devices, ops +from cirq.devices.noise_utils import ( + OpIdentifier, + PHYSICAL_GATE_TAG, +) + +if TYPE_CHECKING: + import cirq + + +@dataclass +class InsertionNoiseModel(devices.NoiseModel): + """Simple base noise model for inserting operations. + + Operations generated by this model for a given moment are all added into a + single "noise moment", which is added before or after the original moment + based on `prepend`. + + Args: + ops_added: a map of gate types (and optionally, qubits they act on) to + operations that should be added. + prepend: whether to add the new moment before the current one. + require_physical_tag: whether to only apply noise to operations tagged + with PHYSICAL_GATE_TAG. + """ + + ops_added: Dict[OpIdentifier, 'cirq.Operation'] = field(default_factory=dict) + prepend: bool = False + require_physical_tag: bool = True + + def noisy_moment( + self, moment: 'cirq.Moment', system_qubits: Sequence['cirq.Qid'] + ) -> 'cirq.OP_TREE': + noise_ops: List['cirq.Operation'] = [] + for op in moment: + if self.require_physical_tag and PHYSICAL_GATE_TAG not in op.tags: + # Only non-virtual gates get noise applied. + continue + match_id: Optional[OpIdentifier] = None + for op_id in self.ops_added: + if op not in op_id: + continue + elif match_id is None: + match_id = op_id + continue + elif op_id.is_proper_subtype_of(match_id): + match_id = op_id + if match_id is not None: + noise_ops.append(self.ops_added[match_id]) + if not noise_ops: + return [moment] + if self.prepend: + return [ops.Moment(noise_ops), moment] + return [moment, ops.Moment(noise_ops)] diff --git a/cirq-core/cirq/devices/insertion_noise_model_test.py b/cirq-core/cirq/devices/insertion_noise_model_test.py new file mode 100644 index 00000000000..15216def58c --- /dev/null +++ b/cirq-core/cirq/devices/insertion_noise_model_test.py @@ -0,0 +1,103 @@ +# 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 cirq +from cirq.devices.insertion_noise_model import InsertionNoiseModel +from cirq.devices.noise_utils import ( + PHYSICAL_GATE_TAG, + OpIdentifier, +) + + +def test_insertion_noise(): + q0, q1 = cirq.LineQubit.range(2) + op_id0 = OpIdentifier(cirq.XPowGate, q0) + op_id1 = OpIdentifier(cirq.ZPowGate, q1) + model = InsertionNoiseModel( + {op_id0: cirq.T(q0), op_id1: cirq.H(q1)}, require_physical_tag=False + ) + assert not model.prepend + + moment_0 = cirq.Moment(cirq.X(q0), cirq.X(q1)) + assert model.noisy_moment(moment_0, system_qubits=[q0, q1]) == [ + moment_0, + cirq.Moment(cirq.T(q0)), + ] + + moment_1 = cirq.Moment(cirq.Z(q0), cirq.Z(q1)) + assert model.noisy_moment(moment_1, system_qubits=[q0, q1]) == [ + moment_1, + cirq.Moment(cirq.H(q1)), + ] + + moment_2 = cirq.Moment(cirq.X(q0), cirq.Z(q1)) + assert model.noisy_moment(moment_2, system_qubits=[q0, q1]) == [ + moment_2, + cirq.Moment(cirq.T(q0), cirq.H(q1)), + ] + + moment_3 = cirq.Moment(cirq.Z(q0), cirq.X(q1)) + assert model.noisy_moment(moment_3, system_qubits=[q0, q1]) == [moment_3] + + +def test_prepend(): + q0, q1 = cirq.LineQubit.range(2) + op_id0 = OpIdentifier(cirq.XPowGate, q0) + op_id1 = OpIdentifier(cirq.ZPowGate, q1) + model = InsertionNoiseModel( + {op_id0: cirq.T(q0), op_id1: cirq.H(q1)}, prepend=True, require_physical_tag=False + ) + + moment_0 = cirq.Moment(cirq.X(q0), cirq.Z(q1)) + assert model.noisy_moment(moment_0, system_qubits=[q0, q1]) == [ + cirq.Moment(cirq.T(q0), cirq.H(q1)), + moment_0, + ] + + +def test_require_physical_tag(): + q0, q1 = cirq.LineQubit.range(2) + op_id0 = OpIdentifier(cirq.XPowGate, q0) + op_id1 = OpIdentifier(cirq.ZPowGate, q1) + model = InsertionNoiseModel({op_id0: cirq.T(q0), op_id1: cirq.H(q1)}) + assert model.require_physical_tag + + moment_0 = cirq.Moment(cirq.X(q0).with_tags(PHYSICAL_GATE_TAG), cirq.Z(q1)) + assert model.noisy_moment(moment_0, system_qubits=[q0, q1]) == [ + moment_0, + cirq.Moment(cirq.T(q0)), + ] + + +def test_supertype_matching(): + # Demonstrate that the model applies the closest matching type + # if multiple types match a given gate. + q0 = cirq.LineQubit(0) + op_id0 = OpIdentifier(cirq.Gate, q0) + op_id1 = OpIdentifier(cirq.XPowGate, q0) + model = InsertionNoiseModel( + {op_id0: cirq.T(q0), op_id1: cirq.S(q0)}, require_physical_tag=False + ) + + moment_0 = cirq.Moment(cirq.Rx(rads=1).on(q0)) + assert model.noisy_moment(moment_0, system_qubits=[q0]) == [ + moment_0, + cirq.Moment(cirq.S(q0)), + ] + + moment_1 = cirq.Moment(cirq.Y(q0)) + assert model.noisy_moment(moment_1, system_qubits=[q0]) == [ + moment_1, + cirq.Moment(cirq.T(q0)), + ] diff --git a/cirq-core/cirq/devices/noise_properties.py b/cirq-core/cirq/devices/noise_properties.py index d2d3a39944c..3e472feb657 100644 --- a/cirq-core/cirq/devices/noise_properties.py +++ b/cirq-core/cirq/devices/noise_properties.py @@ -1,227 +1,274 @@ -# pylint: disable=wrong-or-nonexistent-copyright-notice -import warnings -from typing import Sequence, TYPE_CHECKING, List -from itertools import product +# 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 dataclasses import dataclass, field +from typing import Dict, Iterable, Sequence, TYPE_CHECKING, List, Set + from cirq import ops, protocols, devices -import numpy as np +from cirq.devices.noise_utils import ( + OpIdentifier, + PHYSICAL_GATE_TAG, + decoherence_pauli_error, +) if TYPE_CHECKING: - from typing import Iterable import cirq - +SINGLE_QUBIT_GATES: Set[type] = { + ops.ZPowGate, + ops.PhasedXZGate, + ops.MeasurementGate, + ops.ResetChannel, +} +SYMMETRIC_TWO_QUBIT_GATES: Set[type] = { + ops.FSimGate, + ops.PhasedFSimGate, + ops.ISwapPowGate, + ops.CZPowGate, +} +ASYMMETRIC_TWO_QUBIT_GATES: Set[type] = set() +TWO_QUBIT_GATES = SYMMETRIC_TWO_QUBIT_GATES | ASYMMETRIC_TWO_QUBIT_GATES + + +# TODO: missing per-device defaults +@dataclass 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. - - 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 + """Noise-defining properties for a quantum device. + + Args: + gate_times_ns: Dict[type, float] of gate types to their duration on + Google hardware. + T1_ns: Dict[cirq.Qid, float] of qubits to their T1 time, in ns. + Tphi_ns: Dict[cirq.Qid, float] of qubits to their Tphi time, in ns. + ro_fidelities: Dict[cirq.Qid, np.ndarray] of qubits to their readout + fidelity matrix. + gate_pauli_errors: Dict[OpIdentifier, float] of gate types + (potentially on specific qubits) to the Pauli error for that gate. + validate: If True, performs validation on input arguments. Defaults + to True. + """ + + gate_times_ns: Dict[type, float] + T1_ns: Dict['cirq.Qid', float] + Tphi_ns: Dict['cirq.Qid', float] + ro_fidelities: Dict['cirq.Qid', List[float]] + gate_pauli_errors: Dict[OpIdentifier, float] + + validate: bool = True + _qubits: List['cirq.Qid'] = field(init=False, default_factory=list) + _depolarizing_error: Dict[OpIdentifier, float] = field(init=False, default_factory=dict) + + def __post_init__(self): + if not self.validate: + return + t1_qubits = set(self.T1_ns) + tphi_qubits = set(self.Tphi_ns) + + if t1_qubits != tphi_qubits: + raise ValueError('Keys specified for T1 and Tphi are not identical.') + + # validate two qubit gate errors. + self._validate_symmetric_errors('gate_pauli_errors') + + def _validate_symmetric_errors(self, field_name: str) -> None: + gate_error_dict = getattr(self, field_name) + for op_id in gate_error_dict: + if len(op_id.qubits) != 2: + # single qubit op_ids also present, or generic values are + # specified. Skip these cases + if len(op_id.qubits) > 2: + raise ValueError( + f'Found gate {op_id.gate_type} with {len(op_id.qubits)} qubits. ' + 'Symmetric errors can only apply to 2-qubit gates.' + ) + elif op_id.gate_type not in self.two_qubit_gates(): + raise ValueError( + f'Found gate {op_id.gate_type} which does not appear in the ' + 'symmetric or asymmetric gate sets.' + ) + else: + # TODO: this assumes op is symmetric. + # If asymmetric gates are added, we will need to update it. + op_id_swapped = op_id.swapped() + if op_id_swapped not in gate_error_dict: + raise ValueError( + f'Operation {op_id} of field {field_name} has errors ' + f'but its symmetric id {op_id_swapped} does not.' + ) @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 qubits(self) -> List['cirq.Qid']: + """Qubits for which we have data""" + if not self._qubits: + self._qubits = sorted(self.T1_ns) + return self._qubits - 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 + @classmethod + def single_qubit_gates(cls) -> Set[type]: + return SINGLE_QUBIT_GATES - def pauli_error_to_decay_constant(self, pauli_error: float, num_qubits: int = 1): - """Calculates depolarization decay constant from pauli error. + @classmethod + def two_qubit_gates(cls) -> Set[type]: + return TWO_QUBIT_GATES - Args: - pauli_error: The pauli error - num_qubits: Number of qubits - """ - N = 2 ** num_qubits - return 1 - (pauli_error / (1 - 1 / N / N)) + @classmethod + def expected_gates(cls) -> Set[type]: + return cls.single_qubit_gates() | cls.two_qubit_gates() - def xeb_fidelity_to_decay_constant(self, xeb_fidelity: float, num_qubits: int = 2): - """Calculates the depolarization decay constant from the XEB noise_properties. + def get_depolarizing_error(self) -> Dict[OpIdentifier, float]: + """Returns the portion of Pauli error from depolarization. - Args: - xeb_fidelity: The XEB noise_properties - num_qubits: Number of qubits + The result of this method is memoized. """ - 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). + if self._depolarizing_error: + return self._depolarizing_error + + depol_errors = {} + for op_id, p_error in self.gate_pauli_errors.items(): + gate_type = op_id.gate_type + if gate_type in self.single_qubit_gates(): + if issubclass(gate_type, ops.MeasurementGate): + # Non-measurement error can be ignored on measurement gates. + continue + if len(op_id.qubits) != 1: + raise ValueError( + f'Gate {gate_type} only takes one qubit, but {op_id.qubits} were given.' + ) + time_ns = float(self.gate_times_ns[gate_type]) + q0 = op_id.qubits[0] + # Subtract decoherence error. + if q0 in self.T1_ns: + p_error -= decoherence_pauli_error(self.T1_ns[q0], self.Tphi_ns[q0], time_ns) - 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 + # This must be a 2-qubit gate. + if gate_type not in self.two_qubit_gates(): + raise ValueError(f'Gate {gate_type} is not in the supported gate list.') + if len(op_id.qubits) != 2: + raise ValueError( + f'Gate {gate_type} takes two qubits, but {op_id.qubits} were given.' + ) + time_ns = float(self.gate_times_ns[gate_type]) + # Subtract decoherence error. + q0, q1 = op_id.qubits + if q0 in self.T1_ns: + p_error -= decoherence_pauli_error(self.T1_ns[q0], self.Tphi_ns[q0], time_ns) + if q1 in self.T1_ns: + p_error -= decoherence_pauli_error(self.T1_ns[q1], self.Tphi_ns[q1], time_ns) + + depol_errors[op_id] = p_error + # memoization is OK + self._depolarizing_error = depol_errors + return self._depolarizing_error + + def build_noise_models(self) -> List['cirq.NoiseModel']: + """Construct all NoiseModels associated with this NoiseProperties.""" + noise_models: List['cirq.NoiseModel'] = [] + + if set(self.T1_ns) != set(self.Tphi_ns): + raise ValueError( + f'T1 data has qubits {set(self.T1_ns)}, but Tphi has qubits {set(self.Tphi_ns)}.' + ) + if self.T1_ns: # level 1 sophistication + noise_models.append( + devices.ThermalNoiseModel( + {q: 2 for q in self.T1_ns}, + self.gate_times_ns, + cool_rate_GHz={q: 1 / T1 for q, T1 in self.T1_ns.items()}, + dephase_rate_GHz={q: 1 / Tp for q, Tp in self.Tphi_ns.items()}, ) - 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( - ops.Moment( - ops.GeneralizedAmplitudeDampingChannel(p=p, gamma=gamma)(q) for q in measurement_qubits - ) - ) + ) + gate_types = set(op_id.gate_type for op_id in self.gate_pauli_errors) + if not gate_types.issubset(self.expected_gates()): + raise ValueError( + 'Some gates are not in the supported set.' + f'\nGates: {gate_types}\nSupported: {self.expected_gates()}' + ) -def _apply_depol_noise(pauli_error, moments, system_qubits): + depolarizing_error = self.get_depolarizing_error() + added_pauli_errors = { + # TODO: handle op_id not having qubits. + op_id: ops.depolarize(p_error, len(op_id.qubits)).on(*op_id.qubits) + for op_id, p_error in depolarizing_error.items() + if p_error > 0 + } + + # This adds per-qubit pauli error after ops on those qubits. + noise_models.append(devices.InsertionNoiseModel(ops_added=added_pauli_errors)) + + # This adds per-qubit measurement error BEFORE measurement on those qubits. + if self.ro_fidelities: + added_measure_errors: Dict[OpIdentifier, 'cirq.Operation'] = {} + for qubit in self.ro_fidelities: + p_00, p_11 = self.ro_fidelities[qubit] + p = p_11 / (p_00 + p_11) + gamma = p_11 / p + added_measure_errors[ + OpIdentifier(ops.MeasurementGate, qubit) + ] = ops.generalized_amplitude_damp(p, gamma).on(qubit) + + noise_models.append( + devices.InsertionNoiseModel(ops_added=added_measure_errors, prepend=True) + ) - _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(ops.Moment(ops.depolarize(p_other)(q) for q in system_qubits)) + return noise_models + def __str__(self) -> str: + return 'NoiseProperties' -def _apply_amplitude_damp_noise(duration, t1, moments, system_qubits): - moments.append( - ops.Moment(ops.amplitude_damp(1 - np.exp(-duration / t1)).on_each(system_qubits)) - ) + def __repr__(self) -> str: + args = [] + gate_times_repr = ', '.join( + f'{key.__module__}.{key.__qualname__}: {val}' for key, val in self.gate_times_ns.items() + ) + args.append(f' gate_times_ns={{{gate_times_repr}}}') + args.append(f' T1_ns={self.T1_ns!r}') + args.append(f' Tphi_ns={self.Tphi_ns!r}') + args.append(f' ro_fidelities={self.ro_fidelities!r}') + args.append(f' gate_pauli_errors={self.gate_pauli_errors!r}') + args_str = ',\n'.join(args) + return f'cirq.NoiseProperties(\n{args_str}\n)' + + def _json_dict_(self): + storage_gate_times = { + protocols.json_cirq_type(key): val for key, val in self.gate_times_ns.items() + } + return { + # JSON requires mappings to have keys of basic types. + # Pairs must be sorted to ensure consistent serialization. + 'gate_times_ns': sorted(storage_gate_times.items(), key=str), + 'T1_ns': sorted(self.T1_ns.items()), + 'Tphi_ns': sorted(self.Tphi_ns.items()), + 'ro_fidelities': sorted(self.ro_fidelities.items()), + 'gate_pauli_errors': sorted(self.gate_pauli_errors.items(), key=str), + 'validate': self.validate, + } + + @classmethod + def _from_json_dict_( + cls, gate_times_ns, T1_ns, Tphi_ns, ro_fidelities, gate_pauli_errors, validate, **kwargs + ): + gate_type_times = {protocols.cirq_type_from_json(gate): val for gate, val in gate_times_ns} + return NoiseProperties( + gate_times_ns=gate_type_times, + T1_ns=dict(T1_ns), + Tphi_ns=dict(Tphi_ns), + ro_fidelities=dict(ro_fidelities), + gate_pauli_errors=dict(gate_pauli_errors), + validate=validate, + ) class NoiseModelFromNoiseProperties(devices.NoiseModel): @@ -234,37 +281,68 @@ 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') - - def noisy_moment( - self, moment: ops.Moment, system_qubits: Sequence['cirq.Qid'] - ) -> 'cirq.OP_TREE': - moments: List[ops.Moment] = [] - - 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) + self._noise_properties = noise_properties + self.noise_models = self._noise_properties.build_noise_models() + + def virtual_predicate(self, op: 'cirq.Operation') -> bool: + """Returns True if an operation is virtual. + + Device-specific subclasses should implement this method to mark any + operations which their device handles outside the quantum hardware. + """ + return False + + def noisy_moments( + self, moments: Iterable['cirq.Moment'], system_qubits: Sequence['cirq.Qid'] + ) -> Sequence['cirq.OP_TREE']: + # Hack to bypass import order constraints + from cirq import circuits + + # 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(ops.Moment(split_measure_ops)) + + 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 ] - 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) - - 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 + if virtual_ops: + new_moments.append(ops.Moment(virtual_ops)) + if physical_ops: + new_moments.append(ops.Moment(physical_ops)) + + split_measure_circuit = circuits.Circuit(new_moments) + + # Add actual noise. + 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(ops.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..a4b801ea927 100644 --- a/cirq-core/cirq/devices/noise_properties_test.py +++ b/cirq-core/cirq/devices/noise_properties_test.py @@ -1,298 +1,386 @@ -# 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 Dict, List, Tuple +import numpy as np import cirq -from cirq.testing import assert_equivalent_op_tree +import pytest + +# from cirq.testing import assert_equivalent_op_tree from cirq.devices.noise_properties import ( NoiseProperties, NoiseModelFromNoiseProperties, - get_duration_ns, + SINGLE_QUBIT_GATES, + TWO_QUBIT_GATES, +) +from cirq.devices.noise_utils import ( + OpIdentifier, + PHYSICAL_GATE_TAG, ) -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')]), +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, + cirq.PhasedFSimGate: 32.0, + cirq.ISwapPowGate: 32.0, + cirq.CZPowGate: 32.0, + # cirq.WaitGate is a special case. +} + + +# 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]] +): + return NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q: 1e5 for q in system_qubits}, + Tphi_ns={q: 2e5 for q in system_qubits}, + ro_fidelities={q: [0.001, 0.01] 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 TWO_QUBIT_GATES for q0, q1 in qubit_pairs}, + }, ) - # 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')]), +def test_str(): + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + assert str(props) == 'NoiseProperties' + + +def test_repr_evaluation(): + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + props_from_repr = eval(repr(props)) + assert props_from_repr == props + + +def test_json_serialization(): + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + props_json = cirq.to_json(props) + print(props_json) + props_from_json = cirq.read_json(json_text=props_json) + assert props_from_json == props + + +def test_init_validation(): + q0, q1, q2 = cirq.LineQubit.range(3) + with pytest.raises(ValueError, match='Keys specified for T1 and Tphi are not identical.'): + _ = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={}, + ) + + with pytest.raises(ValueError, match='Symmetric errors can only apply to 2-qubit gates.'): + _ = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={OpIdentifier(cirq.CCNOT, q0, q1, q2): 0.1}, + ) + + with pytest.raises(ValueError, match='does not appear in the symmetric or asymmetric'): + _ = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={ + OpIdentifier(cirq.CNOT, q0, q1): 0.1, + OpIdentifier(cirq.CNOT, q1, q0): 0.1, + }, + ) + + with pytest.raises(ValueError, match='has errors but its symmetric id'): + _ = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={OpIdentifier(cirq.CZPowGate, q0, q1): 0.1}, + ) + + # Single-qubit gates are ignored in symmetric-gate validation. + _ = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={ + OpIdentifier(cirq.ZPowGate, q0): 0.1, + OpIdentifier(cirq.CZPowGate, q0, q1): 0.1, + OpIdentifier(cirq.CZPowGate, q1, q0): 0.1, + }, ) - 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')]), + # All errors are ignored if validation is disabled. + _ = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={ + OpIdentifier(cirq.CCNOT, q0, q1, q2): 0.1, + OpIdentifier(cirq.CNOT, q0, q1): 0.1, + }, + validate=False, ) - 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')]), +def test_qubits(): + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + assert props.qubits == [q0] + # Confirm memoization behavior. + assert props.qubits == [q0] + + +def test_depol_memoization(): + # Verify that depolarizing error is memoized. + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + depol_error_a = props.get_depolarizing_error() + depol_error_b = props.get_depolarizing_error() + assert depol_error_a == depol_error_b + assert depol_error_a is depol_error_b + + +def test_depol_validation(): + q0, q1, q2 = cirq.LineQubit.range(3) + # Create unvalidated properties with too many qubits on a Z gate. + z_2q_props = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={OpIdentifier(cirq.ZPowGate, q0, q1): 0.1}, + validate=False, ) - - 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')]), + with pytest.raises(ValueError, match='only takes one qubit'): + _ = z_2q_props.get_depolarizing_error() + + # Create unvalidated properties with an unsupported gate. + toffoli_props = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={OpIdentifier(cirq.CCNOT, q0, q1, q2): 0.1}, + validate=False, ) - - # 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)]), + with pytest.raises(ValueError, match='not in the supported gate list'): + _ = toffoli_props.get_depolarizing_error() + + # Create unvalidated properties with too many qubits on a CZ gate. + cz_3q_props = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={OpIdentifier(cirq.CZPowGate, q0, q1, q2): 0.1}, + validate=False, ) - assert_equivalent_op_tree(expected_circuit, noisy_circuit) - + with pytest.raises(ValueError, match='takes two qubits'): + _ = cz_3q_props.get_depolarizing_error() + + # If T1_ns is missing, values are filled in as needed. + + +def test_build_noise_model_validation(): + q0, q1, q2 = cirq.LineQubit.range(3) + # Create unvalidated properties with mismatched T1 and Tphi qubits. + t1_tphi_props = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={}, + validate=False, + ) + with pytest.raises(ValueError, match='but Tphi has qubits'): + _ = t1_tphi_props.build_noise_models() + + # Create unvalidated properties with unsupported gates. + toffoli_props = NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={OpIdentifier(cirq.CCNOT, q0, q1, q2): 0.1}, + validate=False, + ) + with pytest.raises(ValueError, match='Some gates are not in the supported set.'): + _ = toffoli_props.build_noise_models() -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')]), +@pytest.mark.parametrize( + 'op', + [ + cirq.Z(cirq.LineQubit(0)) ** 0.3, + 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 = NoiseModelFromNoiseProperties(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], + ], ) - # 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)]), + +@pytest.mark.parametrize( + 'op', + [ + cirq.ISWAP(*cirq.LineQubit.range(2)) ** 0.6, + cirq.CZ(*cirq.LineQubit.range(2)) ** 0.3, + ], +) +def test_two_qubit_gates(op): + q0, q1 = cirq.LineQubit.range(2) + props = sample_noise_properties([q0, q1], [(q0, q1), (q1, q0)]) + model = NoiseModelFromNoiseProperties(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.00952008) + + # Thermal noise + assert len(noisy_circuit.moments[2].operations) == 2 + thermal_op_0 = noisy_circuit.moments[2].operation_at(q0) + thermal_op_1 = noisy_circuit.moments[2].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)) + print(thermal_choi_0) + thermal_choi_1 = cirq.kraus_to_choi(cirq.kraus(thermal_op_1)) + # TODO: check iswap noise + 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_equivalent_op_tree(expected_circuit, noisy_circuit) - - -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 - - t1_ns = 2000.0 - p11 = 0.01 - - # 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)] - 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')]), + assert np.allclose(thermal_choi_0, expected_thermal_choi) + assert np.allclose(thermal_choi_1, expected_thermal_choi) + + +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), + ], ) - - # 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 - 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)]), + model = NoiseModelFromNoiseProperties(props) + op = cirq.measure(*qubits, key='m') + circuit = cirq.Circuit(cirq.measure(*qubits, key='m')) + noisy_circuit = circuit.with_noise(model) + print(noisy_circuit.moments) + 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 = NoiseModelFromNoiseProperties(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], + ], ) - assert_equivalent_op_tree(expected_circuit, noisy_circuit) diff --git a/cirq-core/cirq/devices/noise_utils.py b/cirq-core/cirq/devices/noise_utils.py new file mode 100644 index 00000000000..c3b5f346681 --- /dev/null +++ b/cirq-core/cirq/devices/noise_utils.py @@ -0,0 +1,208 @@ +# 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 + ) + return ( + (more_specific_qubits or more_specific_gate) + and (more_specific_qubits or self.qubits == op_id.qubits) + and (more_specific_gate or self.gate_type == op_id.gate_type) + ) + + 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 + """ + 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 + """ + 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 + """ + 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 + """ + N = 2 ** num_qubits + return 1 - (1 - xeb_fidelity) / (1 - 1 / N) + + +def pauli_error_from_t1(t: float, t1_ns: float) -> float: + """Calculates the pauli error from T1 decay constant. + + This computes error for a specific duration, `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(t: 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: The duration of the gate + t1_ns: The T1 decay constant in ns + pauli_error: The pauli error + """ + t1_pauli_error = pauli_error_from_t1(t, t1_ns) + if pauli_error >= t1_pauli_error: + return pauli_error - t1_pauli_error + else: + 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 + """ + 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. + """ + Gamma2 = (1 / (2 * T1_ns)) + 1 / Tphi_ns + + exp1 = np.exp(-gate_time_ns / T1_ns) + exp2 = np.exp(-gate_time_ns * Gamma2) + 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..61b5852ced4 --- /dev/null +++ b/cirq-core/cirq/devices/noise_utils_test.py @@ -0,0 +1,140 @@ +# 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_id(): + op_id = OpIdentifier(cirq.XPowGate) + assert cirq.X(cirq.LineQubit(1)) in op_id + assert cirq.Rx(rads=1) in op_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/devices/thermal_noise_model.py b/cirq-core/cirq/devices/thermal_noise_model.py new file mode 100644 index 00000000000..0bdc135d703 --- /dev/null +++ b/cirq-core/cirq/devices/thermal_noise_model.py @@ -0,0 +1,251 @@ +# 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 dataclasses import dataclass +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Union +from scipy.linalg import expm +import numpy as np + +from cirq import devices, ops, protocols, qis +from cirq.devices.noise_utils import ( + PHYSICAL_GATE_TAG, +) + +if TYPE_CHECKING: + import cirq + + +def _left_mul(mat: np.ndarray) -> np.ndarray: + """Superoperator associated with left multiplication by a square matrix.""" + mat = np.asarray(mat) + if mat.shape[-1] != mat.shape[-2]: + raise ValueError( + f'_left_mul only accepts square matrices, but input matrix has shape {mat.shape}.' + ) + dim = mat.shape[-1] + + return np.kron(mat, np.eye(dim)) + + +def _right_mul(mat: np.ndarray) -> np.ndarray: + """Superoperator associated with right multiplication by a square matrix.""" + mat = np.asarray(mat) + if mat.shape[-1] != mat.shape[-2]: + raise ValueError( + f'_right_mul only accepts square matrices, but input matrix has shape {mat.shape}.' + ) + dim = mat.shape[-1] + + return np.kron(np.eye(dim), np.swapaxes(mat, -2, -1)) + + +def _lindbladian(left_op: np.ndarray) -> np.ndarray: + r"""Superoperator representing a Lindbladian. + + The Lindbladian generated by a single operator A is the superoperator + + $$ + L(\rho) = A \rho A^\dagger - 0.5 (A^\dagger A \rho + \rho A^\dagger A) + $$ + + Args: + left_op: The operator acting on the left in the Lindbladian (A above). + + Returns: + Superoperator corresponding to the Lindbladian. + """ + left_op = np.asarray(left_op) + right_op = left_op.conj().T + square = right_op @ left_op + out = _left_mul(left_op) @ _right_mul(right_op) + out -= 0.5 * (_left_mul(square) + _right_mul(square)) + return out + + +def _decoherence_matrix( + cool_rate: float, dephase_rate: float, heat_rate: float = 0.0, dim: int = 2 +) -> np.ndarray: + """Construct a rate matrix associated with decay and dephasing. + + The units of the matrix match the units of the rates specified. + This matrix can be used to construct a ThermalChannel after rescaling + by an idling time (to make it dimensionless). + + Args: + cool_rate: Decay rate of the system, usually 1 / T_1 + dephase_rate: Static dephasing rate of the system, usually 1 / T_phi + heat_rate: Heating rate of the system (default 0). + dim: Number of energy levels to include (default 2). + + Returns: + np.ndarray rate matrix for decay and dephasing. + """ + # Heating (related to a^dag) + rate_matrix = np.diag(np.arange(1, dim) * heat_rate, 1).T.astype(float) + # Cooling (related to a) + rate_matrix += np.diag(np.arange(1, dim) * cool_rate, 1) + # Dephasing (related to n=a^dag * a) + # We specify i^2 since we take square root to get the Lindblad op later. + rate_matrix += np.diag(dephase_rate * np.arange(dim) ** 2) + return rate_matrix + + +def _validate_rates(qubit_dims: Dict['cirq.Qid', int], rates: Dict['cirq.Qid', np.ndarray]) -> None: + """Check all rate matrices are square and of appropriate dimension. + + We check rates are positive in the class validator. + """ + if set(qubit_dims) != set(rates): + raise ValueError('qubits for rates inconsistent with those through qubit_dims') + for q in rates: + if rates[q].shape != (qubit_dims[q], qubit_dims[q]): + raise ValueError( + f'Invalid shape for rate matrix: should be ({qubit_dims[q]}, {qubit_dims[q]}), ' + f'but got {rates[q].shape}' + ) + + +@dataclass +class ThermalNoiseModel(devices.NoiseModel): + """NoiseModel representing simulated thermalization of a qubit. + + This model is designed for qubits which use energy levels as their states. + "Heating" and "cooling" here are used to refer to environmental noise which + transitions a qubit to higher or lower energy levels, respectively. + """ + + def __init__( + self, + qubit_dims: Dict['cirq.Qid', int], + gate_durations_ns: Dict[type, float], + heat_rate_GHz: Union[float, Dict['cirq.Qid', float], None] = None, + cool_rate_GHz: Union[float, Dict['cirq.Qid', float], None] = None, + dephase_rate_GHz: Union[float, Dict['cirq.Qid', float], None] = None, + require_physical_tag: bool = True, + skip_measurements: bool = True, + ): + """Construct a ThermalNoiseModel data object. + + Required Args: + qubit_dims: Dimension for all qubits in the system. + Currently only supports dimension=2 (qubits, not qudits) + Optional Args: + heat_rate_GHz: single number (units GHz) specifying heating rate, + either per qubit, or global value for all. + Given a rate gh, the Lindblad op will be sqrt(gh)*a^dag + (where a is annihilation), + so that the heating Lindbldian is + gh(a^dag • a - 0.5{a*a^dag, •}). + cool_rate_GHz: single number (units GHz) specifying cooling rate, + either per qubit, or global value for all. + Given a rate gc, the Lindblad op will be sqrt(gc)*a + so that the cooling Lindbldian is + gc(a • a^dag - 0.5{n, •}) + This number is equivalent to 1/T1. + dephase_rate_GHz: single number (units GHz) specifying dephasing + rate, either per qubit, or global value for all. + Given a rate gd, Lindblad op will be sqrt(2*gd)*n where + n = a^dag * a, so that the dephasing Lindbldian is + 2 * gd * (n • n - 0.5{n^2, •}). + This number is equivalent to 1/Tphi. + require_physical_tag: whether to only apply noise to operations + tagged with PHYSICAL_GATE_TAG. + skip_measurements: whether to skip applying noise to measurements. + + Returns: + The ThermalNoiseModel with specified parameters. + """ + qubits = set(qubit_dims) + rate_dict = {} + + def _as_rate_dict( + rate_or_dict: Optional[Union[float, Dict['cirq.Qid', float]]] + ) -> Dict['cirq.Qid', float]: + # Convert float or None input into dictionary form. Make sure no + # qubits are missing from dictionary input. + if rate_or_dict is None: + return {qb: 0.0 for qb in qubits} + elif isinstance(rate_or_dict, dict): + out = rate_or_dict.copy() + for qb in qubits: + if qb not in rate_or_dict: + out[qb] = 0.0 + return out + else: + return {qb: rate_or_dict for qb in qubits} + + heat_rate_GHz = _as_rate_dict(heat_rate_GHz) + cool_rate_GHz = _as_rate_dict(cool_rate_GHz) + dephase_rate_GHz = _as_rate_dict(dephase_rate_GHz) + + for q, dim in qubit_dims.items(): + gamma_h = heat_rate_GHz[q] + gamma_c = cool_rate_GHz[q] + gamma_phi = dephase_rate_GHz[q] + + rate_dict[q] = _decoherence_matrix(gamma_c, gamma_phi, gamma_h, dim) + + _validate_rates(qubit_dims, rate_dict) + self.gate_durations_ns: Dict[type, float] = gate_durations_ns + self.rate_matrix_GHz: Dict['cirq.Qid', np.ndarray] = rate_dict + self.require_physical_tag: bool = require_physical_tag + self.skip_measurements: bool = skip_measurements + + def noisy_moment( + self, moment: 'cirq.Moment', system_qubits: Sequence['cirq.Qid'] + ) -> 'cirq.OP_TREE': + noise_ops: List['cirq.Operation'] = [] + moment_ns: float = 0 + for op in moment: + op_duration: Optional[float] = None + for key, duration in self.gate_durations_ns.items(): + if not issubclass(type(op.gate), key): + continue # gate type doesn't match + # TODO: remove assumption of same time across qubits + # if len(key) > 1 and op_data[:1] != key[:1]: + # continue # qubits don't match + op_duration = duration + break + if op_duration is None: + if not isinstance(op.gate, ops.WaitGate): + continue + # special case for wait gates if not predefined + op_duration = op.gate.duration.total_nanos() + moment_ns = max(moment_ns, op_duration) + + if moment_ns == 0: + return [moment] + + for qubit in system_qubits: + qubit_op = moment.operation_at(qubit) + if qubit_op is None: + continue + if self.skip_measurements and protocols.is_measurement(qubit_op): + continue + if self.require_physical_tag and PHYSICAL_GATE_TAG not in qubit_op.tags: + # Only non-virtual gates get noise applied. + continue + rates = self.rate_matrix_GHz[qubit] * moment_ns + num_op = np.diag(np.sqrt(np.diag(rates))) + annihilation = np.sqrt(np.triu(rates, 1)) + creation = np.sqrt(np.triu(rates.T, 1)).T + # Lindbladian with three Lindblad ops for the three processes + # Note: 'time' parameter already specified implicitly through rates + L = _lindbladian(annihilation) + _lindbladian(creation) + 2 * _lindbladian(num_op) + superop = expm(L.real) + kraus_ops = qis.superoperator_to_kraus(superop) + noise_ops.append(ops.KrausChannel(kraus_ops).on(qubit)) + if not noise_ops: + return [moment] + return [moment, ops.Moment(noise_ops)] diff --git a/cirq-core/cirq/devices/thermal_noise_model_test.py b/cirq-core/cirq/devices/thermal_noise_model_test.py new file mode 100644 index 00000000000..cbb273e55cd --- /dev/null +++ b/cirq-core/cirq/devices/thermal_noise_model_test.py @@ -0,0 +1,251 @@ +# 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 ( + PHYSICAL_GATE_TAG, +) +from cirq.devices.thermal_noise_model import ( + _left_mul, + _right_mul, + _validate_rates, + ThermalNoiseModel, +) + + +def test_helper_method_errors(): + with pytest.raises(ValueError, match='_left_mul only accepts square matrices'): + _ = _left_mul(np.array([[1, 2, 3], [4, 5, 6]])) + + with pytest.raises(ValueError, match='_right_mul only accepts square matrices'): + _ = _right_mul(np.array([[1, 2, 3], [4, 5, 6]])) + + q0, q1 = cirq.LineQubit.range(2) + with pytest.raises(ValueError, match='qubits for rates inconsistent'): + _validate_rates({q0: 2, q1: 2}, {q0: np.array([[0.01, 0.1], [0.02, 0.2]])}) + + with pytest.raises(ValueError, match='qubits for rates inconsistent'): + _validate_rates( + {q0: 2}, + {q0: np.array([[0.01, 0.1], [0.02, 0.2]]), q1: np.array([[0.03, 0.3], [0.04, 0.4]])}, + ) + + with pytest.raises(ValueError, match='Invalid shape for rate matrix'): + _validate_rates({q0: 2}, {q0: np.array([[0.001, 0.01, 0.1], [0.002, 0.02, 0.2]])}) + + +def test_create_thermal_noise_per_qubit(): + q0, q1 = cirq.LineQubit.range(2) + gate_durations = {cirq.PhasedXZGate: 25.0} + heat_rate_GHz = {q0: 1e-5, q1: 2e-5} + cool_rate_GHz = {q0: 1e-4, q1: 2e-4} + dephase_rate_GHz = {q0: 3e-4, q1: 4e-4} + model = ThermalNoiseModel( + qubit_dims={q0: 2, q1: 2}, + gate_durations_ns=gate_durations, + heat_rate_GHz=heat_rate_GHz, + cool_rate_GHz=cool_rate_GHz, + dephase_rate_GHz=dephase_rate_GHz, + ) + assert model.gate_durations_ns == gate_durations + assert model.require_physical_tag + assert model.skip_measurements + assert np.allclose(model.rate_matrix_GHz[q0], np.array([[0, 1e-4], [1e-5, 3e-4]])) + assert np.allclose(model.rate_matrix_GHz[q1], np.array([[0, 2e-4], [2e-5, 4e-4]])) + + +def test_create_thermal_noise_mixed_type(): + q0, q1 = cirq.LineQubit.range(2) + gate_durations = {cirq.PhasedXZGate: 25.0} + heat_rate_GHz = None + cool_rate_GHz = {q0: 1e-4, q1: 2e-4} + dephase_rate_GHz = 3e-4 + model = ThermalNoiseModel( + qubit_dims={q0: 2, q1: 2}, + gate_durations_ns=gate_durations, + heat_rate_GHz=heat_rate_GHz, + cool_rate_GHz=cool_rate_GHz, + dephase_rate_GHz=dephase_rate_GHz, + ) + assert model.gate_durations_ns == gate_durations + assert model.require_physical_tag + assert model.skip_measurements + assert np.allclose(model.rate_matrix_GHz[q0], np.array([[0, 1e-4], [0, 3e-4]])) + assert np.allclose(model.rate_matrix_GHz[q1], np.array([[0, 2e-4], [0, 3e-4]])) + + +def test_incomplete_rates(): + q0, q1 = cirq.LineQubit.range(2) + gate_durations = {cirq.PhasedXZGate: 25.0} + heat_rate_GHz = {q1: 1e-5} + cool_rate_GHz = {q0: 1e-4} + model = ThermalNoiseModel( + qubit_dims={q0: 2, q1: 2}, + gate_durations_ns=gate_durations, + heat_rate_GHz=heat_rate_GHz, + cool_rate_GHz=cool_rate_GHz, + dephase_rate_GHz=None, + ) + assert model.gate_durations_ns == gate_durations + assert model.require_physical_tag + assert model.skip_measurements + assert np.allclose(model.rate_matrix_GHz[q0], np.array([[0, 1e-4], [0, 0]])) + assert np.allclose(model.rate_matrix_GHz[q1], np.array([[0, 0], [1e-5, 0]])) + + +def test_noise_from_zero_duration(): + # Verify that a moment with no duration has no noise. + q0, q1 = cirq.LineQubit.range(2) + gate_durations = {} + heat_rate_GHz = {q1: 1e-5} + cool_rate_GHz = {q0: 1e-4} + model = ThermalNoiseModel( + qubit_dims={q0: 2, q1: 2}, + gate_durations_ns=gate_durations, + heat_rate_GHz=heat_rate_GHz, + cool_rate_GHz=cool_rate_GHz, + dephase_rate_GHz=None, + require_physical_tag=False, + skip_measurements=False, + ) + moment = cirq.Moment(cirq.Z(q0), cirq.Z(q1)) + assert model.noisy_moment(moment, system_qubits=[q0, q1]) == [moment] + + +def test_noise_from_virtual_gates(): + # Verify that a moment with only virtual gates has no noise if + # require_physical_tag is True. + q0, q1 = cirq.LineQubit.range(2) + gate_durations = {cirq.ZPowGate: 25.0} + heat_rate_GHz = {q1: 1e-5} + cool_rate_GHz = {q0: 1e-4} + model = ThermalNoiseModel( + qubit_dims={q0: 2, q1: 2}, + gate_durations_ns=gate_durations, + heat_rate_GHz=heat_rate_GHz, + cool_rate_GHz=cool_rate_GHz, + dephase_rate_GHz=None, + require_physical_tag=True, + skip_measurements=False, + ) + moment = cirq.Moment(cirq.Z(q0), cirq.Z(q1)) + assert model.noisy_moment(moment, system_qubits=[q0, q1]) == [moment] + + part_virtual_moment = cirq.Moment(cirq.Z(q0), cirq.Z(q1).with_tags(PHYSICAL_GATE_TAG)) + assert len(model.noisy_moment(part_virtual_moment, system_qubits=[q0, q1])) == 2 + + model.require_physical_tag = False + assert len(model.noisy_moment(moment, system_qubits=[q0, q1])) == 2 + + +def test_noise_from_measurement(): + # Verify that a moment with only measurement gates has no noise if + # skip_measurements is True. + q0, q1 = cirq.LineQubit.range(2) + gate_durations = { + cirq.ZPowGate: 25.0, + cirq.MeasurementGate: 4000.0, + } + heat_rate_GHz = {q1: 1e-5} + cool_rate_GHz = {q0: 1e-4} + model = ThermalNoiseModel( + qubit_dims={q0: 2, q1: 2}, + gate_durations_ns=gate_durations, + heat_rate_GHz=heat_rate_GHz, + cool_rate_GHz=cool_rate_GHz, + dephase_rate_GHz=None, + require_physical_tag=False, + skip_measurements=True, + ) + moment = cirq.Moment(cirq.measure(q0, q1, key='m')) + assert model.noisy_moment(moment, system_qubits=[q0, q1]) == [moment] + + part_measure_moment = cirq.Moment(cirq.measure(q0, key='m'), cirq.Z(q1)) + assert len(model.noisy_moment(part_measure_moment, system_qubits=[q0, q1])) == 2 + + model.skip_measurements = False + assert len(model.noisy_moment(moment, system_qubits=[q0, q1])) == 2 + + +def test_noisy_moment_one_qubit(): + q0, q1 = cirq.LineQubit.range(2) + model = ThermalNoiseModel( + qubit_dims={q0: 2, q1: 2}, + gate_durations_ns={ + cirq.PhasedXZGate: 25.0, + cirq.CZPowGate: 25.0, + }, + heat_rate_GHz={q0: 1e-5, q1: 2e-5}, + cool_rate_GHz={q0: 1e-4, q1: 2e-4}, + dephase_rate_GHz={q0: 3e-4, q1: 4e-4}, + require_physical_tag=False, + ) + gate = cirq.PhasedXZGate(x_exponent=1, z_exponent=0.5, axis_phase_exponent=0.25) + moment = cirq.Moment(gate.on(q0)) + noisy_moment = model.noisy_moment(moment, system_qubits=[q0, q1]) + assert noisy_moment[0] == moment + assert len(noisy_moment[1]) == 1 + noisy_choi = cirq.kraus_to_choi(cirq.kraus(noisy_moment[1].operations[0])) + assert np.allclose( + noisy_choi, + [ + [9.99750343e-01, 0, 0, 9.91164267e-01], + [0, 2.49656565e-03, 0, 0], + [0, 0, 2.49656565e-04, 0], + [9.91164267e-01, 0, 0, 9.97503434e-01], + ], + ) + + +def test_noisy_moment_two_qubit(): + q0, q1 = cirq.LineQubit.range(2) + model = ThermalNoiseModel( + qubit_dims={q0: 2, q1: 2}, + gate_durations_ns={ + cirq.PhasedXZGate: 25.0, + cirq.CZPowGate: 25.0, + }, + heat_rate_GHz={q0: 1e-5, q1: 2e-5}, + cool_rate_GHz={q0: 1e-4, q1: 2e-4}, + dephase_rate_GHz={q0: 3e-4, q1: 4e-4}, + require_physical_tag=False, + ) + gate = cirq.CZ ** 0.5 + moment = cirq.Moment(gate.on(q0, q1)) + noisy_moment = model.noisy_moment(moment, system_qubits=[q0, q1]) + assert noisy_moment[0] == moment + assert len(noisy_moment[1]) == 2 + noisy_choi_0 = cirq.kraus_to_choi(cirq.kraus(noisy_moment[1].operations[0])) + assert np.allclose( + noisy_choi_0, + [ + [9.99750343e-01, 0, 0, 9.91164267e-01], + [0, 2.49656565e-03, 0, 0], + [0, 0, 2.49656565e-04, 0], + [9.91164267e-01, 0, 0, 9.97503434e-01], + ], + ) + noisy_choi_1 = cirq.kraus_to_choi(cirq.kraus(noisy_moment[1].operations[1])) + assert np.allclose( + noisy_choi_1, + [ + [9.99501372e-01, 0, 0, 9.87330937e-01], + [0, 4.98627517e-03, 0, 0], + [0, 0, 4.98627517e-04, 0], + [9.87330937e-01, 0, 0, 9.95013725e-01], + ], + ) diff --git a/cirq-core/cirq/json_resolver_cache.py b/cirq-core/cirq/json_resolver_cache.py index f18fbc90c91..12c2a0fa357 100644 --- a/cirq-core/cirq/json_resolver_cache.py +++ b/cirq-core/cirq/json_resolver_cache.py @@ -114,6 +114,8 @@ def _parallel_gate_op(gate, qubits): 'NamedQubit': cirq.NamedQubit, 'NamedQid': cirq.NamedQid, 'NoIdentifierQubit': cirq.testing.NoIdentifierQubit, + 'NoiseProperties': cirq.NoiseProperties, + '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/NoiseProperties.json b/cirq-core/cirq/protocols/json_test_data/NoiseProperties.json new file mode 100644 index 00000000000..83a5c47893a --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/NoiseProperties.json @@ -0,0 +1,122 @@ +{ + "cirq_type": "NoiseProperties", + "gate_times_ns": [ + [ + "CZPowGate", + 32.0 + ], + [ + "FSimGate", + 32.0 + ], + [ + "ISwapPowGate", + 32.0 + ], + [ + "MeasurementGate", + 4000.0 + ], + [ + "PhasedFSimGate", + 32.0 + ], + [ + "PhasedXZGate", + 25.0 + ], + [ + "ResetChannel", + 250.0 + ], + [ + "ZPowGate", + 25.0 + ] + ], + "T1_ns": [ + [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + 100000.0 + ] + ], + "Tphi_ns": [ + [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + 200000.0 + ] + ], + "ro_fidelities": [ + [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + [ + 0.001, + 0.01 + ] + ] + ], + "gate_pauli_errors": [ + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ResetChannel", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ZPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "MeasurementGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "PhasedXZGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.001 + ] + ], + "validate": true +} \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/NoiseProperties.repr b/cirq-core/cirq/protocols/json_test_data/NoiseProperties.repr new file mode 100644 index 00000000000..42c51a7eedc --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/NoiseProperties.repr @@ -0,0 +1,21 @@ +cirq.NoiseProperties( + gate_times_ns={ + cirq.ops.common_gates.ZPowGate: 25.0, + cirq.ops.measurement_gate.MeasurementGate: 4000.0, + cirq.ops.common_channels.ResetChannel: 250.0, + cirq.ops.phased_x_z_gate.PhasedXZGate: 25.0, + cirq.ops.fsim_gate.FSimGate: 32.0, + cirq.ops.fsim_gate.PhasedFSimGate: 32.0, + cirq.ops.swap_gates.ISwapPowGate: 32.0, + cirq.ops.common_gates.CZPowGate: 32.0 + }, + T1_ns={cirq.LineQubit(0): 100000.0}, + Tphi_ns={cirq.LineQubit(0): 200000.0}, + ro_fidelities={cirq.LineQubit(0): [0.001, 0.01]}, + gate_pauli_errors={ + cirq.devices.noise_utils.OpIdentifier(cirq.ops.phased_x_z_gate.PhasedXZGate, cirq.LineQubit(0)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.measurement_gate.MeasurementGate, cirq.LineQubit(0)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_gates.ZPowGate, cirq.LineQubit(0)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_channels.ResetChannel, cirq.LineQubit(0)): 0.001 + } +) \ No newline at end of file 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 diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py index a1602e8e7f2..3c8d8ea259f 100644 --- a/cirq-core/cirq/protocols/json_test_data/spec.py +++ b/cirq-core/cirq/protocols/json_test_data/spec.py @@ -174,6 +174,7 @@ 'ParamDictType', # utility: 'CliffordSimulator', + 'NoiseModelFromNoiseProperties', 'Simulator', 'StabilizerSampler', 'Unique', diff --git a/cirq-google/cirq_google/__init__.py b/cirq-google/cirq_google/__init__.py index 6a5bef1fc00..53a17a09a6a 100644 --- a/cirq-google/cirq_google/__init__.py +++ b/cirq-google/cirq_google/__init__.py @@ -57,6 +57,8 @@ from cirq_google.devices import ( Bristlecone, Foxtail, + GoogleNoiseProperties, + NoiseModelFromGoogleNoiseProperties, SerializableDevice, Sycamore, Sycamore23, diff --git a/cirq-google/cirq_google/devices/__init__.py b/cirq-google/cirq_google/devices/__init__.py index 9746693735a..2ba59d16b53 100644 --- a/cirq-google/cirq_google/devices/__init__.py +++ b/cirq-google/cirq_google/devices/__init__.py @@ -12,6 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from cirq_google.devices.google_noise_properties import ( + GoogleNoiseProperties, + NoiseModelFromGoogleNoiseProperties, +) + from cirq_google.devices.known_devices import ( Bristlecone, Foxtail, 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 00000000000..e4b63ff221e --- /dev/null +++ b/cirq-google/cirq_google/devices/google_noise_properties.py @@ -0,0 +1,154 @@ +# 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 dataclasses import dataclass, field +from typing import Dict, List, Set +from cirq import protocols +import numpy as np + +import cirq, cirq_google +from cirq import devices +from cirq.devices.noise_utils import ( + OpIdentifier, +) +from cirq_google.optimizers.two_qubit_gates.math_utils import unitary_entanglement_fidelity + + +@dataclass +class GoogleNoiseProperties(cirq.NoiseProperties): + """Noise-defining properties for a Google device. + + Inherited args: + gate_times_ns: Dict[type, float] of gate types to their duration on + Google hardware. + T1_ns: Dict[cirq.Qid, float] of qubits to their T1 time, in ns. + Tphi_ns: Dict[cirq.Qid, float] of qubits to their Tphi time, in ns. + ro_fidelities: Dict[cirq.Qid, np.ndarray] of qubits to their readout + fidelity matrix. + gate_pauli_errors: Dict[OpIdentifier, float] of gate types + (potentially on specific qubits) to the Pauli error for that gate. + validate: If True, performs validation on input arguments. Defaults + to True. + + Additional args: + fsim_errors: Dict[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[OpIdentifier, cirq.PhasedFSimGate] = field(default_factory=dict) + + def __post_init__(self): + if not self.validate: + return + t1_qubits = set(self.T1_ns) + tphi_qubits = set(self.Tphi_ns) + + if t1_qubits != tphi_qubits: + raise ValueError(f'Keys specified for T1 and Tphi are not' f' identical.') + + # validate two qubit gate errors. + self._validate_symmetric_errors('gate_pauli_errors') + if self.fsim_errors is not None: + self._validate_symmetric_errors('fsim_errors') + + @classmethod + def two_qubit_gates(cls) -> Set[type]: + return super().two_qubit_gates() | {cirq_google.SycamoreGate} + + def get_depolarizing_error(self) -> Dict[OpIdentifier, float]: + if self._depolarizing_error: + return self._depolarizing_error + + depol_errors = super().get_depolarizing_error() + + for op_id in depol_errors: + if op_id.gate_type not in self.two_qubit_gates(): + continue + # Number of qubits is checked by the parent class. + # Subtract entangling angle error. + if op_id in self.fsim_errors: + unitary_err = cirq.unitary(self.fsim_errors[op_id]) + fid = unitary_entanglement_fidelity(unitary_err, np.eye(4)) + depol_errors[op_id] -= 1 - fid + + # memoization is OK + self._depolarizing_error = depol_errors + return self._depolarizing_error + + 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 + + def __str__(self) -> str: + return 'GoogleNoiseProperties' + + def __repr__(self) -> str: + args = [] + gate_times_repr = ', '.join( + f'{key.__module__}.{key.__qualname__}: {val}' for key, val in self.gate_times_ns.items() + ) + args.append(f' gate_times_ns={{{gate_times_repr}}}') + args.append(f' T1_ns={self.T1_ns!r}') + args.append(f' Tphi_ns={self.Tphi_ns!r}') + args.append(f' ro_fidelities={self.ro_fidelities!r}') + args.append(f' gate_pauli_errors={self.gate_pauli_errors!r}') + args.append(f' fsim_errors={self.fsim_errors!r}') + args_str = ',\n'.join(args) + return f'cirq_google.GoogleNoiseProperties(\n{args_str}\n)' + + def _json_dict_(self): + json_dict = super()._json_dict_() + json_dict['fsim_errors'] = sorted(self.fsim_errors.items(), key=str) + return json_dict + + @classmethod + def _from_json_dict_( + cls, + gate_times_ns, + T1_ns, + Tphi_ns, + ro_fidelities, + gate_pauli_errors, + validate, + fsim_errors, + **kwargs, + ): + # TODO: overlaps op_ids with same qubits in different order + gate_type_times = {protocols.cirq_type_from_json(gate): val for gate, val in gate_times_ns} + return GoogleNoiseProperties( + gate_times_ns=gate_type_times, + T1_ns=dict(T1_ns), + Tphi_ns=dict(Tphi_ns), + ro_fidelities=dict(ro_fidelities), + gate_pauli_errors=dict(gate_pauli_errors), + validate=validate, + fsim_errors=dict(fsim_errors), + ) + + +class NoiseModelFromGoogleNoiseProperties(devices.NoiseModelFromNoiseProperties): + """A noise model defined from noise properties of a Google device.""" + + def virtual_predicate(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 00000000000..fb77b172444 --- /dev/null +++ b/cirq-google/cirq_google/devices/google_noise_properties_test.py @@ -0,0 +1,313 @@ +# 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 Dict, List, Tuple +import numpy as np +import pytest +import cirq, cirq_google + +# from cirq.testing import assert_equivalent_op_tree +from cirq.devices.noise_utils import ( + OpIdentifier, + PHYSICAL_GATE_TAG, +) + +from cirq_google.devices.google_noise_properties import ( + GoogleNoiseProperties, + NoiseModelFromGoogleNoiseProperties, +) + + +SINGLE_QUBIT_GATES = GoogleNoiseProperties.single_qubit_gates() +TWO_QUBIT_GATES = GoogleNoiseProperties.two_qubit_gates() +SYMMETRIC_TWO_QUBIT_GATES = TWO_QUBIT_GATES + +DEFAULT_GATE_NS: Dict[type, float] = { + cirq.ZPowGate: 25.0, + cirq.MeasurementGate: 4000.0, + cirq.ResetChannel: 250.0, + cirq.PhasedXZGate: 25.0, + # SYC is normally 12ns, but setting it equal to other two-qubit gates + # simplifies the tests. + cirq_google.SycamoreGate: 32.0, + cirq.FSimGate: 32.0, + cirq.PhasedFSimGate: 32.0, + cirq.ISwapPowGate: 32.0, + cirq.CZPowGate: 32.0, + # cirq.WaitGate is a special case. +} + + +# 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]] +): + return GoogleNoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q: 1e5 for q in system_qubits}, + Tphi_ns={q: 2e5 for q in system_qubits}, + ro_fidelities={q: [0.001, 0.01] 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 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_str(): + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + assert str(props) == 'GoogleNoiseProperties' + + +def test_repr_evaluation(): + q0, q1 = cirq.LineQubit.range(2) + props = sample_noise_properties([q0, q1], [(q0, q1), (q1, q0)]) + props_from_repr = eval(repr(props)) + assert props_from_repr == props + + +def test_json_serialization(): + q0, q1 = cirq.LineQubit.range(2) + props = sample_noise_properties([q0, q1], [(q0, q1), (q1, q0)]) + props_json = cirq.to_json(props) + props_from_json = cirq.read_json(json_text=props_json) + assert props_from_json == props + + +def test_init_validation(): + q0, q1 = cirq.LineQubit.range(2) + with pytest.raises(ValueError, match='Keys specified for T1 and Tphi are not identical.'): + _ = GoogleNoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={}, + ) + + with pytest.raises(ValueError, match='does not appear in the symmetric or asymmetric'): + _ = GoogleNoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={ + OpIdentifier(cirq.ZPowGate, q0): 0.1, + OpIdentifier(cirq.CZPowGate, q0, q1): 0.1, + OpIdentifier(cirq.CZPowGate, q1, q0): 0.1, + }, + fsim_errors={ + OpIdentifier(cirq.CNOT, q0, q1): cirq.PhasedFSimGate(theta=0.1), + }, + ) + + # Errors are ignored if validation is disabled. + _ = GoogleNoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns={q0: 1}, + Tphi_ns={q0: 1}, + ro_fidelities={q0: [0.1, 0.2]}, + gate_pauli_errors={ + OpIdentifier(cirq.ZPowGate, q0): 0.1, + OpIdentifier(cirq.CZPowGate, q0, q1): 0.1, + OpIdentifier(cirq.CZPowGate, q1, q0): 0.1, + }, + validate=False, + fsim_errors={ + OpIdentifier(cirq.CNOT, q0, q1): cirq.PhasedFSimGate(theta=0.1), + }, + ) + + +def test_depol_memoization(): + # Verify that depolarizing error is memoized. + q0 = cirq.LineQubit(0) + props = sample_noise_properties([q0], []) + depol_error_a = props.get_depolarizing_error() + depol_error_b = props.get_depolarizing_error() + assert depol_error_a == depol_error_b + assert depol_error_a is depol_error_b + + +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], + ], + ) + + +@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 == cirq.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)) + # TODO: check iswap noise + 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) + + +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) + 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], + ], + ) 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 index 06f202a1b7f..0e27d51772c 100644 --- 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 @@ -1,117 +1,113 @@ -# pylint: disable=wrong-or-nonexistent-copyright-notice -import cirq_google +# 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 Dict, Optional +import cirq, cirq_google import numpy as np -from cirq.devices.noise_properties import NoiseProperties +from cirq.devices.noise_properties import ( + NoiseProperties, + SINGLE_QUBIT_GATES, +) +from cirq.devices.noise_utils import ( + OpIdentifier, +) -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 +def _within_tolerance(val_1: Optional[float], val_2: Optional[float], tolerance: float) -> bool: + """Helper function to check if two values are within a given 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 _unpack_from_calibration( + metric_name: str, calibration: cirq_google.Calibration +) -> Dict[cirq.Qid, float]: + """Converts a single-qubit metric from Calibration to dict format.""" + if metric_name not in calibration: + return {} + return { + cirq_google.Calibration.key_to_qubit(key): cirq_google.Calibration.value_to_float(val) + for key, val in calibration[metric_name].items() + } -def noise_properties_from_calibration( - calibration: cirq_google.Calibration, validate: bool = True, tolerance: float = 0.01 -): +def noise_properties_from_calibration(calibration: cirq_google.Calibration) -> NoiseProperties: """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 """ + # TODO: acquire this based on the target device. + # Default map of gates to their durations. + 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, + cirq.ISwapPowGate: 32.0, + cirq.CZPowGate: 32.0, + # cirq.WaitGate is a special case. + } + # 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 + # 1. Extract T1 for all qubits + T1_micros = _unpack_from_calibration('single_qubit_idle_t1_micros', calibration) + T1_ns = {q: T1_micro * 1000 for q, T1_micro in T1_micros.items()} + + # 2. Extract Tphi for all qubits + rb_incoherent_errors = _unpack_from_calibration( + 'single_qubit_rb_incoherent_error_per_gate', calibration ) + Tphi_ns = {} + if rb_incoherent_errors: + microwave_time_ns = DEFAULT_GATE_NS[cirq.PhasedXZGate] + for qubit, t1_ns in T1_ns.items(): + tphi_err = rb_incoherent_errors[qubit] - microwave_time_ns / (3 * t1_ns) + if tphi_err > 0: + tphi_ns = microwave_time_ns / (3 * tphi_err) + else: + tphi_ns = 1e10 + Tphi_ns[qubit] = tphi_ns + + # 3a. Extract Pauli error for single-qubit gates. + rb_pauli_errors = _unpack_from_calibration('single_qubit_rb_pauli_error_per_gate', calibration) + gate_pauli_errors = { + OpIdentifier(gate, q): pauli_err + for q, pauli_err in rb_pauli_errors.items() + for gate in SINGLE_QUBIT_GATES + } + + # TODO: 3a. Extract Pauli error for two-qubit gates. + + # 4. Extract readout fidelity for all qubits. 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) + ro_fidelities = { + q: np.array([p00.get(q, 0), p11.get(q, 0)]) for q in set(p00.keys()) | set(p11.keys()) + } + + # TODO: include entangling errors. + + return NoiseProperties( + gate_times_ns=DEFAULT_GATE_NS, + T1_ns=T1_ns, + Tphi_ns=Tphi_ns, + ro_fidelities=ro_fidelities, + gate_pauli_errors=gate_pauli_errors, + ) 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 index 5412130a321..3e1b94afa8d 100644 --- 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 @@ -1,94 +1,130 @@ -# pylint: disable=wrong-or-nonexistent-copyright-notice -import pytest -import cirq_google +# 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 cirq, cirq_google from cirq_google.api import v2 from cirq_google.experimental.noise_models.calibration_to_noise_properties import ( noise_properties_from_calibration, ) +from cirq.devices.noise_utils import ( + OpIdentifier, +) from google.protobuf.text_format import Merge import numpy as np +import pytest 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 + qubits = [cirq.GridQubit(0, 0), cirq.GridQubit(0, 1), cirq.GridQubit(1, 0)] + pauli_error = [0.001, 0.002, 0.003] + incoherent_error = [0.0001, 0.0002, 0.0003] + p00_error = [0.004, 0.005, 0.006] + p11_error = [0.007, 0.008, 0.009] + t1_micros = [10, 20, 30] _CALIBRATION_DATA = Merge( f""" timestamp_ms: 1579214873, metrics: [{{ - name: 'xeb', - targets: ['0_0', '0_1'], + name: 'single_qubit_rb_pauli_error_per_gate', + targets: ['0_0'], + values: [{{ + double_val: {pauli_error[0]} + }}] + }}, {{ + name: 'single_qubit_rb_pauli_error_per_gate', + targets: ['0_1'], + values: [{{ + double_val:{pauli_error[1]} + }}] + }}, {{ + name: 'single_qubit_rb_pauli_error_per_gate', + targets: ['1_0'], + values: [{{ + double_val:{pauli_error[2]} + }}] + }}, {{ + name: 'single_qubit_rb_incoherent_error_per_gate', + targets: ['0_0'], + values: [{{ + double_val: {incoherent_error[0]} + }}] + }}, {{ + name: 'single_qubit_rb_incoherent_error_per_gate', + targets: ['0_1'], values: [{{ - double_val: {xeb_error_1} + double_val:{incoherent_error[1]} }}] }}, {{ - name: 'xeb', - targets: ['0_0', '1_0'], + name: 'single_qubit_rb_incoherent_error_per_gate', + targets: ['1_0'], values: [{{ - double_val:{xeb_error_2} + double_val:{incoherent_error[2]} }}] }}, {{ name: 'single_qubit_p00_error', targets: ['0_0'], values: [{{ - double_val: {p00_1} + double_val: {p00_error[0]} }}] }}, {{ name: 'single_qubit_p00_error', targets: ['0_1'], values: [{{ - double_val: {p00_2} + double_val: {p00_error[1]} }}] }}, {{ name: 'single_qubit_p00_error', targets: ['1_0'], values: [{{ - double_val: {p00_3} + double_val: {p00_error[2]} }}] }}, {{ - name: 'single_qubit_readout_separation_error', + name: 'single_qubit_p11_error', targets: ['0_0'], values: [{{ - double_val: .004 + double_val: {p11_error[0]} }}] }}, {{ - name: 'single_qubit_readout_separation_error', + name: 'single_qubit_p11_error', targets: ['0_1'], values: [{{ - double_val: .005 + double_val: {p11_error[1]} }}] - }},{{ - name: 'single_qubit_readout_separation_error', + }}, {{ + name: 'single_qubit_p11_error', targets: ['1_0'], values: [{{ - double_val: .006 + double_val: {p11_error[2]} }}] }}, {{ name: 'single_qubit_idle_t1_micros', targets: ['0_0'], values: [{{ - double_val: {t1_1} + double_val: {t1_micros[0]} }}] }}, {{ name: 'single_qubit_idle_t1_micros', targets: ['0_1'], values: [{{ - double_val: {t1_2} + double_val: {t1_micros[1]} }}] }}, {{ name: 'single_qubit_idle_t1_micros', targets: ['1_0'], values: [{{ - double_val: {t1_3} + double_val: {t1_micros[2]} }}] }}] """, @@ -99,186 +135,109 @@ def test_noise_properties_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) - + for i, q in enumerate(qubits): + assert np.isclose( + prop.gate_pauli_errors[OpIdentifier(cirq.PhasedXZGate, q)], pauli_error[i] + ) + assert np.allclose(prop.ro_fidelities[q], np.array([p00_error[i], p11_error[i]])) + assert np.isclose(prop.T1_ns[q], t1_micros[i] * 1000) + # TODO: test Tphi + microwave_time_ns = 25.0 + tphi_err = incoherent_error[i] - microwave_time_ns / (3 * prop.T1_ns[q]) + if tphi_err > 0: + tphi_ns = microwave_time_ns / (3 * tphi_err) + else: + tphi_ns = 1e10 + assert prop.Tphi_ns[q] == tphi_ns + + +def test_incomplete_calibration(): + pauli_error = [0.001, 0.002, 0.003] + p00_error = [0.004, 0.005, 0.006] + p11_error = [0.007, 0.008, 0.009] + t1_micros = [10, 20, 30] -def test_from_calibration_rb(): - rb_pauli_1 = 0.001 - rb_pauli_2 = 0.002 - rb_pauli_3 = 0.003 - - _CALIBRATION_DATA_RB = Merge( + _CALIBRATION_DATA = Merge( f""" timestamp_ms: 1579214873, metrics: [{{ - name: 'single_qubit_rb_pauli_error_per_gate', targets: ['0_0'], values: [{{ - double_val: {rb_pauli_1} + double_val: {pauli_error[0]} }}] }}, {{ name: 'single_qubit_rb_pauli_error_per_gate', targets: ['0_1'], values: [{{ - double_val: {rb_pauli_2} + double_val:{pauli_error[1]} }}] }}, {{ name: 'single_qubit_rb_pauli_error_per_gate', targets: ['1_0'], values: [{{ - double_val: {rb_pauli_3} + double_val:{pauli_error[2]} }}] - }}] - """, - 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', + }}, {{ + name: 'single_qubit_p00_error', targets: ['0_0'], values: [{{ - double_val: {rb_pauli_error} + double_val: {p00_error[0]} }}] }}, {{ - name: 'single_qubit_rb_average_error_per_gate', + name: 'single_qubit_p00_error', targets: ['0_1'], values: [{{ - double_val: {rb_average_error} + double_val: {p00_error[1]} }}] - }}] - """, - 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', + }}, {{ + name: 'single_qubit_p00_error', + targets: ['1_0'], + values: [{{ + double_val: {p00_error[2]} + }}] + }}, {{ + name: 'single_qubit_p11_error', targets: ['0_0'], values: [{{ - double_val: {rb_pauli_error} + double_val: {p11_error[0]} }}] }}, {{ - name: 'xeb', - targets: ['0_0', '1_0'], + name: 'single_qubit_p11_error', + targets: ['0_1'], values: [{{ - double_val:{1 - xeb_fidelity} + double_val: {p11_error[1]} }}] - }}] - """, - 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', + }}, {{ + name: 'single_qubit_p11_error', + targets: ['1_0'], + values: [{{ + double_val: {p11_error[2]} + }}] + }}, {{ + name: 'single_qubit_idle_t1_micros', targets: ['0_0'], values: [{{ - double_val: {rb_average_error} + double_val: {t1_micros[0]} }}] }}, {{ - name: 'xeb', - targets: ['0_0', '1_0'], + name: 'single_qubit_idle_t1_micros', + targets: ['0_1'], values: [{{ - double_val:{1 - xeb_fidelity} + double_val: {t1_micros[1]} }}] - }}] - """, - 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'], + targets: ['1_0'], values: [{{ - double_val: {t1} + double_val: {t1_micros[2]} }}] }}] - """, +""", 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) + # Create NoiseProperties object from Calibration + calibration = cirq_google.Calibration(_CALIBRATION_DATA) + with pytest.raises(ValueError, match='Keys specified for T1 and Tphi are not identical.'): + _ = noise_properties_from_calibration(calibration) 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 index 2996d5fb75d..15f12820c8f 100644 --- 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 @@ -1,8 +1,12 @@ # pylint: disable=wrong-or-nonexistent-copyright-notice # coverage: ignore +from typing import Any, List import cirq_google -import numpy as np from cirq.devices.noise_properties import NoiseModelFromNoiseProperties +from cirq.devices.noise_utils import ( + OpIdentifier, + decay_constant_to_pauli_error, +) from cirq_google.experimental.noise_models.calibration_to_noise_properties import ( noise_properties_from_calibration, ) @@ -10,73 +14,55 @@ import pandas as pd -def compare_generated_noise_to_metrics( - calibration: cirq_google.Calibration, validate: bool = True, tolerance: float = 0.01 -): +def compare_generated_noise_to_metrics(calibration: cirq_google.Calibration): """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) + calibration: Calibration object to convert to noise Returns: - df: Pandas dataframe comparing input and measured values for each calibration metric + 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_prop = noise_properties_from_calibration(calibration) 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)] + # TODO: refactor this experiment + q0 = cirq.LineQubit(0) + p00 = noise_prop.ro_fidelities[q0][0] + p11 = noise_prop.ro_fidelities[q0][1] + pauli_error = noise_prop.gate_pauli_errors[OpIdentifier(cirq.PhasedXZGate, q0)] + t1_ns = noise_prop.T1_ns[q0] - # Create simulator for experiments with noise model + # # Create simulator for experiments with noise model simulator = cirq.sim.DensityMatrixSimulator(noise=noise_model) - # Experiments to measure metrics + # # Experiments to measure metrics estimate_readout = cirq.experiments.estimate_single_qubit_readout_errors( - simulator, qubits=[qubits[0]], repetitions=1000 + simulator, qubits=[q0], repetitions=1000 ) xeb_result = cirq.experiments.cross_entropy_benchmarking( - simulator, qubits, num_circuits=50, repetitions=1000 + simulator, [q0], 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 + decay_constant_result = xeb_result.depolarizing_model().cycle_depolarization + pauli_error_result = decay_constant_to_pauli_error(decay_constant_result) - output = [] + output: List[Any] = [] - 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]) + output.append(['p00', p00, estimate_readout.zero_state_errors[cirq.LineQubit(0)]]) + output.append(['p11', p11, estimate_readout.one_state_errors[cirq.LineQubit(0)]]) + t1_results = cirq.experiments.t1_decay( + simulator, + qubit=q0, + 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]) + output.append(['Pauli Error', pauli_error, pauli_error_result]) columns = ["Metric", "Initial value", "Measured value"] df = pd.DataFrame(output, columns=columns) diff --git a/cirq-google/cirq_google/json_resolver_cache.py b/cirq-google/cirq_google/json_resolver_cache.py index 29e6f90a359..699cc601612 100644 --- a/cirq-google/cirq_google/json_resolver_cache.py +++ b/cirq-google/cirq_google/json_resolver_cache.py @@ -34,6 +34,7 @@ def _class_resolver_dictionary() -> Dict[str, ObjectFactory]: 'GateTabulation': cirq_google.GateTabulation, 'PhysicalZTag': cirq_google.PhysicalZTag, 'FSimGateFamily': cirq_google.FSimGateFamily, + 'GoogleNoiseProperties': cirq_google.GoogleNoiseProperties, 'FloquetPhasedFSimCalibrationOptions': cirq_google.FloquetPhasedFSimCalibrationOptions, 'FloquetPhasedFSimCalibrationRequest': cirq_google.FloquetPhasedFSimCalibrationRequest, 'PhasedFSimCalibrationResult': cirq_google.PhasedFSimCalibrationResult, diff --git a/cirq-google/cirq_google/json_test_data/GoogleNoiseProperties.json b/cirq-google/cirq_google/json_test_data/GoogleNoiseProperties.json new file mode 100644 index 00000000000..e71869651a7 --- /dev/null +++ b/cirq-google/cirq_google/json_test_data/GoogleNoiseProperties.json @@ -0,0 +1,614 @@ +{ + "cirq_type": "GoogleNoiseProperties", + "gate_times_ns": [ + [ + "CZPowGate", + 32.0 + ], + [ + "FSimGate", + 32.0 + ], + [ + "ISwapPowGate", + 32.0 + ], + [ + "MeasurementGate", + 4000.0 + ], + [ + "PhasedFSimGate", + 32.0 + ], + [ + "PhasedXZGate", + 25.0 + ], + [ + "ResetChannel", + 250.0 + ], + [ + "SycamoreGate", + 32.0 + ], + [ + "ZPowGate", + 25.0 + ] + ], + "T1_ns": [ + [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + 100000.0 + ], + [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + 100000.0 + ] + ], + "Tphi_ns": [ + [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + 200000.0 + ], + [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + 200000.0 + ] + ], + "ro_fidelities": [ + [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + [ + 0.001, + 0.01 + ] + ], + [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + [ + 0.001, + 0.01 + ] + ] + ], + "gate_pauli_errors": [ + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ResetChannel", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ResetChannel", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "CZPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "CZPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ZPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ZPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "FSimGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "FSimGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "PhasedFSimGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "PhasedFSimGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "MeasurementGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "MeasurementGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "PhasedXZGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "PhasedXZGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.001 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ISwapPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ISwapPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "SycamoreGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + 0.01 + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "SycamoreGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + 0.01 + ] + ], + "validate": true, + "fsim_errors": [ + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "CZPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "CZPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "FSimGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "FSimGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "PhasedFSimGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "PhasedFSimGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ISwapPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "ISwapPowGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "SycamoreGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 0 + }, + { + "cirq_type": "LineQubit", + "x": 1 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ], + [ + { + "cirq_type": "OpIdentifier", + "gate_type": "SycamoreGate", + "qubits": [ + { + "cirq_type": "LineQubit", + "x": 1 + }, + { + "cirq_type": "LineQubit", + "x": 0 + } + ] + }, + { + "cirq_type": "PhasedFSimGate", + "theta": 0.01, + "zeta": 0.03, + "chi": 0.04, + "gamma": 0.05, + "phi": 0.02 + } + ] + ] +} \ No newline at end of file diff --git a/cirq-google/cirq_google/json_test_data/GoogleNoiseProperties.repr b/cirq-google/cirq_google/json_test_data/GoogleNoiseProperties.repr new file mode 100644 index 00000000000..2c873684e0a --- /dev/null +++ b/cirq-google/cirq_google/json_test_data/GoogleNoiseProperties.repr @@ -0,0 +1,57 @@ +cirq_google.GoogleNoiseProperties( + gate_times_ns={ + cirq.ops.common_gates.ZPowGate: 25.0, + cirq.ops.measurement_gate.MeasurementGate: 4000.0, + cirq.ops.common_channels.ResetChannel: 250.0, + cirq.ops.phased_x_z_gate.PhasedXZGate: 25.0, + cirq_google.ops.sycamore_gate.SycamoreGate: 32.0, + cirq.ops.fsim_gate.FSimGate: 32.0, + cirq.ops.fsim_gate.PhasedFSimGate: 32.0, + cirq.ops.swap_gates.ISwapPowGate: 32.0, + cirq.ops.common_gates.CZPowGate: 32.0 + }, + T1_ns={ + cirq.LineQubit(0): 100000.0, + cirq.LineQubit(1): 100000.0 + }, + Tphi_ns={ + cirq.LineQubit(0): 200000.0, + cirq.LineQubit(1): 200000.0 + }, + ro_fidelities={ + cirq.LineQubit(0): [0.001, 0.01], + cirq.LineQubit(1): [0.001, 0.01] + }, + gate_pauli_errors={ + cirq.devices.noise_utils.OpIdentifier(cirq.ops.measurement_gate.MeasurementGate, *(cirq.LineQubit(0),)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.measurement_gate.MeasurementGate, *(cirq.LineQubit(1),)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.phased_x_z_gate.PhasedXZGate, *(cirq.LineQubit(0),)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.phased_x_z_gate.PhasedXZGate, *(cirq.LineQubit(1),)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_channels.ResetChannel, *(cirq.LineQubit(0),)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_channels.ResetChannel, *(cirq.LineQubit(1),)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_gates.ZPowGate, *(cirq.LineQubit(0),)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_gates.ZPowGate, *(cirq.LineQubit(1),)): 0.001, + cirq.devices.noise_utils.OpIdentifier(cirq_google.ops.sycamore_gate.SycamoreGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq_google.ops.sycamore_gate.SycamoreGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.fsim_gate.PhasedFSimGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.fsim_gate.PhasedFSimGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.fsim_gate.FSimGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.fsim_gate.FSimGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.swap_gates.ISwapPowGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.swap_gates.ISwapPowGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_gates.CZPowGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): 0.01, + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_gates.CZPowGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): 0.01 + }, + fsim_errors={ + cirq.devices.noise_utils.OpIdentifier(cirq_google.ops.sycamore_gate.SycamoreGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq_google.ops.sycamore_gate.SycamoreGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq.ops.fsim_gate.PhasedFSimGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq.ops.fsim_gate.PhasedFSimGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq.ops.fsim_gate.FSimGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq.ops.fsim_gate.FSimGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq.ops.swap_gates.ISwapPowGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq.ops.swap_gates.ISwapPowGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_gates.CZPowGate, *(cirq.LineQubit(0), cirq.LineQubit(1))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02), + cirq.devices.noise_utils.OpIdentifier(cirq.ops.common_gates.CZPowGate, *(cirq.LineQubit(1), cirq.LineQubit(0))): cirq.PhasedFSimGate(theta=0.01, zeta=0.03, chi=0.04, gamma=0.05, phi=0.02) + } +) diff --git a/cirq-google/cirq_google/json_test_data/spec.py b/cirq-google/cirq_google/json_test_data/spec.py index 1a7e08b94b0..52c377c7637 100644 --- a/cirq-google/cirq_google/json_test_data/spec.py +++ b/cirq-google/cirq_google/json_test_data/spec.py @@ -39,6 +39,7 @@ 'EngineProgram', 'FSimPhaseCorrections', 'NAMED_GATESETS', + 'NoiseModelFromGoogleNoiseProperties', 'ProtoVersion', 'GateOpSerializer', 'GateOpDeserializer',