Path: blob/main/quick-start-to-quantum/solutions/02_quick_start_to_quantum_solution.ipynb
1127 views
Quick start to Quantum Computing with CUDA-Q
Lab 2 - Move onto Bigger and More Entangled Things: Programming with multiple qubits
Overview
We'll continue to use the same template for creating a quantum program as in Lab 1:
Encode information into the quantum state by initializing qubit(s)
Manipulate the quantum state of the qubit(s) with quantum gate(s)
Extract information from the quantum state by measuring the state of the qubit(s)
These three steps are outlined in the diagram below:

However, in this lab, in addition to programming with more than one qubit, we'll use this opportunity to demonstrate a practical programming practice of creating "subkernels" (similar to subroutines in a classical program) that can be reused and combined.
What you'll do:
Through the interactive code blocks and exercises in this lab, you will
Recognize allowable arguments accepted into a cudaq kernel function
Apply a variety of single and multi-qubit gates to qubits using CUDA-Q and reason about the outcome of these operations
Write quantum kernels that accept individual qubits or a register of qubits as arguments
Write and apply kernels as subkernels (i.e., subroutines)
Read and interpret quantum circuit diagrams
Encode classical information (in particular, integers) into qubits using binary notation and the
xgate
Terminology you'll use:
Computational basis states (basis states for short)
Probability amplitude
Statevector
(optional) Phase, relative phase, global phase
Entanglement
Interference
Control and target for multi-qubit controlled-operations
CUDA-Q syntax you'll use:
defining a quantum kernel:
@cudaq.kernel,qubit initialization:
cudaq.qvectorandcudaq.qubitquantum gates:
x,h,y,z,t,s,swap, and thectrlmethodextracting information from a kernel:
get_state,get_state.amplitude(),samplevisualization tools:
add_to_bloch_sphere,show,draw
Execute the cell below to load all the necessary packages for this lab.
2.1 Program with more than one qubit
In this section, we'll create two quantum programs to illustrate the concepts of global phase and entanglement. Additionally, these examples will allow us to demonstrate alternative ways of writing and combining multiple kernels — a useful technique to have on hand as our quantum algorithms grow in the number of qubits and the number of gate operations. Both the number of qubits and the number of gate operations are limiting factors in the kind of quantum algorithms that can be run on current quantum hardware or simulated on GPUs. In fact, we'll quickly see examples of quantum algorithms that we can program, but cannot implement with today's existing technology.
2.1.1 Notation for a 2-qubit state
In Lab 1, we saw that the quantum state of a single qubit could be written as linear combinations of the states and . The states and are often referred to as the computational basis states of a single qubit. When we have 2 qubits, we will need basis states to describe all possible quantum states of 2 qubits because each qubit could be measured as either a or a . The computational basis states used to describe a state of 2 qubits are
What does this notation mean? The expression represents the state of a system of 2 qubits, (e.g., and ), where qubit is in the state and is in the state . In other words, the left most term (the in ) corresponds to qubit 's state and the rightmost term will correspond to the state of the 2nd qubit. Since there is no agreed upon standard ordering of qubits, this notational choice may differ from other quantum programming languages that you might run across.
The general form of a quantum state of 2 qubits, is a linear combination (or a superposition) of the computational basis states:
where each coefficient (also called a probability amplitude) is a complex number and the sum of the probabilities corresponding to these coefficients is 1, that is:
Sometimes it is useful, albeit often less compact, to write the state as a vector of probability amplitudes:
Note: The ordering listed in the sum of the computational basis states differs from the ordering in the vertical vector. We introduce both of these conventions because the ordering is used when we execute the
get_statefunction, while the ordering in the vertical vector is used in matrix and tensor algebra computations.
In the literature, the term statevector refers to a quantum state , but can also refer to the list of probability amplitudes of a quantum state .
2.1.2 Write and interpret a 2-qubit quantum program
In this section, we'll write a program that builds upon the Bit-Flip and Hello World (minus state) programs from Lab 1. In particular, our goal is to create one program with 2 qubits. The first qubit will be initialized in the zero state, and we'll flip it to the one state. The second qubit will be initialized in the minus state, and just for fun we'll apply the bit flip gate x to it too. The diagram represents the circuit for this program.

