Repository: PennyLaneAI/pennylane
Pull request #2305 (2022-03-11 09:37:46)

Add `qml.generator(op)` backwards compatibility
**Context:** 

- In previous versions of PennyLane, `Operator.generator` was a property, that returned a list of the form `[type, float]` or `[array, float]`.

- In the current master, this has been changed. `Operator.generator` is now a _method_, and it returns an Operator _instance_.

- In #2256, a high-level `qml.generator()` function was added, intended to be the user-facing interface to `Operator.generator()`.

**Description of the Change:** A small modification to `qml.generator()` has been made, to ensure it continues to work with old-style operators that define the `Operator.generator` property. In addition, a deprecation warning is raised.

**Benefits:** `qml.generator()` should work with old-style operators.

**Possible Drawbacks:** This backwards compatibility is constrained, in that it requires developers start using `qml.generator`, a new function!

**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/2305?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@78447d0`). [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/2305/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/2305?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    #2305   +/-   ##
==============================================
  Coverage               ?   96.71%           
==============================================
  Files                  ?      242           
  Lines                  ?    19154           
  Branches               ?        0           
==============================================
  Hits                   ?    18525           
  Misses                 ?      629           
  Partials               ?        0           
```



------

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

antalszava: Hi @josh146 @dwierichs,

We have an example in `operation.py` for the `parameter_frequencies` method that fails to execute:
```python
>>> op = qml.ControlledPhaseShift(0.1, wires=[0, 1])
>>> op.parameter_frequencies
[(1,)]
>>> gen_eigvals = tuple(np.linalg.eigvals(op.generator[0] * op.generator[1]))
>>> qml.gradients.eigvals_to_frequencies(gen_eigvals)
```
```
TypeError: 'method' object is not subscriptable
```
When switching to `qml.generator` and using this branch, still getting an error though:
```
>>> gen_eigvals = tuple(np.linalg.eigvals(qml.generator(op)[0] * qml.generator(op)[1]))
>>> qml.gradients.eigvals_to_frequencies(gen_eigvals)
```
```
LinAlgError: 0-dimensional array given. Array must be at least two-dimensional
```
Maybe I'm missing something here?
dwierichs: `qml.generator(op)[0] * qml.generator(op)[1]` creates a `qml.Hamiltonian`, which `np.linalg.eigvals` can not parse. I tried inserting `qml.matrix` to make this work, but that tries to use a sparse representation, which is not implemented for projectors, apparently (this is definitely a to do, as projectors of computational basis states are archetypes of sparse matrices). @josh146 what would be the best way to obtain the matrix of `qml.generator(op)[0] * qml.generator(op)[1]`
josh146: @antalszava where is the failing example, is in a docstring? 🤔 

The following should work:

```pycon
>>> op = qml.ControlledPhaseShift(0.1, wires=[0, 1])
>>> gen = qml.generator(op, format="observable")
>>> gen_eigvals = qml.eigvals(gen)
>>> qml.gradients.eigvals_to_frequencies(tuple(gen_eigvals))
(1.0,)
```

Unfortunately, this doesn't, as @dwierichs notes:

```pycon
>>> op = qml.ControlledPhaseShift(0.1, wires=[0, 1])
>>> gen = qml.generator(op, format="observable")
>>> gen_eigvals = qml.eigvals(gen)
ValueError: Can only sparsify Hamiltonians whose constituent observables consist of (tensor products of) single-qubit operators; got Projector([1, 1], wires=[0, 1]).
```

This is because `1.0 * Projector()` becomes a Hamiltonian, and the Hamiltonian class doesn't know how to represent itself in matrix form if it contains `Projector` 😬 This sounds like a separate bug we need to fix relating to `Projector` and the `Hamiltonian` class.

antalszava: > where is the failing example, is in a docstring? thinking

Docstring of `parameter_frequencies`. We're advertising that feature with v0.22.0. Should the example be removed for now for safety until the bug is being worked on?
josh146: > Docstring of parameter_frequencies. We're advertising that feature with v0.22.0. Should the example be removed for now for safety until the bug is being worked on?

Ah! Great catch Antal, this should be updated. I think the docstring was written before `op.generator` became a method `op.generator()`.

Patch:
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 55d299511..a51955bc8 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1291,9 +1291,10 @@ class Operation(Operator):
         >>> op = qml.ControlledPhaseShift(0.1, wires=[0, 1])
         >>> op.parameter_frequencies
         [(1,)]
