Repository: PennyLaneAI/pennylane
Pull request #6936 (2025-02-07 15:08:18)

Add a decomposition for multi-controlled global phases
**Context:**
Currently singly-controlled `GlobalPhase` operations are decomposed into an uncontrolled `PhaseShift` operation.
Multi-controlled `GlobalPhase` operations are decomposed into nothing, raising a warning about this being wrong.

**Description of the Change:**
This PR decomposes multi-controlled `GlobalPhase` operations into one-less-controlled `PhaseShift` operations instead.

**Benefits:**
Correct decomposition

**Possible Drawbacks:**
N/A

**Related GitHub Issues:**


[sc-84163]


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6936?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.59%. Comparing base [(`53df3dc`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/53df3dc89afea4b4c4a9a2f82534a1d85013629c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`c86487c`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/c86487cbca08524561f8e15c9445859b50dd7b2e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 2 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #6936   +/-   ##
=======================================
  Coverage   99.59%   99.59%           
=======================================
  Files         480      480           
  Lines       45496    45497    +1     
=======================================
+ Hits        45311    45312    +1     
  Misses        185      185           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6936?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index a385cbfe0..f86a46130 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -6,6 +6,9 @@
 
 <h3>Improvements 🛠</h3>
 
+* Add a decomposition for multi-controlled global phases into a one-less-controlled phase shift.
+  [(#6936)](https://github.com/PennyLaneAI/pennylane/pull/6936)
+ 
 * Add a `qml.capture.pause()` context manager for pausing program capture in an error-safe way.
   [(#6911)](https://github.com/PennyLaneAI/pennylane/pull/6911)
 
@@ -304,4 +307,5 @@ Pietropaolo Frisoni,
 Marcus Gisslén,
 Christina Lee,
 Mudit Pandey,
-Andrija Paurevic
+Andrija Paurevic,
+David Wierichs
diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py
index b3221a689..aad76ba5e 100644
--- a/pennylane/ops/op_math/controlled.py
+++ b/pennylane/ops/op_math/controlled.py
@@ -864,18 +864,19 @@ def _decompose_custom_ops(op: Controlled) -> list["operation.Operator"]:
         # has some special case handling of its own for further decomposition
         return _decompose_pauli_x_based_no_control_values(op)
 
-    if isinstance(op.base, qml.GlobalPhase) and len(op.control_wires) == 1:
-        # use Lemma 5.2 from https://arxiv.org/pdf/quant-ph/9503016
-        return [qml.PhaseShift(phi=-op.data[0], wires=op.control_wires)]
-    # A multi-wire controlled PhaseShift should be decomposed first using the decomposition
-    # of ControlledPhaseShift. This is because the decomposition of PhaseShift contains a
-    # GlobalPhase that we do not have a handling for.
-    # TODO: remove this special case when we support ControlledGlobalPhase [sc-44933]
-    if isinstance(op.base, qml.PhaseShift):
-        base_decomp = qml.ControlledPhaseShift.compute_decomposition(*op.data, op.wires[-2:])
-        return [
-            ctrl(new_op, op.control_wires[:-1], work_wires=op.work_wires) for new_op in base_decomp
-        ]
+    if isinstance(op.base, qml.GlobalPhase):
+        # A singly-controlled global phase is the same as a phase shift on the control wire
+        # (Lemma 5.2 from https://arxiv.org/pdf/quant-ph/9503016)
+        # Mathematically, this is the equation (with Id_2 being the 2-dim. identity matrix)
+        # |0><0|⊗ Id_2 + |1><1|⊗ e^{i\phi} = [|0><0| + |1><1| e^{i\phi}] ⊗ Id_2
+        phase_shift = qml.PhaseShift(phi=-op.data[0], wires=op.control_wires[-1])
+        if len(op.control_wires) == 1:
+            return [phase_shift]
+        # For N>1 control wires, we simply add N-1 control wires to the phase shift
+        # Mathematically, this is the equation (proven by inserting an identity)
+        # (Id_{2^N} - |1><1|^N)⊗ Id_2 + |1><1|^N ⊗ e^{i\phi}
+        # = (Id_{2^{N-1}} - |1><1|^{N-1}) ⊗ Id_4 + |1><1|^{N-1} ⊗ [|0><0|+|1><1|e^{i\phi}]⊗ Id_2
+        return [ctrl(phase_shift, control=op.control_wires[:-1])]
 
     # TODO: will be removed in the second part of the controlled rework [sc-37951]
     if len(op.control_wires) == 1 and hasattr(op.base, "_controlled"):
@@ -905,14 +906,6 @@ def _decompose_no_control_values(op: Controlled) -> Optional[list["operation.Ope
         return None
 
     base_decomp = op.base.decomposition()
-    if len(base_decomp) == 0 and isinstance(op.base, qml.GlobalPhase) and len(op.control_wires) > 1:
-        warnings.warn(
-            "Multi-Controlled-GlobalPhase currently decomposes to nothing, and this will likely "
-            "produce incorrect results. Consider implementing your circuit with a different set "
-            "of operations, or use a device that natively supports GlobalPhase.",
-            UserWarning,
-        )
-
     return [ctrl(newop, op.control_wires, work_wires=op.work_wires) for newop in base_decomp]
 
 


Test patch:
diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py
index d016ffc29..43c93fc28 100644
--- a/tests/ops/op_math/test_controlled.py
+++ b/tests/ops/op_math/test_controlled.py
@@ -924,6 +924,22 @@ special_par_op_decomps = [
             qml.PhaseShift(0.123 / 2, wires=1),
         ],
     ),
+    (
+        qml.GlobalPhase,
+        [0.123],
+        [1],
+        [0],
+        (lambda x, wires: qml.ctrl(qml.GlobalPhase(x, wires[-1]), control=wires[:-1])),
+        [qml.PhaseShift(-0.123, wires=0)],
+    ),
+    (
+        qml.GlobalPhase,
+        [0.123],
+        [3],
+        [0, 1, 2],
+        (lambda x, wires: qml.ctrl(qml.GlobalPhase(x, wires[-1]), control=wires[:-1])),
+        [qml.ctrl(qml.PhaseShift(-0.123, wires=2), control=[0, 1])],
+    ),
 ]
 
 custom_ctrl_op_decomps = special_non_par_op_decomps + special_par_op_decomps
@@ -1043,6 +1059,7 @@ class TestDecomposition:
         decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)()
         assert qml.math.allclose(op.matrix(), decomp_mat)
 
+    # pylint: disable=too-many-positional-arguments
     @pytest.mark.parametrize(
         "base_cls, params, base_wires, ctrl_wires, custom_ctrl_cls, expected",
         custom_ctrl_op_decomps,
@@ -1067,7 +1084,10 @@ class TestDecomposition:
         assert ctrl_op.decomposition() == expected
         assert qml.tape.QuantumScript(ctrl_op.decomposition()).circuit == expected
         assert custom_ctrl_op.decomposition() == expected
-        assert custom_ctrl_cls.compute_decomposition(*params, active_wires) == expected
+        # There is not custom ctrl class for GlobalPhase (yet), so no `compute_decomposition`
+        # to test, just the controlled decompositions logic.
+        if base_cls != qml.GlobalPhase:
+            assert custom_ctrl_cls.compute_decomposition(*params, active_wires) == expected
 
         mat = qml.matrix(ctrl_op.decomposition, wire_order=active_wires)()
         assert np.allclose(mat, custom_ctrl_op.matrix(), atol=tol, rtol=0)
@@ -1130,14 +1150,6 @@ class TestDecomposition:
         with pytest.raises(DecompositionUndefinedError):
             op.decomposition()
 
-    def test_global_phase_decomp_raises_warning(self):
-        """Test that ctrl(GlobalPhase).decomposition() raises a warning with more than one control."""
-        op = qml.ctrl(qml.GlobalPhase(1.23), control=[0, 1])
-        with pytest.warns(
-            UserWarning, match="Controlled-GlobalPhase currently decomposes to nothing"
-        ):
-            assert op.decomposition() == []
-
     def test_control_on_zero(self):
         """Test decomposition applies PauliX gates to flip any control-on-zero wires."""
 
@@ -1711,6 +1723,7 @@ class TestCtrl:
     @pytest.mark.parametrize("op, ctrl_wires, _", custom_ctrl_ops)
     def test_custom_controlled_ops_wrong_wires(self, op, ctrl_wires, _):
         """Tests custom controlled ops with wrong number of wires are handled correctly."""
+        # pylint: disable=possibly-used-before-assignment
 
         ctrl_wires = ctrl_wires + ["a", "b", "c"]
 
@@ -2008,11 +2021,8 @@ class TestTapeExpansionWithControlled:
         ]
 
         assert tape.expand(depth=2).circuit == [
-            qml.ControlledPhaseShift(np.pi / 4, wires=[3, 7]),
-            qml.Toffoli(wires=[3, 7, 0]),
-            qml.ControlledPhaseShift(-np.pi / 4, wires=[3, 0]),
-            qml.Toffoli(wires=[3, 7, 0]),
-            qml.ControlledPhaseShift(np.pi / 4, wires=[3, 0]),
+            Controlled(qml.RZ(np.pi / 2, wires=[0]), control_wires=[3, 7]),
+            Controlled(qml.GlobalPhase(-np.pi / 4, wires=[]), control_wires=[3, 7]),
         ]
 
     def test_adjoint_of_ctrl(self):
@@ -2160,7 +2170,7 @@ class TestTapeExpansionWithControlled:
     @pytest.mark.parametrize(
         "op, params, depth, expected",
         [
-            (qml.templates.QFT, [], 2, 14),
+            (qml.templates.QFT, [], 2, 11),
             (qml.templates.BasicEntanglerLayers, [pnp.ones([3, 2])], 1, 9),
         ],
     )