Browse code

Fix some minor typos

Joseph Weston authored on 01/11/2023 04:33:23
Showing 1 changed files
... ...
@@ -38,7 +38,8 @@ def apply(op, qubits, state):
38 38
     # qubit corresponds to the *last* axis in the tensor etc.)
39 39
     qubit_axes = tuple(n_state_qubits - 1 - np.asarray(qubits))
40 40
 
41
-    # Applying the op to the state vector is then the tensor product over the appropriate axes
41
+    # Applying the op to the state vector is then the tensor product over the
42
+    # appropriate axes.
42 43
     axes = (np.arange(n_op_qubits, 2 * n_op_qubits), qubit_axes)
43 44
     new_state = np.tensordot(op, state, axes=axes)
44 45
 
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
1 1
new file mode 100644
... ...
@@ -0,0 +1,126 @@
1
+import numpy as np
2
+
3
+from .state import num_qubits
4
+
5
+__all__ = ["apply", "is_hermitian", "is_unitary", "is_valid", "n_qubits"]
6
+
7
+
8
+def apply(op, qubits, state):
9
+    """Apply an operator to the specified qubits of a state
10
+
11
+    Parameters
12
+    ----------
13
+    op: ndarray[complex]
14
+        The operator to apply.
15
+    qubits : sequence of int
16
+        The qubits on which to act. Qubit 0 is the least significant qubit.
17
+    state : ndarray[complex]
18
+
19
+    Returns
20
+    -------
21
+    new_state : ndarray[complex]
22
+    """
23
+    _check_apply(op, qubits, state)
24
+
25
+    n_op_qubits = n_qubits(op)
26
+    n_state_qubits = num_qubits(state)
27
+
28
+    # We can view an n-qubit op as a 2*n-tensor (n contravariant and n contravariant
29
+    # indices) and an n-qubit state as an n-tensor (contravariant indices)
30
+    # with each axis having length 2 (the state space of a single qubit).
31
+    op = op.reshape((2,) * 2 * n_op_qubits)
32
+    state = state.reshape((2,) * n_state_qubits)
33
+
34
+    # Our qubits are labeled from least significant to most significant, i.e. our
35
+    # computational basis (for e.g. 2 qubits) is ordered like |00⟩, |01⟩, |10⟩, |11⟩.
36
+    # We represent the state as a tensor in *row-major* order; this means that the
37
+    # axis ordering is *backwards* compared to the qubit ordering (the least significant
38
+    # qubit corresponds to the *last* axis in the tensor etc.)
39
+    qubit_axes = tuple(n_state_qubits - 1 - np.asarray(qubits))
40
+
41
+    # Applying the op to the state vector is then the tensor product over the appropriate axes
42
+    axes = (np.arange(n_op_qubits, 2 * n_op_qubits), qubit_axes)
43
+    new_state = np.tensordot(op, state, axes=axes)
44
+
45
+    # tensordot effectively re-orders the qubits such that the qubits we operated
46
+    # on are in the most-significant positions (i.e. their axes come first). We
47
+    # thus need to transpose the axes to place them back into their original positions.
48
+    untouched_axes = tuple(
49
+        idx for idx in range(n_state_qubits) if idx not in qubit_axes
50
+    )
51
+    inverse_permutation = np.argsort(qubit_axes + untouched_axes)
52
+    return np.transpose(new_state, inverse_permutation).reshape(-1)
53
+
54
+
55
+def _all_distinct(elements):
56
+    if not elements:
57
+        return True
58
+    elements = iter(elements)
59
+    fst = next(elements)
60
+    return all(fst != x for x in elements)
61
+
62
+
63
+def _check_apply(op, qubits, state):
64
+    if not _all_distinct(qubits):
65
+        raise ValueError("Cannot apply an operator to repeated qubits.")
66
+
67
+    n_op_qubits = n_qubits(op)
68
+    if n_op_qubits != len(qubits):
69
+        raise ValueError(
70
+            f"Cannot apply an {n_op_qubits}-qubit operator to {len(qubits)} qubits."
71
+        )
72
+
73
+    n_state_qubits = num_qubits(state)
74
+
75
+    if n_op_qubits > n_state_qubits:
76
+        raise ValueError(
77
+            f"Cannot apply an {n_op_qubits}-qubit operator "
78
+            f"to an {n_state_qubits}-qubit state."
79
+        )
80
+
81
+    invalid_qubits = [q for q in qubits if q >= n_state_qubits]
82
+    if invalid_qubits:
83
+        raise ValueError(
84
+            f"Cannot apply operator to qubits {invalid_qubits} "
85
+            f"of an {n_state_qubits}-qubit state."
86
+        )
87
+
88
+
89
+def is_hermitian(op: np.ndarray) -> bool:
90
+    """Return True if and only if 'op' is a valid Hermitian operator."""
91
+    return is_valid(op) and np.allclose(op, op.conj().T)
92
+
93
+
94
+def is_unitary(op: np.ndarray) -> bool:
95
+    """Return True if and only if 'op' is a valid unitary operator."""
96
+    return is_valid(op) and np.allclose(op @ op.conj().T, np.identity(op.shape[0]))
97
+
98
+
99
+def is_valid(op: np.ndarray) -> bool:
100
+    """Return True if and only if 'op' is a valid operator."""
101
+    return (
102
+        # is an array
103
+        isinstance(op, np.ndarray)
104
+        # is complex
105
+        and np.issubdtype(op.dtype, np.complex128)
106
+        # is square
107
+        and op.shape[0] == op.shape[1]
108
+        # has size 2**n, n > 1
109
+        and np.log2(op.shape[0]).is_integer()
110
+        and op.shape[0].bit_length() > 1
111
+    )
112
+
113
+
114
+def _check_valid_operator(op):
115
+    if not is_valid(op):
116
+        raise ValueError("Operator is invalid")
117
+
118
+
119
+def n_qubits(op):
120
+    """Return the number of qubits that the operator acts on.
121
+
122
+    Raises ValueError if 'o' does not have a shape that is
123
+    an integer power of 2.
124
+    """
125
+    _check_valid_operator(op)
126
+    return op.shape[0].bit_length() - 1