Skip to content

[Decomposition] controlled decomposition with single work wire #7383

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 10 commits into
base: ctrl-qu
Choose a base branch
from
3 changes: 3 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
0: ──RX(0.00)──RY(1.57)──RX(3.14)──GlobalPhase(-1.57)─┤ <Z>
```

* 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)
Expand Down
8 changes: 7 additions & 1 deletion pennylane/decomposition/decomposition_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -283,7 +284,12 @@ def _get_controlled_decompositions(

# 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.
Expand Down
39 changes: 39 additions & 0 deletions pennylane/decomposition/symbolic_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,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)
29 changes: 29 additions & 0 deletions tests/decomposition/test_decomposition_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions tests/decomposition/test_symbolic_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -699,3 +700,32 @@ 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)

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