Skip to content

Commit

Permalink
Utilities for approximate hardware noise (quantumlib#4671)
Browse files Browse the repository at this point in the history
* Remove files for subsequent PRs

* Add coverage for OpId

* Clarify if-case

* snake_case_gamma

Co-authored-by: Cirq Bot <craiggidney+github+cirqbot@google.com>
  • Loading branch information
2 people authored and rht committed May 1, 2023
1 parent b49d759 commit 14164cc
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 0 deletions.
1 change: 1 addition & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
NO_NOISE,
NOISE_MODEL_LIKE,
NoiseModel,
OpIdentifier,
SymmetricalQidPair,
UNCONSTRAINED_DEVICE,
NamedTopology,
Expand Down
12 changes: 12 additions & 0 deletions cirq-core/cirq/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,15 @@
get_placements,
draw_placements,
)

from cirq.devices.noise_utils import (
OpIdentifier,
decay_constant_to_xeb_fidelity,
decay_constant_to_pauli_error,
pauli_error_to_decay_constant,
xeb_fidelity_to_decay_constant,
pauli_error_from_t1,
pauli_error_from_depolarization,
average_error,
decoherence_pauli_error,
)
233 changes: 233 additions & 0 deletions cirq-core/cirq/devices/noise_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# Copyright 2021 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import TYPE_CHECKING, Any, Dict, Tuple, Type, Union
import warnings
import numpy as np

from cirq import ops, protocols, value

if TYPE_CHECKING:
import cirq


# Tag for gates to which noise must be applied.
PHYSICAL_GATE_TAG = 'physical_gate'


@value.value_equality(distinct_child_types=True)
class OpIdentifier:
"""Identifies an operation by gate and (optionally) target qubits."""

def __init__(self, gate_type: Type['cirq.Gate'], *qubits: 'cirq.Qid'):
self._gate_type = gate_type
self._gate_family = ops.GateFamily(gate_type)
self._qubits: Tuple['cirq.Qid', ...] = tuple(qubits)

@property
def gate_type(self) -> Type['cirq.Gate']:
# set to a type during initialization, never modified
return self._gate_type

@property
def qubits(self) -> Tuple['cirq.Qid', ...]:
return self._qubits

def _predicate(self, *args, **kwargs):
return self._gate_family._predicate(*args, **kwargs)

def swapped(self):
return OpIdentifier(self.gate_type, *self.qubits[::-1])

def is_proper_subtype_of(self, op_id: 'OpIdentifier'):
"""Returns true if this is contained within op_id, but not equal to it.
If this returns true, (x in self) implies (x in op_id), but the reverse
implication does not hold. op_id must be more general than self (either
by accepting any qubits or having a more general gate type) for this
to return true.
"""
more_specific_qubits = self.qubits and not op_id.qubits
more_specific_gate = self.gate_type != op_id.gate_type and issubclass(
self.gate_type, op_id.gate_type
)
if more_specific_qubits:
return more_specific_gate or self.gate_type == op_id.gate_type
elif more_specific_gate:
return more_specific_qubits or self.qubits == op_id.qubits
else:
return False

def __contains__(self, item: Union[ops.Gate, ops.Operation]) -> bool:
if isinstance(item, ops.Gate):
return (not self._qubits) and self._predicate(item)
return (
(not self.qubits or (item.qubits == self._qubits))
and item.gate is not None
and self._predicate(item.gate)
)

def __str__(self):
return f'{self.gate_type}{self.qubits}'

def __repr__(self) -> str:
fullname = f'{self.gate_type.__module__}.{self.gate_type.__qualname__}'
qubits = ', '.join(map(repr, self.qubits))
return f'cirq.devices.noise_utils.OpIdentifier({fullname}, {qubits})'

def _value_equality_values_(self) -> Any:
return (self.gate_type, self.qubits)

def _json_dict_(self) -> Dict[str, Any]:
gate_json = protocols.json_cirq_type(self._gate_type)
return {
'gate_type': gate_json,
'qubits': self._qubits,
}

@classmethod
def _from_json_dict_(cls, gate_type, qubits, **kwargs) -> 'OpIdentifier':
gate_type = protocols.cirq_type_from_json(gate_type)
return cls(gate_type, *qubits)


