Create Your Own Q # Simulator - Part 1

Simulators are a particularly versatile feature of the QDK. They allow you to perform various tasks in a Q # program without changing it. Such tasks include full state simulation , resource estimation, or tracing simulation . The new interface IQuantumProcessormakes it very easy to create your own simulators and integrate them into your Q # projects.



This post is the first in a series on this interface. We'll start by implementing a reversible simulator as a first example, which we'll expand on in future blog posts. Reversible simulator can simulate quantum programs that consist only of classical operations: X, CNOT,CCNOT(Toffoli gate) or randomly controlled X-operations. Since a reversible simulator can represent a quantum state by assigning one boolean value to each qubit, it can even run quantum programs of thousands of qubits. This simulator is very useful for testing quantum operations that evaluate boolean functions.







Simulator implementation in C #



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


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



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

    //       (intrinsic operations)...
}


The 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 Boolean values ​​false and true, respectively. Simulators define internal operations in a Q # program. QuantumProcessorBasecontains a method for each internal operation that you can override to define its behavior in the new simulator. Note that unimplemented methods will throw a default exception. This will help you identify cases where the operation still needs to be implemented and will inform the simulator user that the inline 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 start by falsely initializing the newly allocated qubits by overriding the OnAllocateQubits method. Likewise, qubits are removed from the dictionary when they are released. In this case, OnReleaseQubits is called.



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 simulation values ​​are available when the operation is applied to some qubit, and that there are no simulation values ​​left in the dictionary for qubits that are not in the current region.



As a next step, let's implement the classic operations actions. In this case, two methods are sufficient: the X method is called when the X operation is simulated, and the ControlledX method is called when the X operation is simulated, which also includes CNOT and CCNOT. The X operation inverts the simulation value of the qubit, whereas in the case of an arbitrarily controlled X operation, the target qubit is inverted if and only if all control qubits are set to 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 the qubit in the Q # program returns a result (One or Zero), which can be calculated based on the current simulation value of the qubit. We also implement a Reset method that is called when a reset operation is called in a Q # program. The latter does not remove the qubit from the current region, 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 instantiating the QuantumProcessorDispatcher. The recommended design style is to provide a custom 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:



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 performs a majority operation by providing three boolean inputs:



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 it all together by invoking a Q # operation with a new simulator in the C # host program. The following example evaluates the main operation for all different input destinations and displays 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 this post will inspire you to create your own simulator. In the next steps, we will discuss how to improve the performance of the current implementation (for example, by not using a dictionary to store 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.



All Articles