From da51564f84482895f02aced18a8b0d976e7a30f0 Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Mon, 19 Oct 2026 16:13:21 -0400 Subject: [PATCH 1/8] Fix mixed control values in qjit capture [sc-133997] Assisted-by: Codex --- doc/releases/changelog-dev.md | 4 ++++ pennylane/ops/op_math/controlled.py | 10 ++++++++-- pennylane/ops/op_math/controlled2.py | 4 +++- tests/capture/test_nested_plxpr.py | 21 +++++++++++++++++++++ tests/ops/op_math/test_controlled2.py | 4 ++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index ec0974a605d..a38d937ab5a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -1966,6 +1966,10 @@

Bug fixes 🐛

+* :func:`~.ctrl` now supports mixed static and dynamic control values under + `qp.qjit(capture=True)`, for both operator instances and callables. + [(#XXXX)](https://github.com/PennyLaneAI/pennylane/pull/XXXX) + * `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. diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 6f3d00c3105..d26f5914d20 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -300,7 +300,7 @@ def create_controlled_op(op, control, control_values, work_wires, work_wire_type if isinstance(control_values, (int, bool)): control_values = [bool(control_values)] elif isinstance(control_values, tuple): - control_values = [bool(v) for v in control_values] + control_values = [v if math.is_abstract(v) else bool(v) for v in control_values] qp.QueuingManager.remove(op) @@ -453,14 +453,20 @@ def new_qfunc(*args, **kwargs): ) flat_args = jax.tree_util.tree_leaves(args) control_wires = qp.wires.Wires(control) # make sure is iterable + dynamic_control_values = () + if any(math.is_abstract(value) for value in control_values): + dynamic_control_values = tuple(math.cast(value, dtype=bool) for value in control_values) + + # None marks control-value operands immediately before the control wires. ctrl_transform_prim.bind( *jaxpr.consts, *abstract_shapes, *flat_args, + *dynamic_control_values, *control_wires, jaxpr=jaxpr.jaxpr, n_control=len(control_wires), - control_values=control_values, + control_values=None if dynamic_control_values else control_values, work_wires=qp.wires.Wires(work_wires) if work_wires is not None else work_wires, n_consts=len(jaxpr.consts), ) diff --git a/pennylane/ops/op_math/controlled2.py b/pennylane/ops/op_math/controlled2.py index 0ed54021cac..51ffee94a22 100644 --- a/pennylane/ops/op_math/controlled2.py +++ b/pennylane/ops/op_math/controlled2.py @@ -67,7 +67,9 @@ def _setup_control_values(control_values, num_control_wires): raise ValueError("control_values should be the same length as control_wires") if isinstance(control_values, (list, tuple)): - control_values = qp.math.asarray(control_values, like=control_values[0]) + control_values = qp.math.asarray( + control_values, like=qp.math.get_interface(*control_values) + ) if not isinstance(control_values, AbstractArray): control_values = qp.math.cast(control_values, dtype=bool) diff --git a/tests/capture/test_nested_plxpr.py b/tests/capture/test_nested_plxpr.py index b317b15e361..540b00b4d7d 100644 --- a/tests/capture/test_nested_plxpr.py +++ b/tests/capture/test_nested_plxpr.py @@ -387,6 +387,27 @@ def f(z): assert plxpr.eqns[0].params["control_values"] == (False, True) assert plxpr.eqns[0].params["n_control"] == 2 + @pytest.mark.parametrize("container, static_first", ((tuple, False), (list, True))) + def test_mixed_control_values(self, container, static_first): + """Capture supplies runtime control-value operands for qjit lowering.""" + + def circuit(x, val): + values = (True, val) if static_first else (val, True) + qp.ctrl(qp.RX, (0, 1), container(values))(x, 2) + + with jax.checking_leaks(): + plxpr = jax.make_jaxpr(circuit)(0.5, False) + + assert len(plxpr.eqns) == 1 + ctrl_eqn = plxpr.eqns[0] + assert ctrl_eqn.primitive == ctrl_transform_prim + assert ctrl_eqn.params["control_values"] is None + assert ctrl_eqn.params["n_control"] == 2 + value_args = ctrl_eqn.invars[-4:-2] + assert value_args[int(static_first)] == plxpr.jaxpr.invars[1] + assert value_args[1 - int(static_first)].val + assert [wire.val for wire in ctrl_eqn.invars[-2:]] == [0, 1] + def test_nested_control(self): """Test that control can be nested.""" diff --git a/tests/ops/op_math/test_controlled2.py b/tests/ops/op_math/test_controlled2.py index fb0ed4af768..00ae93085b6 100644 --- a/tests/ops/op_math/test_controlled2.py +++ b/tests/ops/op_math/test_controlled2.py @@ -636,6 +636,8 @@ def circ(cvals): qp.ctrl(op, control=[3, 4], control_values=[0, 1]) op2 = qp.ctrl(qp.H(0), control=[1, 2], control_values=[1, 0]) qp.ctrl(op2, control=[3, 4], control_values=cvals) + qp.ctrl(qp.H(0), control=[1, 2], control_values=[True, cvals[0]]) + qp.ctrl(qp.H(0), control=[1, 2], control_values=[cvals[0], True]) jaxpr = jax.make_jaxpr(circ)(jax.numpy.array([True, False])) tape = qp.testing.plxpr_to_tape(jaxpr.jaxpr, jaxpr.consts, jax.numpy.array([False, True])) @@ -644,6 +646,8 @@ def circ(cvals): qp.ctrl(qp.H(1), control=[2, 3, 0], control_values=[0, 1, 1]), qp.ctrl(qp.H(0), control=[3, 4, 1, 2], control_values=[0, 1, 0, 1]), qp.ctrl(qp.H(0), control=[3, 4, 1, 2], control_values=[0, 1, 1, 0]), + qp.ctrl(qp.H(0), control=[1, 2], control_values=[1, 0]), + qp.ctrl(qp.H(0), control=[1, 2], control_values=[0, 1]), ] def test_old_decomp_integration(self): From 817bf298551412afff1f335613f77ed53c9501a2 Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Mon, 19 Oct 2026 16:16:57 -0400 Subject: [PATCH 2/8] Link dynamic control changelog to #10356 [sc-133997] Assisted-by: Codex --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index a38d937ab5a..190da37489c 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -1968,7 +1968,7 @@ * :func:`~.ctrl` now supports mixed static and dynamic control values under `qp.qjit(capture=True)`, for both operator instances and callables. - [(#XXXX)](https://github.com/PennyLaneAI/pennylane/pull/XXXX) + [(#10356)](https://github.com/PennyLaneAI/pennylane/pull/10356) * `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 From dea10221c4adaa54b1f18a0a31c0f2a5dcf65458 Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Tue, 20 Oct 2026 13:08:18 -0400 Subject: [PATCH 3/8] Pass all captured control values as operands [sc-133997] Assisted-by: Codex --- doc/releases/changelog-dev.md | 5 +++++ pennylane/capture/base_interpreter.py | 8 ++++---- pennylane/ops/op_math/controlled.py | 17 +++++++---------- pennylane/tape/plxpr_conversion.py | 11 ++++++++--- tests/capture/test_base_interpreter.py | 8 +++++--- tests/capture/test_nested_plxpr.py | 23 ++++++++++++++--------- tests/tape/test_plxpr_conversion.py | 6 +++--- 7 files changed, 46 insertions(+), 32 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 5e9ba1966f4..4e3152f7a57 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -1332,6 +1332,11 @@

Breaking changes 💔

+* The `ctrl_transform` capture primitive now receives Boolean control values as operands + after the control wires. Its `control_values` parameter has been removed, so consuming + interpreters, including Catalyst, must support the new operand layout. + [(#10356)](https://github.com/PennyLaneAI/pennylane/pull/10356) + * :func:`~.transforms.decompose` is now a pass-only transform that always dispatches to Catalyst's ``graph_decomposition`` under :func:`~.qjit`. The previous tape-based implementation is available as :func:`~.transforms._tape_decompose`. diff --git a/pennylane/capture/base_interpreter.py b/pennylane/capture/base_interpreter.py index f13469eadc0..d6611dbaeea 100644 --- a/pennylane/capture/base_interpreter.py +++ b/pennylane/capture/base_interpreter.py @@ -487,19 +487,19 @@ 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, work_wires, n_consts): """Interpret a ctrl transform primitive.""" + controls_start = len(invals) - 2 * n_control consts = invals[:n_consts] - args = invals[n_consts:-n_control] + args = invals[n_consts:controls_start] jaxpr = jaxpr_to_jaxpr(copy(self), jaxpr, consts, *args) return ctrl_transform_prim.bind( *jaxpr.consts, *args, - *invals[-n_control:], + *invals[controls_start:], n_control=n_control, jaxpr=jaxpr.jaxpr, - control_values=control_values, work_wires=work_wires, n_consts=len(jaxpr.consts), ) diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index d26f5914d20..b2299d5209a 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -418,14 +418,17 @@ def wrapper(*args, **kwargs): @ctrl_transform_prim.def_impl -def _ctrl_transform_impl(*args, n_control, jaxpr, control_values, work_wires, n_consts): +def _ctrl_transform_impl(*args, n_control, jaxpr, work_wires, n_consts): from pennylane.tape.plxpr_conversion import ( # pylint: disable=import-outside-toplevel CollectOpsandMeas, ) + values_start = len(args) - n_control + controls_start = values_start - n_control consts = args[:n_consts] - control_wires = args[-n_control:] - args = args[n_consts:-n_control] + control_wires = args[controls_start:values_start] + control_values = args[values_start:] + args = args[n_consts:controls_start] collector = CollectOpsandMeas() with qp.QueuingManager.stop_recording(): @@ -453,20 +456,14 @@ def new_qfunc(*args, **kwargs): ) flat_args = jax.tree_util.tree_leaves(args) control_wires = qp.wires.Wires(control) # make sure is iterable - dynamic_control_values = () - if any(math.is_abstract(value) for value in control_values): - dynamic_control_values = tuple(math.cast(value, dtype=bool) for value in control_values) - - # None marks control-value operands immediately before the control wires. ctrl_transform_prim.bind( *jaxpr.consts, *abstract_shapes, *flat_args, - *dynamic_control_values, *control_wires, + *(math.cast(value, dtype=bool) for value in control_values), jaxpr=jaxpr.jaxpr, n_control=len(control_wires), - control_values=None if dynamic_control_values else control_values, work_wires=qp.wires.Wires(work_wires) if work_wires is not None else work_wires, n_consts=len(jaxpr.consts), ) diff --git a/pennylane/tape/plxpr_conversion.py b/pennylane/tape/plxpr_conversion.py index 6d6274256f6..021903ae6a9 100644 --- a/pennylane/tape/plxpr_conversion.py +++ b/pennylane/tape/plxpr_conversion.py @@ -131,9 +131,12 @@ def _ctrl_transform_prim(self, *invals, n_control, jaxpr, n_consts, **params): """Handle a control transform primitive by collecting the operations in the jaxpr, and then applying their controlled versions. """ + values_start = len(invals) - n_control + controls_start = values_start - n_control consts = invals[:n_consts] - args = invals[n_consts:-n_control] - control = math.array(invals[-n_control:]) + args = invals[n_consts:controls_start] + control = math.array(invals[controls_start:values_start]) + control_values = invals[values_start:] child = CollectOpsandMeas() child.eval(jaxpr, consts, *args) @@ -141,7 +144,9 @@ def _ctrl_transform_prim(self, *invals, n_control, jaxpr, n_consts, **params): for op in child.state["ops"]: with pause(): - self.state["ops"].append(ops.ctrl(op, control=control, **params)) + self.state["ops"].append( + ops.ctrl(op, control=control, control_values=control_values, **params) + ) return [] diff --git a/tests/capture/test_base_interpreter.py b/tests/capture/test_base_interpreter.py index 54c0678e841..872f38fd78d 100644 --- a/tests/capture/test_base_interpreter.py +++ b/tests/capture/test_base_interpreter.py @@ -391,15 +391,17 @@ def test_ctrl_transform(self): """Test the higher order ctrl transform.""" @SimplifyInterpreter() - def f(x, control): + def f(x, control, val): def g(y): _ = qp.RY(y, 0) ** 3 - qp.ctrl(g, control)(x) + qp.ctrl(g, control, control_values=[val])(x) - jaxpr = jax.make_jaxpr(f)(0.5, 1) + jaxpr = jax.make_jaxpr(f)(0.5, 1, False) assert jaxpr.eqns[0].primitive == ctrl_transform_prim + assert jaxpr.eqns[0].invars[-2:] == jaxpr.jaxpr.invars[1:] + assert "control_values" not in jaxpr.eqns[0].params inner_jaxpr = jaxpr.eqns[0].params["jaxpr"] # first eqn mul, second RY assert_eqn_matches_op(inner_jaxpr.eqns[1], qp.RY) diff --git a/tests/capture/test_nested_plxpr.py b/tests/capture/test_nested_plxpr.py index 540b00b4d7d..9e194b91070 100644 --- a/tests/capture/test_nested_plxpr.py +++ b/tests/capture/test_nested_plxpr.py @@ -324,7 +324,7 @@ def f(x, w): qp.assert_equal(q.queue[0], expected) assert plxpr.eqns[0].primitive == ctrl_transform_prim - assert plxpr.eqns[0].params["control_values"] == (True,) + assert plxpr.eqns[0].invars[-1].val assert plxpr.eqns[0].params["n_control"] == 1 assert plxpr.eqns[0].params["work_wires"] is None assert plxpr.eqns[0].params["n_consts"] == 0 @@ -346,7 +346,7 @@ def f(w1, w2, w3): assert len(q) == 1 assert plxpr.eqns[0].primitive == ctrl_transform_prim - assert plxpr.eqns[0].params["control_values"] == (True, True) + assert [value.val for value in plxpr.eqns[0].invars[-2:]] == [True, True] assert plxpr.eqns[0].params["n_control"] == 2 assert plxpr.eqns[0].params["work_wires"] is None @@ -384,7 +384,7 @@ def f(z): qp.assert_equal(q.queue[0], expected) assert len(q) == 1 - assert plxpr.eqns[0].params["control_values"] == (False, True) + assert [value.val for value in plxpr.eqns[0].invars[-2:]] == [False, True] assert plxpr.eqns[0].params["n_control"] == 2 @pytest.mark.parametrize("container, static_first", ((tuple, False), (list, True))) @@ -401,12 +401,17 @@ def circuit(x, val): assert len(plxpr.eqns) == 1 ctrl_eqn = plxpr.eqns[0] assert ctrl_eqn.primitive == ctrl_transform_prim - assert ctrl_eqn.params["control_values"] is None + assert "control_values" not in ctrl_eqn.params assert ctrl_eqn.params["n_control"] == 2 - value_args = ctrl_eqn.invars[-4:-2] + value_args = ctrl_eqn.invars[-2:] assert value_args[int(static_first)] == plxpr.jaxpr.invars[1] assert value_args[1 - int(static_first)].val - assert [wire.val for wire in ctrl_eqn.invars[-2:]] == [0, 1] + assert [wire.val for wire in ctrl_eqn.invars[-4:-2]] == [0, 1] + + with qp.queuing.AnnotatedQueue() as queue: + jax.core.eval_jaxpr(plxpr.jaxpr, plxpr.consts, 1.2, False) + expected_values = [True, False] if static_first else [False, True] + qp.assert_equal(queue.queue[0], qp.ctrl(qp.RX(1.2, 2), (0, 1), expected_values)) def test_nested_control(self): """Test that control can be nested.""" @@ -423,8 +428,8 @@ def f(x, w1, w2): plxpr.eqns[1].invars[0] is plxpr.jaxpr.invars[1] ) # first input is first control wire, const assert plxpr.eqns[1].invars[1] is plxpr.jaxpr.invars[0] # second input is x, first arg - assert plxpr.eqns[1].invars[-1] is plxpr.jaxpr.invars[2] # second control wire - assert len(plxpr.eqns[1].invars) == 6 # one const, 4 args, one control wire + assert plxpr.eqns[1].invars[-2] is plxpr.jaxpr.invars[2] # second control wire + assert len(plxpr.eqns[1].invars) == 7 # one const, 4 args, one control wire and value with qp.queuing.AnnotatedQueue() as q: jax.core.eval_jaxpr(plxpr.jaxpr, plxpr.consts, 1.2, 3, 4) @@ -460,7 +465,7 @@ def workflow(wire): qp.assert_equal(q.queue[2], qp.ctrl(qp.S(2), 0)) eqn = jaxpr.eqns[0] - assert eqn.params["control_values"] == (True,) + assert eqn.invars[-1].val assert eqn.params["n_consts"] == 0 assert eqn.params["n_control"] == 1 assert eqn.params["work_wires"] is None diff --git a/tests/tape/test_plxpr_conversion.py b/tests/tape/test_plxpr_conversion.py index 282dbfbec19..9e54e5059e3 100644 --- a/tests/tape/test_plxpr_conversion.py +++ b/tests/tape/test_plxpr_conversion.py @@ -227,12 +227,12 @@ def qfunc(x, wire): qp.RX(x, wire) qp.X(wire) - def f(x): - qp.ctrl(qfunc, control=[1, 2], control_values=[False, False])(x, 0) + def f(x, val): + qp.ctrl(qfunc, control=[1, 2], control_values=[False, val])(x, 0) obj = CollectOpsandMeas() x = jax.numpy.array(-0.98) - obj(f)(x) + obj(f)(x, False) assert len(obj.state["ops"]) == 2 expected0 = qp.ctrl(qp.RX(x, 0), [1, 2], control_values=[False, False]) From fb6cd357252bf7dfeb502394402b48d822e48923 Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Tue, 20 Oct 2026 14:29:59 -0400 Subject: [PATCH 4/8] Mark specs control capture dependency as strict xfail [sc-133997] Assisted-by: Codex --- tests/resource/test_specs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/resource/test_specs.py b/tests/resource/test_specs.py index 48f0c82024f..3d158c67a5d 100644 --- a/tests/resource/test_specs.py +++ b/tests/resource/test_specs.py @@ -163,6 +163,17 @@ def circuit(): ), ) + @pytest.mark.xfail( + strict=True, + raises=pytest.RaisesExc( + TypeError, + match=( + r"^handle_ctrl_transform\(\) missing 1 required keyword-only " + r"argument: 'control_values'" + ), + ), + reason="Requires Catalyst control-value operand support (sc-133997).", + ) def test_complex(self): """Test a complex case of qp.specs() against PennyLane""" From 73ffb02c558005c0a503c2c0ace76c33a7dca42e Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Tue, 20 Oct 2026 17:06:08 -0400 Subject: [PATCH 5/8] Cast tuple control values to Boolean dtype [sc-133997] Assisted-by: Codex --- doc/releases/changelog-dev.md | 5 ----- pennylane/ops/op_math/controlled.py | 2 +- tests/capture/test_nested_plxpr.py | 5 +++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 19ebf6d7537..3d075cd5190 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -1336,11 +1336,6 @@

Breaking changes 💔

-* The `ctrl_transform` capture primitive now receives Boolean control values as operands - after the control wires. Its `control_values` parameter has been removed, so consuming - interpreters, including Catalyst, must support the new operand layout. - [(#10356)](https://github.com/PennyLaneAI/pennylane/pull/10356) - * :func:`~.transforms.decompose` is now a pass-only transform that always dispatches to Catalyst's ``graph_decomposition`` under :func:`~.qjit`. The previous tape-based implementation is available as :func:`~.transforms._tape_decompose`. diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index b2299d5209a..e13285b2e2b 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -300,7 +300,7 @@ def create_controlled_op(op, control, control_values, work_wires, work_wire_type if isinstance(control_values, (int, bool)): control_values = [bool(control_values)] elif isinstance(control_values, tuple): - control_values = [v if math.is_abstract(v) else bool(v) for v in control_values] + control_values = [math.cast(v, dtype=bool) for v in control_values] qp.QueuingManager.remove(op) diff --git a/tests/capture/test_nested_plxpr.py b/tests/capture/test_nested_plxpr.py index 9e194b91070..8f9b193153d 100644 --- a/tests/capture/test_nested_plxpr.py +++ b/tests/capture/test_nested_plxpr.py @@ -369,10 +369,10 @@ def f(w): assert plxpr.eqns[0].params["work_wires"] == qp.wires.Wires(["aux"]) def test_control_values(self): - """Test that control values can be provided.""" + """Test that integer tuple control values are captured as Booleans.""" def f(z): - return qp.ctrl(qp.RZ, (3, 4), [False, True])(z, 0) + return qp.ctrl(qp.RZ, (3, 4), (0, 1))(z, 0) plxpr = jax.make_jaxpr(f)(0.5) @@ -385,6 +385,7 @@ def f(z): assert len(q) == 1 assert [value.val for value in plxpr.eqns[0].invars[-2:]] == [False, True] + assert all(value.aval.dtype == bool for value in plxpr.eqns[0].invars[-2:]) assert plxpr.eqns[0].params["n_control"] == 2 @pytest.mark.parametrize("container, static_first", ((tuple, False), (list, True))) From c5f3118cbf5744ae48e98a4038a4239be69b46e3 Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Tue, 20 Oct 2026 17:39:33 -0400 Subject: [PATCH 6/8] mf --- tests/resource/test_specs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/resource/test_specs.py b/tests/resource/test_specs.py index b3c4cb5ab5b..f99b32d2c2e 100644 --- a/tests/resource/test_specs.py +++ b/tests/resource/test_specs.py @@ -165,8 +165,8 @@ def circuit(): @pytest.mark.xfail( reason=( - "Requires Catalyst support for control-value operands (sc-133997) " - "and work_wire_spec." + "Requires Catalyst support for control-value operands (sc-133997) " + "and work_wire_spec." ), ) def test_complex(self): From 004addbc7bf05917dcae9c6499242f00c8e8fc7f Mon Sep 17 00:00:00 2001 From: "Yushao Chen (Jerry)" Date: Wed, 21 Oct 2026 10:17:34 -0400 Subject: [PATCH 7/8] Update pennylane/ops/op_math/controlled.py Co-authored-by: Christina Lee --- pennylane/ops/op_math/controlled.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 88aaf399b0f..33cf4e6dfb0 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -299,7 +299,7 @@ def create_controlled_op(op, control, control_values, work_wires, work_wire_type control = Wires(control) if isinstance(control_values, (int, bool)): control_values = [bool(control_values)] - elif isinstance(control_values, tuple): + elif isinstance(control_values, (tuple, list)): control_values = [math.cast(v, dtype=bool) for v in control_values] qp.QueuingManager.remove(op) From f9fd281faef11e36f6a0164ed4999239ad0fe7fc Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Wed, 21 Oct 2026 10:26:50 -0400 Subject: [PATCH 8/8] https://github.com/PennyLaneAI/pennylane/pull/10356#discussion_r4337881169 --- tests/capture/test_nested_plxpr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/capture/test_nested_plxpr.py b/tests/capture/test_nested_plxpr.py index a0184d1be8c..f1a9c8360cc 100644 --- a/tests/capture/test_nested_plxpr.py +++ b/tests/capture/test_nested_plxpr.py @@ -386,11 +386,12 @@ def f(w): 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 integer tuple control values are captured as Booleans.""" + @pytest.mark.parametrize("container", (list, tuple)) + def test_control_values(self, container): + """Test that integer control values are captured as Booleans.""" def f(z): - return qp.ctrl(qp.RZ, (3, 4), (0, 1))(z, 0) + return qp.ctrl(qp.RZ, (3, 4), container((0, 1)))(z, 0) plxpr = jax.make_jaxpr(f)(0.5)