diff --git a/qiskit/quantum_info/__init__.py b/qiskit/quantum_info/__init__.py index 34c33c917ef7..3681d3bda178 100644 --- a/qiskit/quantum_info/__init__.py +++ b/qiskit/quantum_info/__init__.py @@ -59,6 +59,9 @@ :toctree: ../stubs/ state_fidelity + average_gate_fidelity + process_fidelity + gate_error Random ====== @@ -95,6 +98,8 @@ from .operators.quaternion import Quaternion from .operators.channel import Choi, SuperOp, Kraus, Stinespring, Chi, PTM from .operators.measures import process_fidelity +from .operators import average_gate_fidelity +from .operators import gate_error from .states import Statevector, DensityMatrix from .states.states import basis_state, projector, purity from .states.measures import state_fidelity diff --git a/qiskit/quantum_info/operators/__init__.py b/qiskit/quantum_info/operators/__init__.py index a0bbbc688cd5..19a37421dfe6 100644 --- a/qiskit/quantum_info/operators/__init__.py +++ b/qiskit/quantum_info/operators/__init__.py @@ -18,3 +18,4 @@ from .pauli import Pauli, pauli_group from .channel import Choi, SuperOp, Kraus, Stinespring, Chi, PTM from .quaternion import Quaternion +from .measures import process_fidelity, average_gate_fidelity, gate_error diff --git a/qiskit/quantum_info/operators/measures.py b/qiskit/quantum_info/operators/measures.py index 05c59898fdb9..8824c42e3b35 100644 --- a/qiskit/quantum_info/operators/measures.py +++ b/qiskit/quantum_info/operators/measures.py @@ -13,80 +13,187 @@ # that they have been altered from the originals. # pylint: disable=invalid-name - """ A collection of useful quantum information functions for operators. """ +import warnings import numpy as np from qiskit.exceptions import QiskitError -from qiskit.quantum_info.operators import Operator -from qiskit.quantum_info.operators import SuperOp +from qiskit.quantum_info.operators.operator import Operator +from qiskit.quantum_info.operators.pauli import Pauli +from qiskit.quantum_info.operators.channel import SuperOp -def process_fidelity(channel1, channel2, require_cptp=True): - """Return the process fidelity between two quantum channels. +def process_fidelity(channel, + target=None, + require_cp=True, + require_tp=False, + require_cptp=False): + r"""Return the process fidelity of a noisy quantum channel. - This is given by + This process fidelity :math:`F_{\text{pro}}` is given by - F_p(E1, E2) = Tr[S2^dagger.S1])/dim^2 + .. math:: + F_{\text{pro}}(\mathcal{E}, U) + = \frac{Tr[S_U^\dagger S_{\mathcal{E}}]}{d^2} - where S1 and S2 are the SuperOp matrices for channels E1 and E2, - and dim is the dimension of the input output statespace. + where :math:`S_{\mathcal{E}}, S_{U}` are the + :class:`~qiskit.quantum_info.SuperOp` matrices for the input quantum + *channel* :math:`\cal{E}` and *target* unitary :math:`U` respectively, + and :math:`d` is the dimension of the *channel*. Args: - channel1 (QuantumChannel or matrix): a quantum channel or unitary matrix. - channel2 (QuantumChannel or matrix): a quantum channel or unitary matrix. - require_cptp (bool): require input channels to be CPTP [Default: True]. + channel (QuantumChannel): noisy quantum channel. + target (Operator or None): target unitary operator. + If `None` target is the identity operator [Default: None]. + require_cp (bool): require channel to be completely-positive + [Default: True]. + require_tp (bool): require channel to be trace-preserving + [Default: False]. + require_cptp (bool): (DEPRECATED) require input channels to be + CPTP [Default: False]. Returns: - array_like: The state fidelity F(state1, state2). + float: The process fidelity :math:`F_{\text{pro}}`. Raises: - QiskitError: if inputs channels do not have the same dimensions, - have different input and output dimensions, or are not CPTP with - `require_cptp=True`. + QiskitError: if the channel and target do not have the same + dimensions, or have different input and output dimensions, or are + not completely-positive (with ``require_cp=True``) or not + trace-preserving (with ``require_tp=True``). """ - # First we must determine if input is to be interpreted as a unitary matrix - # or as a channel. - # If input is a raw numpy array we will interpret it as a unitary matrix. - is_cptp1 = None - is_cptp2 = None - if isinstance(channel1, (list, np.ndarray)): - channel1 = Operator(channel1) - if require_cptp: - is_cptp1 = channel1.is_unitary() - if isinstance(channel2, (list, np.ndarray)): - channel2 = Operator(channel2) - if require_cptp: - is_cptp2 = channel2.is_unitary() - - # Next we convert inputs SuperOp objects - # This works for objects that also have a `to_operator` or `to_channel` method - s1 = SuperOp(channel1) - s2 = SuperOp(channel2) - - # Check inputs are CPTP + # Format inputs + if isinstance(channel, (list, np.ndarray, Operator, Pauli)): + channel = Operator(channel) + else: + channel = SuperOp(channel) + input_dim, output_dim = channel.dim + if input_dim != output_dim: + raise QiskitError( + 'Quantum channel must have equal input and output dimensions.') + + if target is not None: + # Multiple channel by adjoint of target + target = Operator(target) + if (input_dim, output_dim) != target.dim: + raise QiskitError( + 'Quantum channel and target must have the same dimensions.') + channel = channel @ target.adjoint() + + # Validate complete-positivity and trace-preserving if require_cptp: - # Only check SuperOp if we didn't already check unitary inputs - if is_cptp1 is None: - is_cptp1 = s1.is_cptp() - if not is_cptp1: - raise QiskitError('channel1 is not CPTP') - if is_cptp2 is None: - is_cptp2 = s2.is_cptp() - if not is_cptp2: - raise QiskitError('channel2 is not CPTP') - - # Check dimensions match - input_dim1, output_dim1 = s1.dim - input_dim2, output_dim2 = s2.dim - if input_dim1 != output_dim1 or input_dim2 != output_dim2: - raise QiskitError('Input channels must have same size input and output dimensions.') - if input_dim1 != input_dim2: - raise QiskitError('Input channels have different dimensions.') - - # Compute process fidelity - fidelity = np.trace(s1.compose(s2.adjoint()).data) / (input_dim1 ** 2) - return fidelity + # require_cptp kwarg is DEPRECATED + # Remove in future qiskit version + warnings.warn( + "Please use `require_cp=True, require_tp=True` " + "instead of `require_cptp=True`.", DeprecationWarning) + require_cp = True + require_tp = True + if isinstance(channel, Operator) and (require_cp or require_tp): + is_unitary = channel.is_unitary() + # Validate as unitary + if require_cp and not is_unitary: + raise QiskitError('channel is not completely-positive') + if require_tp and not is_unitary: + raise QiskitError('channel is not trace-preserving') + else: + # Validate as QuantumChannel + if require_cp and not channel.is_cp(): + raise QiskitError('channel is not completely-positive') + if require_tp and not channel.is_tp(): + raise QiskitError('channel is not trace-preserving') + + # Compute process fidelity with identity channel + if isinstance(channel, Operator): + # |Tr[U]/dim| ** 2 + fid = np.abs(np.trace(channel.data) / input_dim)**2 + else: + # Tr[S] / (dim ** 2) + fid = np.trace(channel.data) / (input_dim**2) + return float(np.real(fid)) + + +def average_gate_fidelity(channel, + target=None, + require_cp=True, + require_tp=False): + r"""Return the average gate fidelity of a noisy quantum channel. + + The average gate fidelity :math:`F_{\text{ave}}` is given by + + .. math:: + F_{\text{ave}}(\mathcal{E}, U) + &= \int d\psi \langle\psi|U^\dagger + \mathcal{E}(|\psi\rangle\!\langle\psi|)U|\psi\rangle \\ + &= \frac{d F_{\text{pro}}(\mathcal{E}, U) + 1}{d + 1} + + where :math:`F_{\text{pro}}(\mathcal{E}, U)` is the + :meth:`~qiskit.quantum_info.process_fidelity` of the input quantum + *channel* :math:`\mathcal{E}` with a *target* unitary :math:`U`, and + :math:`d` is the dimension of the *channel*. + + Args: + channel (QuantumChannel): noisy quantum channel. + target (Operator or None): target unitary operator. + If `None` target is the identity operator [Default: None]. + require_cp (bool): require channel to be completely-positive + [Default: True]. + require_tp (bool): require channel to be trace-preserving + [Default: False]. + + Returns: + float: The average gate fidelity :math:`F_{\text{ave}}`. + + Raises: + QiskitError: if the channel and target do not have the same + dimensions, or have different input and output dimensions, or are + not completely-positive (with ``require_cp=True``) or not + trace-preserving (with ``require_tp=True``). + """ + if isinstance(channel, (list, np.ndarray, Operator, Pauli)): + channel = Operator(channel) + else: + channel = SuperOp(channel) + dim, _ = channel.dim + f_pro = process_fidelity(channel, + target=target, + require_cp=require_cp, + require_tp=require_tp) + return (dim * f_pro + 1) / (dim + 1) + + +def gate_error(channel, target=None, require_cp=True, require_tp=False): + r"""Return the gate error of a noisy quantum channel. + + The gate error :math:`E` is given by the average gate infidelity + + .. math:: + E(\mathcal{E}, U) = 1 - F_{\text{ave}}(\mathcal{E}, U) + + where :math:`F_{\text{ave}}(\mathcal{E}, U)` is the + :meth:`~qiskit.quantum_info.average_gate_fidelity` of the input + quantum *channel* :math:`\mathcal{E}` with a *target* unitary + :math:`U`. + + Args: + channel (QuantumChannel): noisy quantum channel. + target (Operator or None): target unitary operator. + If `None` target is the identity operator [Default: None]. + require_cp (bool): require channel to be completely-positive + [Default: True]. + require_tp (bool): require channel to be trace-preserving + [Default: False]. + + Returns: + float: The average gate error :math:`E`. + + Raises: + QiskitError: if the channel and target do not have the same + dimensions, or have different input and output dimensions, or are + not completely-positive (with ``require_cp=True``) or not + trace-preserving (with ``require_tp=True``). + """ + return 1 - average_gate_fidelity( + channel, target=target, require_cp=require_cp, require_tp=require_tp) diff --git a/releasenotes/notes/opertor-measures-33682f22a4c58d68.yaml b/releasenotes/notes/opertor-measures-33682f22a4c58d68.yaml new file mode 100644 index 000000000000..e7b4cb21a032 --- /dev/null +++ b/releasenotes/notes/opertor-measures-33682f22a4c58d68.yaml @@ -0,0 +1,22 @@ +--- +features: + - | + Added ``average_gate_fidelity`` and ``gate_error`` functions for + ``qiskit.quantum_info`` operator and quantum channel objects. +issues: + - | + List known issues here, or remove this section. All of the list items in + this section are combined when the release notes are rendered, so the text + needs to be worded so that it does not depend on any information only + available in another section, such as the prelude. This may mean repeating + some details. +upgrade: + - | + Changed second argument of ``process_fidelity`` function to be optional. + If a target unitary is not specified, the process fidelity of the input + channel with the identity operator will be returned. +deprecations: + - | + The ``require_cptp`` kwarg of the ``process_fidelity`` function has been + deprecated. It is superseded by two separate kwargs ``require_cp`` and + ``require_tp``. diff --git a/test/python/quantum_info/operators/test_measures.py b/test/python/quantum_info/operators/test_measures.py new file mode 100644 index 000000000000..5c61f8433b21 --- /dev/null +++ b/test/python/quantum_info/operators/test_measures.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2018. +# +# 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. +"""Tests for operator measures.""" + +import unittest +import numpy as np + +from qiskit.quantum_info import Operator, Choi +from qiskit.quantum_info import process_fidelity +from qiskit.quantum_info import average_gate_fidelity +from qiskit.quantum_info import gate_error +from qiskit.test import QiskitTestCase + + +class TestOperatorMeasures(QiskitTestCase): + """Tests for Operator measures""" + def test_operator_process_fidelity(self): + """Test the process_fidelity function for operator inputs""" + # Orthogonal operator + op = Operator.from_label('X') + f_pro = process_fidelity(op, require_cp=True, require_tp=True) + self.assertAlmostEqual(f_pro, 0.0, places=7) + + # Global phase operator + op1 = Operator.from_label('X') + op2 = -1j * op1 + f_pro = process_fidelity(op1, op2, require_cp=True, require_tp=True) + self.assertAlmostEqual(f_pro, 1.0, places=7) + + def test_channel_process_fidelity(self): + """Test the process_fidelity function for channel inputs""" + depol = Choi(np.eye(4) / 2) + iden = Choi(Operator.from_label('I')) + + # Completely depolarizing channel + f_pro = process_fidelity(depol, require_cp=True, require_tp=True) + self.assertAlmostEqual(f_pro, 0.25, places=7) + + # Identity + f_pro = process_fidelity(iden, require_cp=True, require_tp=True) + self.assertAlmostEqual(f_pro, 1.0, places=7) + + # Depolarizing channel + prob = 0.3 + chan = prob * depol + (1 - prob) * iden + f_pro = process_fidelity(chan, require_cp=True, require_tp=True) + f_target = prob * 0.25 + (1 - prob) + self.assertAlmostEqual(f_pro, f_target, places=7) + + # Depolarizing channel + prob = 0.5 + op = Operator.from_label('Y') + chan = (prob * depol + (1 - prob) * iden) @ op + f_pro = process_fidelity(chan, op, require_cp=True, require_tp=True) + target = prob * 0.25 + (1 - prob) + self.assertAlmostEqual(f_pro, target, places=7) + + def test_operator_average_gate_fidelity(self): + """Test the average_gate_fidelity function for operator inputs""" + # Orthogonal operator + op = Operator.from_label('Z') + f_ave = average_gate_fidelity(op, require_cp=True, require_tp=True) + self.assertAlmostEqual(f_ave, 1 / 3, places=7) + + # Global phase operator + op1 = Operator.from_label('Y') + op2 = -1j * op1 + f_ave = average_gate_fidelity(op1, + op2, + require_cp=True, + require_tp=True) + self.assertAlmostEqual(f_ave, 1.0, places=7) + + def test_channel_average_gate_fidelity(self): + """Test the average_gate_fidelity function for channel inputs""" + depol = Choi(np.eye(4) / 2) + iden = Choi(Operator.from_label('I')) + + # Completely depolarizing channel + f_ave = average_gate_fidelity(depol, require_cp=True, require_tp=True) + self.assertAlmostEqual(f_ave, 0.5, places=7) + + # Identity + f_ave = average_gate_fidelity(iden, require_cp=True, require_tp=True) + self.assertAlmostEqual(f_ave, 1.0, places=7) + + # Depolarizing channel + prob = 0.11 + chan = prob * depol + (1 - prob) * iden + f_ave = average_gate_fidelity(chan, require_cp=True, require_tp=True) + f_target = (2 * (prob * 0.25 + (1 - prob)) + 1) / 3 + self.assertAlmostEqual(f_ave, f_target, places=7) + + # Depolarizing channel + prob = 0.5 + op = Operator.from_label('Y') + chan = (prob * depol + (1 - prob) * iden) @ op + f_ave = average_gate_fidelity(chan, + op, + require_cp=True, + require_tp=True) + target = (2 * (prob * 0.25 + (1 - prob)) + 1) / 3 + self.assertAlmostEqual(f_ave, target, places=7) + + def test_operator_gate_error(self): + """Test the gate_error function for operator inputs""" + # Orthogonal operator + op = Operator.from_label('Z') + err = gate_error(op, require_cp=True, require_tp=True) + self.assertAlmostEqual(err, 2 / 3, places=7) + + # Global phase operator + op1 = Operator.from_label('Y') + op2 = -1j * op1 + err = gate_error(op1, op2, require_cp=True, require_tp=True) + self.assertAlmostEqual(err, 0, places=7) + + def test_channel_gate_error(self): + """Test the gate_error function for channel inputs""" + depol = Choi(np.eye(4) / 2) + iden = Choi(Operator.from_label('I')) + + # Depolarizing channel + prob = 0.11 + chan = prob * depol + (1 - prob) * iden + err = gate_error(chan, require_cp=True, require_tp=True) + target = 1 - average_gate_fidelity(chan) + self.assertAlmostEqual(err, target, places=7) + + # Depolarizing channel + prob = 0.5 + op = Operator.from_label('Y') + chan = (prob * depol + (1 - prob) * iden) @ op + err = gate_error(chan, op, require_cp=True, require_tp=True) + target = 1 - average_gate_fidelity(chan, op) + self.assertAlmostEqual(err, target, places=7) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/python/quantum_info/test_operators.py b/test/python/quantum_info/test_operators.py deleted file mode 100644 index ff53302b6d71..000000000000 --- a/test/python/quantum_info/test_operators.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. - -"""Quick program to test the quantum operators modules.""" - -import unittest -import numpy as np - -from scipy.linalg import expm -from qiskit.quantum_info import process_fidelity, Pauli -from qiskit.test import QiskitTestCase - - -class TestOperators(QiskitTestCase): - """Tests for qi.py""" - - def test_process_fidelity(self): - """Test the process_fidelity function""" - unitary1 = Pauli(label='XI').to_matrix() - unitary2 = np.kron(np.array([[0, 1], [1, 0]]), np.eye(2)) - process_fidelity(unitary1, unitary2) - self.assertAlmostEqual(process_fidelity(unitary1, unitary2), 1.0, places=7) - theta = 0.2 - unitary1 = expm(-1j*theta*Pauli(label='X').to_matrix()/2) - unitary2 = np.array([[np.cos(theta/2), -1j*np.sin(theta/2)], - [-1j*np.sin(theta/2), np.cos(theta/2)]]) - self.assertAlmostEqual(process_fidelity(unitary1, unitary2), 1.0, places=7) - - -if __name__ == '__main__': - unittest.main()