Introduction to quantum computing with Q# – Part 1, The background and the qubit

· 3008 words · 15 minutes to read

Quantum mechanics is one of the fundamental theories of physics, and has been tremendously successful at describing the behavior of subatomic particles. However, its counter-intuitive probabilistic nature, bizarre rules and confusing epistemology have troubled some of the greatest physicists of the 20th century, even prompting Albert Einstein to remark “Old Man (often translated as ‘God’) doesn’t play dice”.

In this post I am starting a new series that will introduce the basics of quantum computing - using examples in Q#.

Quantum computing - past and present 🔗

Historically, quantum computing dates back to early 1980s when Paul Benioff and Richard Feynman proposed building a Turing machine based on quantum mechanical phenomana.

What followed was plenty of progress in terms of theory and algorithms, but the field always lacked the hardware to make it all reality. Only in recent years we have finally reached the point at which quantum computers are no longer theoretical devices. Quantum hardware is getting more and more powerful, stable and accessible to the masses. Since 2016, IBM Q Experience provides access to quantum computer via the cloud (the smallest one for free!), while AWS Braket and Azure Quantum are both offering public preview programs with the goal to launch soon. Smaller companies and startups are also disrupting the field, for example AQT, an Innsbruck based company, offers access to ion-trap quantum computers on the cloud too.

Additionally, a lot is happening around bringing the quantum experience closer to regular software developers. For example, Microsoft is currently building Q#, a high level programming language specifically tailored for quantum programming, IBM founded Qiskit, a Python framework for quantum computing, while Google AI Quantum Team started Cirq, another Python framework for quantum development.

All these efforts lead to what I like to call, democratization of quantum computing. You no longer have to be a theoretical physicist, a PhD researcher or work for a large company with massive R&D resources to be able to access and program quantum computers - and that process will only continue to speed up.

Quantum mechanics - historical background 🔗

It is impossible to talk about quantum computing without a little background on quantum mechanics. Contrary to general relativity, which we owe entirely to the brilliance of Albert Einstein, it is commonly known that quantum mechanics has many pioneers that contributed to its formation.

However, if we had to single out one man that played the pivotal in the history of quantum mechanics, I’d say it was Werner Heisenberg, who published the foundations of what became modern quantum mechanics in his 1925 paper Über quantentheoretische Umdeutung kinematischer und mechanischer Beziehungen. Following the paper, the theory was further refined, contributed to and developed by Heisenberg himself and many other brilliant physicists - Paul Dirac, Max Born, Pascual Jordan, Wolfgang Pauli and others (including especially profound contributions, sort of against his will, from Erwin Schrödinger, but that’s a story for a separate day).

Heisenberg realized that it is impossible to use the paradigms macro-scale physics to correctly describe the behavior of subatomic particles. The genius of Heisenberg was that he abandoned the approach that was at the very core of classical physics - describing the nature of reality (in this case particles) using idealized mathematical models and its realism based on deterministic results. Instead, he introduced new matrix-based mathematical formalism which was based on algebraic probabilistic approach for predicting the outcome of experiments. In other words, he realized that nature is random (no pun intended) by nature, took the equations of classical physics and mathematically reinterpreted them. The departure from the idealized, deterministic approach of classical physics was a profound, brilliant step, especially considering that he introudced noncommutativity, which didn’t exist in theoretical physics at the time. Heisenberg realized that at quantum level, observable properties such as momentum and position should not commute.

Throughout these series we will continue to look back at some of the historical context of the development of quantum mechanics and the radical epistemological challenges it posted.

Quantum computing in the quantum mechanical context 🔗

While the field of quantum computing (and quantum information theory), is an offspring of quantum mechanics, to program a quantum computer and to use some of the high level languages and frameworks we mentioned earlier, it is not absolutely necessary to be fluent in quantum mechanics.

