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