Browse code

Merge branch 'feature/gate-apply'

Implement application of a gate to a state

Joseph Weston authored on 15/11/2019 20:52:38
Showing 2 changed files
... ...
@@ -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)