Skip to content

PyTorch layers

fdatools.nn: differentiable BasisLayer and SmoothingLayer, and the FDataDataset for torch.utils.data.DataLoader. This module needs the optional extra: uv add "fdatools[torch]". import fdatools does not import PyTorch; fdatools.nn is loaded on first use.

PyTorch building blocks for functional data (the fdatools[torch] extra).

Three pieces connect fdatools' basis expansions to :mod:torch:

  • :class:BasisLayer maps basis coefficients to curve values, x(t) = Φ(t) c -- a differentiable fd(t) for use inside a network, with gradients to the coefficients and, when the points are given at call time, to the points too.
  • :class:SmoothingLayer maps raw observations on a grid to basis coefficients by penalised least squares, c = (ΦᵀWΦ + λR)⁻¹ΦᵀW y, exactly as :func:fdatools.smoothing.smooth does; λ can be learned.
  • :class:FDataDataset serves the curves of an :class:~fdatools.core.FData (coefficients, or values on a grid) and optional labels to a :class:torch.utils.data.DataLoader.

Basis and penalty matrices depend only on the basis and the grid, so they are built once in float64 and stored as buffers: layer.to("cuda") or layer.to(torch.float32) moves them with the module.

This module imports PyTorch; import fdatools itself never does.

Examples:

>>> import numpy as np
>>> import torch
>>> import fdatools as fdt
>>> from fdatools.nn import BasisLayer, SmoothingLayer
>>> basis = fdt.BSpline(domain=(0.0, 1.0), n_basis=8)
>>> t = np.linspace(0.0, 1.0, 50)
>>> smoother, evaluate = SmoothingLayer(basis, t, lam=1e-6), BasisLayer(basis, t)
>>> y = torch.sin(2 * torch.pi * torch.tensor(t))[None, :]
>>> bool(torch.max(torch.abs(evaluate(smoother(y)) - y)) < 1e-2)
True

BasisLayer

BasisLayer(
    basis: Basis,
    t: Any = None,
    *,
    deriv: int | LDO = 0,
    dtype: dtype = torch.float64,
)

Bases: Module

Evaluate basis expansions: map coefficients c to x(t) = Φ(t) c.

A differentiable fd(t): the input's last axis holds the n_basis coefficients of one curve, the output's last axis the curve's values at the evaluation points. The layer has no parameters of its own; put an :class:torch.nn.Parameter in front of it to learn a functional weight.

Parameters:

Name Type Description Default
basis Basis

The expansion basis.

required
t array_like

Fixed evaluation points. The basis matrix is precomputed and stored as the buffer design. When omitted, points must be passed to :meth:forward.

None
deriv int or LDO

Derivative order, or linear differential operator, to evaluate. Default 0.

0
dtype dtype

Dtype of the stored basis matrix. Default torch.float64; use torch.float32 on devices without double precision (Apple MPS).

float64

Examples:

>>> import numpy as np
>>> import torch
>>> import fdatools as fdt
>>> from fdatools.nn import BasisLayer
>>> layer = BasisLayer(fdt.BSpline(domain=(0.0, 1.0), n_basis=6), np.linspace(0.0, 1.0, 11))
>>> layer(torch.ones(4, 6, dtype=torch.float64)).shape
torch.Size([4, 11])

extra_repr

extra_repr() -> str

Describe the basis, the grid and the derivative.

forward

forward(coefs: Tensor, t: Any = None) -> torch.Tensor

Evaluate the curves whose coefficients are coefs.

Parameters:

Name Type Description Default
coefs Tensor

Coefficients of shape (..., n_basis).

required
t array_like or Tensor

Evaluation points of shape (n_t,), overriding the fixed grid. A tensor that requires grad receives gradients.

None

Returns:

Type Description
Tensor

Values of shape (..., n_t).

Raises:

Type Description
ValueError

If there are no evaluation points, or the last axis of coefs does not match the basis.

Examples:

>>> import torch
>>> import fdatools as fdt
>>> from fdatools.nn import BasisLayer
>>> layer = BasisLayer(fdt.Fourier(domain=(0.0, 1.0), n_basis=3))
>>> t = torch.linspace(0.0, 1.0, 5, dtype=torch.float64)
>>> layer(torch.ones(3, dtype=torch.float64), t).shape
torch.Size([5])

SmoothingLayer

SmoothingLayer(
    basis: Basis,
    t: Any,
    *,
    lam: float = 0.0,
    penalty: int | LDO = 2,
    weights: Any = None,
    trainable_lam: bool = False,
    dtype: dtype = torch.float64,
)

