Skip to content

logic

The quotonic.logic module includes functions required to conduct basic modeling of qubits and logic gate operations when resolved in the computational basis. It is by no means comprehensive, only including functionalities that are helpful to the other modules within quotonic (training_sets in particular).

build_comp_basis(n)

Generate the computational basis for a given number of qubits.

The array formed lists the possible combinations of qubit states (i.e. 0s and 1s) for a given number of qubits, \(n\). To understand the ordering, please see the examples below.

Parameters:

Name Type Description Default
n int

number of qubits, \(n\)

required

Returns:

Type Description
np_ndarray

\(N\times n\) array that catalogs all states in the \(N\)-dimensional computational basis

Examples:

>>> build_comp_basis(1)
array([[0],
       [1]])

>>> build_comp_basis(2)
array([[0, 0],
       [0, 1],
       [1, 0],
       [1, 1]])

>>> build_comp_basis(3)
array([[0, 0, 0],
       [0, 0, 1],
       [0, 1, 0],
       [0, 1, 1],
       [1, 0, 0],
       [1, 0, 1],
       [1, 1, 0],
       [1, 1, 1]])
Source code in src/quotonic/logic.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def build_comp_basis(n: int) -> np_ndarray:
    """Generate the computational basis for a given number of qubits.

    The array formed lists the possible combinations of qubit states (i.e. 0s and 1s) for a given number of qubits,
    $n$. To understand the ordering, please see the examples below.

    Args:
        n: number of qubits, $n$

    Returns:
        $N\\times n$ array that catalogs all states in the $N$-dimensional computational basis

    Examples:
        ```python
        >>> build_comp_basis(1)
        array([[0],
               [1]])

        >>> build_comp_basis(2)
        array([[0, 0],
               [0, 1],
               [1, 0],
               [1, 1]])

        >>> build_comp_basis(3)
        array([[0, 0, 0],
               [0, 0, 1],
               [0, 1, 0],
               [0, 1, 1],
               [1, 0, 0],
               [1, 0, 1],
               [1, 1, 0],
               [1, 1, 1]])
        ```
    """

    # compute the dimension of the computational basis
    N = 2**n

    # leverage the relation between computational basis states and binary to create the states
    basis = np.zeros((N, n), dtype=int)
    for i in range(N):
        basis[i] = np.array([int(j) for j in format(i, "0" + str(n) + "b")])

    return basis

H(n=1)

Generate the matrix representation of \(n\) Hadamard gates applied to \(n\) qubits individually.

Constructs and returns the matrix \(H^{\otimes n}\), where

\[ H = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix}. \]

Parameters:

Name Type Description Default
n int

number of qubits, \(n\)

1

Returns:

Type Description
np_ndarray

Matrix representation of a Hadamard gate, as a $2\times 2 array

Examples:

>>> H()
array([[ 0.70710678+0.j,  0.70710678+0.j],
       [ 0.70710678+0.j, -0.70710678+0.j]])
>>> H(n=2)
array([[ 0.5+0.j,  0.5+0.j,  0.5+0.j,  0.5+0.j],
       [ 0.5+0.j, -0.5+0.j,  0.5+0.j, -0.5+0.j],
       [ 0.5+0.j,  0.5+0.j, -0.5+0.j, -0.5+0.j],
       [ 0.5+0.j, -0.5+0.j, -0.5+0.j,  0.5-0.j]])
Source code in src/quotonic/logic.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def H(n: int = 1) -> np_ndarray:
    """Generate the matrix representation of $n$ Hadamard gates applied to $n$ qubits individually.

    Constructs and returns the matrix $H^{\\otimes n}$, where

    $$ H = \\frac{1}{\\sqrt{2}} \\begin{pmatrix} 1 & 1 \\\\ 1 & -1 \\end{pmatrix}. $$

    Args:
        n: number of qubits, $n$

    Returns:
        Matrix representation of a Hadamard gate, as a $2\\times 2 array

    Examples:
        ```python
        >>> H()
        array([[ 0.70710678+0.j,  0.70710678+0.j],
               [ 0.70710678+0.j, -0.70710678+0.j]])
        >>> H(n=2)
        array([[ 0.5+0.j,  0.5+0.j,  0.5+0.j,  0.5+0.j],
               [ 0.5+0.j, -0.5+0.j,  0.5+0.j, -0.5+0.j],
               [ 0.5+0.j,  0.5+0.j, -0.5+0.j, -0.5+0.j],
               [ 0.5+0.j, -0.5+0.j, -0.5+0.j,  0.5-0.j]])
        ```
    """
    mat_1 = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2)
    mat = reduce(np.kron, [mat_1] * n)
    return mat  # type: ignore

