1# This code is part of Qiskit.
2#
3# (C) Copyright IBM 2018, 2019.
4#
5# This code is licensed under the Apache License, Version 2.0. You may
6# obtain a copy of this license in the LICENSE.txt file in the root directory
7# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8#
9# Any modifications or derivative works of this code must retain this
10# copyright notice, and modified files need to carry a notice indicating
11# that they have been altered from the originals.
12"""
13Readout error class for Qiskit Aer noise model.
14"""
15
16import copy
17
18import numpy as np
19from numpy.linalg import norm
20
21from qiskit.circuit import Instruction
22from qiskit.quantum_info.operators.predicates import ATOL_DEFAULT, RTOL_DEFAULT
23
24from ..noiseerror import NoiseError
25from .errorutils import qubits_from_mat
26
27
28class ReadoutError:
29    """
30    Readout error class for Qiskit Aer noise model.
31    """
32    # pylint: disable=invalid-name
33    _ATOL_DEFAULT = ATOL_DEFAULT
34    _RTOL_DEFAULT = RTOL_DEFAULT
35    _MAX_TOL = 1e-4
36
37    def __init__(self, probabilities, atol=ATOL_DEFAULT):
38        """
39        Create a readout error for a noise model.
40
41        For an N-qubit readout error probabilities are entered as vectors:
42
43        .. code-block:: python
44
45            probabilities[j] = [P(0|m), P(1|m), ..., P(2 ** N - 1|m)]
46
47        where ``P(j|m)`` is the probability of recording a measurement outcome
48        of ``m`` as the value ``j``. Where ``j`` and ``m`` are integer
49        representations of bit-strings.
50
51        **Example: 1-qubit**
52
53        .. code-block:: python
54
55            probabilities[0] = [P("0"|"0"), P("1"|"0")]
56            probabilities[1] = [P("0"|"1"), P("1"|"1")]
57
58        **Example: 2-qubit**
59
60        .. code-block:: python
61
62            probabilities[0] = [P("00"|"00"), P("01"|"00"), P("10"|"00"), P("11"|"00")]
63            probabilities[1] = [P("00"|"01"), P("01"|"01"), P("10"|"01"), P("11"|"01")]
64            probabilities[2] = [P("00"|"10"), P("01"|"10"), P("10"|"10"), P("11"|"10")]
65            probabilities[3] = [P("00"|"11"), P("01"|"11"), P("10"|"11"), P("11"|"11")]
66
67        Args:
68            probabilities (matrix): List of outcome assignment probabilities.
69            atol (double): Threshold for checking probabilities are normalized
70                           (Default: 1e-8).
71        """
72        self._check_probabilities(probabilities, atol)
73        self._probabilities = np.array(probabilities, dtype=float)
74        self._number_of_qubits = qubits_from_mat(probabilities)
75
76    def __repr__(self):
77        """Display ReadoutError."""
78        return "ReadoutError({})".format(self._probabilities)
79
80    def __str__(self):
81        """Print error information."""
82        output = "ReadoutError on {} qubits.".format(self._number_of_qubits) + \
83                 " Assignment probabilities:"
84        for j, vec in enumerate(self._probabilities):
85            output += "\n P(j|{0}) =  {1}".format(j, vec)
86        return output
87
88    def __eq__(self, other):
89        """Test if two ReadoutErrors are equal."""
90        if not isinstance(other, ReadoutError):
91            return False
92        if self.number_of_qubits != other.number_of_qubits:
93            return False
94        return np.allclose(self._probabilities, other._probabilities,
95                           atol=self.atol, rtol=self.rtol)
96
97    def copy(self):
98        """Make a copy of current ReadoutError."""
99        # pylint: disable=no-value-for-parameter
100        # The constructor of subclasses from raw data should be a copy
101        return copy.deepcopy(self)
102
103    @property
104    def number_of_qubits(self):
105        """Return the number of qubits for the error."""
106        return self._number_of_qubits
107
108    @property
109    def probabilities(self):
110        """Return the readout error probabilities matrix."""
111        return self._probabilities
112
113    @property
114    def atol(self):
115        """The default absolute tolerance parameter for float comparisons."""
116        return ReadoutError._ATOL_DEFAULT
117
118    @property
119    def rtol(self):
120        """The relative tolerance parameter for float comparisons."""
121        return ReadoutError._RTOL_DEFAULT
122
123    @classmethod
124    def set_atol(cls, value):
125        """Set the class default absolute tolerance parameter for float comparisons."""
126        if value < 0:
127            raise NoiseError(
128                "Invalid atol ({}) must be non-negative.".format(value))
129        if value > cls._MAX_TOL:
130            raise NoiseError(
131                "Invalid atol ({}) must be less than {}.".format(
132                    value, cls._MAX_TOL))
133        cls._ATOL_DEFAULT = value
134
135    @classmethod
136    def set_rtol(cls, value):
137        """Set the class default relative tolerance parameter for float comparisons."""
138        if value < 0:
139            raise NoiseError(
140                "Invalid rtol ({}) must be non-negative.".format(value))
141        if value > cls._MAX_TOL:
142            raise NoiseError(
143                "Invalid rtol ({}) must be less than {}.".format(
144                    value, cls._MAX_TOL))
145        cls._RTOL_DEFAULT = value
146
147    def ideal(self):
148        """Return True if current error object is an identity"""
149        iden = np.eye(2**self.number_of_qubits)
150        delta = round(norm(np.array(self.probabilities) - iden), 12)
151        if delta == 0:
152            return True
153        return False
154
155    def to_instruction(self):
156        """Convert the ReadoutError to a circuit Instruction."""
157        return Instruction("roerror", 0, self.number_of_qubits, self._probabilities)
158
159    def to_dict(self):
160        """Return the current error as a dictionary."""
161        error = {
162            "type": "roerror",
163            "operations": ["measure"],
164            "probabilities": self._probabilities.tolist()
165        }
166        return error
167
168    def compose(self, other, front=False):
169        """Return the composition readout error other * self.
170
171        Note that for `front=True` this is equivalent to the
172        :meth:`ReadoutError.dot` method.
173
174        Args:
175            other (ReadoutError): a readout error.
176            front (bool): If True return the reverse order composation
177                          self * other instead [default: False].
178
179        Returns:
180            ReadoutError: The composition readout error.
181
182        Raises:
183            NoiseError: if other is not a ReadoutError or has incompatible
184            dimensions.
185        """
186        if front:
187            return self._matmul(other)
188        return self._matmul(other, left_multiply=True)
189
190    def dot(self, other):
191        """Return the composition readout error self * other.
192
193        Args:
194            other (ReadoutError): a readout error.
195
196        Returns:
197            ReadoutError: The composition readout error.
198
199        Raises:
200            NoiseError: if other is not a ReadoutError or has incompatible
201            dimensions.
202        """
203        return self._matmul(other)
204
205    def power(self, n):
206        """Return the compose of the readout error with itself n times.
207
208        Args:
209            n (int): the number of times to compose with self (n>0).
210
211        Returns:
212            ReadoutError: the n-times composition channel.
213
214        Raises:
215            NoiseError: if the power is not a positive integer.
216        """
217        if not isinstance(n, int) or n < 1:
218            raise NoiseError("Can only power with positive integer powers.")
219        ret = self.copy()
220        for _ in range(1, n):
221            ret = ret.compose(self)
222        return ret
223
224    def tensor(self, other):
225        """Return the tensor product readout error self ⊗ other.
226
227        Args:
228            other (ReadoutError): a readout error.
229
230        Returns:
231            ReadoutError: the tensor product readout error self ⊗ other.
232
233        Raises:
234            NoiseError: if other is not a ReadoutError.
235        """
236        return self._tensor_product(other, reverse=False)
237
238    def expand(self, other):
239        """Return the tensor product readout error self ⊗ other.
240
241        Args:
242            other (ReadoutError): a readout error.
243
244        Returns:
245            ReadoutError: the tensor product readout error other ⊗ self.
246
247        Raises:
248            NoiseError: if other is not a ReadoutError.
249        """
250        return self._tensor_product(other, reverse=True)
251
252    @staticmethod
253    def _check_probabilities(probabilities, threshold):
254        """Check probabilities are valid."""
255        # probabilities parameter can be a list or a numpy.ndarray
256        if (isinstance(probabilities, list) and not probabilities) or \
257           (isinstance(probabilities, np.ndarray) and probabilities.size == 0):
258            raise NoiseError("Input probabilities: empty.")
259        num_outcomes = len(probabilities[0])
260        num_qubits = int(np.log2(num_outcomes))
261        if 2**num_qubits != num_outcomes:
262            raise NoiseError("Invalid probabilities: length "
263                             "{} != 2**{}".format(num_outcomes, num_qubits))
264        if len(probabilities) != num_outcomes:
265            raise NoiseError("Invalid probabilities.")
266        for vec in probabilities:
267            arr = np.array(vec)
268            if len(arr) != num_outcomes:
269                raise NoiseError(
270                    "Invalid probabilities: vectors are different lengths.")
271            if abs(sum(arr) - 1) > threshold:
272                raise NoiseError("Invalid probabilities: sum({})= {} "
273                                 "is not 1.".format(vec, sum(arr)))
274            if arr[arr < 0].size > 0:
275                raise NoiseError(
276                    "Invalid probabilities: {} "
277                    "contains a negative probability.".format(vec))
278
279    def _matmul(self, other, left_multiply=False):
280        """Return the composition readout error.
281
282        Args:
283            other (ReadoutError): a readout error.
284            left_multiply (bool): If True return other * self
285                                  If False return self * other [Default:False]
286        Returns:
287            ReadoutError: The composition readout error.
288
289        Raises:
290            NoiseError: if other is not a ReadoutError or has incompatible
291            dimensions.
292        """
293        if not isinstance(other, ReadoutError):
294            other = ReadoutError(other)
295        if self.number_of_qubits != other.number_of_qubits:
296            raise NoiseError("other must have same number of qubits.")
297        if left_multiply:
298            probs = np.dot(other._probabilities, self._probabilities)
299        else:
300            probs = np.dot(self._probabilities, other._probabilities)
301        return ReadoutError(probs)
302
303    def _tensor_product(self, other, reverse=False):
304        """Return the tensor product readout error.
305
306        Args:
307            other (ReadoutError): a readout error.
308            reverse (bool): If False return self ⊗ other, if True return
309                            if True return (other ⊗ self) [Default: False
310        Returns:
311            ReadoutError: the tensor product readout error.
312        """
313        if not isinstance(other, ReadoutError):
314            other = ReadoutError(other)
315        if reverse:
316            probs = np.kron(other._probabilities, self._probabilities)
317        else:
318            probs = np.kron(self._probabilities, other._probabilities)
319        return ReadoutError(probs)
320
321    # Overloads
322    def __matmul__(self, other):
323        return self.compose(other)
324
325    def __mul__(self, other):
326        return self.dot(other)
327
328    def __pow__(self, n):
329        return self.power(n)
330
331    def __xor__(self, other):
332        return self.tensor(other)
333
334    def __rmul__(self, other):
335        raise NotImplementedError(
336            "'ReadoutError' does not support scalar multiplication.")
337
338    def __truediv__(self, other):
339        raise NotImplementedError("'ReadoutError' does not support division.")
340
341    def __add__(self, other):
342        raise NotImplementedError("'ReadoutError' does not support addition.")
343
344    def __sub__(self, other):
345        raise NotImplementedError(
346            "'ReadoutError' does not support subtraction.")
347
348    def __neg__(self):
349        raise NotImplementedError("'ReadoutError' does not support negation.")
350