Skip to main content

src/svg_path@root.erl

-module(svg_path@root).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/svg_path/root.gleam").
-export([default_options/0, bisect_with/4, bisect/3]).
-export_type([options/0, error/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Bracketed root-finding helpers for scalar functions.\n"
    "\n"
    " This module intentionally keeps the numerical method small and explicit.\n"
    " `bisect` implements the standard bisection method: for a continuous\n"
    " function `f` and a bracket `[a, b]` where `f(a)` and `f(b)` have opposite\n"
    " signs, repeatedly halve the interval until the root estimate is within the\n"
    " requested tolerance.\n"
).

-type options() :: {options, float(), integer()}.

-type error() :: {invalid_tolerance, float()} |
    {invalid_max_iterations, integer()} |
    {not_bracketed, float(), float(), float(), float()} |
    {max_iterations_reached, float(), float()}.

-file("src/svg_path/root.gleam", 36).
?DOC(" Return the default bisection options.\n").
-spec default_options() -> options().
default_options() ->
    {options, 0.000000001, 100}.

-file("src/svg_path/root.gleam", 160).
-spec same_sign(float(), float()) -> boolean().
same_sign(A, B) ->
    ((A < +0.0) andalso (B < +0.0)) orelse ((A > +0.0) andalso (B > +0.0)).

-file("src/svg_path/root.gleam", 156).
-spec is_close_to_zero(float(), float()) -> boolean().
is_close_to_zero(Value, Tolerance) ->
    gleam@float:absolute_value(Value) =< Tolerance.

-file("src/svg_path/root.gleam", 103).
-spec bisect_loop(
    fun((float()) -> float()),
    float(),
    float(),
    float(),
    float(),
    integer()
) -> {ok, float()} | {error, error()}.
bisect_loop(F, Left, Left_value, Right, Tolerance, Remaining_iterations) ->
    Midpoint = Left + ((Right - Left) / 2.0),
    Midpoint_value = F(Midpoint),
    case is_close_to_zero(Midpoint_value, Tolerance) orelse (((Right - Left) / 2.0)
    =< Tolerance) of
        true ->
            {ok, Midpoint};

        false ->
            case Remaining_iterations =< 1 of
                true ->
                    {error, {max_iterations_reached, Midpoint, Midpoint_value}};

                false ->
                    case same_sign(Left_value, Midpoint_value) of
                        true ->
                            bisect_loop(
                                F,
                                Midpoint,
                                Midpoint_value,
                                Right,
                                Tolerance,
                                Remaining_iterations - 1
                            );

                        false ->
                            bisect_loop(
                                F,
                                Left,
                                Left_value,
                                Midpoint,
                                Tolerance,
                                Remaining_iterations - 1
                            )
                    end
            end
    end.

-file("src/svg_path/root.gleam", 149).
-spec ordered_bracket(float(), float()) -> {float(), float()}.
ordered_bracket(Left, Right) ->
    case Left =< Right of
        true ->
            {Left, Right};

        false ->
            {Right, Left}
    end.

-file("src/svg_path/root.gleam", 58).
?DOC(
    " Find a root of `f` in a bracket using explicit options.\n"
    "\n"
    " The returned value is an approximation. Convergence succeeds when either\n"
    " `abs(f(estimate)) <= tolerance` or the current bracket width is no larger\n"
    " than `tolerance`.\n"
).
-spec bisect_with(fun((float()) -> float()), float(), float(), options()) -> {ok,
        float()} |
    {error, error()}.
bisect_with(F, Left, Right, Options) ->
    case erlang:element(2, Options) =< +0.0 of
        true ->
            {error, {invalid_tolerance, erlang:element(2, Options)}};

        false ->
            case erlang:element(3, Options) =< 0 of
                true ->
                    {error,
                        {invalid_max_iterations, erlang:element(3, Options)}};

                false ->
                    {Left@1, Right@1} = ordered_bracket(Left, Right),
                    Left_value = F(Left@1),
                    Right_value = F(Right@1),
                    case is_close_to_zero(
                        Left_value,
                        erlang:element(2, Options)
                    ) of
                        true ->
                            {ok, Left@1};

                        false ->
                            case is_close_to_zero(
                                Right_value,
                                erlang:element(2, Options)
                            ) of
                                true ->
                                    {ok, Right@1};

                                false ->
                                    case same_sign(Left_value, Right_value) of
                                        true ->
                                            {error,
                                                {not_bracketed,
                                                    Left@1,
                                                    Right@1,
                                                    Left_value,
                                                    Right_value}};

                                        false ->
                                            bisect_loop(
                                                F,
                                                Left@1,
                                                Left_value,
                                                Right@1,
                                                erlang:element(2, Options),
                                                erlang:element(3, Options)
                                            )
                                    end
                            end
                    end
            end
    end.

-file("src/svg_path/root.gleam", 45).
?DOC(
    " Find a root of `f` in a bracket using default options.\n"
    "\n"
    " `f(from)` and `f(to)` must have opposite signs, unless either endpoint is\n"
    " already within tolerance of zero. `from` may be greater than `to`; the\n"
    " bracket is normalized before solving.\n"
).
-spec bisect(fun((float()) -> float()), float(), float()) -> {ok, float()} |
    {error, error()}.
bisect(F, Left, Right) ->
    bisect_with(F, Left, Right, default_options()).