#### 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)`