Skip to content

API reference

Public signatures include types and defaults from the Python annotations. The constructors return immutable Smooth objects.

Smooth class

Smooth dataclass

Hold the two pieces needed to use a smooth in a model.

Call basis to turn input values into model columns. Use penalty to discourage unnecessarily wiggly coefficient patterns. A Smooth never changes in place; each transformation returns a new one.

Parameters:

Name Type Description Default
basis Callable[[ArrayLike], Array]

Function mapping covariate values to a design matrix with one row per value and one column per coefficient.

required
penalty Array

Square matrix defining the coefficient penalty.

required
rank int

Numerical rank of penalty.

required
knots Array | None

Knot or center locations retained by the constructor, when applicable.

None

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import pspline
>>> x = jnp.linspace(0.0, 1.0, 8)
>>> smooth = pspline(x, k=5, degree=3, penalty_order=2)
>>> smooth.basis(x).shape
(8, 5)
>>> smooth.penalty.shape
(5, 5)
>>> smooth.rank
3

Smooth methods

constrain

constrain(
    constraint: str | ArrayLike,
    *,
    values: ArrayLike | None = None,
) -> Smooth

Remove patterns that the smooth should not be allowed to represent.

"sumzero_coef" makes the coefficients add to zero, "sumzero_term" makes the fitted values at values add to zero, and "constant_and_linear" removes constant and linear trends. A matrix can describe any other rule as A @ coefficients == 0.

Parameters:

Name Type Description Default
constraint str | ArrayLike

Built-in constraint name or a full-row-rank constraint matrix.

required
values ArrayLike | None

Covariate values used by term-based constraints. Required for "sumzero_term" and "constant_and_linear".

None

Returns:

Name Type Description
smooth Smooth

A reparameterized smooth with one column removed per constraint.

Raises:

Type Description
ValueError

If the constraint is unknown, malformed, or does not leave a free coefficient, or if required values are missing.

References

Kneib, T., Klein, N., Lang, S., & Umlauf, N. (2019). Modular regression—A Lego system for building structured additive distributional regression models with tensor product interactions. TEST, 28(1), 1–39. https://doi.org/10.1007/s11749-019-00631-z

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import pspline
>>> x = jnp.linspace(0.0, 1.0, 8)
>>> smooth = pspline(x, k=5, degree=3, penalty_order=2)
>>> constrained = smooth.constrain("sumzero_coef")
>>> smooth.basis(x).shape
(8, 5)
>>> constrained.basis(x).shape
(8, 4)

scale_penalty

scale_penalty(*, values: ArrayLike) -> Smooth

Put the penalty on a scale that matches the evaluated basis.

The penalty is multiplied by ||B||_inf**2 / ||K||_1. This makes its strength less dependent on how the basis happens to be parameterized. The basis itself stays unchanged.

Parameters:

Name Type Description Default
values ArrayLike

Covariate values at which to evaluate the basis scale.

required

Returns:

Name Type Description
smooth Smooth

A smooth with the scaled penalty.

Raises:

Type Description
ValueError

If the evaluated basis or penalty has zero or non-finite norm.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import pspline
>>> x = jnp.linspace(0.0, 1.0, 8)
>>> smooth = pspline(x, k=5, degree=3, penalty_order=2)
>>> scaled = smooth.scale_penalty(values=x)
>>> scaled.basis(x).shape
(8, 5)
>>> scaled.penalty.shape
(5, 5)
>>> bool(jnp.all(jnp.isfinite(scaled.penalty)))
True

diagonalize_penalty

diagonalize_penalty(*, values: ArrayLike) -> Smooth

Rewrite the smooth so its penalty is diagonal and made of ones and zeros.

Penalized directions receive a one; unpenalized directions receive a zero. Unpenalized directions are scaled to the average squared norm of the penalized design columns, following mgcv::nat.param(type=2). The represented smooth stays the same, although the basis columns can change sign or rotate when penalty values are tied.

Parameters:

