Repository: PennyLaneAI/pennylane
Pull request #2288 (2022-03-07 15:40:50)

Add ctrl values
**Context:**

Previously, the `qml.ctrl` function could not take any control values. Users had to manually add `qml.PauliX` operations before and after every relevant use of `qml.ctrl`. This did not match the expected behaviour from `qml.ControlledQubitUnitary`, `qml.MultiControlledX` etc.

**Description of the Change:**

In accordance with suggestions by @albi3ro, an optional argument `control_values` was introduced, which takes an integer or a list of integers to supply said control values. These are then passed into `qml.ControlledOperation`, where they are applied as `qml.PauliX` operations within the `expand` method, but outside `expand_with_control`.

**Benefits:**

The behaviour of `qml.ctrl` is now more consistent with other controlled operations. Furthermore, this leads to a simplified use case. Backwards compatibility is retained.

**Possible Drawbacks:**

Currently, the use of integers is slightly unusual in comparison to the other operations, which often rely on lists of strings instead. However, this was indicated as the desired behaviour in the [issue](https://github.com/PennyLaneAI/pennylane/issues/2176) I raised. No other drawbacks known.

**Related GitHub Issues:**
Closes  #2176

antalszava: Hi @KarimAED thank you for opening this PR! :slightly_smiling_face: Let us know whenever you'd like us to give a review here.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#2288](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (afdb00d) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/d201dd52def0dfa44efd485e06ea06defda22dc0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (d201dd5) will **decrease** coverage by `2.64%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #2288      +/-   ##
==========================================
- Coverage   99.31%   96.67%   -2.65%     
==========================================
  Files         241      241              
  Lines       19089    19112      +23     
==========================================
- Hits        18958    18476     -482     
- Misses        131      636     +505     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/transforms/control.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY29udHJvbC5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/interfaces/batch/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvYmF0Y2gvamF4LnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/batch/jax\_jit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvYmF0Y2gvamF4X2ppdC5weQ==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4LnB5) | `0.00% <0.00%> (-97.44%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF9qYXgucHk=) | `0.00% <0.00%> (-96.63%)` | :arrow_down: |
| [pennylane/qnn/cost.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3Fubi9jb3N0LnB5) | `42.85% <0.00%> (-57.15%)` | :arrow_down: |
| [pennylane/transforms/classical\_jacobian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY2xhc3NpY2FsX2phY29iaWFuLnB5) | `77.08% <0.00%> (-22.92%)` | :arrow_down: |
| [pennylane/math/utils.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvdXRpbHMucHk=) | `84.41% <0.00%> (-15.59%)` | :arrow_down: |
| [pennylane/math/multi\_dispatch.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvbXVsdGlfZGlzcGF0Y2gucHk=) | `87.73% <0.00%> (-12.27%)` | :arrow_down: |
| [pennylane/math/is\_independent.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21hdGgvaXNfaW5kZXBlbmRlbnQucHk=) | `88.50% <0.00%> (-11.50%)` | :arrow_down: |
| ... and [16 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [d201dd5...afdb00d](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2288?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

KarimAED: Hi @antalszava, thanks for the info! I got that sorted out, and I added the changelog info as required. I am still slightly confused by the codecov result, though. Many files seem to have lost coverage, even though I didn't modify them at all. If I'm not mistaken, after that is sorted out, the PR would be ready for review. Sorry again for requiring so much attention, as I said, it's my first PR. :sweat_smile:
KarimAED: Hi, I'm still confused why codecov is making trouble. The diff clearly has sufficient coverage, but apparently somehow the project coverage went down? Can anybody help me with this?
antalszava: Hi @KarimAED, we've experienced that at times Codecov might report unexpected results. Having said that, it seems that on the latest push, the checks were passing well. :+1: We could assist further if it comes up again.

We'll go for a review then. :+1: 

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 9d6ba19d8..eeb06586f 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -6,6 +6,13 @@
 
 <h3>Improvements</h3>
 
+* The function `qml.ctrl` was given the optional argument `control_values=None`.
+  If overridden, `control_values` takes an integer or a list of integers corresponding to
+  the binary value that each control value should take. The same change is reflected in
+  `ControlledOperation`. Control values of `0` are implemented by `qml.PauliX` applied
+  before and after the controlled operation
+  [(#2288)](https://github.com/PennyLaneAI/pennylane/pull/2288)
+  
 <h3>Breaking changes</h3>
 
 <h3>Deprecations</h3>
@@ -15,3 +22,4 @@
 <h3>Contributors</h3>
 
 This release contains contributions from (in alphabetical order):
+Karim Alaa El-Din
\ No newline at end of file
diff --git a/pennylane/transforms/control.py b/pennylane/transforms/control.py
index 7707c443d..eb49137d5 100644
--- a/pennylane/transforms/control.py
+++ b/pennylane/transforms/control.py
@@ -81,12 +81,14 @@ class ControlledOperation(Operation):
         tape: A QuantumTape. This tape defines the unitary that should be applied relative
             to the control wires.
         control_wires: A wire or set of wires.
+        control_values: An int or list of ints indicating the values each control wire should
+            take.
     """
 
     grad_method = None
     num_wires = AnyWires
 
-    def __init__(self, tape, control_wires, do_queue=True):
+    def __init__(self, tape, control_wires, do_queue=True, control_values=None):
         self.subtape = tape
         """QuantumTape: The tape that defines the underlying operation."""
 
@@ -98,6 +100,20 @@ class ControlledOperation(Operation):
         self._control_wires = Wires(control_wires)
         """Wires: The control wires."""
 
+        if control_values is not None:
+            self._control_values = (
+                [control_values] if isinstance(control_values, int) else control_values
+            )
+
+            len_ctrl_wires = len(self.control_wires)
+            len_ctrl_values = len(self.control_values)
+
+            assert (
+                len_ctrl_wires == len_ctrl_values
+            ), f"ControlledOperation: length of control values and wires must match, \
+                but are {len_ctrl_values} and {len_ctrl_wires}"
+        else:
+            self._control_values = [1] * len(self.control_wires)
         wires = self.control_wires + tape.wires
         super().__init__(*tape.get_parameters(), wires=wires, do_queue=do_queue)
 
@@ -109,6 +125,16 @@ class ControlledOperation(Operation):
     def control_wires(self):
         return self._control_wires
 
+    @property
+    def control_values(self):
+        r"""Values the control wires should take.
+        For operations that have control wires which are all ``1``,
+        this can be set to None.
+        Returns:
+            list[int]: The values each control wire should take.
+        """
+        return self._control_values
+
     def expand(self):
         tape = self.subtape
         tape.set_parameters(self.data)
@@ -116,6 +142,17 @@ class ControlledOperation(Operation):
         for wire in self.control_wires:
             tape = expand_with_control(tape, wire)
 
+        if self.control_values is not None:
+            with QuantumTape(do_queue=False) as ctrl_tape:
+                for i, val in enumerate(self.control_values):
+                    if not bool(val):
+                        qml.PauliX(self.control_wires[i])
+                requeue_ops_in_tape(tape)
+                for i, val in enumerate(self.control_values):
+                    if not bool(val):
+                        qml.PauliX(self.control_wires[i])
+
+            tape = ctrl_tape
         return tape
 
     def adjoint(self):
@@ -134,18 +171,24 @@ class ControlledOperation(Operation):
                 # Execute all ops adjointed.
                 adjoint(requeue_ops_in_tape)(self.subtape)
 
-        return ControlledOperation(new_tape, self.control_wires)
+        return ControlledOperation(new_tape, self.control_wires, control_values=self.control_values)
 
     def _controlled(self, wires):
-        ControlledOperation(tape=self.subtape, control_wires=Wires(wires) + self.control_wires)
+        new_values = [1] * len(Wires(wires))
+        ControlledOperation(
+            tape=self.subtape,
+            control_wires=Wires(wires) + self.control_wires,
+            control_values=new_values + self.control_values,
+        )
 
 
-def ctrl(fn, control):
+def ctrl(fn, control, control_values=None):
     """Create a method that applies a controlled version of the provided method.
 
     Args:
         fn (function): Any python function that applies pennylane operations.
         control (Wires): The control wire(s).
+        control_values (list[int]): The values the control wire(s) should take.
 
     Returns:
         function: A new function that applies the controlled equivalent of ``fn``. The returned
@@ -214,12 +257,31 @@ def ctrl(fn, control):
             # These two ops are equivalent.
             op1 = qml.ctrl(qml.ctrl(my_ops, 1), 2)
             op2 = qml.ctrl(my_ops, [2, 1])
+
+        **Control Value Assignment**
+
+        Control values can be assigned as follows.
+
+        .. code-block:: python3
+
+            op = qml.ctrl(qml.ctrl(my_ops, 1), 2, control_values=0)
+            op()
+
+        This is equivalent to the following.
+
+        .. code-block:: python3
+
+            qml.PauliX(wires=2)
+            op = qml.ctrl(qml.ctrl(my_ops, 1), 2)
+            op()
+            qml.PauliX(wires=2)
+
     """
 
     @wraps(fn)
     def wrapper(*args, **kwargs):
         with QuantumTape(do_queue=False) as tape:
             fn(*args, **kwargs)
-        return ControlledOperation(tape, control)
+        return ControlledOperation(tape, control, control_values=control_values)
 
     return wrapper


Test patch:
diff --git a/tests/transforms/test_control.py b/tests/transforms/test_control.py
index 3bc311cab..49d013e8e 100644
--- a/tests/transforms/test_control.py
+++ b/tests/transforms/test_control.py
@@ -403,3 +403,65 @@ class TestDifferentiation:
         expected = np.sin(b / 2) / 2
 
         assert np.allclose(res, expected)
+
+
+def test_control_values_sanity_check():
+    """Test that control works with control values on a very standard usecase."""
+
+    def make_ops():
+        qml.RX(0.123, wires=0)
+        qml.RY(0.456, wires=2)
+        qml.RX(0.789, wires=0)
+        qml.Rot(0.111, 0.222, 0.333, wires=2),
+        qml.PauliX(wires=2)
+        qml.PauliY(wires=4)
+        qml.PauliZ(wires=0)
+
+    with QuantumTape() as tape:
+        cmake_ops = ctrl(make_ops, control=1, control_values=0)
+        # Execute controlled version.
+        cmake_ops()
+
+    expected = [
+        qml.PauliX(wires=1),
+        qml.CRX(0.123, wires=[1, 0]),
+        qml.CRY(0.456, wires=[1, 2]),
+        qml.CRX(0.789, wires=[1, 0]),
+        qml.CRot(0.111, 0.222, 0.333, wires=[1, 2]),
+        qml.CNOT(wires=[1, 2]),
+        qml.CY(wires=[1, 4]),
+        qml.CZ(wires=[1, 0]),
+        qml.PauliX(wires=1),
+    ]
+    assert len(tape.operations) == 1
+    ctrl_op = tape.operations[0]
+    assert isinstance(ctrl_op, ControlledOperation)
+    expanded = ctrl_op.expand()
+    assert_equal_operations(expanded.operations, expected)
+
+
+@pytest.mark.parametrize("ctrl_values", [[0, 0], [0, 1], [1, 0], [1, 1]])
+def test_multi_control_values(ctrl_values):
+    """Test control with a list of wires and control values."""
+
+    def expected_ops(ctrl_val):
+        exp_op = []
+        ctrl_wires = [3, 7]
+        for i, j in enumerate(ctrl_val):
+            if not bool(j):
+                exp_op.append(qml.PauliX(ctrl_wires[i]))
+        exp_op.append(qml.Toffoli(wires=[7, 3, 0]))
+        for i, j in enumerate(ctrl_val):
+            if not bool(j):
+                exp_op.append(qml.PauliX(ctrl_wires[i]))
+
+        return exp_op
+
+    with QuantumTape() as tape:
+        CCX = ctrl(qml.PauliX, control=[3, 7], control_values=ctrl_values)
+        CCX(wires=0)
+    assert len(tape.operations) == 1
+    op = tape.operations[0]
+    assert isinstance(op, ControlledOperation)
+    new_tape = expand_tape(tape, 1)
+    assert_equal_operations(new_tape.operations, expected_ops(ctrl_values))