Examples

This page demonstrates how QCircuits can be used to simulate example quantum algorithms.

Producing Bell States

Example code producing each of the four entangled Bell states for a two-qubit system.

The circuit diagram is

images/bell.png

where |x⟩ and |y⟩ are each one of the computational basis states, |0⟩ or |1⟩.

E.g., \(|\beta_{00}⟩ = \frac{1}{\sqrt{2}} (|00⟩ + |11⟩)\).

Code:

import qcircuits as qc
from itertools import product


# Creates each of the four Bell states


def bell_state(x, y):
    H = qc.Hadamard()
    CNOT = qc.CNOT()

    phi = qc.bitstring(x, y)
    phi = H(phi, qubit_indices=[0])

    return CNOT(phi)


if __name__ == '__main__':

    for x, y in product([0, 1], repeat=2):

        print('\nInput: {} {}'.format(x, y))
        print('Bell state:')
        print(bell_state(x, y))

Quantum Teleportation

images/teleport.png

Code:

import qcircuits as qc


# Quantum Teleportation: transmitting two classical bits to transport a qubit state
# Alice has a qubit in a given quantum state.
# Alice and Bob have previously prepared a Bell state, and have since
# physically separated the qubits.
# Alice manipulates her hidden qubit and her half of the Bell state, and then
# measures both qubits.
# She sends the result (two classical bits) to Bob, who is able to reconstruct
# Alice's state by applying operators based on the measurement outcomes.


def quantum_teleportation(alice_state):
    # Get operators we will need
    CNOT = qc.CNOT()
    H = qc.Hadamard()
    X = qc.PauliX()
    Z = qc.PauliZ()

    # The prepared, shared Bell state
    bell = qc.bell_state(0, 0)
    # The whole state vector
    state = alice_state * bell

    # Apply CNOT and Hadamard gate
    state = CNOT(state, qubit_indices=[0, 1])
    state = H(state, qubit_indices=[0])

    # Measure the first two bits
    # The only uncollapsed part of the state vector is Bob's
    M1, M2 = state.measure(qubit_indices=[0, 1], remove=True)

    # Apply X and/or Z gates to third qubit depending on measurements
    if M2:
        state = X(state)
    if M1:
        state = Z(state)

    return state


if __name__ == '__main__':
    # Alice's original state to be teleported to Bob
    alice = qc.qubit(theta=1.5, phi=0.5, global_phase=0.2)

    # Bob's state after quantum teleportation
    bob = quantum_teleportation(alice)

    print('Original state:', alice)
    print('\nTeleported state:', bob)

Quantum Parallelism

images/parallel.png

Code:

import qcircuits as qc
import numpy as np


# Example of quantum parallelism


# Construct a Boolean function
def construct_problem():
    answers = np.random.randint(0, 2, size=2)

    def f(bit):
        return answers[bit]

    return f


def quantum_parallelism(f):
    U_f = qc.U_f(f, d=2)
    H = qc.Hadamard()

    phi = qc.zeros(2)
    phi = H(phi, qubit_indices=[0])
    phi = U_f(phi)


if __name__ == '__main__':
    f = construct_problem()

    quantum_parallelism(f)

Deutsch’s Algorithm

images/deutsch.png

Code:

import qcircuits as qc
import numpy as np


# Deutsch's Algorithhm:
# We use interference to determine if f(0) = f(1) using a single function evaluation.


# Construct a Boolean function that is constant or balanced
def construct_problem():
    answers = np.random.randint(0, 2, size=2)

    def f(bit):
        return answers[bit]

    return f


def deutsch_algorithm(f):
    U_f = qc.U_f(f, d=2)
    H = qc.Hadamard()

    phi = H(qc.zeros()) * H(qc.ones())
    phi = U_f(phi)
    phi = H(phi, qubit_indices=[0])

    measurement = phi.measure(qubit_indices=0)
    return measurement


if __name__ == '__main__':
    f = construct_problem()
    parity = f(0) == f(1)

    measurement = deutsch_algorithm(f)

    print('f(0): {}, f(1): {}'.format(f(0), f(1)))
    print('f(0) == f(1): {}'.format(parity))
    print('Measurement: {}'.format(measurement))

The Deutsch-Jozsa Algorithm

images/deutsch_jozsa.png

Code:

import qcircuits as qc
import numpy as np
import random