Name Type Description Default
values ArrayLike

Covariate values used to balance unpenalized and penalized design columns.

required

Returns:

Name Type Description
smooth Smooth

A reparameterized smooth with a diagonal penalty.

Raises:

Type Description
ValueError

If a penalized eigenvalue is not positive.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import pspline
>>> x = jnp.linspace(0.0, 1.0, 8)
>>> smooth = pspline(x, k=5, degree=3, penalty_order=2)
>>> diagonal = smooth.diagonalize_penalty(values=x)
>>> jnp.diag(diagonal.penalty).astype(int).tolist()
[1, 1, 1, 0, 0]

Univariate functions

pspline

pspline(
    x: ArrayLike,
    *,
    k: int,
    degree: int,
    penalty_order: int,
    knots: ArrayLike | None = None,
) -> Smooth

Create a smooth curve from local B-spline pieces.

The penalty discourages neighboring coefficients from changing too abruptly, which keeps the fitted curve smooth. Beyond the boundary knots, the curve continues as a straight line.

Parameters:

Name Type Description Default
x ArrayLike

One-dimensional values used to choose or validate the knots.

required
k int

Number of basis functions and coefficients.

required
degree int

Polynomial degree of the B-spline basis.

required
penalty_order int

Order of the coefficient differences in the penalty.

required
knots ArrayLike | None

Knot specification. Use None for automatic knots, two values for boundary limits, or k + degree + 1 values for the full sequence.

None

Returns:

Name Type Description
smooth Smooth

A smooth with k basis columns and penalty rank k - penalty_order.

Raises:

Type Description
ValueError

If the basis dimension or knot specification is invalid.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import pspline
>>> x = jnp.linspace(0.0, 1.0, 10)
>>> smooth = pspline(x, k=6, degree=3, penalty_order=2)
>>> smooth.basis(x).shape
(10, 6)
>>> smooth.penalty.shape
(6, 6)
>>> smooth.rank
4

bspline

bspline(
    x: ArrayLike,
    *,
    k: int,
    degree: int,
    penalty_order: int,
    knots: ArrayLike | None = None,
) -> Smooth

Create a smooth curve by penalizing its derivatives directly.

Instead of comparing neighboring coefficients, this construction penalizes the total squared derivative of the fitted curve. Beyond the boundary knots, the curve continues as a straight line.

Parameters:

Name Type Description Default
x ArrayLike

One-dimensional values used to choose or validate the knots.

required
k int

Number of basis functions and coefficients.

required
degree int

Polynomial degree of the B-spline basis.

required
penalty_order int

Derivative order used in the integrated squared penalty.

required
knots ArrayLike | None

Knot specification. Use None for automatic knots, two values for boundary limits, four mgcv-style boundary knots, or k + degree + 1 values for the full sequence.

None

Returns:

Name Type Description
smooth Smooth

A smooth with k basis columns and penalty rank k - penalty_order.

Raises:

Type Description
ValueError

If the knot specification is invalid or penalty_order exceeds degree.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import bspline
>>> x = jnp.linspace(0.0, 1.0, 10)
>>> smooth = bspline(x, k=6, degree=3, penalty_order=2)
>>> smooth.basis(x).shape
(10, 6)
>>> smooth.penalty.shape
(6, 6)
>>> smooth.rank
4

cyclic_pspline

cyclic_pspline(
    x: ArrayLike,
    *,
    k: int,
    degree: int,
    penalty_order: int,
    knots: ArrayLike | None = None,
) -> Smooth

Create a smooth B-spline curve whose two ends meet.

Values outside the boundary interval wrap around to the other side. Both the curve and its coefficient penalty join across the boundary, making this useful for seasons, times of day, angles, and other repeating inputs.

Parameters:

Name Type Description Default
x ArrayLike

One-dimensional values used to choose or validate the boundary range.

required
k int

Number of periodic basis functions and coefficients.

required
degree int

Polynomial degree of the B-spline basis.

required
penalty_order int

