Build your own Q# simulator – Part 2: Advanced features for the reversible simulator

Mathias Soeken

Mathias

In the first part of this blog post series we learned how to use the IQuantumProcessor interface to write a custom simulator that performs reversible simulation in a quantum circuit that is composed solely using classical reversible operations.

This blog post builds up on the first part of this series to show how to implement more advanced simulator capabilities:

  • Integrating non-intrinsic operations to support operations such as ApplyAnd in the reversible simulator, which have a classical simulation behavior but are implemented using non-classical quantum operations.
  • Extending error handling to implement useful assertions such as AssertQubit.
  • Calling the Q# code directly without a C# driver file and setting the simulator to use via the project file.
  • Improving performance by storing simulation values in a BitArray instead of a dictionary.

Integrating a non-intrinsic operation

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

Recall the ApplyMajority Q# operation from the previous blog post. It uses a CCNOT operation whose target is the qubit that will hold the output value for the majority operation. In typical applications it’s safe to assume that the target qubit is in a zero state. Therefore, we can replace the CCNOT operation with the ApplyAnd operation. Using this operation has the advantage that it requires less resources in a fault-tolerant quantum computing implementation, and therefore yields more accurate resource estimates. To make sure that the ApplyMajority operation is invoked as expected, we further add a call to AssertQubit.

operation ApplyMajority(a : Qubit, b : Qubit, c : Qubit, f : Qubit) : Unit {
    AssertQubit(Zero, f);

    within {
        CNOT(b, a);
        CNOT(b, c);
    } apply {
        ApplyAnd(a, c, f);
        CNOT(b, f);
    }
}

The current implementation of the reversible simulator is not suitable to simulate this operation due to two problems:

  1. The call to AssertQubit will not raise an error when qubit f is in state One, because asserting qubit states is not yet implemented in the custom simulator.
  2. The execution of ApplyAnd will cause a runtime exception, because its library implementation contains non-classical operations such as H and T.

We address the first problem by overriding and implementing the Assert method in ReversibleSimulatorProcessor as follows:

public override void Assert(IQArray<Qubit> bases, IQArray<Qubit> qubits, Result expected, string msg) {
    if (bases.Count == 1 && bases[0] == Pauli.PauliZ) {
        if (GetValue(qubits[0]).ToResult() != expected) {
            throw new ExecutionFailException(msg);
        }
    } else {
        throw new InvalidOperationException("Assert only supported for single-qubit Z-measurements");
    }
}

In this implementation we only consider the case of a single-qubit measurement in the Z-basis. In this case we check whether the current state of the asserted qubit corresponds to the expected value. Since GetValue returns the qubit’s state as a bool, we translate it to a Result type using the ToResult() method.

The second problem is addressed by simply treating the ApplyAnd function as the CCNOT function, since they behave equally when simulated classically. You can setup this mapping in the constructor of the ReversibleSimulator class:

public ReversibleSimulator(bool throwOnReleasingQubitsNotInZeroState = true)
    : base(new ReversibleSimulatorProcessor
      { ThrowOnReleasingQubitsNotInZeroState = throwOnReleasingQubitsNotInZeroState } {
    
    // Perform ApplyAnd as it was CCNOT
    Register(typeof(ApplyAnd), typeof(CCNOT), typeof(IUnitary<(Qubit, Qubit, Qubit)>));
}

The type that is passed as the third argument must match the type of both operations, and can be derived from the operation’s Q# signature. For most operations, the type is IUnitary specified over a value tuple type with respect to the signature in Q#, which is 3 Qubit arguments in the case of ApplyAnd and CCNOT.

Also note that the constructor of the ReversibleSimulator contains a parameter throwOnReleasingQubitsNotInZeroState that mimics the behavior in QuantumSimulator. Its use in the OnReleaseQubits is discussed later in the blog post.

Calling the Q# code directly

With the QDK it’s possible to directly write the host program in Q# and specify the simulator used to run it via the project file (or command line). For this purpose, it is advised to develop the Q# host program and the C# simulator in two separate projects, let’s say host.csproj for the Q# program and simulator.csproj for the C# library containing the custom Q# simulator. This will also allow you to reuse the simulator among several different Q# projects. The Q# project only contains Q# source files with one file including the entry point operation:

@EntryPoint()
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 IsResultOne(MResetZ(f));
    }
}

This entry point operation takes as input 3 Boolean values for the simulation of the majority function, and it returns a Boolean value indicating the result.

The simulator is specified in the Q# project file as follows:

<Project Sdk="Microsoft.Quantum.Sdk/0.11.2004.2825">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <OutputType>Exe</OutputType>
    <DefaultSimulator>Microsoft.Quantum.Samples.ReversibleSimulator</DefaultSimulator>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\simulator\simulator.csproj" />
  </ItemGroup>
</Project>

The DefaultSimulator tag contains the class name (including the complete namespace) for the custom simulator that should be used to run the Q# program, and the ProjectReference contains a reference to the simulator project file (which could also be a NuGet package reference, for example).

If you are using the command line, you can run the Q# program and pass it some example simulation values that will be passed as parameters to the @EntryPoint operation (in our case RunMajority):

dotnet run -- -a true -b true -c true

which will output True to the terminal. You can also override the default simulator for the project (specify a different simulator as simulation target) using the -s option in the call to dotnet run.

Improving the performance

The implementation for the reversible simulator in the first blog post was using IDictionary<Qubit, bool> as a container to store the simulation values, which is simple to use but not very efficient in runtime and memory usage. In this implementation we are using a BitArray instead, which stores multiple Boolean values in a single integer. We can use the qubit’s id to index this array.

private BitArray simulationValues = new BitArray(64);

We pre-initialize the array to 64 bits, and grow it dynamically whenever qubits are allocated which exceed the size of the array:

public override void OnAllocateQubits(IQArray<Qubit> qubits) {
    // returns the largest qubit's id in qubits
    var maxId = qubits.Max(q => q.Id);

    // double the bit array's size as a dynamic growing strategy;
    // newly allocated bits are initialized to false
    while (maxId >= simulationValues.Length) {
        simulationValues.Length *= 2;
    }
}

The following three helper methods are added to the ReversibleSimulatorProcessor to facilitate the access and modification of the simulation values:

private bool GetValue(Qubit qubit) {
    return simulationValues[qubit.Id];
}

private void SetValue(Qubit qubit, bool value) {
    simulationValues[qubit.Id] = value;
}

private void InvertValue(Qubit qubit) {
    simulationValues[qubit.Id] = !simulationValues[qubit.Id];
}

These methods are, e.g., used in the OnReleaseQubits method, in which simulation values are restored to false. The method also throws an assertion in case a qubit’s simulation value is true when being released and the ThrowOnReleasingQubitsNotInZeroState is set to true.

public override void OnReleaseQubits(IQArray<Qubit> qubits) {
    foreach (var qubit in qubits) {
        if (GetValue(qubit)) {
            if (ThrowOnReleasingQubitsNotInZeroState) {
                throw new ReleasedQubitsAreNotInZeroState();
            }
            SetValue(qubit, false);
        }
    }
}

Next steps

We hope that this blog post provided further insights in how to integrate some more advanced techniques when developing your own simulator. In the next blog post we will show you how to build a custom simulator that allows you to create a circuit diagram of your code. We are eager to hear your questions or suggestions, and hope to address all of them in upcoming posts in this series!

0 comments

Comments are closed.