Search
Hello Qiskit Game

Hello Qiskit

Simple puzzles to get started with qubits and quantum gates.

Level 1: Beginning with bits

Like all of the textbook, this document is a Jupyter notebook. However, unlike most of the textbook, you'll have to actually run it to make it work.

Don't worry if you've never used a Jupyter notebook before. It just means you'll see lots of grey boxes with code in, like the one below. These are known as cells.

print("Hello! I'm a code cell")

The way to run the code within a cell depends on the device and input method you are using. In most cases, you should click on the cell and press Shift-Enter. However, if you are running this on the Qiskit textbook website, you can just click the 'Run' button below.

Get started by doing this for the cell below (it will take a second or two to run).

print('Set up started...')
from qiskit_textbook.games import hello_quantum
print('Set up complete!')

The rest of the cells in this notebook contain code that sets up puzzles for you to solve. To get the puzzles, just run the cells. To restart a puzzle, just rerun it.

Puzzle 1

Intro

Quantum computers are based on qubits: the quantum version of a bit. But what exactly is a bit? And how are they used in computers?

The defining feature of a bit is that it has two possible output values. These can be called 1 and 0, or 'on' and 'off', or 'True' and 'False'. The names we use don't reall matter. The important point is that there are two of them.

To get familiar with bits, let's play with one. The simplest thing you can do with a bit (other than leave it alone) is flip its value. We give this simple operation a fancy name: it is called the the NOT gate.

Try it out below.

Exercise

  • Use the NOT gate 3 times.
initialize = []
success_condition = {}
allowed_gates = {'0': {'NOT': 3}, '1': {}, 'both': {}}
vi = [[1], False, False]
qubit_names = {'0':'the only bit', '1':None}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

Here we visualized our bit using a circle that was either on (white) or off (black). The effect of the NOT gate was to turn it on and off, flipping between the two states of the bit.

Puzzle 2

Intro

It's more interesting to play with two bits than just one. So here is another to play with. It will look the same as before. But because it is a different bit, it'll be in a different place.

Exercise

  • Turn the other bit on.