Order of the wrapped coefficient differences.

required
knots ArrayLike | None

Knot specification. Use None for automatic knots, two values for boundary limits, or exactly k + 1 knot values.

None

Returns:

Name Type Description
smooth Smooth

A periodic smooth with k basis columns and rank k - 1.

Raises:

Type Description
ValueError

If the boundary range, knot specification, or penalty order is invalid.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import cyclic_pspline
>>> x = jnp.linspace(0.0, 1.0, 10)
>>> smooth = cyclic_pspline(x, k=6, degree=3, penalty_order=2)
>>> endpoints = smooth.basis(jnp.array([0.0, 1.0]))
>>> smooth.basis(x).shape
(10, 6)
>>> smooth.rank
5
>>> bool(jnp.allclose(endpoints[0], endpoints[1]))
True

cubic_regression

cubic_regression(
    x: ArrayLike,
    *,
    k: int,
    knots: ArrayLike | None = None,
    shrinkage: bool = False,
) -> Smooth

Create a smooth cubic curve whose coefficients are values at the knots.

The penalty discourages the curve from bending too much. Constant and straight-line patterns stay unpenalized unless shrinkage is enabled. Beyond the boundary knots, the curve continues as a straight line.

Parameters:

Name Type Description Default
x ArrayLike

One-dimensional values used to place or validate the knots.

required
k int

Number of knots, basis functions, and coefficients.

required
knots ArrayLike | None

Exact knot locations. None chooses quantiles of the unique values.

None
shrinkage bool

Whether to add small penalties to the two null-space directions.

False

Returns:

Name Type Description
smooth Smooth

A natural cubic smooth with rank k - 2, or k with shrinkage.

Raises:

Type Description
ValueError

If there are fewer than k unique values or the supplied knots are invalid.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import cubic_regression
>>> x = jnp.linspace(0.0, 1.0, 12)
>>> smooth = cubic_regression(x, k=6)
>>> smooth.basis(x).shape
(12, 6)
>>> smooth.penalty.shape
(6, 6)
>>> smooth.rank
4

cyclic_cubic

cyclic_cubic(
    x: ArrayLike, *, k: int, knots: ArrayLike | None = None
) -> Smooth

Create a smooth cubic curve whose beginning and end meet.

The first and last knot describe the same point in the repeating cycle, so k knots produce k - 1 coefficients. Values outside the knot range wrap around to the other side.

Parameters:

Name Type Description Default
x ArrayLike

One-dimensional values used to place or validate the knots.

required
k int

Number of knots, with a minimum of four.

required
knots ArrayLike | None

Knot specification. None chooses quantiles, two values contribute boundary information, and k values specify the complete sequence.

None

Returns:

Name Type Description
smooth Smooth

A periodic cubic smooth with k - 1 basis columns and rank k - 2.

Raises:

Type Description
ValueError

If there are fewer than k unique values or the supplied knots are invalid.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import cyclic_cubic
>>> x = jnp.linspace(0.0, 1.0, 12)
>>> smooth = cyclic_cubic(x, k=6)
>>> smooth.basis(x).shape
(12, 5)
>>> smooth.rank
4
>>> endpoints = smooth.basis(jnp.array([0.0, 1.0]))
>>> bool(jnp.allclose(endpoints[0], endpoints[1], atol=1e-7))
True

Radial functions

thin_plate

thin_plate(
    x: ArrayLike,
    *,
    k: int,
    penalty_order: int,
    knots: ArrayLike | None = None,
    shrinkage: bool = False,
    remove_null_space: bool = False,
) -> Smooth

Create a smooth that can bend in one or more dimensions.

A thin-plate spline penalizes bending equally in every direction, so there is no need to choose separate knot axes. This smaller, low-rank version follows Wood (2003). Constant and low-order polynomial patterns remain unpenalized unless they are shrunk or removed.

Parameters:

Name Type Description Default
x ArrayLike

Covariate vector or matrix with observations in rows and dimensions in columns.

required
k int

