Skip to content
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

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from

Conversation

juanboschero
Copy link
Collaborator

  • 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

@rturrado rturrado changed the title [CQT 43] take global phases into account when verifying decompositions [CQT 43] Take global phases into account when verifying decompositions Nov 6, 2024
Copy link
Contributor

@rturrado rturrado left a 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.

@@ -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:
Copy link
Contributor

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.


return np.complex128(self.qubit_phase_map[Qubit(qubit).index])

def add_qubit_phase(self, qubit: QubitLike, phase: np.complex128) -> ArrayLike:
Copy link
Contributor

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.

Comment on lines 120 to 123
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
Copy link
Contributor

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])

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Comment on lines 63 to 64
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])))
Copy link
Contributor

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()?

Copy link
Contributor

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:
Copy link
Contributor

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.

Copy link
Contributor

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:
Copy link
Contributor

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?

Comment on lines +163 to +188
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]
Copy link
Contributor

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?

juanboschero and others added 2 commits November 8, 2024 11:25
Co-authored-by: Roberto Turrado Camblor <[email protected]>
Comment on lines +163 to +166
if qubit in self.phase_map:
return True

return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if qubit in self.phase_map:
return True
return False
return qubit in self.phase_map

Comment on lines +168 to +170
def set_phase_map(self, phase_map: NDArray[np.complex128]) -> None:
self.qubit_phase_map = phase_map

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Collaborator

@elenbaasc elenbaasc left a 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):
Copy link
Collaborator

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)?

Copy link
Contributor

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.

Comment on lines +157 to +159
def __init__(self, phase_map: ArrayLike[np.complex128]):
"""Initialize a PhaseMap object."""
self.phase_map = phase_map
Copy link
Collaborator

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...

Comment on lines +168 to +169
def set_phase_map(self, phase_map: NDArray[np.complex128]) -> None:
self.qubit_phase_map = phase_map
Copy link
Collaborator

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.

Comment on lines +60 to +61
def to_euler_form(scalar: np.complex128) -> np.complex128:
""" " Derives the Euler rotation angle from a scalar.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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:
Copy link
Collaborator

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).

@rturrado rturrado changed the title [CQT 43] Take global phases into account when verifying decompositions [CQT-43] Take global phases into account when verifying decompositions Nov 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants