microsoft/qsharp-runtime

AssertOperationsEqualInPlace incorrectly throws exceptions and has incorrect error messages.

tobiashagge opened this issue · 3 comments

Describe the bug

AssertOperationsEqualInPlace throws exceptions claiming that released qubits aren't in a zero state when two operations are approximately, but not exactly, equal. Since the qubits are released by library code there is no way to prevent the exception. Also, when operations are different, there is a bug in the text for the exception thrown.

To Reproduce

namespace mwe_AssertOperationsEqualInPlace {

    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Diagnostics as Diag;
    
    @EntryPoint()
    operation SayHello() : Unit {
        Message("Hello Minimum Working Example");
        Diag.AssertOperationsEqualInPlace(1,Nothing,NotMuch);
    }

    operation Nothing(q : Qubit[]) : Unit is Adj+Ctl {
        I(q[0]);
    }

    operation NotMuch(q : Qubit[]) : Unit is Adj+Ctl {
        //let theta = .1; // exercise the incorrect "Zero" error message
        let theta = .0001; // exercize failure to reset qubits to zero
        R1(theta,q[0]);
    }
}

Expected behavior

When the theta = .1, an exception, but one that does not claim that the expected value should be "Zero".

When theta=.001, the program should complete without throwing an exception.

Actual behavior

For theta = .1:

