QuDotPy Tutorial

Welcome to the QuDotPy Tutorial! This is meant to be an interactive tutorial where you learn about the QuDotPy library through examples. Please see the README file for installation instructions, the only external dependency is numpy. Once you have QuDotPy on your machine its time to start exploring quantum computation! This is not an introduction to quantum computing or quantum mechanics (Although that is something I am working on, stay tuned...). This is meant to show you how to use QuDotPy to DO quantum computing. QuDotPy can be used to explore basic qubits, build arbitrary quantum states, emulate measurement/collapse, apply quantum gates to quantum states, build your own gates and to build quantum circuits. So lets get started and import qudotpy:

The most fundamental property of quantum computing is the qubit. This is a two level quantum system which is a bunch of fancy words for saying it is something that can have two possible values. You can call those two values what you like, but the standard is to call them |0> and |1> (The Computational Basis) or |+> and |-> (The Hadamard Basis). QuDotPy supports qubits through the QuBit class. We support both the computation and Hadamard basis. The QuBit class is meant to represent a single-qubit system. Multiple-qubit systems are described further down this tutorial. QuDotPy displays qubits using Dirac Notation. They are stored as numpy arrays and the underlying array can be access with the ket and bra properties.

Quantum gates are the main logical units in quantum computing, much like the logic gates of classical computing (AND, OR, XOR etc). QuDotPy has predefined gates:

qudot.H: Hadamard Gate

qudot.X: Pauli-X Gate

qudot.Z: Pauli-Z Gate

qudot.Y: Pauli-Y Gate

qudot.CNOT: CNOT Gate

Also, you can create custom quantum gates via the QuGate class. There are multiple ways to initialize a custom QuGate. You can initialize through a string representation of the gate, through a multiplication of existing gates, or through a tensor product of existing gates. For example, the string representation for the Z gate is ("1 0; 0 -1"). We do require that quantum gates are unitary transformations. This is again fancy talk that says the gate changes (transforms) the qubit into another qubit but preserves the angle between qubits. A QuGate is represented as a numpy matrix. You can access two properties: matrix and dagger. matrix returns the matrix representation of the gate and dagger returns the Hermitian of the matrix.

You apply a gate to a qubit or a quantum state (quantum states are covered later). To apply a gate to a qubit and return the result use the module level apply_gate method. This will apply the gate to the input and return a new state as output. Lastly, we override the == and != operators so you can test gate equality.

So far we have been working with single-qubit systems represented by the QuBit class, such as |0> or |+>. Now it's time to have some real fun with multiple-qubit systems. Multiple-qubit systems are combinations of single-qubit systems, a|001> + b|100> + c|111> is an example of such a system. QuDotPy has a class called QuState to represent such systems. QuState is the main workhorse of QuDotPy, it supports creating multiple-qubit states from various input, can make measurement predictions, can measure and collapse the state, and can apply a gate to the state. There are two basic properties that give you information about the size of the QuState:

num_qubits: this tells you how many qubits the state is built from. |01000> would be 5 qubits

hilbert_dimension: this tells you the dimensionality of the associated Hilbert Space, a.k.a how many elements your state vector has

So lets see QuState in action by initializing some quantum states. There are five different ways to init a QuState:

init from a state map QuState(state_map): This is the default initialization method as is the most extensible. The input is a map whose keys are the states and values are the probability amplitudes.

init from a list of states QuState.init_from_state_list([list_of_states]): This is a convenience class method that will create a QuState with the states specified in list_of_states and equal probability amplitudes.

init superposition QuState.init_superposition(dimension): This is a convenience class method that will create a QuState that is a superposition of all states in the Hilbert space of the specified dimension

init from vector QuState.init_from_vector(column_vector): This is a convenience class method that will create a QuState from the specified column_vector, which is expected to have the form of a numpy column_vector

init zeros QuState.init_zeros(num_bits): This is a convenience class method that will create a QuState which has just one state, the |0...n> state. The number of zeros is determined by num_bits

QuState is built on top of the computational basis. You can initialize with Hadamard elements |+>, |-> but under the scenes it will be converted to the computational basis. This actually lets us test something in quantum computing books. They claim that the bell states are the same in both computational and Hadamard basis.

So 1/sqrt(2)|++> + 1/sqrt(2)|--> = 1/sqrt(2)|00> + 1/sqrt(2)|++>

I find this hard to believe... I mean common. Let's test it:

In [35]:

bell_1=qudot.QuState.init_from_state_list(["++","--"])printbell_1

0.707106781187|00> + 0.707106781187|11>

In [36]:

bell_2=qudot.QuState({"++":qudot.ROOT2,"--":-qudot.ROOT2})printbell_2

0.707106781187|01> + 0.707106781187|10>

