Repository: GAA-UAM/scikit-fda

Issue:
Extend Lp norm to more object types
Currently, the Lp norm cannot deal with:

- np.ndarrays (unless `p=2`)
- FDataBasis (unless `p=2`)
- FDataGrid with multidimensional domains (unless `p=math.inf`)

We should extend the norm to cover also these cases.

Discussion:
@vnmabus: @luisheb I think this is part of what you were doing. I will assign this to you.@luisheb: I'm on it.

Solution patch:
diff --git a/skfda/misc/metrics/_lp_norms.py b/skfda/misc/metrics/_lp_norms.py
index 0465dc00..fd23e329 100644
--- a/skfda/misc/metrics/_lp_norms.py
+++ b/skfda/misc/metrics/_lp_norms.py
@@ -1,4 +1,5 @@
 """Implementation of Lp norms."""
+
 import math
 from builtins import isinstance
 from typing import Union
@@ -10,9 +11,10 @@ from typing_extensions import Final
 from ...representation import FData, FDataBasis, FDataGrid
 from ...typing._metric import Norm
 from ...typing._numpy import NDArrayFloat
+from ..._utils import nquad_vec
 
 
-class LpNorm():
+class LpNorm:
     r"""
     Norm of all the observations in a FDataGrid object.
 
@@ -98,10 +100,7 @@ class LpNorm():
         self.vector_norm = vector_norm
 
     def __repr__(self) -> str:
-        return (
-            f"{type(self).__name__}("
-            f"p={self.p}, vector_norm={self.vector_norm})"
-        )
+        return f"{type(self).__name__}(" f"p={self.p}, vector_norm={self.vector_norm})"
 
     def __call__(self, vector: Union[NDArrayFloat, FData]) -> NDArrayFloat:
         """Compute the Lp norm of a functional data object."""
@@ -124,16 +123,25 @@ class LpNorm():
             return np.sqrt(inner_product(vector, vector))
 
         if isinstance(vector, FDataBasis):
-            if self.p != 2:
-                raise NotImplementedError
-
-            start, end = vector.domain_range[0]
-            integral = scipy.integrate.quad_vec(
-                lambda x: np.power(np.abs(vector(x)), self.p),
-                start,
-                end,
+            domain = vector.basis.domain_range
+            call = vector
+
+            def integrand(*args: NDArrayFloat) -> NDArrayFloat:  # noqa: WPS430
+                f_args = np.asarray(args)
+
+                try:
+                    f1 = call(f_args)[:, 0, :]
+                except Exception:
+                    f1 = call(f_args)
+
+                return np.power(np.abs(f1), self.p)
+
+            integral = nquad_vec(
+                integrand,
+                domain,
             )
-            res = np.sqrt(integral[0]).flatten()
+
+            res = (np.sum(integral, axis=-1)) ** (1 / self.p)
 
         elif isinstance(vector, FDataGrid):
             data_matrix = vector.data_matrix
@@ -161,7 +169,7 @@ class LpNorm():
             else:
 
                 integrand = vector.copy(
-                    data_matrix=data_matrix ** self.p,
+                    data_matrix=data_matrix**self.p,
                     coordinate_names=(None,),
                 )
                 # Computes the norm, approximating the integral with Simpson's


Test patch:
diff --git a/skfda/tests/test_metrics.py b/skfda/tests/test_metrics.py
index ad866392..cfca2660 100644
--- a/skfda/tests/test_metrics.py
+++ b/skfda/tests/test_metrics.py
@@ -31,6 +31,7 @@ class TestLp(unittest.TestCase):
         )
         basis = MonomialBasis(n_basis=3, domain_range=(1, 5))
         self.fd_basis = FDataBasis(basis, [[1, 1, 0], [0, 0, 1]])
+        self.fd_grid_in_basis = self.fd.to_basis(MonomialBasis(n_basis=4))
         self.fd_vector_valued = self.fd.concatenate(
             self.fd,
             as_coordinates=True,
@@ -41,6 +42,14 @@ class TestLp(unittest.TestCase):
             random_state=0,
         )
 
+        self.array = np.array(
+            [
+                [2, 3, 4, 5, 6],
+                [1, 4, 9, 16, 25],
+            ],
+            dtype=float,
+        )
+
     def test_lp_norm_grid(self) -> None:
         """Test that the Lp norms work with FDataGrid."""
         np.testing.assert_allclose(
@@ -70,6 +79,26 @@ class TestLp(unittest.TestCase):
             [8.326664, 24.996],
         )
 
+    def test_lp_norm_basis_equivalent(self) -> None:
+        """Test that the Lp norms in basis are similar to FDataGrid"""
+        np.testing.assert_allclose(
+            l1_norm(self.fd_grid_in_basis),
+            [16.0, 41.33333333],
+            rtol=1e-2,
+        )
+
+        np.testing.assert_allclose(
+            l2_norm(self.fd_grid_in_basis),
+            [8.326664, 25.006666],
+            rtol=1e-2,
+        )
+
+        np.testing.assert_allclose(
+            lp_norm(self.fd_grid_in_basis, p=3),
+            [6.839904, 22.401268],
+            rtol=1e-2,
+        )
+
     def test_lp_norm_vector_valued(self) -> None:
         """Test that the Lp norms work with vector-valued FDataGrid."""
         np.testing.assert_allclose(
@@ -136,6 +165,27 @@ class TestLp(unittest.TestCase):
         with np.testing.assert_raises(ValueError):
             l2_distance(self.fd, fd2)
 
+    def test_lp_array(self) -> None:
+        """Test that the Lp norms work with arrays."""
+        np.testing.assert_allclose(
+            l1_norm(self.array),
+            [20, 55],
+        )
+        np.testing.assert_allclose(
+            l2_norm(self.array),
+            [9.48683298, 31.28897569],
+        )
+
+        np.testing.assert_allclose(
+            lp_norm(self.array, p=3),
+            [7.60590492, 27.37519199],
+        )
+
+        np.testing.assert_allclose(
+            linf_norm(self.array),
+            [6, 25],
+        )
+
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()