defmodule Astro.Lunar do
@moduledoc """
Calulates lunar phases.
Each of the phases of the Moon is defined by the
angle between the Moon and Sun in the sky. When the Moon
is in between the Earth and the Sun, so that there is nearly a
zero degree separation, we see a New Moon.
Because the orbit of the Moon is tilted in relation to the
Earth’s orbit around the Sun, a New Moon can still be as much
as 5.2 degrees away from the Sun, thus why there isn't a
solar eclipse every month.
A crescent moon is 45 degrees from the Sun, a quarter moon
is 90 degrees from the Sun, a gibbous moon is 135 degrees
from the Sun, and the Full Moon is 180 degrees away from
the Sun.
"""
alias Astro.{Math, Time, Solar}
import Astro.Math, only: [
deg: 1,
sin: 1,
cos: 1,
mt: 1,
asin: 1,
sigma: 2,
mod: 2,
degrees: 1,
poly: 2,
invert_angular: 4
]
import Astro.Time, only: [
j2000: 0,
julian_centuries_from_moment: 1,
mean_synodic_month: 0
]
import Astro.Earth, only: [
nutation: 1
]
@months_epoch_to_j2000 24_724
@average_distance_earth_to_moon 385_000_560.0
@doc """
Returns the date time of the new
moon before a given moment.
## Arguments
* a `t:Time.moment()` which is a float number of days
since `0000-01-01`
## Returns
* a `t:Time.moment()` which is a float number of days
since `0000-01-01`
## Example
iex> Astro.Lunar.date_time_new_moon_before 738390
738375.5757777032
"""
@doc since: "0.5.0"
@spec date_time_new_moon_before(Time.moment()) :: Time.moment()
def date_time_new_moon_before(t) when is_number(t) do
t0 = nth_new_moon(0)
phi = lunar_phase_at(t)
n = round((t - t0) / mean_synodic_month() - phi / deg(360)) |> trunc()
nth_new_moon(Math.final(n - 1, &(nth_new_moon(&1) < t)))
end
@doc """
Returns the date time of the new
moon at or after a given date or
date time.
## Arguments
* a `t:Time.moment()` which is a float number of days
since `0000-01-01`
## Returns
* a `t:Time.moment()` which is a float number of days
since `0000-01-01`
## Example
iex> Astro.Lunar.date_time_new_moon_at_or_after 738390
738405.0352292997
"""
@doc since: "0.5.0"
@spec date_time_new_moon_at_or_after(Time.moment()) :: Time.moment()
def date_time_new_moon_at_or_after(t) when is_number(t) do
t0 = nth_new_moon(0)
phi = lunar_phase_at(t)
n = round((t - t0) / mean_synodic_month() - phi / deg(360.0))
nth_new_moon(Math.next(n, &(nth_new_moon(&1) >= t)))
end
@doc """
Returns the lunar phase as a
float number of degrees at a given
moment.
## Arguments
* a `t:Time.moment()` which is a float number of days
since `0000-01-01`
## Returns
* the lunar phase as a float number of
degrees.
## Example
iex> Astro.Lunar.lunar_phase_at 738389.5007195644
180.00001498208536
iex> Astro.Lunar.lunar_phase_at 738346.0544609067
0.021567106773019873
"""
@doc since: "0.5.0"
@spec lunar_phase_at(Time.moment()) :: Time.moment()
def lunar_phase_at(t) when is_number(t) do
phi = mod(lunar_longitude(t) - solar_longitude(t), 360)
t0 = nth_new_moon(0)
n = round((t - t0) / mean_synodic_month())
phi_prime = deg(360) * mod((t - nth_new_moon(n)) / mean_synodic_month(), 1)
if abs(phi - phi_prime) > deg(180.0) do
phi_prime
else
phi
end
end
@doc """
Returns the date time of a given
lunar phase at or before a given
moment.
## Arguments
* a `t:Time.moment()` which is a float number of days
since `0000-01-01`
* `phase` is the required lunar phase expressed
as a float number of degrees between `0.0` and
`360.0`
## Returns
* a `t:Time.moment()` which is a float number of days
since `0000-01-01`
## Example
iex> Astro.Lunar.date_time_lunar_phase_at_or_before(738368, Astro.Lunar.new_moon())
738346.0524695957
"""
@doc since: "0.5.0"
@spec date_time_lunar_phase_at_or_before(Time.moment(), Astro.phase()) :: Time.moment()
def date_time_lunar_phase_at_or_before(t, phase) do
tau = t - mean_synodic_month() * (1.0 / deg(360.0)) * mod(lunar_phase_at(t) - phase, 360.0)
a = tau - 2
b = min(t, tau + 2)
invert_angular(&lunar_phase_at/1, phase, a, b)
end
@doc """
Returns the date time of a given
lunar phase at or after a given
date time or date.
## Arguments
* a `moment` which is a float number of days
since `0000-01-01`
* `phase` is the required lunar phase expressed
as a float number of degrees between `0` and
`3660`
## Returns
* a `t:Time.moment()` which is a float number of days
since `0000-01-01`
## Example
iex> Astro.Lunar.date_time_lunar_phase_at_or_after(738368, Astro.Lunar.full_moon())
738389.5007195254
"""
@doc since: "0.5.0"
@spec date_time_lunar_phase_at_or_after(Time.moment(), Astro.phase()) :: Time.moment()
def date_time_lunar_phase_at_or_after(t, phase) do
tau = t + mean_synodic_month() * (1 / deg(360.0)) * mod(phase - lunar_phase_at(t), 360.0)
a = max(t, tau - 2)
b = tau + 2
invert_angular(&lunar_phase_at/1, phase, a, b)
end
@doc since: "0.6.0"
@spec lunar_position(Time.moment()) :: {Astro.angle(), Astro.angle(), Astro.meters()}
def lunar_position(t) do
lambda = lunar_longitude(t)
beta = lunar_latitude(t)
distance = lunar_distance(t)
{Astro.right_ascension(t, beta, lambda), Astro.declination(t, beta, lambda), distance}
end
@doc """
Returns the fractional illumination of the moon
at a given time as a fraction between 0.0 and 1.0.
"""
@doc since: "0.6.0"
@spec illuminated_fraction_of_moon(Time.time()) :: float()
def illuminated_fraction_of_moon(t) do
{a0, d0, r0} = lunar_position(t)
{a, d, r} = Solar.solar_position(t)
r = Math.au_to_m(r)
phi = :math.acos(sin(d0) * sin(d) + cos(d0) * cos(d) * cos(a0 - a))
i = Math.atan_r(r * :math.sin(phi), r0 - r * :math.cos(phi))
0.5 * (1 + :math.cos(i))
end
@doc """
Returns the new moon lunar
phase expressed as a float number
of degrees.
"""
@doc since: "0.5.0"
@spec new_moon() :: Astro.phase()
def new_moon() do
deg(0.0)
end
@doc """
Returns the full moon lunar
phase expressed as a float number
of degrees.
"""
@doc since: "0.5.0"
@spec full_moon() :: Astro.phase()
def full_moon() do
deg(180.0)
end
@doc """
Returns the first quarter lunar
phase expressed as a float number
of degrees.
"""
@doc since: "0.5.0"
@spec first_quarter() :: Astro.phase()
def first_quarter() do
deg(90.0)
end
@doc """
Returns the last quarter lunar
phase expressed as a float number
of degrees.
"""
@doc since: "0.5.0"
@spec last_quarter() :: Astro.phase()
def last_quarter() do
deg(270.0)
end
@doc false
@doc since: "0.5.0"
@spec lunar_longitude(Time.moment()) :: Astro.phase()
def lunar_longitude(t) do
c = julian_centuries_from_moment(t)
l = mean_lunar_longitude(c)
d = lunar_elongation(c)
m = solar_anomaly(c)
m_prime = lunar_anomaly(c)
f = moon_node(c)
e = poly(c, [1.0, -0.002516, -0.0000074])
args_lunar_elong = [
0, 2, 2, 0, 0, 0, 2, 2, 2, 2, 0, 1, 0, 2, 0, 0, 4, 0, 4, 2, 2, 1,
1, 2, 2, 4, 2, 0, 2, 2, 1, 2, 0, 0, 2, 2, 2, 4, 0, 3, 2, 4, 0, 2,
2, 2, 4, 0, 4, 1, 2, 0, 1, 3, 4, 2, 0, 1, 2
]
args_solar_anom = [
0, 0, 0, 0, 1, 0, 0, -1, 0, -1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, -1, 0, 0, 0, 1, 0, -1, 0, -2, 1, 2, -2, 0, 0, -1, 0, 0, 1,
-1, 2, 2, 1, -1, 0, 0, -1, 0, 1, 0, 1, 0, 0, -1, 2, 1, 0
]
args_lunar_anom = [
1, -1, 0, 2, 0, 0, -2, -1, 1, 0, -1, 0, 1, 0, 1, 1, -1, 3, -2,
-1, 0, -1, 0, 1, 2, 0, -3, -2, -1, -2, 1, 0, 2, 0, -1, 1, 0,
-1, 2, -1, 1, -2, -1, -1, -2, 0, 1, 4, 0, -2, 0, 2, 1, -2, -3,
2, 1, -1, 3
]
args_moon_node = [
0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, -2, 2, -2, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, -2, 2, 0, 2, 0, 0, 0, 0,
0, 0, -2, 0, 0, 0, 0, -2, -2, 0, 0, 0, 0, 0, 0, 0
]
sine_coeff = [
6288774, 1274027, 658314, 213618, -185116, -114332,
58793, 57066, 53322, 45758, -40923, -34720, -30383,
15327, -12528, 10980, 10675, 10034, 8548, -7888,
-6766, -5163, 4987, 4036, 3994, 3861, 3665, -2689,
-2602, 2390, -2348, 2236, -2120, -2069, 2048, -1773,
-1595, 1215, -1110, -892, -810, 759, -713, -700, 691,
596, 549, 537, 520, -487, -399, -381, 351, -340, 330,
327, -323, 299, 294
]
correction = deg(1 / 1000000) * sigma(
[sine_coeff, args_lunar_elong, args_solar_anom, args_lunar_anom, args_moon_node],
fn [v,w,x,y,z] ->
v * :math.pow(e, abs(x)) * sin(w * d + x * m + y * m_prime + z * f)
end
)
venus =
deg(3958 / 1000000) *
sin(deg(119.75) + c * deg(131.849))
jupiter =
deg(318 / 1000000) *
sin(deg(53.09) + c * deg(479_264.29))
flat_earth =
deg(1962 / 1000000) *
sin(l - f)
mod(l + correction + venus + jupiter + flat_earth + nutation(c), 360)
end
@doc since: "0.6.0"
@spec lunar_latitude(Time.moment()) :: Astro.angle()
def lunar_latitude(t) do
c = julian_centuries_from_moment(t)
l = mean_lunar_longitude(c)
d = lunar_elongation(c)
m = solar_anomaly(c)
m_prime = lunar_anomaly(c)
f = moon_node(c)
e = poly(c, [1.0, -0.002516, -0.0000074])
lunar_elongation = [
0,0,0,2,2,2,2,0,2,0,2,2,2,2,2,2,2,0,4,0,0,0,
1,0,0,0,1,0,4,4,0,4,2,2,2,2,0,2,2,2,2,4,2,2,
0,2,1,1,0,2,1,2,0,4,4,1,4,1,4,2
]
solar_anomaly = [
0,0,0,0,0,0,0,0,0,0,-1,0,0,1,-1,-1,-1,1,0,1,
0,1,0,1,1,1,0,0,0,0,0,0,0,0,-1,0,0,0,0,1,1,
0,-1,-2,0,1,1,1,1,1,0,-1,1,0,-1,0,0,0,-1,-2]
lunar_anomaly = [
0,1,1,0,-1,-1,0,2,1,2,0,-2,1,0,-1,0,-1,-1,-1,
0,0,-1,0,1,1,0,0,3,0,-1,1,-2,0,2,1,-2,3,2,-3,
-1,0,0,1,0,1,1,0,0,-2,-1,1,-2,2,-2,-1,1,1,-2,
0,0
]
moon_node = [
1,1,-1,-1,1,-1,1,1,-1,-1,-1,-1,1,-1,1,1,-1,-1,
-1,1,3,1,1,1,-1,-1,-1,1,-1,1,-3,1,-3,-1,-1,1,
-1,1,-1,1,1,1,1,-1,3,-1,-1,1,-1,-1,1,-1,1,-1,
-1,-1,-1,-1,-1,1
]
sine_coeff = [
5128122, 280602, 277693, 173237, 55413, 46271, 32573,
17198, 9266, 8822, 8216, 4324, 4200, -3359, 2463, 2211,
2065, -1870, 1828, -1794, -1749, -1565, -1491, -1475,
-1410, -1344, -1335, 1107, 1021, 833, 777, 671, 607,
596, 491, -451, 439, 422, 421, -366, -351, 331, 315,
302, -283, -229, 223, 223, -220, -220, -185, 181,
-177, 176, 166, -164, 132, -119, 115, 107
]
beta = deg(1.0 / 1000000.0) * sigma(
[sine_coeff, lunar_elongation, solar_anomaly, lunar_anomaly, moon_node],
fn [v, w, x, y, z] ->
v * :math.pow(e, abs(x)) * sin(w*d + x*m + y*m_prime + z*f)
end
)
venus =
deg(175.0 / 1000000.0) *
sin(deg(119.75) + c * deg(131.849) + f) *
sin(deg(119.75) + c * deg(131.849) - f)
flat_earth =
deg(-2235.0 / 1000000.0) * sin(l) +
deg(127.0 / 1000000.0) * sin(l - m_prime) +
deg(-115.0 / 1000000.0) * sin(l + m_prime)
extra = deg(382.0 / 1000000.0) * sin(deg(313.45) + (c * deg(481266.484)))
beta + venus + flat_earth + extra
end
@doc since: "0.4.0"
@spec lunar_altitude(Time.moment(), Geo.PointZ.t()) :: Astro.angle()
def lunar_altitude(t, %Geo.PointZ{coordinates: {psi, phi, _alt}}) do
lambda = lunar_longitude(t)
beta = lunar_latitude(t)
alpha = Astro.right_ascension(t, beta, lambda)
delta = Astro.declination(t, beta, lambda)
theta = Time.sidereal_from_moment(t)
h = mod(theta + psi - alpha, 360.0)
altitude = asin(sin(phi) * sin(delta) + cos(phi) * cos(delta) * cos(h))
mod(altitude + deg(180.0), 360.0) - deg(180.0)
end
@doc since: "0.6.0"
@spec lunar_distance(Time.moment()) :: Astro.meters()
def lunar_distance(t) do
c = Time.julian_centuries_from_moment(t)
d = lunar_elongation(c)
m = solar_anomaly(c)
m_prime = lunar_anomaly(c)
f = moon_node(c)
e = poly(c, [1.0, -0.002516, -0.0000074])
lunar_elongation = [
0,2,2,0,0,0,2,2,2,2,0,1,0,2,0,0,4,0,4,2,2,1,
1,2,2,4,2,0,2,2,1,2,0,0,2,2,2,4,0,3,2,4,0,2,
2,2,4,0,4,1,2,0,1,3,4,2,0,1,2,2
]
solar_anomaly = [
0,0,0,0,1,0,0,-1,0,-1,1,0,1,0,0,0,0,0,0,1,1,
0,1,-1,0,0,0,1,0,-1,0,-2,1,2,-2,0,0,-1,0,0,1,
-1,2,2,1,-1,0,0,-1,0,1,0,1,0,0,-1,2,1,0,0
]
lunar_anomaly = [
1,-1,0,2,0,0,-2,-1,1,0,-1,0,1,0,1,1,-1,3,-2,
-1,0,-1,0,1,2,0,-3,-2,-1,-2,1,0,2,0,-1,1,0,
-1,2,-1,1,-2,-1,-1,-2,0,1,4,0,-2,0,2,1,-2,-3,
2,1,-1,3,-1
]
moon_node = [
0,0,0,0,0,2,0,0,0,0,0,0,0,-2,2,-2,0,0,0,0,0,
0,0,0,0,0,0,0,2,0,0,0,0,0,0,-2,2,0,2,0,0,0,0,
0,0,-2,0,0,0,0,-2,-2,0,0,0,0,0,0,0,-2
]
cos_coeff = [
-20905355,-3699111,-2955968,-569925,48888,-3149,
246158,-152138,-170733,-204586,-129620,108743,
104755,10321,0,79661,-34782,-23210,-21636,24208,
30824,-8379,-16675,-12831,-10445,-11650,14403,
-7003,0,10056,6322,-9884,5751,0,-4950,4130,0,
-3958,0,3258,2616,-1897,-2117,2354,0,0,-1423,
-1117,-1571,-1739,0,-4421,0,0,0,0,1165,0,0,
8752
]
correction = sigma(
[cos_coeff, lunar_elongation, solar_anomaly, lunar_anomaly, moon_node],
fn [v, w, x, y, z] ->
v * :math.pow(e, abs(x)) * cos((w * d) + (x * m) + (y * m_prime) + (z * f))
end
)
mt(@average_distance_earth_to_moon) + correction
end
@doc false
@doc since: "0.4.0"
@spec nth_new_moon(number()) :: Time.moment()
def nth_new_moon(n) do
k = n - @months_epoch_to_j2000
c = k / 1_236.85
approx =
j2000() + poly(c, [
5.09766, mean_synodic_month() * 1236.85, 0.0001437, -0.000000150, 0.00000000073
])
e = poly(c, [
1, -0.002516, -0.0000074
])
solar_anomaly = poly(c, [
2.5534, 1236.85 * 29.10535669, -0.0000014, -0.00000011
])
lunar_anomaly = poly(c, [
201.5643, 385.81693528 * 1236.85,
0.0107582, 0.00001238, -0.000000058
])
moon_argument = poly(c, [
160.7108, 390.67050284 * 1236.85,
-0.0016118, -0.00000227, 0.000000011
])
omega = poly(c, [
124.7746, -1.56375588 * 1236.85,
0.0020672, 0.00000215
])
e_factor = [
0, 1, 0, 0, 1, 1, 2, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0
]
solar_coeff = [
0, 1, 0, 0, -1, 1, 2, 0, 0, 1, 0, 1, 1, -1, 2,
0, 3, 1, 0, 1, -1, -1, 1, 0
]
lunar_coeff = [
1, 0, 2, 0, 1, 1, 0, 1, 1, 2, 3, 0, 0, 2, 1, 2,
0, 1, 2, 1, 1, 1, 3, 4
]
moon_coeff = [
0, 0, 0, 2, 0, 0, 0, -2, 2, 0, 0, 2, -2, 0, 0,
-2, 0, -2, 2, 2, 2, -2, 0, 0
]
sine_coeff = [
-0.40720, 0.17241, 0.01608,
0.01039, 0.00739, -0.00514,
0.00208, -0.00111, -0.00057,
0.00056, -0.00042, 0.00042,
0.00038, -0.00024, -0.00007,
0.00004, 0.00004, 0.00003,
0.00003, -0.00003, 0.00003,
-0.00002, -0.00002, 0.00002
]
correction =
deg(-0.00017) * sin(omega) +
sigma(
[sine_coeff, e_factor, solar_coeff, lunar_coeff, moon_coeff],
fn [v,w,x,y,z] ->
v * :math.pow(e, w) *
sin((x * solar_anomaly) + (y * lunar_anomaly) + (z * moon_argument))
end
)
extra =
deg(0.000325) *
sin(poly(c, [299.77, 132.8475848, -0.009173]))
add_const = [
251.88, 251.83, 349.42, 84.66, 141.74, 207.14, 154.84,
34.52, 207.19, 291.34, 161.72, 239.56, 331.55
]
add_coeff = [
0.016321, 26.651886, 36.412478, 18.206239, 53.303771,
2.453732, 7.306860, 27.261239, 0.121824, 1.844379,
24.198154, 25.513099, 3.592518
]
add_factor = [
0.000165, 0.000164, 0.000126, 0.000110, 0.000062, 0.000060,
0.000056, 0.000047, 0.000042, 0.000040, 0.000037, 0.000035,
0.000023
]
additional = sigma([add_const, add_coeff, add_factor], fn [i,j,l] -> l * sin(i + j * k) end)
Time.universal_from_dynamical(approx + correction + extra + additional)
end
def lunar_parallax(t, location) do
geo = lunar_altitude(t, location)
delta = lunar_distance(t)
alt = mt(6_378_140) / delta
arg = alt * cos(geo)
asin(arg)
end
def topocentric_lunar_altitude(t, location) do
lunar_altitude(t, location) - lunar_parallax(t, location)
end
@doc false
def mean_lunar_longitude(c) do
c
|> poly([218.3164477, 481267.88123421, -0.0015786, 1 / 538841.0, -1 /65194000.0])
|> degrees()
end
@doc false
def lunar_elongation(c) do
c
|> poly([297.8501921, 445267.1114034, -0.0018819, 1/545868, -1 / 113065000.0])
|> degrees()
end
@doc false
def solar_anomaly(c) do
c
|> poly([357.5291092, 35999.0502909, -0.0001536, 1 / 24490000.0])
|> degrees()
end
@doc false
def lunar_anomaly(c) do
c
|> poly([134.9633964, 477198.8675055, 0.0087414, 1 / 69699.0, -1 / 14712000.0])
|> degrees()
end
defp solar_longitude(t) do
c = julian_centuries_from_moment(t)
Astro.Solar.sun_apparent_longitude_alt(c)
end
@doc false
def lunar_node(t) do
c = julian_centuries_from_moment(t)
moon_node(c + deg(90.0))
|> mod(180.0)
|> Kernel.-(90.0)
end
@doc false
def moon_node(c) do
c
|> poly([93.2720950, 483202.0175233, -0.0036539, -1 / 3526000.0, 1 / 863310000.0])
|> degrees()
end
end