# In-Class Assignment 23: Classical and Quantum Bits ¶

In yesterday's pre-class assignment, we got introduced to bits and qubits. Today, we'll get more practice working with both units of information.

## Itinerary ¶

 Assignment Topic Description Pre Class 23 Background for Quantum Computing How Computers Store Information In Class 23 Classsical and Quantum Bits Information in Quantum States Pre Class 24 Software for Quantum Computing High Level Software and the Circuit Model In Class 24 Programming Quantum Computers Manipulating Quantum Bits to Perform Useful Computations

## Learning Goals for Today's In-Class Assignment ¶

The purpose of this notebook is to understand how bits are used in classical computers and how qubits are used in quantum computers. In particular, by the end of today's assignment, you should:

1. Know the difference between a bit and a qubit. (And thus the difference between classical and quantum computing.)
2. Understand how information is stored and retrieved from bits, and what operations can be done on a bit.
3. Understand how information is stored and retrieved from qubits, and what operations can be done on a qubit.
In [ ]:
"""Imports for the assignment."""
import numpy as np
import matplotlib.pyplot as plt


## Recap of the Pre-Class Assignment ¶

In the pre-class assignment, we learned that all information is stored in bits on (classical) computers. The key difference in quantum computers is that they store information in quantum bits, or qubits.

To get a deeper understanding of qubits, we reviewed/learned three important topics:

• A complex number has real and imaginary parts and a complex conjugate that allows us to compute it's modulus squared.

• A probability distribution is a list of numbers (or vector, now that we know what that is) that add up to one.

• A vector is a list of numbers (real, complex, etc.) that we can form linear combinations, or superpositions, with.

Question: Is this a valid probability distribution?

In [ ]:
"""Exercise: is this a valid probability distribution?"""
distribution = np.array([0.6, 0.4])


Question: If you answered yes, what could this probability distribution represent? (Give an example.)

Question: What's the complex conjugate and modulus squared of

\begin{equation} \alpha = 2 + 4i? \end{equation}

Answer: Write code in the following cell to answer the above question.

In [ ]:
"""Exercise: compute the complex conjugate and modulus squared of the given complex number."""
# complex number
alpha = 2 + 4j

# TODO: compute and print out the complex conjugate

# TODO: compute and print out the modulus squared


# Working with Bits ¶

Remember that a bit can have the values of either 1 or 0 (True or False, on or off, yes or no.) All information on (classical) computers is stored in bits. Computers process information by operating on bits.

Question: How can you encode one letter of the alphabet (a, b, c, ..., x, y, z) using only bits? How many bits do you need at minimum?

Question: In our laptops, bits are represented by electrical signals. Think of other physical systems that we could use to represent bits of information. List as many as you can (at least three).

## Writing a Bit Class ¶

Now that we know what binary digits are and how to use them to represent information (like letters in the alphabet), let's do a bit of coding (excuse the pun). Specifically, let's code up a Bit class to understand them better. A skeleton of the class is provided below, with some methods implemented, which you should not change.

You're recommended to make all edits to the class here (rather than copying and pasting the class several times below). Unfortunately, this requires some scrolling back and forth between directions and the code. You may wish to have another copy of the notebook open and use one for reading instructions and the other for writing code.

In [ ]:
"""Code cell with a bit class. Keep your class here and modify it as you work through the notebook.
A skeleton of the class is provided for you."""

class Bit:
"""Binary digit class."""

def display_value(self):# <-- do not modify this method
"""Displays the value of the Bit."""
print("The bit's value is ", self.value, ".", sep="")


We know that every class needs an __init__ method, which here will create our Bit. Let's agree by convention to always start our bit in the "off" state, represented by 0, unless a different initial value is provided. We'll do this by using keyword arguments as described below.

Do the following to your class:

(1) Define an __init__ method. This method should have a keyword argument (input to the function) called initial_value whose default value is 0. Create a class attribute called value and set it to be initial_value.

In [ ]:
"""Create a bit and display it's value."""
b = Bit()
b.display_value()


Do the following to your class:

(2) Write a method called measure which returns the value of the bit.

Now run the following code block.

In [ ]:
"""Create a bit, display its value, and print out its state."""
b = Bit()
b.display_value()
print("The measured state of the bit is {}".format(b.measure()))


I know what you're thinking! "Well duh! The measured state of a bit is just going to be it's value..." Hold that thought! We're going to see a big difference when we write a class for a quantum bit.

