Build your own Q# simulator – Part 1: A simple reversible simulator

Mathias Soeken

Mathias

Simulators are a particularly versatile feature of the QDK. They allow you to perform various different tasks on a Q# program without changing it. Such tasks include full state simulation, resource estimation, or trace simulation. The new IQuantumProcessor interface makes it very easy to write your own simulators and integrate them into your Q# projects. This blog post is the first in a series that covers this interface. We start by implementing a reversible simulator as a first example, which we extend in future blog posts. A reversible simulator can simulate quantum programs that consist only of classical operations: X, CNOT, CCNOT (Toffoli gate), or arbitrarily controlled X operations. Since a reversible simulator can represent the quantum state by assigning one Boolean value to each qubit, it can run even quantum programs that consist of thousands of qubits. This simulator is very useful for testing quantum operations that evaluate Boolean functions.

Implementing the simulator in C#

ℹ️ This blog post highlights the essential code snippets. The complete source code can be found in the Microsoft QDK Samples repository

You start writing your own simulator by extending the QuantumProcessorBase class:

class ReversibleSimulatorProcessor : QuantumProcessorBase {
    private IDictionary<Qubit, bool> simulationValues = new Dictionary<Qubit, bool>();

    // define custom actions for intrinsic operations...
}

A dictionary that will store the current simulation value for each qubit in the program has already been added to the class. The classical quantum states |0⟩ and |1⟩ are represented as the Boolean values false and true, respectively. Simulators define intrinsic operations in the Q# program. QuantumProcessorBase contains a method for each intrinsic operation, which you can override to define its behavior in the new simulator. Note that unimplemented methods throw an exception by default. This will help you spot cases in which an operation still needs to be implemented and tells the user of the simulator that an intrinsic operation is not supported by the simulator. For example, a reversible simulator cannot be used to simulate quantum programs that contain non-classical operations such as H or T.

Let’s begin by initializing newly allocated qubits to false by overriding the OnAllocateQubits method. Similarly, qubits are removed from the dictionary when they are released. The OnReleaseQubits is called in that case.

public override void OnAllocateQubits(IQArray qubits) {
    foreach (var qubit in qubits) {
        simulationValues[qubit] = false;
    }
}

public override void OnReleaseQubits(IQArray qubits) {
    foreach (var qubit in qubits) {
        simulationValues.Remove(qubit);
    }
}

With these two operations, it is guaranteed that the simulation values are available whenever an operation is applied to some qubit, and that no simulation values remain in the dictionary for qubits which are not present in the current scope.

As a next step, let’s implement the actions of classical operations. Two methods suffice in this case, the X method is called when an X operation is simulated, and the ControlledX method is called when an arbitrarily controlled X operation is simulated, which also includes CNOT and CCNOT. The action of an X operation inverts the simulation value of a qubit, whereas in the case of an arbitrarily controlled X operation the target qubit is inverted if and only if all control qubits are assigned true.

public override void X(Qubit qubit) {
    simulationValues[qubit] = !simulationValues[qubit];
}

public override void ControlledX(IQArray controls, Qubit qubit) {
    simulationValues[qubit] ^= And(controls);
}

Finally, measuring a qubit in a Q# program returns a result (One or Zero), which can be computed based on the qubit’s current simulation value. We also implement the Reset method that is called when the Reset operation is called in a Q# program. The latter does not remove the qubit from the current scope, but resets the simulation value back to its initial value, which is false.

public override Result M(Qubit qubit) {
    return simulationValues[qubit] ? Result.One : Result.Zero;
}

public override void Reset(Qubit qubit) {
    simulationValues[qubit] = false;
}

The ReversibleSimulatorProcessor can be used as a simulator by constructing a QuantumProcessorDispatcher instance. A recommended design style is to provide a dedicated class for the simulator:

public class ReversibleSimulator : QuantumProcessorDispatcher {
    public ReversibleSimulator() : base(new ReversibleSimulatorProcessor()) {}
}

Using the new simulator

Let’s try the new simulator on the following Q# operation, which implements a majority-of-three voter:

operation ApplyMajority(a : Qubit, b : Qubit, c : Qubit, f : Qubit) : Unit {
    within {
        CNOT(b, a);
        CNOT(b, c);
    } apply {
        CCNOT(a, c, f);
        CNOT(b, f);
    }
}

We also write an operation that exercises the majority operation by providing three Boolean input values:

operation RunMajority(a : Bool, b : Bool, c : Bool) : Bool {
    using ((qa, qb, qc, f) = (Qubit(), Qubit(), Qubit(), Qubit())) {
        within {
            ApplyPauliFromBitString(PauliX, true, [a, b, c], [qa, qb, qc]);
        } apply {
            ApplyMajority(qa, qb, qc, f);
        }
        return MResetZ(f) == One;
    }
}

Finally, you can put everything together by calling the Q# operation using the new simulator in a C# host program. The following example evaluates the majority operation for all different input assignments and prints all simulation results:

public static void Main(string[] args) {
    var sim = new ReversibleSimulator();
    var bits = new[] {false, true};

    foreach (var a in bits) {
        foreach (var b in bits) {
            foreach (var c in bits) {
                var f = RunMajority.Run(sim, a, b, c).Result;
                Console.WriteLine($"Majority({a,5}, {b,5}, {c,5})  =  {f,5}");
            }
        }
    }
}

Ready to write your own simulator?

We hope that this blog post inspires you to try writing your own simulator. Follow this blog for some more techniques to extend the capabilities of the simulator. In the next steps, we’ll discuss how to improve the performance of the current implementation (e.g., by not using a dictionary to store the simulation values), how to turn it into a standalone Q# project, how to provide custom actions for non-intrinsic operations, and how to provide diagnostic operations that help debugging. We are eager to hear your questions or suggestions, and hope to address all of them in upcoming posts in this series!

1 comment

Leave a comment