-
Notifications
You must be signed in to change notification settings - Fork 4
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
[CQT-43] Take global phases into account when verifying decompositions #368
base: develop
Are you sure you want to change the base?
[CQT-43] Take global phases into account when verifying decompositions #368
Conversation
juanboschero
commented
Oct 28, 2024
- Changed decomposer object logic
- Phase of qubits are now tracked by circuit object
- Tests added to verify and correct global phase leak into relative phase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
I would mainly have a look at the check_gate_replacements function.
I would also try to:
- Encapsulate the phase map in a class.
- Use more functions.
- Make the code more readable.
opensquirrel/circuit.py
Outdated
@@ -101,6 +106,22 @@ def qubit_register_name(self) -> str: | |||
def bit_register_name(self) -> str: | |||
return self.register_manager.get_bit_register_name() | |||
|
|||
def _set_phase_map(self, phase_map: NDArray[np.complex128]) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have a PhaseMap class?
The description of the class should explain what are the keys and values of that map.
The implementation of that class should hide that we are internally using an NDArray.
opensquirrel/circuit.py
Outdated
|
||
return np.complex128(self.qubit_phase_map[Qubit(qubit).index]) | ||
|
||
def add_qubit_phase(self, qubit: QubitLike, phase: np.complex128) -> ArrayLike: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this function returning something? It should just update a qubit phase.
opensquirrel/circuit.py
Outdated
temp_phase_map = self.qubit_phase_map | ||
temp_phase_map[Qubit(qubit).index] += phase | ||
self.qubit_phase_map = temp_phase_map | ||
return temp_phase_map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't need this temporary. A user can update a qubit phase and then get a qubit phase.
|
||
phase_difference = matrix_a[first_non_zero] / matrix_b[first_non_zero] | ||
return np.complex128(matrix_a[first_non_zero] / matrix_b[first_non_zero]) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
if len(gate_qubit_indices) > 1: | ||
relative_phase = float(np.real(qc.get_qubit_phase(qubit_list[1]) - qc.get_qubit_phase(qubit_list[0]))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code should be more readable. For example:
if (circuit_has_two_qubits):
relative_phase = get_relative_phase(qc)
All that float(blah - blah)
deserves a function on its own. Why not calling qc.get_relative_phase()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the circuit has 3 or more qubits?
|
||
def decompose(ir: IR, decomposer: Decomposer) -> None: | ||
def decompose(circuit: Circuit, decomposer: Decomposer) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If Circuit cannot be None here, it shouldn't be None at check_gate_replacement either. That would make Circuit the first parameter at check_gate_replacement also. Anyway, I think the global phase work shouldn't be part of check_gate_replacement. Once we have the replacement_gates, line 86, we could go further and check_global_phase_correction or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moreover, why not delaying the check_global_phase_correction out of the decomposition? Is this because that Rz instruction that can be added at the end may need to be decomposed in some cases?
@@ -115,6 +115,81 @@ def test_Spin2_backend() -> None: | |||
) | |||
|
|||
|
|||
def test_integration_global_phase() -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a comment somewhere in the code saying where a global phase is expected to be introduced?
qubit[3] q | ||
|
||
Rz(1.5707963) q[1] | ||
X90 q[1] | ||
Rz(1.5707963) q[1] | ||
Rz(3.1415927) q[0] | ||
X90 q[0] | ||
Rz(0.0081036221) q[0] | ||
X90 q[0] | ||
Rz(3.1415927) q[0] | ||
CZ q[1], q[0] | ||
X90 q[2] | ||
Rz(1.3907963) q[2] | ||
X90 q[2] | ||
X90 q[0] | ||
Rz(0.051592695) q[0] | ||
X90 q[0] | ||
Rz(3.1415927) q[0] | ||
CZ q[2], q[0] | ||
Rz(1.5707963) q[0] | ||
X90 q[0] | ||
Rz(1.5707963) q[0] | ||
Rz(3.1415927) q[1] | ||
X90 q[1] | ||
Rz(2.8235927) q[1] | ||
X90 q[1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just out of curiosity, what instructions, if any, do the global phase correction here?
Co-authored-by: Roberto Turrado Camblor <[email protected]>
if qubit in self.phase_map: | ||
return True | ||
|
||
return False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if qubit in self.phase_map: | |
return True | |
return False | |
return qubit in self.phase_map |
def set_phase_map(self, phase_map: NDArray[np.complex128]) -> None: | ||
self.qubit_phase_map = phase_map | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def set_phase_map(self, phase_map: NDArray[np.complex128]) -> None: | |
self.qubit_phase_map = phase_map |
I think this is a copy construction, and should be taken care of in the/a constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still needs some looking into.
I think the phase map may be useful (necessary), but I'm not convinced yet.
|
||
|
||
@dataclass(init=False) | ||
class PhaseMap(Circuit): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite an odd construction: a dataclass that subclasses from a normal class. Also, why would a PhaseMap have a 'is a' relationship with a Circuit, i.e., it is not clear why a PhaseMap would subclass from a Circuit (just like it is not clear why a Parser 'is a' InstructionLibrary)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not that it's not clear. It's wrong. It's a wrong way to get access to information from the base class. But inheritance should only be used when there is an is a relationship with the base class.
A PhaseMap should be an independent data structure, managing some data, e.g., a map of phases, encapsulating the definition of that map of phases (today a dictionary, tomorrow whatever), and providing a given API to access, query, etc. that data.
If PhaseMap turns out to be very simple, e.g., just a map over which we only do set and get, we can decide not to use it. But at the very first moment that it is more complex than that, you won't want to be writing complicating code all around the code base to access a map of phases. In that case, we'd better have a PhaseMap.
def __init__(self, phase_map: ArrayLike[np.complex128]): | ||
"""Initialize a PhaseMap object.""" | ||
self.phase_map = phase_map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems unnecessary for a 'dataclass', normally if the properties of the dataclass need to be preprocessed, one would use the __post_init__
method. But since here the property is not processed at all, I'm struggling to see why an __init__
is needed at all...
def set_phase_map(self, phase_map: NDArray[np.complex128]) -> None: | ||
self.qubit_phase_map = phase_map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setter and getter functionality is sort of built-in in dataclasses, that's there purpose. So this is either not needed, or this should be a normal class, where the phase_map
is a property.
def to_euler_form(scalar: np.complex128) -> np.complex128: | ||
""" " Derives the Euler rotation angle from a scalar. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def to_euler_form(scalar: np.complex128) -> np.complex128: | |
""" " Derives the Euler rotation angle from a scalar. | |
def get_phase_angle(scalar: np.complex128) -> np.complex128: | |
"""Derives the Euler phase angle from a scalar. |
@@ -32,25 +55,37 @@ def check_gate_replacement(gate: Gate, replacement_gates: Iterable[Gate]) -> Non | |||
msg = f"replacement for gate {gate.name} does not preserve the quantum state" | |||
raise ValueError(msg) | |||
|
|||
if qc is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess qc
should be circuit
, but then the methods that are called on circuit
are the methods of the PhaseMap
class, i.e., not of the Circuit class.
I'm still confused what the code block (of this if-statement) actually does. I get the idea, but I feel that this might actually introduce relative phases, instead of mitigating them...
Can you better explain in the Confluence page what is being implemented and how that prevents a global phase difference in the gates (between original and replacement), from resulting in a relative phase between the qubits, because that bit is still unclear to me. To put it another way: How can a global phase difference in a gate replacement, lead to a relative phase difference. As far as I see it, this can only happen if a replaced gate (that differs by a global phase from the original) is made into a controlled gate, but this doesn't happen anywhere in the compilation process (yet).