initialize = []
success_condition = {}
allowed_gates = {'0': {}, '1': {'NOT': 0}, 'both': {}}
vi = [[], False, False]
qubit_names = {'0':'the bit on the left', '1':'the bit on the right'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

You've now mastered the NOT gate: the most basic building block of computing.

Puzzle 3

Intro

We use the word 'gate' to describe the simple tools we can use to manipulate qubits. The NOT gate is the simplest example. But to do interesting things with bits, we need to do more than just turn them on and off.

A slightly more sophisticated gate is called the CNOT, which is short for 'controlled-NOT'. Thisis used on pairs of bits.

We choose one of the bits to be the 'control'. This gets to descide whether the CNOT actually does anything. The other bit is the 'target'. Simply put, the CNOT does a NOT on the target bit, but only if the control bit is on.

The best way to understand this is to try it out!

Exercise

  • Use the CNOT to turn on the bit on the right.
  • Note: When you are asked to choose a bit, you are choosing which will be the target bit.
initialize = [['x', '0']]
success_condition = {'IZ': -1.0}
allowed_gates = {'0': {'CNOT': 0}, '1': {'CNOT': 0}, 'both': {}}
vi = [[], False, False]
qubit_names = {'0':'the bit on the left', '1':'the bit on the right'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Puzzle 4

Intro

Any program we run is made up of many gates on many qubits. As a step towards this, let's try something where you'll need to use a couple of CNOTs.

Exercise

  • Use some CNOTs to turn the left bit off and the right bit on.
initialize = [['x', '0']]
success_condition = {'ZI': 1.0, 'IZ': -1.0}
allowed_gates = {'0': {'CNOT': 0}, '1': {'CNOT': 0}, 'both': {}}
vi = [[], False, False]
qubit_names = {'0':'the bit on the left', '1':'the bit on the right'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

Well done!

These kind of manipulations are what all computing compiles down to. If you had more bits and a controlled-controlled-NOT gate, you could build everything from Tetris to self-driving cars.

Puzzle 5

Intro

You have probably noticed that there is a lot of empty space on the puzzle board. This is because it can be used to visualize more information about our bits. For example, what if our bit values have been generated by some random process?

We have used black and white circles to represent the bit values of 0 or 1. So for a random bit, which will give us 0 or 1 with equal probability, we'll use a grey circle.

Now we can look at how our gates manipulate this randomness.

Exercise

  • Make the bit on the right random using a CNOT.
initialize = [['h', '0']]
success_condition = {'IZ': 0.0}
allowed_gates = {'0': {'CNOT': 0}, '1': {'CNOT': 0}, 'both': {}}
vi = [[], False, False]
qubit_names = {'0':'the bit on the left', '1':'the bit on the right'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

Well done!

Let's think about what happened here. We did a CNOT, controlled by a bit that was randomly either on or off. If it was off, the CNOT does nothing and the right bit stays off. If the left bit was on, the CNOT does a NOT on the right bit to switch it on too.

This process effectively copies the random value of the left bit over to the right. So outputs from both will look random, but with an important property: they will always output the same random result as each other.

At the end of the puzzle above, the two bits were represented by two grey circles. This shows that both are random, but tells us nothing about the fact that they always agree. The same two grey circles would also be used if the bits were independently random, or if they were random but always disagreed. To help us tell the difference, we need to add something to the puzzle board.

Puzzle 6

Intro

In the puzzle below, you'll see a new circle.

Unlike the circles you've seen so far, doesn't represent a new bit. Instead it tells us whether or not our two bits will agree or not.

When they are certain to agree, it will be off. When they are certain to disagree, it will be on. If their agreements and disagreements are just random, it will be grey.

Exercise

  • Make the two bits always disagree (that means making the middle circle white).
initialize = [['h', '0']]
success_condition = {'ZZ': -1.0}
allowed_gates = {'0': {'NOT': 0, 'CNOT': 0}, '1': {'NOT': 0, 'CNOT': 0}, 'both': {}}
vi = [[], False, True]
qubit_names = {'0':'the bit on the left', '1':'the bit on the right'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Puzzle 7

Intro

Now you know pretty much need all you need to know about bits. Let's have one more exercise before we move on.

Exercise

  • Turn on bit the bit on the right.
initialize = [['h', '1']]
success_condition = {'IZ': -1.0}
allowed_gates = {'0': {'NOT': 0, 'CNOT': 0}, '1': {'NOT': 0, 'CNOT': 0}, 'both': {}}
vi = [[], False, True]
qubit_names = {'0':'the bit on the left', '1':'the bit on the right'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

Now it's time to see what happens when bits become quantum. It's time for qubits!

Level 2: Basic single qubit gates

Puzzle 1

Intro

Instead of starting with a lecture on quantum mechanics, here is a qubit to play with. Try out the simplest qubit gate, which is called x.

Exercise

  • Use the x gate 3 times, and see what happens
initialize = [ ["x","0"] ]
success_condition = {"ZI":1.0}
allowed_gates = { "0":{"x":3}, "1":{}, "both":{} }
vi = [[1],True,True]
qubit_names = {'0':'the only qubit', '1':None}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

This puzzle should have seemed very familar. It was exactly the same as the first one for a bit, except that there was an extra circle that didn't do anything. Other than that, the x gate had exactly the same the effect as the NOT.

Puzzle 2

Intro

The last puzzle had two circles. These did not represent different bits. Instead, they both represented the same qubit.

Qubits are the quantum version of bits. They have some of the same properties as bits, but they also have some extra features.

Just as bits are limited to two possible values, so are qubits. When you extract an output from a qubit, you will get a simple bit value: 0 or 1. However, with qubits there are multiple method that we can use to extract this bit. The result we get depends on the method we use.

The two circles in the last puzzle represent two different ways we could get a bit out of the same qubit. They are called X and Z measurements. The bottom circle represents the output for the Z measurement, and the top circle represents the X output.

The color of the circle is used exactly the same as before. If a circle is black, the corresponding output would give the value 0. A white one means we'd get a 1.

Exercise

  • Turn off the Z output.
initialize = [['x', '0']]
success_condition = {'ZI': 1.0}
allowed_gates = {'0': {'x': 0}, '1': {}, 'both': {}}
vi = [[1], True, True]
qubit_names = {'0':'the only qubit', '1':None}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

The process of extracting a bit from a qubit is called 'measurement'.

It is not possible to independently extract both the X and Z outputs. You have to choose just one.

Puzzle 3

Intro

In this puzzle we'll see another qubit. This will again have its inner workings represented by two circles. For this qubit, those circles will be on the right of the puzzle board.

Exercise

  • Turn the Z output of the other qubit off.
initialize = [['x', '1']]
success_condition = {'IZ': 1.0}
allowed_gates = {'0': {}, '1': {'x': 0}, 'both': {}}
vi = [[0], True, True]
qubit_names = {'0':None, '1':'the other qubit'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

From now on, we'll start calling the qubits by the names used in programs: The one on the left will be q[0], and the one on the right will be q[1].

Also, note that the terms Z output and X ouput are not widely used. It would be more usual to say "output of a Z measurement", etc. But that's a bit verbose for these puzzles.

Puzzle 4

Intro

Now it's time to try a new gate: the h gate.

This is not something that is possible for simple bits. It has the effect of swapping the the two circles of the qubit that it's applied to.

If you want to see this in a nice animated form, check out the Hello Quantum app. But while you are here, test it out with the old trick of repeating three times.

Exercise

  • Use the h gate 3 times.
initialize = []
success_condition = {'ZI': 0.0}
allowed_gates = {'0': {'h': 3}, '1': {}, 'both': {}}
vi = [[1], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)

Outro

Now you have started making quantum programs, you'll see them appear below the puzzle.

These are actual Qiskit programs that can be run on real quantum computers. They can also be represented by so-called circuit diagrams. To see the circuit diagram for your quantum program, run the code cell below.

puzzle.get_circuit().draw(output='mpl')

Puzzle 5

Intro

When the Z output is certain (fully off or on), the x gate simply flips the value. But what if the Z output is random, as we saw in the last puzzle? With this exercise, you'll find out.

Exercise

  • Get the Z output fully off. You can use as many h gates as you like, but use x exactly 3 times.
initialize = [['h', '1']]
success_condition = {'IZ': 1.0}
allowed_gates = {'0': {}, '1': {'x': 3, 'h': 0}, 'both': {}}
vi = [[0], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

It turns out that a random result is just a random result, even if you flip it.

Puzzle 6

Intro

We've seen that the x gate flips the Z output, but it has no effect on the X output. For that we need a new gate: the z gate.

Exercise

  • Turn the X output off.
initialize = [['h', '0'], ['z', '0']]
success_condition = {'XI': 1.0}
allowed_gates = {'0': {'z': 0, 'h': 0}, '1': {}, 'both': {}}
vi = [[1], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

The x gate flips the Z output and the z gate flips the x output. This might seem like a strange way to name things, for now. But as you learn more about quantum computing, it will come to make some sense.

Puzzle 7

Intro

By combining gates we can get new effects. As a simple example, by combining z and h we can do the job of an x.

Exercise

  • Turn on the Z output without using the x gate
initialize = []
success_condition = {'ZI': -1.0}
allowed_gates = {'0': {'z': 0, 'h': 0}, '1': {}, 'both': {}}
vi = [[1], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Puzzle 8

Intro

You might notice that the X outputs are always random when the Z outputs are fully on or off. This is because qubits can never be simultaneously certain about each kind of output. If they are certain about one, the other must be random.

If this wasn't true, we'd be able to use the Z and X outputs to store two bits. This is more memory than a qubit has. Despite the fact that we can extract an output in multiple ways, we are nevertheless not allowed to store more than a bit in a qubit.

Exercise

  • Make the X output be off and the Z output be random.
initialize = [['h', '0']]
success_condition = {'IX': 1.0}
allowed_gates = {'0': {}, '1': {'z': 0, 'h': 0}, 'both': {}}
vi = [[0], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Puzzle 9

Intro

The limited certainty of our qubits can also be shared between the two outputs. For example, we can compromise by making both outputs mostly certain (but not completely certain) about the output they would give.

We will visualize this by using different shades of grey. The darker a circle is, the more likely it's output would be 0. The lighter it is, the more likely the output would be 1.

Exercise

  • Make the two circles for q[1] both light grey. This means that they'd be highly likely, but not certain, to output a 1.
initialize = [['ry(pi/4)', '1']]
success_condition = {'IZ': -0.7071, 'IX': -0.7071}
allowed_gates = {'0': {}, '1': {'z': 0, 'h': 0}, 'both': {}}
vi = [[0], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Puzzle 10

Intro

Now you know the basic tools, you can tackle both qubits at once.

Exercise

  • Make both Z outputs random.
initialize = [['x', '1']]
success_condition = {'ZI': 0.0, 'IZ': 0.0}
allowed_gates = {'0': {'x': 0, 'z': 0, 'h': 0}, '1': {'x': 0, 'z': 0, 'h': 0}, 'both': {}}
vi = [[], True, False]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

Each Z output here would randomly output a 0 or a 1. But will their outputs be correlated? Anti-correlated? Completely unrelated?

Just as we did with bits, we'll keep track of this information with some extra circles.

Puzzle 11

Intro

In this puzzle you'll see four new circles. One of them you have already seen before in Level 1. There it kept track of whether the two bit values would be certain to agree (black) or disagree (white). Here it does the same job for the Z outputs of both qubits.

Exercise

  • Make the Z outputs certain to disagree.
initialize = [['h','0'],['h','1']]
success_condition = {'ZZ': -1.0}
allowed_gates = {'0': {'x': 0, 'z': 0, 'h': 0}, '1': {'x': 0, 'z': 0, 'h': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Puzzle 12

Intro

The new circle at the very top has a similar job. In this case, it keeps track of the probability to agree and disagree for the X outputs of both qubits.

Exercise

  • Make the X outputs certain to agree.
initialize = [['x','0']]
success_condition = {'XX': 1.0}
allowed_gates = {'0': {'x': 0, 'z': 0, 'h': 0}, '1': {'x': 0, 'z': 0, 'h': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

If you are wondering why it is this new circle that represents this information, think of a row extending out from the X output of one qubit, and another row coming out from the X output of the other. The circle at the very top is located where these two rows meet. That's why it represents agreements between the two X outputs.

Puzzle 13

Intro

There are still two new circles to explain. One is where the Z output row from q[0] meets the X output row from q[1]. This tells us whether Z output for q[0] would agree with the X output from q[1]. The other is the same, but with the X output on q[0] and the Z output on q[1].

Exercise

  • Make the X output for q[0] certain to disagree with the Z output for q[1].
initialize = []
success_condition = {'XZ': -1.0}
allowed_gates = {'0': {'x': 0, 'z': 0, 'h': 0}, '1': {'x': 0, 'z': 0, 'h': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Puzzle 14

Intro

Notice how the the x, z and h gates affect the new circles. Specifically, the x gates don't just affect a single Z output, but a whole row of them: flipping each circle from black to white (or dark to light) and vice-versa.

Exercise

  • Turn the two Z outputs on as much as you can.
initialize = [['ry(-pi/4)', '1'], ['ry(-pi/4)','0']]
success_condition = {'ZI': -0.7071, 'IZ': -0.7071}
allowed_gates = {'0': {'x': 0}, '1': {'x': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

As you'll see in the next puzzle, the z gates affect X output rows in the same way.

Puzzle 15

Intro

We've previously seen how the h gate swaps a pair of circles. Now it affects a pair of rows in the same way: swapping each pair of circles until the whole rows have swapped. Again, you may want to check out the Hello Quantum app for a nice animation.

Exercise

  • Turn off the X outputs.
initialize = [['x', '1'], ['x','0']]
success_condition = {'XI':1, 'IX':1}
allowed_gates = {'0': {'z': 0, 'h': 0}, '1': {'z': 0, 'h': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Output

You've now gone through the basics of what two qubits look like and how to manipulate them indivially. But the real fun comes once we start using two qubit gates.

Level 3: Two qubit gates

Puzzle 1

Introduction

In the exercises on bits, we used the CNOT gate. For qubits we have a similiar gate, using the x as the quantum version of the NOT. For this reason, we refer to it in Qiskit programs as cx.

Like the classical CNOT, the cx gate has a 'control' and a 'target'. It effectively looks at what the Z output would be for the control, and uses that to decide whether an x is applied to the target qubit.

When you apply this gate, the qubit you choose will serve as the target. The other qubit will then be the control.

Exercise

  • Use a cx or two to turn on the Z output of q[1], and turn off the Z output of q[0].
initialize = [['x', '0']]
success_condition = {'ZI': 1.0, 'IZ': -1.0}
allowed_gates = {'0': {'cx': 0}, '1': {'cx': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Puzzle 2

Introduction

As well as a cx gate, there is also the cz. This does the same, except that it potentially applies a z to the target instead of an x.

Exercise

  • Turn on the X output of q[0], and turn off the Z output of q[1].
initialize = [['h', '0'],['x', '1']]
success_condition = {'XI': -1.0, 'IZ': 1.0}
allowed_gates = {'0': {'cz': 0}, '1': {}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Puzzle 2b

Introduction

An interesting thing about quantum gates is that there is often multiple ways to explain what they are doing. These explanations can sometimes seem completely incompatible, but they are also equally true.

For example, the cz can also be described as a gate which applies a z to the control qubit, depending on the potential Z output of the target. Exactly the same explanation as before, but with the roles of the qubits reversed. Nevertheless, it is equally true.

Exercise

  • Same as the last exercise, but with the qubits reversed.
initialize = [['h', '1'],['x', '0']]
success_condition = {'IX': -1.0, 'ZI': 1.0}
allowed_gates = {'0': {}, '1': {'cz': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Puzzle 3

Introduction

Now for another completely different, but equally true, explanation of the cz. Like the h gate, we can think of it in terms of circles being swapped. The cz has the effect of:

  • Swapping the X output of q[0] with the neighbouring circle to the top-right;
  • Doing the same with the X output of q[1] (for the neighbour to the top-left). The cz also does something weird with the circle at the top of the grid, but that is a mystery to be solved later!

Again, the Hello Quantum app will give you some nice animations.

Exercise

  • Do the cz twice with each qubit as control, and see what happens.
initialize = [['h', '0'],['x', '1'],['h', '1']]
success_condition = { }
allowed_gates = {'0':{'cz': 2}, '1':{'cz': 2}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

We've now learned something very useful about the cz: it doesn't matter which qubit you choose as control, the cz does the same thing in either case. Because of this, choosing the control qubit for the cz is not required from now on.

Puzzle 3b

Introduction

As mentioned earlier, the X and Z outputs correspond to two ways of getting an output from a qubit: the X and Z measurements. As these names hint, there is also a third way, known as the Y measurement.

In these puzzles we mostly ignore the Y measurement, just to make things a little simpler. However, for a complete description of a qubit we do need to keep track of that the Y output might look like. This just means adding in another couple of rows of circles, for the Y outputs of each qubit.

The next exercise is exactly the same as the last, except that the Y output rows are shown. With these the weird effect seen in the last puzzle becomes not so weird at all. See for yourself!

Exercise

  • Do the cz twice and see what happens.
initialize = [['h', '0'],['x', '1'],['h', '1']]
success_condition = { }
allowed_gates = {'0': {}, '1': {}, 'both': {'cz': 2}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='y')
puzzle.get_circuit().draw(output='mpl')

Outro

Here we see the third swapping of circles done by the cz. The circle at the top (representing correlations for X outputs of both qubits) is swapped with the one in the middle (representing correlations for Y outputs of both qubits).

This seemed strange when the middle rows were missing, just because our description of the qubits wasn't complete. Nevertheless, we'll keep on using the simpler grid without the Y outputs. If you want to add them in yourself, just use the mode='y' argument in hello_quantum.run_game().

Puzzle 4

Introduction

In a previous exercise, you've built an x from a z and some hs. In the same way, it's possible to build a cx from a cz and some hs.

Exercise

  • Turn on the Z output of q[1].
initialize = [['x', '0']]
success_condition = {'IZ': -1.0}
allowed_gates = {'0': {'h':0}, '1': {'h':0}, 'both': {'cz': 0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

Unlike the cz, the cx is not symmetric. If you instead wanted to make a cx whose target was q[0], you would have to do the hs on q[0] instead.

Puzzle 5

Introduction

We were able to interpret the cz 'backwards': an alternate explanation in which the target qubit played the role of control, and vice-versa. We'll now do the same with the cx. However, since this gate doesn't have a symmetric effect, this will be a little more tricky.

Specifically, instead instead of thinking of it as doing an x on the target depending on what the Z output of the control is doing, we can think of it as doing a z to the control depending on what the X output of the target is doing.

In this exercise, you can see how it seems as though the target doing the controlling, and the control being the target!

Exercise

  • Turn on the X output of q[0].
initialize = [['h', '0'],['h', '1']]
success_condition = {'XI': -1.0, 'IX': -1.0}
allowed_gates = {'0': {}, '1': {'z':0,'cx': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

Though these two different stories about how the cx works may seem to be contradictory, they are equally valid descriptions. A great example of the weird and wonderful nature of quantum gates.

Puzzle 6

Introduction

Now we know these two interpretations of a cx, we can do do something pretty useful: turning one around.

In this puzzle you'll get a cx with q[1] as the target, but you'll need one with q[0] as target. See if you can work out how to get the same effect with the help pf some h gates.

Exercise

  • Keep the Z output of q[1], but turn the Z output of q[0] off.
initialize = [('x','0'),('x','1')]
success_condition = {'ZI': 1.0,'IZ': -1.0}
allowed_gates = {'0': {'h':0}, '1': {'h':0,'cx':0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

If you remember anything from these exercises, it should probably be this. It is common for real qubit devices to limit which way around you can do the cx, so the ability to turn them around comes in very handy.

Puzzle 7

Introduction

Another useful quantum gate is the swap. This does exactly what the name suggests: it swaps the states of two qubits. Though Qiskit allows us to simply invoke the swap command, it is more interesting to make this gate ourselves out of cz or cx gates.

Exercise

  • Swap the two qubits:
    • Make the Z output white and the X output grey for q[0];
    • Make the Z output dark grey and the X output light grey for q[1].
initialize = [['ry(-pi/4)','0'],['ry(-pi/4)','0'],['ry(-pi/4)','0'],['x','0'],['x','1']]
success_condition = {'ZI': -1.0,'XI':0,'IZ':0.7071,'IX':-0.7071}
allowed_gates = {'0': {'h':0}, '1': {'h':0}, 'both': {'cz': 0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

Note that your solution to this puzzle might not have been a general purpose swap. Compare your solution to those for the next few puzzles, which also implement swaps.

Puzzle 8

Intro

Here's another puzzle based on the idea of making a swap.

Exercise

  • Swap the two qubits:
    • Make the X output black for q[0].
    • Make the Z output white for q[1].
  • And do it with 3 cz gates
initialize = [['x','0'],['h','1']]
success_condition = {'XI':1,'IZ':-1}
allowed_gates = {'0': {'h':0}, '1': {'h':0}, 'both': {'cz':3}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names,shots=2000)
puzzle.get_circuit().draw(output='mpl')

Puzzle 9

Intro

And another swap-based puzzle.

Exercise

  • Swap the two qubits:
    • Turn on the Z output for q[0].
    • Turn off the Z output q[1].
initialize = [['x','1']]
success_condition = {'IZ':1.0,'ZI':-1.0}
allowed_gates = {'0': {'h':0}, '1': {'h':0}, 'both': {'cz':0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names,shots=2000)
puzzle.get_circuit().draw(output='mpl')

Level 4: Beyond Clifford gates

Puzzle 1a

Introduction

The gates you've seen so far are called the 'Clifford gates'. They are very important for moving and manipulating information in quantum computers. However, we cannot create algorithms that can outperform standard computers using Clifford gates alone. We need some new gates.

This puzzle has one for you to try. Simply do it a few times, and see if you can work out what it does.

Exercise

  • Apply ry(pi/4) four times to q[0].
initialize = []
success_condition = {}
allowed_gates = {'0': {'ry(pi/4)': 4}, '1': {}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names)
puzzle.get_circuit().draw(output='mpl')

Outro

Well done if you were able to work it out! For the rest of us, let's try something new to help us figure it out.

Puzzle 1b

Introduction

To better understand the gate we just saw, we are going to use to a slightly differemt way to visualize the qubits. In this an output that is certain to give 0 will be represented by a white line rather than a white circle. An output certain to give 1 will be a black line instead of a black circle. For a random output, you'll see a line that's part white and part black instead of a grey circle.

Here's an old exercise to help you get used to this new visualization.

Exercise

  • Make the X outputs certain to agree.
initialize = [['x','0']]
success_condition = {'XX': 1.0}
allowed_gates = {'0': {'x': 0, 'z': 0, 'h': 0}, '1': {'x': 0, 'z': 0, 'h': 0}, 'both': {}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='line')
puzzle.get_circuit().draw(output='mpl')

Puzzle 1c

Introduction

In this puzzle you'll see a new thing that you can do: bloch. This isn't actually a gate, and won't show up in the quantum program. Instead it just changes the visualization, by drawing the two lines for each qubit on top of each other. It also puts a point where their levels intersect. Using bloch, you should hopefully be able to figure out how ry(pi/4) works.

Exercise

  • Turn the bottom line of q[0] fully on, and use the bloch gate.
initialize = []
success_condition = {'ZI': -1.0}
allowed_gates = {'0': {'bloch':1, 'ry(pi/4)': 0}, '1':{}, 'both': {'unbloch':0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='line')
puzzle.get_circuit().draw(output='mpl')

Outro

If you followed the points, you should have noticed that the effect of ry(pi/4) is to rotate them by $\pi/4$ radians (or 45 degrees). The levels of the lines also change to match. The effect of the ry(-pi/4) gate would be the same, except the rotation is in the other direction

Alaos, as you probably noticed, using bloch doesn't just combine the two lines for each qubit. It combines their whole rows.

Puzzle 2

Introduction

Now let's use these gates on the other qubit too.

Exercise

  • Turn the bottom lines fully on.
initialize = [['h','0'],['h','1']]
success_condition = {'ZI': -1.0,'IZ': -1.0}
allowed_gates = {'0': {'bloch':0, 'ry(pi/4)': 0, 'ry(-pi/4)': 0}, '1': {'bloch':0, 'ry(pi/4)': 0, 'ry(-pi/4)': 0}, 'both': {'unbloch':0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='line')
puzzle.get_circuit().draw(output='mpl')

Puzzle 3

Introduction

Here's a puzzle you could solve with a simple cx, or a cz and some hs. Unfortunately you have neither cx nor h, so you'll need to work out how cz and rys can do the job.

Exercise

  • Make the Z outputs agree.
initialize = [['h','0']]
success_condition = {'ZZ': 1.0}
allowed_gates = {'0': {}, '1': {'bloch':0, 'ry(pi/4)': 0, 'ry(-pi/4)': 0}, 'both': {'unbloch':0,'cz':0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='line')
puzzle.get_circuit().draw(output='mpl')

Puzzle 4

Introduction

Using xs or zs you can effectively reflect an ry, to make it move in the opposite direction.

Exercise

  • Turn the Z outputs fully off with just one ry(pi/4) on each.
initialize = [['ry(pi/4)','0'],['ry(pi/4)','1']]
success_condition = {'ZI': 1.0,'IZ': 1.0}
allowed_gates = {'0': {'bloch':0, 'z':0, 'ry(pi/4)': 1}, '1': {'bloch':0, 'x':0, 'ry(pi/4)': 1}, 'both': {'unbloch':0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='line')
puzzle.get_circuit().draw(output='mpl')

Puzzle 5

Introduction

With the rys, we can make conditional gates that are more interesting than just cz and cx. For example, we can make a controlled-h.

Exercise

  • Turn off the Z output for q[1] using exactly one ry(pi/4) and ry(-pi/4) on that qubit.
initialize = [['x','0'],['h','1']]
success_condition = {'IZ': 1.0}
allowed_gates = {'0': {}, '1': {'bloch':0, 'cx':0, 'ry(pi/4)': 1, 'ry(-pi/4)': 1}, 'both': {'unbloch':0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='line')
puzzle.get_circuit().draw(output='mpl')

Bonus Level: Sandbox

You now know enough basic quantum gates to build fully powerful quantum programs. You'll get a taste of this in the final level. But before then, here are a couple of bonus levels.

Firstly, a sandbox to try out your new skills: two grids with all the gates enabled, so you can have a play around.

Here's one with the line-based visualization, to help with the non-Clifford gates.

initialize = []
success_condition = {'IZ': 1.0,'IX': 1.0}
allowed_gates = {'0': {'bloch':0, 'x':0, 'z':0, 'h':0, 'cx':0, 'ry(pi/4)': 0, 'ry(-pi/4)': 0}, '1': {'bloch':0, 'x':0, 'z':0, 'h':0, 'cx':0, 'ry(pi/4)': 0, 'ry(-pi/4)': 0}, 'both': {'cz':0, 'unbloch':0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
line_sandbox = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='line')
line_sandbox.get_circuit().draw(output='mpl')

Here is a grid with the middle lines, which describe outputs for y measurements. With this you can also try some new non-Clifford gates: rx(pi/4' and rx(-pi/4).

initialize = []
success_condition = {'IZ': 1.0,'IX': 1.0}
allowed_gates = {'0': {'x':0, 'z':0, 'h':0, 'cx':0, 'ry(pi/4)': 0, 'rx(pi/4)': 0, 'ry(-pi/4)': 0, 'rx(-pi/4)': 0}, '1': {'x':0, 'z':0, 'h':0, 'cx':0, 'ry(pi/4)': 0, 'rx(pi/4)': 0, 'ry(-pi/4)': 0, 'rx(-pi/4)': 0}, 'both': {'cz':0}}
vi = [[], True, True]
qubit_names = {'0':'q[0]', '1':'q[1]'}
y_sandbox = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='y')
y_sandbox.get_circuit().draw(output='mpl')

Bonus Level: Make your own puzzles

As well as giving you some puzzles, we've also given you the opportunity to make and share puzzles of your own. Using the Quantum Experience you can easily make your own notebook full of your own puzzles.

Start by clicking the 'New Notebook' button on this page. Then add the following line to the list of imports.

from qiskit_textbook.games import hello_quantum

This gives you the tools you need for making puzzles. Specifically, you need to create your own call to the run_game function using the following form.

puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode=None)

All the elements of this are explained below.

puzzle

  • This is an object containing the puzzle you create.
  • The quantum circuit made by the player can be accessed using puzzle.get_circuit().

initialize

  • A list of gates applied before the puzzle begins, to prepare the initial state.
  • If this is empty, the default initial state of qubits is used (the Z output is certain to be 0).
  • Supported single qubit gates (applied to qubit '0' or '1') are 'x', 'y', 'z', 'h' and 'ry(pi/4)'.
  • Supported two qubit gates are 'cz' and 'cx'. For these, specify only the target qubit.
  • Example: initialize = [['x', '0'],['cx', '1']].

success_condition

  • Values for pauli observables that must be obtained for the puzzle to declare success.
  • Expressed as a dictionary whose keys are the names of the circles in the following form:
    • 'ZI' refers to the Z output of the qubit on the left, and 'XI' for its X output.
    • 'IZ' and 'IX' are similarly the outputs for the qubit on the right.
    • 'ZX' refers to the circle for correlations between Z output for the qubit on the left and the X output on the right.
    • And so on.
  • The values of the dictionary are the values that these circles must hold for the puzzle to be completed.
    • 1.0 for black.
    • -1.0 for white.
    • 0.0 for grey.
    • And so on.
  • Only the circles you want to be included in the condition need to be listed.
  • Example: success_condition = {'IZ': 1.0}.

allowed_gates

  • For each qubit, specify which operations are allowed in this puzzle.
  • For operations that don't need a qubit to be specified ('cz' and 'unbloch'), assign the operation to 'both' instead of qubit '0' or '1'.
  • Gates are expressed as a dictionary with an integer as the value.
    • If the integer is non-zero, it specifies the exact number of times the gate must be used for the puzzle to be successfully solved.
    • If it is zero, the player can use the gate any number of times.
  • Example: allowed_gates = {'0': {'h':0}, '1': {'h':0}, 'both': {'cz': 1}}.

vi

  • Some visualization information as a three element list: vi=[hidden,qubit,corr]. These specify:
    • hidden: Which qubits are hidden (empty list if both shown).
    • qubit: Whether both circles shown for each qubit? (use True for qubit puzzles and False for bit puzzles).
    • corr: Whether the correlation circles (the four in the middle) are shown.
  • Example: vi = [[], True, True].

qubit_names

  • The two qubits are always called '0' and '1' internally. But for the player, we can display different names.
  • Example: qubit_names = {'0':'qubit 0', '1':'qubit 1'}

mode

  • An optional argument, which allows you to choose whether to include the Y output rows or use the line-based visualization.
  • Using mode=None, or not including the mode argument at all, gives the default mode.
  • mode='y' includes the Y output rows.
  • mode='line' gives the line-based visualization.

The puzzle defined by the examples given here can be run in the following cell.

initialize = [['x', '0'],['cx', '1']]
success_condition = {'IZ': 1.0}
allowed_gates = {'0': {'h':0}, '1': {'h':0}, 'both': {'cz': 1}}
vi = [[], True, True]
qubit_names = {'0':'qubit 0', '1':'qubit 1'}
mode = None
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode=mode)

Remember to tell players what they should be aiming for. For all the puzzles in this notebook, the target state was described with text. But you can instead create an image of the target state, including a short message for the player, in the following way. This again uses the example parameters provided above.

message = '\nRules:\n    Use exactly one cz gate.'
grid = hello_quantum.pauli_grid(mode=mode)
grid.update_grid(rho=success_condition, hidden=vi[0], qubit=vi[1], corr=vi[2], message=message)

Note that only the circles that need to be matched are shown.

To share your puzzles with someone else, simply save the notebook you created and send it to them. They can play by opening an running on the Quantum Experience, using the 'Import' button on this page.

Level 5: Proving the Uniqueness of Quantum Variables

Bell test for classical variables

Here we'll investigate how quantum variables (based on qubits) differ from standard ones (based on bits).

We'll do this by creating a pair of variables, which we will call A and B. We aren't going to put any conditions on what these can be, or how they are initialized. So there are a lot of possibilities:

  • They could be any kind of variable, such as
    • integer
    • list
    • dictionary
    • ...
  • They could be initialized by any kind of process, such as
    • left empty
    • filled with a given set of values
    • generated by a given random process
      • indepedently applied to A and B
      • applied to A and B together, allowing for correlations between their randomness

If the variables are initialized by a random process, it means they'll have different values every time we run our program. This is perfectly fine. The only rule we need to obey is that the process of generating the randomness is the same for every run.

We'll use the function below to set up these variables. This currently has A and B defined as to be partially correlated random floating point numbers. But you can change it to whatever you want.

import random
def setup_variables ():
    
    ### Replace this section with anything you want ###
    
    r = random.random()
    
    A = r*(2/3)
    B = r*(1/3)
    
    ### End of section ###
    
    return A, B

Our next job is to define a hashing function. This simply needs to take one of the variables as input, and then give a bit value as an output.

This function must also be capable of performing two different types of hash. So it needs to be able to be able to chew on a variable and spit out a bit in to different ways. We'll therefore also need to tell the function what kind of hash we want to use.

To be consistent with the rest of the program, the two possible hash types should be called 'H' and 'V'. Also, the output must be in the form of a single value bit string: either '0' or '1'.

In the (fairly arbitrary) example given, the bits were created by comparing A and B to a certain value. The output is '1' if they are under that value, and '0' otherwise. The type of hash determines the value used.

def hash2bit ( variable, hash ):
    
    ### Replace this section with anything you want ###
    
    if hash=='V':
        bit = (variable<0.5)
    elif hash=='H':
        bit = (variable<0.25)
        
    bit = str(int(bit)) # Turn True or False into '1' and '0'
    
    ### End of section ###
        
    return bit

Once these are defined, there are four quantities we wish to calculate: P['HH'], P['HV'], P['VH'] and P['VV'].

Let's focus on P['HV'] as an example. This is the probability that the bit value derived from an 'H' type hash on A is different to that from a 'V' type has on B. We will estimate this probability by sampling many times and determining the fraction of samples for which the corresponding bit values disagree.

The other probabilities are defined similarly: P['HH'] compares a 'H' type hash on both A and B, P['VV'] compares a V type hash on both, and P['VH'] compares a V type hash on A with a H type has on B.

These probabilities are calculated in the following function, which returns all the values of P in a dictionary. The parameter shots is the number of samples we'll use.

shots = 8192
def calculate_P ( ):
    
    P = {}
    for hashes in ['VV','VH','HV','HH']:
        
        # calculate each P[hashes] by sampling over `shots` samples
        P[hashes] = 0
        for shot in range(shots):

            A, B = setup_variables()

            a = hash2bit ( A, hashes[0] ) # hash type for variable `A` is the first character of `hashes`
            b = hash2bit ( B, hashes[1] ) # hash type for variable `B` is the second character of `hashes`

            P[hashes] += (a!=b) / shots
 
    return P

Now let's actually calculate these values for the method we have chosen to set up and hash the variables.

P = calculate_P()
print(P)

These values will vary slightly from one run to the next due to the fact that we only use a finite number of shots. To change them significantly, we need to change the way the variables are initiated, and/or the way the hash functions are defined.

No matter how these functions are defined, there are certain restrictions that the values of P will always obey.

For example, consider the case that P['HV'], P['VH'] and P['VV'] are all 0.0. The only way that this can be possible is for P['HH'] to also be 0.0.

To see why, we start by noting that P['HV']=0.0 is telling us that hash2bit(A, H) and hash2bit(B, V) were never different in any of the runs. So this means we can always expect them to be equal.

hash2bit(A, H) = hash2bit(B, V)        (1)

From P['VV']=0.0 and P['VH']=0.0 we can similarly get

hash2bit(A, V) = hash2bit(B, V)        (2)

hash2bit(A, V) = hash2bit(B, H)        (3)

Putting (1) and (2) together implies that

hash2bit(A, H) = hash2bit(A, V)        (4)

Combining this with (3) gives

hash2bit(A, H) = hash2bit(B, H)        (5)

And if these values are always equal, we'll never see a run in which they are different. This is exactly what we set out to prove: P['HH']=0.0.

More generally, we can use the values of P['HV'], P['VH'] and P['VV'] to set an upper limit on what P['HH'] can be. By adapting the CHSH inequality we find that

$\,\,\,\,\,\,\,$ P['HH'] $\, \leq \,$ P['HV'] + P['VH'] + P['VV']

This is not just a special property of P['HH']. It's also true for all the others: each of these probabilities cannot be greater than the sum of the others.

To test whether this logic holds, we'll see how well the probabilities obey these inequalities. Note that we might get slight violations due to the fact that our the P values aren't exact, but are estimations made using a limited number of samples.

def bell_test (P):
    
    sum_P = sum(P.values())
    for hashes in P:
        
        bound = sum_P - P[hashes]
        
        print("The upper bound for P['"+hashes+"'] is "+str(bound))
        print("The value of P['"+hashes+"'] is "+str(P[hashes]))
        if P[hashes]<=bound:
            print("The upper bound is obeyed :)\n")
        else:
            if P[hashes]-bound < 0.1:
                print("This seems to have gone over the upper bound, but only by a little bit :S\nProbably just rounding errors or statistical noise.\n")
            else:
                print("!!!!! This has gone well over the upper bound :O !!!!!\n")
bell_test(P)

With the initialization and hash functions provided in this notebook, the value of P('HV') should be pretty much the same as the upper bound. Since the numbers are estimated statistically, and therefore are slightly approximate due to statistical noise, you might even see it go a tiny bit over. But you'll never see it significantly surpass the bound.

If you don't believe me, try it for yourself. Change the way the variables are initialized, and how the hashes are calculated, and try to get one of the bounds to be significantly broken.

Bell test for quantum variables

Now we are going to do the same thing all over again, except our variables A and B will be quantum variables. Specifically, they'll be the simplest kind of quantum variable: qubits.

When writing quantum programs, we have to set up our qubits and bits before we can use them. This is done by the function below. It defines a register of two bits, and assigns them as our variables A and B. It then sets up a register of two bits to receive the outputs, and assigns them as a and b.

Finally it uses these registers to set up an empty quantum program. This is called qc.

from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit

def initialize_program ():
    
    qubit = QuantumRegister(2)
    A = qubit[0]
    B = qubit[1]
    
    bit = ClassicalRegister(2)
    a = bit[0]
    b = bit[1]
    
    qc = QuantumCircuit(qubit, bit)
    
    return A, B, a, b, qc

Before we start writing the quantum program to set up our variables, let's think about what needs to happen at the end of the program. This will be where we define the different hash functions, which turn our qubits into bits.

The simplest way to extract a bit from a qubit is through the measure gate. This corresponds to the Z output of a qubit in the visualization we've been using. Let's use this as our V type hash.

For the output that corresponds to the X output, there is no direct means of access. However, we can do it indirectly by first doing an h to swap the top and Z outputs, and then using the measure gate. This will be our H type hash.

Note that this function has more inputs that its classical counterpart. We have to tell it the bit on which to write the result, and the quantum program, qc, on which we write the gates.

def hash2bit  ( variable, hash, bit, qc ):
    
    if hash=='H':
        qc.h( variable )
        
    qc.measure( variable, bit )

Now its time to set up the variables A and B. To write this program, you can use the grid below. You can either follow the suggested exercise, or do whatever you like. Once you are ready, just move on. The cell containing the setup_variables() function, will then use the program you wrote with the grid.

Note that our choice of means that the probabilities P['HH'], P['HV'], P['VH'] and P['VV'] will explicitly correspond to circles on our grid. For example, the circle at the very top tells us how likely the two X outputs would be to disagree. If this is white, then P['HH']=1 , if it is black then P['HH']=0.

Exercise

  • Make it so that the X outputs of both qubits are more likely to disagree, whereas all other combinations of outputs are more likely to agree.
initialize = []
success_condition = {'ZZ':+0.7071,'ZX':+0.7071,'XZ':+0.7071,'XX':-0.7071}
allowed_gates = {'0': {'bloch':0, 'x':0, 'z':0, 'h':0, 'cx':0, 'ry(pi/4)': 0, 'ry(-pi/4)': 0}, '1': {'bloch':0, 'x':0, 'z':0, 'h':0, 'cx':0, 'ry(pi/4)': 0, 'ry(-pi/4)': 0}, 'both': {'cz':0, 'unbloch':0}}
vi = [[], True, True]
qubit_names = {'0':'A', '1':'B'}
puzzle = hello_quantum.run_game(initialize, success_condition, allowed_gates, vi, qubit_names, mode='line')

Now the program as written above will be used to set up the quantum variables.

import numpy as np
def setup_variables ( A, B, qc ):
    
    for line in puzzle.program:
        eval(line)

The values of P are calculated in the function below. In this, as in the puzzles in the rest of this notebook, this is done by running the job using Qiskit and getting results which tell us how many of the samples gave each possible output. The output is given as a bit string, string, which Qiskit numbers from right to left. This means that the value of a, which corresponds to bit[0] is the first from the right

a = string[-1]

and the value of b is right next to it at the second from the right

b = string[-2]

The number of samples for this bit string is provided by the dictionary of results, stats, as stats[string].

shots = 8192
from qiskit import execute

def calculate_P ( backend ):
    
    P = {}
    program = {}
    for hashes in ['VV','VH','HV','HH']:

        A, B, a, b, program[hashes] = initialize_program ()

        setup_variables( A, B, program[hashes] )

        hash2bit ( A, hashes[0], a, program[hashes])
        hash2bit ( B, hashes[1], b, program[hashes])
            
    # submit jobs
    job = execute( list(program.values()), backend, shots=shots )

    # get the results
    for hashes in ['VV','VH','HV','HH']:
        stats = job.result().get_counts(program[hashes])
        
        P[hashes] = 0
        for string in stats.keys():
            a = string[-1]
            b = string[-2]
            
            if a!=b:
                P[hashes] += stats[string] / shots

    return P

Now its time to choose and set up the actually device we are going to use. By default, we'll use a simulator. You could instead use a real cloud-based device by changing the backend accordingly.

from qiskit import Aer
device = 'qasm_simulator'
backend = Aer.get_backend(device)
P = calculate_P( backend )
print(P)
bell_test( P )

If you prepared the state suggestion by the exercise, you will have found a significant violation of the upper bound for P['HH']. So what is going on here? The chain of logic we based the Bell test on obviously doesn't apply to quantum variables. But why?

The answer is that there is a hidden assumption in that logic. To see why, let's revisit point (4).

hash2bit ( A, H ) = hash2bit ( A, V )        (4)

Here we compare the value we'd get from an H type of hash of the variable A with that for a V type hash.

For classical variables, this is perfectly sensible. There is nothing stopping us from calculating both hashes and comparing the results. Even if calculating the hash of a variable changes the variable, that's not a problem. All we need to do is copy it beforehand and we can do both hashes without any issue.

The same is not true for quantum variables. The result of the hashes is not known until we actually do them. It's only then that the qubit actually decides what bit value to give. And once it decides the value for one type of hash, we can never determine what it would have decided if we had used another type of hash. We can't get around this by copying the quantum variables either, because quantum variables cannot be copied. This means there is no context in which the values hash2bit(A,H) and hash2bit(A,V) are well-defined at the same time, and so it is impossible to compare them.

Another hidden assumption is that hash2bit(A,hash) depends only on the type of hash chosen for variable A, and not the one chosen for variable B. This is also perfectly sensible, since this exactly the way we set up the hash2bit() function. However, the very fact that the upper bound was violated does seem to imply that each variable knows what hash is being done to the other, so they they can conspire to give very different behaviour when both have a H type hash.

Even so, we cannot say that our choice of hash on one qubit affects the outcome on the other. The effect is more subtle than that. For example, it is impossible to determine which variable is affecting which: You can change the order in which the hashes are done, or effectively do them at the same time, and you'll get the same results. What we can say is that the results are contextual: to fully understand results from one variable, it is sometimes required to look at what was done to another.

All this goes to show that quantum variables don't always follow the logic we are used to. They follow different rules, the rules of quantum mechanics, which will allow us to find ways of performing computation in new and different ways.