# TODO: expose all from top-level cirq?
def decay_constant_to_xeb_fidelity(decay_constant: float, num_qubits: int = 2) -> float:
"""Calculates the XEB fidelity from the depolarization decay constant.
Args:
decay_constant: Depolarization decay constant.
num_qubits: Number of qubits.
Returns:
Calculated XEB fidelity.
"""
N = 2 ** num_qubits
return 1 - ((1 - decay_constant) * (1 - 1 / N))


def decay_constant_to_pauli_error(decay_constant: float, num_qubits: int = 1) -> float:
"""Calculates pauli error from the depolarization decay constant.
Args:
decay_constant: Depolarization decay constant.
num_qubits: Number of qubits.
Returns:
Calculated Pauli error.
"""
N = 2 ** num_qubits
return (1 - decay_constant) * (1 - 1 / N / N)


def pauli_error_to_decay_constant(pauli_error: float, num_qubits: int = 1) -> float:
"""Calculates depolarization decay constant from pauli error.
Args:
pauli_error: The pauli error.
num_qubits: Number of qubits.
Returns:
Calculated depolarization decay constant.
"""
N = 2 ** num_qubits
return 1 - (pauli_error / (1 - 1 / N / N))


def xeb_fidelity_to_decay_constant(xeb_fidelity: float, num_qubits: int = 2) -> float:
"""Calculates the depolarization decay constant from XEB fidelity.
Args:
xeb_fidelity: The XEB fidelity.
num_qubits: Number of qubits.
Returns:
Calculated depolarization decay constant.
"""
N = 2 ** num_qubits
return 1 - (1 - xeb_fidelity) / (1 - 1 / N)


def pauli_error_from_t1(t_ns: float, t1_ns: float) -> float:
"""Calculates the pauli error from T1 decay constant.
This computes error for a specific duration, `t`.
Args:
t_ns: The duration of the gate in ns.
t1_ns: The T1 decay constant in ns.
Returns:
Calculated Pauli error resulting from T1 decay.
"""
t2 = 2 * t1_ns
return (1 - np.exp(-t_ns / t2)) / 2 + (1 - np.exp(-t_ns / t1_ns)) / 4


def pauli_error_from_depolarization(t_ns: float, t1_ns: float, pauli_error: float = 0) -> float:
"""Calculates the amount of pauli error from depolarization.
This computes non-T1 error for a specific duration, `t`. If pauli error
from T1 decay is more than total pauli error, this returns zero; otherwise,
it returns the portion of pauli error not attributable to T1 error.
Args:
t_ns: The duration of the gate in ns.
t1_ns: The T1 decay constant in ns.
pauli_error: The total pauli error.
Returns:
Calculated Pauli error resulting from depolarization.
"""
t1_pauli_error = pauli_error_from_t1(t_ns, t1_ns)
if pauli_error >= t1_pauli_error:
return pauli_error - t1_pauli_error

warnings.warn("Pauli error from T1 decay is greater than total Pauli error", RuntimeWarning)
return 0


def average_error(decay_constant: float, num_qubits: int = 1) -> float:
"""Calculates the average error from the depolarization decay constant.
Args:
decay_constant: Depolarization decay constant.
num_qubits: Number of qubits.
Returns:
Calculated average error.
"""
N = 2 ** num_qubits
return (1 - decay_constant) * (1 - 1 / N)


def decoherence_pauli_error(t1_ns: float, tphi_ns: float, gate_time_ns: float) -> float:
"""The component of Pauli error caused by decoherence.
Args:
t1_ns: T1 time in nanoseconds.
tphi_ns: Tphi time in nanoseconds.
gate_time_ns: Duration in nanoseconds of the gate affected by this error.
Returns:
Calculated Pauli error resulting from decoherence.
"""
gamma_2 = (1 / (2 * t1_ns)) + 1 / tphi_ns

exp1 = np.exp(-gate_time_ns / t1_ns)
exp2 = np.exp(-gate_time_ns * gamma_2)
px = 0.25 * (1 - exp1)
py = px
pz = 0.5 * (1 - exp2) - px
return px + py + pz
Loading

0 comments on commit 14164cc

Please sign in to comment.