Linear interpolation

To compute the points of a linear interpolation for a given function $f(x)$, we can use the points_linear function.

AdaptiveStepSize.points_linearFunction
points_linear(f, domain, tol::T; scan_step = (domain[end] - domain[begin]) / 100) where T <: Real

Computes points used for a linear interpolation of the function f in the given domain within a tolerance tol.

See also points_linear_singular for managing singular points.

Arguments

  • f: The function used for the linear interpolation. It must be of the form of f(x), where x is a number.
  • domain: a tuple or array representing the interpolation domain, e.g., the tuple (a, b).
  • tol::T where T <: Real: the desired tolerance of the points. The real value of the function at any point xᵢ minus the aproximation will be smaller than tol, i.e., |f(xᵢ) - yᵢ| < tol.

Keywords

  • scan_step: Minimum step size that will be used to scan the whole domain. The returned points will have at least a spacing of scan_step. Very small values will produce a very long execution time. By default it will divide the domain in 100 intervals.

Returns

  • (xs, ys): A tuple containing two arrays of points, xs for the independent variable and ys for the computed values of the function.

Notes

The function f must have a continuous second derivative in order to compute the linear interpolation error. This second derivative is computed using Enzyme's automatic differentiation.

If the returned (xs, ys) contains just the endpoints, try decreasing the scan_step size and/or increasing the tolerance tol.

If the execution time of a single call to the function f is quite long, this adaptive method might not be suitable.

source

For example, let us apply this function to $f(x) = \sqrt{2x}$ on the interval $[0,\, 2]$ with a tolerance of $10^{-2}$.

julia> using AdaptiveStepSize
julia> f(x) = 2 * sqrt(x)f (generic function with 1 method)
julia> domain = (0.0, 2.0)(0.0, 2.0)
julia> tol = 1e-20.01
julia> xs, ys = points_linear(f, domain, tol; scan_step = 1e-4)([0.0, 0.0004, 0.0018000000000000006, 0.005500000000000001, 0.013699999999999973, 0.029899999999999875, 0.05880000000000066, 0.10670000000000203, 0.18149999999999633, 0.2927999999999841, 0.45209999999996653, 0.6726999999999422, 0.9698999999999095, 1.3608999999998665, 2.0], [0.0, 0.04, 0.08485281374238572, 0.14832396974191328, 0.23409399821439228, 0.3458323293158109, 0.48497422611928837, 0.6532993188424492, 0.8520563361656232, 1.0822199406774653, 1.3447676379210893, 1.6403658128599756, 1.9696700231256092, 2.3331523739352016, 2.8284271247461903])

The returned xs and ys contain the points needed for the linear interpolation with an error smaller than the given tolerance. When calling the function, we have set a scan_step of $10^{-4}$. By default this value is computed such that the domain is divided in 100 steps. It is recommeneded to not set a very small value of the scan step. Depending on the desired tolerance, between 100 and 1e4 maximum possible subintervals is desired, otherwise, the execution time will be very small. It is recommendended to set the scan_step value as

julia> domain = (0.0, 2.0)(0.0, 2.0)
julia> scan_step_value = (domain[end] - domain[begin]) / 1e40.0002

where the 1e4 can be the maximum number of points you want.

We can plot the points xs and ys on top of the function and observe that when $f(x)$ varies more, the points are closer, whereas the get further away when they are more linear.

Example block output

Note that even though the second derivative of our function $f(x) = \sqrt{2x}$ is not defined at $x = 0$ (therefore is not continuous), there is no problem. The function points_linear will never evaluate the second derivative at the boundaries defined by the domain parameter.

Singularities

A singularity is a point at which the function is not defined or is not well-behaved. A typical example is $|x|$, in which the function is continuous at $x = 0$ but not differentiable (the derivative is not continuous at that point). For these kind of functions, we can use points_linear_singular. The only difference between this method and points_linear is that here we need to pass an extra parameter called singularities. This must be a Vector{T} where T<:Real containing all those singular points. The algorithm will compute the points for each subinterval delimited by this singularities vector.

AdaptiveStepSize.points_linear_singularFunction
points_linear_singular(f, domain, singularities::Vector{T}, tol::T; scan_step = (domain[end] - domain[begin]) / 100) where T <: Real

