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"""Utility methods for checking properties of matrices."""
15from typing import cast, List, Optional, Sequence, Union, Tuple
16
17import numpy as np
18
19from cirq.linalg import tolerance, transformations
20from cirq import value
21
22
23def is_diagonal(matrix: np.ndarray, *, atol: float = 1e-8) -> np.bool_:
24    """Determines if a matrix is a approximately diagonal.
25
26    A matrix is diagonal if i!=j implies m[i,j]==0.
27
28    Args:
29        matrix: The matrix to check.
30        atol: The per-matrix-entry absolute tolerance on equality.
31
32    Returns:
33        Whether the matrix is diagonal within the given tolerance.
34    """
35    matrix = np.copy(matrix)
36    for i in range(min(matrix.shape)):
37        matrix[i, i] = 0
38    return tolerance.all_near_zero(matrix, atol=atol)
39
40
41def is_hermitian(matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8) -> bool:
42    """Determines if a matrix is approximately Hermitian.
43
44    A matrix is Hermitian if it's square and equal to its adjoint.
45
46    Args:
47        matrix: The matrix to check.
48        rtol: The per-matrix-entry relative tolerance on equality.
49        atol: The per-matrix-entry absolute tolerance on equality.
50
51    Returns:
52        Whether the matrix is Hermitian within the given tolerance.
53    """
54    return matrix.shape[0] == matrix.shape[1] and np.allclose(
55        matrix, np.conj(matrix.T), rtol=rtol, atol=atol
56    )
57
58
59def is_orthogonal(matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8) -> bool:
60    """Determines if a matrix is approximately orthogonal.
61
62    A matrix is orthogonal if it's square and real and its transpose is its
63    inverse.
64
65    Args:
66        matrix: The matrix to check.
67        rtol: The per-matrix-entry relative tolerance on equality.
68        atol: The per-matrix-entry absolute tolerance on equality.
69
70    Returns:
71        Whether the matrix is orthogonal within the given tolerance.
72    """
73    return (
74        matrix.shape[0] == matrix.shape[1]
75        and np.all(np.imag(matrix) == 0).item()
76        and np.allclose(matrix.dot(matrix.T), np.eye(matrix.shape[0]), rtol=rtol, atol=atol)
77    )
78
79
80def is_special_orthogonal(matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8) -> bool:
81    """Determines if a matrix is approximately special orthogonal.
82
83    A matrix is special orthogonal if it is square and real and its transpose
84    is its inverse and its determinant is one.
85
86    Args:
87        matrix: The matrix to check.
88        rtol: The per-matrix-entry relative tolerance on equality.
89        atol: The per-matrix-entry absolute tolerance on equality.
90
91    Returns:
92        Whether the matrix is special orthogonal within the given tolerance.
93    """
94    return is_orthogonal(matrix, rtol=rtol, atol=atol) and (
95        matrix.shape[0] == 0 or np.allclose(np.linalg.det(matrix), 1, rtol=rtol, atol=atol)
96    )
97
98
99def is_unitary(matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8) -> bool:
100    """Determines if a matrix is approximately unitary.
101
102    A matrix is unitary if it's square and its adjoint is its inverse.
103
104    Args:
105        matrix: The matrix to check.
106        rtol: The per-matrix-entry relative tolerance on equality.
107        atol: The per-matrix-entry absolute tolerance on equality.
108
109    Returns:
110        Whether the matrix is unitary within the given tolerance.
111    """
112    return matrix.shape[0] == matrix.shape[1] and np.allclose(
113        matrix.dot(np.conj(matrix.T)), np.eye(matrix.shape[0]), rtol=rtol, atol=atol
114    )
115
116
117def is_special_unitary(matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8) -> bool:
118    """Determines if a matrix is approximately unitary with unit determinant.
119
120    A matrix is special-unitary if it is square and its adjoint is its inverse
121    and its determinant is one.
122
123    Args:
124        matrix: The matrix to check.
125        rtol: The per-matrix-entry relative tolerance on equality.
126        atol: The per-matrix-entry absolute tolerance on equality.
127    Returns:
128        Whether the matrix is unitary with unit determinant within the given
129        tolerance.
130    """
131    return is_unitary(matrix, rtol=rtol, atol=atol) and (
132        matrix.shape[0] == 0 or np.allclose(np.linalg.det(matrix), 1, rtol=rtol, atol=atol)
133    )
134
135
136def is_normal(matrix: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8) -> bool:
137    """Determines if a matrix is approximately normal.
138
139    A matrix is normal if it's square and commutes with its adjoint.
140
141    Args:
142        matrix: The matrix to check.
143        rtol: The per-matrix-entry relative tolerance on equality.
144        atol: The per-matrix-entry absolute tolerance on equality.
145
146    Returns:
147        Whether the matrix is normal within the given tolerance.
148    """
149    return matrix_commutes(matrix, matrix.T.conj(), rtol=rtol, atol=atol)
150
151
152def is_cptp(*, kraus_ops: Sequence[np.ndarray], rtol: float = 1e-5, atol: float = 1e-8):
153    """Determines if a channel is completely positive trace preserving (CPTP).
154
155    A channel composed of Kraus operators K[0:n] is a CPTP map if the sum of
156    the products `adjoint(K[i]) * K[i])` is equal to 1.
157
158    Args:
159        kraus_ops: The Kraus operators of the channel to check.
160        rtol: The relative tolerance on equality.
161        atol: The absolute tolerance on equality.
162    """
163    sum_ndarray = cast(np.ndarray, sum(matrix.T.conj() @ matrix for matrix in kraus_ops))
164    return np.allclose(sum_ndarray, np.eye(*sum_ndarray.shape), rtol=rtol, atol=atol)
165
166
167def matrix_commutes(
168    m1: np.ndarray, m2: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8
169) -> bool:
170    """Determines if two matrices approximately commute.
171
172    Two matrices A and B commute if they are square and have the same size and
173    AB = BA.
174
175    Args:
176        m1: One of the matrices.
177        m2: The other matrix.
178        rtol: The per-matrix-entry relative tolerance on equality.
179        atol: The per-matrix-entry absolute tolerance on equality.
180
181    Returns:
182        Whether the two matrices have compatible sizes and a commutator equal
183        to zero within tolerance.
184    """
185    return (
186        m1.shape[0] == m1.shape[1]
187        and m1.shape == m2.shape
188        and np.allclose(m1.dot(m2), m2.dot(m1), rtol=rtol, atol=atol)
189    )
190
191
192def allclose_up_to_global_phase(
193    a: np.ndarray,
194    b: np.ndarray,
195    *,
196    rtol: float = 1.0e-5,
197    atol: float = 1.0e-8,
198    equal_nan: bool = False,
199) -> bool:
200    """Determines if a ~= b * exp(i t) for some t.
201
202    Args:
203        a: A numpy array.
204        b: Another numpy array.
205        rtol: Relative error tolerance.
206        atol: Absolute error tolerance.
207        equal_nan: Whether or not NaN entries should be considered equal to
208            other NaN entries.
209    """
210
211    if a.shape != b.shape:
212        return False
213    a, b = transformations.match_global_phase(a, b)
214
215    # Should now be equivalent.
216    return np.allclose(a=a, b=b, rtol=rtol, atol=atol, equal_nan=equal_nan)
217
218
219# TODO(#3388) Add documentation for Raises.
220# pylint: disable=missing-raises-doc
221def slice_for_qubits_equal_to(
222    target_qubit_axes: Sequence[int],
223    little_endian_qureg_value: int = 0,
224    *,  # Forces keyword args.
225    big_endian_qureg_value: int = 0,
226    num_qubits: Optional[int] = None,
227    qid_shape: Optional[Tuple[int, ...]] = None,
228) -> Tuple[Union[slice, int, 'ellipsis'], ...]:
229    """Returns an index corresponding to a desired subset of an np.ndarray.
230
231    It is assumed that the np.ndarray's shape is of the form (2, 2, 2, ..., 2).
232
233    Example:
234        ```python
235        # A '4 qubit' tensor with values from 0 to 15.
236        r = np.array(range(16)).reshape((2,) * 4)
237
238        # We want to index into the subset where qubit #1 and qubit #3 are ON.
239        s = cirq.slice_for_qubits_equal_to([1, 3], 0b11)
240        print(s)
241        # (slice(None, None, None), 1, slice(None, None, None), 1, Ellipsis)
242
243        # Get that subset. It corresponds to numbers of the form 0b*1*1.
244        # where here '*' indicates any possible value.
245        print(r[s])
246        # [[ 5  7]
247        #  [13 15]]
248        ```
249
250    Args:
251        target_qubit_axes: The qubits that are specified by the index bits. All
252            other axes of the slice are unconstrained.
253        little_endian_qureg_value: An integer whose bits specify what value is
254            desired for of the target qubits. The integer is little endian
255            w.r.t. the target qubit axes, meaning the low bit of the integer
256            determines the desired value of the first targeted qubit, and so
257            forth with the k'th targeted qubit's value set to
258            bool(qureg_value & (1 << k)).
259        big_endian_qureg_value: Same as `little_endian_qureg_value` but big
260            endian w.r.t. to target qubit axes, meaning the low bit of the
261            integer dertemines the desired value of the last target qubit, and
262            so forth.  Specify exactly one of the `*_qureg_value` arguments.
263        num_qubits: If specified the slices will extend all the way up to
264            this number of qubits, otherwise if it is None, the final element
265            return will be Ellipsis. Optional and defaults to using Ellipsis.
266        qid_shape: The qid shape of the state vector being sliced.  Specify this
267            instead of `num_qubits` when using qids with dimension != 2.  The
268            qureg value is interpreted to store digits with corresponding bases
269            packed into an int.
270
271    Returns:
272        An index object that will slice out a mutable view of the desired subset
273        of a tensor.
274    """
275    qid_shape_specified = qid_shape is not None
276    if qid_shape is not None or num_qubits is not None:
277        if num_qubits is None:
278            num_qubits = len(cast(Tuple[int, ...], qid_shape))
279        elif qid_shape is None:
280            qid_shape = (2,) * num_qubits
281        if num_qubits != len(cast(Tuple[int, ...], qid_shape)):
282            raise ValueError('len(qid_shape) != num_qubits')
283    if little_endian_qureg_value and big_endian_qureg_value:
284        raise ValueError(
285            'Specify exactly one of the arguments little_endian_qureg_value '
286            'or big_endian_qureg_value.'
287        )
288    out_size_specified = num_qubits is not None
289    out_size = (
290        cast(int, num_qubits) if out_size_specified else max(target_qubit_axes, default=-1) + 1
291    )
292    result = cast(List[Union[slice, int, 'ellipsis']], [slice(None)] * out_size)
293    if not out_size_specified:
294        result.append(Ellipsis)
295    if qid_shape is None:
296        qid_shape = (2,) * out_size
297    target_shape = tuple(qid_shape[i] for i in target_qubit_axes)
298    if big_endian_qureg_value:
299        digits = value.big_endian_int_to_digits(big_endian_qureg_value, base=target_shape)
300    else:
301        if little_endian_qureg_value < 0 and not qid_shape_specified:
302            # Allow negative binary numbers
303            little_endian_qureg_value &= (1 << len(target_shape)) - 1
304        digits = value.big_endian_int_to_digits(little_endian_qureg_value, base=target_shape[::-1])[
305            ::-1
306        ]
307    for axis, digit in zip(target_qubit_axes, digits):
308        result[axis] = digit
309    return tuple(result)
310
311
312# pylint: enable=missing-raises-doc
313