Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -2056,6 +2056,10 @@

<h3>Bug fixes 🐛</h3>

* :func:`~.ctrl` of a quantum function now forwards ``work_wire_type`` to each inner operator,
both with and without program capture.
[(#10355)](https://github.com/PennyLaneAI/pennylane/pull/10355)

* `qp.cond` can now handle `Operator2` instances being provided instead of branch functions.
While this syntax was supported with tape-based PennyLane, it had never been
properly supported with program capture and Catalyst.
Expand Down
12 changes: 11 additions & 1 deletion pennylane/capture/base_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,16 @@ def handle_adjoint_transform(self, *invals, jaxpr, lazy, n_consts):

# pylint: disable=too-many-arguments
@PlxprInterpreter.register_primitive(ctrl_transform_prim)
def handle_ctrl_transform(self, *invals, n_control, jaxpr, control_values, work_wires, n_consts):
def handle_ctrl_transform(
self,
*invals,
n_control,
jaxpr,
control_values,
work_wires,
n_consts,
work_wire_type="borrowed",
):
"""Interpret a ctrl transform primitive."""
consts = invals[:n_consts]
args = invals[n_consts:-n_control]
Expand All @@ -501,6 +510,7 @@ def handle_ctrl_transform(self, *invals, n_control, jaxpr, control_values, work_
jaxpr=jaxpr.jaxpr,
control_values=control_values,
work_wires=work_wires,
work_wire_type=work_wire_type,
n_consts=len(jaxpr.consts),
)

Expand Down
49 changes: 39 additions & 10 deletions pennylane/ops/op_math/controlled.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,16 +369,23 @@ def create_controlled_op(op, control, control_values, work_wires, work_wire_type
"This error might occur if you apply ctrl to a list "
"of operations instead of a function or Operator."
)
return _ctrl_transform(op, control, control_values, work_wires, one_controlled)
return _ctrl_transform(op, control, control_values, work_wires, work_wire_type, one_controlled)


def _ctrl_transform(op, control, control_values, work_wires, one_controlled):
# pylint: disable=too-many-arguments
def _ctrl_transform(op, control, control_values, work_wires, work_wire_type, one_controlled):

@wraps(op)
def wrapper(*args, **kwargs):

if qp.capture.enabled():
return _capture_ctrl_transform(op, control, control_values, work_wires)(*args, **kwargs)
return _capture_ctrl_transform(
op,
control,
control_values,
work_wires,
work_wire_type,
)(*args, **kwargs)

qscript = qp.tape.make_qscript(op)(*args, **kwargs)

Expand All @@ -394,7 +401,13 @@ def wrapper(*args, **kwargs):

_ = [
(
ctrl(op, control=control, control_values=op_control_values, work_wires=work_wires)
ctrl(
op,
control=control,
control_values=op_control_values,
work_wires=work_wires,
work_wire_type=work_wire_type,
)
if not isinstance(op, (Allocate, Deallocate))
else qp.apply(op)
)
Expand All @@ -417,11 +430,19 @@ def wrapper(*args, **kwargs):
ctrl_transform_prim.prim_type = "higher_order"


# pylint: disable=too-many-arguments
@ctrl_transform_prim.def_impl
def _ctrl_transform_impl(*args, n_control, jaxpr, control_values, work_wires, n_consts):
from pennylane.tape.plxpr_conversion import ( # pylint: disable=import-outside-toplevel
CollectOpsandMeas,
)
def _ctrl_transform_impl(
*args,
n_control,
jaxpr,
control_values,
work_wires,
n_consts,
work_wire_type="borrowed",
):
# pylint: disable=import-outside-toplevel
from pennylane.tape.plxpr_conversion import CollectOpsandMeas

consts = args[:n_consts]
control_wires = args[-n_control:]
Expand All @@ -432,7 +453,8 @@ def _ctrl_transform_impl(*args, n_control, jaxpr, control_values, work_wires, n_
collector.eval(jaxpr, consts, *args)

for op in collector.state["ops"]:
ctrl(op, control_wires, control_values, work_wires)
ctrl(op, control_wires, control_values, work_wires, work_wire_type=work_wire_type)

return []


Expand All @@ -441,7 +463,13 @@ def _ctrl_transform_abstract_eval(*_, **__):
return []


def _capture_ctrl_transform(qfunc: Callable, control, control_values, work_wires) -> Callable:
def _capture_ctrl_transform(
qfunc: Callable,
control,
control_values,
work_wires,
work_wire_type="borrowed",
) -> Callable:
"""Capture compatible way of performing an ctrl transform."""
# note that this logic is tested in `tests/capture/test_nested_plxpr.py`

Expand All @@ -462,6 +490,7 @@ def new_qfunc(*args, **kwargs):
n_control=len(control_wires),
control_values=control_values,
work_wires=qp.wires.Wires(work_wires) if work_wires is not None else work_wires,
work_wire_type=work_wire_type,
n_consts=len(jaxpr.consts),
)

Expand Down
18 changes: 18 additions & 0 deletions tests/capture/test_nested_plxpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,24 @@ def f(w):

assert plxpr.eqns[0].params["work_wires"] == qp.wires.Wires(["aux"])

@pytest.mark.parametrize("work_wire_type", ["zeroed", "borrowed"])
def test_work_wire_type(self, work_wire_type):
"""Test that work_wire_type is forwarded to each inner operator."""

def f(w):
return qp.ctrl(qp.S, (1, 2), work_wires="aux", work_wire_type=work_wire_type)(w)

plxpr = jax.make_jaxpr(f)(6)

with qp.queuing.AnnotatedQueue() as q:
out = jax.core.eval_jaxpr(plxpr.jaxpr, plxpr.consts, 5)

assert out == []
expected = qp.ctrl(qp.S(5), (1, 2), work_wires="aux", work_wire_type=work_wire_type)
qp.assert_equal(q.queue[0], expected)
assert q.queue[0].work_wire_type == work_wire_type
assert plxpr.eqns[0].params["work_wire_type"] == work_wire_type

def test_control_values(self):
"""Test that control values can be provided."""

Expand Down
16 changes: 16 additions & 0 deletions tests/ops/op_math/test_controlled.py
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,22 @@ def test_nested_pauli_x_based_ctrl_ops(self):
expected = qp.MultiControlledX(wires=[3, 2, 1, 0], control_values=[1, 0, 1])
assert op == expected

@pytest.mark.parametrize("work_wire_type", ["zeroed", "borrowed"])
def test_qfunc_ctrl_forwards_work_wire_type(self, work_wire_type):
"""``qp.ctrl`` of a qfunc must pass ``work_wire_type`` to each inner operator."""

def f():
qp.X(0)
qp.X(1)

with qp.queuing.AnnotatedQueue() as q:
qp.ctrl(f, control=[2, 3], work_wires=[4], work_wire_type=work_wire_type)()

assert len(q.queue) == 2
for op in q.queue:
assert op.work_wire_type == work_wire_type
assert op.work_wires == Wires([4])

def test_correct_queued_operators(self):
"""Test that args and kwargs do not add operators to the queue."""

Expand Down
1 change: 1 addition & 0 deletions tests/resource/test_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def circuit():
),
)

@pytest.mark.xfail(reason="xfailed until work_wire_spec is added in Catalyst.")
def test_complex(self):
"""Test a complex case of qp.specs() against PennyLane"""

Expand Down
Loading