Requested basis dimension. It is raised when necessary to leave at least one penalized direction beyond the polynomial null space.

required
penalty_order int

Thin-plate derivative order. Invalid low orders are raised to the smallest valid order for the covariate dimension.

required
knots ArrayLike | None

Optional center locations, as a vector in one dimension or a matrix in multiple dimensions. None uses unique covariate locations.

None
shrinkage bool

Whether to add small penalties to the polynomial null space.

False
remove_null_space bool

Whether to drop the polynomial null-space columns and center the remaining basis over x.

False

Returns:

Name Type Description
smooth Smooth

A low-rank thin-plate smooth whose knots contain its centers.

Raises:

Type Description
ValueError

If the covariates or custom knots have invalid dimensions, or too few unique locations support the requested basis.

References

Wood, S. N. (2003). Thin plate regression splines. Journal of the Royal Statistical Society: Series B, 65(1), 95–114. https://doi.org/10.1111/1467-9868.00374

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import thin_plate
>>> x = jnp.linspace(0.0, 1.0, 10)
>>> smooth = thin_plate(x, k=6, penalty_order=2)
>>> smooth.basis(x).shape
(10, 6)
>>> smooth.penalty.shape
(6, 6)
>>> smooth.rank
4

gaussian_process

gaussian_process(
    x: ArrayLike,
    *,
    k: int,
    kernel_name: str,
    linear_trend: bool,
    range_: float | None,
    power: float,
    knots: ArrayLike | None = None,
) -> Smooth

Create a smooth in which nearby inputs tend to have similar effects.

The selected kernel and range control how quickly that similarity fades with distance. This deterministic, low-rank version includes a constant or linear trend and follows mgcv's fixed-range construction.

Parameters:

Name Type Description Default
x ArrayLike

Covariate vector or matrix with observations in rows and dimensions in columns.

required
k int

Requested total basis dimension. It is raised when necessary to include the chosen trend and at least one penalized direction.

required
kernel_name str

One of "spherical", "power_exponential", "matern1.5", "matern2.5", or "matern3.5".

required
linear_trend bool

Whether to include linear covariate columns in addition to an intercept.

required
range_ float | None

Positive kernel range. None or a non-positive value uses the maximum pairwise distance between centers.

required
power float

Exponent for the "power_exponential" kernel; ignored by the other kernels.

required
knots ArrayLike | None

Optional kernel centers, as a vector in one dimension or a matrix in multiple dimensions. None uses unique covariate locations.

None

Returns:

Name Type Description
smooth Smooth

A low-rank Gaussian-process smooth whose knots contain its centers.

Raises:

Type Description
ValueError

If the kernel name, covariates, or custom knots are invalid, or too few unique locations support the requested basis.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import gaussian_process
>>> x = jnp.linspace(0.0, 1.0, 10)
>>> smooth = gaussian_process(
...     x,
...     k=5,
...     kernel_name="matern1.5",
...     linear_trend=True,
...     range_=None,
...     power=1.5,
... )
>>> smooth.basis(x).shape
(10, 5)
>>> smooth.penalty.shape
(5, 5)
>>> smooth.rank
3

Markov random field functions

infer_neighbors_from_polygons

infer_neighbors_from_polygons(
    polygons: Mapping[str, ArrayLike],
) -> dict[str, list[str]]

Find which named polygonal regions touch each other.

Two regions count as neighbors when their outlines share at least one exactly equal coordinate. Rows containing NaN are ignored, so they can be used to separate parts of a multipart polygon.

Parameters:

Name Type Description Default
polygons Mapping[str, ArrayLike]

Mapping from region labels to two-column coordinate arrays.

required

Returns:

Name Type Description
neighbors dict[str, list[str]]

Symmetric mapping from each label to its neighboring labels.

Raises:

Type Description
ValueError

If a polygon is not a two-column coordinate array.

Examples:

