Testing and Debugging

As with classical programming, it is essential to be able to check that quantum programs act as intended, and to be able to diagnose a quantum program that is incorrect. In this section, we cover the tools offered by Q# for testing and debugging quantum programs.

Unit Tests

One common approach to testing classical programs is to write small programs called unit tests which run code in a library and compare its output to some expected output. For instance, we may want to ensure that Square(2) returns 4, since we know a priori that $2^2 = 4$.

Q# supports creating unit tests for quantum programs, and which can be executed as tests within the xUnit unit testing framework.

Creating a Test Project

Open Visual Studio 2017. Go to the File menu and select New > Project.... In the project template explorer, under Installed > Visual C#, select the Q# Test Project template.

In either case, your new project will have two files open. The first file, Tests.qs, provides a convenient place to define new Q# unit tests. Initially this file contains one sample unit test AllocateQubitTest which checks that a newly allocated qubit is in the $\ket{0}$ state and prints a message:

operation AllocateQubitTest () : Unit
{
    body (...)
    {
        using (qs = Qubit()) 
        {
            Assert([PauliZ], [qs], Zero, "Newly allocated qubit must be in |0> state");
        }
        Message("Test passed");
    }
}

Any Q# operation with the signature Unit => Unit or function with the signature Unit -> Unit can be executed as a unit test.

The second file, TestSuiteRunner.cs contains a method that discovers and runs Q# unit tests. This is the method TestTarget annotated with OperationDriver attribute. The OperationDriver attribute is a part of the Xunit extension library Microsoft.Quantum.Simulation.Xunit. The unit testing framework calls TestTarget method for every Q# unit test it has discovered. The framework passes the unit test description to the method through op argument. The following line of code:

op.TestOperationRunner(sim);

executes the unit test on QuantumSimulator.

By default, the unit test discovery mechanism looks for all Q# functions or operations with signatures Unit => Unit or Unit -> Unit that satisfy the following properties:

  • Located in the same assembly as the method annotated with the OperationDriver attribute.
  • Located in the same namespace as the method annotated with the OperationDriver attribute.
  • Has a name ending with Test.

An assembly, a namespace, and a suffix for unit test functions and operations can be set using optional parameters of the OperationDriver attribute:

  • AssemblyName parameter sets the name of the assembly which is being searched for tests.
  • TestNamespace parameter sets the name of the namespace which is being searched for tests.
  • Suffix sets the suffix of operation or function names that are considered to be unit tests.

In addition, the TestCasePrefix optional parameter lets you set a prefix for the name of the test case. The prefix in front of the operation name will appear in the list of test cases. For example, TestCasePrefix = "QSim:" will cause AllocateQubitTest to appear as QSim:AllocateQubitTest in the list of found tests. This can be useful to indicate, for instance, which simulator is used to run a test.

Running Q# Unit Tests

As a one-time per-solution setup, go to Test menu and select Test Settings > Default Processor Architecture > X64.

Tip

The default processor architecture setting for Visual Studio is stored in the solution options (.suo) file for each solution. If you delete this file, then you will need to select X64 as your processor architecture again.

Build the project, go to the Test menu and select Windows > Test Explorer. AllocateQubitTest will show up in the list of tests in the Not Run Tests group. Select Run All or run this individual test, and it should pass!

Logging and Assertions

One important consequence of the fact that functions in Q# have no side effects is that any effects of executing a function whose output type is the empty tuple () can never be observed from within a Q# program. That is, a target machine can choose not to execute any function which returns () with the guarantee that this omission will not modify the behavior of any following Q# code. This makes functions returning () a useful tool for embedding assertions and debugging logic into Q# programs.

Logging

The primitive function Message has type String -> () and enables the creation of diagnostic messages.

The onLog action of QuantumSimulator can be used to define actions performed when Q# code calls Message. By default logged messages are printed to standard output.

When defining a unit test suite, the logged messages can be directed to the test output. When a project is created from Q# Test Project template, this redirection is pre-configured for the suite and created by default as follows:

using (var sim = new QuantumSimulator())
{
    // OnLog defines action(s) performed when Q# test calls operation Message
    sim.OnLog += (msg) => { output.WriteLine(msg); };
    op.TestOperationRunner(sim);
}

After you execute a test in Test Explorer and click on the test, a panel will appear with information about test execution: Passed/Failed status, elapsed time and an "Output" link. If you click the "Output" link, test output will open in a new window.

test output

Assertions

The same logic can be applied to implementing assertions. Let's consider a simple example:

function AssertPositive(value : Double) : Unit 
{
    if (value <= 0) 
    {
        fail "Expected a positive number.";
    }
}

Here, the keyword fail indicates that the computation should not proceed, raising an exception in the target machine running the Q# program. By definition, a failure of this kind cannot be observed from within Q#, as no further Q# code is run after a fail statement is reached. Thus, if we proceed past a call to AssertPositive, we can be assured by that its input was positive.

