Current section

Files

Jump to
gleastsq src gleastsq.erl
Raw

src/gleastsq.erl

-module(gleastsq).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([least_squares/8]).
-export_type([fit_errors/0]).
-opaque fit_errors() :: non_converged.
-spec convert_func_params(fun((float(), list(float())) -> float())) -> fun((gleam@dynamic:dynamic_(), gleam@dynamic:dynamic_()) -> float()).
convert_func_params(Func) ->
fun(X, Params) ->
Func('Elixir.Nx':to_number(X), 'Elixir.Nx':to_list(Params))
end.
-spec compute_jacobian(
gleam@dynamic:dynamic_(),
fun((gleam@dynamic:dynamic_(), gleam@dynamic:dynamic_()) -> float()),
gleam@dynamic:dynamic_(),
gleam@dynamic:dynamic_(),
float(),
integer(),
integer()
) -> gleam@dynamic:dynamic_().
compute_jacobian(X, Func, Params, Jac, Epsilon, N, I) ->
case I of
I@1 when I@1 >= N ->
Jac;
_ ->
Mask = 'Elixir.Nx':indexed_put(
'Elixir.Nx':broadcast(+0.0, {N}),
'Elixir.Nx':tensor([I]),
Epsilon
),
Up_params = 'Elixir.Nx':add(Params, Mask),
Down_params = 'Elixir.Nx':subtract(Params, Mask),
Up_f = 'Elixir.Nx':map(
X,
fun(_capture) -> Func(_capture, Up_params) end
),
Down_f = 'Elixir.Nx':map(
X,
fun(_capture@1) -> Func(_capture@1, Down_params) end
),
Deriv = 'Elixir.Nx':new_axis(
'Elixir.Nx':divide(
'Elixir.Nx':subtract(Up_f, Down_f),
2.0 * Epsilon
),
1
),
Updated_jac = 'Elixir.Nx':put_slice(Jac, [0, I], Deriv),
compute_jacobian(X, Func, Params, Updated_jac, Epsilon, N, I + 1)
end.
-spec jacobian(
gleam@dynamic:dynamic_(),
fun((gleam@dynamic:dynamic_(), gleam@dynamic:dynamic_()) -> float()),
gleam@dynamic:dynamic_(),
float()
) -> gleam@dynamic:dynamic_().
jacobian(X, Func, Params, Epsilon) ->
{N} = 'Elixir.Nx':shape(Params),
{M} = 'Elixir.Nx':shape(X),
Jac = 'Elixir.Nx':broadcast(+0.0, {M, N}),
compute_jacobian(X, Func, Params, Jac, Epsilon, N, 0).
-spec do_least_squares(
gleam@dynamic:dynamic_(),
gleam@dynamic:dynamic_(),
fun((gleam@dynamic:dynamic_(), gleam@dynamic:dynamic_()) -> float()),
gleam@dynamic:dynamic_(),
integer(),
float(),
float(),
float()
) -> {ok, gleam@dynamic:dynamic_()} | {error, fit_errors()}.
do_least_squares(
X,
Y,
Func,
Params,
Max_iterations,
Epsilon,
Tolerance,
Lambda_reg
) ->
M = erlang:element(1, 'Elixir.Nx':shape(Params)),
case Max_iterations of
0 ->
{error, non_converged};
Iterations ->
R = begin
_pipe = X,
_pipe@1 = 'Elixir.Nx':map(
_pipe,
fun(_capture) -> Func(_capture, Params) end
),
'Elixir.Nx':subtract(Y, _pipe@1)
end,
J = jacobian(X, Func, Params, Epsilon),
Jt = 'Elixir.Nx':transpose(J),
Lambda_eye = begin
_pipe@2 = 'Elixir.Nx':eye(M),
'Elixir.Nx':multiply(_pipe@2, Lambda_reg)
end,
H = 'Elixir.Nx':add('Elixir.Nx':dot(Jt, J), Lambda_eye),
G = 'Elixir.Nx':dot(Jt, R),
Delta = 'Elixir.Nx.LinAlg':solve(H, G),
case 'Elixir.Nx':to_number('Elixir.Nx.LinAlg':norm(Delta)) of
X@1 when X@1 < Tolerance ->
{ok, Params};
_ ->
do_least_squares(
X,
Y,
Func,
'Elixir.Nx':add(Params, Delta),
Iterations - 1,
Epsilon,
Tolerance,
Lambda_reg
)
end
end.
-spec least_squares(
list(float()),
list(float()),
fun((float(), list(float())) -> float()),
list(float()),
gleam@option:option(integer()),
gleam@option:option(float()),
gleam@option:option(float()),
gleam@option:option(float())
) -> {ok, list(float())} | {error, fit_errors()}.
least_squares(
X,
Y,
Func,
Initial_params,
Iterations,
Epsilon,
Tolerance,
Lambda_reg
) ->
P = 'Elixir.Nx':tensor(Initial_params),
X@1 = 'Elixir.Nx':tensor(X),
Y@1 = 'Elixir.Nx':tensor(Y),
Func@1 = convert_func_params(Func),
Iter = case Iterations of
{some, X@2} ->
X@2;
none ->
100
end,
Eps = case Epsilon of
{some, X@3} ->
X@3;
none ->
0.0001
end,
Reg = case Lambda_reg of
{some, X@4} ->
X@4;
none ->
0.0001
end,
Tol = case Tolerance of
{some, X@5} ->
X@5;
none ->
0.0001
end,
gleam@result:'try'(
do_least_squares(X@1, Y@1, Func@1, P, Iter, Eps, Tol, Reg),
fun(Fitted) ->
{ok,
begin
_pipe = Fitted,
'Elixir.Nx':to_list(_pipe)
end}
end
).