Interpolation with Interpolations.jl

Interpolations.jl implements (cardinal) B-splines, i.e. interpolating polynomial functions which are fit to the data and to each other.

We'll start with a simple 1D example - interpolating $f(x) = sin(x)$ over one period, using 10 grid points.


In [1]:
nx = 10
f(x) = sin(2pi/(nx-1) * (x-1))

xcoarse = 1:nx
ycoarse = f.(xcoarse);

xfine = 1:0.1:xcoarse[end]
yfine = f.(xfine);

An interpolation object is basically an array that supports evaluation with any real numbers.

To create an interpolation object, simply call the constructor interpolate, providing the array and some configuration for the interpolation:


In [2]:
using Interpolations

yitp = interpolate(ycoarse, BSpline(Linear()))


Out[2]:
10-element interpolate(::Array{Float64,1}, BSpline(Linear())) with element type Float64:
  0.0                   
  0.6427876096865393    
  0.984807753012208     
  0.8660254037844387    
  0.3420201433256689    
 -0.34202014332566866   
 -0.8660254037844385    
 -0.9848077530122081    
 -0.6427876096865396    
 -2.4492935982947064e-16

There are a couple of noteworthy points here. First, note the extra argument to interpolate, BSpline(Linear()). This determines the behavior of the interpolating function inside the domain. Linear tells us that it will be a linear interpolation (duh...); in other words, for yitp(x) interpolation will be performed like this:

begin
    ix = ifloor(x)
    dx = x - ix
    (1-dx)*ycoarse[ix] + dx*ycoarse[ix+1]
end

A B-spline interpolation is basically a piecewise polynomial function, where each grid cell is associated with its own polynomial. The coefficients are chosen such that the interpolating function passes through all the data points, while the transitions between polynomials are as smooth as possible given their degree. For example, each grid cell in a linear interpolation (such as yitp) is associated with a straight line, and the interpolation is continuous (with discontinuous derivative) over the entire domain.

Let's take a look at how the different interpolation degrees behave:


In [4]:
yitp_const = interpolate(ycoarse, BSpline(Constant()))
yconst = [yitp_const(x) for x in xfine]

yitp_linear = interpolate(ycoarse, BSpline(Linear()))
ylinear = [yitp_linear(x) for x in xfine]

yitp_quadratic = interpolate(ycoarse, BSpline(Quadratic(Line(OnCell()))))
yquadratic = [yitp_quadratic(x) for x in xfine];

You'll notice that when creating the quadratic interpolation, we had to give another input parameter to the interpolation type: the Line(OnCell()) argument which specifies the boundary condition. The first term (Line) specifies the behavior of the boundary condition, the second (OnCell) specifies where it applies. In this case yitp_quadratic will become linear for x < 0.5 or x > 10.5. The alternative, OnGrid, would apply the boundary condition for x < 1 or x > 10.

All interpolations of quadratic degree or higher require a prefiltering step, which entails solving a tridiagonal system of equations (details can be found for example in this paper), in order to make the interpolating function pass through the data points. Interpolations.jl takes care of solving this system for you, but in order to close the system a boundary condition is requred.

Let's see the implemented functionality in action!


In [6]:
using Gadfly

plot(
    layer(x=xcoarse,y=ycoarse,Geom.point),
    layer(x=xfine,y=yconst,Geom.line,Theme(default_color=colorant"magenta")),
    layer(x=xfine,y=ylinear,Geom.line,Theme(default_color=colorant"red")),
    layer(x=xfine,y=yquadratic,Geom.line,Theme(default_color=colorant"green"))
)


┌ Info: Precompiling Gadfly [c91e804a-d5a3-530f-b6f0-dfbca275c004]
└ @ Base loading.jl:1186
ERROR: LoadError: syntax: try without catch or finally
Stacktrace:
 [1] include at ./boot.jl:317 [inlined]
 [2] include_relative(::Module, ::String) at ./loading.jl:1038
 [3] include(::Module, ::String) at ./sysimg.jl:29
 [4] top-level scope at none:2
 [5] eval at ./boot.jl:319 [inlined]
 [6] eval(::Expr) at ./client.jl:389
 [7] top-level scope at ./none:3
in expression starting at /home/tim/.julia/packages/Compose/y7cU7/src/Compose.jl:207
ERROR: LoadError: Failed to precompile Compose [a81c6b42-2e10-5240-aca2-a61377ecd94b] to /home/tim/.julia/compiled/v1.0/Compose/sbiEw.ji.
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] macro expansion at ./logging.jl:313 [inlined]
 [3] compilecache(::Base.PkgId, ::String) at ./loading.jl:1184
 [4] _require(::Base.PkgId) at ./logging.jl:311
 [5] require(::Base.PkgId) at ./loading.jl:852
 [6] macro expansion at ./logging.jl:311 [inlined]
 [7] require(::Module, ::Symbol) at ./loading.jl:834
 [8] include at ./boot.jl:317 [inlined]
 [9] include_relative(::Module, ::String) at ./loading.jl:1038
 [10] include(::Module, ::String) at ./sysimg.jl:29
 [11] top-level scope at none:2
 [12] eval at ./boot.jl:319 [inlined]
 [13] eval(::Expr) at ./client.jl:389
 [14] top-level scope at ./none:3
in expression starting at /home/tim/.julia/packages/Gadfly/p8TXc/src/Gadfly.jl:7
Failed to precompile Gadfly [c91e804a-d5a3-530f-b6f0-dfbca275c004] to /home/tim/.julia/compiled/v1.0/Gadfly/DvECm.ji.

Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] macro expansion at ./logging.jl:313 [inlined]
 [3] compilecache(::Base.PkgId, ::String) at ./loading.jl:1184
 [4] macro expansion at ./logging.jl:311 [inlined]
 [5] _require(::Base.PkgId) at ./loading.jl:941
 [6] require(::Base.PkgId) at ./loading.jl:852
 [7] macro expansion at ./logging.jl:311 [inlined]
 [8] require(::Module, ::Symbol) at ./loading.jl:834
 [9] top-level scope at In[6]:1

2D interpolation

Interpolations.jl supports interpolation in arbitrary dimensions - just give it an Array{T,N} where elements t::T support operations like a::Real * t + b::Real * t and it will figure it out. However, since it starts getting difficult to visualize higher-dimensional functions, we'll stick with an example in 2D, namely the function $g(x,y) = x^2 \cdot \sin(y)$.


In [7]:
xs = 1:5
ys = 1:8
g = Float64[x^2 * sin(y) for x in xs, y in ys]

gitp_quad2d = interpolate(g, BSpline(Quadratic(Line(OnCell()))))

display(plot(x=xs,y=ys,z=g,Geom.contour))
display(plot(x=1:.1:5, y=1:.1:8, z=[gitp_quad2d(x,y) for x in 1:.1:5, y in 1:.1:8], Geom.contour))


UndefVarError: plot not defined

Stacktrace:
 [1] top-level scope at In[7]:7

Simple, huh? =)

Extrapolation

The behavior outside the domain is not carefully specified by interpolate, and most likely you should avoid using such objects outside the original domain. If you do want to control behavior outside the domain, you will likely be interested in extrapolate:

yextr = extrapolate(yitp, Flat)

The Flat parameter to extrapolate signifies that we want the value to remain constant (flat) outside of the domain. Other options are...(how the interpolation object will behave when an index outside of the domain $[1, 10]$ is given; with ExtrapError, a BoundsError is thrown, while with ExtrapNaN (the only other implemented extrapolation behavior at the moment), NaN is returned.)