# Deutsch-Jozsa Algorithhm:
# We are presented with a Boolean function that is either constant or
# balanced (i.e., 0 for half of inputs, 1 for the other half).
# We make use of interference to determine whether the function is constant
# or balanced in a single function evaluation.


# Construct a Boolean function that is constant or balanced
def construct_problem(d=1, problem_type='constant'):
    num_inputs = 2**d
    answers = np.zeros(num_inputs, dtype=np.int32)

    if problem_type == 'constant':
        answers[:] = int(np.random.random() < 0.5)
    else: # function is balanced
        indices = np.random.choice(num_inputs, size=num_inputs//2, replace=False)
        answers[indices] = 1

    def f(*bits):
        index = sum(v * 2**i for i, v in enumerate(bits))

        return answers[index]

    return f


def deutsch_jozsa_algorithm(d, f):
    # The operators we will need
    U_f = qc.U_f(f, d=d+1)
    H_d = qc.Hadamard(d)
    H = qc.Hadamard()

    state = qc.zeros(d) * qc.ones(1)
    state = (H_d * H)(state)
    state = U_f(state)
    state = H_d(state, qubit_indices=range(d))

    measurements = state.measure(qubit_indices=range(d))
    return measurements


if __name__ == '__main__':
    d = 10
    problem_type = random.choice(['constant', 'balanced'])

    f = construct_problem(d, problem_type)
    measurements = deutsch_jozsa_algorithm(d, f)

    print('Problem type: {}'.format(problem_type))
    print('Measurement: {}'.format(measurements))
    print('Observed all zeros: {}'.format(not any(measurements)))

Superdense Coding

images/superdense.png

Code:

import qcircuits as qc
import numpy as np


# Superdense Coding: transmitting a qubit to transport two classical bits
# Alice and Bob have previously prepared a Bell state, and have since
# physically separated the qubits.
# Alice has two classical bits she wants to transmit to Bob.
# She manipulates her half of the Bell state depending on the values of those bits,
# then transmits her qubit to Bob, who then measures the system.


def superdense_coding(bit_1, bit_2):
    # Get operators we will need
    CNOT = qc.CNOT()
    H = qc.Hadamard()
    X = qc.PauliX()
    Z = qc.PauliZ()

    # The prepared, shared Bell state
    # Initially, half is in Alice's possession, and half in Bob's
    phi = qc.bell_state(0, 0)

    # Alice manipulates her qubit
    if bit_2:
        phi = X(phi, qubit_indices=[0])
    if bit_1:
        phi = Z(phi, qubit_indices=[0])

    # Bob decodes the two bits
    phi = CNOT(phi)
    phi = H(phi, qubit_indices=[0])
    measurements = phi.measure()
    return measurements


if __name__ == '__main__':
    # Alice's classical bits she wants to transmit
    bit_1, bit_2 = np.random.randint(0, 2, size=2)
    
    # Bob's measurements
    measurements = superdense_coding(bit_1, bit_2)
    
    print("Alice's initial bits:\t{}, {}".format(bit_1, bit_2))
    print("Bob's measurements:\t{}, {}".format(measurements[0], measurements[1]))

Phase Estimation

We are give a black-box d-qubit operator U and one of its eigenstates. The task is to estimate the phase of the corresponding eigenvalue, storing the result in a t-qubit register.

Code:

import qcircuits as qc
import numpy as np
from scipy.stats import unitary_group


# Phase estimation
# We are given a black-box unitary operator, and one of its eigenstates.
# The task is to estimate the phase of the corresponding eigenvalue.
# This is done making use of the efficient inverse quantum Fourier transform.


# Prepares a state that when the inverse Fourier transform is applied,
# unpacks the binary fractional expansion of the phase into the
# first t-qubit register.
def stage_1(state, U, t, d):
    state = qc.Hadamard(d=t)(state, qubit_indices=range(t))

    # For each qubit in reverse order, apply the Hadamard gate,
    # then apply U^(2^i) to the d-qubit register
    # conditional on the state of the t-i qubit in the
    # t-qubit register.
    for idx, t_i in enumerate(range(t-1, -1, -1)):
        U_2_idx = U
        for app_i in range(idx):
            U_2_idx = U_2_idx(U_2_idx)
        C_U = qc.ControlledU(U_2_idx)
        state = C_U(
            state,
            qubit_indices=[t_i] + list(range(t, t+d, 1))
        )
        
    return state


# The t-qubit quantum Fourier transform
def QFT(t):
    Op = qc.Identity(t)
    H = qc.Hadamard()

    # The R_k gate applies a 2pi/2^k phase is the qubit is set
    C_Rs = {}
    for k in range(2, t+1, 1):
        R_k = np.exp(np.pi * 1j / 2**k) * qc.RotationZ(2*np.pi / 2**k)
        C_Rs[k] = qc.ControlledU(R_k)

    # For each qubit in order, apply the Hadamard gate, and then
    # apply the R_2, R_3, ... conditional on the remainder of the qubits
    for t_i in range(t):
        Op = H(Op, qubit_indices=[t_i])
        for k in range(2, t+1 - t_i, 1):
            Op = C_Rs[k](Op, qubit_indices=[t_i + k - 1, t_i])

    # We have the QFT, but with the qubits in reverse order
    # Swap them back
    Swap = qc.Swap()
    for i, j in zip(range(t), range(t-1, -1, -1)):
        if i >= j:
            break
        Op = Swap(Op, qubit_indices=[i, j])

    return Op


# The t-qubit inverse quantum Fourier transform
def inv_QFT(t):
    return QFT(t).adj


# Do phase estimation for a random d-qubit operator,
# recording the result in a t-qubit register.
def phase_estimation(d=2, t=8):
    # a d-qubit gate
    U = unitary_group.rvs(2**d)
    eigvals, eigvecs = np.linalg.eig(U)    
    U = qc.Operator.from_matrix(U)
    
    # an eigenvector u and the phase of its eigenvalue, phi
    phi = np.real(np.log(eigvals[0]) / (2j*np.pi))
    if phi < 0:
        phi += 1
    u = eigvecs[:, 0]
    u = qc.State.from_column_vector(u)
    
    # add the t-qubit register
    state = qc.zeros(t) * u

    state = stage_1(state, U, t, d)
    state = inv_QFT(t)(state, qubit_indices=range(t))
    measurement = state.measure(qubit_indices=range(t))
    phi_estimate = sum(measurement[i] * 2**(-i-1) for i in range(t))

    return phi, phi_estimate


if __name__ == '__main__':
    phi, phi_estimate = phase_estimation(d=2, t=8)

    print('True phase: {}'.format(phi))
    print('Estimated phase: {}'.format(phi_estimate))

Grover’s Algorithm

We are given a black-box boolean function f(x) that evaluates to 1 for exactly one value of x. Grover’s algorithm finds the solution with resources proportional to the square root of the size of the search space.

Code:

import qcircuits as qc
import numpy as np
import random


# Grover's algorithm (search)
# Given a boolean function f, Grover's algorithm finds an x such that
# f(x) = 1.
# If there are N values of x, and M possible solutions, it requires
# O(sqrt(N/M)) time.
# Here, we construct a search problem with 1 solution amongst 1024
# possible answers, and find the solution with 25 applications of
# the Grover iteration operator.


# Construct a Boolean function that is 1 in exactly one place
def construct_problem(d=10):
    num_inputs = 2**d
    answers = np.zeros(num_inputs, dtype=np.int32)
    answers[np.random.randint(0, num_inputs)] = 1

    def f(*bits):
        index = sum(v * 2**i for i, v in enumerate(bits))

        return answers[index]

    return f


def grover_algorithm(d, f):
    # The operators we will need
    Oracle = qc.U_f(f, d=d+1)
    H_d = qc.Hadamard(d)
    H = qc.Hadamard()
    N = 2**d
    zero_projector = np.zeros((N, N))
    zero_projector[0, 0] = 1
    Inversion = H_d((2 * qc.Operator.from_matrix(zero_projector) - qc.Identity(d))(H_d))
    Grover = Inversion(Oracle, qubit_indices=range(d))

    # Initial state
    state = qc.zeros(d) * qc.ones(1)
    state = (H_d * H)(state)

    # Number of Grover iterations
    angle_to_rotate = np.arccos(np.sqrt(1 / N))
    rotation_angle = 2 * np.arcsin(np.sqrt(1 / N))
    iterations = int(round(angle_to_rotate / rotation_angle))
    for i in range(iterations):
        state = Grover(state)

    measurements = state.measure(qubit_indices=range(d))
    return measurements


if __name__ == '__main__':
    d = 10

    f = construct_problem(d)
    bits = grover_algorithm(d, f)

    print('Measurement: {}'.format(bits))
    print('Evaluate f at measurement: {}'.format(f(*bits)))