Implement application of a gate to a state
Joseph Weston authored on 15/11/2019 20:52:38... | ... |
@@ -7,6 +7,7 @@ matrix written in the computational basis. |
7 | 7 |
import numpy as np |
8 | 8 |
|
9 | 9 |
__all__ = [ |
10 |
+ "apply", |
|
10 | 11 |
"n_qubits", |
11 | 12 |
"controlled", |
12 | 13 |
# -- Single qubit gates -- |
... | ... |
@@ -28,6 +29,38 @@ __all__ = [ |
28 | 29 |
] # type: ignore |
29 | 30 |
|
30 | 31 |
|
32 |
+def apply(gate, qubits, state): |
|
33 |
+ n_gate_qubits = gate.shape[0].bit_length() - 1 |
|
34 |
+ n_state_qubits = state.shape[0].bit_length() - 1 |
|
35 |
+ assert len(qubits) == n_gate_qubits |
|
36 |
+ |
|
37 |
+ # We can view an n-qubit gate as a 2*n-tensor (n contravariant and n contravariant |
|
38 |
+ # indices) and an n-qubit state as an n-tensor (contravariant indices) |
|
39 |
+ # with each axis having length 2 (the state space of a single qubit). |
|
40 |
+ gate = gate.reshape((2,) * 2 * n_gate_qubits) |
|
41 |
+ state = state.reshape((2,) * n_state_qubits) |
|
42 |
+ |
|
43 |
+ # Our qubits are labeled from least significant to most significant, i.e. our |
|
44 |
+ # computational basis (for e.g. 2 qubits) is ordered like |00⟩, |01⟩, |10⟩, |11⟩. |
|
45 |
+ # We represent the state as a tensor in *row-major* order; this means that the |
|
46 |
+ # axis ordering is *backwards* compared to the qubit ordering (the least significant |
|
47 |
+ # qubit corresponds to the *last* axis in the tensor etc.) |
|
48 |
+ qubit_axes = tuple(n_state_qubits - 1 - np.asarray(qubits)) |
|
49 |
+ |
|
50 |
+ # Applying the gate to the state vector is then the tensor product over the appropriate axes |
|
51 |
+ axes = (np.arange(n_gate_qubits, 2 * n_gate_qubits), qubit_axes) |
|
52 |
+ new_state = np.tensordot(gate, state, axes=axes) |
|
53 |
+ |
|
54 |
+ # tensordot effectively re-orders the qubits such that the qubits we operated |
|
55 |
+ # on are in the most-significant positions (i.e. their axes come first). We |
|
56 |
+ # thus need to transpose the axes to place them back into their original positions. |
|
57 |
+ untouched_axes = tuple( |
|
58 |
+ idx for idx in range(n_state_qubits) if idx not in qubit_axes |
|
59 |
+ ) |
|
60 |
+ inverse_permutation = np.argsort(qubit_axes + untouched_axes) |
|
61 |
+ return np.transpose(new_state, inverse_permutation).reshape(-1) |
|
62 |
+ |
|
63 |
+ |
|
31 | 64 |
def _check_valid_gate(gate): |
32 | 65 |
if not ( |
33 | 66 |
# is an array |
... | ... |
@@ -1,3 +1,5 @@ |
1 |
+from functools import reduce |
|
2 |
+ |
|
1 | 3 |
from hypothesis import given |
2 | 4 |
import hypothesis.strategies as st |
3 | 5 |
import hypothesis.extra.numpy as hnp |
... | ... |
@@ -13,6 +15,17 @@ import qsim.gate |
13 | 15 |
n_qubits = st.shared(st.integers(min_value=1, max_value=6)) |
14 | 16 |
|
15 | 17 |
|
18 |
+# Choose which qubits from 'n_qubits' to operate on with a gate that |
|
19 |
+# operates on 'gate_size' qubits |
|
20 |
+def select_n_qubits(gate_size): |
|
21 |
+ def _strat(n_qubits): |
|
22 |
+ assert n_qubits >= gate_size |
|
23 |
+ possible_qubits = st.integers(0, n_qubits - 1) |
|
24 |
+ return st.lists(possible_qubits, gate_size, gate_size, unique=True).map(tuple) |
|
25 |
+ |
|
26 |
+ return _strat |
|
27 |
+ |
|
28 |
+ |
|
16 | 29 |
valid_complex = st.complex_numbers(allow_infinity=False, allow_nan=False) |
17 | 30 |
phases = st.floats( |
18 | 31 |
min_value=0, max_value=2 * np.pi, allow_nan=False, allow_infinity=False |
... | ... |
@@ -28,10 +41,30 @@ def unitary(n_qubits): |
28 | 41 |
) |
29 | 42 |
|
30 | 43 |
|
44 |
+def ket(n_qubits): |
|
45 |
+ size = 1 << n_qubits |
|
46 |
+ return ( |
|
47 |
+ hnp.arrays(complex, (size,), valid_complex) |
|
48 |
+ .filter(lambda v: np.linalg.norm(v) > 0) # vectors must be normalizable |
|
49 |
+ .map(lambda v: v / np.linalg.norm(v)) |
|
50 |
+ ) |
|
51 |
+ |
|
52 |
+ |
|
31 | 53 |
single_qubit_gates = unitary(1) |
32 | 54 |
two_qubit_gates = unitary(2) |
33 | 55 |
n_qubit_gates = n_qubits.flatmap(unitary) |
34 | 56 |
|
57 |
+# Projectors on the single qubit computational basis |
|
58 |
+project_zero = np.array([[1, 0], [0, 0]]) |
|
59 |
+project_one = np.array([[0, 0], [0, 1]]) |
|
60 |
+ |
|
61 |
+ |
|
62 |
+def product_gate(single_qubit_gates): |
|
63 |
+ # We reverse so that 'single_qubit_gates' can be indexed by the qubit |
|
64 |
+ # identifier; e.g. qubit #0 is actually the least-significant qubit |
|
65 |
+ return reduce(np.kron, reversed(single_qubit_gates)) |
|
66 |
+ |
|
67 |
+ |
|
35 | 68 |
# -- Tests -- |
36 | 69 |
|
37 | 70 |
|
... | ... |
@@ -111,3 +144,42 @@ def test_deutch(): |
111 | 144 |
|
112 | 145 |
def test_swap(): |
113 | 146 |
assert np.all(qsim.gate.swap @ qsim.gate.swap == np.identity(4)) |
147 |
+ |
|
148 |
+ |
|
149 |
+@given(single_qubit_gates, n_qubits.flatmap(ket), n_qubits.flatmap(select_n_qubits(1))) |
|
150 |
+def test_applying_single_gates(gate, state, selected): |
|
151 |
+ qubit, = selected |
|
152 |
+ n_qubits = state.shape[0].bit_length() - 1 |
|
153 |
+ parts = [np.identity(2)] * n_qubits |
|
154 |
+ parts[qubit] = gate |
|
155 |
+ big_gate = product_gate(parts) |
|
156 |
+ |
|
157 |
+ should_be = big_gate @ state |
|
158 |
+ state = qsim.gate.apply(gate, [qubit], state) |
|
159 |
+ |
|
160 |
+ assert np.allclose(state, should_be) |
|
161 |
+ |
|
162 |
+ |
|
163 |
+@given( |
|
164 |
+ single_qubit_gates, |
|
165 |
+ n_qubits.filter(lambda n: n > 1).flatmap(ket), |
|
166 |
+ n_qubits.filter(lambda n: n > 1).flatmap(select_n_qubits(2)), |
|
167 |
+) |
|
168 |
+def test_applying_controlled_single_qubit_gates(gate, state, selected): |
|
169 |
+ control, qubit = selected |
|
170 |
+ n_qubits = state.shape[0].bit_length() - 1 |
|
171 |
+ # When control qubit is |0⟩ the controlled gate acts like the identity on the other qubit |
|
172 |
+ parts_zero = [np.identity(2)] * n_qubits |
|
173 |
+ parts_zero[control] = project_zero |
|
174 |
+ parts_zero[qubit] = np.identity(2) |
|
175 |
+ # When control qubit is |1⟩ the controlled gate acts like the original gate on the other qubit |
|
176 |
+ parts_one = [np.identity(2)] * n_qubits |
|
177 |
+ parts_one[control] = project_one |
|
178 |
+ parts_one[qubit] = gate |
|
179 |
+ # The total controlled gate is then the sum of these 2 product gates |
|
180 |
+ big_gate = product_gate(parts_zero) + product_gate(parts_one) |
|
181 |
+ |
|
182 |
+ should_be = big_gate @ state |
|
183 |
+ state = qsim.gate.apply(qsim.gate.controlled(gate), [control, qubit], state) |
|
184 |
+ |
|
185 |
+ assert np.allclose(state, should_be) |