CNOT(control=0, target=1, n=2)

Generate the matrix representation of a CNOT gate between the specified control and target qubits.

For any number of qubits, this function will form the matrix representation of a single CNOT gate between a specified control qubit and a specified target qubit. By default, it forms the familiar

\[ \mathrm{CNOT} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{pmatrix}. \]

Parameters:

Name Type Description Default
control int

index of the control qubit

0
target int

index of the target qubit

1
n int

total number of qubits, \(n\)

2

Returns:

Type Description
np_ndarray

Matrix representation of a CNOT gate between the control and target qubits, as a \(2^n\times 2^n\) array

Examples:

>>> CNOT()
array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
       [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]])
>>> CNOT(control=0, target=1, n=3)
array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]])
Source code in src/quotonic/logic.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def CNOT(control: int = 0, target: int = 1, n: int = 2) -> np_ndarray:
    """Generate the matrix representation of a CNOT gate between the specified control and target qubits.

    For any number of qubits, this function will form the matrix representation of a single CNOT gate between a
    specified control qubit and a specified target qubit. By default, it forms the familiar

    $$ \\mathrm{CNOT} = \\begin{pmatrix} 1 & 0 & 0 & 0 \\\\ 0 & 1 & 0 & 0 \\\\ 0 & 0 & 0 & 1 \\\\ 0 & 0 & 1 & 0
    \\end{pmatrix}. $$

    Args:
        control: index of the control qubit
        target: index of the target qubit
        n: total number of qubits, $n$

    Returns:
        Matrix representation of a CNOT gate between the control and target qubits, as a $2^n\\times 2^n$ array

    Examples:
        ```python
        >>> CNOT()
        array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
               [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
               [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
               [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]])
        >>> CNOT(control=0, target=1, n=3)
        array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
               [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
               [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
               [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
               [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
               [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
               [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
               [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]])
        ```
    """
    assert (n > control) and (
        n > target
    ), "Check that you are indexing correctly and that you are passing the correct number of qubits"
    assert control != target, "Control and target qubits should be different"

    # define relevant single-qubit gates
    Id = np.eye(2, dtype=complex)
    X = np.array([[0, 1], [1, 0]], dtype=complex)

    # define single-qubit states and their projectors
    ket0 = Id[0].reshape(2, 1)
    ket1 = Id[1].reshape(2, 1)
    proj0 = np.kron(ket0, ket0.T)
    proj1 = np.kron(ket1, ket1.T)

    # apply the appropriate Kronecker product based on each qubit
    term1 = np.array([1], dtype=complex)
    term2 = np.array([1], dtype=complex)
    for i in reversed(range(n)):
        if i == control:
            term1 = np.kron(proj0, term1)
            term2 = np.kron(proj1, term2)
        elif i == target:
            term1 = np.kron(Id, term1)
            term2 = np.kron(X, term2)
        else:
            term1 = np.kron(Id, term1)
            term2 = np.kron(Id, term2)

    mat: npt.NDArray[np.complex128] = term1 + term2
    return mat

CZ(control=0, target=1, n=2)

Generate the matrix representation of a CZ gate between the specified control and target qubits.

For any number of qubits, this function will form the matrix representation of a single CZ gate between a specified control qubit and a specified target qubit. By default, it forms the familiar

\[ \mathrm{CZ} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & -1 \end{pmatrix}. \]

Parameters:

Name Type Description Default
control int

index of the control qubit

0
target int

index of the target qubit

1
n int

total number of qubits, \(n\)