Exception has occurred: CLR/Microsoft.Quantum.Simulation.Core.ExecutionFailException
An exception of type 'Microsoft.Quantum.Simulation.Core.ExecutionFailException' occurred in Microsoft.Quantum.Runtime.Core.dll but was not handled in user code: 'Qubit in invalid state. Expecting: Zero with tolerance 1E-05
    Expected:	1
    Actual:	0.9975020826390129'
   at Microsoft.Quantum.Simulation.Simulators.CommonNativeSimulator.QSimAssertProb.<get___Body__>b__5_0(ValueTuple`6 _args)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at Microsoft.Quantum.Diagnostics.AssertQubitWithinTolerance.<get___Body__>b__11_0(ValueTuple`3 __in__)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at Microsoft.Quantum.Diagnostics.AssertAllZeroWithinTolerance.<get___Body__>b__15_0(ValueTuple`2 __in__)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at Microsoft.Quantum.Diagnostics.AssertEqualOnBasisVector.<get___Body__>b__27_0(ValueTuple`3 __in__)
   at Microsoft.Quantum.Simulation.Core.OperationPartial`3.<get___Body__>b__22_0(P a)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Microsoft.Quantum.Simulation.Core.ICallable.Apply(Object args)
   at Microsoft.Quantum.Diagnostics.IterateThroughCartesianPower.<>c.<get___Body__>b__11_0(ValueTuple`3 __in__)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at Microsoft.Quantum.Diagnostics.AssertOperationsEqualInPlace.<get___Body__>b__15_0(ValueTuple`3 __in__)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at mwe_AssertOperationsEqualInPlace.SayHello.<get___Body__>b__24_0(QVoid __in__) in C:\Users\hagg940\OneDrive - PNNL\qsc\d\mwe_AssertOperationsEqualInPlace\Program.qs:line 10
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)

For theta = .001:

Exception has occurred: CLR/Microsoft.Quantum.Simulation.Simulators.Exceptions.ReleasedQubitsAreNotInZeroState
An exception of type 'Microsoft.Quantum.Simulation.Simulators.Exceptions.ReleasedQubitsAreNotInZeroState' occurred in Microsoft.Quantum.Runtime.Core.dll but was not handled in user code: 'Released qubits are not in zero state.'
   at Microsoft.Quantum.Simulation.Simulators.CommonNativeSimulator.QSimQubitManager.Release(Qubit qubit, Boolean wasUsedOnlyForBorrowing)
   at Microsoft.Quantum.Simulation.Common.QubitManager.Release(IQArray`1 qubitsToRelease)
   at Microsoft.Quantum.Simulation.Common.SimulatorBase.Release.Apply(IQArray`1 qubits)
   at Microsoft.Quantum.Diagnostics.AssertEqualOnBasisVector.<get___Body__>b__27_0(ValueTuple`3 __in__)
   at Microsoft.Quantum.Simulation.Core.OperationPartial`3.<get___Body__>b__22_0(P a)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Microsoft.Quantum.Simulation.Core.ICallable.Apply(Object args)
   at Microsoft.Quantum.Diagnostics.IterateThroughCartesianPower.<>c.<get___Body__>b__11_0(ValueTuple`3 __in__)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at Microsoft.Quantum.Diagnostics.AssertOperationsEqualInPlace.<get___Body__>b__15_0(ValueTuple`3 __in__)
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)
   at mwe_AssertOperationsEqualInPlace.SayHello.<get___Body__>b__24_0(QVoid __in__) in C:\Users\hagg940\OneDrive - PNNL\qsc\d\mwe_AssertOperationsEqualInPlace\Program.qs:line 10
   at Microsoft.Quantum.Simulation.Core.Operation`2.Apply(I a)

System information

  • OS: Windows 10

  • .NET Core Version: 6.0.405

  • IQ# Version: iqsharp: 0.26.233415
    Jupyter Core: 3.0.0.0
    .NET Runtime: .NETCoreApp,Version=v6.0

Additional context

I suspect the issue is in Simulation/TargetDefinitions/Decompositions/AssertOperationsEqualInPlace.qs, at the end of operation AssertEqualOnBasisVector. ResetAll is never performed on the qubits and I suspect that a tolerance mismatch allows the exception to be thrown.

I have verified that adding ResetAll(qubits) at the end of AssertEqualOnBasisVector seems to address the behavior. Not sure whether the absence of the added line is the bug or if some tolerance code is not behaving to spec.

Thanks for the detailed issue, @tobiashagge! There are a few points to go over in this and I will get into more detail below, but the summary is that your suggestion of this being an issue with the tolerance in AssertEqualOnBasisVector is correct. I’ll work on a PR to resolve this.

Regarding your first point in your expected behavior above:

When the theta = .1, an exception, but one that does not claim that the expected value should be "Zero".

This is actually the expected error message, and is consistent with the behavior of our other assert APIs. Most of the different assert operations are built from calls to these two operations:

operation AssertQubit (expected : Result, q : Qubit) : Unit is Adj + Ctl {
AssertMeasurement([PauliZ], [q], expected, $"Qubit in invalid state. Expecting: {expected}");
}

And
operation AssertQubitWithinTolerance(expected : Result, q : Qubit, tolerance : Double) : Unit is Adj + Ctl {
AssertMeasurementProbability([PauliZ], [q], expected, 1.0, $"Qubit in invalid state. Expecting: {expected} with tolerance {tolerance}", tolerance);
}

The other asserts use combinations of gate operations, entanglement, and calls to these two functions to enforce the assertion. Some target qubit or auxiliary qubit deviating from the expected state is the trigger for the assertion, and that’s why you see the message they use.

For your second point regarding the difference in behavior between the two angles in your example,

When theta=.001, the program should complete without throwing an exception.

Here is where the problem with tolerance comes in. Because the AssertOperationsEqualInPlace does not have a tolerance parameter and instead uses a hard-coded value, it should use the least permissive tolerance possible to adequately enforce the assertion. The value used in this case is 1e-5 which is definitely too permissive. Picking the perfect value is tricky given that floating point precision can vary by platform and standard library (see for example C numeric limits and Machine Epsilon). The underlying C++ full state simulator that checks for qubits in the Zero state on release has an epsilon of approx 2e-16 on Windows. Looking through our codebase, there are a few other places where we rely on an epsilon/tolerance, and it seems the best value to standardize behind is 1e-15 since that’s what the Q# implementation of Double conversions uses:

function Floor(value : Double) : Int {
let (truncated, remainder, isPositive) = ExtendedTruncation(value);
if AbsD(remainder) <= 1e-15 {
return truncated;
} else {
return isPositive ? truncated | truncated - 1;
}
}

Combining this with your suggestion of including a ResetAll before the qubits are released will bring the behavior of AssertOperationsEqualInPlace in line with our other assertion utilities.

On that note of other assertion utilities, if your goal is to have an efficient validation that two unitaries are equivalent for all input states, I would suggest switching to AssertOperationsEqualReferenced instead. As noted in the docs, AssertOperationsEqualInPlace requires both additional qubits and multiple calls to the provided unitaries, while AssertOperationsEqualReferenced uses Choi–Jamiołkowski isomorphism to perform the same validation with only one call to the underlying unitaries. Using the examples you posted above, AssertOperationsEqualReferenced correctly asserts on both inputs and runs faster. This is the same strategy we use in our test cases to validate equivalence of different decompositions of quantum gates, and is quite handy.

Hmm… looks like 1e-15 is a reasonable value for the full state simulator, but fails the tests for the stabilizer simulator based on the noise levels. To make these tests still pass, I’ll use the same tolerance used by the other tests:

function Accuracy () : Double {
return 1E-10;
}

That’s the value that gets passed for other test operations that call AssertQubitInStateWithinTolerance.