Repository: PennyLaneAI/pennylane
Pull request #1505 (2021-08-08 12:34:00)

Remove tape check for observable without owners in the queue
**Context:**

Some observables that are composed of other observables may define methods that change the constituent parts. This leaves observables without measurements in the queue. The tape's `process_queue` method, called upon exiting the queueing context, then throws an error.

The only example (as far as I know) in the current code base is the `Tensor.prune()` function:
``` python

with qml.tape.QuantumTape() as tape:
    a = qml.PauliX(wires=0)
    b = qml.Identity(wires=2)
    T = qml.operation.Tensor(a, b).prune()
    qml.expval(T)

# "ValueError: Observable PauliX(wires=[0]) @ Identity(wires=[2]) does not have a measurement type specified."
```

However, once the Hamiltonian class becomes an Observable, the same is caused by the `Hamiltonain.simplify()` method. 

Note: one could in principle not allow such methods to be called in a tape context, but checking this everywhere is error-prone for development, and once Hamiltonians become trainable there are situations when they are created (and should be simplified) inside a quantum function. 

**Description of the Change:**

Removed the check when processing a tape during exit of the queueing context. 
Added a test that the queue is recorded as expected for `Tensor.prune()`.

**Benefits:**

Allowing "orphan" observables (i.e. those that have no measurement)  in the queue is much safer than manipulating the queue during observable methods. An observable whose owners are not owned by a measurement can simply be regarded as a leftover from processing. 

**Possible Drawbacks:**

The tests pass and the annotated queue is not used much during the code base. Also, this behaviour makes changes to observables to some extend trackable. 



