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
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:
- The call to
AssertQubit
will not raise an error when qubitf
is in stateOne
, because asserting qubit states is not yet implemented in the custom simulator. - The execution of
ApplyAnd
will cause a runtime exception, because its library implementation contains non-classical operations such asH
andT
.
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