Current section

Files

Jump to
astro lib astro solar.ex
Raw

lib/astro/solar.ex

defmodule Astro.Solar do
@moduledoc """
Implements sunrise and sunset according to the
US NOAA algorithm which is based upon
[Astronomical Algorithms](https://www.amazon.com/Astronomical-Algorithms-Jean-Meeus/dp/0943396352)
by Jean Meeus.
"""
alias Astro.{Math, Earth, Time, Location}
import Time, only: [
minutes_per_day: 0,
hours_per_day: 0,
minutes_per_hour: 0,
julian_centuries_from_moment: 1
]
import Math, only: [
sin: 1,
cos: 1,
sigma: 2,
deg: 1,
mod: 2
]
import Astro.Earth, only: [
nutation: 1
]
@minutes_per_degree 4.0
@solar_elevation %{
geometric: 90.0,
civil: 96.0,
nautical: 102.0,
astronomical: 108.0
}
@doc false
def solar_position(t) do
julian_centuries = julian_centuries_from_moment(t)
apparent_longitude = sun_apparent_longitude(julian_centuries)
declination = solar_declination(julian_centuries)
distance = solar_distance(julian_centuries)
right_ascension = Astro.right_ascension(t, 0.0, apparent_longitude)
{right_ascension, declination, distance}
end
@doc false
def solar_distance(julian_centuries) do
eccentricity = earth_orbit_eccentricity(julian_centuries)
mean_solar_anomaly = sun_geometric_mean_anomaly(julian_centuries)
true_anomaly = mean_solar_anomaly + julian_centuries
1.000001018 * (1.0 - eccentricity * eccentricity) / (1 + eccentricity * cos(true_anomaly))
end
@doc false
@spec sun_rise_or_set(Astro.location(), Astro.date(), map() | keyword()) ::
{:ok, DateTime.t()} | {:error, :time_zone_not_found | :no_time}
def sun_rise_or_set(location, date, options) when is_list(options) do
options =
Astro.default_options()
|> Keyword.merge(options)
|> Map.new()
sun_rise_or_set(location, date, options)
end
@doc false
def sun_rise_or_set(%Geo.PointZ{} = location, %Date{} = date, options) do
with {:ok, naive_datetime} <-
NaiveDateTime.new(date.year, date.month, date.day, 0, 0, 0, {0, 0}, date.calendar) do
sun_rise_or_set(location, naive_datetime, options)
end
end
@doc false
def sun_rise_or_set(%Geo.PointZ{} = location, %NaiveDateTime{} = datetime, options) do
%{time_zone_database: time_zone_database} = options
with {:ok, iso_datetime} <- NaiveDateTime.convert(datetime, Calendar.ISO),
{:ok, time_zone} <- Time.timezone_at(location, options[:time_zone_resolver]),
{:ok, utc_datetime} <- Time.datetime_in_utc(iso_datetime, time_zone, time_zone_database) do
sun_rise_or_set(location, utc_datetime, options)
end
end
@doc false
def sun_rise_or_set(%Geo.PointZ{} = location, %DateTime{} = datetime, options) do
with {:ok, iso_datetime} <-
DateTime.convert(datetime, Calendar.ISO),
{:ok, adjusted_datetime} <-
Time.antimeridian_adjustment(location, iso_datetime, options),
{:ok, moment_of_rise_or_set} <-
utc_sun_rise_or_set(adjusted_datetime, location, options),
{:ok, utc_rise_or_set} <-
Time.moment_to_datetime(moment_of_rise_or_set, adjusted_datetime),
{:ok, adjusted_rise_or_set} <-
Time.adjust_for_wraparound(utc_rise_or_set, location, options),
{:ok, local_rise_or_set} <-
Time.datetime_in_requested_zone(adjusted_rise_or_set, location, options) do
DateTime.convert(local_rise_or_set, datetime.calendar)
end
end
@doc false
def sun_rise_or_set(location, datetime, options) do
Location.normalize_location(location)
|> sun_rise_or_set(datetime, options)
end
defp utc_sun_rise_or_set(utc_datetime, location, %{rise_or_set: :rise} = options) do
utc_sunrise(utc_datetime, location, options)
end
defp utc_sun_rise_or_set(utc_datetime, location, %{rise_or_set: :set} = options) do
utc_sunset(utc_datetime, location, options)
end
defp utc_sunrise(date, %Geo.PointZ{} = geo_location, options) do
solar_elevation =
options
|> Map.fetch!(:solar_elevation)
|> solar_elevation()
utc_sun_position(date, geo_location, solar_elevation, :sunrise)
end
defp utc_sunset(date, %Geo.PointZ{} = geo_location, options) do
solar_elevation =
options
|> Map.fetch!(:solar_elevation)
|> solar_elevation()
utc_sun_position(date, geo_location, solar_elevation, :sunset)
end
@valid_solar_elevation Map.keys(@solar_elevation)
@doc false
def solar_elevation(solar_elevation) when solar_elevation in @valid_solar_elevation do
Map.get(@solar_elevation, solar_elevation)
end
def solar_elevation(solar_elevation) when is_number(solar_elevation) do
solar_elevation
end
@doc """
Returns the UTC time of sun's position
for a given location as a float time-of-day.
## Arguments
* `date` is a `DateTime.t()` in the UTC
time zone.
* `location` is any `t:Geo.PointZ.t/0`
location.
* `solar_elevation` is the required solar
elevation in degrees (90 degrees for sunrise
and sunset).
* `mode` is `:sunrise` or `:sunset`.
## Returns
* `{:ok, moment}` where `moment` is float
representing the number of hours after
midnight for sunrise or sunset or
* `{:error, :no_time}` if there is no
sunrise/sunset for the given date at the
given location. This can occur for very
high latitudes in winter and summer.
## Notes
This implementation is based on equations from
[Astronomical Algorithms](https://www.amazon.com/astronomical-algorithms-jean-meeus/dp/0943396611),
by Jean Meeus. The sunrise and sunset results are
theoretically accurate to within a minute for
locations between +/- 72° latitude, and within
10 minutes outside of those latitudes. However, due to
variations in atmospheric composition, temperature,
pressure and conditions, observed values may vary from
calculations.
"""
@spec utc_sun_position(DateTime.t(), Geo.PointZ.t(), float(), :sunrise | :sunset) ::
{:ok, float} | {:error, :no_time}
def utc_sun_position(date, %Geo.PointZ{coordinates: {lng, lat, alt}}, solar_elevation, mode) do
adjusted_solar_elevation = Earth.adjusted_solar_elevation(solar_elevation, alt)
with {:ok, utc_time_in_minutes} <-
calculate_utc_sun_position(Time.ajd(date), lat, -lng, adjusted_solar_elevation, mode) do
{:ok, Math.mod(utc_time_in_minutes / minutes_per_hour(), hours_per_day())}
end
end
defp calculate_utc_sun_position(julian_day, latitude, longitude, solar_elevation, mode) do
julian_centuries = Time.julian_centuries_from_julian_day(julian_day)
# first pass using solar noon
noonmin = solar_noon_utc(julian_centuries, longitude)
tnoon = Time.julian_centuries_from_julian_day(julian_day + noonmin / minutes_per_day())
first_pass = approximate_utc_sun_position(tnoon, latitude, longitude, solar_elevation, mode)
# refine using output of first pass
trefinement = Time.julian_centuries_from_julian_day(julian_day + first_pass / minutes_per_day())
position =
approximate_utc_sun_position(trefinement, latitude, longitude, solar_elevation, mode)
{:ok, position}
rescue
ArithmeticError ->
{:error, :no_time}
end
defp approximate_utc_sun_position(
approx_julian_centuries,
latitude,
longitude,
solar_elevation,
mode
) do
eq_time = equation_of_time(approx_julian_centuries)
solar_dec = solar_declination(approx_julian_centuries)
hour_angle = sun_hour_angle_at_horizon(latitude, solar_dec, solar_elevation, mode)
delta = longitude - Math.to_degrees(hour_angle)
time_delta = delta * 4.0
720.0 + time_delta - eq_time
end
defp sun_hour_angle_at_horizon(latitude, solar_dec, solar_elevation, mode) do
lat_r = Math.to_radians(latitude)
solar_dec_r = Math.to_radians(solar_dec)
solar_elevation_r = Math.to_radians(solar_elevation)
hour_angle =
:math.acos(
:math.cos(solar_elevation_r) / (:math.cos(lat_r) * :math.cos(solar_dec_r)) -
:math.tan(lat_r) * :math.tan(solar_dec_r)
)
if mode == :sunset do
-hour_angle
else
hour_angle
end
end
@doc """
Returns the solar declination in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* the solar declination in degrees as
a `float`.
## Notes
The solar declination is the angle between
the direction of the center of the solar
disk measured from Earth's center and the
equatorial plane.
"""
@spec solar_declination(float) :: float()
def solar_declination(julian_centuries) do
correction = obliquity_correction(julian_centuries)
lambda = sun_apparent_longitude(julian_centuries)
sint = Math.sin(correction) * Math.sin(lambda)
sint
|> :math.asin()
|> Math.to_degrees()
end
@doc """
Returns the solar longitude in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* the solar longitude in degrees as
a `float`.
## Notes
The solar longitude is the position of the sun on the celestial
sphere along the ecliptic.
It is also an effective measure of the position of the earth
(or any other sun-orbiting body) in its orbit around the sun,
usually taken as zero at the moment of the vernal equinox.
Since it is based on how far the earth has moved in its orbit
since the equinox, it is a measure of what time of the tropical
year (the year of seasons) the planet is in, but without the
inaccuracies of a calendar date, which is perturbed by leap years
and calendar imperfections.
"""
def solar_longitude(t) do
c = Time.julian_centuries_from_moment(t)
sun_apparent_longitude(c)
end
@doc """
Return the moment UT of the first time at or after moment, tee,
when the solar longitude will be lambda degrees.
"""
@spec solar_longitude_after(number(), Time.time()) :: Time.time()
def solar_longitude_after(lambda, t) do
rate = Time.mean_tropical_year() / deg(360)
tau = t + rate * mod(lambda - solar_longitude(t), 360)
a = max(t, tau - 5)
b = tau + 5
Math.invert_angular(&solar_longitude/1, lambda, a, b)
end
@doc """
Return approximate moment at or before tee
when solar longitude just exceeded lambda degrees.
"""
def estimate_prior_solar_longitude(lambda, t) do
rate = (Time.mean_tropical_year() / deg(360))
tau = t - (rate * mod(solar_longitude(t) - lambda, 360))
cap_delta = mod(solar_longitude(tau) - lambda + deg(180), 360) - deg(180)
min(t, tau - (rate * cap_delta))
end
@doc """
Returns the suns apparent longitude in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* equation of the center in degrees
as a `float`.
## Notes
The apparent longitude is the sun's celestial
longitude corrected for aberration and nutation
as opposed to mean longitude.
An equinox is the instant when the Sun's
apparent geocentric longitude is 0° (northward
equinox) or 180° (southward equinox).
"""
@spec sun_apparent_longitude(Time.julian_centuries()) :: float()
def sun_apparent_longitude(julian_centuries) do
true_longitude = sun_true_longitude(julian_centuries)
omega = omega(julian_centuries)
true_longitude - 0.00569 - 0.00478 * :math.sin(Math.to_radians(omega))
|> Math.mod(360)
end
defp omega(julian_centuries) do
125.04 - 1934.136 * julian_centuries
end
@doc false
@spec sun_apparent_longitude_alt(Time.julian_centuries()) :: Time.season()
def sun_apparent_longitude_alt(julian_centuries) do
coefficients = [
403406.0, 195207.0, 119433.0, 112392.0, 3891.0, 2819.0, 1721.0,
660.0, 350.0, 334.0, 314.0, 268.0, 242.0, 234.0, 158.0, 132.0, 129.0, 114.0,
99.0, 93.0, 86.0, 78.0, 72.0, 68.0, 64.0, 46.0, 38.0, 37.0, 32.0, 29.0, 28.0, 27.0, 27.0,
25.0, 24.0, 21.0, 21.0, 20.0, 18.0, 17.0, 14.0, 13.0, 13.0, 13.0, 12.0, 10.0, 10.0, 10.0,
10.0
]
multipliers = [
0.9287892, 35999.1376958, 35999.4089666,
35998.7287385, 71998.20261, 71998.4403,
36000.35726, 71997.4812, 32964.4678,
-19.4410, 445267.1117, 45036.8840, 3.1008,
22518.4434, -19.9739, 65928.9345,
9038.0293, 3034.7684, 33718.148, 3034.448,
-2280.773, 29929.992, 31556.493, 149.588,
9037.750, 107997.405, -4444.176, 151.771,
67555.316, 31556.080, -4561.540,
107996.706, 1221.655, 62894.167,
31437.369, 14578.298, -31931.757,
34777.243, 1221.999, 62894.511,
-4442.039, 107997.909, 119.066, 16859.071,
-4.578, 26895.292, -39.127, 12297.536,
90073.778
]
addends = [
270.54861, 340.19128, 63.91854, 331.26220,
317.843, 86.631, 240.052, 310.26, 247.23,
260.87, 297.82, 343.14, 166.79, 81.53,
3.50, 132.75, 182.95, 162.03, 29.8,
266.4, 249.2, 157.6, 257.8, 185.1, 69.9,
8.0, 197.1, 250.4, 65.3, 162.7, 341.5,
291.6, 98.5, 146.7, 110.0, 5.2, 342.6,
230.9, 256.1, 45.3, 242.9, 115.2, 151.8,
285.3, 53.3, 126.6, 205.7, 85.9,
146.1
]
lambda =
deg(282.7771834) + deg(36000.76953744) * julian_centuries +
deg(0.000005729577951308232) *
sigma(
[coefficients, addends, multipliers],
fn [x, y, z] -> x * sin(y + z * julian_centuries) end
)
mod(lambda + aberration(julian_centuries) + nutation(julian_centuries), 360.0)
end
@doc false
@spec aberration(Time.moment()) :: Astro.angle()
def aberration(c) do
deg(0.0000974) * cos(deg(177.63) + deg(35999.01848) * c) - deg(0.005575)
end
@doc """
Returns the suns true longitude in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* the suns true longitude in degrees
as a `float`.
## Notes
In celestial mechanics true longitude is the
ecliptic longitude at which an orbiting body
could actually be found if its inclination
were zero.
Together with the inclination and the ascending
node, the true longitude can tell us the precise
direction from the central object at which the
body would be located at a particular time.
"""
@spec sun_true_longitude(float) :: float()
def sun_true_longitude(julian_centuries) do
sun_geometric_mean_longitude(julian_centuries) + sun_equation_of_center(julian_centuries)
end
@doc """
Return the sun's equation of the center in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* equation of the center in degrees
as a `float`.
## Notes
In two-body, Keplerian orbital mechanics, the equation
of the center is the angular difference between the
actual position of a body in its elliptical orbit and
the position it would occupy if its motion were uniform,
in a circular orbit of the same period.
It is defined as the difference true anomaly, ν,
minus mean anomaly, M, and is typically expressed a
function of mean anomaly, M, and orbital eccentricity, e.
"""
@spec sun_equation_of_center(float) :: float()
def sun_equation_of_center(julian_centuries) do
mrad = sun_geometric_mean_anomaly(julian_centuries) |> Math.to_radians()
sinm = :math.sin(mrad)
sin2m = :math.sin(2 * mrad)
sin3m = :math.sin(3 * mrad)
sinm * (1.914602 - julian_centuries * (0.004817 + 0.000014 * julian_centuries)) +
sin2m * (0.019993 - 0.000101 * julian_centuries) +
sin3m * 0.000289
end
@doc """
Returns solar noon as minutes since
midnight UTC.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
* `longitude` is the longitude in degrees
of the location from which solar noon
is to be measured.
## Returns
* solar noon as a `float` number of
minutes since midnight UTC.
## Notes
Solar noon is the moment when the Sun passes a
location's meridian and reaches its highest position
in the sky. In most cases, it doesn't happen at 12 o'clock.
At solar noon, the Sun reaches its
highest position in the sky as it passes the
local meridian.
"""
@spec solar_noon_utc(float, Astro.longitude()) :: float()
def solar_noon_utc(julian_centuries, longitude) do
century_start = Time.julian_day_from_julian_centuries(julian_centuries)
# first pass to yield approximate solar noon
approx_tnoon = Time.julian_centuries_from_julian_day(century_start + longitude / 360.0)
approx_eq_time = equation_of_time(approx_tnoon)
approx_sol_noon = 720.0 + longitude * @minutes_per_degree - approx_eq_time
# refinement using output of first pass
tnoon = Time.julian_centuries_from_julian_day(century_start - 0.5 + approx_sol_noon / minutes_per_day())
eq_time = equation_of_time(tnoon)
720.0 + longitude * @minutes_per_degree - eq_time
end
@doc """
Returns the equation of time in minutes.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* The discrepency between apparent time and
mean solar time in minutes as a `float`.
## Notes
The equation of time describes the discrepancy between
two kinds of solar time. The word equation is used in
the medieval sense of "reconcile a difference". The two
times that differ are the apparent solar time, which
directly tracks the diurnal motion of the Sun, and mean
solar time, which tracks a theoretical mean Sun with uniform
motion. Apparent solar time can be obtained by measurement
of the current position (hour angle) of the Sun, as
indicated (with limited accuracy) by a sundial. Mean solar
time, for the same place, would be the time indicated by a steady
clock set so that over the year its differences from apparent
solar time would have a mean of zero.
During a year the equation of time varies as shown on the
graph; its change from one year to the next is slight.
Apparent time, and the sundial, can be ahead (fast) by as
much as 16 min 33 s (around 3 November), or behind (slow) by
as much as 14 min 6 s (around 11 February). The equation of
time has zeros near 15 April, 13 June, 1 September, and
25 December. Ignoring very slow changes in the Earth's
orbit and rotation, these events are repeated at the same
times every tropical year. However, due to the non-integral
number of days in a year, these dates can vary by a day or
so from year to year.
The graph of the equation of time is closely approximated by
the sum of two sine curves, one with a period of a year and
one with a period of half a year. The curves reflect two
astronomical effects, each causing a different non-uniformity
in the apparent daily motion of the Sun relative to the stars:
* the obliquity of the ecliptic (the plane of the Earth's annual
orbital motion around the Sun), which is inclined by about 23.44
degrees relative to the plane of the Earth's equator; and
* the eccentricity of the Earth's orbit around the Sun, which is
about 0.0167.
The equation of time is constant only for a planet with zero axial
tilt and zero orbital eccentricity. On Mars the difference between
sundial time and clock time can be as much as 50 minutes, due to
the considerably greater eccentricity of its orbit. The planet
Uranus, which has an extremely large axial tilt, has an equation
of time that makes its days start and finish several hours earlier
or later depending on where it is in its orbit.
"""
@spec equation_of_time(float) :: float()
def equation_of_time(julian_centuries) when is_float(julian_centuries) do
epsilon = obliquity_correction(julian_centuries) |> Math.to_radians()
sgml = sun_geometric_mean_longitude(julian_centuries) |> Math.to_radians()
sgma = sun_geometric_mean_anomaly(julian_centuries) |> Math.to_radians()
eoe = earth_orbit_eccentricity(julian_centuries)
y = :math.tan(epsilon / 2.0)
y = y * y
sin2l0 = :math.sin(2.0 * sgml)
sin4l0 = :math.sin(4.0 * sgml)
cos2l0 = :math.cos(2.0 * sgml)
sinm = :math.sin(sgma)
sin2m = :math.sin(2.0 * sgma)
eq_time =
y * sin2l0 - 2.0 * eoe * sinm + 4.0 * eoe * y * sinm * cos2l0 - 0.5 * y * y * sin4l0 -
1.25 * eoe * eoe * sin2m
Math.to_degrees(eq_time) * 4.0
end
@doc """
Returns the earth orbit eccentricity.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* a unitless value of eccentricity as a `float`.
A value of `0` is a circular orbit, values
between `0` and `1` form an elliptic orbit,
`1` is a parabolic escape orbit, and greater
than `1` is a hyperbola.
## Notes
The orbital eccentricity of earth - and any astronomical
object - is a dimensionless parameter that determines
the amount by which its orbit around another body
deviates from a perfect circle. The term derives
its name from the parameters of conic sections, as
every Kepler orbit is a conic section.
"""
@spec earth_orbit_eccentricity(float) :: float()
def earth_orbit_eccentricity(julian_centuries) do
0.016708634 - julian_centuries * (0.000042037 + 0.0000001267 * julian_centuries)
end
@doc """
Returns the suns geometric mean anomoly in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* the mean anomoly in degrees as a `float`
## Notes
In celestial mechanics, the mean anomaly is the
fraction of an elliptical orbit's period that has
elapsed since the orbiting body passed periapsis,
expressed as an angle which can be used in calculating
the position of that body in the classical two-body
problem.
It is the angular distance from the pericenter
which a fictitious body would have if it moved
in a circular orbit, with constant speed, in the same
orbital period as the actual body in its elliptical orbit.
"""
@spec sun_geometric_mean_anomaly(float) :: float()
def sun_geometric_mean_anomaly(julian_centuries) do
anomaly = 357.52911 + julian_centuries * (35999.05029 - 0.0001537 * julian_centuries)
Math.mod(anomaly, 360.0)
end
@doc """
Returns the sun's geometric mean longitude in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* the mean solar longitude in degrees as a float.
## Notes
Mean longitude, like mean anomaly, does not measure
an angle between any physical objects. It is simply
a convenient uniform measure of how far around its orbit
a body has progressed since passing the reference
direction. While mean longitude measures a mean position
and assumes constant speed, true longitude measures the
actual longitude and assumes the body has moved with its
actual speed, which varies around its elliptical orbit.
The difference between the two is known as the equation
of the center.
"""
@spec sun_geometric_mean_longitude(float) :: float()
def sun_geometric_mean_longitude(julian_centuries) do
longitude = 280.46646 + julian_centuries * (36000.76983 + 0.0003032 * julian_centuries)
Math.mod(longitude, 360.0)
end
@doc """
Returns the obliquity correction in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* the obliquity correction in
degrees as a `float`.
"""
@spec obliquity_correction(float) :: float()
def obliquity_correction(julian_centuries) do
obliquity_of_ecliptic = mean_obliquity_of_ecliptic(julian_centuries)
omega = omega(julian_centuries)
correction = obliquity_of_ecliptic + 0.00256 * :math.cos(Math.to_radians(omega))
Math.mod(correction, 360.0)
end
@doc """
Returns the mean obliquity of the ecliptic in degrees.
## Arguments
* `julian_centuries` is the any moment
in time expressed as julian centuries.
## Returns
* the mean obliquity of the ecliptic in
degrees as a `float`.
## Notes
Obliquity, also known as tilt, is the angle between
the rotation access of the earth from the orbital
plane of the earth around the sun.
Earth's obliquity angle is measured from the imaginary
line that runs perpendicular to another imaginary line;
Earth's ecliptic plane or orbital plane
.
At the moment, Earth's obliquity is about 23.4 degrees
and decreasing. We say 'at the moment' because the
obliquity changes over time, although very, very slowly.
"""
@spec mean_obliquity_of_ecliptic(float) :: float()
def mean_obliquity_of_ecliptic(julian_centuries) do
seconds =
21.448 -
julian_centuries * (46.8150 + julian_centuries * (0.00059 - julian_centuries * 0.001813))
# in degrees
23.0 + (26.0 + seconds / 60.0) / 60.0
end
@doc """
Returns the datetime of an equinox or solstice
"""
@spec equinox_and_solstice(pos_integer, :march | :june | :september | :december) ::
{:ok, DateTime.t()}
def equinox_and_solstice(year, event) when event in [:march, :june, :september, :december] do
jde0 = initial_estimate(year, event)
t = (jde0 - 2_451_545.0) / 36_525
w = 35_999.373 * t - 2.47
dl = 1 + 0.0334 * Math.cos(w) + 0.0007 * Math.cos(2.0 * w)
s = periodic24(t)
jde = jde0 + 0.00001 * s / dl
{:ok, tdt} = Time.datetime_from_julian_days(jde)
Time.utc_datetime_from_terrestrial_datetime(tdt)
end
defp initial_estimate(year, event) do
year = (year - 2000) / 1000
equinox_and_solstice_solar_terms()
|> Map.get(event)
|> Enum.with_index()
|> Enum.reduce(0, fn {term, i}, acc ->
acc + term * :math.pow(year, i)
end)
end
defp equinox_and_solstice_solar_terms do
%{
march: [2_451_623.80984, 365_242.37404, 0.05169, -0.00411, -0.00057],
june: [2_451_716.56767, 365_241.62603, 0.00325, 0.00888, -0.00030],
september: [2_451_810.21715, 365_242.01767, -0.11575, 0.00337, 0.00078],
december: [2_451_900.05952, 365_242.74049, -0.06223, -0.00823, 0.00032]
}
end
defp periodic24(t) do
[a, b, c] = periodic24_terms()
periodic24(t, a, b, c)
end
defp periodic24(_t, [], [], []) do
0
end
defp periodic24(t, [a | rest_a], [b | rest_b], [c | rest_c]) do
a * Math.cos(b + c * t) + periodic24(t, rest_a, rest_b, rest_c)
end
defp periodic24_terms do
[
[
485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, 18,
17, 16, 14, 12, 12, 12, 9, 8
],
[
324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, 243.58,
119.81, 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04,
199.76, 95.39, 287.11, 320.81, 227.73, 15.45
],
[
1934.136, 32964.467, 20.186, 445_267.112, 45036.886, 22518.443, 65928.934,
3034.906, 9037.513, 33718.147, 150.678, 2281.226, 29929.562, 31555.956,
4443.417, 67555.328, 4562.452, 62894.029, 31436.921, 14577.848, 31931.756,
34777.259, 1222.114, 16859.074
]
]
end
end