Skip to content

Simplify decomposition of controlled eigengates with global phase #7291

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

codrut3
Copy link
Contributor

@codrut3 codrut3 commented Apr 21, 2025

Fixes #7238 following the recommendations in the issue description: I added a _decompose_ method to EigenGate that extracts global phase, and updated ControlledGate._decompose_with_context_ to try to extract the global phase first.

This PR may be undesirable because gates that fix global_shift need to implement their own _decompose_ method. I did this for Rx, Ry and Rz, but I don't have a nice solution for a not-yet implemented gate that derives from EigenGate and fixes global_shift: any such gate must have a _decompose_ method. See the discussion in #7238.

While implementing this change I found two issues:

  • On line 650 in cirq-core/cirq/ops/raw_types_test.py, the check should be assert [*tagged_h._decompose_()] == cirq.decompose_once(h). tagged_h._decompose_ calls decompose_once, so it must be compared with cirq.decompose_once, not with cirq.decompose. The difference became visible when a second step in the decomposition extracted the global phase.
  • merge_single_qubit_moments_to_phxz doesn't handle well global phase. A global phase gate gets assigned to the first moment, and in the current implementation this makes it unmergeable with the next moment. I found this while examining a test failure. I fixed this, and created a test case to show the problem.

It may make sense to address these issues in separate PRs, independent of this one. Let me know!

Copy link

codecov bot commented Apr 21, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.66%. Comparing base (65a4105) to head (1296df5).
Report is 27 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7291      +/-   ##
==========================================
+ Coverage   98.64%   98.66%   +0.01%     
==========================================
  Files        1106     1106              
  Lines       95985    96144     +159     
==========================================
+ Hits        94688    94864     +176     
+ Misses       1297     1280      -17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@@ -159,6 +160,12 @@ def _decompose_with_context_(
self, qubits: Tuple['cirq.Qid', ...], context: Optional['cirq.DecompositionContext'] = None
) -> Union[None, NotImplementedType, 'cirq.OP_TREE']:
control_qubits = list(qubits[: self.num_controls()])
# If the subgate is an EigenGate with non-zero phase, try to decompose it
# into a phase-free gate and a global phase gate.
if isinstance(self.sub_gate, eigen_gate.EigenGate) and self.sub_gate.global_shift != 0:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there any way you can get rid of this condition? Like try decomposing the subgate first, then only enter the branch if the subgate has decomposed? A big goal is to reduce the amount of type checking needed here.

If this works, then the duplicate function call at the end of this function can be removed, and the function can just return NotImplemented if it gets to that point.

# their own _decompose_ method.
self_without_phase._global_shift = 0
global_phase = 1j ** (2 * self.global_shift * self.exponent)
return [self_without_phase.on(*qubits), global_phase_op.GlobalPhaseGate(global_phase)()]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe check that the remaining phase isn't zero, and drop it if it is. This could be the case if shift==0.5 and exponent==4 for instance. The decomposition would factor out the shift, leaving, say, X**4 and an identity phase gate that there's no reason to keep.

@@ -473,7 +473,8 @@ def test_decompose():
op = cirq.H(q0).with_classical_controls('a')
assert cirq.decompose(op) == [
(cirq.Y(q0) ** 0.5).with_classical_controls('a'),
cirq.XPowGate(exponent=1.0, global_shift=-0.25).on(q0).with_classical_controls('a'),
cirq.X(q0).with_classical_controls('a'),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, so here's where we probably need to have an opt in flag on the decompose context object. Having the basic gates suddenly decompose to multiple gates by default probably would be too likely to break something. While generally it seems that changing details around complex decompositions have been approved, this change seems a little too fundamental.

@github-actions github-actions bot added the size: L 250< lines changed <1000 label Apr 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size: L 250< lines changed <1000
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplify decomposition of controlled eigengates with global phase
2 participants