New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mitigating confusing global phase #1317
Comments
I think the subtle difference (which was present in the Legacy QDK as well when using sparse simulation) was that phase does not get cleared when using |
Mathematically, this is correct behavior. In most of the scenarios I'm concerned about (and in most questions I got on this topic), the global phase shows up without any measurements being involved. (And the BellState.qs example would be better off with qubits deallocated after each state and allocated from scratch, since we're not really reusing them.) |
Thanks for digging into this. There is definitely a lot going on, and it's going to be tricky finding the right balance between "mathematically correct but confusing" on the one hand, and "more predictable but less rigorous" on the other. I tried finding a minimal repro of sorts to show the limitations of how simulation treats global phase, and I think I have a good one to foster continued discussion. You make a good point that focusing on reset and making sure it actually puts the qubit back to the |0⟩ state, so I've starting thinking how we might do that consistently. But even when there is no reset or measurement we can still run into problems. Here's a sample program: namespace MyQuantumProgram {
open Microsoft.Quantum.Diagnostics;
@EntryPoint()
operation Main() : Unit {
{
use outer = Qubit();
Message("outer before");
DumpMachine();
{
use inner = Qubit();
Message("outer + inner");
DumpMachine();
X(inner);
Z(inner);
X(inner);
Message("outer + inner_phase");
DumpMachine();
}
Message("outer after");
DumpMachine();
}
Message("all done");
DumpMachine();
}
} When you run that, you'll see an example of odd phase behavior that seems mathematically incorrect: The qubit that had Z applied gets a phase as expected, but when it goes out of scope the phase "jumps" to the other qubit, even without any entanglement, superposition, measurement or reset in the picture. That's because the simulator just tracks the "0" state (with some padding for number of qubits) but doesn't know which parts of the phase in that state are associated with which qubits. At least, the update to the sparse simulator in qir-alliance/qir-runner#66 causes it clean up this phase once all qubits go out of scope. Compare this to the classic QDK which had the same quirk but without the better result on the "all done" state (note: I had to use the older
Even with the old full state simulator, the phase still "jumps" and worse persists even across all qubits being released. So what would be the desirable behavior for a sample like this? Even if the qubit was explicitly reset before going out of scope, the sparse state simulator will only know that qubit 1 is being reset/released and the current state vector is a single entry with index 0 and amplitude -1.0. How can it know which qubit to associate that negative phase to and when to drop it? Is it possible we need some record of when phase gets applied and which qubit it is applied to? |
In this particular example I'm more bothered by DumpMachine outputting |0> when there are no qubits allocated... I don't think we can say the phase is applied to a specific qubit - for example, we can apply a global phase -1 to a |11> state using Controlled Z to find ourselves a phase associated with two qubits, and multi-controlled Z to apply it to as many |1> qubits as we want. In this scenario, unfortunately mathematically it makes sense to have the global phase stick around. What I'd really like to do is to track down the root causes of phases appearing in the examples I gave, and figure out how to make those behave. I don't think I ever noticed running into issues with a global phase persisting when measuring or deallocating qubits - our BellState example is the first time this happened, and the second example is the kind of code I've never written. But the issues that occur without measurements/deallocation look very confusing.
|
Is it that R(PauliI) is ignored unless it's controlled? |
I like this list of tasks. Would you be ok with updating the description to add those tasks for tracking and update the title to something like, "Mitigating confusing global phase" instead? Then we can take them one at a time or promote them into sub-issues if appropriate. I'm happy to do the edits if you prefer (I'll preserve the original description at the bottom). To that list, I'd add one other task:
I think this would help with the authoring of Python test cases, and it can explain in that it's not mathematical equivalence but state equivalence that ignores the global phase.
Yes, qsharp/library/std/intrinsic.qs Lines 397 to 408 in d88711e
And that utility ApplyGlobalPhase only operates on the control qubits and is no-op otherwise:qsharp/library/std/internal.qs Lines 30 to 41 in d88711e
|
Sure, go ahead with the edits and/or splitting into separate subtasks. I think we need to change that behavior to have R(PauliI) act even if it's not controlled. It's super confusing when talking about phase oracles to have different oracles with the same matrix when they should really have different matrices (and to have different ways to get the same global phase yield different results), I got so many questions about that. For the state equivalence check, are you thinking about comparing two dictionaries (as used by our StateDump)? That would not help my scenario, since in it my argument is a plain list of amplitudes... Maybe one more util to convert list of amplitudes into a StateDump type? Could put in into qsharp_utils, same as dump_operation |
I was actually thinking something like this (easier to code it up than try to describe it): qsharp/pip/tests/test_qsharp.py Lines 101 to 112 in dd6b894
It takes a dictionary and performs the check by ensuring the states are equivalent by ensuring the same non-zero indices are present and share the same phase. |
The next issue I notice is with R1 gate: it's supposed to introduce global phase only for |1⟩, but a global phase creeps in.
is supposed to convert the state to |-⟩|0⟩ (R1(PI()) is effectively the Z gate), but there's a relative phase involving i there. This looks like a side effect of ignoring R(PauliI), if we decompose R1 into R(PauliZ) followed by R(PauliI), but the controlled version of R1 also introduces a weird global phase:
|
I think you are on to something critical with the difference in behavior vs expectation for R(PauliI). If I look back at the old qsharp-runtime repo, I can see the C++ full-state simulator would add a global phase via the use q = Qubit();
R(PauliI, PI(), q);
DumpMachine();
Reset(q); The classic full-state simulator applies a global phase of -𝑖:
while both classic and modern sparse-state simulator resulted in a no-op:
This use of Between the decompositions for hardware and the explicit behavior of the sparse-state simulator, it seems like someone made an intentional choice to change the global phase behavior of R(PauliI), but I don't think we have enough information to understand why, which makes it hard to justify relative to the confusion that approach to global phase causes. |
I'm still not sure what happens with the Controlled R1, since controlled variants of R1 gate should not introduce a global phase... I think we need to dig deeper into this intentional choice - I'm not convinced that there is any benefit in this behavior, and there is definitely a lot of confusion introduced by it. If I cannot even use Controlled variants of a gate to get rid of the global phase, I'm not sure I trust the decompositions to be accurate, so I won't get to running the code on hardware. |
Ok, I think I may finally understand where the oddities of phase are coming from. It seems that across the three simulators we used (classic full-state, classic sparse-state, and modern sparse-state), we made slightly different shortcuts for execution that create different behavior. Each of these different behaviors could be defended under the banner of "states that differ only by a global phase represent the same physical system," but they do not help with educating on the mathematical behaviors of the gates. I'm going to try to put it all together in this comment, which will get long... First, the interplay between Rz, Ri, and R1. In the classic full-state simulator, these were each distinct operations, with Ri applying a global phase (aka the G or Ph gate). You can see this by checking the behavior of each gate when applied to the |+⟩ state with a parameter of
For both the classic sparse-state and modern sparse-state simulator, the code treats Ri as a no-op, essentially dropping the phase of exp(i * theta). Since (Rz(theta))(Ri(theta / 2)) = R1(theta), this means each simulator treating Ri as a no-op must decide how to treat the resulting equality of Rz(theta) = R1(theta). Confusingly, they each do it in different ways. The classic sparse-state simulator does this by implementing R1 as a phase shift and having Rz just perform an R1. Checking that by having the classic sparse-state operate on |+⟩ with a parameter of
But the modern sparse-state simulator (plus the stdlib decompositions in the modern QDK) goes the other way, and only defines Rz, letting R1 just be an application of Rz. So taking the same application to the |+⟩ state with a parameter of
Arguably, the classic full-state simulator is mathematically correct, as each gate has distinct behavior that corresponds to the matrix traditionally used to represent that gate. Both the sparse-state simulators cheat by having Ri = I, losing expected phase, and the classic sparse-state uses R1 for Rz (so Rz is missing the expected phase) while modern sparse-state use Rz for R1 (making R1 have an unexpected phase). To better match expectation, it seems like the right fix is what I'd worried it would be: introduce a global phase intrinsic that is used in simulation and treated as a no-op when compiling for hardware. As an added bonus quirk of global phase, we expect Rx(PI()) = -iX and Ry(PI()) = -iY. This is true for the classic full-state simulator, where again operation on |+⟩ shows the expected phase difference:
But for both classic and modern sparse-state simulation, the Rx/Ry gates have an optimization that detects when the rotation is effectively a Pauli rotation and hands off to the corresponding X or Y implementation, causing rotations by
I think this can also be solved via application of the global phase gate, where the shortcut linked above applies the phase of -i manually after the application of X or Y. Given all of that, I would propose two work items:
Does this sound reasonable? |
Sounds convincing :-) We'll need to include the examples I had in this thread in the tests to make sure they are processed correctly, and I'll report back if I find any additional odd cases |
In the teleportation sample (#1456) global phase shows up on the target qubit for teleporting |->, but bringing qubit allocation within the loop doesn't really help, since it occurs within one iteration as a result of teleportation measurement. |
Yup, I think we found another bug (or at the very least, confusing behavior) specifically for |
Turns out there was one more issue in the simulator that affected rotations by 2 Pi. I put up a fix for this in the simulator which we'll need to pull into qsharp by updating the tag. @tcNickolas thanks for pointing this out! |
Discussion landed at a few concrete tasks to help with mitigating how confusing global phase in simulation can be:
R
withPauliI
to make application of phase more consistent #1450PauliI
rotation andDumpRegister
#1461)check_eq
forStateDump
in Python #1372)DumpRegister
does not add phase on display (Fix global phase forPauliI
rotation andDumpRegister
#1461)Describe the bug
QDK sparse simulator introduces a global phase at random that shows up in
DumpMachine
output (and the outputs derived from it, such asdump_operation
).This is very confusing for several ways:
assert state_vector == pytest.approx(a)
to compare the state vectors, in Q# I have to first detect the global phase difference between the expected state and the actual state and then as I loop over the elements to remember to include it in every element comparison.To Reproduce
Several examples:
Expected behavior
Simulator not introducing a random global phase in vast majority of scenarios (for example, when there are only real-valued gates involved).
The text was updated successfully, but these errors were encountered: