Solar position, orbital mechanics, and equinox/solstice calculations.
Two sources of the Sun's position
The Sun's apparent position comes from one of two sources:
The JPL DE440s ephemeris, read by
Astro.Ephemeris. It is a numerical integration of the solar system, accurate to well under a milliarcsecond, but it covers a limited span of years (1900–2100 for the file bundled with Astro).Analytical series, computed in this module. They describe the Earth's orbit with a mean motion, the equation of the centre and a sum of periodic perturbations. They are less accurate (about 1″ near the present) but give a position for any date.
The accuracy guide compares the two.
The analytical series work from the Earth's orbital elements:
sun_geometric_mean_longitude/1, sun_geometric_mean_anomaly/1,
earth_orbit_eccentricity/1 and sun_equation_of_center/1.
For sunrise and sunset calculations see Astro.Solar.SunRiseSet.
Function groups
Position
solar_position/1— right ascension, declination and distancesolar_ecliptic_longitude/1— apparent ecliptic longitude at a momentsolar_ecliptic_longitude_after/2,estimate_prior_solar_ecliptic_longitude/2
Analytical series
analytical_solar_ecliptic_position/1— apparent ecliptic longitude, latitude and distanceanalytical_solar_position/1— apparent right ascension, declination and distance
Orbital elements
sun_geometric_mean_longitude/1,sun_geometric_mean_anomaly/1earth_orbit_eccentricity/1,sun_equation_of_center/1
Time
equation_of_time/1— difference between apparent and mean solar timesolar_noon/2— the moment the Sun crosses a meridiansolar_noon_utc/2— the same, as minutes since midnight UTC
Equinoxes and solstices
equinox_and_solstice/2— the moment the Sun's longitude reaches 0°, 90°, 180° or 270°
Solar elevation
solar_elevation/1— zenith angle for geometric, civil, nautical, astronomical twilight or a custom elevation
Time conventions
Functions in this module accept either a moment (fractional days
since epoch) or Julian centuries from J2000.0, as noted in each
function's documentation. Use Astro.Time.julian_centuries_from_moment/1
to convert between the two.
Summary
Functions
Returns the Sun's apparent geocentric ecliptic position from analytical series.
Returns the Sun's apparent geocentric right ascension, declination and distance from analytical series.
Returns the eccentricity of the Earth's orbit.
Returns the equation of time in minutes.
Returns the UTC datetime of an equinox or solstice.
Returns an approximate moment at or before t
when the solar ecliptic longitude just exceeded lambda degrees.
Returns the solar ecliptic longitude in degrees.
Returns the moment (UT) of the first time at or after moment t
when the solar ecliptic longitude will be lambda degrees.
Returns the local day on which the Sun's apparent longitude first
reaches lambda at or after the start of a local day.
Returns the local day on which the Sun's apparent longitude last
reached lambda, on or before a local day.
Returns the moment of solar noon on a day at a longitude.
Returns solar noon as minutes since midnight UTC.
Returns the Sun's equation of the centre in degrees.
Returns the Sun's geometric mean anomaly in degrees.
Returns the Sun's geometric mean longitude in degrees.
Functions
@spec analytical_solar_ecliptic_position(Astro.Time.moment()) :: {float(), float(), float()}
Returns the Sun's apparent geocentric ecliptic position from analytical series.
The longitude is the Sun's true geometric longitude from the 49-term
series of Bretagnon and Simon (as given in Calendrical Calculations),
corrected for aberration and for the nutation in longitude of
Astro.Earth.nutation/1. Over 1900–2100 it agrees with the JPL DE440s
ephemeris to about 1″ RMS and 3.3″ at most. The latitude is taken as
zero, since the Sun's never exceeds about 1.2″, and the distance is the
radius vector of the Keplerian ellipse.
Arguments
tis a moment (fractional days since the epoch).
Returns
{longitude, latitude, distance}wherelongitudeis in degrees in the range 0 to 360,latitudeis0.0anddistanceis in astronomical units.
Accuracy
Against JPL over 1900–2100: longitude 0.75″ RMS and 2.3″ at most, with a mean bias of +0.56″, and distance 4,300 km RMS. Between −2000 and 3000 the series agrees with Meeus's chapter 25 series within 45″; at ±10,000 years they differ by more than a day of the Sun's motion. The accuracy guide gives the details.
Examples
iex> {longitude, latitude, distance} =
...> Astro.Solar.analytical_solar_ecliptic_position(Astro.Time.j2000())
iex> {Float.round(longitude, 4), latitude, Float.round(distance, 6)}
{280.369, 0.0, 0.983308}Reference
Edward M. Reingold and Nachum Dershowitz, Calendrical Calculations, for the series of Bretagnon and Simon.
Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 25 (solar coordinates).
@spec analytical_solar_position(Astro.Time.moment()) :: {float(), float(), float()}
Returns the Sun's apparent geocentric right ascension, declination and distance from analytical series.
The ecliptic position of analytical_solar_ecliptic_position/1 is
rotated to the equator with the true obliquity of the ecliptic by
Astro.Coordinates.ecliptic_to_equatorial/3.
Arguments
tis a moment (fractional days since the epoch).
Returns
{right_ascension, declination, distance}whereright_ascensionis in degrees in the range 0 to 360,declinationis in degrees anddistanceis in astronomical units.
Accuracy
Against JPL over 1900–2100: right ascension 0.76″ RMS and declination 0.50″ RMS (2.2″ and 1.3″ at most). The accuracy guide gives the details.
Examples
iex> {right_ascension, declination, distance} =
...> Astro.Solar.analytical_solar_position(Astro.Time.j2000())
iex> {Float.round(right_ascension, 4), Float.round(declination, 4), Float.round(distance, 6)}
{281.2785, -23.0327, 0.983308}Reference
Edward M. Reingold and Nachum Dershowitz, Calendrical Calculations, for the series of Bretagnon and Simon.
Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 13 (transformation of coordinates).
@spec earth_orbit_eccentricity(Astro.Time.julian_centuries()) :: float()
Returns the eccentricity of the Earth's orbit.
Arguments
julian_centuriesis any moment in time expressed as Julian centuries from J2000.0.
Returns
- the eccentricity as a unitless
float. Zero is a circle and values between 0 and 1 are ellipses; the Earth's is about 0.0167 and slowly decreasing (Meeus, Astronomical Algorithms, equation 25.4).
Accuracy
This is a mean element, not a position. Combined as in Meeus, Astronomical Algorithms, chapter 25 (the Sun's true longitude is the mean longitude plus the equation of the centre, then corrected for aberration and nutation), the elements give the Sun's apparent longitude to 0.003° RMS and 0.009° at most against JPL over 1900–2100, the 0.01° Meeus states for the method.
Examples
iex> Astro.Solar.earth_orbit_eccentricity(0.0)
0.016708634Reference
Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 25 (solar coordinates).
Orbital eccentricity, Wikipedia.
Returns the equation of time in minutes.
Arguments
julian_centuriesis any moment in time expressed as Julian centuries from J2000.0.
Returns
- the discrepancy between apparent solar time and
mean solar time in minutes as a
float. A positive value means the sundial is ahead of the clock.
Notes
The equation of time describes the discrepancy between apparent solar time (which directly tracks the Sun's diurnal motion) and mean solar time (which tracks a theoretical mean Sun with uniform motion).
During a year the equation of time ranges from about +16 min 33 s (around 3 November) to −14 min 6 s (around 11 February), with zeros near 15 April, 13 June, 1 September, and 25 December.
The two principal causes are the obliquity of the ecliptic (~23.44°) and the eccentricity of the Earth's orbit (~0.0167).
It is calculated as in Meeus, Astronomical Algorithms, chapter 28: the Sun's mean longitude, less the constant of aberration (0.0057183°), less the Sun's apparent right ascension, plus the nutation in right ascension (Δψ cos ε), converted to minutes at four minutes of time a degree.
Accuracy
Solar noon taken as 12:00 local mean time less this equation of
time agrees with the meridian transit to 0.2 s over 1850–2149. Far
from the present the mean Sun, in dynamical time, and the Earth's
rotation drift apart by ΔT, and the agreement falls to about 3 s at
the year 1000, which is why solar_noon/2 solves for the transit
instead.
Examples
iex> Astro.Solar.equation_of_time(0.0) |> Float.round(4)
-3.2815Reference
Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 28 (equation of time).
Equation of time, Wikipedia.
@spec equinox_and_solstice(integer(), :march | :june | :september | :december) :: {:ok, DateTime.t()} | {:error, :year_out_of_range}
Returns the UTC datetime of an equinox or solstice.
An equinox or solstice is the moment the Sun's apparent ecliptic
longitude reaches a multiple of 90°: 0° at the March equinox, 90° at
the June solstice, 180° at the September equinox and 270° at the
December solstice. It is found by searching for that longitude with
solar_ecliptic_longitude_after/2, starting 20 days before the 1st of
the month the event falls in, so it uses the same positions as the rest
of this module: the JPL ephemeris where it covers the year, and the
analytical series for any other year.
The month is a date in dynamical time, in which the Sun's motion is uniform, so the search starts ΔT earlier in Universal Time. ΔT is about a minute today but grows to days thousands of years from the present, and far enough back the event falls in the previous year in UTC.
Arguments
yearis an integer Gregorian year within 50,000 years of 2000.eventis one of:march,:june,:september, or:decemberidentifying the equinox or solstice.
Returns
{:ok, datetime}wheredatetimeis aDateTime.t()in UTC.{:error, :year_out_of_range}ifyearis more than 50,000 years from 2000.
Accuracy
As for Astro.equinox/3: within a second of Meeus's full VSOP87
time for the June 1962 solstice inside the ephemeris, and 18 s RMS
from JPL with the series outside it.
Examples
iex> {:ok, dt} = Astro.Solar.equinox_and_solstice(2024, :march)
iex> {dt.year, dt.month, dt.day}
{2024, 3, 20}
iex> {:ok, dt} = Astro.Solar.equinox_and_solstice(-1000, :june)
iex> {dt.year, dt.month}
{-1000, 6}Reference
- Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 27 (equinoxes and solstices).
Returns an approximate moment at or before t
when the solar ecliptic longitude just exceeded lambda degrees.
Arguments
lambdais the target solar ecliptic longitude in degrees.tis any moment in time expressed as a moment (fractional days since the epoch).
Returns
- the approximate moment (fractional days since the epoch)
at or before
twhen the solar ecliptic longitude last exceededlambdadegrees.
Examples
# Estimate when the Sun last passed 90° before the June solstice
iex> moment = Astro.Time.date_time_to_moment(~D[2024-06-21])
iex> result = Astro.Solar.estimate_prior_solar_ecliptic_longitude(90, moment)
iex> {:ok, dt} = Astro.Time.date_time_from_moment(result)
iex> dt.year
2024
iex> dt.month
6Reference
- Edward M. Reingold and Nachum Dershowitz, Calendrical
Calculations,
estimate-prior-solar-longitude.
Returns the solar ecliptic longitude in degrees.
Arguments
tis any moment in time expressed as a moment (fractional days since the epoch).
Returns
- the solar ecliptic longitude in degrees as a
float, in the range 0..360.
Notes
The solar ecliptic 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 in its orbit around the Sun, taken as 0° 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) has elapsed, without the inaccuracies of a calendar date which is perturbed by leap years and calendar imperfections.
The longitude comes from the JPL ephemeris where the loaded kernel
covers t, and from analytical_solar_ecliptic_position/1, accurate
to about 1″ near the present, everywhere else. Calendars that count
months by the Sun's longitude, such as the Chinese and Japanese
lunisolar calendars, find their solar terms for dates of any era this
way.
Accuracy
From JPL inside the loaded ephemeris; elsewhere from
analytical_solar_ecliptic_position/1, within 0.75″ RMS and about
2″ of JPL over 1900–2100. The accuracy guide gives
the details.
Examples
iex> moment = Astro.Time.date_time_to_moment(~D[2024-03-20])
iex> Astro.Solar.solar_ecliptic_longitude(moment)
359.87140902027863
iex> moment = Astro.Time.date_time_to_moment(~D[1000-06-01])
iex> Astro.Solar.solar_ecliptic_longitude(moment) |> Float.round(4)
69.5898Reference
- Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 25 (solar coordinates).
@spec solar_ecliptic_longitude_after(number(), Astro.Time.time()) :: Astro.Time.time()
Returns the moment (UT) of the first time at or after moment t
when the solar ecliptic longitude will be lambda degrees.
Arguments
lambdais the target solar ecliptic longitude in degrees.tis any moment in time expressed as a moment (fractional days since the epoch).
Returns
- the moment (fractional days since the epoch) when
the solar ecliptic longitude reaches
lambdadegrees.
Accuracy
The search ends within a millisecond of the crossing, so the moment is as accurate as the longitude: the Sun moves about 2.5″ a minute, so 1″ of longitude is about 24 s.
Examples
# Find the June solstice (longitude 90°) in 2024
iex> moment = Astro.Time.date_time_to_moment(~D[2024-05-22])
iex> result = Astro.Solar.solar_ecliptic_longitude_after(90, moment)
iex> {:ok, dt} = Astro.Time.date_time_from_moment(result)
iex> dt.year
2024
iex> dt.month
6Reference
- Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 27 (equinoxes and solstices).
@spec solar_longitude_day_on_or_after( number(), integer(), number() | (Astro.Time.moment() -> number()) ) :: integer()
Returns the local day on which the Sun's apparent longitude first
reaches lambda at or after the start of a local day.
Calendars that follow the Sun, such as the Chinese solar terms, need
only the day on which its longitude reaches a value, in the time they
keep. This is that day, at the offset in force when the longitude is
reached: the same day as solar_ecliptic_longitude_after/2 from
local midnight, read at the local date, found faster.
The Sun's longitude always advances, at between 0.95° and 1.03° a day, so one longitude evaluated near the crossing places it within a narrow window. The search narrows the window until it falls on one local day, and only a crossing near local midnight is found to its moment.
Arguments
lambdais the apparent ecliptic longitude in degrees.dayis a local day as an integer number of days since0000-01-01.offsetis the local time's offset from UTC in days, as a number, or a 1-arity function that returns the offset for a moment.
Returns
- the local day as an integer number of days since
0000-01-01.
Accuracy
The same day as the moment search; see
solar_ecliptic_longitude_after/2.
Examples
iex> day = Date.to_gregorian_days(~D[2024-01-01])
iex> Astro.Solar.solar_longitude_day_on_or_after(315, day, 8 / 24) |> Date.from_gregorian_days()
~D[2024-02-04]Reference
- Edward M. Reingold and Nachum Dershowitz, Calendrical
Calculations,
solar-longitude-after.
@spec solar_longitude_day_on_or_before( number(), integer(), number() | (Astro.Time.moment() -> number()) ) :: integer()
Returns the local day on which the Sun's apparent longitude last
reached lambda, on or before a local day.
This is the day, at the offset in force when the longitude is
reached, of the last time it reached lambda before the end of
day. With lambda 270 it is the day of the December solstice on
or before day, which the Chinese calendar counts its years from.
See solar_longitude_day_on_or_after/3 for how it is found.
Arguments
lambdais the apparent ecliptic longitude in degrees.dayis a local day as an integer number of days since0000-01-01.offsetis the local time's offset from UTC in days, as a number, or a 1-arity function that returns the offset for a moment.
Returns
- the local day as an integer number of days since
0000-01-01.
Accuracy
The day of the crossing found to within a millisecond, as
solar_ecliptic_longitude_after/2 finds it.
Examples
iex> day = Date.to_gregorian_days(~D[2025-03-01])
iex> Astro.Solar.solar_longitude_day_on_or_before(270, day, 8 / 24) |> Date.from_gregorian_days()
~D[2024-12-21]Reference
- Edward M. Reingold and Nachum Dershowitz, Calendrical
Calculations,
estimate-prior-solar-longitudeand the winter solstice of the Chinese calendar.
@spec solar_noon(Astro.Time.moment(), Astro.longitude()) :: Astro.Time.moment()
Returns the moment of solar noon on a day at a longitude.
Solar noon is the moment the Sun crosses a location's meridian,
when its local hour angle (the local apparent sidereal time less
the Sun's apparent right ascension) is zero. The search starts at
12:00 local mean time, longitude / 360 of a day before 12:00 UT,
which is within about 16 minutes of the answer (the equation of
time, equation_of_time/1). Each step then moves the moment by the
hour angle divided by the 360° a day at which it grows, until the
step is under a millisecond; three steps are usually enough.
Arguments
tis the moment of 0h UT on the day of solar noon.longitudeis the longitude in degrees of the location from which solar noon is to be measured. West is negative.
Returns
- the moment of solar noon.
Accuracy
The hour angle is solved to a millisecond, so solar noon is as
accurate as the Sun's right ascension and the sidereal time; see
Astro.solar_noon/2.
Examples
iex> midnight = Astro.Time.date_time_to_moment(~D[2019-12-06])
iex> noon = Astro.Solar.solar_noon(midnight, 151.20666584)
iex> {:ok, date_time} = Astro.Time.date_time_from_moment(noon)
iex> DateTime.truncate(date_time, :second)
~U[2019-12-06 01:45:52Z]Reference
- Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 15 (rising, transit and setting).
@spec solar_noon_utc(float(), Astro.longitude()) :: float()
Returns solar noon as minutes since midnight UTC.
This is solar_noon/2 measured in minutes from the start of the
day.
Arguments
julian_centuriesis 0h UT on the day of solar noon, as Julian centuries from J2000.0 (for example, fromAstro.Time.julian_day_from_date/1andAstro.Time.julian_centuries_from_julian_day/1).longitudeis the longitude in degrees of the location from which solar noon is to be measured. West is negative.
Returns
- solar noon as a
floatnumber of minutes since midnight UTC.
Accuracy
As for solar_noon/2.
Examples
iex> julian_day = Astro.Time.julian_day_from_date(~D[2000-01-01])
iex> julian_centuries = Astro.Time.julian_centuries_from_julian_day(julian_day)
iex> Astro.Solar.solar_noon_utc(julian_centuries, 151.2093) |> Float.round(2)
118.25
@spec sun_equation_of_center(Astro.Time.julian_centuries()) :: float()
Returns the Sun's equation of the centre in degrees.
Arguments
julian_centuriesis any moment in time expressed as Julian centuries from J2000.0.
Returns
- the equation of the centre in degrees as a
float.
Notes
The equation of the centre is the difference between the true anomaly, where the Earth actually is on its ellipse, and the mean anomaly, where it would be at a constant speed. Adding it to the mean longitude gives the true longitude. This is the series of Meeus, Astronomical Algorithms, chapter 25, in the mean anomaly.
Accuracy
This is a mean element, not a position. Combined as in Meeus, Astronomical Algorithms, chapter 25 (the Sun's true longitude is the mean longitude plus the equation of the centre, then corrected for aberration and nutation), the elements give the Sun's apparent longitude to 0.003° RMS and 0.009° at most against JPL over 1900–2100, the 0.01° Meeus states for the method.
Examples
iex> Astro.Solar.sun_equation_of_center(0.0)
-0.08430148943719645Reference
Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 25 (solar coordinates).
Equation of the center, Wikipedia.
@spec sun_geometric_mean_anomaly(Astro.Time.julian_centuries()) :: float()
Returns the Sun's geometric mean anomaly in degrees.
Arguments
julian_centuriesis any moment in time expressed as Julian centuries from J2000.0.
Returns
- the mean anomaly in degrees as a
float, in the range 0..360.
Notes
The mean anomaly is the angle the Earth would have moved through since perihelion if it travelled its orbit at a constant speed (Meeus, Astronomical Algorithms, equation 25.3). It grows uniformly with time, which makes it the natural argument for the equation of the centre.
Accuracy
This is a mean element, not a position. Combined as in Meeus, Astronomical Algorithms, chapter 25 (the Sun's true longitude is the mean longitude plus the equation of the centre, then corrected for aberration and nutation), the elements give the Sun's apparent longitude to 0.003° RMS and 0.009° at most against JPL over 1900–2100, the 0.01° Meeus states for the method.
Examples
iex> Astro.Solar.sun_geometric_mean_anomaly(0.0)
357.52911Reference
Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 25 (solar coordinates).
Mean anomaly, Wikipedia.
Returns the Sun's geometric mean longitude in degrees.
Arguments
julian_centuriesis any moment in time expressed as Julian centuries from J2000.0.
Returns
- the mean solar longitude in degrees as a
float, in the range 0..360.
Notes
Mean longitude is a convenient uniform measure of how far around its orbit a body has progressed since passing the reference direction. While mean longitude assumes constant speed, true longitude accounts for the body's actual speed which varies around its elliptical orbit. The difference between the two is the equation of the centre.
Accuracy
This is a mean element, not a position. Combined as in Meeus, Astronomical Algorithms, chapter 25 (the Sun's true longitude is the mean longitude plus the equation of the centre, then corrected for aberration and nutation), the elements give the Sun's apparent longitude to 0.003° RMS and 0.009° at most against JPL over 1900–2100, the 0.01° Meeus states for the method.
Examples
iex> Astro.Solar.sun_geometric_mean_longitude(0.0)
280.46646Reference
Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 25 (solar coordinates).
Mean longitude, Wikipedia.