Repository: PennyLaneAI/pennylane
Pull request #3875 (2023-03-06 22:00:30)

Add `has_generator` property to operation.
**Context:**
Currently, we have the function `qml.operation.has_gen` that checks via a try-except clause whether the generator of an operation may be constructed. This can be very slow, given that we only want to know whether there is a generator implemented, which even is static class information (except for a few symbolic ops).

**Description of the Change:**
This PR adds `Operation.has_generator` in order to make a faster check available to `qml.operation.has_gen`.
It follows the implementation of the other `has_..` attributes, like `has_matrix` or `has_decomposition`.

**Benefits:**
Faster checks via `qml.operation.has_gen`

**Possible Drawbacks:**
Code to be maintained. This should be a minor drawback, given that there are only a few operations that have explicit `has_generator` additions.

**Related GitHub Issues:**
This will make #3874 feasible.


codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3875](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (4928e02) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/295d2c9341d6acffe9dce53d2b7ac7a61f9f9ed2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (295d2c9) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 4928e02 differs from pull request most recent head e2a76c8. Consider uploading reports for the commit e2a76c8 to get more accurate results

```diff
@@            Coverage Diff             @@
##           master    #3875      +/-   ##
==========================================
- Coverage   99.71%   99.70%   -0.01%     
==========================================
  Files         340      340              
  Lines       29561    29581      +20     
==========================================
+ Hits        29476    29495      +19     
- Misses         85       86       +1     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/operation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `97.23% <100.00%> (-0.13%)` | :arrow_down: |
| [pennylane/ops/op\_math/adjoint.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2Fkam9pbnQucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/controlled.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2NvbnRyb2xsZWQucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/evolution.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2V2b2x1dGlvbi5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/exp.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2V4cC5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/pow.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3875?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL3Bvdy5weQ==) | `99.31% <100.00%> (+0.01%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

dwierichs: Thanks for the nice summary of the situation regarding the class attribute :100:

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index be00e47a5..e6d2b378f 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -14,6 +14,10 @@
 
 <h3>Improvements</h3>
 
+* `Operator` now has a `has_generator` attribute that returns whether or not the operator
+  has a generator defined. It is used in `qml.operation.has_gen`, improving its performance.
+  [(#3875)](https://github.com/PennyLaneAI/pennylane/pull/3875)
+
 * The custom JVP rules in PennyLane now also support non-scalar and mixed-shape tape parameters as
   well as multi-dimensional tape return types, like broadcasted `qml.probs`, for example.
   [(#3766)](https://github.com/PennyLaneAI/pennylane/pull/3766)
@@ -34,6 +38,10 @@
 
 <h3>Breaking changes</h3>
 
+* An operation that implements a custom `generator` method, but does not always return a valid generator, also has
+  to implement a `has_generator` property that reflects in which scenarios a generator will be returned.
+  [(#3875)](https://github.com/PennyLaneAI/pennylane/pull/3875)
+ 
 * Trainable parameters for the Jax interface are the parameters that are `JVPTracer`, defined by setting
   `argnums`. Previously, all JAX tracers, including those used for JIT compilation, were interpreted to be trainable.
   [(#3697)](https://github.com/PennyLaneAI/pennylane/pull/3697)
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 0cfeed70f..553a8983c 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1269,6 +1269,15 @@ class Operator(abc.ABC):
             *self.parameters, wires=self.wires, **self.hyperparameters
         )
 
+    # pylint: disable=no-self-argument, comparison-with-callable
+    @classproperty
+    def has_generator(cls):
+        r"""Bool: Whether or not the Operator returns a defined generator.
+
+        Note: Child classes may have this as an instance property instead of as a class property.
+        """
+        return cls.generator != Operator.generator
+
     def generator(self):  # pylint: disable=no-self-use
         r"""Generator of an operator that is in single-parameter-form.
 
@@ -2712,11 +2721,12 @@ def not_tape(obj):
 @qml.BooleanFn
 def has_gen(obj):
     """Returns ``True`` if an operator has a generator defined."""
+    if isinstance(obj, Operator):
+        return obj.has_generator
     try:
         obj.generator()
     except (AttributeError, OperatorPropertyUndefined, GeneratorUndefinedError):
         return False
-
     return True
 
 
@@ -2763,8 +2773,7 @@ def is_trainable(obj):
 
 @qml.BooleanFn
 def defines_diagonalizing_gates(obj):
-    """Returns ``True`` if an operator defines the diagonalizing
-    gates are defined.
+    """Returns ``True`` if an operator defines the diagonalizing gates.
 
     This helper function is useful if the property is to be checked in
     a queuing context, but the resulting gates must not be queued.
diff --git a/pennylane/ops/op_math/adjoint.py b/pennylane/ops/op_math/adjoint.py
index 2b96d1a9b..ef55edf93 100644
--- a/pennylane/ops/op_math/adjoint.py
+++ b/pennylane/ops/op_math/adjoint.py
@@ -369,5 +369,10 @@ class AdjointOperation(Operation):
     def parameter_frequencies(self):
         return self.base.parameter_frequencies
 
+    # pylint: disable=arguments-renamed, invalid-overridden-method
+    @property
+    def has_generator(self):
+        return self.base.has_generator
+
     def generator(self):
         return -1.0 * self.base.generator()
diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py
index 45c1dbaf4..24d5fac02 100644
--- a/pennylane/ops/op_math/controlled.py
+++ b/pennylane/ops/op_math/controlled.py
@@ -499,6 +499,11 @@ class Controlled(SymbolicOp):
         d += [qml.PauliX(w) for w, val in zip(self.control_wires, self.control_values) if not val]
         return d
 
+    # pylint: disable=arguments-renamed, invalid-overridden-method
+    @property
+    def has_generator(self):
+        return self.base.has_generator
+
     def generator(self):
         sub_gen = self.base.generator()
         proj_tensor = operation.Tensor(*(qml.Projector([1], wires=w) for w in self.control_wires))
diff --git a/pennylane/ops/op_math/evolution.py b/pennylane/ops/op_math/evolution.py
index b3d1c312b..c0571134a 100644
--- a/pennylane/ops/op_math/evolution.py
+++ b/pennylane/ops/op_math/evolution.py
@@ -111,6 +111,11 @@ class Evolution(Exp):
             return Evolution(new_base.base, self.param * new_base.scalar)
         return Evolution(new_base, self.param)
 
+    # pylint: disable=arguments-renamed, invalid-overridden-method
+    @property
+    def has_generator(self):
+        return not np.real(self.coeff)
+
     def generator(self):
         r"""Generator of an operator that is in single-parameter-form.
 
diff --git a/pennylane/ops/op_math/exp.py b/pennylane/ops/op_math/exp.py
index a99ddf9cc..7cf3cdd88 100644
--- a/pennylane/ops/op_math/exp.py
+++ b/pennylane/ops/op_math/exp.py
@@ -447,6 +447,11 @@ class Exp(ScalarSymbolicOp, Operation):
             return Exp(new_base.base, self.coeff * new_base.scalar)
         return Exp(new_base, self.coeff)
 
+    # pylint: disable=arguments-renamed, invalid-overridden-method
+    @property
+    def has_generator(self):
+        return self.base.is_hermitian and not np.real(self.coeff)
+
     def generator(self):
         r"""Generator of an operator that is in single-parameter-form.
 
diff --git a/pennylane/ops/op_math/pow.py b/pennylane/ops/op_math/pow.py
index 7af8d18a3..4d32b4673 100644
--- a/pennylane/ops/op_math/pow.py
+++ b/pennylane/ops/op_math/pow.py
@@ -316,6 +316,11 @@ class Pow(ScalarSymbolicOp):
         base_eigvals = self.base.eigvals()
         return [value**self.z for value in base_eigvals]
 
+    # pylint: disable=arguments-renamed, invalid-overridden-method
+    @property
+    def has_generator(self):
+        return self.base.has_generator
+
     def generator(self):
         r"""Generator of an operator that is in single-parameter-form.
 


Test patch:
diff --git a/tests/ops/op_math/test_adjoint.py b/tests/ops/op_math/test_adjoint.py
index 4fb5c1115..35e02cbc4 100644
--- a/tests/ops/op_math/test_adjoint.py
+++ b/tests/ops/op_math/test_adjoint.py
@@ -434,6 +434,20 @@ class TestAdjointOperation:
         op = Adjoint(base)
         assert op.base_name == adjoint_base_name
 
+    def test_has_generator_true(self):
+        """Test `has_generator` property carries over when base op defines generator."""
+        base = qml.RX(0.5, 0)
+        op = Adjoint(base)
+
+        assert op.has_generator is True
+
+    def test_has_generator_false(self):
+        """Test `has_generator` property carries over when base op does not define a generator."""
+        base = qml.PauliX(0)
+        op = Adjoint(base)
+
+        assert op.has_generator is False
+
     def test_generator(self):
         """Assert that the generator of an Adjoint is -1.0 times the base generator."""
         base = qml.RX(1.23, wires=0)
diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py
index a8d09a67a..a0e6a32a7 100644
--- a/tests/ops/op_math/test_controlled.py
+++ b/tests/ops/op_math/test_controlled.py
@@ -408,6 +408,20 @@ class TestMiscMethods:
 
         assert qml.math.allclose(mat_eigvals, sort_eigs)
 
+    def test_has_generator_true(self):
+        """Test `has_generator` property carries over when base op defines generator."""
+        base = qml.RX(0.5, 0)
+        op = Controlled(base, ("b", "c"))
+
+        assert op.has_generator is True
+
+    def test_has_generator_false(self):
+        """Test `has_generator` property carries over when base op does not define a generator."""
+        base = qml.PauliX(0)
+        op = Controlled(base, ("b", "c"))
+
+        assert op.has_generator is False
+
     def test_generator(self):
         """Test that the generator is a tensor product of projectors and the base's generator."""
 
diff --git a/tests/ops/op_math/test_evolution.py b/tests/ops/op_math/test_evolution.py
index f850d6bdd..a956024f7 100644
--- a/tests/ops/op_math/test_evolution.py
+++ b/tests/ops/op_math/test_evolution.py
@@ -47,6 +47,19 @@ class TestEvolution:
 
         assert np.all(op1.matrix() == op2.matrix())
 
+    def test_has_generator_true(self):
+        """Test that has_generator returns True if the coefficient is purely imaginary."""
+        U = Evolution(qml.PauliX(0), 3)
+        assert U.has_generator is True
+
+    def test_has_generator_false(self):
+        """Test that has_generator returns False if the coefficient is not purely imaginary."""
+        U = Evolution(qml.PauliX(0), 3j)
+        assert U.has_generator is False
+
+        U = Evolution(qml.PauliX(0), 0.01 + 2j)
+        assert U.has_generator is False
+
     def test_generator(self):
         U = Evolution(qml.PauliX(0), 3)
         assert U.base == U.generator()
diff --git a/tests/ops/op_math/test_exp.py b/tests/ops/op_math/test_exp.py
index 01261cfdf..c28513970 100644
--- a/tests/ops/op_math/test_exp.py
+++ b/tests/ops/op_math/test_exp.py
@@ -876,6 +876,26 @@ class TestDifferentiation:
         with pytest.raises(GeneratorUndefinedError):
             op.generator()
 
+    def test_has_generator_true(self):
+        """Test that has_generator returns True if the coefficient is
+        purely imaginary and the base is Hermitian."""
+        op = Exp(qml.PauliX(0), 1j)
+        assert op.has_generator is True
+
+    def test_base_not_hermitian_has_generator_false(self):
+        """Test that has_generator returns False if the coefficient is
+        purely imaginary but the base is not Hermitian."""
+        op = Exp(qml.RX(1.23, 0), 1j)
+        assert op.has_generator is False
+
+    def test_real_component_has_generator_false(self):
+        """Test that has_generator returns False if the coefficient is not purely imaginary."""
+        op = Exp(qml.PauliX(0), 3)
+        assert op.has_generator is False
+
+        op = Exp(qml.PauliX(0), 0.01 + 2j)
+        assert op.has_generator is False
+
     def test_real_component_coefficient_generator_undefined(self):
         """Test that Hermitian base operator but real coefficient raises GeneratorUndefinedError"""
         op = Exp(qml.PauliX(0), 1)
diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py
index a822e4d5d..7c0c01e4f 100644
--- a/tests/ops/op_math/test_pow_op.py
+++ b/tests/ops/op_math/test_pow_op.py
@@ -524,6 +524,20 @@ class TestMiscMethods:
 
         assert qml.math.allclose(mat_eigvals, op.eigvals())
 
+    def test_has_generator_true(self):
+        """Test `has_generator` property carries over when base op defines generator."""
+        base = qml.RX(0.5, 0)
+        op = Pow(base, 0.3)
+
+        assert op.has_generator is True
+
+    def test_has_generator_false(self):
+        """Test `has_generator` property carries over when base op does not define a generator."""
+        base = qml.PauliX(0)
+        op = Pow(base, 0.3)
+
+        assert op.has_generator is False
+
     def test_generator(self):
         """Test that the generator is the base's generator multiplied by the power."""
         z = 2.5
diff --git a/tests/test_operation.py b/tests/test_operation.py
index c7b528458..2b2613079 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -451,6 +451,42 @@ class TestOperatorConstruction:
         assert MyOp.has_diagonalizing_gates is False
         assert MyOp(wires=0).has_diagonalizing_gates is False
 
+    def test_has_generator_true(self):
+        """Test `has_generator` property detects overriding of `generator` method."""
+
+        class MyOp(qml.operation.Operator):
+            num_wires = 1
+
+            @staticmethod
+            def generator():
+                return np.eye(2)
+
+        assert MyOp.has_generator is True
+        assert MyOp(wires=0).has_generator is True
+
+    def test_has_generator_true_concrete_op(self):
+        """Test has_generator with a concrete operation (RZ)
+        that does have a generator defined."""
+
+        op = qml.RZ(0.3, 0)
+        assert op.has_generator is True
+
+    def test_has_generator_false(self):
+        """Test `has_generator` property defaults to false if `generator` not overwritten."""
+
+        class MyOp(qml.operation.Operator):
+            num_wires = 1
+
+        assert MyOp.has_generator is False
+        assert MyOp(wires=0).has_generator is False
+
+    def test_has_generator_false_concrete_op(self):
+        """Test has_generator with a concrete operation (Rot)
+        that does not have a generator defined."""
+
+        op = qml.Rot(0.3, 0.2, 0.1, 0)
+        assert op.has_generator is False
+
     @pytest.mark.tf
     @pytest.mark.parametrize("jit_compile", [True, False])
     def test_with_tf_function(self, jit_compile):
diff --git a/tests/transforms/test_tape_expand.py b/tests/transforms/test_tape_expand.py
index 20966db8f..d38f70826 100644
--- a/tests/transforms/test_tape_expand.py
+++ b/tests/transforms/test_tape_expand.py
@@ -149,6 +149,10 @@ class TestExpandMultipar:
         dev = qml.device("default.qubit", wires=3)
 
         class _CRX(qml.CRX):
+            @property
+            def has_generator(self):
+                return False
+
             def generator(self):
                 raise qml.operations.GeneratorUndefinedError()