First, let's talk about the operations we can perform on a bit.

### Operations on a Bit ¶

There's only one non-trivial operation that can be performed on a bit -- negating, or flipping, its value. We'll call this operation the NOT operation, which has the following effect:

\begin{align} \text{NOT(0)} &= 1 \\ \text{NOT(1)} &= 0 \end{align}

Do the following to your class:

(3) Define a method called NOT which negates the value of the bit. This method should NOT return a value (no pun intended).

Now run the following code with your class.

In [ ]:
"""Perform operations on a bit."""
# create a bit
b = Bit()
b.display_value()
print("The measured state of the bit is {}.\n".format(b.measure()))

# apply a NOT operation
b.NOT()
b.display_value()
print("The measured state of the bit is {}.\n".format(b.measure()))

# apply another NOT operation
b.NOT()
b.display_value()
print("The measured state of the bit is {}.\n".format(b.measure()))


Note that applying two NOT gates in a row gets us back to the same state, as you might expect.

We can certainly do more operations with multiple bits of information. For example, if we have two bits, we can take the AND or the OR of them. The AND takes in two bits and returns one if both input bits are one and zero otherwise. The OR returns one if either of the input bits is one (including both).

Operations on multiple bits are crucial for information processing (i.e., computation), but for simplicity we won't discuss them in more detail here.

One reason we do mention multiple bits is for copying information, another crucial component of (classical) computation.

## Copying Bits ¶

We can copy a single classical bit into as many bits as we want. How? Well, we just look at the bit, record its value, then prepare another bit with that value.

Do the following to your class:

(4) Define a method called copy which copies the value of the Bit to a new Bit and returns the new Bit. Then execute the following cell.

In [ ]:
"""Copy a bit."""
b = Bit()
new_bit = b.copy()

b.display_value()
new_bit.display_value()

print(b == new_bit)


Note that the bits have the same value, but they are not equivalent, since they are different objects. With bits, we are able to directly "measure" the bits value and write it into a new bit of information.

We highlighted this feature, as well as the others, to now contrast bits with qubits.

# Working with Qubits ¶

Whereas classical computers represent information using bits, quantum computers represent information using qubits. Here's a short refresher on what a qubit is from the Pre-Class Assignment.

A qubit is a vector of complex numbers

\begin{equation} |\psi\rangle = \alpha |0\rangle + \beta |1\rangle = \left[ \begin{matrix} \alpha \\ \beta \\ \end{matrix} \right] . \end{equation}

These complex numbers determine the probability of measuring 0 or 1, as we'll see today, so we require that $|\alpha|^2 + |\beta|^2 = 1$.

As a reminder, the vector $|0\rangle$, sometimes called the ground state, is

\begin{equation} |0\rangle = \left[ \begin{matrix} 1 \\ 0 \\ \end{matrix} \right] \end{equation}

and the vector $|1\rangle$, sometimes called the excited state, is

\begin{equation} |1\rangle = \left[ \begin{matrix} 0 \\ 1 \\ \end{matrix} \right] . \end{equation}

The Greek symbol $\psi$ (psi, pronounced: "sigh") is commonly used to represent qubits.

Question: Bits are made of classical physical systems like light switches or electricity. What kind of quantum systems do people make qubits out of? Search the web to find out and record at least three of your findings below. Cite your source(s).

## Writing a Qubit Class ¶

Let's now get more practice with qubits by writing a Qubit class in the same way that we wrote a Bit class. This will allow you to see the similarites and differences between classical and quantum bits.

In [ ]:
"""Code cell with a qubit class. Keep your class here and modify it as you work through the notebook.
A skeleton of the class is provided for you."""

class Qubit:
"""Quantum bit class."""

def display_wavefunction(self):# <-- do not modify this method!
"""Prints the wavefunction of the Qubit."""
print("The Qubit's wavefunction is", self.wavefunction, self.wavefunction, sep="\n")


Do the following to your Qubit class:

(1) Define an __init__ method. In this method:

(a) Create a class attribute called zero which represents the vector $|0\rangle$ above. This attribute should be a Numpy array. Make sure the datatype (dtype) is complex, for example using np.complex64.

(b) Create a class attribute called one which represents the vector $|1\rangle$ above. This attribute should be a Numpy array. Make sure the datatype (dtype) is complex, for example using np.complex64.

By convention, let's agree to always initialize a Qubit in the ground state $|0\rangle$.