In [37]:

bell_3=qudot.QuState.init_from_state_list(["-+","+-"])printbell_3

0.707106781187|00> + -0.707106781187|11>

In [38]:

bell_4=qudot.QuState({"-+":qudot.ROOT2,"+-":-qudot.ROOT2})printbell_4

0.707106781187|01> + -0.707106781187|10>

Let this be a lesson to you: Nature does not care about your intuition.....

Moving on, sometimes you want an even superposition of all states in a Hilbert space:

As we all know, the strangest thing about quantum mechanics is measurements. We know that we can only predict probabilistic outcomes of measurements. The QuState incorporates this into it's design. You can ask for the possible measurement of a state and you will get back a map where the key is the possible state and the value is the probability of getting that state. Also, you can ask about the possible measurements of a specific qubit. For example, if your state is a|0100> + b|1101> + c|1111> and you ask the for the possible measurements of qubit 2, you will get |1> with probability 1. Whereas qubit 1 can be |0> or |1>.

Also, you can perform a measurement on the state. A measurement will collapse the state. So after you perform a measurement the QuState will be in a definite state not a superposition. Note that the QuState is designed to respect the probabilities when collapsing. That means if you have an ensemble of QuStates, as the ensembles get larger then the probability of collapsing to a specified state will approach the |amplitude|^2

# possible measurements of first qubitprintstate.possible_measurements(qubit_index=1)

{u'|0>': 0.99999999999999989}

In [50]:

printstate.possible_measurements(2)

{u'|0>': 0.74999999999999989, u'|1>': 0.25}

In [51]:

printstate.possible_measurements(3)

{u'|0>': 0.49999999999999989, u'|1>': 0.5}

In [52]:

printstate.possible_measurements(4)

{u'|0>': 0.99999999999999989}

In [53]:

state.measure()printstate

1.0|0010>

Now lets explore measuring ensembles. As we noted earlier, as our ensemble gets larger than the probability of measuring a specific state approaches its amplitude magnitude squared. However, if you have very few states then the probabilities will not be exact.

So hopefully this explains how your probabilities approach theory as your ensemble gets larger. Its basically the law of large numbers. This is a summary of the results for state |0000>. The theory predicts we should get this state 50% of the time, as the ensemble gets larger, we get closer to the theoretical result:

Ok great, now we are experts on QuStates! So far we have not done any actual quantum computing with QuStates. Let's remedy that! To do quantum computing we need to start applying gates to QuStates. A QuState has the apply_gate(qu_gate, qubit_list=None) method. This method applies a QuGate (such as qudot.X, qudot.Z) to the entire state OR to specific qubits of the state.

One important thing to note is that apply_gate will automatically scale your QuGate to the appropriate dimension. For example, qudot.X is a 2x2 matrix, but what if your state is 0.707|0000> + 0.5|0010> + 0.5|0110>? This would require qudot.X to be a 16x16 matrix. Don't worry! QuDotPy to the rescue! QuDotPy is smart, it recognizes this fact and tensors the gates with themselves until the appropriate dimension is reached. In a similar way, we are able to build larger matrices to apply a gate to a specific qubits. This way you can apply a QuGate to only the 1st and 3rd qubits if that is what your heart desires. Lets see this in action

That does it for QuState! I think after these last two examples you see how to develop quantum circuits. However, QuDotPy makes it even easier to build circuits! It also allows you to step through a quantum circuit debugger style! Lets see how

Now you should have gotten the idea from last section that it will be very easy to implement a quantum circuit: just use QuState.apply_gate() repeatedly for your design. You are pretty much right, but we can do even better. We would like to abstract away the actual circuit from the input state. That way we can run the circuit on different input states and examine the output. Also, wouldn't it be cool if you can step through a quantum circuit like you step through a debugger in your code? I think so! That is basically how QuCircuit was born.

The main idea behind QuCircuit is that a quantum circuit can be thought of as a list of operations. Each operation tells you to apply a quantum gate to certain qubits or an entire state. So, QuCircuit is initialized with a list of tuples. Each tuple represents a single operation and has the form (QuGate, qubit_list or None). The first element of the tuple is the QuGate to apply. The second argument is either a qubit_list to apply the gate to or None, which will apply the QuGate to the entire state.

To run a QuCircuit you must first set the in_qu_state attribute. This gives the input state that the circuit will run on. Then you just call run_circuit() and in_qu_state will have the result. You can also step through the circuit one operation at a time using the step_circuit() method. This will return your current index on the operations list. You can always check on which operation you are on by inspecting the step_op_index attribute. Also, if you want to reset the circuit to the beginning you can call the reset_circuit() method.

Lets look at an example by making a circuit that produces Bell states. This circuit has the following mappings (excluding normalization constants):