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 2019. 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 {
        using (q = Qubit()) {
            Assert([PauliZ], [q], Zero, "Newly allocated qubit must be in the |0⟩ state.");
        }
        
        Message("Test passed");
    }

Any Q# operation compatible with the type (Unit => Unit) or function compatible with (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 of compatible type 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 intrinsic function Message has type (String -> Unit) 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.Diagnostics namespace provides several more functions of the Assert family which allow us to check more advanced conditions.

Dump Functions

To help troubleshooting quantum programs, the Microsoft.Quantum.Diagnostics namespace 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:

# wave function for qubits with ids (least to most significant): 0;1
∣0❭:	 0.707107 +  0.000000 i	 == 	**********           [ 0.500000 ]     --- [  0.00000 rad ]
∣1❭:	 0.000000 +  0.000000 i	 == 	                     [ 0.000000 ]                   
∣2❭:	-0.500000 + -0.500000 i	 == 	**********           [ 0.500000 ]   /     [ -2.35619 rad ]
∣3❭:	 0.000000 +  0.000000 i	 == 	                     [ 0.000000 ]                   

The first row provides a comment with the IDs of the corresponding qubits in their significant order. The rest of the rows describe the probability amplitude of measuring the basis state vector $\ket{n}$ in both Cartesian and polar formats. In detail for the first row:

  • ∣0❭: this row corresponds to the 0 computational basis state
  • 0.707107 + 0.000000 i: the probability amplitude in Cartesian format.
  • ==: the equal sign seperates both equivalent representations.
  • **********: A graphical representation of the magnitude, the number of * is proportionate to the probability of measuring this state vector.
  • [ 0.500000 ]: the numeric value of the magnitude
  • ---: A graphical representation of the amplitude's phase (see below).
  • [ 0.0000 rad ]: the numeric value of the phase (in radians).

Both the magnitude and the phase are displayed with a graphical representation. The magnitude representation is straight-forward: it shows a bar of *, the bigger the probability the bigger the bar will be. For the phase, we show the following symbols to represent the angle based on ranges:

[ -π/16,   π/16)       ---
[  π/16,  3π/16)        /-
[ 3π/16,  5π/16)        / 
[ 5π/16,  7π/16)       +/ 
[ 7π/16,  9π/16)      ↑   
[ 8π/16, 11π/16)    \-    
[ 7π/16, 13π/16)    \     
[ 7π/16, 15π/16)   +\     
[15π/16, 19π/16)   ---    
[17π/16, 19π/16)   -/     
[19π/16, 21π/16)    /     
[21π/16, 23π/16)    /+    
[23π/16, 25π/16)      ↓   
[25π/16, 27π/16)       -\ 
[27π/16, 29π/16)        \ 
[29π/16, 31π/16)        \+
[31π/16,   π/16)       ---

The following examples show DumpMachine for some common states:

∣0❭

# wave function for qubits with ids (least to most significant): 0
∣0❭:	 1.000000 +  0.000000 i	 == 	******************** [ 1.000000 ]     --- [  0.00000 rad ]
∣1❭:	 0.000000 +  0.000000 i	 == 	                     [ 0.000000 ]                   

∣1❭

# wave function for qubits with ids (least to most significant): 0
∣0❭:	 0.000000 +  0.000000 i	 == 	                     [ 0.000000 ]                   
∣1❭:	 1.000000 +  0.000000 i	 == 	******************** [ 1.000000 ]     --- [  0.00000 rad ]

∣+❭

# wave function for qubits with ids (least to most significant): 0
∣0❭:	 0.707107 +  0.000000 i	 == 	**********           [ 0.500000 ]      --- [  0.00000 rad ]
∣1❭:	 0.707107 +  0.000000 i	 == 	**********           [ 0.500000 ]      --- [  0.00000 rad ]

∣-❭

# wave function for qubits with ids (least to most significant): 0
∣0❭:	 0.707107 +  0.000000 i	 == 	**********           [ 0.500000 ]      --- [  0.00000 rad ]
∣1❭:	-0.707107 +  0.000000 i	 == 	**********           [ 0.500000 ]  ---     [  3.14159 rad ]

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.Diagnostics namespace, so in order to use it you must add an open statement:

namespace Samples {
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Diagnostics;

    operation Operation () : Unit {
        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:

# wave function for qubits with ids (least to most significant): 0
∣0❭:	-0.707107 + -0.707107 i	 == 	******************** [ 1.000000 ]  /      [ -2.35619 rad ]
∣1❭:	 0.000000 +  0.000000 i	 == 	                     [ 0.000000 ]                   

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

# wave function for qubits with ids (least to most significant): 1
∣0❭:	 0.707107 +  0.000000 i	 == 	***********          [ 0.500000 ]     --- [  0.00000 rad ]
∣1❭:	-0.500000 + -0.500000 i	 == 	***********          [ 0.500000 ]  /      [ -2.35619 rad ]

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.Intrinsic;
    open Microsoft.Quantum.Diagnostics;

    operation Operation () : Unit {

        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.