From 85ff62b9c9218f523ef7ff0535cde7cbf55ebb7b Mon Sep 17 00:00:00 2001 From: David Wierichs Date: Fri, 24 May 2024 09:42:16 +0200 Subject: [PATCH] Allow tuple-valued `control_values` with `qml.ctrl` on a `Controlled` instance. (#5725) **Context:** By default, `control_values` of `Controlled` instances are lists. `qml.defer_measurements` passes a tuple as `control_values` to `ctrl`. `create_controlled_op`, called by `ctrl`, tries to merge `control_values` of the base operation and the provided `control_values`. The consequence is an attempted addition of `list` and `tuple`, if `qml.defer_measurements` hits a `qml.cond` of a `Controlled` instance, leading to #5717 **Description of the Change:** This PR fixes the above problem in two (redundant!) ways: - tuples are allowed to be passed as `control_values` to `ctrl`, which casts them to a `list`. - `defer_measurements` passes a `list` itself. Clearly those changes are redundant for the bug #5717, with the former also providing users with the option of using `tuple`s for `control_values`. We could skip the change to `defer_measurements`, thus. **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** Fixes #5717 [sc-63645] --- doc/releases/changelog-dev.md | 6 ++++++ pennylane/ops/op_math/controlled.py | 2 ++ tests/ops/op_math/test_controlled.py | 11 +++++++++++ tests/ops/op_math/test_controlled_ops.py | 12 ++++++++++++ tests/transforms/test_defer_measurements.py | 8 +++++--- 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 052a8975c4b..bd4e5f0ffc3 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -9,6 +9,9 @@

Improvements 🛠

+* `ctrl` now works with tuple-valued `control_values` when applied to any already controlled operation. + [(#5725)](https://github.com/PennyLaneAI/pennylane/pull/5725) + * Add support for 3 new pytest markers: `unit`, `integration` and `system`. [(#5517)](https://github.com/PennyLaneAI/pennylane/pull/5517) @@ -141,6 +144,9 @@

Bug fixes 🐛

+* `qml.cond` can now be applied to `ControlledOp` operations when deferring measurements. + [(#5725)](https://github.com/PennyLaneAI/pennylane/pull/5725) + * The legacy `Tensor` class can now handle a `Projector` with abstract tracer input. [(#5720)](https://github.com/PennyLaneAI/pennylane/pull/5720) diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 2628976289b..33d23ca4e73 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -140,6 +140,8 @@ def create_controlled_op(op, control, control_values=None, work_wires=None): control_values = [control_values] elif control_values is None: control_values = [True] * len(control) + elif isinstance(control_values, tuple): + control_values = list(control_values) ctrl_op = _try_wrap_in_custom_ctrl_op( op, control, control_values=control_values, work_wires=work_wires diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py index b9630d206df..2d4234a7795 100644 --- a/tests/ops/op_math/test_controlled.py +++ b/tests/ops/op_math/test_controlled.py @@ -163,6 +163,17 @@ def test_zero_one_control_values(self): op = Controlled(self.temp_op, (0, 1), control_values=[0, 1]) assert op.control_values == [False, True] + @pytest.mark.parametrize("control_values", [True, False, 0, 1]) + def test_scalar_control_values(self, control_values): + """Test assignment of provided control_values.""" + op = Controlled(self.temp_op, 0, control_values=control_values) + assert op.control_values == [control_values] + + def test_tuple_control_values(self): + """Test assignment of provided control_values.""" + op = Controlled(self.temp_op, (0, 1), control_values=(0, 1)) + assert op.control_values == [False, True] + def test_non_boolean_control_values(self): """Test control values are converted to booleans.""" op = Controlled(self.temp_op, (0, 1, 2), control_values=["", None, 5]) diff --git a/tests/ops/op_math/test_controlled_ops.py b/tests/ops/op_math/test_controlled_ops.py index a0bbc879d89..415502e9664 100644 --- a/tests/ops/op_math/test_controlled_ops.py +++ b/tests/ops/op_math/test_controlled_ops.py @@ -737,3 +737,15 @@ def test_controlled_method(base, cbase): """Tests the _controlled method for parametric ops.""" # pylint: disable=protected-access assert qml.equal(base._controlled("a"), cbase) + + +@pytest.mark.parametrize( + "control, control_values", + [([0, 1], [True, False]), ([10, "a"], (0, 0)), ([2], 1), (2, (True,))], +) +@pytest.mark.parametrize( + "base_op", [qml.CRX(0.2, [21, 22]), qml.CNOT([21, 22]), qml.CPhase(0.6, [21, 22])] +) +def test_controlling_a_controlled_operation(control, control_values, base_op): + """Test that a controlled op can be controlled again.""" + qml.ctrl(base_op, control=control, control_values=control_values) diff --git a/tests/transforms/test_defer_measurements.py b/tests/transforms/test_defer_measurements.py index cdc1fd5f138..4d4faea65db 100644 --- a/tests/transforms/test_defer_measurements.py +++ b/tests/transforms/test_defer_measurements.py @@ -1063,7 +1063,7 @@ def qnode(): ) def test_cond_qfunc(self, device): """Test that a qfunc can also used with qml.cond.""" - dev = qml.device(device, wires=3) + dev = qml.device(device, wires=4) r = 2.324 @@ -1074,12 +1074,14 @@ def normal_circuit(rads): qml.CNOT(wires=[0, 1]) qml.CRY(rads, wires=[0, 1]) qml.CZ(wires=[0, 1]) - return qml.probs(wires=1) + qml.ctrl(qml.CRX, control=0, control_values=[1])(0.5, [1, 2]) + return qml.probs(wires=[1, 2]) def f(x): qml.PauliX(1) qml.RY(x, wires=1) qml.PauliZ(1) + qml.CRX(0.5, [1, 2]) @qml.defer_measurements @qml.qnode(dev) @@ -1087,7 +1089,7 @@ def quantum_control_circuit(r): qml.Hadamard(0) m_0 = qml.measure(0) qml.cond(m_0, f)(r) - return qml.probs(wires=1) + return qml.probs(wires=[1, 2]) exp = normal_circuit(r) cond_probs = quantum_control_circuit(r)