Building on these ideas, the prelude offers two especially useful assertions, Assert and AssertProb both modeled as operations onto (). These assertions each take a Pauli operator describing a particular measurement of interest, a quantum register on which a measurement is to be performed, and a hypothetical outcome. On target machines which work by simulation, we are not bound by the no-cloning theorem, and can perform such measurements without disturbing the register passed to such assertions. A simulator can then, similar to the AssertPositive function above, abort computation if the hypothetical outcome would not be observed in practice:

using (register = Qubit()) 
{
    H(register);
    Assert([PauliX], [register], Zero);
    // Even though we do not have access to states in Q#,
    // we know by the anthropic principle that the state
    // of register at this point is |+〉.
}

On physical quantum hardware, where the no-cloning theorem prevents examination of quantum state, the Assert and AssertProb operations simply return () with no other effect.

The Microsoft.Quantum.Canon namespace provides several more functions of the Assert family which allow us to check more advanced conditions. They are detailed in Q# standard libraries: Testing and Debugging section.

Dump Functions

To help troubleshooting quantum programs, the prelude offers two functions that can dump into a file the current status of the target machine: DumpMachine and DumpRegister. The generated output of each depends on the target machine.

DumpMachine

The full-state quantum simulator distributed as part of the Quantum Development Kit writes into the file the wave function of the entire quantum system, as a one-dimensional array of complex numbers, in which each element represents the amplitude of the probability of measuring the computational basis state $\ket{n}$, where $\ket{n} = \ket{b_{n-1}...b_1b_0}$ for bits ${b_i}$. For example, on a machine with only two qubits allocated and in the quantum state $$ \begin{align} \ket{\psi} = \frac{1}{\sqrt{2}} \ket{00} - \frac{(1 + i)}{2} \ket{10}, \end{align} $$ calling DumpMachine generates this output:

Ids:    [1;0;]
Wavefunction:
0:      0.707107        0
1:      0               0
2:      -0.5            -0.5
3:      0               0

Notice how the ids of the qubits are shown at the top in their significant order. For each computational basis state $\ket{n}$, the first column represents the Real part of the amplituted, and the second column represents its Imaginary part.

Note

The id of a qubit is assigned at runtime and it's not necessarily aligned with the order in which the qubit was allocated or its position within a qubit register.

Tip

You can figure out a qubit id in Visual Studio by putting a breakpoint in your code and inspecting the value of a qubit variable, for example:

show qubit id in Visual Studio

the qubit with index 0 on register2 has id=3, the qubit with index 1 has id=2.

DumpMachine is part of the Microsoft.Quantum.Extensions.Diagnostics namespace, so in order to use it you must add an open statement:

namespace Samples
{
    open Microsoft.Quantum.Primitive;
    open Microsoft.Quantum.Extensions.Diagnostics;

    operation Operation () : Unit
    {
        body (...)
        {
            using (qubits = Qubit[2])
            {
                H(qubits[1]);
                DumpMachine("dump.txt");
            }
        }
    }
}

DumpRegister

DumpRegister works like DumpMachine, except it also takes an array of qubits to limit the amount of information to only that relevant to the corresponding qubits.

As with DumpMachine, the information generated by DumpRegister depends on the target machine. For the full-state quantum simulator it writes into the file the wave function up to a global phase of the quantum sub-system generated by the provided qubits in the same format as DumpMachine. For example, take again a machine with only two qubits allocated and in the quantum state $$ \begin{align} \ket{\psi} = \frac{1}{\sqrt{2}} \ket{00} - \frac{(1 + i)}{2} \ket{10} = - e^{-i\pi/4} ( (\frac{1}{\sqrt{2}} \ket{0} - \frac{(1 + i)}{2} \ket{1} ) \otimes \frac{-(1 + i)}{\sqrt{2}} \ket{0} ) , \end{align} $$ calling DumpRegister for qubit[0] generates this output:

Ids:    [0;]
Wavefunction:
0:      -0.707107       -0.707107
1:      0               0

and calling DumpRegister for qubit[1] generates this output:

Ids:    [1;]
Wavefunction:
0:      0.707107        0
1:      -0.5            -0.5

In general, the state of a register that is entangled with another register is a mixed state rather than a pure state. In this case, DumpRegister outputs the following message:

Qubits provided (0;) are entangled with some other qubit.

The following example shows you how you can use both DumpRegister and DumpMachine in your Q# code:

namespace app
{
    open Microsoft.Quantum.Primitive;
    open Microsoft.Quantum.Extensions.Diagnostics;

    operation Operation () : Unit
    {
        body (...)
        {
            using (qubits = Qubit[2])
            {
                X(qubits[1]);
                H(qubits[1]);
                R1Frac(1, 2, qubits[1]);

                DumpMachine("dump.txt");
                DumpRegister("q0.txt", qubits[0..0]);
                DumpRegister("q1.txt", qubits[1..1]);

                ResetAll(qubits);
            }
        }
    }
}

Debugging

On top of Assert and Dump functions and operations, Q# supports a subset of standard Visual Studio debugging capabilities: setting line breakpoints, stepping through code using F10 and inspecting values of classic variables are all possible during code execution on the simulator.

Debugging in Visual Studio Code is not yet supported.