"""Define quantum channels."""
from itertools import product
from math import exp, sqrt
from typing import Optional, Tuple
import numpy as np
from qibo.backends import _check_backend
from qibo.config import PRECISION_TOL, raise_error
from qibo.gates.abstract import Gate
from qibo.gates.gates import I, Unitary, X, Y, Z
from qibo.gates.special import FusedGate
class Channel(Gate):
"""Abstract class for channels."""
def __init__(self):
super().__init__()
self.coefficients = tuple()
self.gates = tuple()
def controlled_by(self, *q):
raise_error(ValueError, f"Noise channel cannot be controlled on qubits {q}.")
def on_qubits(self, qubit_map): # pragma: no cover
# future TODO
raise_error(
NotImplementedError,
"`on_qubits` method is not available for the `Channel` gate.",
)
def apply(self, backend, state, nqubits): # pragma: no cover
raise_error(
NotImplementedError,
f"{self.__class__.__name__} cannot be applied to state vector.",
)
def apply_density_matrix(self, backend, state, nqubits):
return backend.apply_channel_density_matrix(self, state, nqubits)
def apply_clifford(self, backend, state, nqubits):
return backend.apply_channel(self, state, nqubits)
def to_choi(self, nqubits: Optional[int] = None, order: str = "row", backend=None):
"""Returns the Choi representation :math:`\\mathcal{E}`
of the Kraus channel :math:`\\{K_{\\alpha}\\}_{\\alpha}`.
.. math::
\\mathcal{E} = \\sum_{\\alpha} \\, |K_{\\alpha})(K_{\\alpha}| \\, ,
where :math:`|K_{\\alpha})` is the vectorization of the Kraus operator
:math:`K_{\\alpha}`.
For a definition of vectorization, see :func:`qibo.quantum_info.vectorization`.
Args:
nqubits (int, optional): total number of qubits to be considered
in a channel. Must be equal or greater than ``target_qubits``.
If ``None``, defaults to the number of target qubits in the
channel. Default is ``None``.
order (str, optional): if ``"row"``, vectorization of
Kraus operators is performed row-wise. If ``"column"``,
vectorization is done column-wise. If ``"system"``,
vectorization is done block-wise. Defaut is ``"row"``.
backend (:class:`qibo.backends.abstract.Backend`, optional):
backend to be used in the execution. If ``None``,
it uses :class:`qibo.backends.GlobalBackend`.
Defaults to ``None``.
Returns:
Choi representation of the channel.
"""
if nqubits is not None and nqubits < 1 + max(self.target_qubits):
raise_error(
ValueError,
f"nqubits={nqubits}, but channel acts on qubit "
+ f"with index {max(self.target_qubits)}.",
)
from qibo.quantum_info.superoperator_transformations import ( # pylint: disable=C0415
vectorization,
)
backend = _check_backend(backend)
nqubits = 1 + max(self.target_qubits) if nqubits is None else nqubits
if type(self) not in [KrausChannel, ReadoutErrorChannel]:
p_0 = 1 - sum(self.coefficients)
if p_0 > PRECISION_TOL:
self.coefficients += (p_0,)
self.gates += (I(*self.target_qubits),)
super_op = np.zeros((4**nqubits, 4**nqubits), dtype=complex)
super_op = backend.cast(super_op, dtype=super_op.dtype)
for coeff, gate in zip(self.coefficients, self.gates):
kraus_op = FusedGate(*range(nqubits))
kraus_op.append(gate)
kraus_op = kraus_op.matrix(backend)
kraus_op = vectorization(kraus_op, order=order, backend=backend)
super_op = super_op + coeff * backend.np.outer(
kraus_op, backend.np.conj(kraus_op)
)
del kraus_op
return super_op
def to_liouville(self, nqubits: int = None, order: str = "row", backend=None):
"""Returns the Liouville representation of the channel.
Args:
nqubits (int, optional): total number of qubits to be considered
in a channel. Must be equal or greater than ``target_qubits``.
If ``None``, defaults to the number of target qubits in the
channel. Default is ``None``.
order (str, optional): If ``"row"``, vectorization of
Kraus operators is performed row-wise. If ``"column"``,
vectorization is done column-wise. If ``"system"``,
it raises ``NotImplementedError``. Defaut is ``"row"``.
backend (:class:`qibo.backends.abstract.Backend`, optional):
backend to be used in the execution. If ``None``,
it uses :class:`qibo.backends.GlobalBackend`.
Defaults to ``None``.
Returns:
Liouville representation of the channel.
"""
from qibo.quantum_info.superoperator_transformations import ( # pylint: disable=C0415
choi_to_liouville,
)
backend = _check_backend(backend)
super_op = self.to_choi(nqubits=nqubits, order=order, backend=backend)
super_op = choi_to_liouville(super_op, order=order, backend=backend)
return super_op
def to_pauli_liouville(
self,
nqubits: int = None,
normalize: bool = False,
pauli_order: str = "IXYZ",
backend=None,
):
"""Returns the Liouville representation of the channel
in the Pauli basis.
Args:
nqubits (int, optional): total number of qubits to be considered
in a channel. Must be equal or greater than ``target_qubits``.
If ``None``, defaults to the number of target qubits in the
channel. Default is ``None``.
normalize (bool, optional): If ``True``, normalized basis is returned.
Defaults to False.
pauli_order (str, optional): corresponds to the order of 4 single-qubit
Pauli elements in the basis. Default is "IXYZ".
backend (:class:`qibo.backends.abstract.Backend`, optional): backend
to be used in the execution. If ``None``, it uses
:class:`qibo.backends.GlobalBackend`.
Defaults to ``None``.
Returns:
Pauli-Liouville representation of the channel.
"""
from qibo.quantum_info.basis import comp_basis_to_pauli # pylint: disable=C0415
backend = _check_backend(backend)
super_op = self.to_liouville(nqubits=nqubits, backend=backend)
if nqubits is None:
nqubits = int(np.log2(np.sqrt(super_op.shape[0])))
# unitary that transforms from comp basis to pauli basis
unitary = comp_basis_to_pauli(
nqubits, normalize, pauli_order=pauli_order, backend=backend
)
super_op = (
unitary @ super_op @ backend.np.transpose(backend.np.conj(unitary), (1, 0))
)
return super_op
def matrix(self, backend=None):
""""""
raise_error(
NotImplementedError,
"`matrix` method not defined for Channels. "
+ "Please use one of the following methods: "
+ "`to_choi` or `to_liouville` or `to_pauli_liouville`.",
)
[docs]class KrausChannel(Channel):
"""General channel defined by arbitrary Kraus operators.
Implements the following transformation:
.. math::
\\mathcal{E}(\\rho ) = \\sum _k A_k \\rho A_k^\\dagger
where A are arbitrary Kraus operators given by the user. Note that Kraus
operators set should be trace preserving, however this is not checked.
Simulation of this gate requires the use of density matrices.
For more information on channels and Kraus operators please check
`J. Preskill's notes <http://theory.caltech.edu/~preskill/ph219/chap3_15.pdf>`_.
Args:
qubits (int or list or tuple): Qubits that the Kraus operators act on.
Type ``int`` and ``tuple`` will be considered as the same qubit ids for
all operators. A ``list`` should contain tuples of qubits corresponding
to each operator. Can be ``[]`` if ``operators`` are of type :class:`qibo.gates.Gate`,
otherwise adds given gates on specified qubits.
operators (list): List of Kraus operators ``Ak`` as matrices of type
``ndarray | tf.Tensor`` or gates :class:`qibo.gates.Gate`.
Example:
.. testcode::
import numpy as np
from qibo import Circuit, gates
# initialize circuit with 3 qubits
circuit = Circuit(3, density_matrix=True)
# define a sqrt(0.4) * X gate
a_1 = np.sqrt(0.4) * np.array([[0, 1], [1, 0]])
# define a sqrt(0.6) * CNOT gate
a_2 = np.sqrt(0.6) * np.array([[1, 0, 0, 0], [0, 1, 0, 0],
[0, 0, 0, 1], [0, 0, 1, 0]])
# define the channel rho -> 0.4 X{1} rho X{1} + 0.6 CNOT{0, 2} rho CNOT{0, 2}
channel_1 = gates.KrausChannel([(1,), (0, 2)], [a_1, a_2])
# add channel to the circuit
circuit.add(channel_1)
# define the same channel using qibo.gates.Unitary
a_1 = gates.Unitary(a_1, 1)
a_2 = gates.Unitary(a_2, 0, 2)
channel_2 = gates.KrausChannel([], [a_1, a_2])
# add channel to the circuit
circuit.add(channel_2)
# define the channel rho -> 0.4 X{0} rho X{0} + 0.6 CNOT{1, 2} rho CNOT{1, 2}
channel_3 = gates.KrausChannel([(0,), (1, 2)], [a_1, a_2])
# add channel to the circuit
circuit.add(channel_3)
"""
def __init__(self, qubits, operators):
super().__init__()
self.name = "KrausChannel"
self.draw_label = "K"
# Check qubits type
if isinstance(qubits, int):
qubits = [(qubits,)] * len(operators)
elif isinstance(qubits, tuple):
qubits = [qubits] * len(operators)
elif not isinstance(qubits, list):
raise_error(
TypeError,
"``qubits`` must be of type int or tuple or int. "
+ f"Got {type(qubits)} instead",
)
elif not all(isinstance(q, (tuple)) for q in qubits):
raise_error(TypeError, "All elements of ``qubits`` list must be tuples.")
if isinstance(operators[0], Gate):
if qubits:
operators = [
operators[k].on_qubits(
{
operators[k].qubits[i]: qubits[k][i]
for i in range(len(operators[k].qubits))
}
)
for k in range(len(operators))
]
self.gates = tuple(operators)
self.target_qubits = tuple(
sorted({q for gate in operators for q in gate.target_qubits})
)
unitary_check = [gate.unitary for gate in self.gates]
elif len(qubits) != len(operators):
raise_error(
ValueError,
f"``qubits`` list has length {len(qubits)} while "
+ f"{len(operators)} operators were given.",
)
else:
gates, qubitset, unitary_check = [], set(), []
for qubit_tuple, matrix in zip(qubits, operators):
rank = 2 ** len(qubit_tuple)
shape = tuple(matrix.shape)
if shape != (rank, rank):
raise_error(
ValueError,
f"Invalid Kraus operator shape {shape} for "
+ f"acting on {len(qubit_tuple)} qubits.",
)
gate = Unitary(matrix, *list(qubit_tuple))
qubitset.update(qubit_tuple)
gates.append(gate)
unitary_check.append(gate.unitary)
self.gates = tuple(gates)
self.target_qubits = tuple(sorted(qubitset))
self.init_args = [self.gates]
self.coefficients = len(self.gates) * (1,)
self.coefficient_sum = 1
self._all_unitary_operators = True if all(unitary_check) else False
[docs]class UnitaryChannel(KrausChannel):
"""Channel that is a probabilistic sum of unitary operations.
Implements the following transformation:
.. math::
\\mathcal{E}(\\rho ) = \\left (1 - \\sum _k p_k \\right )\\rho +
\\sum _k p_k U_k \\rho U_k^\\dagger
where U are arbitrary unitary operators and p are floats between 0 and 1.
Note that unlike :class:`qibo.gates.KrausChannel` which requires
density matrices, it is possible to simulate the unitary channel using
state vectors and probabilistic sampling. For more information on this
approach we refer to :ref:`Using repeated execution <repeatedexec-example>`.
Args:
qubits (int or list or tuple): Qubits that the unitary operators
act on. Types ``int`` and ``tuple`` will be considered as the same
qubit(s) for all unitaries. A ``list`` should contain tuples of
qubits corresponding to each operator. Can be ``[]`` if ``operators`` are of type
:class:`qibo.gates.Gate`, otherwise adds given gates on specified qubits.
operators (list): List of operators as pairs ``(pk, Uk)`` where
``pk`` is float probability corresponding to a unitary ``Uk``
of type ``ndarray``/``tf.Tensor`` or gates :class:`qibo.gates.Gate`.
"""
def __init__(self, qubits, operators):
if not all(isinstance(pair, (tuple)) for pair in operators):
raise_error(
TypeError, "``operators`` must be a list of tuples ``(pk, Uk)``."
)
probabilities = [pair[0] for pair in operators]
operators = [pair[1] for pair in operators]
if any((p < 0 or p > 1) for p in probabilities):
raise_error(
ValueError,
"Probabilities should be between 0 and 1.",
)
super().__init__(qubits, operators)
self.name = "UnitaryChannel"
self.draw_label = "U"
self.coefficients = tuple(probabilities)
self.coefficient_sum = sum(probabilities)
if self.coefficient_sum > 1 + PRECISION_TOL or self.coefficient_sum < 0:
raise_error(
ValueError,
"UnitaryChannel probability sum should be "
+ f"between 0 and 1 but is {self.coefficient_sum}.",
)
self.init_args = [probabilities, self.gates]
def apply(self, backend, state, nqubits):
return backend.apply_channel(self, state, nqubits)
[docs]class PauliNoiseChannel(UnitaryChannel):
"""Multi-qubit noise channel that applies Pauli operators with given probabilities.
Implements the following transformation:
.. math::
\\mathcal{E}(\\rho ) = \\left (1 - \\sum _{k} p_{k} \\right ) \\, \\rho +
\\sum_{k} \\, p_{k} \\, P_{k} \\, \\rho \\, P_{k}
where :math:`P_{k}` is the :math:`k`-th Pauli ``string`` and :math:`p_{k}` is
the probability associated to :math:`P_{k}`.
Example:
.. testcode::
from itertools import product
import numpy as np
from qibo.gates.channels import PauliNoiseChannel
qubits = (0, 2)
nqubits = len(qubits)
# excluding the Identity operator
paulis = list(product(["I", "X"], repeat=nqubits))[1:]
# this next line is optional
paulis = [''.join(pauli) for pauli in paulis]
probabilities = np.random.rand(len(paulis) + 1)
probabilities /= np.sum(probabilities)
#Excluding probability of Identity operator
probabilities = probabilities[1:]
channel = PauliNoiseChannel(
qubits, list(zip(paulis, probabilities))
)
This channel can be simulated using either density matrices or state vectors
and sampling with repeated execution.
See :ref:`How to perform noisy simulation? <noisy-example>` for more
information.
Args:
qubits (int or list or tuple): Qubits that the noise acts on.
operators (list): list of operators as pairs :math:`(P_{k}, p_{k})`.
"""
def __init__(self, qubits: Tuple[int, list, tuple], operators: list):
if isinstance(qubits, int):
qubits = (qubits,)
probabilities, paulis = [], []
for pauli, probability in operators:
probabilities.append(probability)
paulis.append(pauli)
single_paulis = {"I": I, "X": X, "Y": Y, "Z": Z}
gates = []
for pauli in paulis:
fgate = FusedGate(*qubits)
for qubit, pauli_single in zip(qubits, pauli):
fgate.append(single_paulis[pauli_single](qubit))
gates.append(fgate)
self.gates = tuple(gates)
self.coefficients = tuple(probabilities)
super().__init__([], list(zip(probabilities, gates)))
self.name = "PauliNoiseChannel"
self.draw_label = "PN"
self.init_args = qubits
self.init_kwargs = dict(operators)
[docs]class DepolarizingChannel(PauliNoiseChannel):
""":math:`n`-qubit Depolarizing quantum error channel,
.. math::
\\mathcal{E}(\\rho ) = (1 - \\lambda) \\rho +
\\lambda \\text{Tr}_q[\\rho]\\otimes \\frac{I}{2^n}
where :math:`\\lambda` is the depolarizing error parameter
and :math:`0 \\le \\lambda \\le 4^n / (4^n - 1)`.
* If :math:`\\lambda = 1` this is a completely depolarizing channel
:math:`E(\\rho) = I / 2^n`
* If :math:`\\lambda = 4^n / (4^n - 1)` this is a uniform Pauli
error channel: :math:`E(\\rho) = \\sum_j P_j \\rho P_j / (4^n - 1)` for
all :math:`P_j \\neq I`.
Args:
qubits (int or list or tuple): Qubit ids that the noise acts on.
lam (float): Depolarizing error parameter.
"""
def __init__(self, qubits, lam: float):
if isinstance(qubits, int):
qubits = (qubits,)
num_qubits = len(qubits)
num_terms = 4**num_qubits
max_param = num_terms / (num_terms - 1)
if lam < 0 or lam > max_param:
raise_error(
ValueError,
f"Depolarizing parameter must be in between 0 and {max_param}.",
)
pauli_noise_params = list(product(["I", "X", "Y", "Z"], repeat=num_qubits))[1::]
pauli_noise_params = zip(
pauli_noise_params, [lam / num_terms] * (num_terms - 1)
)
super().__init__(qubits, pauli_noise_params)
self.name = "DepolarizingChannel"
self.draw_label = "D"
self.target_qubits = qubits
self.init_args = [qubits]
self.init_kwargs = {"lam": lam}
def apply_density_matrix(self, backend, state, nqubits):
return backend.depolarizing_error_density_matrix(self, state, nqubits)
[docs]class ThermalRelaxationChannel(KrausChannel):
"""Single-qubit thermal relaxation error channel.
Implements the following transformation:
If :math:`T_1 \\geq T_2`:
.. math::
\\mathcal{E} (\\rho ) = (1 - p_z - p_0 - p_1) \\rho + p_z \\, Z\\rho Z
+ \\mathrm{Tr}_{q}[\\rho] \\otimes (p_0 | 0\\rangle \\langle 0|
+ p_1|1\\rangle \\langle 1|)
while if :math:`T_1 < T_2`:
.. math::
\\mathcal{E}(\\rho ) = \\mathrm{Tr}_\\mathcal{X}
\\left[\\Lambda_{\\mathcal{X}\\mathcal{Y}} (\\rho_\\mathcal{X}^T
\\otimes I_{\\mathcal{Y}}) \\right]
with
.. math::
\\Lambda = \\begin{pmatrix}
1 - p_1 & 0 & 0 & e^{-t / T_2} \\\\
0 & p_1 & 0 & 0 \\\\
0 & 0 & p_0 & 0 \\\\
e^{-t / T_2} & 0 & 0 & 1 - p_0
\\end{pmatrix}
where :math:`p_0 = (1 - e^{-t / T_1})(1 - \\eta )`,
:math:`p_1 = (1 - e^{-t / T_1})\\eta`, and
:math:`p_z = (e^{-t / T_1} - e^{-t / T_2})/2`.
Here :math:`\\eta` is the ``excited_population``
and :math:`t` is the ``time``, both controlled by the user.
This gate is based on `Qiskit's thermal relaxation error channel
<https://qiskit.org/documentation/stubs/qiskit.providers.aer.noise.thermal_relaxation_error.html#qiskit.providers.aer.noise.thermal_relaxation_error>`_.
Args:
qubit (int): Qubit id that the noise channel acts on.
parameters (list): list of 3 or 4 parameters
(t_1, t_2, time, excited_population=0), where
t_1 (float): T1 relaxation time. Should satisfy ``t_1 > 0``.
t_2 (float): T2 dephasing time.
Should satisfy ``t_1 > 0`` and ``t_2 < 2 * t_1``.
time (float): the gate time for relaxation error.
excited_population (float): the population of the excited state at
equilibrium. Default is 0.
"""
def __init__(self, qubit: int, parameters: list):
if len(parameters) not in [3, 4]:
raise_error(
ValueError,
"``parameters`` list must have 3 or 4 elements "
+ f"while {len(parameters)} were given.",
)
t_1, t_2, time = parameters[:3]
excited_population = parameters[-1] if len(parameters) == 4 else 0.0
if excited_population < 0 or excited_population > 1:
raise_error(
ValueError, f"Invalid excited state population {excited_population}."
)
if time < 0.0:
raise_error(ValueError, f"Invalid gate time: {time} < 0.")
if t_1 <= 0.0:
raise_error(
ValueError, f"Invalid t_1 relaxation time parameter: {t_1} <= 0.0."
)
if t_2 <= 0.0:
raise_error(
ValueError, f"Invalid t_2 relaxation time parameter: {t_2} <= 0.0."
)
if t_2 > 2 * t_1:
raise_error(
ValueError,
"Invalid t_2 relaxation time parameter: t_2 > 2 * t_1.",
)
# calculate probabilities
self.t_1, self.t_2 = t_1, t_2
p_reset = 1 - exp(-time / t_1)
p_0 = p_reset * (1 - excited_population)
p_1 = p_reset * excited_population
if t_1 < t_2:
e_t2 = exp(-time / t_2)
operators = [
sqrt(p_0) * sqrt(p_0) * np.array([[0, 1], [0, 0]]),
sqrt(p_1) * np.array([[0, 0], [1, 0]]),
]
k_term = sqrt(4 * e_t2**2 + (p_0 - p_1) ** 2)
kraus_coeff = sqrt(1 - (p_0 + p_1 + k_term) / 2)
operators.append(
kraus_coeff
* np.array(
[
[(p_0 - p_1 - k_term) / (2 * e_t2), 0],
[0, 1],
]
)
)
operators.append(
kraus_coeff
* np.array(
[
[(p_0 - p_1 + k_term) / (2 * e_t2), 0],
[0, 1],
]
)
)
super().__init__([(qubit,)] * len(operators), operators)
self.init_kwargs["e_t2"] = e_t2
else:
p_z = (exp(-time / t_1) - exp(-time / t_2)) / 2
operators = (
sqrt(p_0) * np.array([[1, 0], [0, 0]]),
sqrt(p_0) * np.array([[0, 1], [0, 0]]),
sqrt(p_1) * np.array([[0, 0], [1, 0]]),
sqrt(p_1) * np.array([[0, 0], [0, 1]]),
sqrt(p_z) * np.array([[1, 0], [0, -1]]),
sqrt(1 - p_0 - p_1 - p_z) * np.eye(2),
)
super().__init__([(qubit,)] * len(operators), operators)
self.init_kwargs["p_z"] = p_z
self.init_args = [qubit, t_1, t_2, time]
self.t_1, self.t_2 = t_1, t_2
self.init_kwargs["excited_population"] = excited_population
self.init_kwargs["p_0"] = p_0
self.init_kwargs["p_1"] = p_1
self.name = "ThermalRelaxationChannel"
self.draw_label = "TR"
def apply_density_matrix(self, backend, state, nqubits):
qubit = self.target_qubits[0]
if self.t_1 < self.t_2:
preset0, preset1, e_t2 = (
self.init_kwargs["p_0"],
self.init_kwargs["p_1"],
self.init_kwargs["e_t2"],
)
matrix = [
[1 - preset1, 0, 0, preset0],
[0, e_t2, 0, 0],
[0, 0, e_t2, 0],
[preset1, 0, 0, 1 - preset0],
]
qubits = (qubit, qubit + nqubits)
gate = Unitary(matrix, *qubits)
return backend.thermal_error_density_matrix(gate, state, nqubits)
p_z = self.init_kwargs["p_z"]
return (
backend.reset_error_density_matrix(self, state, nqubits)
- p_z * backend.cast(state)
+ p_z * backend.apply_gate_density_matrix(Z(0), state, nqubits)
)
[docs]class AmplitudeDampingChannel(KrausChannel):
"""Single-qubit amplitude damping channel in its Kraus representation, i.e.
.. math::
K_{0} = \\begin{pmatrix}
1 & 0 \\\\
0 & \\sqrt{1 - \\gamma} \\\\
\\end{pmatrix} \\,\\, , \\,\\,
K_{1} = \\begin{pmatrix}
0 & \\sqrt{\\gamma} \\\\
0 & 0 \\\\
\\end{pmatrix}
Args:
qubit (int): Qubit id that the noise channel acts on.
gamma (float): amplitude damping strength.
"""
def __init__(self, qubit, gamma: float):
if not isinstance(gamma, float):
raise_error(
TypeError, f"gamma must be type float, but it is type {type(gamma)}."
)
if gamma < 0.0 or gamma > 1.0:
raise_error(ValueError, "gamma must be a float between 0 and 1.")
operators = []
operators.append(np.array([[1, 0], [0, sqrt(1 - gamma)]], dtype=complex))
operators.append(np.array([[0, sqrt(gamma)], [0, 0]], dtype=complex))
super().__init__([(qubit,)] * len(operators), operators)
self.name = "AmplitudeDampingChannel"
self.draw_label = "AD"
[docs]class PhaseDampingChannel(KrausChannel):
"""Single-qubit phase damping channel in its Kraus representation, i.e.
.. math::
K_{0} = \\begin{pmatrix}
1 & 0 \\\\
0 & \\sqrt{1 - \\gamma} \\\\
\\end{pmatrix} \\,\\, , \\,\\,
K_{1} = \\begin{pmatrix}
0 & 0 \\\\
0 & \\sqrt{\\gamma} \\\\
\\end{pmatrix}
Args:
qubit (int): Qubit id that the noise channel acts on.
gamma (float): phase damping strength.
"""
def __init__(self, qubit, gamma: float):
if not isinstance(gamma, float):
raise_error(
TypeError, f"gamma must be type float, but it is type {type(gamma)}."
)
if gamma < 0.0 or gamma > 1.0:
raise_error(ValueError, "gamma must be a float between 0 and 1.")
operators = []
operators.append(np.array([[1, 0], [0, sqrt(1 - gamma)]], dtype=complex))
operators.append(np.array([[0, 0], [0, sqrt(gamma)]], dtype=complex))
super().__init__([(qubit,)] * len(operators), operators)
self.name = "PhaseDampingChannel"
self.draw_label = "PD"
[docs]class ReadoutErrorChannel(KrausChannel):
"""Readout error channel implemented as a quantum-to-classical channel.
Args:
qubits (int or list or tuple): Qubit ids that the channel acts on.
probabilities (array): row-stochastic matrix :math:`P` with all
readout transition probabilities.
Example:
For 1 qubit, the transition matrix :math:`P` would be
.. math::
P = \\begin{pmatrix}
p(0 \\, | \\, 0) & p(1 \\, | \\, 0) \\\\
p(0 \\, | \\, 1) & p(1 \\, | \\, 1)
\\end{pmatrix} \\, .
"""
def __init__(self, qubits: Tuple[int, list, tuple], probabilities):
if any(sum(row) < 1 - PRECISION_TOL for row in probabilities) or any(
sum(row) > 1 + PRECISION_TOL for row in probabilities
):
raise_error(ValueError, "all rows of probabilities must sum to 1.")
if isinstance(qubits, int):
qubits = (qubits,)
if isinstance(probabilities, list):
probabilities = np.array(probabilities)
dim = len(probabilities)
operators = []
for j in range(dim):
for k in range(dim):
operator = np.zeros((dim, dim))
operator[j, k] = sqrt(probabilities[k, j])
operators.append(operator)
super().__init__([qubits] * len(operators), operators)
self.name = "ReadoutErrorChannel"
self.draw_label = "RE"
[docs]class ResetChannel(KrausChannel):
"""Single-qubit reset channel.
Implements the following transformation:
.. math::
\\mathcal{E}(\\rho ) = (1 - p_{0} - p_{1}) \\rho
+ \\mathrm{Tr}_{q}[\\rho] \\otimes (p_{0} \\, |0\\rangle \\langle 0|
+ p_{1} \\, |1\\rangle \\langle 1|),
Args:
qubit (int): qubit id that the channel acts on.
probabilities (list or ndarray): list :math:`[p_{0}, p_{1}]`,
where :math:`p_{0}` and :math:`p_{1}` are the probabilities to
reset to 0 and 1, respectively.
"""
def __init__(self, qubit, probabilities):
if len(probabilities) != 2:
raise_error(
ValueError,
f"ResetChannel needs 2 probabilities, got {len(probabilities)} instead.",
)
p_0, p_1 = probabilities
if p_0 < 0:
raise_error(ValueError, "Invalid p_0 ({p_0} < 0).")
if p_1 < 0:
raise_error(ValueError, "Invalid p_1 ({p_1} < 0).")
if p_0 + p_1 > 1 + PRECISION_TOL:
raise_error(
ValueError, f"Invalid probabilities (p_0 + p_1 = {p_0 + p_1} > 1)."
)
operators = [
sqrt(p_0) * np.array([[1, 0], [0, 0]]),
sqrt(p_0) * np.array([[0, 1], [0, 0]]),
sqrt(p_1) * np.array([[0, 0], [1, 0]]),
sqrt(p_1) * np.array([[0, 0], [0, 1]]),
]
if p_0 + p_1 < 1:
operators.append(sqrt(np.abs(1 - p_0 - p_1)) * np.eye(2))
super().__init__([(qubit,)] * len(operators), operators)
self.init_kwargs = {"p_0": p_0, "p_1": p_1}
self.name = "ResetChannel"
self.draw_label = "R"
def apply_density_matrix(self, backend, state, nqubits):
return backend.reset_error_density_matrix(self, state, nqubits)