B-splines are a way to represent a smooth function as a local weighted sum:
where p is the degree, c_i are coefficients, and B_{i,p} are basis
functions defined by a knot vector. The reason they belong in a differentiable
foundation package is simple: each basis function has local support, the basis
is nonnegative, and inside the active knot domain the basis functions form a
partition of unity.
That gives a stable primitive for downstream packages that need smooth table-like functions: atmosphere-grid interpolation, microphysics tables, stellar tracks, calibration curves, or any other place where global polynomials would be too eager to oscillate.

Figure 1:Each colored curve is one column returned by bspline_basis(...) for a fixed
six-function cubic basis. The right panel sums those same returned columns. It
visualizes one executable open-uniform configuration; it is not evidence about
adaptive-knot quality or smoothing-model selection.
Learning objectives¶
After this chapter, you should be able to explain local support, verify partition of unity, and distinguish basis construction from coefficient fitting and scientific regularization choices.
Concept check: local change, local effect¶
Predict which query interval changes when one cubic-spline coefficient changes. Compute the basis support, then audit nonnegativity and the partition-of-unity sum before interpreting a fitted curve.
The current boundary¶
jaxstro’s spline surface is deliberately fixed-knot first:
from jaxstro.jaxconfig import enable_high_precision
enable_high_precision() # before creating JAX arrays
import jax.numpy as jnp
from jaxstro.numerics import (
BSpline1D,
bspline_basis,
bspline_derivative,
bspline_eval,
open_uniform_knots,
)
knots = open_uniform_knots(0.0, 1.0, n_basis=6, degree=3)
coeffs = jnp.array([0.0, 0.25, 0.9, 0.7, 0.2, 0.1])
x = jnp.linspace(0.0, 1.0, 9)
basis = bspline_basis(knots, x, degree=3)
values = bspline_eval(knots, coeffs, x, degree=3)
derivative = bspline_derivative(knots, coeffs, x, degree=3)
spline = BSpline1D(knots, coeffs, degree=3)
wrapped_values = spline(x)
assert basis.shape == (9, 6)
assert jnp.allclose(basis.sum(axis=-1), 1.0)
assert jnp.allclose(values, wrapped_values)
assert jnp.all(jnp.isfinite(derivative))It evaluates supplied coefficients by basis contraction or de Boor recursion, computes derivative and antiderivative values, exposes sample design matrices, solves ordinary least-squares fits for fixed knots, builds quantile-based clamped knots, assembles row-wise tensor-product design matrices, and provides a roughness penalty primitive. It still does not own smoothing-spline model selection, adaptive-knot optimization loops, extrapolation, or domain-specific regularization policy.
Knots and clamping¶
A clamped open-uniform knot vector repeats the first and last knot degree + 1
times. For a cubic spline on [0, 1], a single-span knot vector is:
0 0 0 0 1 1 1 1This makes the first and last coefficients control the endpoint values. jaxstro’s
open_uniform_knots(...) constructs this layout for any valid n_basis and
degree.
Inputs outside the active knot domain are clamped to the endpoint basis values.
This matches the existing fail-closed posture of interp1d: no extrapolated
curve is invented. The trade-off is the same as any hard saturation: gradients
with respect to x are zero outside the active domain. If an optimizer needs to
move an out-of-domain x back into range, the caller should handle the domain
constraint explicitly rather than relying on spline extrapolation.
Cox-de Boor recurrence¶
Boor (1972) gives the normalized-basis recurrence and derivative coefficient relations in equations (10)--(15). The notation here uses degree , where that paper uses order .
The degree-zero basis is an interval indicator:
Higher degrees are built recursively:
Repeated knots make some denominators zero. The implementation uses the standard
safe convention: a term with a zero denominator contributes zero. This is also
the AD-safe convention. The denominator is sanitized before division, so a dead
zero-width term does not leak NaN into the backward pass.
Differentiability¶
The derivative claim depends on which input is changing. The fixed-knot surface has these explicit contracts:
Table 1:B-spline gradient contracts
Operation | Contract | Supported claim | Boundary |
|---|---|---|---|
Coefficients at fixed knots |
| Evaluation is linear in the coefficients; AD returns the active basis vector and is checked independently. | The knot vector and degree remain fixed. |
Interior query coordinate |
| AD agrees with finite differences inside a smooth knot span. | The query is away from repeated knots and the clamped domain boundary. |
Clamped exterior coordinate |
| The public evaluator and analytic derivative are constant outside the active domain. | This zero is a saturation contract, not an inference direction. |
Knot boundaries |
| Values and the derivatives guaranteed by the local knot multiplicity can be checked at a named boundary. | Smoothness is multiplicity-dependent; no universal knot gradient is claimed. |
Quantile knot construction |
| Deterministic quantile placement is checked as a construction result. | Sorting and quantile selection are not presented as a smooth inference path. |
For fixed knots, spline evaluation is linear in the coefficients:
That property is tested directly: the AD gradient with respect to coefficients
matches the basis vector. Gradients with respect to interior x are checked
against finite differences in the validation suite. At knots, the derivative
order depends on the knot multiplicity, so tests use interior points rather than
pretending every knot is smooth.
The analytic derivative uses the standard coefficient transform:
then evaluates a degree p - 1 spline on the trimmed knot vector. Zero-width
denominators use the same safe-zero convention as the basis recurrence. Outside
the active knot domain, bspline_derivative(...) returns zero, matching the
gradient of the public clamped evaluator with respect to x.
Definite integrals use the antiderivative coefficient transform. If has degree , the antiderivative has degree on the knot vector with one extra boundary knot at each end. Coefficient increments are:
Fixed-knot least-squares fitting solves the linear design problem:
It is a convenience around the basis matrix, not a smoothing spline. For noisy
data, bspline_roughness_penalty(...) supplies the common integrated squared
derivative term so callers can build an explicit objective without jaxstro
choosing the smoothing weight.
de Boor and tensor products¶
The de Boor algorithm is the standard stable evaluator for a single spline value
when you already know the active knot span. bspline_eval_deboor(...) now exposes
that evaluator and is validated against the basis-contraction path. The public
mathematical contract is identical; the two spellings exist so callers can choose
the representation that best matches their workflow.
tensor_product_design_matrix(...) performs a row-wise Kronecker product of 1D
basis matrices. That is the construction primitive for tensor-product splines
without making jaxstro own multidimensional smoothing, sparse storage, or
domain-specific grid policy.
adaptive_open_uniform_knots(...) is intentionally modest: it places interior
knots at sample quantiles and clamps the endpoints. It is deterministic knot
construction, not a knot-optimization algorithm.
From explanation to evidence¶
Use the jaxstro.numerics.splines for signatures and
ownership, the Validation for measured spline anchors, and
the Gradient contracts for the package-wide contract taxonomy.
- de Boor, C. (1972). On calculating with B-splines. Journal of Approximation Theory, 6(1), 50–62. 10.1016/0021-9045(72)90080-9