Computes points used for a linear interpolation of the function f in the given domain within a tolerance tol. Manages singular points though the array singularities.

See points_linear for an in depth description of the arguments. This function needs the additional argument singularities. This must be a Vector{T} where T<:Real that contains each singular point.

A singularity in this case is a point at which the function is not well-behaved, like abs(x) at x = 0, where the function is continuous but not differentiable.

If the function has several singularities, we can write those in the singularities vector. The function points_linear will be applied at each subinterval. In addition, the second derivative will never be computed at those endpoints, i.e., at the singularities and the domain points. Note that f will be evaluated at the endpoints and it should handle those discontinuities properly. It is the user responsability to do so.

Notes

If you have a piecewise function, it might be convenient to apply points_linear at each interval of the function, instead of passing the singularities vector here. The reason is that the result contains just one big pair xs and ys vectors and it is not aware of the points where piecewise function is not continuous, hence producing undesiderable results in the interpolation.

The safest bet here is linearly interpolate each region of the piecewise function separately.

source

Hence, let us compute the xs and ys points for the absolute value function $f(x) = |x|$.

julia> using AdaptiveStepSize
julia> f(x) = abs(x)f (generic function with 1 method)
julia> domain = (-1.0, 1.0)(-1.0, 1.0)
julia> tol = 1e-20.01
julia> singularities = [0.0,]1-element Vector{Float64}: 0.0
julia> xs, ys = points_linear_singular(f, domain, singularities, tol; scan_step = 1e-4)([-1.0, 0.0, 1.0], [1.0, 0.0, 1.0])

Since this is a simple function made of two lines that intersect at $x = 0$, we get the expected points. Note that if you call points_linear instead of points_linear_singular you will not get the point $(0.0, 0.0)$ in the results. It is very unlikely to exactly compute a certain point when doing the scan. That is why passing explicitly the singularities to the former method is preferred. We can plot the results to see how it looks like.

Example block output

Piecewise functions

If we have a piecewise function instead, it is more convenient to call points_linear at each interval of the function instead of points_linear_singular and passing the singularities vector with each case point. This is because the result contains just one big pair xs and ys vectors and it is not aware of the points where piecewise function is not continuous, hence producing undesiderable results in the interpolation.

To see this in action, let us consider the following piecewise function

\[f(x) = \begin{cases} \sin(x) &\text{if } x < 2\pi \\ (x - 8)^2 &\text{if } 2\pi \leq x \leq 10 \\ \sqrt{x} - 3 &\text{if } x > 10 \end{cases}\]

We will consider the domain $[0,\, 20]$. First, we will make use of the points_linear_singular and see why is not convenient. In Julia, we can write this as

julia> using AdaptiveStepSize
julia> function f(x) if x < 2π return sin(x) elseif x <= 10.0 return (x - 8)^2 else return sqrt(x) - 3 end endf (generic function with 1 method)
julia> domain = (0.0, 20.0)(0.0, 20.0)
julia> tol = 1e-20.01
julia> singularities = [2π, 10.0]2-element Vector{Float64}: 6.283185307179586 10.0
julia> xs, ys = points_linear_singular(f, domain, singularities, tol; scan_step = 1e-4)([0.0, 0.43549999999996836, 0.7738999999999311, 1.075499999999898, 1.3614999999998665, 1.6443999999998353, 1.9276999999998041, 2.2200000000002604, 2.5370000000009294, 2.912200000001721 … 8.484285307174456, 8.68438530717399, 8.884485307173524, 9.084585307173057, 9.284685307172591, 9.484785307172125, 9.684885307171658, 10.0, 13.181199999992586, 20.0], [0.0, 0.4218637836925768, 0.6989297890393922, 0.8798279065966838, 0.9781773602189574, 0.9972924723126391, 0.9369830917527852, 0.7965654722359292, 0.5684269858028858, 0.22738612873312744 … 0.2345322587450576, 0.4683832486756367, 0.7823142586058426, 1.1763252885356752, 1.6504163384651345, 2.2045874083942207, 2.8388384983229336, 4.0, 0.6305922381882252, 1.4721359549995796])