Of course familiarity with quantum mechanics is going to be advantageous at the theoretical level, to be able to grasp, for example, the spin concept. Knowledge of QM may also be necessary for you to formulate and solve real life problems with quantum computers. But strictly speaking, many of the cornerstones of quantum mechanics like solving time dependent Schrödinger equation do not really play much of the role in quantum computing. So if you are not feeling too comfortable with quantum mechanics, take careful steps forward and see how it feels; there may be more unfamiliarity and weirdness - but it shouldn’t discourage you from attempting to learn quantum computing.

What is needed to get started, though, is a decent understanding of linear algebra.

Q# 🔗

Before we jump into the mathematics, let’s discuss a little Q# and how it fits into the picture. As mentioned earlier, there are various ways of writing programs for quantum computers, many of which revolve around Python. That said, I am personally really excited about Q#, and for a number of reasons.

First of all, I have a .NET development background, and in that sense, Q# is a natural fit. The Quantum Development Kit for Q# is actually built on top of .NET Core SDK, which makes the whole experience very familiar and intuitive for developers that are used to that toolchain. Q# programs are compiled and executed from the dotnet CLI, the libraries are distributed using Nuget package manager and the project file is a standard csproj project file used for C# or F# development, with custom SDK defined in it. That integration level is very similar to IL projects, which we already discussed on this blog.

Secondly, it’s really appealing to have a language specifically designed for quantum computing experience. This allows a lot of quantum specific concepts - such as for example adjoints, to fit naturally into the language, instead of feeling like a bolt-on on a general purpose language. Syntactically, Q# looks like a mix of C# and F# and therefore familiarity with those languages will make entry into Q# easier.

Finally, QDK has extensions for the editors/IDEs known from the .NET world - my recommendation is to use VS Code as it is lightweight and cross platform, but if you prefer, there is an extension for Visual Studio too.

Generally speaking, Q# programming model allows you to write code for quantum hardware in a way where quantum hardware is treated as a coprocessor (much like GPU is). In that sense, your main program can be C# or even Python based, and for given operations, you’d call into your Q# code to execute a given set of instructions on a quantum device (or simulator, when running locally). QDK takes care of the interoperability between the host program (C#, Python) and Q# code itself.

To get started, we will need to install the Quantum Development Kit from Microsoft Research on our machines. The linked page contains instruction for installing the QDK as well as the necessary project templates and the editor extensions. I recommend that you pause for a moment here, and go ahead and set up the QDK now.

Qubits 🔗

When starting to learn quantum computing, a decent place to begin is to explain the notion of a qubit, since, just as in classical computers everything is based on bit, in quantum computing computations are carried out by qubit manipulation. The main difference between classical bits and qubits is that qubits, instead of taking one of the two discrete (binary) values only, can also be in the superposition state. We’ll explain the notion of superposition in a lot more details in the next post, so bear with me, but for now we can say that when in superposition, they are both 0 and 1 at the same time.

There is really no way to reason about qubits without discussing their mathematical representation and using some (hopefully not too complicated) linear algebra, so let’s have a look.

The state of a single qubit is described by a single vector $\begin{bmatrix} \alpha \cr \beta \end{bmatrix}$ in a two dimensional Hilbert space. More generally, we can say that a qubit is a quantum system that in which we can select two linearly independent states representing 0 and 1 and which can be modeled using a two dimensional complex vector space..

While the hardware design is out of scope for this series, in terms of physical implementation of that, qubits could be implemented using electron spins or photon polarizations.

When dealing with qubits, we have to reason about them in terms of the mathematical concept of a basis. There are always infinitely many bases to choose from (as long as the two distinguished chosen states of 0 and 1 are orthonormal), in quantum computing, two basic unit vectors $\begin{bmatrix} 1 \cr 0 \end{bmatrix}$ and $\begin{bmatrix} 0 \cr 1 \end{bmatrix}$ form the so-called computational basis.

In addition, we know from linear algerbra that vectors can be written as linear combinations of basis vectors. As such, qubit state $\ket{\varphi}$ can always be described as:

$$\ket{\varphi} = \alpha\begin{bmatrix} 1 \\ 0 \end{bmatrix} + \beta\begin{bmatrix} 0 \\ 1 \end{bmatrix}$$

