Repository: PennyLaneAI/pennylane (python)

Issue:
shape method for Interferometer
### Feature details

The Interferometer template does not have a `shape` method to give out the required parameters' shapes.
It would be helpful to have this method as three parameters are required that have two different shapes.

### Implementation

The interferometer template is not defined as a class, so probably, it'll need restructuring before this can be implemented.

### How important would you say this feature is?

1: Not important. Would be nice to have.

### Additional information

_No response_

Pull request:
Adds shape method to Interferometer template
**Context:**
Interferometer did not have a shape method
**Description of the Change:**
Defines Interferometer as a class and adds a shape method
**Benefits:**
Interferometer is now a class with shape method
**Possible Drawbacks:**
possibly none
**Related GitHub Issues:**
Closes #1945 

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index a190138a4..3f4fd8e8c 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -6,6 +6,9 @@
 
 <h3>Improvements</h3>
 
+* Interferometer is now a class with `shape` method.
+  [(#1946)](https://github.com/PennyLaneAI/pennylane/pull/1946)
+
 <h3>Breaking changes</h3>
 
 <h3>Bug fixes</h3>
@@ -35,4 +38,4 @@
 
 This release contains contributions from (in alphabetical order):
 
-Olivia Di Matteo, Antal Száva, David Wierichs
+Olivia Di Matteo, Ankit Khandelwal, Antal Száva, David Wierichs
diff --git a/pennylane/templates/layers/cv_neural_net.py b/pennylane/templates/layers/cv_neural_net.py
index 574a8dbbf..46cb9b3e3 100644
--- a/pennylane/templates/layers/cv_neural_net.py
+++ b/pennylane/templates/layers/cv_neural_net.py
@@ -102,6 +102,7 @@ class CVNeuralNetLayers(Operation):
     ):
 
         n_wires = len(wires)
+        # n_if -> theta and phi shape for Interferometer
         n_if = n_wires * (n_wires - 1) // 2
 
         # check that first dimension is the same
@@ -191,6 +192,7 @@ class CVNeuralNetLayers(Operation):
         Returns:
             list[tuple[int]]: list of shapes
         """
+        # n_if -> theta and phi shape for Interferometer
         n_if = n_wires * (n_wires - 1) // 2
 
         shapes = (
@@ -202,7 +204,7 @@ class CVNeuralNetLayers(Operation):
 
         return shapes
 
-    def adjoint(self):
+    def adjoint(self):  # pylint: disable=arguments-differ
         adjoint_op = CVNeuralNetLayers(*self.parameters, wires=self.wires)
         adjoint_op.inverse = not self.inverse
         return adjoint_op
diff --git a/pennylane/templates/subroutines/interferometer.py b/pennylane/templates/subroutines/interferometer.py
index 0ede56eca..42e9fc958 100644
--- a/pennylane/templates/subroutines/interferometer.py
+++ b/pennylane/templates/subroutines/interferometer.py
@@ -19,6 +19,7 @@ import pennylane as qml
 # pylint: disable-msg=too-many-branches,too-many-arguments,protected-access
 from pennylane.ops import Beamsplitter, Rotation
 from pennylane.wires import Wires
+from pennylane.operation import CVOperation, AnyWires
 
 
 def _preprocess(theta, phi, varphi, wires):
@@ -37,15 +38,15 @@ def _preprocess(theta, phi, varphi, wires):
     """
 
     n_wires = len(wires)
-    n_if = n_wires * (n_wires - 1) // 2
+    shape_theta_phi = n_wires * (n_wires - 1) // 2
 
     shape = qml.math.shape(theta)
-    if shape != (n_if,):
-        raise ValueError(f"Theta must be of shape {(n_if,)}; got {shape}.")
+    if shape != (shape_theta_phi,):
+        raise ValueError(f"Theta must be of shape {(shape_theta_phi,)}; got {shape}.")
 
     shape = qml.math.shape(phi)
-    if shape != (n_if,):
-        raise ValueError(f"Phi must be of shape {(n_if,)}; got {shape}.")
+    if shape != (shape_theta_phi,):
+        raise ValueError(f"Phi must be of shape {(shape_theta_phi,)}; got {shape}.")
 
     shape_varphi = qml.math.shape(varphi)
     if shape_varphi != (n_wires,):
@@ -54,7 +55,7 @@ def _preprocess(theta, phi, varphi, wires):
     return shape_varphi
 
 
-def Interferometer(theta, phi, varphi, wires, mesh="rectangular", beamsplitter="pennylane"):
+class Interferometer(CVOperation):
     r"""General linear interferometer, an array of beamsplitters and phase shifters.
 
     For :math:`M` wires, the general interferometer is specified by
@@ -184,53 +185,115 @@ def Interferometer(theta, phi, varphi, wires, mesh="rectangular", beamsplitter="
 
     """
 
-    wires = Wires(wires)
-    M = len(wires)
+    num_wires = AnyWires
+    grad_method = None
 
-    shape_varphi = _preprocess(theta, phi, varphi, wires)
+    def __init__(
+        self,
+        theta,
+        phi,
+        varphi,
+        wires,
+        mesh="rectangular",
+        beamsplitter="pennylane",
+        do_queue=True,
+        id=None,
+    ):
 
-    with qml.tape.OperationRecorder() as rec:
+        wires = Wires(wires)
+        self.shape_varphi = _preprocess(theta, phi, varphi, wires)
 
-        if M == 1:
-            # the interferometer is a single rotation
-            Rotation(varphi[0], wires=wires[0])
-        else:
-            n = 0  # keep track of free parameters
+        super().__init__(
+            theta, phi, varphi, mesh, beamsplitter, wires=wires, do_queue=do_queue, id=id
+        )
 
-            if mesh == "rectangular":
-                # Apply the Clements beamsplitter array
-                # The array depth is N
-                for l in range(M):
-                    for k, (w1, w2) in enumerate(zip(wires[:-1], wires[1:])):
-                        # skip even or odd pairs depending on layer
-                        if (l + k) % 2 != 1:
+    def expand(self):
+
+        wires = Wires(self.wires)
+        M = len(wires)
+
+        theta = self.parameters[0]
+        phi = self.parameters[1]
+        varphi = self.parameters[2]
+        mesh = self.parameters[3]
+        beamsplitter = self.parameters[4]
+
+        with qml.tape.QuantumTape() as tape:
+
+            if M == 1:
+                # the interferometer is a single rotation
+                Rotation(varphi[0], wires=wires[0])
+            else:
+                n = 0  # keep track of free parameters
+
+                if mesh == "rectangular":
+                    # Apply the Clements beamsplitter array
+                    # The array depth is N
+                    for l in range(M):
+                        for k, (w1, w2) in enumerate(zip(wires[:-1], wires[1:])):
+                            # skip even or odd pairs depending on layer
+                            if (l + k) % 2 != 1:
+                                if beamsplitter == "clements":
+                                    Rotation(phi[n], wires=Wires(w1))
+                                    Beamsplitter(theta[n], 0, wires=Wires([w1, w2]))
+                                elif beamsplitter == "pennylane":
+                                    Beamsplitter(theta[n], phi[n], wires=Wires([w1, w2]))
+                                else:
+                                    raise ValueError(
+                                        f"did not recognize beamsplitter {beamsplitter}"
+                                    )
+                                n += 1
+
+                elif mesh == "triangular":
+                    # apply the Reck beamsplitter array
+                    # The array depth is 2*N-3
+                    for l in range(2 * M - 3):
+                        for k in range(abs(l + 1 - (M - 1)), M - 1, 2):
                             if beamsplitter == "clements":
-                                Rotation(phi[n], wires=Wires(w1))
-                                Beamsplitter(theta[n], 0, wires=Wires([w1, w2]))
+                                Rotation(phi[n], wires=wires[k])
+                                Beamsplitter(theta[n], 0, wires=wires.subset([k, k + 1]))
                             elif beamsplitter == "pennylane":
-                                Beamsplitter(theta[n], phi[n], wires=Wires([w1, w2]))
+                                Beamsplitter(theta[n], phi[n], wires=wires.subset([k, k + 1]))
                             else:
-                                raise ValueError(f"did not recognize beamsplitter {beamsplitter}")
+                                raise ValueError(f"did not recognize beamsplitter {beamsplitter} ")
                             n += 1
-
-            elif mesh == "triangular":
-                # apply the Reck beamsplitter array
-                # The array depth is 2*N-3
-                for l in range(2 * M - 3):
-                    for k in range(abs(l + 1 - (M - 1)), M - 1, 2):
-                        if beamsplitter == "clements":
-                            Rotation(phi[n], wires=wires[k])
-                            Beamsplitter(theta[n], 0, wires=wires.subset([k, k + 1]))
-                        elif beamsplitter == "pennylane":
-                            Beamsplitter(theta[n], phi[n], wires=wires.subset([k, k + 1]))
-                        else:
-                            raise ValueError(f"did not recognize beamsplitter {beamsplitter} ")
-                        n += 1
-            else:
-                raise ValueError(f"did not recognize mesh {mesh}")
-
-            # apply the final local phase shifts to all modes
-            for i in range(shape_varphi[0]):
-                act_on = wires[i]
-                Rotation(varphi[i], wires=act_on)
-    return rec.queue
+                else:
+                    raise ValueError(f"did not recognize mesh {mesh}")
+
+                # apply the final local phase shifts to all modes
+                for i in range(self.shape_varphi[0]):
+                    act_on = wires[i]
+                    Rotation(varphi[i], wires=act_on)
+
+        if self.inverse:
+            tape.inv()
+
+        return tape
+
+    @staticmethod
+    def shape(n_wires):
+        r"""Returns a list of shapes for the 3 parameter tensors.
+
+        Args:
+            n_wires (int): number of wires
+
+        Returns:
+            list[tuple[int]]: list of shapes
+        """
+        shape_theta_phi = n_wires * (n_wires - 1) // 2
+
+        shapes = [(shape_theta_phi,)] * 2 + [(n_wires,)]
+
+        return shapes
+
+    def adjoint(self):  # pylint: disable=arguments-differ
+        adjoint_op = Interferometer(
+            theta=self.parameters[0],
+            phi=self.parameters[1],
+            varphi=self.parameters[2],
+            mesh=self.parameters[3],
+            beamsplitter=self.parameters[4],
+            wires=self.wires,
+        )
+        adjoint_op.inverse = not self.inverse
+        return adjoint_op


Test patch:
diff --git a/tests/templates/test_layers/test_cv_neural_net.py b/tests/templates/test_layers/test_cv_neural_net.py
index c285c5b2e..f34ff2c86 100644
--- a/tests/templates/test_layers/test_cv_neural_net.py
+++ b/tests/templates/test_layers/test_cv_neural_net.py
@@ -75,9 +75,17 @@ class TestDecomposition:
         op = qml.CVNeuralNetLayers(*weights, wires=range(n_wires))
         tape = op.expand()
 
-        for i, gate in enumerate(tape.operations):
-            assert gate.name == expected_names[i]
-            assert gate.wires.labels == tuple(expected_wires[i])
+        i = 0
+        for gate in tape.operations:
+            if gate.name != "Interferometer":
+                assert gate.name == expected_names[i]
+                assert gate.wires.labels == tuple(expected_wires[i])
+                i = i + 1
+            else:
+                for gate_inter in gate.expand().operations:
+                    assert gate_inter.name == expected_names[i]
+                    assert gate_inter.wires.labels == tuple(expected_wires[i])
+                    i = i + 1
 
     def test_custom_wire_labels(self, tol):
         """Test that template can deal with non-numeric, nonconsecutive wire labels."""
@@ -124,7 +132,7 @@ class TestInputs:
             circuit()
 
     def test_cvqnn_layers_exception_second_dim(self):
-        """Check exception if weong dimension of weights"""
+        """Check exception if wrong dimension of weights"""
         shapes = expected_shapes(1, 2)
         weights = [np.random.random(shape) for shape in shapes[:-1]]
         weights += [np.random.random((1, shapes[-1][1] - 1))]
diff --git a/tests/templates/test_subroutines/test_interferometer.py b/tests/templates/test_subroutines/test_interferometer.py
index 6f7f0a2be..95a0a0584 100644
--- a/tests/templates/test_subroutines/test_interferometer.py
+++ b/tests/templates/test_subroutines/test_interferometer.py
@@ -61,17 +61,15 @@ class TestInterferometer:
         phi = [0.234]
         varphi = [0.42342, 0.1121]
 
-        with qml.tape.OperationRecorder() as rec_rect:
-            qml.Interferometer(
-                theta, phi, varphi, mesh="rectangular", beamsplitter="clements", wires=wires
-            )
+        op_rect = qml.Interferometer(
+            theta, phi, varphi, mesh="rectangular", beamsplitter="clements", wires=wires
+        )
 
-        with qml.tape.OperationRecorder() as rec_tria:
-            qml.Interferometer(
-                theta, phi, varphi, mesh="triangular", beamsplitter="clements", wires=wires
-            )
+        op_tria = qml.Interferometer(
+            theta, phi, varphi, mesh="triangular", beamsplitter="clements", wires=wires
+        )
 
-        for rec in [rec_rect, rec_tria]:
+        for rec in [op_rect.expand(), op_tria.expand()]:
             assert len(rec.queue) == 4
 
             assert isinstance(rec.queue[0], qml.Rotation)
@@ -90,8 +88,8 @@ class TestInterferometer:
         """Test that a one mode interferometer correctly gives a rotation gate"""
         varphi = [0.42342]
 
-        with qml.tape.OperationRecorder() as rec:
-            qml.Interferometer(theta=[], phi=[], varphi=varphi, wires=0)
+        op = qml.Interferometer(theta=[], phi=[], varphi=varphi, wires=0)
+        rec = op.expand()
 
         assert len(rec.queue) == 1
         assert isinstance(rec.queue[0], qml.Rotation)
@@ -107,8 +105,8 @@ class TestInterferometer:
         phi = [0.234]
         varphi = [0.42342, 0.1121]
 
-        with qml.tape.OperationRecorder() as rec:
-            qml.Interferometer(theta, phi, varphi, wires=wires)
+        op = qml.Interferometer(theta, phi, varphi, wires=wires)
+        rec = op.expand()
 
         isinstance(rec.queue[0], qml.Beamsplitter)
         assert rec.queue[0].parameters == theta + phi
@@ -129,8 +127,8 @@ class TestInterferometer:
         phi = [0.234]
         varphi = [0.42342, 0.1121]
 
-        with qml.tape.OperationRecorder() as rec:
-            qml.Interferometer(theta, phi, varphi, mesh="triangular", wires=wires)
+        op = qml.Interferometer(theta, phi, varphi, mesh="triangular", wires=wires)
+        rec = op.expand()
 
         assert len(rec.queue) == 3
 
@@ -152,27 +150,40 @@ class TestInterferometer:
         phi = [0.234, 0.324, 0.234]
         varphi = [0.42342, 0.234, 0.1121]
 
-        with qml.tape.OperationRecorder() as rec_rect:
-            qml.Interferometer(theta, phi, varphi, wires=wires)
+        op_rect = qml.Interferometer(theta, phi, varphi, wires=wires, mesh="rectangular")
+        op_tria = qml.Interferometer(theta, phi, varphi, wires=wires, mesh="triangular")
+
+        # Test rectangular mesh
+        rec = op_rect.expand()
+        assert len(rec.queue) == 6
+
+        expected_bs_wires = [[0, 1], [1, 2], [0, 1]]
+
+        for idx, op in enumerate(rec.queue[:3]):
+            assert isinstance(op, qml.Beamsplitter)
+            assert op.parameters == [theta[idx], phi[idx]]
+            assert op.wires == Wires(expected_bs_wires[idx])
 
-        with qml.tape.OperationRecorder() as rec_tria:
-            qml.Interferometer(theta, phi, varphi, wires=wires)
+        for idx, op in enumerate(rec.queue[3:]):
+            assert isinstance(op, qml.Rotation)
+            assert op.parameters == [varphi[idx]]
+            assert op.wires == Wires([idx])
 
-        for rec in [rec_rect, rec_tria]:
-            # test both meshes (both give identical results for the 3 mode case).
-            assert len(rec.queue) == 6
+        # Test triangular mesh
+        rec = op_tria.expand()
+        assert len(rec.queue) == 6
 
-            expected_bs_wires = [[0, 1], [1, 2], [0, 1]]
+        expected_bs_wires = [[1, 2], [0, 1], [1, 2]]
 
-            for idx, op in enumerate(rec_rect.queue[:3]):
-                assert isinstance(op, qml.Beamsplitter)
-                assert op.parameters == [theta[idx], phi[idx]]
-                assert op.wires == Wires(expected_bs_wires[idx])
+        for idx, op in enumerate(rec.queue[:3]):
+            assert isinstance(op, qml.Beamsplitter)
+            assert op.parameters == [theta[idx], phi[idx]]
+            assert op.wires == Wires(expected_bs_wires[idx])
 
-            for idx, op in enumerate(rec.queue[3:]):
-                assert isinstance(op, qml.Rotation)
-                assert op.parameters == [varphi[idx]]
-                assert op.wires == Wires([idx])
+        for idx, op in enumerate(rec.queue[3:]):
+            assert isinstance(op, qml.Rotation)
+            assert op.parameters == [varphi[idx]]
+            assert op.wires == Wires([idx])
 
     def test_four_mode_rect(self, tol):
         """Test that a 4 mode interferometer using rectangular mesh gives the correct gates"""
@@ -183,8 +194,8 @@ class TestInterferometer:
         phi = [0.234, 0.324, 0.234, 1.453, 1.42341, -0.534]
         varphi = [0.42342, 0.234, 0.4523, 0.1121]
 
-        with qml.tape.OperationRecorder() as rec:
-            qml.Interferometer(theta, phi, varphi, wires=wires)
+        op = qml.Interferometer(theta, phi, varphi, wires=wires)
+        rec = op.expand()
 
         assert len(rec.queue) == 10
 
@@ -209,8 +220,8 @@ class TestInterferometer:
         phi = [0.234, 0.324, 0.234, 1.453, 1.42341, -0.534]
         varphi = [0.42342, 0.234, 0.4523, 0.1121]
 
-        with qml.tape.OperationRecorder() as rec:
-            qml.Interferometer(theta, phi, varphi, mesh="triangular", wires=wires)
+        op = qml.Interferometer(theta, phi, varphi, wires=wires, mesh="triangular")
+        rec = op.expand()
 
         assert len(rec.queue) == 10
 
@@ -281,3 +292,18 @@ class TestInterferometer:
         with pytest.raises(ValueError, match=r"Varphi must be of shape \(4,\)"):
             wrong_varphi = np.array([0.1, 0.2])
             circuit(theta, phi, wrong_varphi)
+
+    @pytest.mark.parametrize(
+        "n_layers, n_wires, expected",
+        [
+            (2, 3, [(3,), (3,), (3,)]),
+            (3, 1, [(0,), (0,), (1,)]),
+            (4, 2, [(1,), (1,), (2,)]),
+        ],
+    )
+    def test_shapes(self, n_layers, n_wires, tol, expected):
+        """Test that the shape method returns the correct shapes for
+        the weight tensors"""
+
+        shapes = qml.Interferometer.shape(n_wires)
+        assert np.allclose(shapes, expected, atol=tol, rtol=0)
diff --git a/tests/transforms/test_adjoint.py b/tests/transforms/test_adjoint.py
index 5a0909ddd..5698f0b26 100644
--- a/tests/transforms/test_adjoint.py
+++ b/tests/transforms/test_adjoint.py
@@ -150,8 +150,8 @@ class TestOutsideOfQueuing:
     def test_cv_template_adjoint(self):
         """Test that the adjoint correctly inverts CV templates"""
         template, par, wires = qml.templates.Interferometer, [[1], [0.3], [0.2, 0.3]], [2, 3]
-        result = qml.adjoint(template)(*par, wires=wires)
-        expected_ops = template(*par, wires=wires)
+        result = qml.adjoint(template)(*par, wires=wires).expand().operations
+        expected_ops = template(*par, wires=wires).expand().operations
 
         for o1, o2 in zip(result, reversed(expected_ops)):
             o2 = o2.adjoint()
diff --git a/tests/transforms/test_control.py b/tests/transforms/test_control.py
index b86e9be47..45f567806 100644
--- a/tests/transforms/test_control.py
+++ b/tests/transforms/test_control.py
@@ -124,8 +124,8 @@ class TestAdjointOutsideQueuing:
         adjoint_of_controlled_op = qml.ctrl(op, control=control_wires)(*par, wires=wires).adjoint()
 
         assert adjoint_of_controlled_op.control_wires == control_wires
-        res_ops = adjoint_of_controlled_op.subtape.operations
-        expected = qml.adjoint(op)(*par, wires=wires)
+        res_ops = adjoint_of_controlled_op.subtape.operations[0].expand().operations
+        expected = qml.adjoint(op)(*par, wires=wires).expand().operations
 
         for op1, op2 in zip(res_ops, expected):
             assert type(op1) == type(op2)