If we plot now the result over the piecewise $f(x)$ function, we get:

Example block output

What is happening here is that the endpoints of each segment of the piecewise function do not always have a point (blue circle). If we apply linear interpolation to the whole result, we will get wrong results near those endpoints. This is because points_linear_singular is suitable only for continuous functions. To fix this, we have to call points_linear once for each segment:

julia> using AdaptiveStepSize
julia> function f(x) if x < 2π return sin(x) elseif x <= 10.0 return (x - 8)^2 else return sqrt(x) - 3 end endf (generic function with 1 method)
julia> domain1 = (0.0, 2π - 1e-15) # Notice how 2π is not exactly included here(0.0, 6.283185307179585)
julia> domain2 = (2π, 10.0)(6.283185307179586, 10.0)
julia> domain3 = (10.0 + 1e-15, 20.0) # The same happens with x = 10.0, it is an open interval(10.000000000000002, 20.0)
julia> tol = 1e-2;
julia> xs1, ys1 = points_linear(f, domain1, tol; scan_step = 1e-4)([0.0, 0.43549999999996836, 0.7738999999999311, 1.075499999999898, 1.3614999999998665, 1.6443999999998353, 1.9276999999998041, 2.2200000000002604, 2.5370000000009294, 2.912200000001721, 3.436700000002828, 3.798700000003592, 4.110300000003759, 4.400300000003083, 4.683300000002424, 4.9662000000017645, 5.2537000000010945, 5.559300000000382, 6.283185307179585], [0.0, 0.4218637836925768, 0.6989297890393922, 0.8798279065966838, 0.9781773602189574, 0.9972924723126391, 0.9369830917527852, 0.7965654722359292, 0.5684269858028858, 0.22738612873312744, -0.2908425577262733, -0.6108291161898552, -0.8241542879031629, -0.9516942309279802, -0.9995769454427188, -0.967962526480151, -0.8570339020834412, -0.6623006839179572, -1.133107779529596e-15])
julia> xs2, ys2 = points_linear(f, domain2, tol; scan_step = 1e-4)([6.283185307179586, 6.48328530717912, 6.6833853071786535, 6.883485307178187, 7.083585307177721, 7.2836853071772545, 7.483785307176788, 7.683885307176322, 7.883985307175855, 8.084085307175389, 8.284185307174923, 8.484285307174456, 8.68438530717399, 8.884485307173524, 9.084585307173057, 9.284685307172591, 9.484785307172125, 9.684885307171658, 10.0], [2.947452689484052, 2.3004234594187367, 1.7334742493530486, 1.246605059286987, 0.8398158892205523, 0.5131067391537443, 0.266477609086563, 0.09992849901900844, 0.01345940895108062, 0.00707033888277954, 0.0807612888141052, 0.2345322587450576, 0.4683832486756367, 0.7823142586058426, 1.1763252885356752, 1.6504163384651345, 2.2045874083942207, 2.8388384983229336, 4.0])
julia> xs3, ys3 = points_linear(f, domain3, tol; scan_step = 1e-4)([10.000000000000002, 13.181199999992588, 20.0], [0.16227766016837952, 0.6305922381882252, 1.4721359549995796])

Note that when defining the domains, we are adding or substracting a very small number (1e-15). This is because the first segment is valid for $x < 2\pi$, so the first domain must be open on the right side (it cannot include the point $2\pi$). Mathematically this is represented by the interval $[0,\, 2\pi)$. On the computer, we can do the same by subtracting a very small number, i.e., domain1 = [0.0, 2π - 1e-15]. The same happens with the last segment that is only valid for $x > 10$. We cannot include the point $10$ on domain3. That is why we represent it as domain3 = [10.0 + 1e-15, 20.0]. When we plot the results we explicitly get the endpoints.

Example block output

We can now apply a linear interpolation to each segment and the result will be correct.

Use the desired tolerance

In all of these examples we are using a high tolerance of tol = 1e-2 for the sake of clarity on the figures. If you want something more precise like tol = 1e-6 you can do it, bear in mind that when visualizing it, you have a bunch of points close together that you cannot distinguish them individually from the plot.