Current section
Files
Jump to
Current section
Files
lib/astro/math.ex
defmodule Astro.Math do
@moduledoc false
import Kernel, except: [min: 2, max: 2, ceil: 1, floor: 1]
alias Astro.Time
@radians_to_degrees 180.0 / :math.pi()
@au_to_km 149_597_870.7
@au_to_m 149_597_870.7 * 1_000
defmacro to_degrees(radians) do
radians_to_degrees = @radians_to_degrees
quote do
unquote(radians) * unquote(radians_to_degrees)
end
end
defmacro to_radians(degrees) do
radians_to_degrees = @radians_to_degrees
quote do
unquote(degrees) / unquote(radians_to_degrees)
end
end
def au_to_km(au) do
au * @au_to_km
end
def au_to_m(au) do
au * @au_to_m
end
def m_to_au(m) do
m / @au_to_m
end
def degrees(degrees) do
mod(degrees, 360.0)
end
defmacro mt(x) do
quote do
unquote(x)
end
end
defmacro deg(x) do
quote do
unquote(x)
end
end
defmacro angle(d, m, s) do
angle = d + (m + s / Time.seconds_per_minute()) / Time.minutes_per_hour()
quote do
unquote(angle)
end
end
defmacro degrees_minutes_seconds(d, m, s) do
quote do
{unquote(d), unquote(m), unquote(s)}
end
end
def cos(degrees) do
degrees
|> to_radians
|> :math.cos()
end
def sin(degrees) do
degrees
|> to_radians
|> :math.sin()
end
def asin(degrees) do
degrees
|> to_radians
|> :math.asin()
end
def acos(degrees) do
degrees
|> to_radians
|> :math.acos()
end
def tan(degrees) do
degrees
|> to_radians
|> :math.tan()
end
def atan(0, 0) do
:undefined
end
def atan(y, x) do
cond do
x == 0 && y != 0 -> signum(y) * deg(90.0)
x >= 0 -> to_degrees(:math.atan(y / x))
x < 0 -> to_degrees(:math.atan(y / x)) + signum(y) * deg(180.0)
end
|> mod(360.0)
end
def atan_r(0, 0) do
:NaN
end
def atan_r(y, x) do
cond do
x == 0 && y != 0 -> signum(y) * :math.pi() / 2.0
x >= 0 -> :math.atan(y / x)
x < 0 -> :math.atan(y / x) + signum(y) * :math.pi()
end
end
def floor(x) when x >= 0 do
trunc(x)
end
def floor(x) when x < 0 do
t = trunc(x)
if x - t == 0 do
t
else
t - 1
end
end
def ceil(x) do
-floor(-x)
end
@doc """
Returns the minimum number for which
the given function returns a `truthy`
value.
"""
@spec min(number(), function()) :: number()
def min(i, fun) when is_number(i) and is_function(fun) do
if fun.(i), do: i, else: min(i + 1, fun)
end
@doc """
Returns the maximum number for which
the given function returns a `truthy`
value.
"""
@spec max(number(), function()) :: number()
def max(i, fun) when is_number(i) and is_function(fun) do
if fun.(i), do: max(i + 1, fun), else: i - 1
end
@spec poly(number(), [number()]) :: number()
def poly(_, []), do: 0
def poly(x, [a | a_s]), do: a + x * poly(x, a_s)
def amod(x, y) when y != 0 do
y + mod(x, -y)
end
@spec signum(number()) :: -1 | 0 | 1
def signum(x) when x > 0, do: 1
def signum(x) when x < 0, do: -1
def signum(_), do: 0
@spec sigma([[number(), ...]], function()) :: number()
def sigma(list_of_lists, fun) do
if Enum.all?(list_of_lists, &(&1 == [])) do
0
else
# [hd(l) || l <- list_of_lists]
current = Enum.map(list_of_lists, &hd/1)
# [tl(l) || l <- list_of_lists]
next = Enum.map(list_of_lists, &tl/1)
fun.(current) + sigma(next, fun)
end
|> Kernel.*(1.0)
end
@doc """
Calculates the modulo of a number (integer, float).
Note that this function uses `floored division` whereas the builtin `rem`
function uses `truncated division`.
See [Wikipedia](https://en.wikipedia.org/wiki/Modulo_operation) for an
explanation of the difference.
## Examples
iex> Astro.Math,mod(1234.0, 5)
4.0
"""
@spec mod(number, number) :: number
def mod(number, modulus) when is_float(number) and is_number(modulus) do
number - Float.floor(number / modulus) * modulus
end
def mod(number, modulus) when is_integer(number) and is_integer(modulus) do
modulo =
number
|> Integer.floor_div(modulus)
|> Kernel.*(modulus)
number - modulo
end
def mod(number, modulus) when is_integer(number) and is_number(modulus) do
modulo =
number
|> Kernel./(modulus)
|> Float.floor()
|> Kernel.*(modulus)
number - modulo
end
@doc """
Returns the remainder and dividend of two numbers.
"""
@spec div_mod(number, number) :: {number, number}
def div_mod(n1, n2) when is_integer(n1) and is_integer(n2) do
div = div(n1, n2)
mod = n2 - div * n2
{div, mod}
end
def div_mod(n1, n2) when is_number(n1) and is_number(n2) do
div = n1 / n2
mod = n2 - div * n2
{div, mod}
end
def final(k, pred) do
new_k = k + 1
if !pred.(new_k) do
k
else
final(new_k, pred)
end
end
def next(k, pred) do
if pred.(k) do
k
else
next(k + 1, pred)
end
end
@spec invert_angular(function(), number(), number(), number()) :: number()
def invert_angular(f, y, a, b) do
# returns X such that A =< X =< B for which f(X) = Y
# where |X-X0| < Tolerance
tolerance = 1 / 100_000.0
phi = fn l, u -> u - l < tolerance end
psi = fn x -> mod(f.(x) - y, 360.0) < deg(180.0) end
bisection_search(a, b, phi, psi)
end
@spec bisection_search(number(), number(), function(), function()) :: number()
def bisection_search(u, v, phi, psi) do
x = (v + u) / 2.0
if phi.(u, v) do
x
else
if psi.(x) do
bisection_search(u, x, phi, psi)
else
bisection_search(x, v, phi, psi)
end
end
end
end