In quantum mechanics, $\alpha$ and $\beta$ would be complex numbers, since we are really dealing with two-dimensional complex valued vector here, but for the simplicity of this discussion, we can assume those are real numbers for now.

In the Dirac notation, which is prevalent in the quantum mechanics, we can express $\begin{bmatrix} 1 \cr 0 \end{bmatrix}$ and $\begin{bmatrix} 0 \cr 1 \end{bmatrix}$ as $\ket{0}$ and $\ket{1}$, respectively.

$$\ket{\varphi} = \alpha\ket{0} + \beta\ket{1}$$

The main reason to use the Dirac notation, is that, aside from being quite succinct, it is also independent of the basis chosen.

One of the fundamental strengths of quantum computers lies in the fact that a qubit may be in a superposition state, and we can use that fact to our advantage in our algorithms. However, as soon as it is measured (in a certain basis, of course), its state (value) always collapses to one of the two basis states, either $\ket{0}$ or $\ket{1}$. This also leads us to another weird aspect of quantum mechanics - measurement of a quantum state will change that quantum state.

A word of caution here. The choice of basis is fundamentally important here - superposition is basis-dependent; in other words, a state is always in superposition with respect to certain bases and not in superposition to others. The same notion applies to measurement, when a state is measured in certain bases, it will produce deterministic results, while in others it will produce random results.

However, we digressed a bit, so let’s go back to our qubit. We refer to $\alpha$ and $\beta$ as probability amplitudes. We can relate amplitudes to the actual classical probability of receiving a state $\ket{0}$ or $\ket{1}$ using the Born rule:

$$|\alpha|^2 + |\beta|^2 = 1$$

The classical probability of collapsing to $\ket{0}$ is therefore $|\alpha|^2$, and conversely, the probability of collapsing to $\ket{1}$ is $|\beta|^2$. This is actually one of the axioms of quantum mechanics. The rule itself is not derived from anything, it is instead given, based on experimental evidence only.

The final conclusion we can draw here is the following - until we measure it, the qubit state can be in one of infinitely many various superposition states, but we can only ever extract one classical bit out of it upon measurement. We are going to be discussing various qubit transformations in the upcoming posts.

Getting started with qubits in Q 🔗

Earlier in this post, we have mentioned the steps needed to install the QDK. We can now start with our first program.

The simplest way to do so, is to use the dotnet CLI. The below command creates a new C# command line application, with a Q# component. The name of the program is inferred from the name of the current folder.

dotnet new console -lang "Q#"

As previous discussed, the C# program acts as a “host application” here, while we can at any point yield to the Q# part of our application to execute any quantum operation. The template generates a default program that is not particularly exciting, here is how it should look if everything worked correctly. C#:

using System;

using Microsoft.Quantum.Simulation.Core;
using Microsoft.Quantum.Simulation.Simulators;

namespace QubitExample
{
    class Driver
    {
        static void Main(string[] args)
        {
            using (var qsim = new QuantumSimulator())
            {
                HelloQ.Run(qsim).Wait();
            }
        }
    }
}

Q#

namespace QubitExample {

    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    

    operation HelloQ() : Unit {
        Message("Hello quantum world!");
    }
}

You can run the program using the dotnet CLI with the regular dotnet run command, and it should print:

Hello quantum world!

Let’s adapt this out-of-the-box template to something more useful, that will allow us to check some of the statements we made about the qubit behavior. Our first interaction with qubits will be building a small program that will allow us to allocate some qubits, measure their values and print the results.

At the Q# code level, we will change the quantum operation result from Unit (which is semantically equivalent to void in C#) to a Int, since we will want some data to flow back to us. We allocate a qubit with a using statement; you can allocate multiple qubits at once if you need, but in our case we will stick to single qubit operations. A newly allocated qubit is by convention automatically initalized to $\ket{0}$ state. Once the qubit is used and is no longer needed, it must be reset back to $\ket{0}$ state again and safely released.