In our example, qubit , which starts in the state undergoes a bit flip. So we would expect, as we saw in Lab 1, that when the kernel is sampled in a noiseless setting, will be measured of the time as a . On the other hand, because qubit will be in a state of equal superposition, it will collapse to the state approximately half of the time and will collapse to the other times. Putting the information about the state of and together, we expect a probability of sampling a and probability of sampling a when running the program depicted above.
The statevector of the 2-qubit system after the gates are applied, but prior to measurement should be
To understand the appearance of the minus sign in the equation above, refer to the optional section on phase later in this notebook. For now, just notice that by squaring the absolute values of the amplitudes, we see a probability of sampling a and probability of sampling a .
We have several options for writing the CUDA-Q code to carry out the instructions above. In the next subsections, we'll take a look at two options so that you can see some of the functionality of cudaq.kernel. In particular, we'll demonstrate
how to pass individual qubits or a register of qubits to a kernel and
how to use kernels as subkernels (subroutines).
2.1.2.1 All-in-one kernel
We'll first code the quantum circuit like we did in Lab 1, by writing all the instructions sequentially in one kernel.
Let's check that this kernel generates our target state by using the get_state command. We'll also use the get_state.amplitudes option to extract out the coefficients of the computational basis states and that are of interest to us.
Quick Tip:
To remember the ordering of the coefficients in the statevector generated by the get_state command, it helps to recall the convention for associating numbers with their binary representations that places the least significant bit on the left. For example, in a 2-bit system, we have the following translation between integers and bits:
The
get_stateoutput generates a list of the coefficients in increasing order: , corresponding to To conveniently identify the coefficient for a given basis state, use theget_state().amplitude()command. For example, to extract the coefficient for the basis state , useget_state(my_kernel).amplitude([1,0]). To retrieve the amplitudes of more than one state at a time, use theamplitudesoption applied to a list of basis states, as demonstrated in the code block above.
2.1.2.2 Kernels as subroutines
In this section, we will create the same quantum circuit as above, but this time we'll use kernels as subroutines. We'll create three separate kernels:
minus, a kernel for the subroutine that initializes a given qubit as the minus statexgate, a kernel that applies thexgate to a list of qubitsnested_quantum_program, a kernel to allocate the qubits and call theminusandxgatesubroutines
Let's see how this is done. Take note of how we can pass qubits and lists of qubits to the kernel functions.
Exercise 1:
Edit the ##FIX_ME## portion of the code block below that uses the `get_state.amplitudes` to verify that the nested_quantum_program generates the desired state when num_qubits is set to .
Now let's use the sample command to compare the two quantum programs. Notice the difference between the arguments in the sample command applied to the kernel all_in_one versus the sample command applied to the kernel nested_quantum_program which takes as an argument num_qubits.
Quick Tip: The
samplecommand runs the kernel and takes a measurementshots_count-many times. The result of each kernel execution and measurement is a classical bitstring with the bit representing the measurement of the qubit. The result of thesamplecommand is aSampleResultdictionary of the distribution of the bitstrings. To create a more familiardictPython type from your variablemyresultof typeSampleResult, useasDict = {k:v for k,v in myresult.items()}.
Before going on, let's interpret the output of the sample command. Based on our prediction the output { 11:481 10:519 } indicates that the first digit in the bitstrings 11 and 10 corresponds to qubit , as it is 1 of the time, and the second digit corresponds to qubit , which gets measured about half the time as a 0 and the other half of the time as a 1.
CUDA-Q Quick Tip: The code blocks in this section demonstrate the ability for kernels to accept variables and to call one another. It'll be useful to note:
CUDA-Q kernels can only accept variables of certain types. In particular, you can pass an individual qubit (
cudaq:qubit), a list of qubits (cudaq:qview), integers (int), floats (float), complex values (complex), and lists of integers (list[int]),floats (list[float]), and complex values (list[complex]). The full list of allowable types can be found here.When sampling a kernel that accepts variables, the values for these variables, should appear in the
samplefunction as a list following the kernel name, as seen in the example above where our kernelnested_quantum_kernelaccepted an integer valuenum_qubits:cudaq.sample(nested_quantum_program, num_qubits, shots_count=shots)
Exercise 2:
Write a quantum program, using subkernels, to initialize three qubits in the zero state and place each qubit in the state if it has even index and qubits with odd index should be placed in the state (i.e., place and in the plus state and in the minus state.) Use the `get_state` command to verify that your quantum program generates the desired quantum state. Print out the statevector to confirm. As a bonus, try making your program generic enough that it would work for any number of qubits.
For example, the state of a 3 qubit system prior to measurement should be:
The notation above represents a tensor product. For now, you can treat this as a multiplication that satisfies standard multiplication properties such as transitivity and associativity. We will not be using this notation beyond this example. If you'd like a formal introduction to tensor products, check out section 4.2.1 of this resource.
CUDA-Q Quick Tip: Inside a
cudaq.kernelyou can use python commands likeforloops andifstatements.
2.1.1.3 Phase (optional)
Phase is a characteristic of quantum states that we have not yet discussed. Estimating the phase of a given quantum state is an important step in many quantum algorithms including Shor's algorithm. Additionally, phase helps explain the phenomena of interference which is key in algorithms such as Grover's.
Every state has a phase factor. The phase factor of a state can be deduced from the term in the standard representation of a quantum state:
We are often interested when two states have different phases. This can happen in one of two ways: differing by a global phase or by a relative phase. We've seen examples of both of these already in Lab 1 and in the previous examples in this lab.
Let's take a deeper look at qubit in the nested_quantum_program example from the previous section. We can follow the state of as it changes throughout the kernel execution. We begin in the state which is flipped to . Then, an application of the gate takes us to . This is followed by an application of the bit flip operator . Carrying out this matrix multiplication we get that the state of prior to the measurement is:
When we measure we get a similar quasi-probability distribution that we would get if we measured . We saw this behavior in Lab 1, where and had indistinguishable quasi-probability distributions upon measurement with mz. However, if we measured the states and with mx we could distinguish the two states through their quasi-probability distributions. Unlike in Lab 1, there is no measurement that we can apply to and to distinguish them.
The state and only differ by a scalar multiple of . When states only differ by a factor of , they are said to differ by a global phase. And these states cannot be distinguished from one another through any measurements, so they are often treated as being equivalent, or interchangeable.
This is in contrast to the states and which have equal amplitudes but differ by a relative phase, in this case .
In general the relative phase difference between two states and is defined to as
Question: What is the relative phase difference between and ?
2.1.2 Write a "Hello, Entangled World!" program
So far, all of the quantum operations (e.g. x, h) that we programmed acted only on an individual qubit. In this section, we'll write a program using multi-qubit gates. Let's start with the example of the controlled-not gate, often abbreviated the CNOT gate. This is a gate that operates on 2 or more qubits. One of the qubits is considered the "target", and the remaining qubits are the "control". For instance CNOT([q0,q1],q2) would represent a gate with control qubits in the list [q0,q1] and target qubit q2.
In a circuit diagram we depict the CNOT gate as a line connecting the control and target qubits with a located at the target qubit and a solid circle on control qubits. For instance, in the circuit diagram below, we see a CNOT(q0, q1) and a CNOT([q1,q2],q0) gate.

