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 |
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
|
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
|
Returns:
| Name | Type | Description |
|---|---|---|
smooth |
Smooth
|
A smooth with |
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
|
Returns:
| Name | Type | Description |
|---|---|---|
smooth |
Smooth
|
A smooth with |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the knot specification is invalid or |
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
|
Returns:
| Name | Type | Description |
|---|---|---|
smooth |
Smooth
|
A periodic smooth with |
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
|
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If there are fewer than |
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
|
Returns:
| Name | Type | Description |
|---|---|---|
smooth |
Smooth
|
A periodic cubic smooth with |
Raises:
| Type | Description |
|---|---|
ValueError
|
If there are fewer than |
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
|
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 |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
smooth |
Smooth
|
A low-rank thin-plate smooth whose |
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 |
required |
linear_trend
|
bool
|
Whether to include linear covariate columns in addition to an intercept. |
required |
range_
|
float | None
|
Positive kernel range. |
required |
power
|
float
|
Exponent for the |
required |
knots
|
ArrayLike | None
|
Optional kernel centers, as a vector in one dimension or a matrix in
multiple dimensions. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
smooth |
Smooth
|
A low-rank Gaussian-process smooth whose |
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 |
required |
Returns:
| Name | Type | Description |
|---|---|---|
smooth |
Smooth
|
An MRF smooth whose basis evaluates integer region codes. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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 |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
matrix |
Array
|
Basis matrix with |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an order is negative or values fall outside the interior knot range
while |
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 |
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]])