✅ Put your name here

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:

- Know the difference between a bit and a qubit. (And thus the difference between classical and quantum computing.)
- Understand how information is stored and retrieved from bits, and what operations can be done on a bit.
- 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])
```

✎ **Answer:** Erase the contents of this cell and put your answer here!

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

✎ **Answer:** Erase the contents of this cell and put your answer here!

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

✎ **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
```

In [ ]:

```
"""ANSWER."""
# complex number
alpha = 2 + 4j
# TODO: compute and print out the complex conjugate
print(alpha.conjugate())
# TODO: compute and print out the modulus squared
print(abs(alpha)**2)
```

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?

✎ **Answer:** Erase the contents of this cell and put your answer here!

**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).

✎ **Answer:** Erase the contents of this cell and put your answer here!

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="")
```

In [ ]:

```
"""ANSWER: Final class after completing all instructions."""
class Bit:
"""Binary digit class."""
def __init__(self, initial_value=0):
"""Initialize a Bit to the 0 state."""
self.value = initial_value
def NOT(self):
"""Performs the NOT operation on the Bit."""
self.value = (self.value + 1) % 2
def measure(self):
"""Returns the value of the Bit."""
return self.value
def copy(self):
"""Returns a copy of the current bit."""
return Bit(self.value)
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:

✎ 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 *qu*bits.

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**

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

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

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).

✎ **Answer:** Erase the contents of this cell and put your answer here!

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[0], self.wavefunction[1], sep="\n")
```

In [ ]:

```
"""ANSWER: complete class is shown below."""
class Qubit:
"""Quantum bit class."""
# data -- states. NOTE: THESE CAN BE DEFINED ELSEWHERE IN THE CLASS
zero = np.array([1, 0], dtype=np.complex64)
one = np.array([0, 1], dtype=np.complex64)
# data -- gates. NOTE: THESE CAN BE DEFINED ELSEWHERE IN THE CLASS
xmat = np.array([[0, 1], [1, 0]], dtype=np.complex64)
hmat = 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]], dtype=np.complex64)
def __init__(self):
"""Initializes a Qubit to the ground state |0>."""
self.wavefunction = self.zero
def measure(self):
"""Returns the state obtained by measuring the Qubit and modifies the wavefunction accordingly."""
prob_ground_state = abs(self.wavefunction[0])**2
if np.random.uniform() < prob_ground_state:
self.wavefunction = self.zero
return 0
else:
self.wavefunction = self.one
return 1
def NOT(self):
"""Performs a NOT operation on the Qubit's wavefunction."""
self.wavefunction = np.dot(self.xmat, self.wavefunction)
def H(self):
"""Performs a Hadamard operation on the Qubit's wavefunction"""
self.wavefunction = np.dot(self.hmat, self.wavefunction)
def display_wavefunction(self):# <-- do not modify this method!
"""Prints the wavefunction of the Qubit."""
print("The Qubit's wavefunction is", self.wavefunction[0], self.wavefunction[1], 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

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?

✎ **Answer:** Erase the contents of this cell and put your answer here!

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

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

(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?

✎ **Answer:** Erase the contents of this cell and put your answer here!

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

✎ Do the following to your `Qubit`

class:

(1) Write a method called `H`

that applies the Hadamard gate given above to the `Qubit`

s `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.

✎ **Answer:** Erase the contents of this cell and put your answer here!

**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."""
```

In [ ]:

```
"""ANSWER."""
# dictionary to store measurement outcomes
measurements = {0: 0, 1: 0}
# loop over 1000 iterations
for _ in range(1000):
# get a qubit
q = Qubit()
# apply a Hadamard gate
q.H()
# measure the qubit to get a bit
bit = q.measure()
# store the result
measurements[bit] += 1.0 / 1000
# make a histogram
_ = plt.bar(list(measurements.keys()), measurements.values(), color='g')
plt.xlabel("Measurement Outcome");
plt.ylabel("Probability");
```

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

✎ **Answer:** Erase the contents of this cell and put your answer here!

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 --upgrade pip
# !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.

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

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

In the pop-up screen, select "Sign Up" or "Sign up using Github." The first requires an email, the second requires you to log into GitHub and authorize access to your account (to get an email).

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">
Loading...
</iframe>
"""
)
```

Congrats, You're Finished!

¶Now, you just need to submit this assignment by uploading it to the course Desire2Learn web page for today's submission folder. (Don't forget to add your name in the first cell.)

**© Copyright 2019, Michigan State University Board of Trustees.**