>>> import numpy as np
>>> from smoothcon import infer_neighbors_from_polygons
>>> polygons = {
...     "left": np.array([[0, 0], [1, 0], [1, 1], [0, 1]]),
...     "right": np.array([[1, 0], [2, 0], [2, 1], [1, 1]]),
... }
>>> infer_neighbors_from_polygons(polygons)
{'left': ['right'], 'right': ['left']}

normalize_neighbors

normalize_neighbors(
    neighbors: Mapping[
        str, ArrayLike | list[str] | list[int]
    ],
    labels: Sequence[str],
    index_labels: Sequence[str] | None = None,
) -> dict[str, list[str]]

Convert a region-neighbor mapping to one that uses names throughout.

Neighbors can be supplied as region names or zero-based positions. The function checks that every region and neighbor is valid, then returns all neighbors as names. Positions refer to index_labels, or to sorted labels if no order is supplied.

Parameters:

Name Type Description Default
neighbors Mapping[str, ArrayLike | list[str] | list[int]]

Mapping with one entry per region and one-dimensional neighbor values.

required
labels Sequence[str]

Complete set of region labels.

required
index_labels Sequence[str] | None

Label order used to interpret numeric indices.

None

Returns:

Name Type Description
normalized dict[str, list[str]]

Neighborhood mapping expressed entirely with string labels.

Raises:

Type Description
ValueError

If labels, indices, or neighbor-array dimensions are invalid.

TypeError

If neighbor values use an unsupported dtype.

Examples:

>>> from smoothcon import normalize_neighbors
>>> raw = {"a": [1], "b": [0, 2], "c": [1]}
>>> normalize_neighbors(raw, ["a", "b", "c"])
{'a': ['b'], 'b': ['a', 'c'], 'c': ['b']}

build_mrf_penalty

build_mrf_penalty(
    neighbors: Mapping[str, Sequence[str]],
    labels: Sequence[str],
) -> ndarray

Turn a region-neighbor mapping into a smoothing penalty.

In an MRF smooth, this penalizes differences between neighboring regions, encouraging neighboring regions to have similar effects. The resulting matrix is also known as a graph Laplacian.

The diagonal records each region's number of distinct neighbors and a neighboring pair receives -1 off the diagonal. The order of labels determines the matrix rows and columns.

Parameters:

Name Type Description Default
neighbors Mapping[str, Sequence[str]]

Symmetric mapping from region labels to neighboring labels.

required
labels Sequence[str]

Region order for the output matrix.

required

Returns:

Name Type Description
penalty ndarray

Symmetric graph-Laplacian matrix.

Raises:

Type Description
ValueError

If the neighborhood relation is not symmetric.

Examples:

>>> from smoothcon import build_mrf_penalty
>>> neighbors = {"a": ["b"], "b": ["a", "c"], "c": ["b"]}
>>> penalty = build_mrf_penalty(neighbors, ["a", "b", "c"])
>>> penalty
array([[ 1., -1.,  0.],
       [-1.,  2., -1.],
       [ 0., -1.,  1.]])

mrf

mrf(
    codes: ArrayLike, *, penalty: ArrayLike, k: int
) -> Smooth

Create a smooth for values attached to neighboring regions.

The penalty encourages neighboring regions to have similar effects. The full basis gives each region its own column; a smaller k uses fewer columns, which is useful when there are many regions.

Parameters:

Name Type Description Default
codes ArrayLike

Zero-based integer region codes for the observed values.

required
penalty ArrayLike

Square graph penalty aligned with the region-code ordering.

required
k int

Basis dimension. Use -1 or the number of regions for the full basis, or a smaller positive value for a low-rank basis.

required

Returns:

Name Type Description
smooth Smooth

An MRF smooth whose basis evaluates integer region codes.

Raises:

Type Description
ValueError

If k exceeds the number of regions.

Examples:

>>> import numpy as np
>>> from smoothcon import build_mrf_penalty, mrf
>>> neighbors = {"a": ["b"], "b": ["a", "c"], "c": ["b"]}
>>> penalty = build_mrf_penalty(neighbors, ["a", "b", "c"])
>>> smooth = mrf(np.array([0, 1, 2, 1]), penalty=penalty, k=-1)
>>> smooth.basis(np.array([0, 2])).astype(int).tolist()
[[1, 0, 0], [0, 0, 1]]
>>> smooth.rank
2

Low-level spline functions

equidistant_knots

equidistant_knots(
    x: ArrayLike,
    n_param: int,
    order: int = 3,
    eps: float = 0.01,
) -> Array

Create evenly spaced knots for a B-spline basis.

The sequence extends slightly beyond the data at both ends. It contains n_param + order + 1 knots, which define n_param basis functions of polynomial degree order.

Parameters:

Name Type Description Default
x ArrayLike

Values whose minimum and maximum define the data range.

required
n_param int

Number of basis functions to support.

required
order int

Polynomial degree of the B-spline basis.

3
eps float

Relative amount by which to extend the data range before adding the exterior knots.

0.01

Returns:

Name Type Description
knots Array

Increasing extended knot sequence.

Raises:

Type Description
ValueError

If order is negative or n_param is smaller than order.

Examples:

>>> import jax.numpy as jnp
>>> from smoothcon import equidistant_knots
>>> knots = equidistant_knots(jnp.array([0.0, 1.0]), 5, order=3)
>>> knots.shape
(9,)
>>> round(float(knots[0]), 3)
-1.52
>>> round(float(knots[-1]), 3)
2.52

bspline_basis

bspline_basis(
    x: ArrayLike,
    knots: ArrayLike,
    order: int = 3,
    *,
    outer_ok: bool = False,
    derivative: int = 0,
) -> Array

Calculate B-spline basis values, or their derivatives, at given points.

Each input value produces one row; each B-spline piece produces one column. The knots are sorted before use.

Parameters:

Name Type Description Default
x ArrayLike

Values at which to evaluate the basis.

required
knots ArrayLike

Extended knot sequence.

required
order int

Polynomial degree of the B-spline basis.

3
outer_ok bool

Whether to allow values outside the interior knot range.

False
derivative int

Derivative order to evaluate. Orders above order return zeros.

0

Returns:

Name Type Description
matrix Array

Basis matrix with len(x) rows and len(knots) - order - 1 columns.

Raises:

Type Description
ValueError

If an order is negative or values fall outside the interior knot range while outer_ok is false.

Examples:

>>> import jax.numpy as jnp
>>> import numpy as np
>>> from smoothcon import bspline_basis, equidistant_knots
>>> x = jnp.linspace(0.0, 1.0, 6)
>>> knots = equidistant_knots(x, 5, order=3)
>>> matrix = bspline_basis(x, knots, order=3)
>>> matrix.shape
(6, 5)
>>> np.round(np.asarray(matrix)[[0, 5]], 3)
array([[0.162, 0.667, 0.172, 0.   , 0.   ],
       [0.   , 0.   , 0.172, 0.667, 0.162]], dtype=float32)

pspline_penalty

pspline_penalty(d: int, diff: int = 2) -> Array

Create a penalty for changes between neighboring spline coefficients.

diff=1 penalizes jumps between coefficients; diff=2 penalizes changes in those jumps. The result is D.T @ D, where D applies the requested differences to d coefficients.

Parameters:

Name Type Description Default
d int

Number of coefficients.

required
diff int

Difference order.

2

Returns:

Name Type Description
penalty Array

Square positive-semidefinite penalty matrix.

Raises:

Type Description
ValueError

If diff is negative.

Examples:

>>> import numpy as np
>>> from smoothcon import pspline_penalty
>>> penalty = pspline_penalty(5, diff=2)
>>> np.asarray(penalty, dtype=int)
array([[ 1, -2,  1,  0,  0],
       [-2,  5, -4,  1,  0],
       [ 1, -4,  6, -4,  1],
       [ 0,  1, -4,  5, -2],
       [ 0,  0,  1, -2,  1]])