As mentioned, to extract the classical bit out of a qubit, we must measure it. In Q# we can measure using the Measure method, and specifying the basis we want to use. In our case, we are interested in measuring the qubit in the computational basis, known as Pauli-Z basis.

The sample code is shown below:

namespace QubitExample {

    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Measurement;
    

    operation MeasureQubits(count : Int) : Int {

        mutable resultsTotal = 0;

        using (qubit = Qubit()) {

            for (idx in 0..count) {
                let result = Measure([PauliZ], [qubit]);
                set resultsTotal += result == One ? 1 | 0;
                Reset(qubit);
            }

            return resultsTotal;
        }
    }
}

As input we are passing in an integer representing the amount of times we should run a measurement on a fresh qubit (new qubit for first run, and then a reset qubit for subsequent runs). We then keep the running total of the results. If at the end, resultsTotal = 0 it means we only got zeros, if resultsTotal = count it means we only got ones, and anything in between means the measurements were random.

Our updated C# code (including small tweaks to reduce nesting and make it more C# 8 friendly) to invoke this looks as follows:

static async Task Main(string[] args)
{
    using var qsim = new QuantumSimulator();
    
    var repeats = 100;
    Console.WriteLine($"Running qubit measurement {repeats} times.");

    var results = await MeasureQubits.Run(qsim, repeats);
    Console.WriteLine($"Received {results} ones.");
    Console.WriteLine($"Received {repeats - results} zeros.");
}

We can now run this code and see what happens. The result is below:

Received 0 ones.
Received 100 zeros.

We got 100 zeros in 100 attempts, which is quite encouraging. Remember that we said that newly initialized qubits have a $\ket{0}$ state, and the output of the program seems to agree. We also never did anything to put the qubit into a superposition (this will be covered in the next post in this series) so no randomness should occur too.

We can verify one other claim at this point. We said that we can measure the qubit in various bases, and choosing the basis is critical for getting a deterministic or probabilistic value. To check that, let’s measure in a different basis - for example Pauli-X.

It’s a small change in our code - just replace PauliX with PauliZ in our Measure invocation.

    operation MeasureQubits(count : Int) : Int {

        mutable resultsTotal = 0;

        using (qubit = Qubit()) {

            for (idx in 0..count) {
                let result = Measure([PauliX], [qubit]);
                set resultsTotal += result == One ? 1 | 0;
                Reset(qubit);
            }

            return resultsTotal;
        }
    }

When we run our program, we should see something like this:

Received 52 ones.
Received 48 zeros.

The distribution is not ideal, because the sample size is small, but the pattern is pretty clear. This aligns with our earlier statement that a quantum state is always in superposition with respect to certain bases and not in superposition to others. This is quite profound, and we’ll discuss superposition extensively next time.

Before we finish for today, one additional note. The Q# code we wrote is actually unnecessarily verbose. The language and its core library ships with a ton of shortcuts and utilities that make quantum code succinct and pleasant. In our sample, we can actually collapse the measure in standard basis (Pauli-Z) and the reset operations into a single one - MResetZ.

The updated code is shown below.

    operation MeasureQubits(count : Int) : Int {

        mutable resultsTotal = 0;

        using (qubit = Qubit()) {
        
            for (idx in 0..count) {               
                let result = MResetZ(qubit);
                set resultsTotal += result == One ? 1 | 0;
            }

            return resultsTotal;
        }
    }

Summary 🔗

In this blog post we looked at the historical background of quantum mechanics and discussed how we currently find ourselves at a breakthrough point, with a booming landscape of quantum hardware and software solutions.

We had a look at how to get started with QDK and Q# and explored the mathematical notion of a qubit. Finally, we had a look at some basic qubit measurement behavior using Q# and the quantum simulator.

In the next post in this series we will explore the mathematics and Q# code related to superposition.

About


Hi! I'm Filip W., a software architect from Zürich 🇨🇭. I like Toronto Maple Leafs 🇨🇦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁢󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP