Repository: PennyLaneAI/pennylane

Issue:
Consequences of removing DiagonalOperation
I have two questions on this:

https://github.com/PennyLaneAI/pennylane/blob/927c4c67a1e1f9c79a5d7d4e89c496fac418657f/pennylane/ops/qubit/attributes.py#L169-L187

1) `IsingZZ` seems to be missing in this list, which maybe shows that the old model of observables being able to declare themselves that they are diagonal was superior over having a centralized list?

2) How can custom `Operation`s declare that they are diagonal now? Should they modify the `pennylane.ops.qubit.attributes.diagonal_in_z_basis`? 


Discussion:
Hey @cvjjm,

Some background: this design decision was part of a larger one, which sets the standard of only having info for the most basic properties built into the operator class. This was necessary because the operator subclasses were a patchwork of tags and attributes that specific applications use, and some applications got deleted in the meantime, leaving legacy code everywhere. Also, if a new application needs certain information, it would have to go through all subclasses to see if it is already defined. Lastly, some properties were defined by the inheritance structure, and others by attributes or boolean functions.

Your questions are very valid though!

1. I cannot find `IsingZZ` being part of the refactor, and since it deleted the `DiagonalOperation` class, this means `IsingZZ` wasn't "marked" as a diagonal op in the past either. So the old system has failed us too :) 

Commenting on that, we know that some ops may not be registered for all possible attributes they belong to, but in some sense it is up to the application developer (i.e. the person who wants fast compiling) to maintain and test the list. It is better to have a clean system that does not exploit the full potential, then a messy one, that doesn't so either...

2. Yes, you can! Quote from the changelog entry:
```
 We can also dynamically add operators to the sets at runtime. This is useful
  for adding custom operations to the attributes such as `composable_rotations`
  and ``self_inverses`` that are used in compilation transforms. For example,
  suppose you have created a new Operation, `MyGate`, which you know to be its
  own inverse. Adding it to the set, like so

  >>> from pennylane.ops.qubits.attributes import self_inverses
  >>> self_inverses.add("MyGate")
```

Once the op class refactor is complete, we will update the developer guide and advertise this more.

Let me know if this makes sense!

Oh, if you'd like to make a PR adding IsingZZ to the attribute, feel free :) Else I'll put it somewhere on my todo list

Fair point, then the old system of marking things as diagonal wasn't any better :-) I am just generally a bit allergic against global variables and keeping information that belong together in different files...

As you can see, I opened a draft PR, but after the innocent change I made the test fail and I didn't have time to look into why. 

While browsing attributes.py, I also noticed that Rot was marked as composable, but looking at its decomposition, I don't see how that could be correct. Am I missing something?
Hi @cvjjm! I see that the PR is still a draft so I'm guessing that you'll look at the failing test later on right? If you get stuck with the failing test and need help you can let us know.

As for the Rot, can you elaborate on what might be wrong, or what you expected to see?
Yes, to be honest I don't understand the error message in the failing test and it would probably take me quite some time why my innocent looking change causes this. If you have an idea what is going on any help is greatly appreciated.

With respect to `Rot`. As far as I understand the doc string of `composable_rotations`, all operations in this list are meant to be composable in the sense that `Gate(a, wires=wires) Gate(b, wires=wires) = Gate(a+b, wires=wires)`. For multi parameter gates I suppose what is meant is `Gate(*a, wires=wires) Gate(*b, wires=wires) = Gate(*(a+b), wires=wires)`. Looking at the [decomposition](https://pennylane.readthedocs.io/en/stable/code/api/pennylane.Rot.html) of `Rot` I do not think it satisfies this condition.
Will take a look, thanks for opening the PR.


I actually consider the presence of `Rot` in `composable_rotations` a bug.
@glassnotes Sorry to rope you in, but I wonder if you can quickly comment on the presence of `Rot` in `composable_rotations`?  (See cvjjm's last post)

Resolving patch:
diff --git a/pennylane/ops/qubit/attributes.py b/pennylane/ops/qubit/attributes.py
--- a/pennylane/ops/qubit/attributes.py
+++ b/pennylane/ops/qubit/attributes.py
@@ -107,6 +107,9 @@ def __contains__(self, obj):
         "CRY",
         "CRZ",
         "ControlledPhaseShift",
+        "IsingXX",
+        "IsingYY",
+        "IsingZZ",
         "Rot",
     ]
 )
@@ -116,6 +119,10 @@ def __contains__(self, obj):
 For example, ``qml.RZ`` is a composable rotation. Applying ``qml.RZ(0.1,
 wires=0)`` followed by ``qml.RZ(0.2, wires=0)`` is equivalent to performing
 a single rotation ``qml.RZ(0.3, wires=0)``.
+
+An example for an alternative accumulation is the ``qml.Rot`` gate: although the three
+angles it takes do not fulfil the composable property, the gate implements a rotation around an
+axis by an effective angle which does.
 """
 
 has_unitary_generator = Attribute(
@@ -178,10 +185,17 @@ def __contains__(self, obj):
         "ControlledPhaseShift",
         "MultiRZ",
         "CRZ",
+        "IsingZZ",
     ]
 )
 """Attribute: Operations that are diagonal in the computational basis.
 
 For such operations, the eigenvalues provide all necessary information to
 construct the matrix representation in the computational basis.
+
+Note: Currently all gates with this attribute need
+to explicitly define an eigenvalue representation.
+The reason is that if this method is missing, eigenvalues are computed from the matrix
+representation using ``np.linalg.eigvals``, which fails for some tensor types that the matrix
+may be cast in on backpropagation devices.
 """
diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py
--- a/pennylane/ops/qubit/parametric_ops.py
+++ b/pennylane/ops/qubit/parametric_ops.py
@@ -1585,6 +1585,18 @@ def _matrix(cls, *params):
 
         return qml.math.diag([neg_phase, pos_phase, pos_phase, neg_phase])
 
+    @classmethod
+    def _eigvals(cls, *params):
+        phi = params[0]
+
+        if qml.math.get_interface(phi) == "tensorflow":
+            phi = qml.math.cast_like(phi, 1j)
+
+        pos_phase = qml.math.exp(1.0j * phi / 2)
+        neg_phase = qml.math.exp(-1.0j * phi / 2)
+
+        return qml.math.stack([neg_phase, pos_phase, pos_phase, neg_phase])
+
     def adjoint(self):
         (phi,) = self.parameters
         return IsingZZ(-phi, wires=self.wires)


Test patch:
diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py
--- a/tests/ops/qubit/test_parametric_ops.py
+++ b/tests/ops/qubit/test_parametric_ops.py
@@ -345,6 +345,7 @@ def get_expected(theta):
     def test_isingzz(self, tol):
         """Test that the IsingZZ operation is correct"""
         assert np.allclose(qml.IsingZZ._matrix(0), np.identity(4), atol=tol, rtol=0)
+        assert np.allclose(qml.IsingZZ._eigvals(0), np.diagonal(np.identity(4)), atol=tol, rtol=0)
 
         def get_expected(theta):
             neg_imag = np.exp(-1j * theta / 2)
@@ -356,9 +357,31 @@ def get_expected(theta):
 
         param = np.pi / 2
         assert np.allclose(qml.IsingZZ._matrix(param), get_expected(param), atol=tol, rtol=0)
+        assert np.allclose(
+            qml.IsingZZ._eigvals(param), np.diagonal(get_expected(param)), atol=tol, rtol=0
+        )
 
         param = np.pi
         assert np.allclose(qml.IsingZZ._matrix(param), get_expected(param), atol=tol, rtol=0)
+        assert np.allclose(
+            qml.IsingZZ._eigvals(param), np.diagonal(get_expected(param)), atol=tol, rtol=0
+        )
+
+    def test_isingzz_matrix_tf(self, tol):
+        """Tests the matrix representation for IsingZZ for tensorflow, since the method contains
+        different logic for this framework"""
+        tf = pytest.importorskip("tensorflow")
+
+        def get_expected(theta):
+            neg_imag = np.exp(-1j * theta / 2)
+            plus_imag = np.exp(1j * theta / 2)
+            expected = np.array(
+                np.diag([neg_imag, plus_imag, plus_imag, neg_imag]), dtype=np.complex128
+            )
+            return expected
+
+        param = tf.Variable(np.pi)
+        assert np.allclose(qml.IsingZZ._matrix(param), get_expected(np.pi), atol=tol, rtol=0)
 
     def test_Rot(self, tol):
         """Test arbitrary single qubit rotation is correct"""