Repository: PennyLaneAI/pennylane (python)

Issue:
Remove `qml.utils.expand`
### Feature details

It seems that the utility function `qml.utils.expand` is no longer used anywhere as of #2609.
It therefore could be removed. A differentiable, more up-to-date version is available in `qml.operation.expand_matrix`.

### Implementation

_No response_

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

1: Not important. Would be nice to have.

### Additional information

_No response_

Pull request description:
Remove qml.utils.expand and replace with qml.operation.expand_matrix. [unitaryhack]
**Context:**
Removes the utility function `qml.utils.expand` and replaces all references to it with `qml.operation.expand_matrix`.
**Description of the Change:**
- Removes `qml.utils.expand` and its corresponding tests.
- Replaces all references of `qml.utils.expand` with `qml.operation.expand_matrix`.
- Renames TestExpand to TestExpandVector in `test_utils.py` to better reflect the remaining test cases.

**Benefits:**
Removes the utility function `qml.utils.expand` which is no longer used anywhere as of #2609.
**Possible Drawbacks:**
Removes `qml.utils.expand` tests from `test_utils.py`.
**Related GitHub Issues:**
Closes #2637 

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 2942673a7..068935a9a 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -665,6 +665,9 @@
 * The `qml.queuing.Queue` class is now removed.
   [(#2599)](https://github.com/PennyLaneAI/pennylane/pull/2599)
 
+* The `qml.utils.expand` function is now removed; `qml.operation.expand_matrix` should be used instead.
+  [(#2654)](https://github.com/PennyLaneAI/pennylane/pull/2654)
+  
 * The module `qml.gradients.param_shift_hessian` has been renamed to
   `qml.gradients.parameter_shift_hessian` in order to distinguish it from the identically named
   function. Note that the `param_shift_hessian` function is unaffected by this change and can be
@@ -757,5 +760,5 @@ This release contains contributions from (in alphabetical order):
 
 Amintor Dusko, Ankit Khandelwal, Avani Bhardwaj, Chae-Yeun Park, Christian Gogolin, Christina Lee, David Wierichs,
 Edward Jiang, Guillermo Alonso-Linaje, Jay Soni, Juan Miguel Arrazola, Katharine Hyatt, Korbinian Kottmann,
-Maria Schuld, Mikhail Andrenkov, Romain Moyard, Qi Hu, Samuel Banning, Soran Jahangiri, Utkarsh Azad, Antal Száva,
+Maria Schuld, Mason Moreland, Mikhail Andrenkov, Romain Moyard, Qi Hu, Samuel Banning, Soran Jahangiri, Utkarsh Azad, Antal Száva,
 WingCode
diff --git a/pennylane/operation.py b/pennylane/operation.py
index af3f3a038..364cd51e5 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -129,11 +129,6 @@ def expand_matrix(base_matrix, wires, wire_order):
     """Re-express a base matrix acting on a subspace defined by a set of wire labels
     according to a global wire order.
 
-    .. note::
-
-        This function has essentially the same behaviour as :func:`.utils.expand` but is fully
-        differentiable.
-
     Args:
         base_matrix (tensor_like): base matrix to expand
         wires (Iterable): wires determining the subspace that base matrix acts on; a base matrix of
@@ -188,7 +183,6 @@ def expand_matrix(base_matrix, wires, wire_order):
     >>> res.requires_grad
     True
     """
-    # TODO[Maria]: In future we should consider making ``utils.expand`` differentiable and calling it here.
     wire_order = Wires(wire_order)
     n = len(wires)
     shape = qml.math.shape(base_matrix)
diff --git a/pennylane/ops/qubit/parametric_ops.py b/pennylane/ops/qubit/parametric_ops.py
index 20a59a0ab..acbbf8333 100644
--- a/pennylane/ops/qubit/parametric_ops.py
+++ b/pennylane/ops/qubit/parametric_ops.py
@@ -24,9 +24,8 @@ from operator import matmul
 import numpy as np
 
 import pennylane as qml
-from pennylane.operation import AnyWires, Operation
+from pennylane.operation import AnyWires, Operation, expand_matrix
 from pennylane.ops.qubit.non_parametric_ops import PauliX, PauliY, PauliZ, Hadamard
-from pennylane.operation import expand_matrix
 from pennylane.utils import pauli_eigs
 from pennylane.wires import Wires
 
diff --git a/pennylane/utils.py b/pennylane/utils.py
index 355dd0716..d3e1d35e7 100644
--- a/pennylane/utils.py
+++ b/pennylane/utils.py
@@ -21,6 +21,7 @@ import functools
 import inspect
 import itertools
 import numbers
+import warnings
 from operator import matmul
 
 import numpy as np
@@ -29,6 +30,20 @@ import scipy
 import pennylane as qml
 
 
+def __getattr__(name):
+    # for more information on overwriting `__getattr__`, see https://peps.python.org/pep-0562/
+    if name == "expand":
+        warning_string = (
+            "qml.utils.expand is deprecated; using qml.operation.expand_matrix instead."
+        )
+        warnings.warn(warning_string, UserWarning)
+        return qml.operation.expand_matrix
+    try:
+        return globals()[name]
+    except KeyError as e:
+        raise AttributeError from e
+
+
 def decompose_hamiltonian(H, hide_identity=False, wire_order=None):
     r"""Decomposes a Hermitian matrix into a linear combination of Pauli operators.
 
@@ -330,77 +345,6 @@ def pauli_eigs(n):
     return np.concatenate([pauli_eigs(n - 1), -pauli_eigs(n - 1)])
 
 
-def expand(matrix, original_wires, expanded_wires):
-    r"""Expand an operator matrix to more wires.
-
-    .. note::
-
-        This function has essentially the same behaviour as :func:`.operation.expand_matrix`, but is not
-        fully differentiable.
-
-
-    Args:
-        matrix (array): :math:`2^n \times 2^n` matrix where n = len(original_wires).
-        original_wires (Sequence[int]): original wires of matrix
-        expanded_wires (Union[Sequence[int], int]): expanded wires of matrix, can be shuffled.
-            If a single int m is given, corresponds to list(range(m))
-
-    Returns:
-        array: :math:`2^m \times 2^m` matrix where m = len(expanded_wires).
-    """
-    if isinstance(expanded_wires, numbers.Integral):
-        expanded_wires = list(range(expanded_wires))
-
-    N = len(original_wires)
-    M = len(expanded_wires)
-    D = M - N
-
-    if not set(expanded_wires).issuperset(original_wires):
-        raise ValueError("Invalid target subsystems provided in 'original_wires' argument.")
-
-    if qml.math.shape(matrix) != (2**N, 2**N):
-        raise ValueError(
-            "Matrix parameter must be of size (2**len(original_wires), 2**len(original_wires))"
-        )
-
-    dims = [2] * (2 * N)
-    tensor = qml.math.reshape(matrix, dims)
-
-    interface = qml.math.get_interface(tensor)
-
-    if D > 0:
-        extra_dims = [2] * (2 * D)
-        identity = qml.math.reshape(qml.math.eye(2**D), extra_dims)
-
-        if interface == "tensorflow":
-            identity = qml.math.cast_like(identity, tensor)
-
-        expanded_tensor = qml.math.tensordot(tensor, identity, axes=0)
-        # Fix order of tensor factors
-        expanded_tensor = qml.math.moveaxis(
-            expanded_tensor, tuple(range(2 * N, 2 * N + D)), tuple(range(N, N + D))
-        )
-    else:
-        expanded_tensor = tensor
-
-    wire_indices = []
-    for wire in original_wires:
-        wire_indices.append(expanded_wires.index(wire))
-
-    wire_indices = np.array(wire_indices)
-
-    # Order tensor factors according to wires
-    original_indices = np.array(range(N))
-    expanded_tensor = qml.math.moveaxis(
-        expanded_tensor, tuple(original_indices), tuple(wire_indices)
-    )
-    expanded_tensor = qml.math.moveaxis(
-        expanded_tensor, tuple(original_indices + M), tuple(wire_indices + M)
-    )
-
-    return qml.math.reshape(expanded_tensor, (2**M, 2**M))
-
-
 def expand_vector(vector, original_wires, expanded_wires):
     r"""Expand a vector to more wires.
 


Test patch:
diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py
index 2f3d1c3b9..1a23e2b9b 100644
--- a/tests/devices/test_default_qubit_tf.py
+++ b/tests/devices/test_default_qubit_tf.py
@@ -209,7 +209,7 @@ class TestTFMatrix:
             expected_mat = qml.math.kron(I, qml.math.kron(reg_mat, I))
 
         tf_mat = op(tf.Variable(param), wires=wires).matrix()
-        obtained_mat = qml.utils.expand(tf_mat, wires, list(range(4)))
+        obtained_mat = qml.operation.expand_matrix(tf_mat, wires, list(range(4)))
 
         assert qml.math.get_interface(obtained_mat) == "tensorflow"
         assert qml.math.allclose(qml.math.unwrap(obtained_mat), expected_mat)
diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py
index 1a950d68f..1f9a76b86 100644
--- a/tests/ops/qubit/test_parametric_ops.py
+++ b/tests/ops/qubit/test_parametric_ops.py
@@ -2142,7 +2142,7 @@ class TestPauliRot:
         """Test PauliRot matrix correctly accounts for identities."""
 
         res = qml.PauliRot.compute_matrix(theta, pauli_word)
-        expected = qml.utils.expand(
+        expected = qml.operation.expand_matrix(
             qml.PauliRot.compute_matrix(theta, compressed_pauli_word), compressed_wires, wires
         )
 
diff --git a/tests/optimize/test_lie_algebra.py b/tests/optimize/test_lie_algebra.py
index a58ce2e86..4b988e782 100644
--- a/tests/optimize/test_lie_algebra.py
+++ b/tests/optimize/test_lie_algebra.py
@@ -104,7 +104,7 @@ def test_lie_algebra_omegas(circuit, hamiltonian):
     ops = opt.get_su_n_operators(None)[0]
     omegas_np = []
     for op in ops:
-        op = qml.utils.expand(op.matrix(), op.wires, wires)
+        op = qml.operation.expand_matrix(op.matrix(), op.wires, wires)
         omegas_np.append(1j * np.trace(lie_algebra_np @ op))
     omegas = opt.get_omegas()
     assert np.allclose(omegas, omegas_np)
@@ -151,7 +151,7 @@ def test_lie_algebra_omegas_restricted(circuit, hamiltonian):
     ops = opt.get_su_n_operators(restriction)[0]
     omegas_np = []
     for op in ops:
-        op = qml.utils.expand(op.matrix(), op.wires, wires)
+        op = qml.operation.expand_matrix(op.matrix(), op.wires, wires)
         omegas_np.append(1j * np.trace(lie_algebra_np @ op))
     omegas = opt.get_omegas()
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 0b4942c0b..04eba56df 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -440,138 +440,8 @@ class TestArgumentHelpers:
             pu._inv_dict(test_data)
 
 
-class TestExpand:
-    """Tests multi-qubit operator expansion"""
-
-    def test_expand_one(self, tol):
-        """Test that a 1 qubit gate correctly expands to 3 qubits."""
-        # test applied to wire 0
-        res = pu.expand(U, [0], 3)
-        expected = np.kron(np.kron(U, I), I)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 1
-        res = pu.expand(U, [1], 3)
-        expected = np.kron(np.kron(I, U), I)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 2
-        res = pu.expand(U, [2], 3)
-        expected = np.kron(np.kron(I, I), U)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-    def test_expand_one_wires_list(self, tol):
-        """Test that a 1 qubit gate correctly expands to 3 qubits."""
-        # test applied to wire 0
-        res = pu.expand(U, [0], [0, 4, 9])
-        expected = np.kron(np.kron(U, I), I)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 4
-        res = pu.expand(U, [4], [0, 4, 9])
-        expected = np.kron(np.kron(I, U), I)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 9
-        res = pu.expand(U, [9], [0, 4, 9])
-        expected = np.kron(np.kron(I, I), U)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-    def test_expand_two_consecutive_wires(self, tol):
-        """Test that a 2 qubit gate on consecutive wires correctly
-        expands to 4 qubits."""
-
-        # test applied to wire 0+1
-        res = pu.expand(U2, [0, 1], 4)
-        expected = np.kron(np.kron(U2, I), I)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 1+2
-        res = pu.expand(U2, [1, 2], 4)
-        expected = np.kron(np.kron(I, U2), I)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 2+3
-        res = pu.expand(U2, [2, 3], 4)
-        expected = np.kron(np.kron(I, I), U2)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-    def test_expand_two_reversed_wires(self, tol):
-        """Test that a 2 qubit gate on reversed consecutive wires correctly
-        expands to 4 qubits."""
-
-        # CNOT with target on wire 1
-        res = pu.expand(CNOT, [1, 0], 4)
-        rows = np.array([0, 2, 1, 3])
-        expected = np.kron(np.kron(CNOT[:, rows][rows], I), I)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-    def test_expand_invalid_wires(self):
-        """test exception raised if unphysical subsystems provided."""
-        with pytest.raises(
-            ValueError,
-            match="Invalid target subsystems provided in 'original_wires' argument",
-        ):
-            pu.expand(U2, [-1, 5], 4)
-
-    def test_expand_invalid_matrix(self):
-        """test exception raised if incorrect sized matrix provided/"""
-        with pytest.raises(ValueError, match="Matrix parameter must be of size"):
-            pu.expand(U, [0, 1], 4)
-
-    def test_expand_three_consecutive_wires(self, tol):
-        """Test that a 3 qubit gate on consecutive
-        wires correctly expands to 4 qubits."""
-
-        # test applied to wire 0,1,2
-        res = pu.expand(U_toffoli, [0, 1, 2], 4)
-        expected = np.kron(U_toffoli, I)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 1,2,3
-        res = pu.expand(U_toffoli, [1, 2, 3], 4)
-        expected = np.kron(I, U_toffoli)
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-    def test_expand_three_nonconsecutive_ascending_wires(self, tol):
-        """Test that a 3 qubit gate on non-consecutive but ascending
-        wires correctly expands to 4 qubits."""
-
-        # test applied to wire 0,2,3
-        res = pu.expand(U_toffoli, [0, 2, 3], 4)
-        expected = (
-            np.kron(SWAP, np.kron(I, I)) @ np.kron(I, U_toffoli) @ np.kron(SWAP, np.kron(I, I))
-        )
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 0,1,3
-        res = pu.expand(U_toffoli, [0, 1, 3], 4)
-        expected = (
-            np.kron(np.kron(I, I), SWAP) @ np.kron(U_toffoli, I) @ np.kron(np.kron(I, I), SWAP)
-        )
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-    def test_expand_three_nonconsecutive_nonascending_wires(self, tol):
-        """Test that a 3 qubit gate on non-consecutive non-ascending
-        wires correctly expands to 4 qubits"""
-
-        # test applied to wire 3, 1, 2
-        res = pu.expand(U_toffoli, [3, 1, 2], 4)
-        # change the control qubit on the Toffoli gate
-        rows = np.array([0, 4, 1, 5, 2, 6, 3, 7])
-        expected = np.kron(I, U_toffoli[:, rows][rows])
-        assert np.allclose(res, expected, atol=tol, rtol=0)
-
-        # test applied to wire 3, 0, 2
-        res = pu.expand(U_toffoli, [3, 0, 2], 4)
-        # change the control qubit on the Toffoli gate
-        rows = np.array([0, 4, 1, 5, 2, 6, 3, 7])
-        expected = (
-            np.kron(SWAP, np.kron(I, I))
-            @ np.kron(I, U_toffoli[:, rows][rows])
-            @ np.kron(SWAP, np.kron(I, I))
-        )
-        assert np.allclose(res, expected, atol=tol, rtol=0)
+class TestExpandVector:
+    """Tests vector expansion to more wires"""
 
     VECTOR1 = np.array([1, -1])
     ONES = np.array([1, 1])
@@ -593,7 +463,7 @@ class TestExpand:
     def test_expand_vector_single_wire(self, original_wires, expanded_wires, expected, tol):
         """Test that expand_vector works with a single-wire vector."""
 
-        res = pu.expand_vector(TestExpand.VECTOR1, original_wires, expanded_wires)
+        res = pu.expand_vector(TestExpandVector.VECTOR1, original_wires, expanded_wires)
 
         assert np.allclose(res, expected, atol=tol, rtol=0)
 
@@ -616,7 +486,7 @@ class TestExpand:
     def test_expand_vector_two_wires(self, original_wires, expanded_wires, expected, tol):
         """Test that expand_vector works with a single-wire vector."""
 
-        res = pu.expand_vector(TestExpand.VECTOR2, original_wires, expanded_wires)
+        res = pu.expand_vector(TestExpandVector.VECTOR2, original_wires, expanded_wires)
 
         assert np.allclose(res, expected, atol=tol, rtol=0)
 
@@ -626,9 +496,21 @@ class TestExpand:
             ValueError,
             match="Invalid target subsystems provided in 'original_wires' argument",
         ):
-            pu.expand_vector(TestExpand.VECTOR2, [-1, 5], 4)
+            pu.expand_vector(TestExpandVector.VECTOR2, [-1, 5], 4)
 
     def test_expand_vector_invalid_vector(self):
         """Test exception raised if incorrect sized vector provided."""
         with pytest.raises(ValueError, match="Vector parameter must be of length"):
-            pu.expand_vector(TestExpand.VECTOR1, [0, 1], 4)
+            pu.expand_vector(TestExpandVector.VECTOR1, [0, 1], 4)
+
+
+class TestWarnings:
+    """Tests deprecation warnings"""
+
+    def test_expand_raises_warning(self):
+        """Test warning occurs when calling deprecated qml.utils.expand"""
+        with pytest.warns(
+            UserWarning,
+            match="qml.utils.expand is deprecated; using qml.operation.expand_matrix instead.",
+        ):
+            pu.expand(U, [9], [0, 4, 9])