github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [.github/CHANGELOG.md](/PennyLaneAI/pennylane/blob/master/.github/CHANGELOG.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.
glassnotes: >  mariaschuld assigned glassnotes 12 minutes ago 

@mariaschuld was this supposed to be a review request? :slightly_smiling_face: 
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1505?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1505](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1505?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (b95a2ef) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/0fe4507c153f071250389ac56ab44d9fea07f57b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (0fe4507) will **decrease** coverage by `0.00%`.
> The diff coverage is `n/a`.

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

```diff
@@            Coverage Diff             @@
##           master    #1505      +/-   ##
==========================================
- Coverage   98.22%   98.21%   -0.01%     
==========================================
  Files         184      184              
  Lines       13203    13201       -2     
==========================================
- Hits        12968    12966       -2     
  Misses        235      235              
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1505?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/tape/tape.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1505/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RhcGUvdGFwZS5weQ==) | `98.60% <ø> (-0.01%)` | :arrow_down: |

------

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

mariaschuld: > > mariaschuld assigned glassnotes 12 minutes ago
> 
> @mariaschuld was this supposed to be a review request?

Whoopsie :) And it is still Sunday anyways!
mariaschuld: Codecov is complaining, but as far as I can see it's just that 2 lines were overall removed, which increases the untested percentage...
mariaschuld: > Should a test be added for Hamiltonian simplify?

I think that is not a bad idea, will do that!

> A lot of the functions/classe...

The list of Operator class overhaul items is growing exponentially :)
mariaschuld: I am now more confident that removing this check is ok - the tape uses all `Operations` that do not have owners to construct `self.operations`, and all measurements to construct `self.measurements`. Then the measurement list is used to construct `self.observables`.

In other words, observables without owners are never an issue.

This is all good info for the upcoming operation class refactor, where the mess will be tidied up!

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 64e14a9bf..4b1bea098 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -465,6 +465,13 @@
 
 <h3>Improvements</h3>
 
+* The tape does not verify any more that all Observables have owners in the annotated queue.
+  [(#1505)](https://github.com/PennyLaneAI/pennylane/pull/1505)
+  This allows manipulation of Observables inside a tape context. An example is 
+  `expval(Tensor(qml.PauliX(0), qml.Identity(1)).prune())` which makes the expval an owner 
+  of the pruned tensor and its constituent observables, but leaves the original tensor in 
+  the queue without an owner.
+
 * The `step` and `step_and_cost` methods of `QNGOptimizer` now accept a custom `grad_fn`
   keyword argument to use for gradient computations.
   [(#1487)](https://github.com/PennyLaneAI/pennylane/pull/1487)
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
index 4ea8c2f01..6d6a82d1e 100644
--- a/pennylane/tape/tape.py
+++ b/pennylane/tape/tape.py
@@ -450,9 +450,6 @@ class QuantumTape(AnnotatedQueue):
                 if obj.return_type is qml.operation.Sample:
                     self.is_sampled = True
 
-            elif isinstance(obj, qml.operation.Observable) and "owner" not in info:
-                raise ValueError(f"Observable {obj} does not have a measurement type specified.")
-
         self._update()
 
     def _update_circuit_info(self):


Test patch:
diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py
index 748ef7fc4..632f19adb 100644
--- a/tests/tape/test_tape.py
+++ b/tests/tape/test_tape.py
@@ -251,21 +251,6 @@ class TestConstruction:
                 qml.RX(0.5, wires=0)
                 qml.expval(qml.PauliZ(wires=1))
 
-    def test_observable_with_no_measurement(self):
-        """Test that an exception is raised if an observable is used without a measurement"""
-
-        with pytest.raises(ValueError, match="does not have a measurement type specified"):
-            with QuantumTape() as tape:
-                qml.RX(0.5, wires=0)
-                qml.Hermitian(np.array([[0, 1], [1, 0]]), wires=1)
-                qml.expval(qml.PauliZ(wires=1))
-
-        with pytest.raises(ValueError, match="does not have a measurement type specified"):
-            with QuantumTape() as tape:
-                qml.RX(0.5, wires=0)
-                qml.PauliX(wires=0) @ qml.PauliY(wires=1)
-                qml.expval(qml.PauliZ(wires=1))
-
     def test_sampling(self):
         """Test that the tape correctly marks itself as returning samples"""
         with QuantumTape() as tape:
diff --git a/tests/test_operation.py b/tests/test_operation.py
index 7612a44ee..7317c0338 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -908,6 +908,70 @@ class TestTensor:
         assert type(O_pruned) == type(expected)
         assert O_pruned.wires == expected.wires
 
+    def test_prune_while_queueing_return_tensor(self):
+        """Tests that pruning a tensor to a tensor in a tape context registers
+        the pruned tensor as owned by the measurement,
+        and turns the original tensor into an orphan without an owner."""
+
+        with qml.tape.QuantumTape() as tape:
+            # we assign operations to variables here so we can compare them below
+            a = qml.PauliX(wires=0)
+            b = qml.PauliY(wires=1)
+            c = qml.Identity(wires=2)
+            T = qml.operation.Tensor(a, b, c)
+            T_pruned = T.prune()
+            m = qml.expval(T_pruned)
+
+        ann_queue = tape._queue
+
+        # the pruned tensor became the owner of Paulis
+        assert ann_queue[a]["owner"] == T_pruned
+        assert ann_queue[b]["owner"] == T_pruned
+
+        # the Identity is still owned by the original Tensor
+        assert ann_queue[c]["owner"] == T
+        # the original tensor still owns all three observables
+        # but is not owned by a measurement
+        assert ann_queue[T]["owns"] == (a, b, c)
+        assert not hasattr(ann_queue[T], "owner")
+
+        # the pruned tensor is owned by the measurement
+        # and owns the two Paulis
+        assert ann_queue[T_pruned]["owner"] == m
+        assert ann_queue[T_pruned]["owns"] == (a, b)
+        assert ann_queue[m]["owns"] == T_pruned
+
+    def test_prune_while_queueing_return_obs(self):
+        """Tests that pruning a tensor to an observable in a tape context registers
+        the pruned observable as owned by the measurement,
+        and turns the original tensor into an orphan without an owner."""
+
+        with qml.tape.QuantumTape() as tape:
+            a = qml.PauliX(wires=0)
+            c = qml.Identity(wires=2)
+            T = qml.operation.Tensor(a, c)
+            T_pruned = T.prune()
+            m = qml.expval(T_pruned)
+
+        ann_queue = tape._queue
+
+        # the pruned tensor is the Pauli observable
+        assert T_pruned == a
+        # pruned tensor/Pauli is owned by the measurement
+        # since the entry in the dictionary got updated
+        # when the pruned tensor's owner was memorized
+        assert ann_queue[a]["owner"] == m
+        # the Identity is still owned by the original Tensor
+        assert ann_queue[c]["owner"] == T
+
+        # the original tensor still owns both observables
+        # but is not owned by a measurement
+        assert ann_queue[T]["owns"] == (a, c)
+        assert not hasattr(ann_queue[T], "owner")
+
+        # the measurement owns the Pauli/pruned tensor
+        assert ann_queue[m]["owns"] == T_pruned
+
 
 equal_obs = [
     (qml.PauliZ(0), qml.PauliZ(0), True),
diff --git a/tests/test_vqe.py b/tests/test_vqe.py
index d84c9ae0c..6fe2d6f99 100644
--- a/tests/test_vqe.py
+++ b/tests/test_vqe.py
@@ -707,6 +707,23 @@ class TestHamiltonian:
         old_H.simplify()
         assert old_H.compare(new_H)
 
+    def test_simplify_while_queueing(self):
+        """Tests that simplifying a Hamiltonian in a tape context
+        queues the simplified Hamiltonian."""
+
+        with qml.tape.QuantumTape() as tape:
+            a = qml.PauliX(wires=0)
+            b = qml.PauliY(wires=1)
+            c = qml.Identity(wires=2)
+            d = b @ c
+            H = qml.Hamiltonian([1.0, 2.0], [a, d])
+            H.simplify()
+
+        # check that H is simplified
+        assert H.ops == [a, b]
+        # check that the simplified Hamiltonian is in the queue
+        assert H in tape._queue
+
     def test_data(self):
         """Tests the obs_data method"""