The rough idea is that if the "control" qubits are all in the state then an application of the CNOT gate will apply a bitflip (i.e. an x gate) to the target qubit. If on the other hand, at least one of the "control" qubits is in the state the CNOT gate has no effect.
To illustrate this, let's code up a few examples.
Quick Tip: The syntax for the CNOT gate is
x.ctrl(control_qubit, target_qubit)if we have only one control qubit. When we have multiple control qubits, we send them as a list to thex.ctrlfunction:x.ctrl(control_qubits_list, target_qubit).
While the previous examples showed CNOT operations on basis states (|0⟩ and |1⟩), the interactive tool below lets you explore more interesting quantum behaviors. Try selecting superposition states (|+⟩ or |-⟩) for either or both qubits to see how the CNOT gate functions. You'll discover some surprising quantum behaviors that have no classical counterpart!
Note: If the widget does not appear above, you can access it directly using this link.
Exercise 3a:
Edit the code block below to see the effect of the gate sequence in the diagram below on the qubits if was initialized as and was initialized as
Did you notice that the alternating_cnots kernel had the effect of swapping the states of and ? This is a useful operation, so there is a dedicated notation for it. We refer to this gate sequence as the SWAP gate, and abbreviate the gate sequence as a line with 2 Xs on the endpoints connecting the qubits:

