Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/GAA-UAM/scikit-fda.git i…
Browse files Browse the repository at this point in the history
…nto develop
  • Loading branch information
vnmabus committed Dec 29, 2022
2 parents cea0336 + 106eeec commit ff06311
Show file tree
Hide file tree
Showing 4 changed files with 581 additions and 6 deletions.
31 changes: 25 additions & 6 deletions skfda/representation/_functional_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import numpy as np
import pandas.api.extensions
from matplotlib.figure import Figure
from typing_extensions import Literal
from typing_extensions import Literal, Protocol

from .._utils import _evaluate_grid, _to_grid_points
from ..typing._base import (
Expand All @@ -45,8 +45,8 @@
from .extrapolation import ExtrapolationLike, _parse_extrapolation

if TYPE_CHECKING:
from .grid import FDataGrid
from .basis import Basis, FDataBasis
from .grid import FDataGrid

T = TypeVar('T', bound='FData')

Expand Down Expand Up @@ -75,6 +75,7 @@ class FData( # noqa: WPS214
coordinate functions.
"""

dataset_name: Optional[str]

def __init__(
Expand Down Expand Up @@ -188,7 +189,7 @@ def dim_codomain(self) -> int:

@property
@abstractmethod
def coordinates(self: T) -> Sequence[T]:
def coordinates(self: T) -> _CoordinateSequence:
r"""Return a component of the FDataGrid.
If the functional object contains multivariate samples
Expand Down Expand Up @@ -1169,7 +1170,6 @@ def take( # noqa: WPS238
Parameters:
indices: Indices to be taken.
allow_fill: How to handle negative values in `indices`.
* False: negative values in `indices` indicate positional
indices from the right (the default). This is similar to
:func:`numpy.take`.
Expand All @@ -1178,10 +1178,8 @@ def take( # noqa: WPS238
other negative values raise a ``ValueError``.
fill_value: Fill value to use for NA-indices
when `allow_fill` is True.
This may be ``None``, in which case the default NA value for
the type, ``self.dtype.na_value``, is used.
For many ExtensionArrays, there will be two representations of
`fill_value`: a user-facing "boxed" scalar, and a low-level
physical NA value. `fill_value` should be the user-facing
Expand Down Expand Up @@ -1317,3 +1315,24 @@ def concatenate(functions: Iterable[T], as_coordinates: bool = False) -> T:
)

return first.concatenate(*functions, as_coordinates=as_coordinates)


F = TypeVar("F", covariant=True)


class _CoordinateSequence(Protocol[F]):
"""
Sequence of coordinates.
Note that this represents a sequence of coordinates, not a sequence of
FData objects.
"""

def __getitem__(
self,
key: Union[int, slice],
) -> F:
pass

def __len__(self) -> int:
pass
2 changes: 2 additions & 0 deletions skfda/representation/basis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'_basis': ["Basis"],
"_bspline_basis": ["BSplineBasis", "BSpline"],
"_constant_basis": ["ConstantBasis", "Constant"],
'_custom_basis': ["CustomBasis"],
"_fdatabasis": ["FDataBasis", "FDataBasisDType"],
"_finite_element_basis": ["FiniteElementBasis", "FiniteElement"],
"_fourier_basis": ["FourierBasis", "Fourier"],
Expand All @@ -28,6 +29,7 @@
Constant as Constant,
ConstantBasis as ConstantBasis,
)
from ._custom_basis import CustomBasis as CustomBasis
from ._fdatabasis import (
FDataBasis as FDataBasis,
FDataBasisDType as FDataBasisDType,
Expand Down
219 changes: 219 additions & 0 deletions skfda/representation/basis/_custom_basis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
"""Abstract base class for basis."""

from __future__ import annotations

from typing import Any, Tuple, TypeVar

import multimethod
import numpy as np

from ...typing._numpy import NDArrayFloat
from .._functional_data import FData
from ..grid import FDataGrid
from ._basis import Basis
from ._fdatabasis import FDataBasis

T = TypeVar("T", bound="CustomBasis")


class CustomBasis(Basis):
"""Basis composed of custom functions.
Defines a basis composed of the functions in the :class: `FData` object
passed as argument.
The functions must be linearly independent, otherwise
an exception is raised.
Parameters:
fdata: Functions that define the basis.
"""

def __init__(
self,
*,
fdata: FData,
) -> None:
"""Basis constructor."""
super().__init__(
domain_range=fdata.domain_range,
n_basis=fdata.n_samples,
)
self._check_linearly_independent(fdata)

self.fdata = fdata

@multimethod.multidispatch
def _check_linearly_independent(self, fdata: FData) -> None:
raise ValueError("Unexpected type of functional data object.")

@_check_linearly_independent.register
def _check_linearly_independent_grid(self, fdata: FDataGrid) -> None:
# Reshape to a bidimensional matrix. This only affects FDataGrids
# whose codomain is not 1-dimensional and it can be done because
# checking linear independence in (R^n)^k is equivalent to doing
# it in R^(nk).
coord_matrix = fdata.data_matrix.reshape(
fdata.data_matrix.shape[0],
-1,
)
return self._check_linearly_independent_matrix(coord_matrix)

@_check_linearly_independent.register
def _check_linearly_independent_basis(self, fdata: FDataBasis) -> None:
return self._check_linearly_independent_matrix(fdata.coefficients)

def _check_linearly_independent_matrix(self, matrix: NDArrayFloat) -> None:
"""Check if the functions are linearly independent."""
if matrix.shape[0] > matrix.shape[1]:
raise ValueError(
"There are more functions than the maximum dimension of the "
"space that they could generate.",
)

rank = np.linalg.matrix_rank(matrix)
if rank != matrix.shape[0]:
raise ValueError(
"There are only {rank} linearly independent "
"functions".format(
rank=rank,
),
)

def _derivative_basis_and_coefs(
self: T,
coefs: NDArrayFloat,
order: int = 1,
) -> Tuple[T, NDArrayFloat]:
deriv_fdata = self.fdata.derivative(order=order)

return self._create_subspace_basis_coef(deriv_fdata, coefs)

@multimethod.multidispatch
def _create_subspace_basis_coef(
self: T,
fdata: FData,
coefs: np.ndarray,
) -> Tuple[T, NDArrayFloat]:
"""
Create a basis of the subspace generated by the given functions.
Args:
fdata: The resulting basis will span the subspace generated
by these functions.
coefs: Coefficients of some functions in the given fdata.
These coefficients will be transformed into the coefficients
of the same functions in the resulting basis.
"""
raise ValueError(
"Unexpected type of functional data object: {type}.".format(
type=type(fdata),
),
)

@_create_subspace_basis_coef.register
def _create_subspace_basis_coef_grid(
self: T,
fdata: FDataGrid,
coefs: np.ndarray,
) -> Tuple[T, NDArrayFloat]:

# Reshape to a bidimensional matrix. This can be done because
# working in (R^n)^k is equivalent to working in R^(nk) when
# it comes to linear independence and basis.
data_matrix_reshaped = fdata.data_matrix.reshape(
fdata.data_matrix.shape[0],
-1,
)
# If the basis formed by the derivatives has maximum rank,
# we can just return that
rank = np.linalg.matrix_rank(data_matrix_reshaped)
if rank == fdata.n_samples:
return type(self)(fdata=fdata), coefs

# Otherwise, we need to find the basis of the subspace generated
# by the functions
q, r = np.linalg.qr(data_matrix_reshaped.T)

# Go back from R^(nk) to (R^n)^k
fdata.data_matrix = q.T.reshape(
-1,
*fdata.data_matrix.shape[1:],
)

new_basis = type(self)(fdata=fdata)

# Since the QR decomponsition yields an orthonormal basis,
# the coefficients are just the projections of values of
# the functions in every point (coefs @ data_matrix_reshaped)
# in the new basis (q).
# Note that to simply the calculations, we use both the data_matrix
# and the basis matrix in R^(nk) instead of the original space
values_in_eval_points = coefs @ data_matrix_reshaped
coefs = values_in_eval_points @ q

return new_basis, coefs

@_create_subspace_basis_coef.register
def _create_subspace_basis_coef_basis(
self: T,
fdata: FDataBasis,
coefs: np.ndarray,
) -> Tuple[T, NDArrayFloat]:

# If the basis formed by the derivatives has maximum rank,
# we can just return that
rank = np.linalg.matrix_rank(fdata.coefficients)
if rank == fdata.n_samples:
return type(self)(fdata=fdata), coefs

q, r = np.linalg.qr(fdata.coefficients.T)

fdata.coefficients = q.T

new_basis = type(self)(fdata=fdata)

# Since the QR decomponsition yields an orthonormal basis,
# the coefficients are just the result of projecting the
# coefficients in the underlying basis of the FDataBasis onto
# the new basis (q)
coefs_wrt_underlying_fdata_basis = coefs @ fdata.coefficients
coefs = coefs_wrt_underlying_fdata_basis @ q

return new_basis, coefs

def _coordinate_nonfull(
self,
coefs: NDArrayFloat,
key: int | slice,
) -> Tuple[Basis, NDArrayFloat]:
return CustomBasis(fdata=self.fdata.coordinates[key]), coefs

def _evaluate(
self,
eval_points: NDArrayFloat,
) -> NDArrayFloat:
return self.fdata(eval_points)

def __len__(self) -> int:
return self.n_basis

@property
def dim_codomain(self) -> int:
return self.fdata.dim_codomain

def __eq__(self, other: Any) -> bool:
return super().__eq__(other) and all(self.fdata == other.fdata)

def __ne__(self, other: Any) -> bool:
return not self.__eq__(other)

def __repr__(self) -> str:
"""Representation of a CustomBasis object."""
return "{super}, fdata={fdata}".format(
super=super().__repr__(),
fdata=self.fdata,
)

def __hash__(self) -> int:
return hash(self.fdata)

0 comments on commit ff06311

Please sign in to comment.