From c47cf0eefadb51f942df437164efe1effb4c7147 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Mon, 5 May 2025 11:33:26 -0400 Subject: [PATCH 1/3] [Decomposition] controlled decomposition with single work wire --- .../decomposition/decomposition_graph.py | 8 +++- .../decomposition/symbolic_decomposition.py | 39 +++++++++++++++++++ .../decomposition/test_decomposition_graph.py | 29 ++++++++++++++ .../test_symbolic_decomposition.py | 16 ++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index aed3fd9a5af..1a2e6ffccac 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -38,6 +38,7 @@ from .symbolic_decomposition import ( adjoint_rotation, cancel_adjoint, + controlled_decomp_with_work_wire, decompose_to_base, flip_control_adjoint, flip_pow_adjoint, @@ -281,7 +282,12 @@ def _get_controlled_decompositions(self, op: CompressedResourceOp) -> list[Decom # General case: apply control to the base op's decomposition rules. base = resource_rep(base_class, **base_params) - return [make_controlled_decomp(decomp) for decomp in self._get_decompositions(base)] + decomps = [make_controlled_decomp(decomp) for decomp in self._get_decompositions(base)] + + # There's always Lemma 7.11 from https://arxiv.org/abs/quant-ph/9503016. + decomps.append(controlled_decomp_with_work_wire) + + return decomps def solve(self, lazy=True): """Solves the graph using the Dijkstra search algorithm. diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index cc10aa91122..23f2971d46d 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -315,3 +315,42 @@ def flip_control_adjoint(*params, wires, control_wires, control_values, work_wir work_wires=work_wires, ) ) + + +def _controlled_decomp_with_work_wire_resource( + base_class, base_params, num_control_wires, num_work_wires, **__ +): + if num_work_wires < 1: + raise DecompositionNotApplicable + if num_control_wires < 2: + # This Lemma isn't helpful for the single control wire case + raise DecompositionNotApplicable + return { + _controlled_resource_rep(resource_rep(qml.X), num_control_wires, num_work_wires - 1): 2, + _controlled_resource_rep(resource_rep(base_class, **base_params), 1, 0): 1, + } + + +# pylint: disable=protected-access,unused-argument +@register_resources(_controlled_decomp_with_work_wire_resource) +def _controlled_decomp_with_work_wire( + *params, wires, control_wires, control_values, work_wires, base, **__ +): + """Implements Lemma 7.11 from https://arxiv.org/abs/quant-ph/9503016.""" + base_op = base._unflatten(*base._flatten()) + qml.ctrl( + qml.X(work_wires[0]), + control=wires[: len(control_wires)], + control_values=control_values, + work_wires=work_wires[1:], + ) + qml.ctrl(base_op, control=work_wires[0]) + qml.ctrl( + qml.X(work_wires[0]), + control=wires[: len(control_wires)], + control_values=control_values, + work_wires=work_wires[1:], + ) + + +controlled_decomp_with_work_wire = flip_zero_control(_controlled_decomp_with_work_wire) diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 673ed28ae25..4e691774d64 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -448,6 +448,35 @@ def test_flip_controlled_adjoint(self, _): graph.decomposition(op)(*op.parameters, wires=op.wires, **op.hyperparameters) assert q.queue == [qml.adjoint(qml.ops.Controlled(qml.U1(0.5, wires=0), control_wires=[1]))] + def test_decompose_with_single_work_wire(self, _): + """Tests that the Lemma 7.11 decomposition is applied correctly.""" + + op = qml.ctrl(qml.Rot(0.123, 0.234, 0.345, wires=0), control=[1, 2, 3], work_wires=[4, 5]) + + graph = DecompositionGraph( + operations=[op], + gate_set={"MultiControlledX", "CRot"}, + ) + graph.solve() + with qml.queuing.AnnotatedQueue() as q: + graph.decomposition(op)(*op.parameters, wires=op.wires, **op.hyperparameters) + assert q.queue == [ + qml.MultiControlledX(wires=[1, 2, 3, 4], work_wires=[5]), + qml.CRot(0.123, 0.234, 0.345, wires=[4, 0]), + qml.MultiControlledX(wires=[1, 2, 3, 4], work_wires=[5]), + ] + assert graph.resource_estimate(op) == to_resources( + { + resource_rep( + qml.MultiControlledX, + num_control_wires=3, + num_zero_control_values=0, + num_work_wires=1, + ): 2, + qml.CRot: 1, + } + ) + @patch( "pennylane.decomposition.decomposition_graph.list_decomps", diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 8583d0e2b56..7758918143d 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -28,6 +28,7 @@ from pennylane.decomposition.symbolic_decomposition import ( adjoint_rotation, cancel_adjoint, + controlled_decomp_with_work_wire, controlled_resource_rep, flip_control_adjoint, flip_pow_adjoint, @@ -699,3 +700,18 @@ def test_flip_control_adjoint(self): ): 1 } ) + + @pytest.mark.unit + def test_controlled_decomp_with_work_wire(self): + """Tests the controlled decomposition with a single work wire (Lemma 7.11).""" + + U = qml.Rot.compute_matrix(0.123, 0.234, 0.345) + op = qml.ctrl(qml.QubitUnitary(U, wires=0), control=[1, 2], work_wires=[3]) + + with queuing.AnnotatedQueue() as q: + qml.Projector([0], wires=3) + controlled_decomp_with_work_wire(*op.parameters, wires=op.wires, **op.hyperparameters) + + mat = qml.matrix(qml.tape.QuantumScript.from_queue(q), wire_order=[0, 1, 2, 3]) + expected_mat = qml.matrix(op @ qml.Projector([0], wires=3), wire_order=[0, 1, 2, 3]) + assert qml.math.allclose(mat, expected_mat) From 4f69e5f069b24997a3908020f25df43747b72522 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Mon, 5 May 2025 13:15:53 -0400 Subject: [PATCH 2/3] changelog --- doc/releases/changelog-dev.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 1aca08fd997..19d54fd0d59 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -68,6 +68,9 @@ 0: ──RX(0.00)──RY(1.57)──RX(3.14)──GlobalPhase(-1.57)─┤ ``` +* A new decomposition rule that uses a single work wire for decomposing multi-controlled operators is added. + [(#7383)](https://github.com/PennyLaneAI/pennylane/pull/7383) + * Decomposition rules can be marked as not-applicable with :class:`~.decomposition.DecompositionNotApplicable`, allowing for flexibility when creating conditional decomposition rules based on parameters that affects the rule's resources. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) From 87ab8e2657b2b538e2eb81b4ee16248b72ddcba7 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 6 May 2025 10:26:28 -0400 Subject: [PATCH 3/3] missing coverage --- tests/decomposition/test_symbolic_decomposition.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 7758918143d..dfe1ffa34bf 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -715,3 +715,17 @@ def test_controlled_decomp_with_work_wire(self): mat = qml.matrix(qml.tape.QuantumScript.from_queue(q), wire_order=[0, 1, 2, 3]) expected_mat = qml.matrix(op @ qml.Projector([0], wires=3), wire_order=[0, 1, 2, 3]) assert qml.math.allclose(mat, expected_mat) + + @pytest.mark.unit + def test_controlled_decomp_with_work_wire_not_applicable(self): + """Tests that the controlled_decomp_with_work_wire is not applicable sometimes.""" + + op = qml.ctrl(qml.RX(0.5, wires=0), control=[1], control_values=[0], work_wires=[3]) + with pytest.raises(DecompositionNotApplicable): + # single control wire + controlled_decomp_with_work_wire.compute_resources(**op.resource_params) + + op = qml.ctrl(qml.RX(0.5, wires=0), control=[1, 2]) + with pytest.raises(DecompositionNotApplicable): + # no work wire available + controlled_decomp_with_work_wire.compute_resources(**op.resource_params)