Repository: PennyLaneAI/pennylane
Pull request #2294 (2022-03-09 14:57:42)

Extend the conditional operations documentation
**Context:**
Continuation of https://github.com/PennyLaneAI/pennylane/pull/2275 adding further docstring examples and polishing the docs.

**Description of the Change:**

Added:
- [X] example with qfuncs,
- [X] example with then/else, and
- [x] example showing how to deal with conditional functions with different signatures

Updated:
- [x] The mcm and cond op description in the intro docs

Fixed:
- [X] `MeasurementValue.__invert__` unexpected behaviour; now a copy is being returned instead of inverting in place.

The reason was that examples like
```python
m = qml.measure(0)
qml.cond(m, RY)(0.3, wires=0)
qml.cond(~m, RX)(1.234, wires=0)
```
were producing unexpected results. The tests were amended accordingly.
- [X] Unexpected errors when using `==` on `MeasurementValue` objects for comparison: the `==` operator is used for assertion. An error message is raised if used in any other way.

**Benefits:**
More docs and fixes.

**Possible Drawbacks:**
N/A

**Related GitHub Issues:**
N/A

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2294?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> :exclamation: No coverage uploaded for pull request base (`v0.22.0-rc0@628493d`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#section-missing-base-commit).
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2294/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/2294?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@              Coverage Diff               @@
##             v0.22.0-rc0    #2294   +/-   ##
==============================================
  Coverage               ?   99.32%           
==============================================
  Files                  ?      242           
  Lines                  ?    19138           
  Branches               ?        0           
==============================================
  Hits                   ?    19008           
  Misses                 ?      130           
  Partials               ?        0           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2294?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/2294?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [628493d...3a27922](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2294?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).


Patch:
diff --git a/doc/introduction/measurements.rst b/doc/introduction/measurements.rst
index 4ee40ae03..9c07dda65 100644
--- a/doc/introduction/measurements.rst
+++ b/doc/introduction/measurements.rst
@@ -158,8 +158,8 @@ Mid-circuit measurements and conditional operations
 ---------------------------------------------------
 
 PennyLane allows specifying measurements in the middle of the circuit.
-Operations can then be conditioned on the measurement outcome of such
-mid-circuit measurements:
+Quantum functions such as operations can then be conditioned on the measurement
+outcome of such mid-circuit measurements:
 
 .. code-block:: python
 
@@ -180,7 +180,7 @@ measurement on qubit 1 yielded ``1`` as an outcome, otherwise doing nothing
 for the ``0`` measurement outcome.
 
 PennyLane implements the deferred measurement principle to transform
-conditional operations with the :func:`defer_measurements` quantum
+conditional operations with the :func:`~.defer_measurements` quantum
 function transform.
 
 .. code-block:: python
@@ -226,6 +226,10 @@ differentiable and device-independent way. Performing true mid-circuit
 measurements and conditional operations is dependent on the
 quantum hardware and PennyLane device capabilities.
 
+For more examples on applying quantum functions conditionally, refer to the
+:func:`~.pennylane.cond` transform.
+
+
 Changing the number of shots
 ----------------------------
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 3e3caecd6..47c5a65ba 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -92,6 +92,7 @@
   [(#2211)](https://github.com/PennyLaneAI/pennylane/pull/2211)
   [(#2236)](https://github.com/PennyLaneAI/pennylane/pull/2236)
   [(#2275)](https://github.com/PennyLaneAI/pennylane/pull/2275)
+  [(#2294)](https://github.com/PennyLaneAI/pennylane/pull/2294)
 
   The addition includes the `defer_measurements` device-independent transform
   that can be applied on devices that have no native mid-circuit measurements
diff --git a/pennylane/measurements.py b/pennylane/measurements.py
index c400d58a9..35d78addd 100644
--- a/pennylane/measurements.py
+++ b/pennylane/measurements.py
@@ -634,17 +634,25 @@ class MeasurementValue(Generic[T]):
         return branch_dict
 
     def __invert__(self):
-        """Inverts the control value of the measurement."""
+        """Return a copy of the measurement value with an inverted control
+        value."""
+        inverted_self = copy.copy(self)
         zero = self._zero_case
         one = self._one_case
 
-        self._control_value = one if self._control_value == zero else zero
+        inverted_self._control_value = one if self._control_value == zero else zero
 
-        return self
+        return inverted_self
 
     def __eq__(self, control_value):
         """Allow asserting measurement values."""
         measurement_outcomes = {self._zero_case, self._one_case}
+
+        if not isinstance(control_value, tuple(type(val) for val in measurement_outcomes)):
+            raise MeasurementValueError(
+                f"The equality operator is used to assert measurement outcomes, but got a value with type {type(control_value)}."
+            )
+
         if control_value not in measurement_outcomes:
             raise MeasurementValueError(
                 f"Unknown measurement value asserted; the set of possible measurement outcomes is: {measurement_outcomes}."
@@ -696,7 +704,7 @@ def measure(wires):
     tensor([0.90165331, 0.09834669], requires_grad=True)
 
     Args:
-        wires (Wires): The wires the measurement process applies to.
+        wires (Wires): The wire of the qubit the measurement process applies to.
 
     Raises:
         QuantumFunctionError: if multiple wires were specified
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 525ecad50..55d299511 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -788,7 +788,7 @@ class Operator(abc.ABC):
 
         Returns:
             tuple[list[tensor_like or float], list[.Operation]]: list of coefficients :math:`c_i`
-                and list of operations :math:`O_i`
+            and list of operations :math:`O_i`
         """
         return self.compute_terms(*self.parameters, **self.hyperparameters)
 
diff --git a/pennylane/transforms/condition.py b/pennylane/transforms/condition.py
index 468177d8a..d0da00a43 100644
--- a/pennylane/transforms/condition.py
+++ b/pennylane/transforms/condition.py
@@ -14,7 +14,6 @@
 """
 Contains the condition transform.
 """
-from copy import copy
 from functools import wraps
 from typing import Type
 
@@ -88,21 +87,148 @@ def cond(condition, true_fn, false_fn=None):
 
         dev = qml.device("default.qubit", wires=3)
 
-        first_par = 0.1
-        sec_par = 0.3
-
         @qml.qnode(dev)
-        def qnode():
+        def qnode(x, y):
+            qml.Hadamard(0)
             m_0 = qml.measure(0)
-            qml.cond(m_0, qml.RY)(first_par, wires=1)
+            qml.cond(m_0, qml.RY)(x, wires=1)
 
+            qml.Hadamard(2)
+            qml.RY(-np.pi/2, wires=[2])
             m_1 = qml.measure(2)
-            qml.cond(m_0, qml.RZ)(sec_par, wires=1)
+            qml.cond(m_1 == 0, qml.RX)(y, wires=1)
             return qml.expval(qml.PauliZ(1))
+
+    .. code-block :: pycon
+
+        >>> first_par = np.array(0.3, requires_grad=True)
+        >>> sec_par = np.array(1.23, requires_grad=True)
+        >>> qnode(first_par, sec_par)
+        tensor(0.32677361, requires_grad=True)
+
+    .. note::
+
+        If the first argument of ``cond`` is a measurement value (e.g., ``m_0``
+        in ``qml.cond(m_0, qml.RY)``), then ``m_0 == 1`` is considered
+        internally.
+
+    .. UsageDetails::
+
+        **Conditional quantum functions**
+
+        The ``cond`` transform allows conditioning quantum functions too:
+
+        .. code-block:: python3
+
+            dev = qml.device("default.qubit", wires=2)
+
+            def qfunc(par, wires):
+                qml.Hadamard(wires[0])
+                qml.RY(par, wires[0])
+
+            @qml.qnode(dev)
+            def qnode(x):
+                qml.Hadamard(0)
+                m_0 = qml.measure(0)
+                qml.cond(m_0, qfunc)(x, wires=[1])
+                return qml.expval(qml.PauliZ(1))
+
+        .. code-block :: pycon
+
+            >>> par = np.array(0.3, requires_grad=True)
+            >>> qnode(par)
+            tensor(0.3522399, requires_grad=True)
+
+        **Passing two quantum functions**
+
+        In the qubit model, single-qubit measurements may result in one of two
+        outcomes. Such measurement outcomes may then be used to create
+        conditional expressions.
+
+        According to the truth value of the conditional expression passed to
+        ``cond``, the transform can apply a quantum function in both the
+        ``True`` and ``False`` case:
+
+        .. code-block:: python3
+
+            dev = qml.device("default.qubit", wires=2)
+
+            def qfunc1(x, wires):
+                qml.Hadamard(wires[0])
+                qml.RY(x, wires[0])
+
+            def qfunc2(x, wires):
+                qml.Hadamard(wires[0])
+                qml.RZ(x, wires[0])
+
+            @qml.qnode(dev)
+            def qnode1(x):
+                qml.Hadamard(0)
+                m_0 = qml.measure(0)
+                qml.cond(m_0, qfunc1, qfunc2)(x, wires=[1])
+                return qml.expval(qml.PauliZ(1))
+
+        .. code-block :: pycon
+
+            >>> par = np.array(0.3, requires_grad=True)
+            >>> qnode1(par)
+            tensor(-0.1477601, requires_grad=True)
+
+        The previous QNode is equivalent to using ``cond`` twice, inverting the
+        conditional expression in the second case using the ``~`` unary
+        operator:
+
+        .. code-block:: python3
+
+            @qml.qnode(dev)
+            def qnode2(x):
+                qml.Hadamard(0)
+                m_0 = qml.measure(0)
+                qml.cond(m_0, qfunc1)(x, wires=[1])
+                qml.cond(~m_0, qfunc2)(x, wires=[1])
+                return qml.expval(qml.PauliZ(1))
+
+        .. code-block :: pycon
+
+            >>> qnode2(par)
+            tensor(-0.1477601, requires_grad=True)
+
+        **Quantum functions with different signatures**
+
+        It may be that the two quantum functions passed to ``qml.cond`` have
+        different signatures. In such a case, ``lambda`` functions taking no
+        arguments can be used with Python closure:
+
+        .. code-block:: python3
+
+            dev = qml.device("default.qubit", wires=2)
+
+            def qfunc1(x, wire):
+                qml.Hadamard(wire)
+                qml.RY(x, wire)
+
+            def qfunc2(x, y, z, wire):
+                qml.Hadamard(wire)
+                qml.Rot(x, y, z, wire)
+
+            @qml.qnode(dev)
+            def qnode(a, x, y, z):
+                qml.Hadamard(0)
+                m_0 = qml.measure(0)
+                qml.cond(m_0, lambda: qfunc1(a, wire=1), lambda: qfunc2(x, y, z, wire=1))()
+                return qml.expval(qml.PauliZ(1))
+
+        .. code-block :: pycon
+
+            >>> par = np.array(0.3, requires_grad=True)
+            >>> x = np.array(1.2, requires_grad=True)
+            >>> y = np.array(1.1, requires_grad=True)
+            >>> z = np.array(0.3, requires_grad=True)
+            >>> qnode(par, x, y, z)
+            tensor(-0.30922805, requires_grad=True)
     """
     if callable(true_fn):
         # We assume that the callable is an operation or a quantum function
-
         with_meas_err = (
             "Only quantum functions that contain no measurements can be applied conditionally."
         )
@@ -127,11 +253,10 @@ def cond(condition, true_fn, false_fn=None):
                 if else_tape.measurements:
                     raise ConditionalTransformError(with_meas_err)
 
-                inverted_m = copy(condition)
-                inverted_m = ~inverted_m
+                inverted_condition = ~condition
 
                 for op in else_tape.operations:
-                    Conditional(inverted_m, op)
+                    Conditional(inverted_condition, op)
 
     else:
         raise ConditionalTransformError(


Test patch:
diff --git a/tests/test_measurements.py b/tests/test_measurements.py
index 90cb7081e..c59f07f23 100644
--- a/tests/test_measurements.py
+++ b/tests/test_measurements.py
@@ -347,10 +347,27 @@ class TestMeasurementValue:
         one_case = val_pair[1]
         mv = MeasurementValue(measurement_id="1234", zero_case=zero_case, one_case=one_case)
         for _ in range(num_inv):
-            mv = mv.__invert__()
+            mv_new = mv.__invert__()
+
+            # Check that inversion involves creating a copy
+            assert not mv_new is mv
+
+            mv = mv_new
 
         assert mv._control_value == val_pair[expected_idx]
 
+    def test_measurement_value_assertion_error_wrong_type(self):
+        """Test that the return_type related info is updated for a
+        measurement."""
+        mv1 = MeasurementValue(measurement_id="1111")
+        mv2 = MeasurementValue(measurement_id="2222")
+
+        with pytest.raises(
+            MeasurementValueError,
+            match="The equality operator is used to assert measurement outcomes, but got a value with type",
+        ):
+            mv1 == mv2
+
     def test_measurement_value_assertion_error(self):
         """Test that the return_type related info is updated for a
         measurement."""
diff --git a/tests/transforms/test_condition.py b/tests/transforms/test_condition.py
index efe98f9e1..a131d44ed 100644
--- a/tests/transforms/test_condition.py
+++ b/tests/transforms/test_condition.py
@@ -69,9 +69,32 @@ class TestCond:
 
         assert ops[4].return_type == qml.operation.Probability
 
-    def test_cond_queues_with_else(self):
-        """Test that qml.cond queues Conditional operations as expected when an
-        else qfunc is also provided."""
+    def tape_with_else(f, g, r):
+        """Tape that uses cond by passing both a true and false func."""
+        with qml.tape.QuantumTape() as tape:
+            m_0 = qml.measure(0)
+            qml.cond(m_0, f, g)(r)
+            qml.probs(wires=1)
+
+        return tape
+
+    def tape_uses_cond_twice(f, g, r):
+        """Tape that uses cond twice such that it's equivalent to using cond
+        with two functions being passed (tape_with_else)."""
+        with qml.tape.QuantumTape() as tape:
+            m_0 = qml.measure(0)
+            qml.cond(m_0, f)(r)
+            qml.cond(~m_0, g)(r)
+            qml.probs(wires=1)
+
+        return tape
+
+    @pytest.mark.parametrize("tape", [tape_with_else, tape_uses_cond_twice])
+    def test_cond_queues_with_else(self, tape):
+        """Test that qml.cond queues Conditional operations as expected in two cases:
+        1. When an else qfunc is provided;
+        2. When qml.cond is used twice equivalent to using an else qfunc.
+        """
         r = 1.234
 
         def f(x):
@@ -82,11 +105,7 @@ class TestCond:
         def g(x):
             qml.PauliY(1)
 
-        with qml.tape.QuantumTape() as tape:
-            m_0 = qml.measure(0)
-            qml.cond(m_0, f, g)(r)
-            qml.probs(wires=1)
-
+        tape = tape(f, g, r)
         ops = tape.queue
         target_wire = qml.wires.Wires(1)
 
@@ -111,6 +130,13 @@ class TestCond:
         assert isinstance(ops[4].then_op, qml.PauliY)
         assert ops[4].then_op.wires == target_wire
 
+        # Check that: the measurement value is the same for true_fn conditional
+        # ops
+        assert ops[1].meas_val is ops[2].meas_val is ops[3].meas_val
+
+        # However, it is not the same for the false_fn
+        assert ops[3].meas_val is not ops[4].meas_val
+
         assert ops[5].return_type == qml.operation.Probability
 
     def test_cond_error(self):