Code
= 18
n = range(-1,1,length=n+1)
t = @. t^2 + t + 0.05*sin(20*t)
y
scatter(t,y,label="data",leg=:top)
In several of our recent tasks, and in many scientific problems, the solution is a function. Representing functions nuerically is a more complicated and difficult task than that of representing real numbers. The process of converting functions into numberical represesentations of finite length is known as discretization.
Polynomials are a natural first candidate for an interpolant, but it is easy to find examples of data where a polynomial interpolant is unusable for the application or task.
= 18
n = range(-1,1,length=n+1)
t = @. t^2 + t + 0.05*sin(20*t)
y
scatter(t,y,label="data",leg=:top)
The points are unremarkable but the interpolant is quite unusual.
= Polynomials.fit(t,y,n)
p = range(-1,1,length=1000)
x plot!(x,p.(x),label="interpolant")
To keep polynomial degrees low while interpolating large data sets, we use piecewise polynomials as interpolants – that is \(p\) is a polynomial on each subinterval \([t_{k-1},t_k]\) for \(k = 1, \cdots, n\).
= 12
n = range(-1,1,length=n+1)
t = @. t^2 + t + 0.5*sin(20*t)
y
scatter(t,y,label="data",leg=:top)
= FNC.plinterp(t,y)
p plot!(p,-1,1,label="piecewise linear")
Resolving package versions...
No Changes to `~/.julia/environments/v1.11/Project.toml`
No Changes to `~/.julia/environments/v1.11/Manifest.toml`
WARNING: using Dierckx.evaluate in module Main conflicts with an existing identifier.
WARNING: using Dierckx.roots in module Main conflicts with an existing identifier.
We can even choose a higher degree polynomial for a smoother interpolant.
= Spline1D(t,y)
p plot!(x->p(x),-1,1,label="piecewise cubic")
We consider the nodes to be fixed and let \(a = t_0, b = t_n\), so the data for the interpolant problem consists of the vector of \(y\) values, \(\mathbf{y}\), and the result is a function on \([a,b]\).
Suppose \(\mathcal{I}\) is a method for producing the interpolant from a data vector – that is \(\mathcal{I}(\mathbf{y}) = p\), where \(p(t_k) = y_k\) for all \(k\).
We consider methods that are linear, which in particular, satisfy \[\mathcal{I}(\mathbf{y}) = \mathcal{I}\left(\sum_{k=0}^n y_k \mathbf{e}_k\right) = \sum_{k=0}^n y_k \mathcal{I}(\mathbf{e}_k).\]
For any set of \(n+1\) nodes, there are \(n+1\) cardinal functions \(\phi_0, \cdots, \phi_n\) which each single out a different interpolation node, and we then have \[\mathcal{I}(\mathbf{y}) = \sum_{k=0}^n y_k \phi_k.\]
We’ll now measure the conditioning of the interpolation problem with respect to the function \(\infty\)-norm defined as \[\|f\|_\infty = \max_{x \in [a,b]} |f(x)|.\]
= 18
n = range(-1,1,length=n+1)
t = [zeros(9);1;zeros(n-9)]
y
scatter(t,y,label="data")
= Spline1D(t,y)
ϕ plot!(x->ϕ(x),-1,1,label="spline", xlabel=L"x",ylabel=L"\phi(x)",title="Piecewise cubic cardinal function")
The piecewise cubic cardinal function has \(\|\phi\|_\infty \le 1\) which ensures a good condition number for any interpolation with these functions. This is very different than the case for global polynomial interpolation!
scatter(t,y,label="data")
= Polynomials.fit(t,y,n)
ϕ plot!(x->ϕ(x),-1,1,label="polynomial",xlabel=L"x",ylabel=L"\phi(x)",title="Polynomial cardinal function")
The condition number for global polynomial interpolation here is at least 500.
Here, the data points are joined pair-wise by line segments.
On each interval \([t_k,t_{k+1}]\), \(p(x)\) is a linear function passing through \((t_k,y_k)\) and \((t_{k+1},y_{k+1})\).
The interpolant is chosen from among linear combinations of a preselected finite set of functions known as hat functions; for \(k = 0, \cdots, n\), \[H_k(x) = \begin{cases} \frac{x - t_{k-1}}{t_k - t_{k-1}} & \text{if } x \in [t_{k-1},t_k], \\ \frac{t_{k+1}-x}{t_{k+1}-t_k} & \text{if } x \in [t_k,t_{k+1}], \\ 0 &\text{otherwise}. \end{cases}\]
The hat functions form a basis for the set of functions that are continuous and piecewise linear relative to the node points \(\mathbf{t}\).
"""
hatfun(t,k)
Create a piecewise linear hat function, where `t` is a vector of n+1
interpolation nodes and `k` is an integer in 0:n giving the index of
the node where the hat function equals one.
"""
function hatfun(t,k)
= length(t)-1
n return function(x)
if k > 0 && t[k] ≤ x ≤ t[k+1]
return (x-t[k])/(t[k+1]-t[k])
elseif k < n && t[k+1] ≤ x ≤ t[k+2]
return (t[k+2] - x)/(t[k+2]-t[k+1])
else
return 0
end
end
end
hatfun
= [0, 0.55, 0.7, 1] t
4-element Vector{Float64}:
0.0
0.55
0.7
1.0
= plot(layout=(4,1),legend=:top,xlabel=L"x",ylims=[-0.1,1.1],ytick=[])
plt for k in 0:3
= hatfun(t,k)
Hk plot!(Hk,0,1,subplot=k+1,legend=false)
scatter!(t,Hk.(t),m=3,subplot=k+1)
annotate!(t[k+1],0.25,text(latexstring("H_$k"),10),subplot=k+1)
end
plt
Notice that hat functions are the cardinal functions for piecewise linear interpolation. We can then say \[p(x) = \sum_{k=0}^n y_k H_k(x).\]
"""
plinterp(t,y)
Construct a piecewise linear interpolating function for data values in `y`
given at nodes in `t`.
"""
function plinterp(t,y)
= length(t)-1
n = [ hatfun(t,k) for k in 0:n ]
H return x -> sum( y[k+1]*H[k+1](x) for k in 0:n )
end
plinterp
= x -> exp(sin(7*x))
f
plot(f,0,1,label="function",xlabel=L"x",ylabel=L"y")
= [0,0.075,0.25,0.55,0.7,1] # nodes
t = f.(t) # function values y
6-element Vector{Float64}:
1.0
1.6507223907458943
2.675097817245369
0.5217195285817878
0.37439173399608494
1.9289708044108762
scatter!(t,y,label="values at nodes")
= plinterp(t,y)
p plot!(p,0,1,label="interpolant",title="PL interpolation")
The previous condition number bounds give that the condition number for PL interpolation satisfies \(1 \le \kappa \le n+1\). However, an even stronger results is:
This result follows from linearity of \(\mathcal{I}\) and the triangle inequality.
We also ask how close the PL interpolant \(p(x)\) is to function \(f\) that generated \(\mathbf{y}\).
PL interpolation is second-order accurate.
Resolving package versions...
Updating `~/.julia/environments/v1.11/Project.toml`
[08abe8d2] + PrettyTables v2.4.0
No Changes to `~/.julia/environments/v1.11/Manifest.toml`
= range(0,1,length=10001)
x = @. round(Int,10^(1:0.25:3.5))
n = zeros(0)
maxerr for n in n
= (0:n)/n # interpolation nodes
t = plinterp(t,f.(t))
p = @. f(x) - p(x)
err push!(maxerr,norm(err,Inf))
end
= (n=n[1:4:end],err=maxerr[1:4:end])
data pretty_table(data)
┌───────┬────────────┐
│ n │ err │
│ Int64 │ Float64 │
├───────┼────────────┤
│ 10 │ 0.150471 │
│ 100 │ 0.00166421 │
│ 1000 │ 1.66494e-5 │
└───────┴────────────┘
= @. 1/n
h = @. 10*(h/h[1])^2
order2
plot(h,maxerr,m=:o,label="error")
plot!(h,order2,l=:dash,label=L"O(h^2)",xflip=true,xaxis=(:log10,L"h"),yaxis=(:log10,L"|| f-p\, ||_\infty"),title="Convergence of PL interpolation")
Piecewise cubic interpolation provides a smoother interpolant than PL interpolation, and has continuous derivatives.
The spline \((x)\) is defined on the interval \([t_{k-1},t_k]\) by cubic polynomial \[S_k(x) = a_k + b_k(x - t_{k-1}) + c_k(x - t_{k-1})^2 + d_k(x - t_{k-1})^3, \quad\quad k=1,\cdots,n,\] where \(a_k, b_k, c_k, d_k\) are values to be determined. Overall there are \(4n\) such undetermined coefficients.
The following constraints guarantee that \(S\) has at least two continuous derivative everywhere.
This condition can be rewritten as \(a_k = y_{k-1}\) and \(a_k + b_k h_k + c_k h_k^2 + d_k h_k^3 = y_k\) for \(k = 1, \cdots, n\). These linear constraints can be written as \[\begin{bmatrix} \mathbf{I} & \mathbf{0} & \mathbf{0} & \mathbf{0} \end{bmatrix} \begin{bmatrix} \mathbf{a} \\ \mathbf{b} \\ \mathbf{c} \\ \mathbf{d} \end{bmatrix} = \begin{bmatrix} y_0 \\ \vdots \\ y_{n-1} \end{bmatrix},\] and \[\begin{bmatrix} \mathbf{I} & \mathbf{H} & \mathbf{H}^2 & \mathbf{H}^3 \end{bmatrix} \begin{bmatrix} \mathbf{a} \\ \mathbf{b} \\ \mathbf{c} \\ \mathbf{d} \end{bmatrix} = \begin{bmatrix} y_1 \\ \vdots \\ y_{n} \end{bmatrix}\] where \(\mathbf{H}\) is a diagonal matrix with \(h_1, \cdots, h_n\) on the diagonal.
This can be written as \(b_k + 2 c_k h_k + 3 d_k h_k^2 = b_{k+1}\) for \(k = 1,, \cdots, n-1\).
This can be rewritten as \[\mathbf{E} \begin{bmatrix} \mathbf{0} & \mathbf{J} & 2\mathbf{H} & 3\mathbf{H}^2 \end{bmatrix} \begin{bmatrix} \mathbf{a} \\ \mathbf{b} \\ \mathbf{c} \\ \mathbf{d} \end{bmatrix} = \mathbf{0},\] where \(\mathbf{J}\) has ones on the diagonal and \(-1\)s on the first diagonal, and zeros elsewhere, and \(\mathbf{E}\) is the \((n-1) \times n\) matrix obtained by deleting the last row from the identity matrix.
This can be written as \(2c_k + 6 d_k h_k = 2 c_{k+1}\) for \(k = 1, \cdots, n-1\).
This can be rewritten as \[\mathbf{E} \begin{bmatrix} \mathbf{0} & \mathbf{0} & \mathbf{J} & 3\mathbf{H} \end{bmatrix} \begin{bmatrix} \mathbf{a} \\ \mathbf{b} \\ \mathbf{c} \\ \mathbf{d} \end{bmatrix} = \mathbf{0}.\]
So far there are \(4n-2\) conditions on the \(4n\) unknowns. To get a well-defined problem, we add two more constraints. There are two typical approaches:
We consider the not-a-knot spline as they give better pointwise accuracy.
Algebraically, these conditions are \(d_1 = d_2\), \(d_{n-1} = d_n\). We append these to the systems we built above.
Note that there is no closed form expression for a cardinal basis for the cubic spline, so the process of computing it requires solving the constructed linear system.
plot(f,0,1,label="function",xlabel=L"x",ylabel=L"y")
= [0, 0.075, 0.25, 0.55, 0.7, 1]
t = f.(t)
y
scatter!(t,y,label="values at nodes")
= FNC.spinterp(t,y)
S
plot!(S,0,1,label="spline")
= (0:10000)/1e4 # sample the differences at many points
x = @. round(Int,2^(3:0.5:7)) # numbers of nodes
n = zeros(0)
errs for n in n
= (0:n)/n
t = FNC.spinterp(t,f.(t))
S = @. f(x) - S(x)
dif push!(errs,norm(dif,Inf))
end
pretty_table((n=n,error=errs))
┌───────┬─────────────┐
│ n │ error │
│ Int64 │ Float64 │
├───────┼─────────────┤
│ 8 │ 0.0305634 │
│ 11 │ 0.0207562 │
│ 16 │ 0.00590761 │
│ 23 │ 0.00134587 │
│ 32 │ 0.000367049 │
│ 45 │ 9.17785e-5 │
│ 64 │ 2.15306e-5 │
│ 91 │ 5.04292e-6 │
│ 128 │ 1.24012e-6 │
└───────┴─────────────┘
= @. (n/n[1])^(-4)
order4
plot(n,[errs order4],m=[:o :none],l=[:solid :dash],label=["error" "4th order"], xaxis=(:log10, "n"), yaxis=(:log10,L"|| f-S\,||_\infty"),title="Convergence of spline interpolation")