Browse code

Refactor common testing components

Joseph Weston authored on 01/11/2023 04:35:17
Showing 1 changed files
... ...
@@ -2,74 +2,30 @@ from functools import reduce
2 2
 
3 3
 from hypothesis import given
4 4
 import hypothesis.strategies as st
5
-import hypothesis.extra.numpy as hnp
6 5
 import numpy as np
7 6
 import pytest
8 7
 
9 8
 import qsim.gate
10 9
 
11
-
12
-# -- Strategies for generating values --
13
-
14
-
15
-n_qubits = st.shared(st.integers(min_value=1, max_value=6))
16
-
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(
25
-            possible_qubits, min_size=gate_size, max_size=gate_size, unique=True
26
-        ).map(tuple)
27
-
28
-    return _strat
29
-
30
-
31
-valid_complex = st.complex_numbers(
32
-    max_magnitude=1e10, allow_infinity=False, allow_nan=False
33
-)
34
-phases = st.floats(
35
-    min_value=0, max_value=2 * np.pi, allow_nan=False, allow_infinity=False
10
+from .common import (
11
+    ket,
12
+    single_qubit_gates,
13
+    n_qubit_gates,
14
+    valid_states,
15
+    n_qubits,
16
+    phases,
17
+    select_n_qubits,
36 18
 )
37 19
 
38 20
 
39
-def unitary(n_qubits):
40
-    size = 1 << n_qubits
41
-    return (
42
-        hnp.arrays(complex, (size, size), elements=valid_complex)
43
-        .map(lambda a: np.linalg.qr(a)[0])
44
-        .filter(lambda u: np.all(np.isfinite(u)))
45
-    )
46
-
47
-
48
-def ket(n_qubits):
49
-    size = 1 << n_qubits
50
-    return (
51
-        hnp.arrays(complex, (size,), elements=valid_complex)
52
-        .filter(lambda v: np.linalg.norm(v) > 0)  # vectors must be normalizable
53
-        .map(lambda v: v / np.linalg.norm(v))
54
-    )
55
-
56
-
57
-single_qubit_gates = unitary(1)
58
-two_qubit_gates = unitary(2)
59
-n_qubit_gates = n_qubits.flatmap(unitary)
60
-
61
-# Projectors on the single qubit computational basis
62
-project_zero = np.array([[1, 0], [0, 0]])
63
-project_one = np.array([[0, 0], [0, 1]])
64
-
65
-
66 21
 def product_gate(single_qubit_gates):
67 22
     # We reverse so that 'single_qubit_gates' can be indexed by the qubit
68 23
     # identifier; e.g. qubit #0 is actually the least-significant qubit
69 24
     return reduce(np.kron, reversed(single_qubit_gates))
70 25
 
71 26
 
72
-# -- Tests --
27
+project_zero = np.array([[1, 0], [0, 0]])
28
+project_one = np.array([[0, 0], [0, 1]])
73 29
 
74 30
 
75 31
 @given(n_qubits, n_qubit_gates)
... ...
@@ -145,7 +101,7 @@ def test_swap():
145 101
     assert np.all(qsim.gate.swap @ qsim.gate.swap == np.identity(4))
146 102
 
147 103
 
148
-@given(single_qubit_gates, n_qubits.flatmap(ket), n_qubits.flatmap(select_n_qubits(1)))
104
+@given(single_qubit_gates, valid_states, n_qubits.flatmap(select_n_qubits(1)))
149 105
 def test_applying_single_gates(gate, state, selected):
150 106
     (qubit,) = selected
151 107
     n_qubits = state.shape[0].bit_length() - 1
... ...
@@ -167,11 +123,13 @@ def test_applying_single_gates(gate, state, selected):
167 123
 def test_applying_controlled_single_qubit_gates(gate, state, selected):
168 124
     control, qubit = selected
169 125
     n_qubits = state.shape[0].bit_length() - 1
170
-    # When control qubit is |0⟩ the controlled gate acts like the identity on the other qubit
126
+    # When control qubit is |0⟩ the controlled gate acts
127
+    # like the identity on the other qubit
171 128
     parts_zero = [np.identity(2)] * n_qubits
172 129
     parts_zero[control] = project_zero
173 130
     parts_zero[qubit] = np.identity(2)
174
-    # When control qubit is |1⟩ the controlled gate acts like the original gate on the other qubit
131
+    # When control qubit is |1⟩ the controlled gate acts
132
+    # like the original gate on the other qubit
175 133
     parts_one = [np.identity(2)] * n_qubits
176 134
     parts_one[control] = project_one
177 135
     parts_one[qubit] = gate
Browse code

Refactor gate module into operator module

This will be necessary for defining measurements also

Joseph Weston authored on 15/09/2021 17:22:20
Showing 1 changed files
... ...
@@ -28,7 +28,9 @@ def select_n_qubits(gate_size):
28 28
     return _strat
29 29
 
30 30
 
31
-valid_complex = st.complex_numbers(allow_infinity=False, allow_nan=False)
31
+valid_complex = st.complex_numbers(
32
+    max_magnitude=1e10, allow_infinity=False, allow_nan=False
33
+)
32 34
 phases = st.floats(
33 35
     min_value=0, max_value=2 * np.pi, allow_nan=False, allow_infinity=False
34 36
 )
... ...
@@ -89,11 +91,6 @@ def test_n_qubits_invalid(gate):
89 91
     # Not size 2**n, n > 0
90 92
     with pytest.raises(ValueError):
91 93
         qsim.gate.n_qubits(gate[:-1, :-1])
92
-    # Not unitary
93
-    nonunitary_part = np.zeros_like(gate)
94
-    nonunitary_part[0, -1] = 1j
95
-    with pytest.raises(ValueError):
96
-        qsim.gate.n_qubits(gate + nonunitary_part)
97 94
 
98 95
 
99 96
 @given(n_qubits, n_qubit_gates)
Browse code

Upgrade requirements to Python 3.8

Joseph Weston authored on 13/09/2021 02:02:38
Showing 1 changed files
... ...
@@ -21,7 +21,9 @@ def select_n_qubits(gate_size):
21 21
     def _strat(n_qubits):
22 22
         assert n_qubits >= gate_size
23 23
         possible_qubits = st.integers(0, n_qubits - 1)
24
-        return st.lists(possible_qubits, gate_size, gate_size, unique=True).map(tuple)
24
+        return st.lists(
25
+            possible_qubits, min_size=gate_size, max_size=gate_size, unique=True
26
+        ).map(tuple)
25 27
 
26 28
     return _strat
27 29
 
... ...
@@ -35,7 +37,7 @@ phases = st.floats(
35 37
 def unitary(n_qubits):
36 38
     size = 1 << n_qubits
37 39
     return (
38
-        hnp.arrays(complex, (size, size), valid_complex)
40
+        hnp.arrays(complex, (size, size), elements=valid_complex)
39 41
         .map(lambda a: np.linalg.qr(a)[0])
40 42
         .filter(lambda u: np.all(np.isfinite(u)))
41 43
     )
... ...
@@ -44,7 +46,7 @@ def unitary(n_qubits):
44 46
 def ket(n_qubits):
45 47
     size = 1 << n_qubits
46 48
     return (
47
-        hnp.arrays(complex, (size,), valid_complex)
49
+        hnp.arrays(complex, (size,), elements=valid_complex)
48 50
         .filter(lambda v: np.linalg.norm(v) > 0)  # vectors must be normalizable
49 51
         .map(lambda v: v / np.linalg.norm(v))
50 52
     )
... ...
@@ -148,7 +150,7 @@ def test_swap():
148 150
 
149 151
 @given(single_qubit_gates, n_qubits.flatmap(ket), n_qubits.flatmap(select_n_qubits(1)))
150 152
 def test_applying_single_gates(gate, state, selected):
151
-    qubit, = selected
153
+    (qubit,) = selected
152 154
     n_qubits = state.shape[0].bit_length() - 1
153 155
     parts = [np.identity(2)] * n_qubits
154 156
     parts[qubit] = gate
Browse code

Add tests for applying gates to a state

For the moment we only test single-qubit and controlled single-qubit
gates, as these gates can be easily expressed on the full Hilbert
space. A single qubit gate is just a tensor product, and a
controlled single-qubit gate is the sum of 2 tensor products.

The only real room for error when implementing gate application
will be in the ordering of the qubits, and these two tests are
already sensitive enough to catch this. Nevertheless it will
be useful later to add more tests, probably by building larger
gates by combining single-qubit and controlled single-qubit gates.

Joseph Weston authored on 15/11/2019 19:50:51
Showing 1 changed files
... ...
@@ -144,3 +144,42 @@ def test_deutch():
144 144
 
145 145
 def test_swap():
146 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)
Browse code

Add a function for constructing an n-qubit gate from a tensor product of 1-qubit gates

Joseph Weston authored on 15/11/2019 19:12:40
Showing 1 changed files
... ...
@@ -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
... ...
@@ -57,6 +59,12 @@ project_zero = np.array([[1, 0], [0, 0]])
57 59
 project_one = np.array([[0, 0], [0, 1]])
58 60
 
59 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
+
60 68
 # -- Tests --
61 69
 
62 70
 
Browse code

Add projectors onto the |0⟩ and |1⟩ states

These will be needed when explicitly constructing controlled gates.

Joseph Weston authored on 15/11/2019 19:00:28
Showing 1 changed files
... ...
@@ -52,6 +52,11 @@ single_qubit_gates = unitary(1)
52 52
 two_qubit_gates = unitary(2)
53 53
 n_qubit_gates = n_qubits.flatmap(unitary)
54 54
 
55
+# Projectors on the single qubit computational basis
56
+project_zero = np.array([[1, 0], [0, 0]])
57
+project_one = np.array([[0, 0], [0, 1]])
58
+
59
+
55 60
 # -- Tests --
56 61
 
57 62
 
Browse code

Add a strategy for selecting qubits to be operated on by a gate

Joseph Weston authored on 15/11/2019 18:55:17
Showing 1 changed files
... ...
@@ -13,6 +13,17 @@ import qsim.gate
13 13
 n_qubits = st.shared(st.integers(min_value=1, max_value=6))
14 14
 
15 15
 
16
+# Choose which qubits from 'n_qubits' to operate on with a gate that
17
+# operates on 'gate_size' qubits
18
+def select_n_qubits(gate_size):
19
+    def _strat(n_qubits):
20
+        assert n_qubits >= gate_size
21
+        possible_qubits = st.integers(0, n_qubits - 1)
22
+        return st.lists(possible_qubits, gate_size, gate_size, unique=True).map(tuple)
23
+
24
+    return _strat
25
+
26
+
16 27
 valid_complex = st.complex_numbers(allow_infinity=False, allow_nan=False)
17 28
 phases = st.floats(
18 29
     min_value=0, max_value=2 * np.pi, allow_nan=False, allow_infinity=False
Browse code

Add a function for constructing an arbitrary state vector

Joseph Weston authored on 15/11/2019 18:34:36
Showing 1 changed files
... ...
@@ -28,6 +28,15 @@ def unitary(n_qubits):
28 28
     )
29 29
 
30 30
 
31
+def ket(n_qubits):
32
+    size = 1 << n_qubits
33
+    return (
34
+        hnp.arrays(complex, (size,), valid_complex)
35
+        .filter(lambda v: np.linalg.norm(v) > 0)  # vectors must be normalizable
36
+        .map(lambda v: v / np.linalg.norm(v))
37
+    )
38
+
39
+
31 40
 single_qubit_gates = unitary(1)
32 41
 two_qubit_gates = unitary(2)
33 42
 n_qubit_gates = n_qubits.flatmap(unitary)
Browse code

Make related strategies closer together in 'test_gate.py'

This is a pure refactoring

Joseph Weston authored on 15/11/2019 18:26:27
Showing 1 changed files
... ...
@@ -7,22 +7,32 @@ import pytest
7 7
 import qsim.gate
8 8
 
9 9
 
10
-def unitary(n):
11
-    valid_complex = st.complex_numbers(allow_infinity=False, allow_nan=False)
12
-    return (
13
-        hnp.arrays(complex, (n, n), valid_complex)
14
-        .map(lambda a: np.linalg.qr(a)[0])
15
-        .filter(lambda u: np.all(np.isfinite(u)))
16
-    )
10
+# -- Strategies for generating values --
17 11
 
18 12
 
19 13
 n_qubits = st.shared(st.integers(min_value=1, max_value=6))
14
+
15
+
16
+valid_complex = st.complex_numbers(allow_infinity=False, allow_nan=False)
20 17
 phases = st.floats(
21 18
     min_value=0, max_value=2 * np.pi, allow_nan=False, allow_infinity=False
22 19
 )
23
-single_qubit_gates = unitary(2)
24
-two_qubit_gates = unitary(4)
25
-n_qubit_gates = n_qubits.map(lambda n: 2 ** n).flatmap(unitary)
20
+
21
+
22
+def unitary(n_qubits):
23
+    size = 1 << n_qubits
24
+    return (
25
+        hnp.arrays(complex, (size, size), valid_complex)
26
+        .map(lambda a: np.linalg.qr(a)[0])
27
+        .filter(lambda u: np.all(np.isfinite(u)))
28
+    )
29
+
30
+
31
+single_qubit_gates = unitary(1)
32
+two_qubit_gates = unitary(2)
33
+n_qubit_gates = n_qubits.flatmap(unitary)
34
+
35
+# -- Tests --
26 36
 
27 37
 
28 38
 @given(n_qubits, n_qubit_gates)
Browse code

Replace one final instance of the power operator with a bitshift

This idiom is used in other places in the code, so we should
remain consistent.

Joseph Weston authored on 15/11/2019 18:03:41
Showing 1 changed files
... ...
@@ -53,7 +53,7 @@ def test_n_qubits_invalid(gate):
53 53
 
54 54
 @given(n_qubits, n_qubit_gates)
55 55
 def test_controlled(n, gate):
56
-    nq = 2 ** n
56
+    nq = 1 << n
57 57
     controlled_gate = qsim.gate.controlled(gate)
58 58
     assert controlled_gate.shape[0] == 2 * nq
59 59
     assert np.all(controlled_gate[:nq, :nq] == np.identity(nq))
Browse code

Add basic quantum gates

Joseph Weston authored on 10/11/2019 16:33:08
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,103 @@
1
+from hypothesis import given
2
+import hypothesis.strategies as st
3
+import hypothesis.extra.numpy as hnp
4
+import numpy as np
5
+import pytest
6
+
7
+import qsim.gate
8
+
9
+
10
+def unitary(n):
11
+    valid_complex = st.complex_numbers(allow_infinity=False, allow_nan=False)
12
+    return (
13
+        hnp.arrays(complex, (n, n), valid_complex)
14
+        .map(lambda a: np.linalg.qr(a)[0])
15
+        .filter(lambda u: np.all(np.isfinite(u)))
16
+    )
17
+
18
+
19
+n_qubits = st.shared(st.integers(min_value=1, max_value=6))
20
+phases = st.floats(
21
+    min_value=0, max_value=2 * np.pi, allow_nan=False, allow_infinity=False
22
+)
23
+single_qubit_gates = unitary(2)
24
+two_qubit_gates = unitary(4)
25
+n_qubit_gates = n_qubits.map(lambda n: 2 ** n).flatmap(unitary)
26
+
27
+
28
+@given(n_qubits, n_qubit_gates)
29
+def test_n_qubits(n, gate):
30
+    assert qsim.gate.n_qubits(gate) == n
31
+
32
+
33
+@given(n_qubit_gates)
34
+def test_n_qubits_invalid(gate):
35
+    # Not a numpy array
36
+    with pytest.raises(ValueError):
37
+        qsim.gate.n_qubits(list(map(list, gate)))
38
+    # Not complex
39
+    with pytest.raises(ValueError):
40
+        qsim.gate.n_qubits(gate.real)
41
+    # Not square
42
+    with pytest.raises(ValueError):
43
+        qsim.gate.n_qubits(gate[:-2])
44
+    # Not size 2**n, n > 0
45
+    with pytest.raises(ValueError):
46
+        qsim.gate.n_qubits(gate[:-1, :-1])
47
+    # Not unitary
48
+    nonunitary_part = np.zeros_like(gate)
49
+    nonunitary_part[0, -1] = 1j
50
+    with pytest.raises(ValueError):
51
+        qsim.gate.n_qubits(gate + nonunitary_part)
52
+
53
+
54
+@given(n_qubits, n_qubit_gates)
55
+def test_controlled(n, gate):
56
+    nq = 2 ** n
57
+    controlled_gate = qsim.gate.controlled(gate)
58
+    assert controlled_gate.shape[0] == 2 * nq
59
+    assert np.all(controlled_gate[:nq, :nq] == np.identity(nq))
60
+    assert np.all(controlled_gate[nq:, nq:] == gate)
61
+
62
+
63
+@given(phases)
64
+def test_phase_gate_inverse(phi):
65
+    assert np.allclose(
66
+        qsim.gate.phase_shift(phi) @ qsim.gate.phase_shift(-phi), np.identity(2)
67
+    )
68
+
69
+
70
+@given(phases, st.integers())
71
+def test_phase_gate_periodic(phi, n):
72
+    atol = np.finfo(complex).resolution * abs(n)
73
+    assert np.allclose(
74
+        qsim.gate.phase_shift(phi),
75
+        qsim.gate.phase_shift(phi + 2 * np.pi * n),
76
+        atol=atol,
77
+    )
78
+
79
+
80
+@given(single_qubit_gates)
81
+def test_id(gate):
82
+    assert np.all(qsim.gate.id @ gate == gate)
83
+    assert np.all(gate @ qsim.gate.id == gate)
84
+
85
+
86
+def test_pauli_gates_are_involutary():
87
+    pauli_gates = [qsim.gate.x, qsim.gate.y, qsim.gate.z]
88
+    assert np.all(qsim.gate.x == qsim.gate.not_)
89
+    for gate in pauli_gates:
90
+        assert np.all(gate @ gate == qsim.gate.id)
91
+    assert np.all(-1j * qsim.gate.x @ qsim.gate.y @ qsim.gate.z == qsim.gate.id)
92
+
93
+
94
+def test_sqrt_not():
95
+    assert np.all(qsim.gate.sqrt_not @ qsim.gate.sqrt_not == qsim.gate.not_)
96
+
97
+
98
+def test_deutch():
99
+    assert np.allclose(qsim.gate.deutsch(np.pi / 2), qsim.gate.toffoli)
100
+
101
+
102
+def test_swap():
103
+    assert np.all(qsim.gate.swap @ qsim.gate.swap == np.identity(4))