2

Returns:

Type Description
np_ndarray

Matrix representation of a CZ gate between the control and target qubits, as a \(2^n\times 2^n\) array

Examples:

>>> CZ()
array([[ 1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j],
       [ 0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j],
       [ 0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j],
       [ 0.+0.j,  0.+0.j,  0.+0.j, -1.+0.j]])
>>> CZ(control=0, target=1, n=3)
array([[ 1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
       [ 0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
       [ 0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
       [ 0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
       [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
       [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j, 0.+0.j],
       [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, -1.+0.j, 0.+0.j],
       [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, -1.+0.j]])
Source code in src/quotonic/logic.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def CZ(control: int = 0, target: int = 1, n: int = 2) -> np_ndarray:
    """Generate the matrix representation of a CZ gate between the specified control and target qubits.

    For any number of qubits, this function will form the matrix representation of a single CZ gate between a
    specified control qubit and a specified target qubit. By default, it forms the familiar

    $$ \\mathrm{CZ} = \\begin{pmatrix} 1 & 0 & 0 & 0 \\\\ 0 & 1 & 0 & 0 \\\\ 0 & 0 & 1 & 0 \\\\ 0 & 0 & 0 & -1
    \\end{pmatrix}. $$

    Args:
        control: index of the control qubit
        target: index of the target qubit
        n: total number of qubits, $n$

    Returns:
        Matrix representation of a CZ gate between the control and target qubits, as a $2^n\\times 2^n$ array

    Examples:
        ```python
        >>> CZ()
        array([[ 1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j],
               [ 0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j],
               [ 0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j],
               [ 0.+0.j,  0.+0.j,  0.+0.j, -1.+0.j]])
        >>> CZ(control=0, target=1, n=3)
        array([[ 1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
               [ 0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
               [ 0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
               [ 0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
               [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j, 0.+0.j],
               [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j, 0.+0.j],
               [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, -1.+0.j, 0.+0.j],
               [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j, -1.+0.j]])
        ```
    """
    assert (n > control) and (
        n > target
    ), "Check that you are indexing correctly and that you are passing the correct number of qubits"
    assert control != target, "Control and target qubits should be different"

    # define relevant single-qubit gates
    Id = np.eye(2, dtype=complex)
    Z = np.array([[1, 0], [0, -1]], dtype=complex)

    # define single-qubit states and their projectors
    ket0 = Id[0].reshape(2, 1)
    ket1 = Id[1].reshape(2, 1)
    proj0 = np.kron(ket0, ket0.T)
    proj1 = np.kron(ket1, ket1.T)

    # apply the appropriate Kronecker product based on each qubit
    term1 = np.array([1], dtype=complex)
    term2 = np.array([1], dtype=complex)
    for i in reversed(range(n)):
        if i == control:
            term1 = np.kron(proj0, term1)
            term2 = np.kron(proj1, term2)
        elif i == target:
            term1 = np.kron(Id, term1)
            term2 = np.kron(Z, term2)
        else:
            term1 = np.kron(Id, term1)
            term2 = np.kron(Id, term2)

    mat: npt.NDArray[np.complex128] = term1 + term2
    return mat

BSA()

Generate the matrix representation of a Bell State Analyzer (BSA) in the computational basis.

The BSA operates on two qubits and is defined as

\[ \mathrm{BSA} = (H \otimes I)\mathrm{CNOT}. \]

Returns:

Type Description
np_ndarray

Matrix representation of a BSA in the computational basis, as a \(4\times 4\) array

Source code in src/quotonic/logic.py
228
229
230
231
232
233
234
235
236
237
238
239
def BSA() -> np_ndarray:
    """Generate the matrix representation of a Bell State Analyzer (BSA) in the computational basis.

    The BSA operates on two qubits and is defined as

    $$ \\mathrm{BSA} = (H \\otimes I)\\mathrm{CNOT}. $$

    Returns:
        Matrix representation of a BSA in the computational basis, as a $4\\times 4$ array
    """
    mat = np.kron(H(), np.eye(2, dtype=complex)) @ CNOT()
    return mat  # type: ignore