"""Module defining Error channels and NoiseModel class(es)."""
import collections
from itertools import combinations
from math import log2
from typing import Optional, Union
from qibo import gates
from qibo.config import raise_error
[docs]class KrausError:
"""Quantum error associated with the :class:`qibo.gates.KrausChannel`.
Args:
ops (list): List of Kraus operators of type ``np.ndarray``
or ``tf.Tensor`` and of the same shape.
"""
def __init__(self, ops):
shape = ops[0].shape
if any(o.shape != shape for o in ops):
raise_error(
ValueError,
"Kraus operators of different shapes."
"Use qibo.noise.CustomError instead.",
)
self.rank = shape[0]
self.options = ops
[docs] def channel(self, qubits, options):
"""Returns the quantum channel associated to the quantum error."""
return [
gates.KrausChannel(q, options)
for q in combinations(qubits, int(log2(self.rank)))
]
[docs]class UnitaryError:
"""Quantum error associated with the :class:`qibo.gates.UnitaryChannel`.
Args:
probabilities (list): List of floats that correspond to the probability
that each unitary Uk is applied.
unitaries (list): List of unitary matrices as ``np.ndarray``/``tf.Tensor``
of the same shape. Must have the same length as the given
probabilities ``p``.
"""
def __init__(self, probabilities, unitaries):
shape = unitaries[0].shape
if any(o.shape != shape for o in unitaries):
raise_error(
ValueError,
"Unitary matrices have different shapes."
"Use qibo.noise.CustomError instead.",
)
self.rank = shape[0]
self.options = list(zip(probabilities, unitaries))
[docs] def channel(self, qubits, options):
"""Returns the quantum channel associated to the quantum error."""
return [
gates.UnitaryChannel(q, options)
for q in combinations(qubits, int(log2(self.rank)))
]
[docs]class PauliError:
"""Quantum error associated with the :class:`qibo.gates.PauliNoiseChannel`.
Args:
operators (list): see :class:`qibo.gates.PauliNoiseChannel`
"""
def __init__(self, operators):
self.options = operators
[docs] def channel(self, qubits, options):
"""Returns the quantum channel associated to the quantum error."""
return [gates.PauliNoiseChannel(q, options) for q in qubits]
[docs]class DepolarizingError:
"""Quantum error associated with the :class:`qibo.gates.DepolarizingChannel`.
Args:
options (float): see :class:`qibo.gates.DepolarizingChannel`
"""
def __init__(self, lam):
self.options = lam
self.channel = gates.DepolarizingChannel
[docs]class ThermalRelaxationError:
"""Quantum error associated with the :class:`qibo.gates.ThermalRelaxationChannel`.
Args:
options (tuple): see :class:`qibo.gates.ThermalRelaxationChannel`
"""
def __init__(self, t1, t2, time, excited_population=0):
self.options = [t1, t2, time, excited_population]
self.channel = gates.ThermalRelaxationChannel
[docs]class AmplitudeDampingError:
"""Quantum error associated with the :class:`qibo.gates.AmplitudeDampingChannel`.
Args:
options (float): see :class:`qibo.gates.AmplitudeDampingChannel`
"""
def __init__(self, gamma):
self.options = gamma
self.channel = gates.AmplitudeDampingChannel
[docs]class PhaseDampingError:
"""Quantum error associated with the :class:`qibo.gates.PhaseDampingChannel`.
Args:
options (float): see :class:`qibo.gates.PhaseDampingChannel`
"""
def __init__(self, gamma):
self.options = gamma
self.channel = gates.PhaseDampingChannel
[docs]class ReadoutError:
"""Quantum error associated with :class:'qibo.gates;ReadoutErrorChannel'.
Args:
options (array): see :class:'qibo.gates.ReadoutErrorChannel'
"""
def __init__(self, probabilities):
self.options = probabilities
self.channel = gates.ReadoutErrorChannel
[docs]class ResetError:
"""Quantum error associated with the `qibo.gates.ResetChannel`.
Args:
options (tuple): see :class:`qibo.gates.ResetChannel`
"""
def __init__(self, p0, p1):
self.options = [p0, p1]
self.channel = gates.ResetChannel
[docs]class CustomError:
"""Quantum error associated with the :class:`qibo.gates.Channel`
Args:
channel (:class:`qibo.gates.Channel`): any channel
Example:
.. testcode::
import numpy as np
from qibo.gates import KrausChannel
from qibo.noise import CustomError
# define |0><0|
a1 = np.array([[1, 0], [0, 0]])
# define |0><1|
a2 = np.array([[0, 1], [0, 0]])
# Create an Error associated with Kraus Channel
# rho -> |0><0| rho |0><0| + |0><1| rho |0><1|
error = CustomError(gates.KrausChannel((0,), [a1, a2]))
"""
def __init__(self, channel):
self.channel = channel
[docs]class NoiseModel:
"""Class for the implementation of a custom noise model.
Example:
.. testcode::
from qibo import Circuit, gates
from qibo.noise import NoiseModel, PauliError
# Build specific noise model with 2 quantum errors:
# - Pauli error on H only for qubit 1.
# - Pauli error on CNOT for all the qubits.
noise_model = NoiseModel()
noise_model.add(PauliError([("X", 0.5)]), gates.H, 1)
noise_model.add(PauliError([("Y", 0.5)]), gates.CNOT)
# Generate noiseless circuit.
circuit = Circuit(2)
circuit.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
# Apply noise to the circuit according to the noise model.
noisy_circuit = noise_model.apply(circuit)
"""
def __init__(self):
self.errors = collections.defaultdict(list)
[docs] def add(
self,
error,
gate: Optional[gates.Gate] = None,
qubits: Optional[Union[int, tuple]] = None,
conditions=None,
):
"""Add a quantum error for a specific gate and qubit to the noise model.
Args:
error: quantum error to associate with the gate. Possible choices
are :class:`qibo.noise.PauliError`,
:class:`qibo.noise.DepolarizingError`,
:class:`qibo.noise.ThermalRelaxationError`,
:class:`qibo.noise.AmplitudeDampingError`,
:class:`qibo.noise.PhaseDampingError`,
:class:`qibo.noise.ReadoutError`,
:class:`qibo.noise.ResetError`,
:class:`qibo.noise.UnitaryError`,
:class:`qibo.noise.KrausError`, and
:class:`qibo.noise.CustomError`.
gate (:class:`qibo.gates.Gate`, optional): gate after which the noise will be added.
If ``None``, the noise will be added after each gate except
:class:`qibo.gates.Channel` and :class:`qibo.gates.M`.
qubits (int or tuple, optional): qubits where the noise will be applied. If ``None``,
the noise will be added after every instance of the gate.
Defaults to ``None``.
condition (callable, optional): function that takes :class:`qibo.gates.Gate`
object as an input and returns ``True`` if noise should be added to it.
Example:
.. testcode::
import numpy as np
from qibo import Circuit, gates
from qibo.noise import NoiseModel, PauliError
# Check if a gate is RX(pi/2).
def is_sqrt_x(gate):
return np.pi/2 in gate.parameters
# Build a noise model with a Pauli error on RX(pi/2) gates.
error = PauliError(list(zip(["X", "Y", "Z"], [0.01, 0.5, 0.1])))
noise = NoiseModel()
noise.add(PauliError([("X", 0.5)]), gates.RX, conditions=is_sqrt_x)
# Generate a noiseless circuit.
circuit = Circuit(1)
circuit.add(gates.RX(0, np.pi / 2))
circuit.add(gates.RX(0, 3 * np.pi / 2))
circuit.add(gates.X(0))
# Apply noise to the circuit.
noisy_circuit = noise.apply(circuit)
"""
if isinstance(qubits, int):
qubits = (qubits,)
if (
conditions is not None
and not callable(conditions)
and not isinstance(conditions, list)
):
raise_error(
TypeError,
"`conditions` should be either a callable or a list of callables. "
+ f"Got {type(conditions)} instead.",
)
if isinstance(conditions, list) and not all(
callable(condition) for condition in conditions
):
raise_error(
TypeError,
"A element of `conditions` list is not a callable.",
)
if callable(conditions):
conditions = [conditions]
self.errors[gate].append((conditions, error, qubits))
[docs] def apply(self, circuit):
"""Generate a noisy quantum circuit according to the noise model built.
Args:
circuit (:class:`qibo.models.circuit.Circuit`): quantum circuit
Returns:
:class:`qibo.models.circuit.Circuit`: initial circuit with noise gates
added according to the noise model.
"""
noisy_circuit = circuit.__class__(**circuit.init_kwargs)
for gate in circuit.queue:
errors_list = (
self.errors[gate.__class__]
if isinstance(gate, (gates.Channel, gates.M))
else self.errors[gate.__class__] + self.errors[None]
)
if all(not isinstance(error, ReadoutError) for _, error, _ in errors_list):
noisy_circuit.add(gate)
for conditions, error, qubits in errors_list:
if conditions is None or all(
condition(gate) for condition in conditions
):
qubits = (
gate.qubits
if qubits is None
else tuple(set(gate.qubits) & set(qubits))
)
if len(qubits) == 0:
continue
if isinstance(error, CustomError) and qubits:
noisy_circuit.add(error.channel)
elif (
isinstance(
error,
(
ThermalRelaxationError,
AmplitudeDampingError,
PhaseDampingError,
ResetError,
),
)
and qubits
):
for qubit in qubits:
noisy_circuit.add(error.channel(qubit, error.options))
elif isinstance(error, ReadoutError) and qubits:
noisy_circuit.add(error.channel(qubits, error.options))
noisy_circuit.add(gate)
else:
noisy_circuit.add(error.channel(qubits, error.options))
if gate.name == "measure":
readout_error_qubits = [
qubits
for _, error, qubits in errors_list
if isinstance(error, ReadoutError)
]
if (
gate.qubits not in readout_error_qubits
and gate.register_name
not in noisy_circuit.measurement_tuples.keys()
):
noisy_circuit.add(gate)
return noisy_circuit
class _Conditions:
def __init__(self, qubits=None):
self.qubits = qubits
def condition_qubits(self, gate):
return gate.qubits == self.qubits
def condition_gate_single(self, gate):
"""Condition that had to be matched to apply noise channel to single-qubit ``gate``."""
return len(gate.qubits) == 1
def condition_gate_two(self, gate):
"""Condition that had to be matched to apply noise channel to two-qubit ``gate``."""
return len(gate.qubits) == 2
[docs]class IBMQNoiseModel(NoiseModel):
"""Class for the implementation of a IBMQ noise model.
This noise model applies a :class:`qibo.gates.DepolarizingChannel` followed by a
:class:`qibo.gates.ThermalRelaxationChannel` after each one- or two-qubit gate in the circuit.
It also applies single-qubit :class:`qibo.gates.ReadoutErrorChannel`
*before* every measurement gate.
Example:
.. testcode::
from qibo import Circuit, gates
from qibo.models.encodings import phase_encoder
from qibo.noise import DepolarizingError, ThermalRelaxationError, ReadoutError
from qibo.noise import IBMQNoiseModel, NoiseModel
nqubits = 4
# creating circuit
phases = list(range(nqubits))
circuit = phase_encoder(phases, rotation="RY")
circuit.add(gates.CNOT(qubit, qubit + 1) for qubit in range(nqubits - 1))
circuit.add(gates.M(qubit) for qubit in range(1, nqubits - 1))
# creating noise model from dictionary
parameters = {
"depolarizing_one_qubit" : {"0": 0.1, "2": 0.04, "3": 0.15},
"depolarizing_two_qubit": {"0-1": 0.2},
"t1" : {"0": 0.1, "1": 0.2, "3": 0.01},
"t2" : {"0": 0.01, "1": 0.02, "3": 0.0001},
"gate_times" : (0.1, 0.2),
"excited_population" : 0.1,
"readout_one_qubit" : {"0": (0.1, 0.1), "1": 0.1, "3": [0.1, 0.1]},
}
noise_model = IBMQNoiseModel()
noise_model.from_dict(parameters)
noisy_circuit = noise_model.apply(circuit)
"""
[docs] def from_dict(self, parameters: dict):
"""Method used to pass noise ``parameters`` as inside dictionary.
Args:
parameters (dict): Contains parameters necessary to initialise
:class:`qibo.noise.DepolarizingError`, :class:`qibo.noise.ThermalRelaxationError`,
and :class:`qibo.noise.ReadoutError`.
The keys and values of the dictionary parameters are defined below:
- ``"depolarizing_one_qubit"`` (*int* or *float* or *dict*): If ``int`` or
``float``, all qubits share the same single-qubit depolarizing parameter.
If ``dict``, expects qubit indexes as keys and their respective
depolarizing parameter as values.
See :class:`qibo.gates.channels.DepolarizingChannel`
for a detailed definition of depolarizing parameter.
- ``"depolarizing_two_qubit"`` (*int* or *float* or *dict*): If ``int`` or
``float``, all two-qubit gates share the same two-qubit depolarizing
parameter regardless in which pair of qubits the two-qubit gate is acting on.
If ``dict``, expects pair qubit indexes as keys separated by a hiphen
(e.g. "0-1" for gate that has "0" as control and "1" as target)
and their respective depolarizing parameter as values.
See :class:`qibo.gates.channels.DepolarizingChannel`
for a detailed definition of depolarizing parameter.
- ``"t1"`` (*int* or *float* or *dict*): If ``int`` or ``float``, all qubits
share the same ``t1``. If ``dict``, expects qubit indexes as keys and its
respective ``t1`` as values.
See :class:`qibo.gates.channels.ThermalRelaxationChannel`
for a detailed definition of ``t1``.
Note that ``t1`` and ``t2`` must be passed with the same type.
- ``"t2"`` (*int* or *float* or *dict*): If ``int`` or ``float``, all qubits share
the same ``t2``. If ``dict``, expects qubit indexes as keys and its
respective ``t2`` as values.
See :class:`qibo.gates.channels.ThermalRelaxationChannel`
for a detailed definition of ``t2``.
Note that ``t2`` and ``t1`` must be passed with the same type.
- ``"gate_times"`` (*tuple* or *list*): pair of gate times representing
gate times for :class:`ThermalRelaxationError` following, respectively,
one- and two-qubit gates.
- ``"excited_population"`` (*int* or *float*): See
:class:`ThermalRelaxationChannel`.
- ``"readout_one_qubit"`` (*int* or *float* or *dict*): If ``int`` or ``float``,
:math:`p(0|1) = p(1|0)`, and all qubits share the same readout error
probabilities. If ``dict``, expects qubit indexes as keys and
values as ``tuple`` (or ``list``) in the format :math:`(p(0|1),\\,p(1|0))`.
If values are ``tuple`` or ``list`` of length 1 or ``float`` or ``int``,
then it is assumed that :math:`p(0|1) = p(1|0)`.
"""
t_1 = parameters["t1"]
t_2 = parameters["t2"]
gate_time_1, gate_time_2 = parameters["gate_times"]
excited_population = parameters["excited_population"]
depolarizing_one_qubit = parameters["depolarizing_one_qubit"]
depolarizing_two_qubit = parameters["depolarizing_two_qubit"]
readout_one_qubit = parameters["readout_one_qubit"]
if isinstance(depolarizing_one_qubit, (float, int)):
self.add(
DepolarizingError(depolarizing_one_qubit),
conditions=_Conditions().condition_gate_single,
)
if isinstance(depolarizing_one_qubit, dict):
for qubit_key, lamb in depolarizing_one_qubit.items():
self.add(
DepolarizingError(lamb),
qubits=int(qubit_key),
conditions=_Conditions().condition_gate_single,
)
if isinstance(depolarizing_two_qubit, (float, int)):
self.add(
DepolarizingError(depolarizing_two_qubit),
conditions=_Conditions().condition_gate_two,
)
if isinstance(depolarizing_two_qubit, dict):
for key, lamb in depolarizing_two_qubit.items():
qubits = key.replace(" ", "").split("-")
qubits = tuple(map(int, qubits))
self.add(
DepolarizingError(lamb),
qubits=qubits,
conditions=[
_Conditions().condition_gate_two,
_Conditions(qubits).condition_qubits,
],
)
if isinstance(t_1, (float, int)) and isinstance(t_2, (float, int)):
self.add(
ThermalRelaxationError(t_1, t_2, gate_time_1, excited_population),
conditions=_Conditions().condition_gate_single,
)
self.add(
ThermalRelaxationError(t_1, t_2, gate_time_2, excited_population),
conditions=_Conditions().condition_gate_two,
)
if isinstance(t_1, dict) and isinstance(t_2, dict):
for qubit_key in t_1.keys():
self.add(
ThermalRelaxationError(
t_1[qubit_key], t_2[qubit_key], gate_time_1, excited_population
),
qubits=int(qubit_key),
conditions=_Conditions().condition_gate_single,
)
self.add(
ThermalRelaxationError(
t_1[qubit_key], t_2[qubit_key], gate_time_2, excited_population
),
qubits=int(qubit_key),
conditions=_Conditions().condition_gate_two,
)
if isinstance(readout_one_qubit, (int, float)):
probabilities = [
[1 - readout_one_qubit, readout_one_qubit],
[readout_one_qubit, 1 - readout_one_qubit],
]
self.add(ReadoutError(probabilities), gate=gates.M)
if isinstance(readout_one_qubit, dict):
for qubit, probs in readout_one_qubit.items():
if isinstance(probs, (int, float)):
probs = (probs, probs)
elif isinstance(probs, (tuple, list)) and len(probs) == 1:
probs *= 2
probabilities = [[1 - probs[0], probs[0]], [probs[1], 1 - probs[1]]]
self.add(
ReadoutError(probabilities),
gate=gates.M,
qubits=int(qubit),
)