# This code is part of Qiskit.
#
# (C) Copyright IBM 2018, 2019.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Standard quantum computing error channels for Qiskit Aer.
"""
import itertools as it
import numpy as np
from qiskit.quantum_info.operators.pauli import Pauli
from qiskit.quantum_info.operators.channel import Choi, Kraus
from qiskit.quantum_info.operators.predicates import is_unitary_matrix
from qiskit.quantum_info.operators.predicates import is_identity_matrix
from ..noiseerror import NoiseError
from .errorutils import make_unitary_instruction
from .errorutils import qubits_from_mat
from .errorutils import standard_gate_unitary
from .quantum_error import QuantumError
[docs]def kraus_error(noise_ops, standard_gates=True, canonical_kraus=False):
"""
Return a Kraus quantum error channel.
Args:
noise_ops (list[matrix]): Kraus matrices.
standard_gates (bool): Check if input matrices are standard gates.
canonical_kraus (bool): Convert input Kraus matrices into the
canonical Kraus representation (default: False)
Returns:
QuantumError: The quantum error object.
Raises:
NoiseError: if error parameters are invalid.
"""
if not isinstance(noise_ops, (list, tuple)):
raise NoiseError("Invalid Kraus error input.")
if not noise_ops:
raise NoiseError("Kraus error noise_ops must not be empty.")
kraus = Kraus(noise_ops)
if canonical_kraus:
# Convert to Choi and back to get canonical Kraus
kraus = Kraus(Choi(kraus))
return QuantumError(kraus, standard_gates=standard_gates)
[docs]def mixed_unitary_error(noise_ops, standard_gates=True):
"""
Return a mixed unitary quantum error channel.
The input should be a list of pairs ``(U[j], p[j])``, where
``U[j]`` is a unitary matrix and ``p[j]`` is a probability. All
probabilities must sum to 1 for the input ops to be valid.
Args:
noise_ops (list[pair[matrix, double]]): unitary error matrices.
standard_gates (bool): Check if input matrices are standard gates.
Returns:
QuantumError: The quantum error object.
Raises:
NoiseError: if error parameters are invalid.
"""
# Error checking
if not isinstance(noise_ops, (list, tuple, zip)):
raise NoiseError("Input noise ops is not a list.")
# Convert to numpy arrays
noise_ops = [(np.array(op, dtype=complex), p) for op, p in noise_ops]
if not noise_ops:
raise NoiseError("Input noise list is empty.")
# Check for identity unitaries
prob_identity = 0.
instructions = []
instructions_probs = []
num_qubits = qubits_from_mat(noise_ops[0][0])
qubits = list(range(num_qubits))
for unitary, prob in noise_ops:
# Check unitary
if qubits_from_mat(unitary) != num_qubits:
raise NoiseError("Input matrices different size.")
if not is_unitary_matrix(unitary):
raise NoiseError("Input matrix is not unitary.")
if is_identity_matrix(unitary):
prob_identity += prob
else:
instr = make_unitary_instruction(
unitary, qubits, standard_gates=standard_gates)
instructions.append(instr)
instructions_probs.append(prob)
if prob_identity > 0:
instructions.append([{"name": "id", "qubits": [0]}])
instructions_probs.append(prob_identity)
return QuantumError(zip(instructions, instructions_probs))
[docs]def coherent_unitary_error(unitary):
"""
Return a coherent unitary quantum error channel.
Args:
unitary (matrix like): unitary error matrix.
Returns:
QuantumError: The quantum error object.
"""
return mixed_unitary_error([(unitary, 1)])
[docs]def pauli_error(noise_ops, standard_gates=True):
"""
Return a mixed Pauli quantum error channel.
The input should be a list of pairs ``(P[j], p[j])``, where
``P[j]`` is a ``Pauli`` object or string label, and ``p[j]`` is a
probability. All probabilities must sum to 1 for the input ops to
be valid.
Args:
noise_ops (list[pair[Pauli, double]]): Pauli error terms.
standard_gates (bool): if True return the operators as standard qobj
Pauli gate instructions. If false return as
unitary matrix qobj instructions.
(Default: True)
Returns:
QuantumError: The quantum error object.
Raises:
NoiseError: If depolarizing probability is less than 0 or greater than 1.
"""
# Error checking
if not isinstance(noise_ops, (list, tuple, zip)):
raise NoiseError("Input noise ops is not a list.")
noise_ops = list(noise_ops)
if not noise_ops:
raise NoiseError("Input noise list is empty.")
num_qubits = None
for pauli, _ in noise_ops:
if isinstance(pauli, Pauli):
pauli_str = pauli.to_label()
elif isinstance(pauli, str):
pauli_str = pauli
else:
raise NoiseError("Invalid Pauli input operator: {}".format(pauli))
if num_qubits is None:
num_qubits = len(pauli_str)
elif num_qubits != len(pauli_str):
raise NoiseError("Pauli's are not all of the same length.")
# Compute Paulis as single matrix
if standard_gates is False:
return _pauli_error_unitary(noise_ops, num_qubits)
# Compute as qobj Pauli gate instructions
return _pauli_error_standard(noise_ops, num_qubits)
def _pauli_error_unitary(noise_ops, num_qubits):
"""Return Pauli error as unitary qobj instructions."""
def single_pauli(pauli):
if pauli == 'I':
return standard_gate_unitary('id')
if pauli == 'X':
return standard_gate_unitary('x')
if pauli == 'Y':
return standard_gate_unitary('y')
if pauli == 'Z':
return standard_gate_unitary('z')
raise NoiseError("Invalid Pauli string.")
prob_identity = 0.0
pauli_circs = []
pauli_probs = []
for pauli, prob in noise_ops:
if prob > 0:
# Pauli strings go from qubit-0 on left to qubit-N on right
# but pauli ops are tensor product of qubit-N on left to qubit-0 on right
# We also drop identity operators to reduce dimension of matrix multiplication
mat = np.identity(1)
qubits = []
if isinstance(pauli, Pauli):
pauli_str = pauli.to_label()
else:
pauli_str = pauli
for qubit, pstr in enumerate(reversed(pauli_str)):
if pstr in ['X', 'Y', 'Z']:
mat = np.kron(single_pauli(pstr), mat)
qubits.append(qubit)
elif pstr != 'I':
raise NoiseError("Invalid Pauli string.")
if mat.size == 1:
prob_identity += prob
else:
circ = make_unitary_instruction(
mat, qubits, standard_gates=False)
pauli_circs.append(circ)
pauli_probs.append(prob)
if prob_identity > 0:
pauli_probs.append(prob_identity)
pauli_circs.append([{"name": "id", "qubits": [0]}])
error = QuantumError(
zip(pauli_circs, pauli_probs), number_of_qubits=num_qubits)
return error
def _pauli_error_standard(noise_ops, num_qubits):
"""Return Pauli error as standard Pauli gate qobj instructions."""
def single_pauli(pauli):
if pauli == 'I':
return {'name': 'id'}
if pauli == 'X':
return {'name': 'x'}
if pauli == 'Y':
return {'name': 'y'}
if pauli == 'Z':
return {'name': 'z'}
raise NoiseError("Invalid Pauli string.")
prob_identity = 0.0
pauli_circuits = []
pauli_probs = []
for pauli, prob in noise_ops:
if prob > 0:
# Pauli strings go from qubit-0 on left to qubit-N on right
# but pauli ops are tensor product of qubit-N on left to qubit-0 on right
# We also drop identity operators to reduce dimension of matrix multiplication
circuit = []
if isinstance(pauli, Pauli):
pauli_str = pauli.to_label()
else:
pauli_str = pauli
for qubit, pstr in enumerate(reversed(pauli_str)):
if pstr in ['X', 'Y', 'Z']:
instruction = single_pauli(pstr)
instruction["qubits"] = [qubit]
circuit.append(instruction)
elif pstr != 'I':
raise NoiseError("Invalid Pauli string.")
if circuit == []:
prob_identity += prob
else:
pauli_circuits.append(circuit)
pauli_probs.append(prob)
if prob_identity > 0:
pauli_circuits.append([{"name": "id", "qubits": [0]}])
pauli_probs.append(prob_identity)
error = QuantumError(
zip(pauli_circuits, pauli_probs), number_of_qubits=num_qubits)
return error
[docs]def depolarizing_error(param, num_qubits, standard_gates=True):
r"""
Return a depolarizing quantum error channel.
The depolarizing channel is defined as:
.. math::
E(ρ) = (1 - λ) ρ + λ \text{Tr}[ρ] \frac{I}{2^n}
with :math:`0 \le λ \le 4^n / (4^n - 1)`
where :math:`λ` is the depolarizing error param and :math`n` is the
number of qubits.
* If :math:`λ = 0` this is the identity channel :math:`E(ρ) = ρ`
* If :math:`λ = 1` this is a completely depolarizing channel
:math:`E(ρ) = I / 2^n`
* If :math:`λ = 4^n / (4^n - 1)` this is a uniform Pauli
error channel: :math:`E(ρ) = \sum_j P_j ρ P_j / (4^n - 1)` for
all :math:`P_j != I`.
Args:
param (double): depolarizing error parameter.
num_qubits (int): the number of qubits for the error channel.
standard_gates (bool): if True return the operators as standard qobj
Pauli gate instructions. If false return as
unitary matrix qobj instructions.
(Default: True)
Returns:
QuantumError: The quantum error object.
Raises:
NoiseError: If noise parameters are invalid.
"""
if not isinstance(num_qubits, int) or num_qubits < 1:
raise NoiseError("num_qubits must be a positive integer.")
# Check that the depolarizing parameter gives a valid CPTP
num_terms = 4**num_qubits
max_param = num_terms / (num_terms - 1)
if param < 0 or param > max_param:
raise NoiseError("Depolarizing parameter must be in between 0 "
"and {}.".format(max_param))
# Rescale completely depolarizing channel error probs
# with the identity component removed
prob_iden = 1 - param / max_param
prob_pauli = param / num_terms
probs = [prob_iden] + (num_terms - 1) * [prob_pauli]
# Generate pauli strings. The order doesn't matter as long
# as the all identity string is first.
paulis = [
"".join(tup)
for tup in it.product(['I', 'X', 'Y', 'Z'], repeat=num_qubits)
]
return pauli_error(zip(paulis, probs), standard_gates=standard_gates)
[docs]def reset_error(prob0, prob1=0):
r"""
Return a single qubit reset quantum error channel.
The error channel returned is given by the map
.. math::
E(ρ) = (1 - p_0 - p_1) ρ + \text{Tr}[ρ] \left(
p_0 |0 \rangle\langle 0|
+ p_1 |1 \rangle\langle 1| \right)
where the probability of no reset is given by :math:`1 - p_0 - p_1`.
Args:
prob0 (double): reset probability to :math:`|0\rangle`.
prob1 (double): reset probability to :math:`|1\rangle`.
Returns:
QuantumError: the quantum error object.
Raises:
NoiseError: If noise parameters are invalid.
"""
if prob0 < 0 or prob1 < 0 or prob0 > 1 or prob1 > 1:
raise NoiseError("Invalid reset probabilities.")
noise_ops = [
([{
'name': 'id',
'qubits': [0]
}], 1 - prob0 - prob1),
([{
'name': 'reset',
'qubits': [0]
}], prob0),
([{
'name': 'reset',
'qubits': [0]
}, {
'name': 'x',
'qubits': [0]
}], prob1),
]
return QuantumError(noise_ops)
# pylint: disable=invalid-name
[docs]def thermal_relaxation_error(t1, t2, time, excited_state_population=0):
r"""
Return a single-qubit thermal relaxation quantum error channel.
Args:
t1 (double): the :math:`T_1` relaxation time constant.
t2 (double): the :math:`T_2` relaxation time constant.
time (double): the gate time for relaxation error.
excited_state_population (double): the population of :math:`|1\rangle`
state at equilibrium (default: 0).
Returns:
QuantumError: a quantum error object for a noise model.
Raises:
NoiseError: If noise parameters are invalid.
Additional information:
* For parameters to be valid :math:`T_1` and :math:`T_2` must
satisfy :math:`T_2 \le 2 T_1`.
* If :math:`T_2 \le T_1` the error can be expressed as a mixed
reset and unitary error channel.
* If :math:`T_1 < T_2 \le 2 T_1` the error must be expressed as a
general non-unitary Kraus error channel.
"""
if excited_state_population < 0:
raise NoiseError("Invalid excited state population "
"({} < 0).".format(excited_state_population))
if excited_state_population > 1:
raise NoiseError("Invalid excited state population "
"({} > 1).".format(excited_state_population))
if time < 0:
raise NoiseError("Invalid gate_time ({} < 0)".format(time))
if t1 <= 0:
raise NoiseError("Invalid T_1 relaxation time parameter: T_1 <= 0.")
if t2 <= 0:
raise NoiseError("Invalid T_2 relaxation time parameter: T_2 <= 0.")
if t2 - 2 * t1 > 0:
raise NoiseError(
"Invalid T_2 relaxation time parameter: T_2 greater than 2 * T_1.")
# T1 relaxation rate
if t1 == np.inf:
rate1 = 0
p_reset = 0
else:
rate1 = 1 / t1
p_reset = 1 - np.exp(-time * rate1)
# T2 dephasing rate
if t2 == np.inf:
rate2 = 0
exp_t2 = 1
else:
rate2 = 1 / t2
exp_t2 = np.exp(-time * rate2)
# Qubit state equilibrium probabilities
p0 = 1 - excited_state_population
p1 = excited_state_population
if t2 > t1:
# If T_2 > T_1 we must express this as a Kraus channel
# We start with the Choi-matrix representation:
chan = Choi(
np.array([[1 - p1 * p_reset, 0, 0, exp_t2],
[0, p1 * p_reset, 0, 0], [0, 0, p0 * p_reset, 0],
[exp_t2, 0, 0, 1 - p0 * p_reset]]))
return QuantumError(Kraus(chan))
else:
# If T_2 < T_1 we can express this channel as a probabilistic
# mixture of reset operations and unitary errors:
circuits = [[{
'name': 'id',
'qubits': [0]
}], [{
'name': 'z',
'qubits': [0]
}], [{
'name': 'reset',
'qubits': [0]
}], [{
'name': 'reset',
'qubits': [0]
}, {
'name': 'x',
'qubits': [0]
}]]
# Probability
p_reset0 = p_reset * p0
p_reset1 = p_reset * p1
p_z = (1 - p_reset) * (1 - np.exp(-time * (rate2 - rate1))) / 2
p_identity = 1 - p_z - p_reset0 - p_reset1
probabilities = [p_identity, p_z, p_reset0, p_reset1]
return QuantumError(zip(circuits, probabilities))
[docs]def phase_amplitude_damping_error(param_amp,
param_phase,
excited_state_population=0,
canonical_kraus=True):
r"""
Return a single-qubit combined phase and amplitude damping quantum error channel.
The single-qubit combined phase and amplitude damping channel is
described by the following Kraus matrices:
.. code-block:: python
A0 = sqrt(1 - p1) * [[1, 0], [0, sqrt(1 - a - b)]]
A1 = sqrt(1 - p1) * [[0, sqrt(a)], [0, 0]]
A2 = sqrt(1 - p1) * [[0, 0], [0, sqrt(b)]]
B0 = sqrt(p1) * [[sqrt(1 - a - b), 0], [0, 1]]
B1 = sqrt(p1) * [[0, 0], [sqrt(a), 0]]
B2 = sqrt(p1) * [[sqrt(b), 0], [0, 0]]
where ``a = param_amp``, ``b = param_phase``, and
``p1 = excited_state_population``. The equilibrium state after infinitely
many applications of the channel is:
.. code-block:: python
rho_eq = [[1 - p1, 0]], [0, p1]]
Args:
param_amp (double): the amplitude damping error parameter.
param_phase (double): the phase damping error parameter.
excited_state_population (double): the population of :math:`|1\rangle`
state at equilibrium (default: 0).
canonical_kraus (bool): Convert input Kraus matrices into the
canonical Kraus representation (default: True)
Returns:
QuantumError: a quantum error object for a noise model.
Raises:
NoiseError: If noise parameters are invalid.
"""
if param_amp < 0:
raise NoiseError("Invalid amplitude damping to |0> parameter "
"({} < 0)".format(param_amp))
if param_phase < 0:
raise NoiseError("Invalid phase damping parameter "
"({} < 0)".format(param_phase))
if param_phase + param_amp > 1:
raise NoiseError("Invalid amplitude and phase damping parameters "
"({} + {} > 1)".format(param_phase, param_amp))
if excited_state_population < 0:
raise NoiseError("Invalid excited state population "
"({} < 0).".format(excited_state_population))
if excited_state_population > 1:
raise NoiseError("Invalid excited state population "
"({} > 1).".format(excited_state_population))
c0 = np.sqrt(1 - excited_state_population)
c1 = np.sqrt(excited_state_population)
param = 1 - param_amp - param_phase
# Damping ops to 0 state
A0 = c0 * np.array([[1, 0], [0, np.sqrt(param)]], dtype=complex)
A1 = c0 * np.array([[0, np.sqrt(param_amp)], [0, 0]], dtype=complex)
A2 = c0 * np.array([[0, 0], [0, np.sqrt(param_phase)]], dtype=complex)
# Damping ops to 1 state
B0 = c1 * np.array([[np.sqrt(param), 0], [0, 1]], dtype=complex)
B1 = c1 * np.array([[0, 0], [np.sqrt(param_amp), 0]], dtype=complex)
B2 = c1 * np.array([[np.sqrt(param_phase), 0], [0, 0]], dtype=complex)
# Select non-zero ops
noise_ops = [
a for a in [A0, A1, A2, B0, B1, B2] if np.linalg.norm(a) > 1e-10
]
return kraus_error(noise_ops, canonical_kraus=canonical_kraus)
[docs]def amplitude_damping_error(param_amp,
excited_state_population=0,
canonical_kraus=True):
r"""
Return a single-qubit generalized amplitude damping quantum error channel.
The single-qubit amplitude damping channel is described by the
following Kraus matrices:
.. code-block:: python
A0 = sqrt(1 - p1) * [[1, 0], [0, sqrt(1 - a)]]
A1 = sqrt(1 - p1) * [[0, sqrt(a)], [0, 0]]
B0 = sqrt(p1) * [[sqrt(1 - a), 0], [0, 1]]
B1 = sqrt(p1) * [[0, 0], [sqrt(a), 0]]
where ``a = param_amp``, ``p1 = excited_state_population``.
The equilibrium state after infinitely many applications of the
channel is:
.. code-block:: python
rho_eq = [[1 - p1, 0]], [0, p1]]
Args:
param_amp (double): the amplitude damping parameter.
excited_state_population (double): the population of :math:`|0\rangle`
state at equilibrium (default: 0).
canonical_kraus (bool): Convert input Kraus matrices into the
canonical Kraus representation (default: True)
Returns:
QuantumError: a quantum error object for a noise model.
"""
return phase_amplitude_damping_error(
param_amp,
0,
excited_state_population=excited_state_population,
canonical_kraus=canonical_kraus)
[docs]def phase_damping_error(param_phase, canonical_kraus=True):
r"""
Return a single-qubit combined phase and amplitude damping quantum error channel.
The single-qubit combined phase and amplitude damping channel is
described by the following Kraus matrices:
.. code-block:: python
A0 = [[1, 0], [0, sqrt(1 - b)]]
A2 = [[0, 0], [0, sqrt(b)]]
where ``b = param_phase``.
The equilibrium state after infinitely many applications of the
channel is:
.. code-block:: python
rho_eq = [[rho_init[0, 0], 0]], [0, rho_init[1, 1]]]
where ``rho_init`` is the input state ρ.
Args:
param_phase (double): the phase damping parameter.
canonical_kraus (bool): Convert input Kraus matrices into the
canonical Kraus representation (default: True)
Returns:
QuantumError: a quantum error object for a noise model.
"""
return phase_amplitude_damping_error(
0,
param_phase,
excited_state_population=0,
canonical_kraus=canonical_kraus)