Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SuperconductingQubitsNoiseProperties #4964

Merged
merged 8 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
NoiseModelFromNoiseProperties,
NoiseProperties,
OpIdentifier,
SuperconductingQubitsNoiseProperties,
SymmetricalQidPair,
UNCONSTRAINED_DEVICE,
NamedTopology,
Expand Down
4 changes: 4 additions & 0 deletions cirq-core/cirq/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
NoiseProperties,
)

from cirq.devices.superconducting_qubits_noise_properties import (
SuperconductingQubitsNoiseProperties,
)

from cirq.devices.noise_utils import (
OpIdentifier,
decay_constant_to_xeb_fidelity,
Expand Down
191 changes: 191 additions & 0 deletions cirq-core/cirq/devices/superconducting_qubits_noise_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# 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.

95-martin-orion marked this conversation as resolved.
Show resolved Hide resolved

"""Class for representing noise on a superconducting qubit device."""

import abc
from dataclasses import dataclass, field
from typing import Dict, TYPE_CHECKING, List, Set, Type

from cirq import _compat, ops, devices
from cirq.devices import noise_utils

if TYPE_CHECKING:
import cirq


# TODO: missing per-device defaults
# Type-ignored because mypy cannot handle abstract dataclasses:
# https://github.com/python/mypy/issues/5374
@dataclass # type: ignore
class SuperconductingQubitsNoiseProperties(devices.NoiseProperties, abc.ABC):
"""Noise-defining properties for a superconducting-qubit-based device.

Args:
gate_times_ns: Dict[type, float] of gate types to their duration on
quantum hardware.
t1_ns: Dict[cirq.Qid, float] of qubits to their T_1 time, in ns.
tphi_ns: Dict[cirq.Qid, float] of qubits to their T_phi time, in ns.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we standardizing on on t_phi isn't t2 a little more common and it still gives you enough info to compute t_phi ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkafri can you comment on this? It's copied pretty directly from the internal version of this behavior.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have T1 then indeed T2 and Tphi can be determined from each other. I used Tphi because in the actual implementation the two terms contributing to the master equation are proportional to (1/ T1) and (1/Tphi). Presumably T2 is quoted more often because it is measured directly, whereas Tphi is usually inferred from T1 and T2 data. So in terms of the user-facing code, I suppose it makes more sense to use T2 and then convert to Tphi in the underlying implementation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, here is another point. Suppose I want to include only T1 decay and no white noise dephasing. In this case I would still need to specify a dictionary of T2 even though I know it must be equal to (2 T1). With the above implementation, I could just specify the T1 dictionary and have the Tphi dictionary remain empty.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just introduce a default for T2 of 2 * T1 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll convert to T2 if that's the consensus here, but I don't know the T2-Tphi relation offhand - how do we translate between the two?

Also consider that the QCS calibration-to-noise tools will also need to be modified to take this change into account, and (on a more minor note) that setting a default for T2 forces it to the end of the arguments list.

readout_errors: Dict[cirq.Qid, np.ndarray] of qubits to their readout
errors in matrix form: [P(read |1> from |0>), P(read |0> from |1>)].
gate_pauli_errors: dict of noise_utils.OpIdentifiers (a gate and the qubits it
targets) to the Pauli error for that operation. Keys in this dict
must have defined qubits.
validate: If True, verifies that t1 and tphi qubits sets match, and
that all symmetric two-qubit gates have errors which are
symmetric over the qubits they affect. Defaults to True.
"""

gate_times_ns: Dict[type, float]
95-martin-orion marked this conversation as resolved.
Show resolved Hide resolved
t1_ns: Dict['cirq.Qid', float]
tphi_ns: Dict['cirq.Qid', float]
readout_errors: Dict['cirq.Qid', List[float]]
gate_pauli_errors: Dict[noise_utils.OpIdentifier, float]

validate: bool = True
_qubits: List['cirq.Qid'] = field(init=False, default_factory=list)

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.')
95-martin-orion marked this conversation as resolved.
Show resolved Hide resolved

# 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.'
)
95-martin-orion marked this conversation as resolved.
Show resolved Hide resolved
elif op_id.gate_type in self.symmetric_two_qubit_gates():
op_id_swapped = noise_utils.OpIdentifier(op_id.gate_type, *op_id.qubits[::-1])
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.'
)
elif op_id.gate_type not in self.asymmetric_two_qubit_gates():
# Asymmetric gates do not require validation.
raise ValueError(
f'Found gate {op_id.gate_type} which does not appear in the '
'symmetric or asymmetric gate sets.'
)

@property
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

@classmethod
@abc.abstractmethod
def single_qubit_gates(cls) -> Set[Type[ops.Gate]]:
"""Returns the set of single-qubit gates this class supports."""

@classmethod
@abc.abstractmethod
def symmetric_two_qubit_gates(cls) -> Set[Type[ops.Gate]]:
"""Returns the set of symmetric two-qubit gates this class supports."""

@classmethod
@abc.abstractmethod
def asymmetric_two_qubit_gates(cls) -> Set[Type[ops.Gate]]:
"""Returns the set of asymmetric two-qubit gates this class supports."""

@classmethod
def two_qubit_gates(cls) -> Set[Type[ops.Gate]]:
"""Returns the set of all two-qubit gates this class supports."""
return cls.symmetric_two_qubit_gates() | cls.asymmetric_two_qubit_gates()

@classmethod
def expected_gates(cls) -> Set[Type[ops.Gate]]:
"""Returns the set of all gates this class supports."""
return cls.single_qubit_gates() | cls.two_qubit_gates()

def _get_pauli_error(self, p_error: float, op_id: noise_utils.OpIdentifier):
time_ns = float(self.gate_times_ns[op_id.gate_type])
for q in op_id.qubits:
p_error -= noise_utils.decoherence_pauli_error(self.t1_ns[q], self.tphi_ns[q], time_ns)
return p_error

@_compat.cached_property
def _depolarizing_error(self) -> Dict[noise_utils.OpIdentifier, float]:
"""Returns the portion of Pauli error from depolarization."""
depol_errors = {}
for op_id, p_error in self.gate_pauli_errors.items():
gate_type = op_id.gate_type
if issubclass(gate_type, ops.MeasurementGate):
# Non-measurement error can be ignored on measurement gates.
continue
expected_qubits = 1 if gate_type in self.single_qubit_gates() else 2
if len(op_id.qubits) != expected_qubits:
raise ValueError(
f'Gate {gate_type} takes {expected_qubits} qubit(s), '
f'but {op_id.qubits} were given.'
)
depol_errors[op_id] = self._get_pauli_error(p_error, op_id)
return depol_errors

def build_noise_models(self) -> List['cirq.NoiseModel']:
noise_models: List['cirq.NoiseModel'] = []

if self.t1_ns:
noise_models.append(
devices.ThermalNoiseModel(
set(self.t1_ns.keys()),
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()},
)
)

depolarizing_error = self._depolarizing_error
added_pauli_errors = {
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.readout_errors:
added_measure_errors: Dict[noise_utils.OpIdentifier, 'cirq.Operation'] = {}
for qubit in self.readout_errors:
p_00, p_11 = self.readout_errors[qubit]
p = p_11 / (p_00 + p_11)
gamma = p_11 / p
added_measure_errors[
noise_utils.OpIdentifier(ops.MeasurementGate, qubit)
] = ops.generalized_amplitude_damp(p, gamma).on(qubit)

noise_models.append(
devices.InsertionNoiseModel(ops_added=added_measure_errors, prepend=True)
)

return noise_models
Loading