/
combinators.py
169 lines (125 loc) · 5.13 KB
/
combinators.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# Copyright 2018 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility methods for combining matrices."""
import functools
from typing import Union, TYPE_CHECKING
import numpy as np
from cirq._doc import document
if TYPE_CHECKING:
from numpy.typing import DTypeLike, ArrayLike
def kron(*factors: Union[np.ndarray, complex, float], shape_len: int = 2) -> np.ndarray:
"""Computes the kronecker product of a sequence of values.
A *args version of lambda args: functools.reduce(np.kron, args).
Args:
*factors: The matrices, tensors, and/or scalars to combine together
using np.kron.
shape_len: The expected number of dimensions in the output. Mainly
determines the behavior of the empty kron product.
Returns:
The kronecker product of all the inputs.
"""
product = np.ones(shape=(1,) * shape_len)
for m in factors:
product = np.kron(product, m)
return np.array(product)
CONTROL_TAG = np.array([[float('nan'), 0], [0, 1]])
document(
CONTROL_TAG,
"""A special indicator value for `cirq.kron_with_controls`.
This value is a stand-in for "control operations on the other qubits based
on the value of this qubit", which otherwise doesn't have a proper matrix.
""",
)
def kron_with_controls(*factors: Union[np.ndarray, complex, float]) -> np.ndarray:
"""Computes the kronecker product of a sequence of values and control tags.
Use `cirq.CONTROL_TAG` to represent controls. Any entry of the output
corresponding to a situation where the control is not satisfied will
be overwritten by identity matrix elements.
The control logic works by imbuing NaN with the meaning "failed to meet one
or more controls". The normal kronecker product then spreads the per-item
NaNs to all the entries in the product that need to be replaced by identity
matrix elements. This method rewrites those NaNs. Thus CONTROL_TAG can be
the matrix [[NaN, 0], [0, 1]] or equivalently [[NaN, NaN], [NaN, 1]].
Because this method re-interprets NaNs as control-failed elements, it won't
propagate error-indicating NaNs from its input to its output in the way
you'd otherwise expect.
Examples:
```
result = cirq.kron_with_controls(
cirq.CONTROL_TAG,
cirq.unitary(cirq.X))
print(result.astype(np.int32))
# prints:
# [[1 0 0 0]
# [0 1 0 0]
# [0 0 0 1]
# [0 0 1 0]]
```
Args:
*factors: The matrices, tensors, scalars, and/or control tags to combine
together using np.kron.
Returns:
The resulting matrix.
"""
product = kron(*factors)
# The NaN from CONTROL_TAG spreads to everywhere identity belongs.
for i in range(product.shape[0]):
for j in range(product.shape[1]):
if np.isnan(product[i, j]):
product[i, j] = 1 if i == j else 0
return product
def dot(*values: 'ArrayLike') -> np.ndarray:
"""Computes the dot/matrix product of a sequence of values.
Performs the computation in serial order without regard to the matrix
sizes. If you are using this for matrices of large and differing sizes,
consider using np.lingalg.multi_dot for better performance.
Args:
*values: The values to combine with the dot/matrix product.
Returns:
The resulting value or matrix.
"""
if len(values) == 0:
raise ValueError("cirq.dot must be called with arguments")
if len(values) == 1:
# note: it's important that we copy input arrays.
return np.array(values[0])
result = np.asarray(values[0])
for value in values[1:]:
result = np.dot(result, value)
return result
def _merge_dtypes(dtype1: 'DTypeLike', dtype2: 'DTypeLike') -> np.dtype:
return (np.zeros(0, dtype1) + np.zeros(0, dtype2)).dtype
def block_diag(*blocks: np.ndarray) -> np.ndarray:
"""Concatenates blocks into a block diagonal matrix.
Args:
*blocks: Square matrices to place along the diagonal of the result.
Returns:
A block diagonal matrix with the given blocks along its diagonal.
Raises:
ValueError: A block isn't square.
"""
for b in blocks:
if b.shape[0] != b.shape[1]:
raise ValueError('Blocks must be square.')
if not blocks:
return np.zeros((0, 0), dtype=np.complex128)
n = sum(b.shape[0] for b in blocks)
dtype = functools.reduce(_merge_dtypes, (b.dtype for b in blocks))
result = np.zeros(shape=(n, n), dtype=dtype)
i = 0
for b in blocks:
j = i + b.shape[0]
result[i:j, i:j] = b
i = j
return result