Bases: Module

Penalised least-squares smoothing: map observations to basis coefficients.

Computes c = (ΦᵀWΦ + λR)⁻¹ ΦᵀW y for observations y on a fixed grid -- the fit of :func:fdatools.smoothing.smooth with a given λ -- as a differentiable layer. With a fixed λ this is one matrix product with a precomputed map; with trainable_lam=True the parameter log_lam = log λ is learned and the system is solved on every call.

Parameters:

Name Type Description Default
basis Basis

The expansion basis.

required
t array_like

Observation points, shape (n_t,).

required
lam float

Smoothing parameter λ ≥ 0 (the initial value when trainable). Default 0.0.

0.0
penalty int or LDO

Roughness operator L of R = ∫ (Lφ)(Lφ)ᵀ. Default 2.

2
weights array_like

Observation weights, shape (n_t,).

None
trainable_lam bool

Learn log λ as a parameter. Requires lam > 0.

False
dtype dtype

Dtype of the stored matrices and of log_lam. Default torch.float64.

float64

Attributes:

Name Type Description
lam float

The current smoothing parameter.

Raises:

Type Description
ValueError

If lam is negative (or not positive when trainable), or weights has the wrong shape.

Examples:

>>> import numpy as np
>>> import torch
>>> import fdatools as fdt
>>> from fdatools.nn import SmoothingLayer
>>> t = np.linspace(0.0, 1.0, 30)
>>> layer = SmoothingLayer(fdt.BSpline(domain=(0.0, 1.0), n_basis=8), t, lam=1e-4)
>>> layer(torch.zeros(5, 30, dtype=torch.float64)).shape
torch.Size([5, 8])

lam property

lam: float

The current smoothing parameter λ.

Returns:

Type Description
float

exp(log_lam) for a trainable layer, else the fixed value.

Examples:

>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.nn import SmoothingLayer
>>> b = fdt.BSpline(n_basis=5)
>>> SmoothingLayer(b, np.linspace(0, 1, 9), lam=0.5, trainable_lam=True).lam
0.5

extra_repr

extra_repr() -> str

Describe the basis, the grid and the smoothing parameter.

forward

forward(y: Tensor) -> torch.Tensor

Smooth observations into basis coefficients.

Parameters:

Name Type Description Default
y Tensor

Observations of shape (..., n_t).

required

Returns:

Type Description
Tensor

Coefficients of shape (..., n_basis).

Raises:

Type Description
ValueError

If the last axis of y does not match the observation grid.

Examples:

>>> import numpy as np
>>> import torch
>>> import fdatools as fdt
>>> from fdatools.nn import SmoothingLayer
>>> layer = SmoothingLayer(fdt.Monomial(n_basis=2), np.linspace(0.0, 1.0, 5))
>>> y = torch.linspace(1.0, 3.0, 5, dtype=torch.float64)  # y = 1 + 2t
>>> [round(v, 10) for v in layer(y).tolist()]
[1.0, 2.0]

FDataDataset

FDataDataset(
    fd: FData,
    labels: Sequence[Any] | Any = None,
    *,
    t: Any = None,
    dtype: dtype = torch.float64,
)

Bases: Dataset[Any]

Serve the curves of an :class:~fdatools.core.FData to a DataLoader.

Item i is curve i -- its coefficients, or its values on a grid -- optionally paired with its label.

Parameters:

Name Type Description Default
fd FData

The curves.

required
labels array_like

One label per curve. Numeric labels become a tensor; anything else (strings, say) is kept as a list.

None
t array_like

Evaluation grid. When given, items are the curves' values at t, shape (n_t,) ((n_t, n_vars) for multivariate curves); otherwise they are coefficient vectors, shape (n_basis,) ((n_basis, n_vars)).

None
dtype dtype

Dtype of the items. Default torch.float64.

float64

Raises:

Type Description
TypeError

If fd is not an :class:~fdatools.core.FData.

ValueError

If the number of labels differs from the number of curves.

Examples:

>>> import numpy as np
>>> import torch
>>> import fdatools as fdt
>>> from fdatools.nn import FDataDataset
>>> fd = fdt.FData(np.eye(5), fdt.BSpline(n_basis=5))
>>> ds = FDataDataset(fd, labels=[0, 1, 0, 1, 0])
>>> x, y = next(iter(torch.utils.data.DataLoader(ds, batch_size=5)))
>>> tuple(x.shape), y.tolist()
((5, 5), [0, 1, 0, 1, 0])

__getitem__

__getitem__(index: int) -> Any

Return curve index, with its label when the dataset has labels.