1# Copyright 2018 The Cirq Developers
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Protocol and methods for obtaining Kraus representation of quantum channels."""
16
17from typing import Any, Sequence, Tuple, TypeVar, Union
18import warnings
19
20import numpy as np
21from typing_extensions import Protocol
22
23from cirq._doc import doc_private
24from cirq.protocols.decompose_protocol import (
25    _try_decompose_into_operations_and_qubits,
26)
27from cirq.protocols.mixture_protocol import has_mixture
28
29
30from cirq.type_workarounds import NotImplementedType
31
32
33# This is a special indicator value used by the channel method to determine
34# whether or not the caller provided a 'default' argument. It must be of type
35# Sequence[np.ndarray] to ensure the method has the correct type signature in
36# that case. It is checked for using `is`, so it won't have a false positive
37# if the user provides a different (np.array([]),) value.
38RaiseTypeErrorIfNotProvided = (np.array([]),)
39
40
41TDefault = TypeVar('TDefault')
42
43
44class SupportsKraus(Protocol):
45    """An object that may be describable as a quantum channel."""
46
47    @doc_private
48    def _kraus_(self) -> Union[Sequence[np.ndarray], NotImplementedType]:
49        r"""A list of Kraus matrices describing the quantum channel.
50
51        These matrices are the terms in the operator sum representation of a
52        quantum channel. If the returned matrices are ${A_0,A_1,..., A_{r-1}}$,
53        then this describes the channel:
54            $$
55            \rho \rightarrow \sum_{k=0}^{r-1} A_k \rho A_k^\dagger
56            $$
57        These matrices are required to satisfy the trace preserving condition
58            $$
59            \sum_{k=0}^{r-1} A_k^\dagger A_k = I
60            $$
61        where $I$ is the identity matrix. The matrices $A_k$ are sometimes
62        called Kraus or noise operators.
63
64        This method is used by the global `cirq.channel` method. If this method
65        or the _unitary_ method is not present, or returns NotImplement,
66        it is assumed that the receiving object doesn't have a channel
67        (resulting in a TypeError or default result when calling `cirq.channel`
68        on it). (The ability to return NotImplemented is useful when a class
69        cannot know if it is a channel until runtime.)
70
71        The order of cells in the matrices is always implicit with respect to
72        the object being called. For example, for GateOperations these matrices
73        must be ordered with respect to the list of qubits that the channel is
74        applied to. The qubit-to-amplitude order mapping matches the
75        ordering of numpy.kron(A, B), where A is a qubit earlier in the list
76        than the qubit B.
77
78        Returns:
79            A list of matrices describing the channel (Kraus operators), or
80            NotImplemented if there is no such matrix.
81        """
82
83    @doc_private
84    def _has_kraus_(self) -> bool:
85        """Whether this value has a Kraus representation.
86
87        This method is used by the global `cirq.has_channel` method.  If this
88        method is not present, or returns NotImplemented, it will fallback
89        to similarly checking `cirq.has_mixture` or `cirq.has_unitary`. If none
90        of these are present or return NotImplemented, then `cirq.has_channel`
91        will fall back to checking whether `cirq.channel` has a non-default
92        value. Otherwise `cirq.has_channel` returns False.
93
94        Returns:
95            True if the value has a channel representation, False otherwise.
96        """
97
98
99def kraus(
100    val: Any, default: Any = RaiseTypeErrorIfNotProvided
101) -> Union[Tuple[np.ndarray, ...], TDefault]:
102    r"""Returns a list of matrices describing the channel for the given value.
103
104    These matrices are the terms in the operator sum representation of
105    a quantum channel. If the returned matrices are ${A_0,A_1,..., A_{r-1}}$,
106    then this describes the channel:
107        $$
108        \rho \rightarrow \sum_{k=0}^{r-1} A_k \rho A_k^\dagger
109        $$
110    These matrices are required to satisfy the trace preserving condition
111        $$
112        \sum_{k=0}^{r-1} A_k^\dagger A_k = I
113        $$
114    where $I$ is the identity matrix. The matrices $A_k$ are sometimes called
115    Kraus or noise operators.
116
117    Args:
118        val: The value to describe by a channel.
119        default: Determines the fallback behavior when `val` doesn't have
120            a channel. If `default` is not set, a TypeError is raised. If
121            default is set to a value, that value is returned.
122
123    Returns:
124        If `val` has a `_kraus_` method and its result is not NotImplemented,
125        that result is returned. Otherwise, if `val` has a `_mixture_` method
126        and its results is not NotImplement a tuple made up of channel
127        corresponding to that mixture being a probabilistic mixture of unitaries
128        is returned.  Otherwise, if `val` has a `_unitary_` method and
129        its result is not NotImplemented a tuple made up of that result is
130        returned. Otherwise, if a default value was specified, the default
131        value is returned.
132
133    Raises:
134        TypeError: `val` doesn't have a _kraus_ or _unitary_ method (or that
135            method returned NotImplemented) and also no default value was
136            specified.
137    """
138    channel_getter = getattr(val, '_channel_', None)
139    if channel_getter is not None:
140        warnings.warn(
141            '_channel_ is deprecated and will be removed in cirq 0.13, rename to _kraus_',
142            DeprecationWarning,
143        )
144
145    kraus_getter = getattr(val, '_kraus_', None)
146    kraus_result = NotImplemented if kraus_getter is None else kraus_getter()
147    if kraus_result is not NotImplemented:
148        return tuple(kraus_result)
149
150    mixture_getter = getattr(val, '_mixture_', None)
151    mixture_result = NotImplemented if mixture_getter is None else mixture_getter()
152    if mixture_result is not NotImplemented and mixture_result is not None:
153        return tuple(np.sqrt(p) * u for p, u in mixture_result)
154
155    unitary_getter = getattr(val, '_unitary_', None)
156    unitary_result = NotImplemented if unitary_getter is None else unitary_getter()
157    if unitary_result is not NotImplemented and unitary_result is not None:
158        return (unitary_result,)
159
160    channel_result = NotImplemented if channel_getter is None else channel_getter()
161    if channel_result is not NotImplemented:
162        return tuple(channel_result)
163
164    if default is not RaiseTypeErrorIfNotProvided:
165        return default
166
167    if kraus_getter is None and unitary_getter is None and mixture_getter is None:
168        raise TypeError(
169            "object of type '{}' has no _kraus_ or _mixture_ or "
170            "_unitary_ method.".format(type(val))
171        )
172
173    raise TypeError(
174        "object of type '{}' does have a _kraus_, _mixture_ or "
175        "_unitary_ method, but it returned NotImplemented.".format(type(val))
176    )
177
178
179def has_kraus(val: Any, *, allow_decompose: bool = True) -> bool:
180    """Returns whether the value has a Kraus representation.
181
182    Args:
183        val: The value to check.
184        allow_decompose: Used by internal methods to stop redundant
185            decompositions from being performed (e.g. there's no need to
186            decompose an object to check if it is unitary as part of determining
187            if the object is a quantum channel, when the quantum channel check
188            will already be doing a more general decomposition check). Defaults
189            to True. When False, the decomposition strategy for determining
190            the result is skipped.
191
192    Returns:
193        If `val` has a `_has_kraus_` method and its result is not
194        NotImplemented, that result is returned. Otherwise, if `val` has a
195        `_has_mixture_` method and its result is not NotImplemented, that
196        result is returned. Otherwise if `val` has a `_has_unitary_` method
197        and its results is not NotImplemented, that result is returned.
198        Otherwise, if the value has a _kraus_ method return if that
199        has a non-default value. Returns False if none of these functions
200        exists.
201    """
202    kraus_getter = getattr(val, '_has_kraus_', None)
203    result = NotImplemented if kraus_getter is None else kraus_getter()
204    if result is not NotImplemented:
205        return result
206
207    result = has_mixture(val, allow_decompose=False)
208    if result is not NotImplemented and result:
209        return result
210
211    if allow_decompose:
212        operations, _, _ = _try_decompose_into_operations_and_qubits(val)
213        if operations is not None:
214            return all(has_kraus(val) for val in operations)
215
216    # No has methods, use `_kraus_` or delegates instead.
217    return kraus(val, None) is not None
218