Do the following to your Qubit class:

(1) In the __init__ method, create an attribute called wavefunction of the Qubit and set it to be equal to the $|0\rangle$ state. (The term wavefunction is physics jargon. We can say "a qubit is..." or "a qubit's wavefunction is..." interchangeably.)

Now run the following code.

In [ ]:
"""Initialize a qubit and display its wavefunction."""
q = Qubit()
q.display_wavefunction()
print()


In the above code, we initialize a qubit and then use the provided display_wavefunction method to print out it's wavefunction. If your __init__ method is correct, you should see the qubit's wavefunction as the $|0\rangle$ vector.

### Measuring a Qubit ¶

Now let's write a method to measure our Qubit. The two measurement rules of a qubit

\begin{equation} |\psi\rangle = \alpha |0\rangle + \beta |1\rangle = \left[ \begin{matrix} \alpha \\ \beta \\ \end{matrix} \right] . \end{equation}

are listed below:

The First Measurement Rule

(1) The probability that $|\psi\rangle$ is in the ground state $|0\rangle$ is $|\alpha|^2$. The probability that $|\psi\rangle$ is in the excited state $|1\rangle$ is $|\beta|^2 = 1 - |\alpha|^2$.

Key Concept: There are two possible measurement outcomes of a qubit. Thus, the measurement outcome of a qubit is a bit. This is why we used 0 and 1 as labels for the vectors all along! When we measure the ground state $|0\rangle$, we call this outcome $0$. When we measure the excited state $|1\rangle$, we call this outcome $1$.

The Second Measurement Rule

(2) If we measure the ground state, the wavefunction becomes $|0\rangle$. If we measure the excited state, the wavefunction becomes $|1\rangle$.

Do the following to your Qubit class:

(1) Write a method called measure which measures a Qubit according to the above rules. Specifically, your method should return a bit (0 if the ground state was measured, or 1 if the excited state was measured) and modify the wavefunction of the Qubit appropriately.

Hints:

(i) Compute the probability the qubit is in the ground state ($|\alpha|^2$).

(ii) Generate a random number between 0 and 1.

(iii) If the random number is less than $|\alpha|^2$, set the wavefunction to be the zero vector, and return the number (bit) 0. Otherwise, set the wavefunction to be the one vector, and return the number (bit) 1.

Now run the following code.

In [ ]:
"""Initialize a qubit and measure it."""
q = Qubit()
q.display_wavefunction()
print("The bit we obtain from measuring the qubit is {}.\n".format(q.measure()))
q.display_wavefunction()


You should have seen the measurement result 0.

Question: Run the cell above many times and note the measurement result (i.e., bit) after each time you run it. (The keyboard shortcut "control + enter" is useful here.) What measured state do you always get? Why?

## Operations on a Qubit ¶

On a classical bit, we could only do one operation, the NOT operation, because the bit only had two states. With our Qubit, we have an underlying wavefunction that helps determine what our measured state will be, as we have seen above. Operations on a Qubit act on its wavefunction. As such, there's a lot more operations we can do to it! (In fact, there's infinitely many operations we can do on a qubit.)

One example of an operation is called the X or NOT operation. Why is it called this? Well, it has the effect

\begin{align} \text{NOT($|0\rangle$)} &= |1\rangle \\ \text{NOT($|1\rangle$)} &= |0\rangle \end{align}

Qubit operations can be written as matrices that act on a qubit's wavefunction to implement the operation. You don't have to know how to do matrix-vector multiplication, just how to do it in Python. An example is shown below.

In [ ]:
"""Example of a matrix vector multiplication using numpy."""
# an example matrix
matrix = np.array([[1, 1], [1, 1]])

# an example vector
vector = np.array([1, 0])

# the matrix-vector product
print(np.dot(matrix, vector))

# another way to do the matrix-vector product
print(matrix @ vector)


It can be shown that a matrix representation for NOT is

\begin{equation} \text{NOT} = \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \\ \end{matrix} \right] . \end{equation}

(If you know linear algebra, prove this to youreself. If not, just take our word for it.)

Do the following to your Qubit class:

(1) Write a method called NOT which multiplies the wavefunction the NOT matrix given above. (This method should NOT return a value (still no pun intended), only modify the wavefunction.)

Now run the following code.

In [ ]:
"""Perform a NOT operation on a qubit."""
q = Qubit()
q.display_wavefunction()
print()

q.NOT()
q.display_wavefunction()


