x = [0.0, 0.15, 0.4, 0.7, 1.5, 2.5, 3.8, 4.2, 4.6, 4.8, 5.0,
5.3, 5.6, 6.0, 6.5, 7.0, 7.3, 7.8, 8.2, 8.5, 9.0, 9.5, 10.0] # an irregular knot vector
f(x) = sin(x) * exp(-x / 5) # the function being sampled
y = f.(x) # the values at the knots
itp = convolution_interpolation(x, y; kernel = :b7) # nonuniform grid detected from the knots
d1 = convolution_interpolation(x, y; kernel = :b7, derivative = 1) # its first derivative
d2 = convolution_interpolation(x, y; kernel = :b7, derivative = 2) # its second derivative
xf = range(0, 10, length = 600) # fine points for plotting
plot(xf, f.(xf), color = :black, label = "f", xlabel = "x")
plot!(xf, itp.(xf), label = "interpolant")
plot!(xf, d1.(xf), label = "first derivative")
plot!(xf, d2.(xf), label = "second derivative")
scatter!(x, y, markercolor = :white, markerstrokewidth = 1.5, label = "knots")Nonuniform grids
On a uniform grid every kernel copy is the same polynomial, shifted. On a nonuniform grid the spacing changes from interval to interval, and the Julia package carries a second family of kernels whose polynomial pieces are recomputed for the actual knots, so that the interpolant keeps its cardinality and its polynomial reproduction on any strictly increasing knot vector. Nothing needs to be switched on: passing knots that are not uniformly spaced selects them.
The Python port works on uniform grids only, so far; the feature matrix tracks the port.
The kernels
Two kinds of nonuniform kernel are available:
:n3is the nonuniform cubic, the counterpart of:a3: closed-form weights, a four-wide stencil and no precomputation, so it costs about what the uniform cubic costs. It is the default on nonuniform grids.:b5to:b13are the nonuniform counterparts of the high-order kernels. They converge at order degree + 1 —:b5at 6th order and:b13at 14th — and provide derivatives up to 7th order. Their weights are projected onto each interval in exact rational arithmetic when the interpolant is built, so construction costs one projection per interval and grows with the number of intervals; evaluation is as local as on a uniform grid.
The uniform :a kernels other than :a3 have no nonuniform counterpart.
A first example
A damped sine sampled on a grid that is dense where the function varies fast and sparse where it has settled, interpolated with :b7 and differentiated twice:
| at x = 2.9 (a sparse part of the grid) | f | f′ |
|---|---|---|
| interpolant | 0.1340734324 | -0.5704459029 |
| exact | 0.1339553086 | -0.5704289524 |
Convergence on a graded grid
The orders hold on nonuniform grids as they do on uniform ones. Here the knots are a uniform grid pushed through a smooth monotone map, so that each refinement keeps the grading and halves every spacing, and Runge’s function is interpolated with four kernels:
The convergence study
runge(x) = 1 / (1 + 25x^2) # Runge's function
grade(u) = u + 0.3 * sin(π * u) / π # a monotone map of [−1, 1] onto itself, dense near the centre
xr = range(-1, 1, length = 2001) # fine points for measuring the error
kernels = (:n3, :b5, :b7, :b9)
ns = [41, 81, 161, 321, 641] # the numbers of knots
hs = 2 ./ (ns .- 1) # the spacings of the uniform grid before grading
errors = [begin
xn = grade.(range(-1, 1, length = n)) # the graded knots
itp_k = convolution_interpolation(xn, runge.(xn); kernel = k)
maximum(abs.(itp_k.(xr) .- runge.(xr)))
end for n in ns, k in kernels]
plot(xscale = :log10, yscale = :log10, xlabel = "mean spacing h", ylabel = "largest error",
legend = :bottomleft, xflip = true)
for (j, k) in enumerate(kernels)
plot!(hs, errors[:, j], marker = :circle, label = string(k))
endn3 |
b5 |
b7 |
b9 |
|
|---|---|---|---|---|
| observed convergence order | 3.00 | 5.84 | 7.72 | 9.14 |
In more dimensions
Nonuniform knots can be given per axis, and the grid is the tensor product of the axes, so a log-spaced axis and a uniform axis combine freely. Each axis chooses its own kernel and derivative order:
xg = 10 .^ range(-2, 0, length = 25) # a log-spaced axis on [0.01, 1]
yg = range(0, 1, length = 20) # a uniform axis
data = [log(xi) * cos(π * yi) for xi in xg, yi in yg] # log(x)·cos(πy) on the grid
itp2 = convolution_interpolation((xg, yg), data; kernel = :b7) # interpolation
dx2 = convolution_interpolation((xg, yg), data; kernel = (:b7, :b5), derivative = (1, 0)) # ∂/∂x, two kernels
itp2(0.05, 0.3), dx2(0.05, 0.3)(-1.7608755746816087, 11.746837334411147)
| at (0.05, 0.3) | f | ∂f/∂x |
|---|---|---|
| interpolant | -1.7608755747 | 11.7468373344 |
| exact | -1.7608472502 | 11.7557050458 |
The precomputation of the :b kernels is per interval and per axis, so it stays cheap on a tensor-product grid: a 1000 × 1000 grid needs two thousand projections, not a million.
What is not available
Antiderivatives are uniform-grid only: the precomputed tails that make an integral cost the same as an interpolation rely on equal spacing. To integrate data on a nonuniform grid, evaluate the nonuniform interpolant on a uniform grid fine enough to carry it and integrate that with derivative = -1, as on the integrals page. Lazy construction, for large grids, is available with :n3 only; the :b kernels build their ghost values when the interpolant is created.