Multiple Qubits

If we have 2 classical bits, the possible states we could encode information in would be 00, 01, 10 and 11. Correspondingly, multiple qubits can be combined and the possible combinations of their states used to process information.

A two qubit system has 4 computational basis states: \(\ket{00}, \ket{01}, \ket{10}, \ket{11}\).

Classically, we cannot encode information within states such as 00 + 11 but quantum mechanics allows us to write linear superpositions

\[\ket{\psi} = \alpha_{00}\ket{00} + \alpha_{01}\ket{01} + \alpha_{10}\ket{10} + \alpha_{11}\ket{11}\]

where the probability of measuring \(x = 00, 01, 10, 11\) occurs with probability \(\lvert \alpha_{x} \rvert ^2\) with the normalization condition that \(\sum_{x \in \{ 0,1 \}^2} \lvert \alpha_{x} \rvert ^2 = 1\)

More generally, the quantum state of a \(n\) qubit system is written as a sum of \(2^n\) possible basis states where the coefficients track the probability of the system collapsing into that state if a measurement is applied.

For \(n = 500\), \(2^n \approx 10^{150}\) which is greater than the number of atoms in the universe. Storing the complex numbers associated with \(2^{500}\) amplitudes would not be feasible using bits and classical computations but nature seems to only require 500 qubits to do so. The art of quantum computation is thus to build quantum systems that we can manipulate with fine precision such that evolving a large statevector can be offloaded onto a quantum computer.

Some notation conventions

Qubit counting starts from 0 and the 0th qubit is represented on the left most side in Dirac notation. For e.g. in \(\ket{01}\) the 0th qubit is in state \(\ket{0}\) and the first in state \(\ket{1}\).

For brevity, we denote gate application with subscripts to reference the qubit it acts on. For e.g. \(X_{0}\ket{00} = \ket{10}\) refers to \(X_{0}\) acting on the 0th qubit flipping it to the state 1 as shown. Below we see how this is done in CUDA-Q.

[1]:
import cudaq


@cudaq.kernel
def kernel():
    # 2 qubits both initialised to the ground/ zero state.
    qvector = cudaq.qvector(2)

    # Application of a flip gate to see ordering notation.
    x(qvector[0])

    mz(qvector[0])
    mz(qvector[1])


print(cudaq.draw(kernel))

result = cudaq.sample(kernel)
print(result)
     ╭───╮
q0 : ┤ x ├
     ╰───╯

{ 10:1000 }

Controlled-NOT gate

Analogous to classical computing, we now introduce multi-qubit gates to quantum computing.

The controlled-NOT or CNOT gate acts on 2 qubits: the control qubit and the target qubit. Its effect is to flip the target if the control is in the excited \(\ket{1}\) state.

We use the notation CNOT01\(\ket{10} = \ket{11}\) to describe its effects. The subscripts denote that the 0th qubit is the control qubit and the 1st qubit is the target qubit.

[2]:
@cudaq.kernel
def kernel():
    # 2 qubits both initialised to the ground/ zero state.
    qvector = cudaq.qvector(2)

    x(qvector[0])

    # Controlled-not gate operation.
    x.ctrl(qvector[0], qvector[1])

    mz(qvector[0])
    mz(qvector[1])


result = cudaq.sample(kernel)
print(result)
{ 11:1000 }

In summary, the CNOT gate in matrix notation is represented as:

\[\begin{split}CNOT \equiv \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{bmatrix}\end{split}\]

To conserve probabilites and preserve the normalization condition, quantum gates must obey unitarity and one can check that \(CNOT^\dagger CNOT = \mathbb{I}\)

and its effect on the computational basis states is:

\[CNOT_{01}\ket{00} = \ket{00}\]
\[CNOT_{01}\ket{01} = \ket{01}\]
\[CNOT_{01}\ket{10} = \ket{11}\]
\[CNOT_{01}\ket{11} = \ket{10}\]