You should have a Qubit whose wavefunction is $|1\rangle$ after executing the above cell. Now let's measure such a qubit to see what we get.

In [ ]:
"""Measure a qubit with wavefunction |1>."""
q = Qubit()
q.NOT()
q.display_wavefunction()
print("The bit we obtain from measuring the qubit is {}.\n".format(q.measure()))
q.display_wavefunction()


You should have seen 1 as the measurement result.

Question: Run the above cell many times and observe your measurement results after each run. What measured state do you always get? Why?

Another quantum operation is called the Hadamard operation or Hadamard gate. A matrix representation for the Hadamard gate is given by

\begin{equation} H = \frac{1}{\sqrt{2}}\left[ \begin{matrix} 1 & 1 \\ 1 & -1 \\ \end{matrix} \right] \end{equation}

Do the following to your Qubit class:

(1) Write a method called H that applies the Hadamard gate given above to the Qubits wavefunction.

Now run the following code.

In [ ]:
"""Performing a Hadamard transform on a qubit."""
q = Qubit()
q.H()
q.display_wavefunction()


You should see that the qubit has equal amplitudes (components of the wavefunction). Now let's measure the Qubit to see what we get.

In [ ]:
"""Measuring a qubit after performing a Hadamard gate."""
q = Qubit()
q.H()
q.display_wavefunction()
print("The bit we obtain from measuring the qubit is {}.\n".format(q.measure()))
q.display_wavefunction()


What measurement result did you get?

Question: Run the above cell several times and observe your measurement results after each run. Write a sentence describing your observation.

Question: Write code to create a qubit, perform the Hadamard gate, and measure the qubit 1000 times. Record each measurement outcome, then make a histogram of the probability of measuring 0 and the probability of measuring 1.

In [ ]:
"""Put your code here."""


Question: Reflect on your results. What can you say about the probability of measuring 0 and the probability of measuring 1?

## Copying Qubits ¶

Remember the question of how to copy a bit? Well, now let's ask this for qubits:

Question: How do you copy a qubit?

Attempted Answer 1: You just copy it's wavefunction?

This won't work! Remember the wavefunction is just a mathematical tool that we use to help us calculate probabilities of what state the system is in. If we have a particle, say an electron, there's no wavefunction that we can just look at and then copy over it's information. (Unlike a light switch, a classical system, which we could look at and see with no issues.

Attempted Answer 2: Well what if we just measure it then copy the measurement result into a new qubit?

This also won't work! Remember the measurement rules above. When we measure a qubit, we inherently change its wavefunction. The wavefunction it changes to is not, in general, the same as it was before measurement.

There is a way to copy one qubit to another, which is known as quantum teleportation, and involves a total of three qubits to get the job done. You'll get a chance to look at this in an upcoming assignment!

# Assignment Wrap-up ¶

## Installing Qiskit

For the next assignments, we'll be using the Quantum Information Science Kit, or Qiskit, which is a Python package for quantum computing. Try to install Qiskit v0.7.0 on your computer now by executing the following cell. We'll be walking around to troubleshoot problems.

Note: Why version 0.7.0? These quantum software packages are new and tend to change a bit. We'll use this version to make sure all the code in future assignments works as anticipated.

In [ ]:
"""Attempt to install Qiskit using pip. Uncomment the following two lines and run the cell."""
# !pip install qiskit==0.7.0


## Instructions to Get an API Key to Use a Quantum Computer ¶

You can use Qiskit to program an actual quantum computer. To do so, one needs to register for an API key. If this interests you, follow the instructions below. (These instructions are also in the next Pre Class Assignment if you're short on time.) This is optional, not required.

1. Navigate to the IBM Quantum Experience website https://quantumexperience.ng.bluemix.net/qx.

2. Click "Sign In" in the upper right hand corner of the page (blue box with white text).

4. Fill out the form, then click "Sign up" at the bottom.

Once you have created an account, you can sign in (follow the first two steps above). Then, click the user icon in the upper right hand corner of the page, then click "My Account." On the new screen, click the "Advanced" tab. Here, you can see your API key and copy it to your clipboard. You'll need to enter this in your notebook to use the real quantum computer backends.

## Survey ¶

In [ ]:
from IPython.display import HTML
HTML(
"""
<iframe
src="https://goo.gl/forms/XnF4lrNsyxRAbggV2"
width="80%"
height="1200px"
frameborder="0"
marginheight="0"
marginwidth="0">