Lagrange basis
Definition
Given $n$ distinct nodes $x_1, \dots, x_n$, the Lagrange basis is
\[\ell_j(x) = \prod_{i \neq j} \frac{x - x_i}{x_j - x_i} , \qquad j = 1, \dots, n ,\]
a basis of the polynomials of degree $\le n-1$. This is the cardinal basis of the nodes,
\[\ell_j(x_i) = \delta_{ij} ,\]
which is the property everything else follows from.
Domain and nodes
Nodal, and the only basis here whose nodes the user supplies. The declared domain is $[0,1]$, but the nodes are whatever they are given as: they need not be sorted, and nothing forces them into the interval.
Note that Lagrange indexes its basis functions from 1, where the other three bases index from 0. Write loops over eachindex(b) rather than over 0:degree(b) and this never comes up.
julia> l = Lagrange([0.0, 0.5, 1.0]);
julia> eachindex(l)
SOneTo(3)
julia> nnodes(l), nbasis(l), order(l), degree(l)
(3, 3, 3, 2)Interpolation is free
Because the basis is cardinal, the coefficients of an interpolant are the values at the nodes — there is no system to solve. This is the reason to choose a Lagrange basis.
julia> l = LagrangeLobatto(3);
julia> f(x) = 1 + x^2;
julia> c = [f(x) for x in nodes(l)]; # the coefficients are just the values
julia> sum(c[j] * l[0.3, j] for j in eachindex(l)) ≈ f(0.3)
trueThe last line is exact up to rounding because $f$ is a polynomial of degree 2 and the basis spans degree 2.
Cardinality and partition of unity
julia> l = Lagrange([0.0, 0.1, 0.7, 1.0]);
julia> all(l[nodes(l)[i], j] ≈ (i == j) for i in 1:4, j in 1:4)
true
julia> sum(l[0.42, j] for j in eachindex(l)) ≈ 1
trueThe cardinal functions do take negative values between the nodes, so unlike Bernstein this partition of unity is not a convex combination.
Which nodes to use
Equidistant nodes are a poor choice at higher degree — see Choice of nodes — so two better sets are provided directly:
julia> collect(nodes(LagrangeGauß(3))) # strictly inside the interval
3-element Vector{Float64}:
0.11270166537925831
0.5
0.8872983346207417
julia> collect(nodes(LagrangeLobatto(3))) # includes both endpoints
3-element Vector{Float64}:
0.0
0.5
1.0The nodes are stored in an SVector, hence the collect above; nodes itself returns the static vector.
Use LagrangeGauß when the expansion is going to be integrated, and LagrangeLobatto when the boundary values have to be imposed or read off.
Derivative
Differentiating the product gives
\[\ell_j'(x) = \sum_{l \neq j} \frac{1}{x_j - x_l} \prod_{i \neq j, l} \frac{x - x_i}{x_j - x_i} ,\]
which is what LagrangeDerivative evaluates, reusing the table of node differences the basis caches at construction.
julia> l = LagrangeLobatto(3);
julia> d = Derivative(axes(l, 1));
julia> sum((d*l)[0.3, j] for j in eachindex(l)) |> abs < 1e-14 # rows sum to zero
trueElement type and precision
All internal quantities — the node differences and the reciprocal denominators — are formed in the element type of the nodes, so a BigFloat basis carries its full precision. Before version 0.3.0 those buffers were allocated in Float64 regardless, so a BigFloat basis reported BigFloat while carrying only double precision, in the basis and in its derivative alike.
julia> setprecision(BigFloat, 256) do
l = Lagrange(gauss_legendre_nodes(BigFloat, 4))
d = Derivative(axes(l, 1))
z = BigFloat(1) / 7
(abs(sum(l[z, j] for j in eachindex(l)) - 1) < 1e-70,
abs(sum((d*l)[z, j] for j in eachindex(l))) < 1e-70)
end
(true, true)Pitfalls
The nodes must be distinct. The denominators are products of node differences, so a repeated node is a division by zero. This is rejected rather than silently producing
Inf:julia> Lagrange([0.0, 0.5, 0.5, 1.0]) ERROR: ArgumentError: the nodes of a Lagrange basis must be distinct, got [0.0, 0.5, 0.5, 1.0] [...]What is tested is the differences themselves, not the node list under
isequal— the comparisonalluniqueuses, and a different question from the one the denominators ask.0.0and-0.0are distinct underisequal, yet their difference is zero:julia> allunique([0.0, -0.0]) true julia> Lagrange([0.0, -0.0]) ERROR: ArgumentError: the nodes of a Lagrange basis must be distinct, got [0.0, -0.0] [...]The nodes must be finite. A
NaNorInfnode isisequalto nothing else and so looks perfectly distinct, while poisoning every difference it takes part in:julia> Lagrange([0.0, NaN, 1.0]) ERROR: ArgumentError: the nodes of a Lagrange basis must be finite, got [0.0, NaN, 1.0] [...]The product of the differences must be representable. Nodes that are distinct and finite can still multiply out to zero or to an infinity — packed into a narrow range the product underflows, spread over a wide one it overflows — and either way the denominator is unusable. The nodes are not at fault there, so the message says so and points at the remedy:
julia> Lagrange([0.0, 1e160, 2e160, 3e160]) ERROR: ArgumentError: the nodes of a Lagrange basis are distinct and finite, but the product of the differences from node 1 is -Inf in Float64, so the denominator it gives is not usable; rescale the nodes or widen the element type [...]Scaling such a node set to
[0,1]and mapping the argument along with it is the way out; a wider element type buys room too, but only a fixed amount of it.Indexing starts at 1, unlike the other three bases.
Equidistant nodes at high degree will diverge; see Choice of nodes.
The nodes are stored in an
SVector, so the number of nodes is part of the type. Building a basis with very many nodes is therefore a compile-time cost, not just a run-time one.
For the barycentric reformulation, which is the preferred way to evaluate a Lagrange interpolant at many points and is more stable than the product form, see [10] and [11].