diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/README.md b/README.md index 8ca55f5..6610226 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ by adding `expline` to your list of dependencies in `mix.exs`: ```elixir def deps do - [{:expline, "~> 0.1.0"}] + [{:expline, "~> 0.2"}] end ``` diff --git a/lib/expline.ex b/lib/expline.ex index d972390..a5573e0 100644 --- a/lib/expline.ex +++ b/lib/expline.ex @@ -13,7 +13,7 @@ defmodule Expline do building, read the `Expline.Spline` module documentation. """ - @typep state() :: Expline.Spline.t + @typep state() :: Expline.Spline.t() @doc """ Builds a spline from the provided list of points and holds the state in a @@ -21,11 +21,17 @@ defmodule Expline do See `start_link/2` for more information. """ - @spec start(list(Expline.Spline.point()), GenServer.options()) :: {:ok, pid()} - | {:error, {:already_started, pid()}} - | {:error, Expline.Spline.creation_error()} + @spec start(list(Expline.Spline.point()), {:graceful_shutdown, boolean()} | GenServer.options()) :: + {:ok, pid()} + | {:error, {:already_started, pid()}} + | {:error, Expline.Spline.creation_error()} def start(points, opts \\ []) do - GenServer.start(__MODULE__, [points], opts) + {graceful_shutdown, opts} = Keyword.pop(opts, :graceful_shutdown, false) + + case GenServer.start(__MODULE__, {graceful_shutdown, [points]}, opts) do + {:error, {:shutdown, reason}} -> {:error, reason} + other -> other + end end @doc """ @@ -36,21 +42,34 @@ defmodule Expline do ## Options and more information + - `graceful_shutdown` when `true`, gracefully shuts down `GenServer` + without a crash report, default: `false` + See `GenServer.start_link/3` for more information. """ - @spec start_link(list(Expline.Spline.point()), GenServer.options()) :: {:ok, pid()} - | {:error, {:already_started, pid()}} - | {:error, Expline.Spline.creation_error()} + @spec start_link( + list(Expline.Spline.point()), + {:graceful_shutdown, boolean()} | GenServer.options() + ) :: + {:ok, pid()} + | {:error, {:already_started, pid()}} + | {:error, Expline.Spline.creation_error()} def start_link(points, opts \\ []) do - GenServer.start_link(__MODULE__, [points], opts) + {graceful_shutdown, opts} = Keyword.pop(opts, :graceful_shutdown, false) + + case GenServer.start_link(__MODULE__, {graceful_shutdown, [points]}, opts) do + {:error, {:shutdown, reason}} -> {:error, reason} + other -> other + end end - def init([list_of_points]) do + def init({graceful_shutdown, [list_of_points]}) do case Expline.Spline.from_points(list_of_points) do {:ok, spline} -> {:ok, spline} + {:error, reason} -> - {:stop, reason} + {:stop, if(graceful_shutdown, do: {:shutdown, reason}, else: reason)} end end @@ -61,17 +80,20 @@ defmodule Expline do `t:Expline.Spline.interpolation_error/0` will be returned. """ - @spec interpolate(GenServer.server(), float(), timeout()) :: {:ok, Expline.Spline.point()} - | {:error, Expline.Spline.interpolation_error()} + @spec interpolate(GenServer.server(), float(), timeout()) :: + {:ok, Expline.Spline.point()} + | {:error, Expline.Spline.interpolation_error()} def interpolate(server, x, timeout \\ 5000) when is_float(x) do GenServer.call(server, {:interpolate, x}, timeout) end - @spec handle_call({:interpolate, Expline.Spline.dependent_value()}, GenServer.from(), state()) :: {:reply, {:ok, Expline.Spline.point()}, state()} + @spec handle_call({:interpolate, Expline.Spline.dependent_value()}, GenServer.from(), state()) :: + {:reply, {:ok, Expline.Spline.point()}, state()} def handle_call({:interpolate, x}, _from, spline) do case Expline.Spline.interpolate(spline, x) do {:ok, y} -> {:reply, {:ok, {x, y}}, spline} + {:error, reason} -> {:reply, {:error, reason}, spline} end diff --git a/lib/expline/matrix.ex b/lib/expline/matrix.ex index c54339e..1341280 100644 --- a/lib/expline/matrix.ex +++ b/lib/expline/matrix.ex @@ -4,86 +4,93 @@ defmodule Expline.Matrix do @enforce_keys [:n_rows, :m_cols, :internal] defstruct [:n_rows, :m_cols, :internal] - @type t() :: %__MODULE__{ n_rows: pos_integer(), - m_cols: pos_integer(), - internal: internal() } + @type t() :: %__MODULE__{n_rows: pos_integer(), m_cols: pos_integer(), internal: internal()} @type vector() :: Expline.Vector.t() @typep internal() :: tuple() - @typep binary_op() :: ( float(), float() -> float() ) - @typep unary_op() :: ( float() -> float() ) + @typep binary_op() :: (float(), float() -> float()) + @typep unary_op() :: (float() -> float()) @spec zeros(pos_integer(), pos_integer()) :: __MODULE__.t() def zeros(n_rows, m_cols) do - construct(n_rows, m_cols, fn (_, _) -> 0.0 end) + construct(n_rows, m_cols, fn _, _ -> 0.0 end) end @spec identity(pos_integer()) :: __MODULE__.t() def identity(n) do construct(n, n, fn - (i, i) -> 1.0 - (_i, _j) -> 0.0 + i, i -> 1.0 + _i, _j -> 0.0 end) end - @spec sub(__MODULE__.t(), __MODULE__.t()) :: {:ok, __MODULE__.t()} - | {:error, :dimension_mismatch} + @spec sub(__MODULE__.t(), __MODULE__.t()) :: + __MODULE__.t() | {:error, :dimension_mismatch} def sub(%__MODULE__{} = a, %__MODULE__{} = b), do: do_binary_op(a, b, &Kernel.-/2) - @spec add(__MODULE__.t(), __MODULE__.t()) :: {:ok, __MODULE__.t()} - | {:error, :dimension_mismatch} + @spec add(__MODULE__.t(), __MODULE__.t()) :: + __MODULE__.t() | {:error, :dimension_mismatch} def add(%__MODULE__{} = a, %__MODULE__{} = b), do: do_binary_op(a, b, &Kernel.+/2) - @spec do_binary_op(__MODULE__.t(), __MODULE__.t(), binary_op()) :: {:ok, __MODULE__.t()} - | {:error, :dimension_mismatch} - defp do_binary_op(%__MODULE__{ n_rows: n_rows, m_cols: m_cols } = a, - %__MODULE__{ n_rows: n_rows, m_cols: m_cols } = b, op) - when is_function(op, 2) do + @spec do_binary_op(__MODULE__.t(), __MODULE__.t(), binary_op()) :: + __MODULE__.t() | {:error, :dimension_mismatch} + defp do_binary_op( + %__MODULE__{n_rows: n_rows, m_cols: m_cols} = a, + %__MODULE__{n_rows: n_rows, m_cols: m_cols} = b, + op + ) + when is_function(op, 2) do construct(n_rows, m_cols, fn - (i, j) -> op.(at(a, i, j), at(b, i, j)) + i, j -> op.(at(a, i, j), at(b, i, j)) end) end + defp do_binary_op(%__MODULE__{}, %__MODULE__{}, _op), - do: {:error, :dimension_mismatch} + do: {:error, :dimension_mismatch} @spec scale(__MODULE__.t(), float()) :: __MODULE__.t() def scale(%__MODULE__{} = matrix, scalar) - when is_float(scalar) do + when is_float(scalar) do transform(matrix, &(scalar * &1)) end @spec transform(__MODULE__.t(), unary_op()) :: __MODULE__.t() - def transform(%__MODULE__{ n_rows: n_rows, m_cols: m_cols } = matrix, op) - when is_function(op, 1) do + def transform(%__MODULE__{n_rows: n_rows, m_cols: m_cols} = matrix, op) + when is_function(op, 1) do construct(n_rows, m_cols, fn - (i, j) -> - matrix |> at(i, j) |> op.() + i, j -> + matrix |> at(i, j) |> op.() end) end - @spec construct(pos_integer(), pos_integer(), (non_neg_integer(), non_neg_integer() -> float())) :: __MODULE__.t() + @spec construct(pos_integer(), pos_integer(), (non_neg_integer(), non_neg_integer() -> float())) :: + __MODULE__.t() def construct(n_rows, m_cols, elem_fn) - when n_rows > 0 - and m_cols > 0 - and is_function(elem_fn, 2) do - internal = 0..(n_rows - 1) - |> Enum.reduce({}, fn (i, matrix) -> - row = 0..(m_cols - 1) - |> Enum.reduce({}, fn (j, row) -> - Tuple.append(row, elem_fn.(i, j)) + when n_rows > 0 and + m_cols > 0 and + is_function(elem_fn, 2) do + internal = + 0..(n_rows - 1) + |> Enum.reduce({}, fn i, matrix -> + row = + 0..(m_cols - 1) + |> Enum.reduce({}, fn j, row -> + Tuple.append(row, elem_fn.(i, j)) + end) + + Tuple.append(matrix, row) end) - Tuple.append(matrix, row) - end) - %__MODULE__{ n_rows: n_rows, m_cols: m_cols, internal: internal } + + %__MODULE__{n_rows: n_rows, m_cols: m_cols, internal: internal} end @spec at(__MODULE__.t(), non_neg_integer(), non_neg_integer()) :: float() - def at(%__MODULE__{ n_rows: n_rows, m_cols: m_cols, internal: internal }, i, j) - when is_integer(i) - and i < n_rows - and is_integer(j) - and j < m_cols do + def at(%__MODULE__{n_rows: n_rows, m_cols: m_cols, internal: internal}, i, j) + when is_integer(i) and + i < n_rows and + is_integer(j) and + j < m_cols do internal |> elem(i) |> elem(j) @@ -92,7 +99,7 @@ defmodule Expline.Matrix do @spec transpose(__MODULE__.t()) :: __MODULE__.t() def transpose(%__MODULE__{} = matrix) do construct(matrix.m_cols, matrix.n_rows, fn - (i, j) -> at(matrix, j, i) + i, j -> at(matrix, j, i) end) end @@ -102,115 +109,150 @@ defmodule Expline.Matrix do end @spec lower_triangular?(__MODULE__.t()) :: boolean() - def lower_triangular?(%__MODULE__{ n_rows: n_rows, m_cols: m_cols } = matrix) do - for i <- 0..(n_rows-1), j <- 0..(m_cols-1), i < j do + def lower_triangular?(%__MODULE__{n_rows: n_rows, m_cols: m_cols} = matrix) do + for i <- 0..(n_rows - 1), j <- 0..(m_cols - 1), i < j do at(matrix, i, j) - end |> Enum.all?(fn (0.0) -> true; (_) -> false end) + end + |> Enum.all?(fn + floating_zero when floating_zero in [+0.0, -0.0] -> true + _ -> false + end) end @spec upper_triangular?(__MODULE__.t()) :: boolean() - def upper_triangular?(%__MODULE__{ n_rows: n_rows, m_cols: m_cols } = matrix) do - for i <- 0..(n_rows-1), j <- 0..(m_cols-1), i > j do + def upper_triangular?(%__MODULE__{n_rows: n_rows, m_cols: m_cols} = matrix) do + for i <- 0..(n_rows - 1), j <- 0..(m_cols - 1), i > j do at(matrix, i, j) - end |> Enum.all?(fn (0.0) -> true; (_) -> false end) + end + |> Enum.all?(fn + floating_zero when floating_zero in [+0.0, -0.0] -> true + _ -> false + end) end @spec positive_definite?(__MODULE__.t()) :: boolean() - def positive_definite?(%__MODULE__{ n_rows: n, m_cols: n } = matrix) do + def positive_definite?(%__MODULE__{n_rows: n, m_cols: n} = matrix) do case cholesky_decomposition(matrix) do {:ok, _} -> true {:error, _} -> false end end - @spec cholesky_decomposition(__MODULE__.t()) :: {:ok, __MODULE__.t()} - | {:error, :not_square} - | {:error, :not_symmetric} - | {:error, :not_positive_definite} - def cholesky_decomposition(%__MODULE__{ n_rows: n, m_cols: n } = matrix) do + @spec cholesky_decomposition(__MODULE__.t()) :: + {:ok, __MODULE__.t()} + | {:error, :not_square} + | {:error, :not_symmetric} + | {:error, :not_positive_definite} + def cholesky_decomposition(%__MODULE__{n_rows: n, m_cols: n} = matrix) do if symmetric?(matrix) do - l_internal = 0..(n - 1) - |> Enum.reduce_while(matrix.internal, fn (i, mat_l) -> - row_a_i = elem(matrix.internal, i) - row_l_i = elem(mat_l, i) - new_row = 0..(n - 1) - |> Enum.reduce_while(row_l_i, fn (j, row_l_i) -> - cond do - i == j -> - summation = for k <- 0..(j-1), k >= 0, k <= (j-1) do - l_jk = elem(row_l_i, k) - :math.pow(l_jk, 2) - end |> Enum.sum - - a_jj = elem(row_a_i, j) - case a_jj - summation do - value when value < 0.0 -> - {:halt, {:error, :not_positive_definite}} - value -> - new_row = put_elem(row_l_i, j, :math.sqrt(value)) + l_internal = + 0..(n - 1) + |> Enum.reduce_while(matrix.internal, fn i, mat_l -> + row_a_i = elem(matrix.internal, i) + row_l_i = elem(mat_l, i) + + new_row = + 0..(n - 1) + |> Enum.reduce_while(row_l_i, fn j, row_l_i -> + cond do + i == j -> + summation = + for k <- 0..(j - 1), k >= 0, k <= j - 1 do + l_jk = elem(row_l_i, k) + :math.pow(l_jk, 2) + end + |> Enum.sum() + + a_jj = elem(row_a_i, j) + + case a_jj - summation do + value when value < 0.0 -> + {:halt, {:error, :not_positive_definite}} + + value -> + new_row = put_elem(row_l_i, j, :math.sqrt(value)) + {:cont, new_row} + end + + i > j -> + summation = + for k <- 0..(j - 1), k >= 0, k <= j - 1 do + row_l_j = elem(mat_l, j) + l_ik = elem(row_l_i, k) + l_jk = elem(row_l_j, k) + l_ik * l_jk + end + |> Enum.sum() + + a_ij = elem(row_a_i, j) + l_jj = mat_l |> elem(j) |> elem(j) + new_row = put_elem(row_l_i, j, (a_ij - summation) / l_jj) {:cont, new_row} + + # i < j + true -> + {:cont, put_elem(row_l_i, j, 0.0)} end + end) + + case new_row do + {:error, :not_positive_definite} -> + {:halt, new_row} - i > j -> - summation = for k <- 0..(j-1), k >= 0, k <= (j-1) do - row_l_j = elem(mat_l, j) - l_ik = elem(row_l_i, k) - l_jk = elem(row_l_j, k) - l_ik * l_jk - end |> Enum.sum - - a_ij = elem(row_a_i, j) - l_jj = mat_l |> elem(j) |> elem(j) - new_row = put_elem(row_l_i, j, (a_ij - summation) / l_jj) - {:cont, new_row} - # i < j - true -> {:cont, put_elem(row_l_i, j, 0.0)} + _row -> + {:cont, put_elem(mat_l, i, new_row)} end end) - case new_row do - {:error, :not_positive_definite} -> - {:halt, new_row} - row -> - {:cont, put_elem(mat_l, i, new_row)} - end - end) + case l_internal do {:error, :not_positive_definite} -> {:error, :not_positive_definite} + _ -> - {:ok, %{ matrix | internal: l_internal }} + {:ok, %{matrix | internal: l_internal}} end else {:error, :not_symmetric} end end + def cholesky_decomposition(%__MODULE__{}), do: {:error, :not_square} - @spec product(__MODULE__.t(), __MODULE__.t()) :: {:ok, __MODULE__.t()} - | {:error, :dimension_mismatch} - def product(%__MODULE__{ n_rows: a_rows, m_cols: a_cols, internal: a_internal }, - %__MODULE__{ n_rows: b_rows, m_cols: b_cols } = b) - when a_cols == b_rows do + @spec product(__MODULE__.t(), __MODULE__.t()) :: + {:ok, __MODULE__.t()} + | {:error, :dimension_mismatch} + def product( + %__MODULE__{n_rows: a_rows, m_cols: a_cols, internal: a_internal}, + %__MODULE__{n_rows: b_rows, m_cols: b_cols} = b + ) + when a_cols == b_rows do b_internal = transpose(b).internal - c = construct(a_rows, b_cols, fn - (i, j) -> - as = elem(a_internal, i) |> Tuple.to_list - bs = elem(b_internal, j) |> Tuple.to_list - - Enum.zip(as, bs) - |> Enum.map(fn ({a_ik, b_kj}) -> a_ik * b_kj end) - |> Enum.sum - end) + + c = + construct(a_rows, b_cols, fn + i, j -> + as = elem(a_internal, i) |> Tuple.to_list() + bs = elem(b_internal, j) |> Tuple.to_list() + + Enum.zip(as, bs) + |> Enum.map(fn {a_ik, b_kj} -> a_ik * b_kj end) + |> Enum.sum() + end) + {:ok, c} end + def product(%__MODULE__{}, %__MODULE__{}), do: {:error, :dimension_mismatch} - @spec forward_substitution(__MODULE__.t(), vector()) :: {:ok, vector()} - | {:error, :dimension_mismatch} - | {:error, :not_lower_triangular} - def forward_substitution(%__MODULE__{ n_rows: n_rows } = matrix, - %Expline.Vector{ n_slots: n_slots } = vector) - when n_rows == n_slots do + @spec forward_substitution(__MODULE__.t(), vector()) :: + {:ok, vector()} + | {:error, :dimension_mismatch} + | {:error, :not_lower_triangular} + def forward_substitution( + %__MODULE__{n_rows: n_rows} = matrix, + %Expline.Vector{n_slots: n_slots} = vector + ) + when n_rows == n_slots do if lower_triangular?(matrix) do solution = do_forward_substitution(matrix, vector, 0, {}) {:ok, solution} @@ -218,80 +260,104 @@ defmodule Expline.Matrix do {:error, :not_lower_triangular} end end + def forward_substitution(%__MODULE__{}, %Expline.Vector{}), do: {:error, :dimension_mismatch} @spec do_forward_substitution(__MODULE__.t(), vector(), integer(), tuple()) :: vector() - defp do_forward_substitution(_matrix, %Expline.Vector{ n_slots: n_slots }, _row, solution) - when n_slots == tuple_size(solution) do + defp do_forward_substitution(_matrix, %Expline.Vector{n_slots: n_slots}, _row, solution) + when n_slots == tuple_size(solution) do Expline.Vector.construct(tuple_size(solution), fn - (i) -> elem(solution, i) + i -> elem(solution, i) end) end + defp do_forward_substitution(matrix, vector, nth_row, solution) do - summation = for i <- 0..(nth_row-1), i >= 0, i <= nth_row-1 do - at(matrix, nth_row, i) * elem(solution, i) - end |> Enum.sum + summation = + for i <- 0..(nth_row - 1), i >= 0, i <= nth_row - 1 do + at(matrix, nth_row, i) * elem(solution, i) + end + |> Enum.sum() + new_solution = (Expline.Vector.at(vector, nth_row) - summation) / at(matrix, nth_row, nth_row) do_forward_substitution(matrix, vector, nth_row + 1, Tuple.append(solution, new_solution)) end - @spec backward_substitution(__MODULE__.t(), vector()) :: {:ok, vector()} - | {:error, :dimension_mismatch} - | {:error, :not_upper_triangular} - def backward_substitution(%__MODULE__{ n_rows: n_rows } = matrix, - %Expline.Vector{ n_slots: n_slots } = vector) - when n_rows == n_slots do + @spec backward_substitution(__MODULE__.t(), vector()) :: + {:ok, vector()} + | {:error, :dimension_mismatch} + | {:error, :not_upper_triangular} + def backward_substitution( + %__MODULE__{n_rows: n_rows} = matrix, + %Expline.Vector{n_slots: n_slots} = vector + ) + when n_rows == n_slots do if upper_triangular?(matrix) do - sln_buffer = (1..n_rows) |> Enum.reduce({}, fn (_, t) -> Tuple.append(t, 0.0) end) + sln_buffer = 1..n_rows |> Enum.reduce({}, fn _, t -> Tuple.append(t, 0.0) end) solution = do_backward_substitution(matrix, vector, n_rows - 1, sln_buffer) {:ok, solution} else {:error, :not_upper_triangular} end end + def backward_substitution(%__MODULE__{}, %Expline.Vector{}), do: {:error, :dimension_mismatch} @spec do_backward_substitution(__MODULE__.t(), vector(), integer(), tuple()) :: vector() defp do_backward_substitution(_matrix, _vector, -1, solution) do Expline.Vector.construct(tuple_size(solution), fn - (i) -> elem(solution, i) + i -> elem(solution, i) end) end + defp do_backward_substitution(matrix, vector, nth_row, solution) do - summation = for i <- nth_row..(matrix.n_rows-1), - i >= 0, - i <= matrix.n_rows do - at(matrix, nth_row, i) * elem(solution, i) - end |> Enum.sum + summation = + for i <- nth_row..(matrix.n_rows - 1), + i >= 0, + i <= matrix.n_rows do + at(matrix, nth_row, i) * elem(solution, i) + end + |> Enum.sum() + new_solution = (Expline.Vector.at(vector, nth_row) - summation) / at(matrix, nth_row, nth_row) - do_backward_substitution(matrix, vector, nth_row - 1, put_elem(solution, nth_row, new_solution)) + + do_backward_substitution( + matrix, + vector, + nth_row - 1, + put_elem(solution, nth_row, new_solution) + ) end - @spec disaugment(__MODULE__.t()) :: {:ok, {__MODULE__.t(), vector()}} - | {:error, :dimension_mismatch} - def disaugment(%__MODULE__{ n_rows: n_rows, m_cols: m_cols } = matrix) - when m_cols > 1 do - augment = Expline.Vector.construct(n_rows, fn - (i) -> - at(matrix, i, m_cols-1) - end) - disaugmented_matrix = construct(n_rows, m_cols - 1, fn - (i, j) -> - at(matrix, i, j) - end) + @spec disaugment(__MODULE__.t()) :: + {:ok, {__MODULE__.t(), vector()}} + | {:error, :dimension_mismatch} + def disaugment(%__MODULE__{n_rows: n_rows, m_cols: m_cols} = matrix) + when m_cols > 1 do + augment = + Expline.Vector.construct(n_rows, fn + i -> + at(matrix, i, m_cols - 1) + end) + + disaugmented_matrix = + construct(n_rows, m_cols - 1, fn + i, j -> + at(matrix, i, j) + end) {:ok, {disaugmented_matrix, augment}} end + def disaugment(%__MODULE__{}), do: {:error, :dimension_mismatch} end defimpl Inspect, for: Expline.Matrix do import Inspect.Algebra - def inspect(%Expline.Matrix{ internal: internal }, opts) do + def inspect(%Expline.Matrix{internal: internal}, opts) do internal - |> Tuple.to_list - |> Enum.map(&(to_doc(&1, opts))) + |> Tuple.to_list() + |> Enum.map(&to_doc(&1, opts)) |> Enum.intersperse(break("\n")) |> concat end diff --git a/lib/expline/spline.ex b/lib/expline/spline.ex index 77fa7dc..9b1023f 100644 --- a/lib/expline/spline.ex +++ b/lib/expline/spline.ex @@ -1,5 +1,3 @@ -require IEx - defmodule Expline.Spline do alias Expline.Matrix alias Expline.Vector @@ -52,11 +50,13 @@ defmodule Expline.Spline do """ @typedoc "The Expline's internal representation of a cubic spline" - @opaque t :: %__MODULE__{ min: independent_value(), - max: independent_value(), - ranges: :ordsets.ordset(range()), - points: %{ required(independent_value()) => dependent_value() }, - derivatives: %{ required(independent_value()) => curvature() } } + @opaque t :: %__MODULE__{ + min: independent_value(), + max: independent_value(), + ranges: :ordsets.ordset(range()), + points: %{required(independent_value()) => dependent_value()}, + derivatives: %{required(independent_value()) => curvature()} + } @typedoc """ The method by which a spline's end conditions are evaluated. @@ -70,10 +70,8 @@ defmodule Expline.Spline do """ @type extrapolation_method :: :natural_spline - @enforce_keys [ :min, :max, :ranges, :points, - :derivatives, :extrapolation_method ] - defstruct [ :min, :max, :ranges, :points, - :derivatives, :extrapolation_method ] + @enforce_keys [:min, :max, :ranges, :points, :derivatives, :extrapolation_method] + defstruct [:min, :max, :ranges, :points, :derivatives, :extrapolation_method] @typedoc """ The type used to denote a value that is independent and may be used to @@ -122,9 +120,9 @@ defmodule Expline.Spline do are too close before input are both straightforward examples of mitigation strategies. """ - @type creation_error() :: :too_few_points - | {:range_too_small, point(), point()} - + @type creation_error() :: + :too_few_points + | {:range_too_small, point(), point()} @typedoc """ The errors that can arise from improper input to `interpolate/2`. @@ -139,8 +137,9 @@ defmodule Expline.Spline do [bug report](https://github.com/isaacsanders/expline/issues) may be needed, as in normal operation, neither should occur. """ - @type interpolation_error() :: :corrupt_extrema - | :corrupt_spline + @type interpolation_error() :: + :corrupt_extrema + | :corrupt_spline @doc """ Create a spline from a list of floating point pairs (tuples). @@ -196,16 +195,15 @@ defmodule Expline.Spline do ranges: [{0.0, 1.0}, {1.0, 2.0}]}} """ - @spec from_points(list(point())) :: {:ok, t()} - | {:error, creation_error()} + @spec from_points(list(point())) :: + {:ok, t()} + | {:error, creation_error()} def from_points(list_of_points) when length(list_of_points) >= 3 do points = Map.new(list_of_points) - xs = points - |> Map.keys + xs = Map.keys(points) - min = xs |> Enum.min - max = xs |> Enum.max + {min, max} = Enum.min_max(xs) ranges = make_ranges(xs) case Enum.find(ranges, &range_too_small?/1) do @@ -213,27 +211,32 @@ defmodule Expline.Spline do y1 = Map.get(points, x1) y2 = Map.get(points, x2) {:error, {:range_too_small, {x1, y1}, {x2, y2}}} + nil -> derivatives = make_derivatives(points) - spline = %__MODULE__{ min: min, - max: max, - ranges: :ordsets.from_list(ranges), - points: points, - derivatives: derivatives, - extrapolation_method: :natural_spline } + spline = %__MODULE__{ + min: min, + max: max, + ranges: :ordsets.from_list(ranges), + points: points, + derivatives: derivatives, + extrapolation_method: :natural_spline + } + {:ok, spline} end end + def from_points(list_of_points) when length(list_of_points) < 3, - do: {:error, :too_few_points} + do: {:error, :too_few_points} @spec make_ranges(list(independent_value())) :: list(range()) defp make_ranges(xs) do xs - |> Enum.sort - |> Enum.chunk(2, 1) - |> Enum.map(fn ([x1, x2]) -> {x1, x2} end) + |> Enum.sort() + |> Enum.chunk_every(2, 1, :discard) + |> Enum.map(&List.to_tuple/1) end @spec range_too_small?(range()) :: boolean() @@ -272,16 +275,19 @@ defmodule Expline.Spline do ...> do: Expline.Spline.interpolate(spline, 2.5) {:ok, 2.5} """ - @spec interpolate(t(), independent_value()) :: {:ok, dependent_value()} - | {:error, interpolation_error()} - | {:error, :corrupt_spline} + @spec interpolate(t(), independent_value()) :: + {:ok, dependent_value()} + | {:error, interpolation_error()} + | {:error, :corrupt_spline} def interpolate(%__MODULE__{} = spline, x) when is_float(x) do with :error <- Map.fetch(spline.points, x) do - case :ordsets.filter(fn ({x1, x2}) -> x1 < x and x < x2 end, spline.ranges) do + case :ordsets.filter(fn {x1, x2} -> x1 < x and x < x2 end, spline.ranges) do [{_x1, _x2} = range] -> do_interpolate(spline, range, x) + [] -> extrapolate(spline, x) + _ranges -> {:error, :corrupt_spline} end @@ -306,8 +312,9 @@ defmodule Expline.Spline do {:ok, y} end - @spec extrapolate(t(), independent_value()) :: {:ok, dependent_value()} - | {:error, :corrupt_extrema} + @spec extrapolate(t(), independent_value()) :: + {:ok, dependent_value()} + | {:error, :corrupt_extrema} defp extrapolate(spline, x) do cond do spline.min > x -> @@ -315,73 +322,163 @@ defmodule Expline.Spline do min_y = Map.get(spline.points, spline.min) y = (x - spline.min) * min_curvature + min_y {:ok, y} + spline.max < x -> max_curvature = Map.get(spline.derivatives, spline.max) max_y = Map.get(spline.points, spline.max) y = (x - spline.max) * max_curvature + max_y {:ok, y} - true -> {:error, :corrupt_extrema} + + true -> + {:error, :corrupt_extrema} + end + end + + @doc """ + Interpolate a curvature from the spline. + + ## 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_curvature(spline, 0.5) + {:ok, 1.0} + """ + @spec interpolate_curvature(t(), independent_value()) :: + {:ok, curvature()} + | {:error, interpolation_error()} + | {:error, :corrupt_spline} + def interpolate_curvature(%__MODULE__{} = spline, x) when is_float(x) do + with :error <- Map.fetch(spline.derivatives, x) do + case :ordsets.filter(fn {x1, x2} -> x1 < x and x < x2 end, spline.ranges) do + [{_x1, _x2} = range] -> + do_interpolate_curvature(spline, range, x) + + [] -> + extrapolate_curvature(spline, x) + + _ranges -> + {:error, :corrupt_spline} + end + end + end + + @spec do_interpolate_curvature(t(), range(), independent_value()) :: {:ok, dependent_value()} + defp do_interpolate_curvature(%__MODULE__{} = spline, {x1, x2}, x) do + y1 = Map.get(spline.points, x1) + y2 = Map.get(spline.points, x2) + + k1 = Map.get(spline.derivatives, x1) + k2 = Map.get(spline.derivatives, x2) + + # Described by equations (1), (2), (3), and (4) on + # https://en.wikipedia.org/wiki/Spline_interpolation + t = (x - x1) / (x2 - x1) + a = k1 * (x2 - x1) - (y2 - y1) + b = -k2 * (x2 - x1) + (y2 - y1) + + dy = (y2 - y1 + (1 - 2 * t) * (a * (1 - t) + b * t) + t * (1 - t) * (b - a)) / (x2 - x1) + {:ok, dy} + end + + @spec extrapolate_curvature(t(), independent_value()) :: + {:ok, dependent_value()} + | {:error, :corrupt_extrema} + defp extrapolate_curvature(spline, x) do + cond do + spline.min > x -> + {:ok, Map.get(spline.derivatives, spline.min)} + + spline.max < x -> + {:ok, Map.get(spline.derivatives, spline.max)} + + true -> + {:error, :corrupt_extrema} end end - @spec make_derivatives(%{ required(independent_value()) => dependent_value() }) :: %{ required(independent_value()) => curvature() } + @spec make_derivatives(%{required(independent_value()) => dependent_value()}) :: %{ + required(independent_value()) => curvature() + } defp make_derivatives(points) do n = map_size(points) - 1 - xs = points - |> Map.keys - |> Enum.sort + xs = + points + |> Map.keys() + |> Enum.sort() [x0, x1] = Enum.take(xs, 2) - [y0, y1] = Enum.take(xs, 2) |> Enum.map(&(Map.get(points, &1))) + [y0, y1] = Enum.take(xs, 2) |> Enum.map(&Map.get(points, &1)) [xn_1, xn] = Enum.drop(xs, n - 1) - [yn_1, yn] = Enum.drop(xs, n - 1) |> Enum.map(&(Map.get(points, &1))) + [yn_1, yn] = Enum.drop(xs, n - 1) |> Enum.map(&Map.get(points, &1)) # Described by equations (15), (16), and (17) on # https://en.wikipedia.org/wiki/Spline_interpolation - system_of_eqns = Expline.Matrix.construct(n + 1, n + 2, fn - # first row - (0, 0) -> 2 / (x1 - x0) - (0, 1) -> 1 / (x1 - x0) - # = - (0, j) when j == n + 1 -> 3.0 * ((y1 - y0) / :math.pow(x1 - x0, 2)) - - # last row - (^n, j) when j == n - 1 -> 1 / (xn - xn_1) - (^n, ^n) -> 2 / (xn - xn_1) - # = - (^n, j) when j == n + 1 -> 3.0 * ((yn - yn_1) / :math.pow(xn - xn_1, 2)) - - # middle rows - (i, j) when j == i - 1 -> - [xi_1, xi] = Enum.map(-1..0, fn (offset) -> Enum.at(xs, i + offset) end) - 1.0 / (xi - xi_1) - (i, i) -> - [xi_1, xi, xi1] = Enum.map(-1..1, fn (offset) -> Enum.at(xs, i + offset) end) - 2.0 * ((1.0 / (xi - xi_1)) + (1.0 / (xi1 - xi))) - (i, j) when j == i + 1 -> - [xi, xi1] = Enum.map(0..1, fn (offset) -> Enum.at(xs, i + offset) end) - 1.0 / (xi1 - xi) - # = - (i, j) when j == n + 1 -> - [xi_1, xi, xi1] = Enum.map(-1..1, fn (offset) -> Enum.at(xs, i + offset) end) - [yi_1, yi, yi1] = [xi_1, xi, xi1] - |> Enum.map(&(Map.get(points, &1))) - 3.0 * ( - ((yi - yi_1) / :math.pow(xi - xi_1, 2)) + - ((yi1 - yi) / :math.pow(xi1 - xi, 2)) - ) - - # empty terms - (_i, _j) -> 0.0 - end) - - with {:ok, {matrix, vector}} <- Matrix.disaugment(system_of_eqns), - {:ok, l} <- Matrix.cholesky_decomposition(matrix), - {:ok, y} <- Matrix.forward_substitution(l, vector), - {:ok, derivative_vector} <- l |> Matrix.transpose |> Matrix.backward_substitution(y) do - Enum.zip(xs, Vector.to_list(derivative_vector)) |> Map.new + system_of_eqns = + Expline.Matrix.construct(n + 1, n + 2, fn + # first row + 0, 0 -> + 2 / subtract(x1, x0) + + 0, 1 -> + 1 / subtract(x1, x0) + + # = + 0, j when j == n + 1 -> + 3.0 * (subtract(y1, y0) / :math.pow(subtract(x1, x0), 2)) + + # last row + ^n, j when j == n - 1 -> + 1 / subtract(xn, xn_1) + + ^n, ^n -> + 2 / subtract(xn, xn_1) + + # = + ^n, j when j == n + 1 -> + 3.0 * (subtract(yn, yn_1) / :math.pow(subtract(xn, xn_1), 2)) + + # middle rows + i, j when j == i - 1 -> + [xi_1, xi] = Enum.map(-1..0, fn offset -> Enum.at(xs, i + offset) end) + 1.0 / subtract(xi, xi_1) + + i, i -> + [xi_1, xi, xi1] = Enum.map(-1..1, fn offset -> Enum.at(xs, i + offset) end) + 2.0 * (1.0 / subtract(xi, xi_1) + 1.0 / subtract(xi1, xi)) + + i, j when j == i + 1 -> + [xi, xi1] = Enum.map(0..1, fn offset -> Enum.at(xs, i + offset) end) + 1.0 / subtract(xi1, xi) + + # = + i, j when j == n + 1 -> + [xi_1, xi, xi1] = Enum.map(-1..1, fn offset -> Enum.at(xs, i + offset) end) + + [yi_1, yi, yi1] = + [xi_1, xi, xi1] + |> Enum.map(&Map.get(points, &1)) + + 3.0 * + (subtract(yi, yi_1) / :math.pow(subtract(xi, xi_1), 2) + + subtract(yi1, yi) / :math.pow(subtract(xi1, xi), 2)) + + # empty terms + _i, _j -> + 0.0 + end) + + with {:ok, {matrix, vector}} <- Matrix.disaugment(system_of_eqns), + {:ok, l} <- Matrix.cholesky_decomposition(matrix), + {:ok, y} <- Matrix.forward_substitution(l, vector), + {:ok, derivative_vector} <- l |> Matrix.transpose() |> Matrix.backward_substitution(y) do + Enum.zip(xs, Vector.to_list(derivative_vector)) |> Map.new() end end + + @spec subtract(minuend :: nil | number(), subtrahend :: nil | number()) :: number() + defp subtract(nil, _), do: 0.0 + defp subtract(_, nil), do: 0.0 + defp subtract(minuend, subtrahend), do: minuend - subtrahend end diff --git a/lib/expline/vector.ex b/lib/expline/vector.ex index 2e64cc5..29d7e7c 100644 --- a/lib/expline/vector.ex +++ b/lib/expline/vector.ex @@ -4,28 +4,26 @@ defmodule Expline.Vector do @enforce_keys [:n_slots, :internal] defstruct [:n_slots, :internal] - @type t() :: %Expline.Vector{ n_slots: pos_integer(), - internal: internal() } + @type t() :: %Expline.Vector{n_slots: pos_integer(), internal: internal()} @typep internal() :: tuple() @spec construct(pos_integer(), (non_neg_integer() -> float())) :: Expline.Vector.t() def construct(n_slots, elem_fn) - when n_slots > 0 - and is_function(elem_fn, 1) do - internal = 0..(n_slots - 1) - |> Enum.reduce({}, fn (i, vec) -> - Tuple.append(vec, elem_fn.(i)) - end) + when n_slots > 0 and + is_function(elem_fn, 1) do + internal = + Enum.reduce(0..(n_slots - 1), {}, fn i, vec -> + Tuple.append(vec, elem_fn.(i)) + end) - %Expline.Vector{ n_slots: n_slots, internal: internal } + %Expline.Vector{n_slots: n_slots, internal: internal} end @spec at(Expline.Vector.t(), non_neg_integer()) :: float() - def at(%Expline.Vector{ n_slots: n_slots, internal: internal }, i) - when is_integer(i) - and i < n_slots do - internal - |> elem(i) + def at(%Expline.Vector{n_slots: n_slots, internal: internal}, i) + when is_integer(i) and + i < n_slots do + elem(internal, i) end def to_list(%Expline.Vector{} = vector) do diff --git a/mix.exs b/mix.exs index cc5a4a8..7c86376 100644 --- a/mix.exs +++ b/mix.exs @@ -1,25 +1,30 @@ defmodule Expline.Mixfile do use Mix.Project + @app :explinex + @version "0.2.4" + def project do - [app: :expline, - version: "0.1.0", - elixir: "~> 1.4", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps(), - - # Hex parameters - description: description(), - package: package(), - - # Type-checking flags - dialyzer: dialyzer(), - - # Generated documentation parameters - name: "Expline", - source_url: "https://github.com/isaacsanders/expline", - docs: docs()] + [ + app: @app, + version: @version, + elixir: "~> 1.8", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps(), + + # Hex parameters + description: description(), + package: package(), + + # Type-checking flags + dialyzer: dialyzer(), + + # Generated documentation parameters + name: "Expline", + source_url: "https://github.com/am-kantox/#{@app}", + docs: docs() + ] end def application do @@ -36,28 +41,29 @@ defmodule Expline.Mixfile do end def package do - [name: :expline, - files: ["lib", "mix.exs", "README*", "LICENSE*"], - licenses: ["MIT"], - maintainers: ["Isaac Sanders"], - links: %{"GitHub" => "https://github.com/isaacsanders/expline"}] + [ + organization: "kantox", + name: @app, + files: ["lib", "mix.exs", "README*", "LICENSE*"], + licenses: ["MIT"], + maintainers: ["Isaac Sanders", "Aleksei Matiushkin"], + links: %{"GitHub" => "https://github.com/am-kantox/#{@app}"} + ] end def dialyzer do - [flags: [:error_handling, :no_behaviours, :no_contracts, :no_fail_call, - :no_fun_app, :no_improper_lists, :no_match, :no_missing_calls, :no_opaque, - :no_return, :no_undefined_callbacks, :no_unused, :race_conditions, - :underspecs, :unknown]] + [] end def docs do - [main: "Expline", - extras: ["README.md"]] + [main: "Expline", extras: ["README.md"]] end defp deps do - [{:ex_doc, "~> 0.14", only: :dev, runtime: false}, - {:dialyxir, "~> 0.5", only: :dev, runtime: false}, - {:quixir, "~> 0.9", only: :test }] + [ + {:ex_doc, "~> 0.14", only: :dev, runtime: false}, + {:dialyxir, "~> 1.0", only: :dev, runtime: false}, + {:quixir, "~> 0.9", only: :test} + ] end end diff --git a/mix.lock b/mix.lock index 9474448..ff7e45e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,14 @@ -%{"decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, - "dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], []}, - "earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, - "pollution": {:hex, :pollution, "0.9.0", "2d172aeedc444f0fc06a69b03bb6a560fb141bcafcc0c8189c29b10ee6e50893", [:mix], []}, - "quixir": {:hex, :quixir, "0.9.1", "b9a45930f330ba485c1cb976afc9f5ceb14ebbe10faf755e0796bb971396c37c", [:mix], [{:pollution, "~> 0.9", [hex: :pollution, optional: false]}]}} +%{ + "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, + "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, + "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "pollution": {:hex, :pollution, "0.9.2", "3f67542631071c99f807d2a8f9da799c07cd983c902f5357b9e1569c20a26e76", [:mix], [], "hexpm", "6399fd8ffd97dcc3d9d277f60542a234d644d7bcc0d48c8fda93d6be4801bac2"}, + "quixir": {:hex, :quixir, "0.9.3", "f01c37386b9e1d0526f01a8734a6d7884af294a0ec360f05c24c7171d74632bd", [:mix], [{:pollution, "~> 0.9.2", [hex: :pollution, repo: "hexpm", optional: false]}], "hexpm", "4f3a1fe7c82b767d935b3f7b94cf34b91ef78bb487ef256b303d77417fc7d589"}, +} diff --git a/test/expline/matrix_test.exs b/test/expline/matrix_test.exs index 2a1eab7..60649f5 100644 --- a/test/expline/matrix_test.exs +++ b/test/expline/matrix_test.exs @@ -5,26 +5,31 @@ defmodule Expline.MatrixTest do describe "Cholesky decomposition" do test "https://en.wikipedia.org/wiki/Cholesky_decomposition#Statement" do ptest n_rows: int(min: 2, max: 100) do - a = Expline.Matrix.construct(n_rows, n_rows, fn - (_i, _j) -> - :rand.uniform - end) + a = + Expline.Matrix.construct(n_rows, n_rows, fn + _i, _j -> + :rand.uniform() + end) - matrix = Expline.Matrix.add(a, Expline.Matrix.transpose(a)) - |> Expline.Matrix.scale(0.5) - |> Expline.Matrix.add( - n_rows - |> Expline.Matrix.identity() - |> Expline.Matrix.scale(1.0 * n_rows) - ) + matrix = + Expline.Matrix.add(a, Expline.Matrix.transpose(a)) + |> Expline.Matrix.scale(0.5) + |> Expline.Matrix.add( + n_rows + |> Expline.Matrix.identity() + |> Expline.Matrix.scale(1.0 * n_rows) + ) case Expline.Matrix.cholesky_decomposition(matrix) do {:ok, l} -> {:ok, product} = Expline.Matrix.product(l, Expline.Matrix.transpose(l)) - diff = Expline.Matrix.sub(matrix, product) - |> Expline.Matrix.transform(&(Float.round(&1, 13))) + + diff = + Expline.Matrix.sub(matrix, product) + |> Expline.Matrix.transform(&Float.round(&1, 13)) assert Expline.Matrix.zeros(n_rows, n_rows) == diff + _error -> assert false end diff --git a/test/expline/spline_test.exs b/test/expline/spline_test.exs index 2772f21..3e85814 100644 --- a/test/expline/spline_test.exs +++ b/test/expline/spline_test.exs @@ -3,70 +3,74 @@ defmodule Expline.SplineTest do use Quixir doctest Expline.Spline - @fun_list [ &:math.sin/1, - &:math.cos/1, - &:math.atan/1 ] + @fun_list [&:math.sin/1, &:math.cos/1, &:math.atan/1] @too_close_points [{0.0, 0.0}, {-5.0e-324, 0.0}] require Expline.Spline test "a spline can't have points that are too close" do - ptest points: list(min: 3, of: tuple(like: { float(), float() })) do - proper_error = case Expline.Spline.from_points(@too_close_points ++ points) do - {:error, {:range_too_small, _p1, _p2}} -> true - _ -> false - end + ptest points: list(min: 3, of: tuple(like: {float(), float()})) do + proper_error = + case Expline.Spline.from_points(@too_close_points ++ points) do + {:error, {:range_too_small, _p1, _p2}} -> true + _ -> false + end assert proper_error end end - test "a spline requires 3 points" do - ptest points: list(max: 2, of: tuple(like: { float(), float() })) do - proper_error = case Expline.Spline.from_points(points) do - {:error, :too_few_points} -> true - _ -> false - end + ptest points: list(max: 2, of: tuple(like: {float(), float()})) do + proper_error = + case Expline.Spline.from_points(points) do + {:error, :too_few_points} -> true + _ -> false + end assert proper_error end end test "removing points that are too close and ensuring a minimum set of 3 provides a valid spline" do - ptest points: list(min: 3, of: tuple(like: { float(), float() })) do - result = points - |> Enum.uniq_by(fn ({x, _}) -> Float.round(x, 15) end) - |> Expline.Spline.from_points + ptest points: list(min: 3, of: tuple(like: {float(), float()})) do + result = + points + |> Enum.uniq_by(fn {x, _} -> Float.round(x, 15) end) + |> Expline.Spline.from_points() - spline = case result do - {:ok, spline} -> spline - _ -> nil - end + spline = + case result do + {:ok, spline} -> spline + _ -> nil + end assert not is_nil(spline) end end test "splines for various functions" do - ptest [ xs: list(min: 100, of: float(min: -50, max: 50)), - sample_xs: list(min: 10, of: float(min: -10, max: 10)), - fun: choose(from: Enum.map(@fun_list, &value/1)) ] do - points = xs - |> Enum.uniq_by(fn (x) -> Float.round(x, 15) end) - |> Enum.map(fn (x) -> {x, fun.(x)} end) + ptest xs: list(min: 100, of: float(min: -50, max: 50)), + sample_xs: list(min: 10, of: float(min: -10, max: 10)), + fun: choose(from: Enum.map(@fun_list, &value/1)) do + points = + xs + |> Enum.uniq_by(fn x -> Float.round(x, 15) end) + |> Enum.map(fn x -> {x, fun.(x)} end) - spline = case Expline.Spline.from_points(points) do - {:ok, spline} -> spline - _ -> nil - end + spline = + case Expline.Spline.from_points(points) do + {:ok, spline} -> spline + _ -> nil + end assert not is_nil(spline) - errors = sample_xs - |> Enum.map(&(Expline.Spline.interpolate(spline, &1))) - |> Keyword.new - |> Keyword.get_values(:error) + errors = + sample_xs + |> Enum.map(&Expline.Spline.interpolate(spline, &1)) + |> Keyword.new() + |> Keyword.get_values(:error) assert Enum.empty?(errors) end diff --git a/test/expline_test.exs b/test/expline_test.exs index b941a55..784f868 100644 --- a/test/expline_test.exs +++ b/test/expline_test.exs @@ -3,70 +3,71 @@ defmodule ExplineTest do use Quixir doctest Expline - @fun_list [ &:math.sin/1, - &:math.cos/1, - &:math.atan/1 ] + @fun_list [&:math.sin/1, &:math.cos/1, &:math.atan/1] @too_close_points [{0.0, 0.0}, {-5.0e-324, 0.0}] require Expline test "a spline can't have points that are too close" do - ptest points: list(min: 3, of: tuple(like: { float(), float() })) do - proper_error = case Expline.start(@too_close_points ++ points) do - {:error, {:range_too_small, _p1, _p2}} -> true - _ -> false - end + ptest points: list(min: 3, of: tuple(like: {float(), float()})) do + proper_error = + match?( + {:error, {:range_too_small, _p1, _p2}}, + Expline.start(@too_close_points ++ points, graceful_shutdown: true) + ) assert proper_error end end - test "a spline requires 3 points" do - ptest points: list(max: 2, of: tuple(like: { float(), float() })) do - proper_error = case Expline.start(points) do - {:error, :too_few_points} -> true - _ -> false - end + ptest points: list(max: 2, of: tuple(like: {float(), float()})) do + proper_error = + match?({:error, :too_few_points}, Expline.start(points, graceful_shutdown: true)) assert proper_error end end test "removing points that are too close and ensuring a minimum set of 3 provides a valid spline" do - ptest points: list(min: 3, of: tuple(like: { float(), float() })) do - result = points - |> Enum.uniq_by(fn ({x, _}) -> Float.round(x, 15) end) - |> Expline.start_link + ptest points: list(min: 3, of: tuple(like: {float(), float()})) do + result = + points + |> Enum.uniq_by(fn {x, _} -> Float.round(x, 15) end) + |> Expline.start_link() - spline = case result do - {:ok, spline} -> spline - _ -> nil - end + spline = + case result do + {:ok, spline} -> spline + _ -> nil + end assert not is_nil(spline) end end test "splines for various functions" do - ptest [ xs: list(min: 100, of: float(min: -50, max: 50)), - sample_xs: list(min: 10, of: float(min: -10, max: 10)), - fun: choose(from: Enum.map(@fun_list, &value/1)) ] do - points = xs - |> Enum.uniq_by(fn (x) -> Float.round(x, 15) end) - |> Enum.map(fn (x) -> {x, fun.(x)} end) + ptest xs: list(min: 100, of: float(min: -50, max: 50)), + sample_xs: list(min: 10, of: float(min: -10, max: 10)), + fun: choose(from: Enum.map(@fun_list, &value/1)) do + points = + xs + |> Enum.uniq_by(fn x -> Float.round(x, 15) end) + |> Enum.map(fn x -> {x, fun.(x)} end) - spline = case Expline.start_link(points) do - {:ok, spline} -> spline - _ -> nil - end + spline = + case Expline.start_link(points) do + {:ok, spline} -> spline + _ -> nil + end assert not is_nil(spline) - errors = sample_xs - |> Enum.map(&(Expline.interpolate(spline, &1))) - |> Keyword.new - |> Keyword.get_values(:error) + errors = + sample_xs + |> Enum.map(&Expline.interpolate(spline, &1)) + |> Keyword.new() + |> Keyword.get_values(:error) assert Enum.empty?(errors) end