ARTICLE

Changing The Odds: an introduction to Q#

From Learn Quantum Computing with Python and Q# by Sarah Kaiser and Chris Granade

This article covers using the Quantum Development Kit to write quantum programs in Q#.

_______________________________________________________________

Take 37% off Learn Quantum Computing with Python and Q# by entering fcckaiser into the discount code box at checkout at manning.com.
_______________________________________________________________

You can use Python to implement your own software stack to simulate quantum programs. In this article, we’ll be writing more intricate quantum programs that benefit from specialized language features which are hard to implement by embedding our software stack inside Python. As we explore quantum algorithms, it’s helpful to have a language tailor-made for quantum programming at our disposal. We’ll also get started with Q#, Microsoft’s domain-specific language for quantum programming, included with the Quantum Development Kit.

Introducing the Quantum Development Kit

The Quantum Development Kit provides a new language, Q#, for writing quantum programs and simulating them using classical resources. Quantum programs written in Q# are run in a way that the quantum devices act as a kind of accelerator, similar to how you might run code on a graphics card OpenCL, this is a similar model.

Image for post
Image for post
Figure 1. Q# software stack on a classical computer.

Let’s take a look at this software stack for Q#. Our Q# program itself consists of operations and functions that instruct quantum and classical hardware to do certain things. A number of libraries which are provided with Q# are helpful, pre-made operations and functions to use in our programs.

Once the Q# program is written, we need a way for it to pass instructions to the hardware. A classical program, sometimes called a “driver” or a “host program,” is responsible for allocating a target machine and running a Q# operation on that machine.

The Quantum Development Kit provides a plugin for Jupyter Notebook called IQ# which makes it easy to get started with Q# by providing host programs automatically. Using the IQ# plugin for Jupyter Notebook, we can use one of two different target machines to run Q# code. The first is the target machine, which is similar to a Python simulator. It’s a lot faster than Python code at simulating qubits.

The second is the target machine which allows us to estimate how many qubits and quantum instructions we need to run it, without having to fully simulate it. This is useful for getting an idea of the resources you need to run a Q# program for your application

Image for post
Image for post
Figure 2. Getting started with IQ# and Jupyter Notebook

To get a sense for how everything works, let’s start by writing out a purely classical Q# “hello, world” application. First, start Jupyter Notebook by running the following in a terminal:

jupyter notebook

This automatically opens a new tab in your browser with the home page for your Jupyter Notebook session. From the New ↓ menu, select “Q#” to make a new Q# notebook. Type the following into the first empty cell in the notebook and press Control + Enter or + Enter to run it.

function HelloWorld() : Unit {           ❶    
Message("Hello, classical world!"); ❷
}

❶ This line defines a new function which takes no arguments, and returns the empty tuple, whose type is written as .

❷ The function tells the target machine to collect a diagnostic message. The target machine prints all diagnostics to the screen, and we can use in the same way as in Python.

TIP Watch out for semicolons! Unlike Python, Q# uses semicolons rather than newlines to end statements. If you get a lot of compiler errors, make sure you remembered your semicolons.

You should get a response back listing that the function was successfully compiled. To run our new function, we can use the command in a new cell.

%simulate HelloWorld

TIP A bit of classical magic: The command we used above as an example of a magic command, in that it’s not a part of Q# itself, but it’s an instruction to the Jupyter Notebook environment. If you’re familiar with the IPython plugin for Jupyter, you may have used similar magic commands to tell Jupyter how to handle Python plotting functionality. The magic commands we use in this book all start with to make them easy to tell apart from Q# code.

In this example, allocates a target machine for us and sends a Q# function or operation to that new target machine.

The Q# program is sent to the simulator, but in this case, the simulator runs the classical logic, because there’s no quantum instructions to worry about yet.

Functions and Operations in Q#

Now that the Quantum Development Kit is up and running with Jupyter Notebook, let’s use Q# to write some quantum programs. One useful thing to do with a qubit is to generate random numbers one classical bit at a time. Revisiting that application makes a great place to start with Q#, because random numbers are useful if you want to play games.