-        >>> gen_eigvals = tuple(np.linalg.eigvals(op.generator[0] * op.generator[1]))
-        >>> qml.gradients.eigvals_to_frequencies(gen_eigvals)
-        (tensor(1., requires_grad=True),)
+        >>> gen = qml.generator(op, format="observable")
+        >>> gen_eigvals = qml.eigvals(gen)
+        >>> qml.gradients.eigvals_to_frequencies(tuple(gen_eigvals))
+        (1.0,)
 
         For more details on this relationship, see :func:`.eigvals_to_frequencies`.
         """
diff --git a/pennylane/ops/functions/generator.py b/pennylane/ops/functions/generator.py
index 47c685b1a..431c8a4f5 100644
--- a/pennylane/ops/functions/generator.py
+++ b/pennylane/ops/functions/generator.py
@@ -15,6 +15,11 @@
 This module contains the qml.generator function.
 """
 # pylint: disable=protected-access
+import inspect
+import warnings
+
+import numpy as np
+
 import pennylane as qml
 
 
@@ -96,6 +101,28 @@ def _generator_prefactor(gen, op):
     return obs, prefactor
 
 
+def _generator_backcompatibility(op):
+    r"""Preserve backwards compatibility behaviour for PennyLane
+    versions <=0.22, where generators returned List[type or ndarray, float].
+    This function raises a deprecation warning, and converts to the new
+    format where an instantiated Operator is returned."""
+    warnings.warn(
+        "The Operator.generator property is deprecated. Please update the operator so that "
+        "\n\t1. Operator.generator() is a method, and"
+        "\n\t2. Operator.generator() returns an Operator instance representing the operator.",
+        UserWarning,
+    )
+    gen = op.generator
+
+    if inspect.isclass(gen[0]):
+        return gen[1] * gen[0](wires=op.wires)
+
+    if isinstance(gen[0], np.ndarray) and len(gen[0].shape) == 2:
+        return gen[1] * qml.Hermitian(gen[0], wires=op.wires)
+
+    raise qml.operation.GeneratorUndefinedError
+
+
 @qml.op_transform
 def generator(op, format="prefactor"):
     r"""Returns the generator of an operation.
@@ -159,7 +186,12 @@ def generator(op, format="prefactor"):
     if op.num_params != 1:
         raise ValueError(f"Operation {op.name} is not written in terms of a single parameter")
 
-    gen = op.generator()
+    try:
+        gen = op.generator()
+    except TypeError:
+        # For backwards compatibility with PennyLane
+        # versions <=0.22, assume op.generator is a property
+        gen = _generator_backcompatibility(op)
 
     if not isinstance(gen, qml.operation.Observable):
         raise qml.QuantumFunctionError(


Test patch:
diff --git a/tests/ops/functions/test_generator.py b/tests/ops/functions/test_generator.py
index ab3fc72f8..a21c7c1b5 100644
--- a/tests/ops/functions/test_generator.py
+++ b/tests/ops/functions/test_generator.py
@@ -136,6 +136,47 @@ class TestValidation:
             qml.generator(qml.RX, format=None)(0.5, wires=0)
 
 
+class TestBackwardsCompatibility:
+    """Test that operators that provide the old style generator property
+    continue to work with a deprecation warning."""
+
+    def test_return_class(self):
+        """Test that an old-style Operator that has a generator property,
+        and that returns a list containing (a) operator class and (b) prefactor,
+        continues to work but also raises a deprecation warning."""
+
+        class DeprecatedClassOp(CustomOp):
+            generator = [qml.PauliX, -0.6]
+
+        op = DeprecatedClassOp(0.5, wires="a")
+
+        with pytest.warns(UserWarning, match=r"The Operator\.generator property is deprecated"):
+            gen, prefactor = qml.generator(op)
+
+        assert isinstance(gen, qml.operation.Operator)
+        assert prefactor == -0.6
+        assert gen.name == "PauliX"
+        assert gen.wires.tolist() == ["a"]
+
+    def test_return_array(self):
+        """Test that an old-style Operator that has a generator property,
+        and that returns a list containing (a) array and (b) prefactor,
+        continues to work but also raises a deprecation warning."""
+
+        class DeprecatedClassOp(CustomOp):
+            generator = [np.diag([0, 1]), -0.6]
+
+        op = DeprecatedClassOp(0.5, wires="a")
+
+        with pytest.warns(UserWarning, match=r"The Operator\.generator property is deprecated"):
+            gen, prefactor = qml.generator(op)
+
+        assert isinstance(gen, qml.operation.Operator)
+        assert prefactor == -0.6
+        assert gen.name == "Hermitian"
+        assert gen.wires.tolist() == ["a"]
+
+
 class TestPrefactorReturn:
     """Tests for format="prefactor". This format attempts to isolate a prefactor
     (if possible) from the generator, which is useful if the generator is a Pauli word."""