Repository: PennyLaneAI/pennylane

Issue:
Allow specification of control_values for qml.ctrl
### Feature details

- qml.ControlledQubitUnitary, qml.MultiControlledX etc allow for specification of control_values in form of e.g. a string "0110" for all control_wires
- qml.ctrl should allow for the specification of a control_values keyword like those other operations
- currently, qml.PauliX has to be used to flip each control_wire to the desired value when using qml.ctrl

### Implementation

- Either, qml.ctrl itself could include the implementation of qml.PauliX on relevant control_wires before and after implementing the passed function

The alternative suggestion I am unsure about, as my understanding of the code & underlying considerations is limited:
- Or, according to my current understanding, expand_with_control requires modification to 1) use qml.ControlledQubitOperation for all ops, or 2) change the default behaviour of different _controlled methods to also allow for specification of control_values

### How important would you say this feature is?

1: Not important. Would be nice to have.

### Additional information

Please excuse me if this behaviour has been considered but determined to be suboptimal. I did not find any issues or explanations regarding this subject, so I thought it might be a nice feature to request.


Discussion:
Hi @KarimAED, thank you for suggesting this feature! It does look like something that could be added. Would you like to try to implement this?
Hi, @CatalinaAlbornoz, yes I would love to try working on this. Which implementation would you suggest I move forward with? The first suggestion seems the most self-contained, but it also feels like it's maybe not the cleanest implementation? Let me know what you think.
@KarimAED 
We are in the process of a deeper set of restructuring and improvements to how we do control operations, and adding `control_values` to all operations is definitely something we want to support.

While currently `MultiControlledX` and `ControlledQubitUnitary` accept a string to denote the control values, I'm wanting to move that to be a list of booleans/ zeroes and ones instead. For example, we could support the syntax `qml.ctrl(qml.PauliX, control=[2,3], control_values=[1, 1])(wires=0)`.

I'm not sure yet on the timeline of our more invasive internal changes.  What you proposed for modifying `ctrl` will be helpful regardless.

A PR could involve:
* adding a `control_values=None` keyword argument to `qml.ctrl`.  This argument should be a list of booleans.
* adding a `control_values=None` keyword argument to `ControlledOperation`. 
* Performing `qml.PauliX` on relevant wires in `ControlledOperation.expand`. This would be before and after the loop over control wires, so `expand_with_control` would not need to take `control_values`.
* pass the `control_values` on to the new `ControlledOperation` ops created in `adjoint` and `_controlled`.
* Adding tests

Thanks for your feedback 🚀 
@albi3ro
Sounds good, I've started implementing it now already.

Slightly unrelated question, but on opening `qml.transforms.control` in my IDE, it flags the `ControlledOperation.adjoint` method because it doesn't match the `Operation.adjoint` signature from which it inherits. The `do_queue` keyword argument is omitted. Is there any reason not to include it in the `adjoint` method with the same default value as in the `ControlledOperation` initializer (`True`) and pass it through? Otherwise, I would just add that in as well.
The adjoint method is also something that is being fixed in the near future. Ignore the `do_queue` keyword.

Resolving patch:
diff --git a/pennylane/transforms/control.py b/pennylane/transforms/control.py
--- 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 @@ def __init__(self, tape, control_wires, do_queue=True):
         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 @@ def num_params(self):
     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 @@ def expand(self):
         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 @@ def adjoint(self):
                 # 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 my_circuit2():
             # 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
--- a/tests/transforms/test_control.py
+++ b/tests/transforms/test_control.py
@@ -403,3 +403,65 @@ def circuit(b):
         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))