Long ago in Camelot, Morgana le Fay shared our love for playing games. Being a clever mathematician with skills well beyond her own day, Morgana was even known to use qubits from time to time as a part of her games. One day, as Sir Lancelot lay sleeping under a tree, Morgana trapped him and challenged him to a little game: each of them must try to guess the outcome of measuring one of Morgana’s qubits.

Two sides of the same… qubit?

You can generate random numbers one bit at a time by preparing and measuring qubits. Qubits can be used to implement coins. We’ll use the same kind of idea in this article, thinking of a coin as a kind of interface that allows its user to “flip” it and get out a random bit. We can implement the coin interface by preparing and measuring qubits.

If the result of measuring along the axis is a 0, then Lancelot wins their game and gets to return to Genevieve. If the result is a 1, though, Morgana wins and Lancelot has to stay and play again. We’ll measure a qubit to generate random numbers for the purpose of playing this game. Morgana and Lancelot could have also flipped a more traditional coin, but where’s the fun in that?

Morgana’s side game

  1. Prepare a qubit in the |0# state
  2. Apply the Hadamard operation (recall that the unitary operator
  3. takes |0# to |+# and vice versa)
  4. Measure the qubit in the ” axis. If the measurement result is a 0, then Lancelot can go home. Otherwise, he must stay and play again!

Sitting at a coffee shop watching the world go by, we can use our laptops to predict what happens in Morgana’s game with Lancelot by writing a quantum program in Q#. Unlike the function that we wrote above, our new program needs to work with qubits; let’s take a moment to see how to do this with the Quantum Development Kit.

The primary way to interact with qubits in Q# is by calling operations that represent quantum instructions. To understand how these operations work, it’s helpful to understand the difference between Q# operations and the functions that we saw in the example above.

  • Functions in Q# represent predictable classical logic, things like mathematical functions (, ). Functions always return the same output when given the same input.
  • Operations in Q# represent code which can have side effects, such as sampling random numbers, or issuing quantum instructions which modify the state of one or more qubits.

This separation helps the compiler figure out how to automatically transform your code as a part of larger quantum programs; we’ll see more about this later.

Another perspective on functions versus operations

Another way of thinking of the difference between functions and operations is that functions compute things, but can’t cause anything to happen. No matter how many times we call the square root function , nothing about our Q# program has changed. By contrast, if we run the operation, then an instruction is sent to our quantum device, which causes a change in the state of the device. Depending on the initial state of the qubit that the instruction was applied to, we can then tell that the instruction has been applied by measuring the qubit. Because functions don’t do anything in this sense, we can always predict their output exactly given the same input.

One important consequence is that functions can’t call operations, but operations can call functions. This is because you can have an operation which isn’t necessarily predictable call a predictable function and you still have something which may or may not be predictable. A predictable function can’t call a potentially unpredictable operation and still be predictable.

We’ll see more about the difference between Q# functions and operations as we use them throughout the rest of the book. Because we want quantum instructions to have an effect on our quantum devices (and on Lancelot’s fate), all quantum operations in Q# are defined as operations (hence the name).

For instance, suppose that Morgana and Lancelot prepare their qubit in the |+ state using the Hadamard instruction. We can predict the outcome of their game by writing out the quantum random number generator (QRNG) example listed below.

def qrng(device : QuantumDevice) -> bool:
with device.using_qubit() as q:
q.h()
return q.measure()

NOTE: There may be side effects to this operation… when we want to send instructions to our target machine to do something with our qubits, we need to do this from an operation, because sending an instruction is a kind of side effect. When we run an operation, we aren’t only computing something, we’re doing something. Running an operation twice isn’t the same as running it once, even if we get the same output both times. Side effects aren’t deterministic or predictable, and we can’t use functions to send instructions on how to manipulate our qubits.

In Listing 1, we’ll do exactly that, starting by writing an operation called to simulate each round of Morgana’s game. Note that because needs to work with qubits, it has to be an operation and not a function. We can ask the target machine for one or more fresh qubits with the block.

NOTE: Allocating qubits in Q#: The using statement is one of the only two ways we can ask the target machine for qubits. The number of using statements that we can have in our Q# programs is unlimited, other than the number of qubits that each target machine can allocate. At the end of each using block, the qubits then go back to the target machine; using blocks ensure that each qubit which is allocated is “owned” by a particular operation. This makes it impossible to “leak” qubits within a Q# program, which is helpful given that qubits are likely to be expensive resources on quantum hardware.

Q# offers one other way to allocate qubits, known as borrowing. Unlike when we allocate qubits with statements, the statement lets us borrow qubits which are owned by different operations without knowing what state they start in. The statement works similarly to the statement in that it makes it impossible for us to forget that we’ve borrowed a qubit.

By convention, all qubits start off in the |0 state right after we get them, and we promise the target machine that we’ll put them back into the |0 state at the end of the block to be ready for the target machine to give to the next operation that needs them.

Listing 1. Simulating one round of Morgana’s game using Q#

operation NextRandomBit() : Result { ❶
mutable result = Zero; ❷
using (qubit = Qubit()) { ❸
H(qubit); ❹
set result = M(qubit); ❺
Reset(qubit); ❻
}
return result; ❼
}

❶ This time, because we want to use a qubit, we declare an operation instead of a function. Because our operation needs to return a result to its caller, we denote by changing the return type to the Q# type .

❷ The keyword in Q# asks the target machine for one or more qubits. Here, we ask for a single value of type , which we store in the new variable .

❸ Quantum operations such as the Hadamard operation can be found in the namespace. For instance, we can call Hadamard using the operation. After calling , is in the

Image for post
Image for post

state.

❹ Next, we use the operation to measure our qubit in the

Image for post
Image for post

basis, saving the result to the variable we declared earlier. Because we’re in an equal superposition of

Image for post
Image for post

and

Image for post
Image for post

, are either or with equal probability.

❺ Before returning our qubit to the target machine, we use the operation to return it to the

Image for post
Image for post

state. Because we’ve already stored the classical data we got from our measurement into the variable, we can safely reset the qubit without losing any information that we care about.

❻ We finish our operation by returning the measurement result back to the caller.

We finish our operation by returning the measurement result back to to the caller.

Next, we need to see how many rounds it takes for Lancelot to get the he needs to go home. Let’s write an operation to play rounds until we get a . Because this operation simulates playing Morgana’s game, we’ll call it .

Listing 2. Simulating many rounds of Morgana’s game using Q#

operation PlayMorganasGame() : Unit {     mutable nRounds = 0; ❶
mutable done = false;
repeat { ❷
set nRounds = nRounds + 1;
set done = (NextRandomBit() == Zero); ❸
}
until (done) ❹
fixup {}

Message($"It took Lancelot {nRounds} turns to get home."); ❺
}

All Q# variables are immutable by default — we can use the keyword to declare a variable which we can change later with the keyword. Here, we start by initializing a mutable variable indicating how many rounds have already passed, and a mutable variable we’ll use to exit the loop.

❷ Q# allows operations to use a kind of loop called a “repeat-until-success” (RUS) loop. Unlike a -loop, RUS loops allow us to specify a “fixup” that runs if the condition to exit the loop isn’t met. Note that the block is required, even if it’s empty.

❸ Inside our loop, we call the QRNG that we wrote above as the operation. We check to see if the result is a (if Lancelot wins and can leave), and if true, set to be .

❹ If we got a , then we can stop the loop.

❺ Finally, we use again to print the number of rounds to the screen. To do this, we use strings which, similar to strings in C# and strings in Python, and include variables in the diagnostic message by using placeholders inside the string.

Why Do We Need to Reset Qubits?

In Q#, when we allocate a new qubit with using, we promise the target machine which we’ll put it back in the |0# state before we deallocate it. At first glance, this seems rather unnecessary, as the target machine could reset the state of qubits when they are deallocated — after all, we’ll often call the Reset operation at the end of a using block.

It’s important to note, though, that the Reset operation works by making a measurement in the basis and flipping the qubit with an X operation if the measurement returns One. In many quantum devices, measurement is much more expensive than other operations, such that if we can avoid calling Reset we can reduce the cost of our quantum programs. Given the limitations of medium-term devices, this kind of optimization can be critical in making a quantum program practically useful.

Later in the article, we’ll see examples of where we know the state of a qubit when it needs to be deallocated, such that we can “unprepare” the qubit instead of measuring it.

We can run this new operation with the command in a similar fashion as the example. When we do this, we can see how long Lancelot has to stay:

Listing 3. Output from running the application

In []: %simulate PlayMorganasGame
It took Lancelot 1 turns to get home.
Out[]: ()

Looks like Lancelot got lucky that time! Or perhaps unlucky, if he was bored of hanging ’round the table in Camelot.

Passing Operations as Arguments

Let’s suppose that in Morgana’s game, we’re interested in sampling random bits with nonuniform probability. After all, Morgana didn’t tell Lancelot how she prepared the qubit that they are measuring; she can keep him playing longer if she makes a biased coin with their qubit instead of a fair coin.

The easiest way to modify Morgana’s game is to, instead of calling directly, take as an input an operation representing what Morgana does to prepare for their game. To take an operation as input, we need to write down the type of the input, as can write down to declare an input of type . Operation types are indicated by thick arrows () from their input type to their output type. For instance, has type because takes a single qubit as input and returns an empty tuple as its output.

Listing 4. Using operations as inputs in order to predict Morgana’s game.

operation PrepareFairCoin(qubit : Qubit) : Unit {   
H(qubit);
}

operation NextRandomBit(
statePreparation : (Qubit => Unit) ❶
) : Result {
using (qubit = Qubit()) {
statePreparation(qubit); ❷
return result = MResetZ(qubit); ❸
}
}

❶ This time, we’ve added a new input called statePreparation to NextRandomBit that represents the operation we want to use to prepare the state we use as a coin. In this case, Qubit => Unit is the type of any operation which takes a single qubit and returns the empty tuple type Unit.

❷ Within NextRandomBit, the operation passed as statePreparation can be called in the same way as any other operation.

❸ The Q# standard libraries provide MResetZ as a convenience for measuring and resetting a qubit in one step. This is equivalent to the set result = M(qubit); Reset(qubit); statements we saw in the previous example, but requires one less measurement to perform.

Tuple-In Tuple-Out

All functions and operations in Q# take a single tuple as an input and return a single tuple as an output. For instance, a function declared as takes as input a tuple and returns a tuple as its output. This works because of a property known as singleton–tuple equivalence. For any type , the tuple containing a single is equivalent to itself. In the example of , this means that we can think of the output as a tuple that is equivalent to .

Image for post
Image for post
Figure 3. Representing operations with a single input and a single output

With this in mind, a function or operation that returns no outputs can be thought of as returning a tuple with no elements, . The type of such tuples is called , similar to other tuple-based languages such as F#. If we think of a tuple as a kind of box, then this is distinct from as used in C, C++, or C# because there still is something there, namely a box with nothing in it.

In Q#, we always return a box, even if that box is empty.

There’s no meaning in Q# to a function or operation that returns “nothing.” For more details, see Section 7.2 of Get Programming with F#.

Image for post
Image for post
Figure 4. versus

In this example, we see that treats its input as a “black box.” The only way to learn anything about Morgana’s preparation strategy is to run it.

Put differently, we don’t want to do anything with that implies we know what it does or what it is. The only way that can interact with is by calling it, passing it a to act on.

This allows us to reuse the logic in for many different kinds of state preparation procedures which Morgana might use to cause Lancelot a bit of trouble. For example, suppose she wants a biased coin that returns a ¾ of the time and a ¼ of the time. Then, we might run something like the following to predict this new strategy:

Listing 5. Passing different state preparation strategies to the example.

open Microsoft.Quantum.Math;          ❶

operation PrepareQuarterCoin(qubit : Qubit) : Unit {
Ry(2.0 * PI() / 3.0, qubit); ❷
}

❶ Classical math functions such as Sin, Cos, Sqrt, and ArcCos, as well as constants like PI() are provided by the Microsoft.Quantum.Math namespace, and we open it as well as the intrinsics.

❷ The Ry operation implements the Y -axis rotation that we saw in Chapter 2. Q# uses radians rather than degrees to express rotations, so this is a rotation of 120° about the Y -axis. Thus, if qubit starts in |0〉, this prepares qubit in the state Ry(-120◦)|0〉 = √3/4 |0〉 + √1/4 |1〉, such that the probability of observing 1 when we measure is √3/42 = 3/4.

We can make this example even more general, allowing Morgana to specify an arbitrary bias for her coin (which is implemented by their shared qubit):

Listing 6. Passing operations to implement PlayMorganasGame with arbitrary coin biases.

operation PrepareBiasedCoin(morganaWinProbability : Double, qubit : Qubit) : Unit {
let rotationAngle = -2.0 * ArcCos(Sqrt(morganaWinProbability)); ❶
Ry(rotationAngle, qubit);
}

operation PrepareMorganasCoin(qubit : Qubit) : Unit { ❷
PrepareBiasedCoin(0.62, qubit);
}

❶ We need to find out what angle we rotate the input qubit by in order to get the right probability of seeing a Zero as our result. This takes a little bit of trigonometry, see the sidebar below for the details.

❷ This operation has the right type signature (Qubit => Unit) and we can see that the probability Morgana wins each round is 62%.

Working out the trigonometry

As we’ve seen a number of times, quantum computing deals extensively with rotations. To figure out what angles we need for our rotations, we need to rely on a little bit on a branch of mathematics for describing rotation angles, known as trigonometry (literally, the study of triangles). For instance, as we saw in Chapter 2, rotating |0〉 by an angle θ about the Y axis results in a state cos(-θ / 2) |0〉 + sin(-θ / 2) |1〉. We know we want to choose θ such that cos(-θ / 2) = √62%, so that we get a 62% probability of getting a result. That means we need to “undo” the cosine function to figure out what θ needs to be. In trigonometry, the inverse of the cosine function is called the arccosine function, and is written arccos. Taking the arccosine of both sides of cos(-θ / 2) = √62% gives us arccos(\cos(-θ / 2)) = arccos(√62%). We can cancel out the arccos and cos to find a rotation angle that gives us what we need, -θ / 2 = arccos(√62%). Finally, we multiply both sides by -2 to get the equation we used in line ❶ of Listing 6.6.

Image for post
Image for post
Figure 5. How Morgana can choose θ to control how her game plays out

This is somewhat unsatisfying, though, in that the operation introduces a lot of boilerplate to lock down the value of for the input argument to . If Morgana changes her strategy to have a different bias, then using this approach means we’ll need another new boilerplate operation to represent it. Taking a step back, let’s look at what does. It starts with an operation , and wraps it into an operation of type by locking down the argument to 0.62. It removes one of the arguments to by fixing the value of that input to 0.62.

Thankfully, Q# provides a convenient shorthand for making new functions and operations by locking down some (but not all!) of the inputs. Using this shorthand, known as partial application, we can rewrite the above in a more readable form:

Listing 7. Using partial application to make it easier to vary Morgana’s strategy.

let flip = NextRandomBit(PrepareBiasedCoin(0.62, _));

The here indicates that a part of the input to is missing. We say that has been partially applied. Whereas had type , because we filled in the part of the input, has type , making it compatible with our modifications to . Partial application in Q# is similar to in Python and the keyword in Scala.

Another way to think of partial application is as a way to make new functions and operations by specializing existing functions and operations:

function BiasedPreparation(headsProbability : Double) : (Qubit => Unit) { ❶    
return PrepareBiasedCoin(headsProbability, _); ❷
}

❶ Here, the output type of BiasedPreparation is an operation that takes a Qubit and returns the empty tuple. BiasedPreparation is a function that makes new operations!

❷ We make the new operation by passing along headsProbability, but leaving a blank

(_) for the target qubit. This gives us an operation that takes a single Qubit and substitutes in the blank.

It may seem a bit confusing that returns an operation from a function, but this is completely consistent with the split between functions and operations described above, because is still predictable. In particular, always returns the same operation for a given , no matter how many times you call the function. We can assure ourselves that this is the case by noticing that only partially applies operations, but never calls them.

That’s all for this article.

If you want to learn more about the book, check it out on our browser-based liveBook reader here.

Written by

Follow Manning Publications on Medium for free content and exclusive discounts.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store