The CUDA-Q command for a SWAP gate applied to qubits and is swap(q_0, q_1). Let's use CUDA-Q to check that the swap gate has the same effect on two qubits as the alternating_cnots kernel.
Now, let's explore another example that will showcase one of the powerful computations achievable with quantum gates.
Exercise 3b:
Edit the ##FIX_ME## phrase in the code block below to apply the `x.ctrl` gate to the state .
To understand the output of the exercise above, let's use the ket notation. First we should point out that one characteristic of quantum gates that we've not mentioned explicitly is linearity. For example, if we have a gate applied to the state , we can understand the action of on by knowing how affects and . In particular, we can distribute across the terms of : This generalizes beyond one-qubit gates to multi-qubit gates.
Therefore, when we apply the CNOT to we get
2.1.3 Multi-qubit gates as matrices
To understand why gate operations are linear, it helps to represent a multi-qubit gate as a matrix, just as we have done in Lab 1 for single qubit gates. For example the matrix for the CNOT gate using the convention of CUDA-Q ordering is:
Using the fact that , we can verify our work from the previous section:
The state is an example of entanglement. Along with superposition, this property helps to distinguish quantum algorithms from classical. Let's take a look at this property next.
FAQ: If quantum states can be represented as vectors, and quantum gates can be realized through matrix operations, then why do we need quantum computers if we can theoretically simulate any quantum circuit through matrix multiplication classically?
Answer: One thing to note is that as the number of qubits increases, the number of possible states increases exponentially and the size of the matrix representing the multi-qubit gate also increases exponentially. For instance, a 3-qubit system (like the Exercise 4 in the next section) can be in one of states, and the size of the matrix representing a 3-qubit gate is . This suggests that straightforward attempts at using matrix multiplication to simulate quantum computation on CPUs fail for circuits with only 30 or so qubits. With CUDA-Q's platform and GPU-accelerated built-in simulators, it is possible to simulate larger systems. That said, there may one day be a fault tolerant quantum computer that can carry out computations too big for even the largest classical supercomputers.
2.1.4 Entanglement
Entanglement of two (or more) qubits is akin to the concept of dependence in linear algebra. That is to say that two qubits are entangled if the state of one of the qubits depends on the other. Formally the definition of entanglement relies on tensor products, which are beyond the scope of this Quick Start series of notebooks. You can read more about tensor products and the precise definition of entanglement here or here. For now, we'll illustrate entanglement through the example of the 2-qubit state
The state has equal probability of being measured in the state or . Let's look at what happens if we measure the first qubit.
If our system of two qubits is in the state and we measure qubit 0 to be , then the state of the system collapses to . In this case, without taking a measurement on qubit 1, we already know its state (it's ).

Similarly if the state of our system is and qubit 0 is measured to be 1, then the state of the system collapses to . Although prior to any measurement on qubit 0, qubit 1 had a probability of being in state or , after measuring qubit 0 to be 1, we know that qubit 1 must be in the state .

So we see that even though prior to measurement, both qubits 0 and 1 have equal probabilities of being measured a 0 or 1, once we measure one of these qubits, the outcome of the other qubit has been determined and depends on the state of the qubit that was measured. In other words, qubit 0 and qubit 1 are entangled.
Exercise 4:
Edit the code block below to create a quantum program with 3 qubits to:
- Initialize the state by editing the
##FIX_ME## - Apply a
x.ctrlgate with control qubit and target . Then apply a Hadamard gate to . - Sample the result
Question: What do you notice about the probability amplitudes in Exercise 4? Pay specific attention to the probability amplitude for , , , and compared to those of the other basis states.
The changing of the probability amplitudes in this exercise is an example of interference, which we'll discuss next.
2.1.5 Interference
In a quantum program, we apply gates to change quantum state from one state to another. In effect, what we're doing is changing the probability amplitudes associated with each of the basis states. This process is called interference. Interference is central in almost every quantum algorithm.
We can have both constructive and destructive interference. Like the classical interference of waves from raindrops on the surface of a puddle, constructive interference occurs when the probability amplitude of basis state is increased, and destructive interference occurs when the probability of a basis state decreases due to the action of quantum gates. The previous example demonstrates both constructive and deconstructive interference. The circuit is initialized in a state having equal magnitude probability amplitudes, but after applying the CNOT gate and Hadamard gate, the probabilities of measuring , , , and have each been amplified, while the probability amplitudes associated with the other basis states have been reduced to .
We will see another example of interference in Lab 3 when explore quantum walks.
2.3 Other multi-qubit gates
There are many more quantum gates than the handful that we've explicitly used so far. In the previous lab, we mentioned that any rotation of the Bloch sphere corresponds to a quantum gate on one qubit. So in theory, there are an infinite number of possible quantum gates: one for each of the possible rotations of the sphere. In this section, we'll first detail what these rotations gates look like both as matrices and as CUDA-Q operations. Then we'll use these rotations gates to define a larger class of multi-qubit controlled gates.
2.3.2 Revisiting single-qubit rotation gates
We've already seen the x-gate that rotates a state around the x-axis of the Bloch sphere by 180 degrees and the Hadamard gate which rotates the Bloch sphere 180 degrees about a different axis causing to move to and to rotate to , and vice versa. But how do we implement gates for other angles of rotation, or different axis of rotation? One easy answer is to use some of the gate operations built into CUDA-Q. These include the y-gate for 180 degree rotation about the -axis and the z-gate for 180 degree rotation about the axis. The t and s gates correspond to other fixed angle rotations about the axis.
Exercise 5:
Using the visualization tool below, experiment with applying the t and s gates to various states in the code block below to deduce the rotation angle for the t and s gates and to predict the relationship between the t and s gates.
Note: If the widget does not appear above, you can access it directly using this link.
To access arbitrary angle rotations about the , , and axis of the Bloch sphere, we can use the rx, ry, and rz commands.
In the code block, we plot the effect of rx on the state for a variety of angles. You can see that when the angle is , the rotation gate rx is equivalent to the x gate. Feel free to edit the code bloch to apply the ry or rz gates to various angles on different initial states.
2.3.3 Controlled gates
Earlier we examined the CNOT gate (x.ctrl) which is a controlled-x gate. That is, the x gate is applied conditionally on the state of one or more of the control qubits. Similarly, we can create controlled gates for any of our single qubit gates using the .ctrl suffix. For instance t.ctrl will apply a t gate conditionally on the state of the control qubit.
With just a few of these gates, we can carry out some interesting computations. Let's check out one step of Shor's algorithm in the next section.
2.4 Application: modular arithmetic
Shor's algorithm is a well-known example that has the potential to break RSA encryption, assuming it could be executed on a fault-tolerant quantum computer at a sufficiently large scale. While we won’t be implementing the full algorithm here (you can explore more details here starting on page 341, Chapter 4 of these lecture notes or chapter 3 of Mermin's book here and you can view the code for Shor's algorithm here), we will focus on one step of the algorithm: performing modular arithmetic. This will serve as a demonstration of the concepts we've covered so far and offer an opportunity to encode classical information (in this case, an integer) onto qubits. The circuit shown below, inspired by this article, performs the operation of multiplying modulo , with the initial state representing various values of .

For instance, if we want to calculate , we would encode by preparing the quantum state through the application of an gate to qubit :

Exercise 6:
Edit the code below to carry out the instructions of the circuit diagrams above to compute and verify that the outcome of the circuit is as expected. For example, when the circuit is initialized as , we should expect to sample the binary representation of 16 (which is `00001`) since .
Exercise 7:
Create a kernel that carries out modular exponentiation 5^x mod 21 using the mult_y_by_5_mod21 of the previous exercise. Hint: .
2.5 Next
While Shor's algorithm for factoring numbers is one of the most famous quantum computing algorithms, it is not considered a (Near-term Intermediate Scale Quantum) NISQ-era program since it requires more qubits, with less noise, than currently available. There are, however, several NISQ-era programs that can be run on current quantum hardware and simulated with GPUs. Many of these programs are based on variational algorithms, which we'll explore in the next lab.