Expline v0.1.0 Expline.Spline
Expline.Spline
is the module defining the struct and functions for
constructing cubic splines and interpolating values along them. It is the core
module and data structure for the library.
Mathematics
Expline uses information from the “Spline interpolation”, “Cholesky decomposition”, and “Triangular matrix” Wikipedia pages.
Spline interpolation requires the curvatures at the spline’s given points. To determine the curvatures, a system of linear equations needs to be solved.
There are a number of ways cubic splines can extrapolate values beyond their minimum and maximum. Currently, the only way Expline implements is “Natural Spline”. Others, such as “Clamped Spline” and “Not-A-Knot”, may be implemented depending on feedback from users.
For “Natural Splines”, a case of splines where the ends of the spline have no
curvature, and are therefore can be linearly extrapolated, the matrix X
in
the system Xk = y
(where the solution k
is the vector of curvatures for
the spline) is by definition Hermitian and positive-definite, a conclusion one
can make by analyzing the equations and enforcing certain conditions by
manipulating the input parameters. This allows us to generate the components
necessary for solving for the curvatures in a more specialized fashion, and in
the author’s experience, a fairly simple fashion.
Cholesky decomposition is a special type of Matrix decomposition that applies to Hermitian, positive-definite matrices. Its runtime is half of LU decomposition, another popular matrix decomposition.
With the Cholesky decomposition, the procedure described in the first paragraph of the “Applications” section of the “Cholesky decomposition” Wikipedia page is used to find the curvatures.
Performance
Given n
points, the algorithm for building a spline is O(n^3)
and
interpolating a value is O(n)
.
Summary
Types
The errors that can arise from improper input to from_points/1
The type used to denote the rate of change in the curvature of function at a given point
The type used to denote a value that depends on another, whether it is by
relation to an independent value in point/0
or returned by
interpolate/2
The method by which a spline’s end conditions are evaluated
The type used to denote a value that is independent and may be used to
interpolate a dependent value from interpolate/2
, reference a point, or
determine the extrema of a spline or its ranges
The errors that can arise from improper input to interpolate/2
The type used to initialize a spline and returned from Expline.interpolate/2
and Expline.interpolate/3
The type used to denote the open range within which a point is interpolated
The Expline’s internal representation of a cubic spline
Functions
Create a spline from a list of floating point pairs (tuples)
Interpolate a value from the spline
Types
The errors that can arise from improper input to from_points/1
.
:too_few_points
occurs when less than 3 points are used to create the
spline. If this error occurs, providing more points should mitigate the
issue. If you want to interpolate values between two points,
Linear interpolation
may be of more interest, but is not in the interest of this library.
{:range_too_small, point(), point()}
occurs when the distance between the
independent_value/0
in each of at least two points are too close for the
virtual machine to determine a curvature between them. If this error occurs,
increasing the resolution between the provided points or dropping points that
are too close before input are both straightforward examples of mitigation
strategies.
The type used to denote the rate of change in the curvature of function at a given point.
The type used to denote a value that depends on another, whether it is by
relation to an independent value in point/0
or returned by
interpolate/2
.
The method by which a spline’s end conditions are evaluated.
This can have a significant effect on the runtime for the spline creation.
“Natural Spline”, denoted by the :natural_spline
extrapolation method, uses
a particular shape of system of linear equations be solved that is more
performant to solce than a more general system. Currently, it is also the
only extrapolation method implemented.
The type used to denote a value that is independent and may be used to
interpolate a dependent value from interpolate/2
, reference a point, or
determine the extrema of a spline or its ranges.
The errors that can arise from improper input to interpolate/2
.
:corrupt_extrema
occurs when the minimum/maximum is not greater/less than a
point that does not fall into a range in the spline.
:corrupt_spline
occurs when the spline’s internal information does not
conform to various mathematical invariants.
For either of these values, a bug report may be needed, as in normal operation, neither should occur.
The type used to initialize a spline and returned from Expline.interpolate/2
and Expline.interpolate/3
.
The type used to denote the open range within which a point is interpolated.
The Expline’s internal representation of a cubic spline
Functions
Create a spline from a list of floating point pairs (tuples).
This function bootstraps a spline and prepares it for use in the
interpolate/2
function.
If fewer than 3 points are passed to the function, the function will
short-circuit and return {:error, :too_few_points}
. This is a mathematical
constraint. A cubic spline cannot be built on fewer than 3 points.
Due to the constraints of Erlang and Elixir, there is a limit on how close
points can be. If points are too close to each other, this leads to a
situation where an error similar to dividing by zero occurs. In light of
this, there is a check that occurs and short-circuits the construction of the
spline. It returns {:error, {:range_too_small, p1, p2}}
where p1
and p2
are two point/0
s that are too close together. If this error is
encountered, mitigating it is use-case specific. In order to find the points
that are too close, the following snippet may prove useful:
points
|> Enum.group_by(fn ({x, y}) -> Float.round(x, 15) end)
Float.round/2
has a maximum precision of 15, and while floating point
numbers appear to have higher precision, with representations with much
higher points of precision, being able to reliably reconcile points is
important.
If this is an issue, a smaller closeness threshold will be used, but until
then, the threshold for determining points that are too close is 1.0e-15, the
smallest value that can’t be rounded by Float.round/2
.
Examples
# A point that is too close
iex> Expline.Spline.from_points([{0.0, 0.0}, {1.0, 1.0}, {5.0e-324, 0.0}])
{:error, {:range_too_small, {0.0, 0.0}, {5.0e-324, 0.0}}}
# Not enough points
iex> Expline.Spline.from_points([{0.0, 0.0}, {1.0, 1.0}])
{:error, :too_few_points}
# Well-spaced, enough points
iex> Expline.Spline.from_points([{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}])
{:ok, %Expline.Spline{derivatives: %{0.0 => 0.9999999999999999,
1.0 => 0.9999999999999999,
2.0 => 1.0000000000000002},
extrapolation_method: :natural_spline,
max: 2.0, min: 0.0,
points: %{0.0 => 0.0, 1.0 => 1.0, 2.0 => 2.0},
ranges: [{0.0, 1.0}, {1.0, 2.0}]}}
interpolate(t, independent_value) :: {:ok, dependent_value} | {:error, interpolation_error} | {:error, :corrupt_spline}
Interpolate a value from the spline.
In regular usage, when the function is given a float and a spline, it will
return a tuple of {:ok, float}
corresponding to the interpolated value of
dependent variable from the given value of the independent variable and the
spline.
If any of the invariants of the spline’s internal representation are not
satisfied, then {:error, :corrupt_spline}
will be returned. If this
happens, please report it, as that would be a sign that there is an issue with
the underlying data structures or algorithms used in the library.
Examples
iex> with {:ok, spline} <- Expline.Spline.from_points([{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}]),
...> do: Expline.Spline.interpolate(spline, -0.5)
{:ok, -0.49999999999999994}
iex> with {:ok, spline} <- Expline.Spline.from_points([{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}]),
...> do: Expline.Spline.interpolate(spline, 0.5)
{:ok, 0.5}
iex> with {:ok, spline} <- Expline.Spline.from_points([{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}]),
...> do: Expline.Spline.interpolate(spline, 1.5)
{:ok, 1.5}
iex> with {:ok, spline} <- Expline.Spline.from_points([{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}]),
...> do: Expline.Spline.interpolate(spline, 2.5)
{:ok, 2.5}