Astro (Astro v2.11.0)

Copy Markdown View Source

High-level API for common astronomical observations.

This module is the primary public interface for the Astro library. It provides sunrise and sunset, moonrise and moonset, equinoxes and solstices, lunar phases, crescent visibility, and the positions of the sun and moon. Functions accept standard Elixir dates and date times. Those that can fail, such as the rise and set times, return {:ok, value} or {:error, reason}. The positions and phases, which cannot fail, return their value directly.

For lower-level access see Astro.Solar, Astro.Lunar, Astro.Time, Astro.Earth and Astro.Ephemeris.

How accurate the results are, inside and outside the JPL ephemeris and far from the present, is described in the accuracy guide.

Specifying a location

A location is a {longitude, latitude} tuple (note the order, which matches Geo.Point), a Geo.Point struct, or a Geo.PointZ struct that also carries an elevation in metres.

  • Longitude is positive east and negative west, in degrees.

  • Latitude is positive north and negative south, in degrees.

Time zone resolution

The rise and set functions (sunrise/3, sunset/3, moonrise/3 and moonset/3) return a DateTime in the local time zone of the location, resolved with TzWorld when it is a dependency. These options override that:

  • :time_zone is a time zone name, :utc, or :default to resolve the time zone from the location.

  • :time_zone_database is the time zone database module, such as Tz.TimeZoneDatabase.

  • :time_zone_resolver is a 1-arity function that receives a Geo.Point and returns {:ok, time_zone_name}.

Function groups

Solar

Lunar

New moon and phase searches

Crescent visibility

Equinoxes and solstices

  • equinox/3 returns the March or September equinox.

  • solstice/3 returns the June or December solstice.

Summary

Functions

Returns the date time of a given lunar phase at or after a given date time or date.

Returns the date time of a given lunar phase at or before a given date time or date.

Returns the date time of the new moon at or after a given date or date time.

Returns the date time of the new moon before a given date or date time.

Returns the date time of the new moon nearest to a given date or date time.

Returns the duration of daylight for a given location on a given date as a Duration.t/0.

Returns the datetime of either the March or September equinox, in UTC or a requested time zone.

Returns the evening on which the next new crescent is first seen, on or after a date.

Returns the evening on which the last new crescent was first seen, on or before a date.

Returns the number of hours of daylight for a given location on a given date.

Returns the fraction of the Moon's disc that is illuminated at a date or date time.

Guards that a value is a lunar phase angle.

Returns the local circumstances of a lunar eclipse at a location: the eclipse's contacts and greatest eclipse, and the Moon's altitude at each.

Returns the first lunar eclipse whose greatest eclipse is at or after a date or date time.

Returns the last lunar eclipse whose greatest eclipse is before a date or date time.

Returns the lunar phase in degrees at a date or date time.

Returns the moon phase as a UTF8 binary representing an emoji of the moon phase.

Returns the Moon's apparent right ascension, declination and distance at a date or date time.

Returns the moonrise for a given location and date.

Returns the moonset for a given location and date.

Predicts the visibility of the new crescent moon at a given location on a given date using one of three published criteria.

Predicts the visibility of the new crescent moon, with options.

Returns the local circumstances of a solar eclipse at a location: when it begins and ends there, its maximum, how much of the Sun is covered, and the Sun's altitude at each contact.

Returns the first solar eclipse whose greatest eclipse is at or after a date or date time.

Returns the last solar eclipse whose greatest eclipse is before a date or date time.

Returns the path of a solar eclipse: its central line and the northern and southern limits of the band in which it is total or annular, and the bounds of the region in which it is seen as a partial eclipse.

Returns solar noon at a location on a date, as a UTC date time.

Returns the datetime of either the June or December solstice, in UTC or a requested time zone.

Returns the Sun's apparent ecliptic longitude at a date or date time.

Returns the Sun's azimuth and elevation as seen from a location at a date time.

Returns the Sun's apparent right ascension, declination and distance at a date or date time.

Calculates the sunrise for a given location and date.

Calculates the sunset for a given location and date.

Types

altitude()

@type altitude() :: float()

angle()

@type angle() :: number()

astronomical_units()

@type astronomical_units() :: number()

date()

@type date() :: Calendar.date() | Calendar.datetime()

degrees()

@type degrees() :: float()

kilometers()

@type kilometers() :: number()

latitude()

@type latitude() :: float()

location()

@type location() ::
  {longitude(), latitude()}
  | {longitude(), latitude(), altitude()}
  | Geo.Point.t()
  | Geo.PointZ.t()

longitude()

@type longitude() :: float()

meters()

@type meters() :: number()

method()

@type method() :: :odeh | :yallop | :schaefer

options()

@type options() :: keyword()

phase()

@type phase() :: angle()

radians()

@type radians() :: float()

Functions

date_time_lunar_phase_at_or_after(date, phase)

(since 0.5.0)
@spec date_time_lunar_phase_at_or_after(date(), phase()) ::
  {:ok, DateTime.t()}
  | {:error, :invalid_date | :invalid_phase | :year_out_of_range}

Returns the date time of a given lunar phase at or after a given date time or date.

The lunar phase (see lunar_phase_at/1) is the Moon's apparent ecliptic longitude less the Sun's: 0° at new moon, 90° at first quarter, 180° at full moon and 270° at last quarter. The search estimates when the phase is next reached at or after the date from the mean synodic month of 29.530589 days, then bisects a four-day bracket around the estimate until it is narrower than a millisecond. The phase comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Returns

  • {:ok, date_time}, the UTC DateTime.t/0 at which the phase occurs.

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000, the range of the new moon searches.

  • {:error, :invalid_phase} if phase is not a number from 0 to 360.

Accuracy

The search ends within a millisecond of the crossing. For the new moon of 18 February 1977, Meeus gives 3h37m40s TD as the correct time (Astronomical Algorithms, example 49.a); Astro gives 3h37m39.9s TD. Outside the ephemeris the series place the phases within about 6 s RMS of JPL over 1900–2100 (new moons 20 s and full moons 30 s at most). Far from the present the result is limited by ΔT; the accuracy guide gives the details.

Examples

iex> Astro.date_time_lunar_phase_at_or_after(~D[2021-08-01], Astro.Lunar.full_moon_phase())
{:ok, ~U[2021-08-22 12:01:58.152306Z]}

Reference

date_time_lunar_phase_at_or_before(date, phase)

(since 0.5.0)
@spec date_time_lunar_phase_at_or_before(date(), phase()) ::
  {:ok, DateTime.t()}
  | {:error, :invalid_date | :invalid_phase | :year_out_of_range}

Returns the date time of a given lunar phase at or before a given date time or date.

The lunar phase (see lunar_phase_at/1) is the Moon's apparent ecliptic longitude less the Sun's: 0° at new moon, 90° at first quarter, 180° at full moon and 270° at last quarter. The search estimates when the phase is next reached at or before the date from the mean synodic month of 29.530589 days, then bisects a four-day bracket around the estimate until it is narrower than a millisecond. The phase comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Returns

  • {:ok, date_time}, the UTC DateTime.t/0 at which the phase occurs.

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000, the range of the new moon searches.

  • {:error, :invalid_phase} if phase is not a number from 0 to 360.

Accuracy

The search ends within a millisecond of the crossing. For the new moon of 18 February 1977, Meeus gives 3h37m40s TD as the correct time (Astronomical Algorithms, example 49.a); Astro gives 3h37m39.9s TD. Outside the ephemeris the series place the phases within about 6 s RMS of JPL over 1900–2100 (new moons 20 s and full moons 30 s at most). Far from the present the result is limited by ΔT; the accuracy guide gives the details.

Examples

iex> Astro.date_time_lunar_phase_at_or_before(~D[2021-08-01], Astro.Lunar.new_moon_phase())
{:ok, ~U[2021-07-10 01:16:37.291067Z]}

Reference

date_time_new_moon_at_or_after(date)

(since 0.5.0)
@spec date_time_new_moon_at_or_after(date()) ::
  {:ok, DateTime.t()} | {:error, :invalid_date | :year_out_of_range}

Returns the date time of the new moon at or after a given date or date time.

A new moon is the moment the lunar phase (see lunar_phase_at/1) passes 0°: the Moon's apparent ecliptic longitude equals the Sun's. The Moon is then between the Earth and the Sun with its dark side towards us. This is a conjunction in longitude; the Moon can pass up to about 5° north or south of the Sun, which is why most new moons are not eclipses.

The search numbers lunations as Meeus, Astronomical Algorithms, chapter 49, does, and that chapter's series estimates each lunation's new moon to within seconds near the present. The estimates of two consecutive lunations bracket the date, and each is refined to the moment the phase passes 0° by the secant method, which converges in a few steps, to under a millisecond. The phase comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Returns

  • {:ok, date_time}, the UTC DateTime.t/0 at which the new moon occurs.

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000, beyond which the Meeus chapter 49 estimates can no longer tell the lunations apart.

Accuracy

For the new moon of 18 February 1977, Meeus gives 3h37m40s TD as the correct time (Astronomical Algorithms, example 49.a); Astro gives 3h37m39.9s TD. Outside the ephemeris the series place new moons within 6.0 s RMS of JPL over 1900–2100, and 20 s at most. Far from the present the result is limited by ΔT, uncertain by minutes before about 1000 and after about 2500; the accuracy guide gives the details.

Examples

iex> Astro.date_time_new_moon_at_or_after(~D[2021-08-23])
{:ok, ~U[2021-09-07 00:51:46.307784Z]}

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 49 (phases of the Moon).

  • New moon and Secant method, Wikipedia.

date_time_new_moon_before(date)

(since 0.5.0)
@spec date_time_new_moon_before(date()) ::
  {:ok, DateTime.t()} | {:error, :invalid_date | :year_out_of_range}

Returns the date time of the new moon before a given date or date time.

A new moon is the moment the lunar phase (see lunar_phase_at/1) passes 0°: the Moon's apparent ecliptic longitude equals the Sun's. The Moon is then between the Earth and the Sun with its dark side towards us. This is a conjunction in longitude; the Moon can pass up to about 5° north or south of the Sun, which is why most new moons are not eclipses.

The search numbers lunations as Meeus, Astronomical Algorithms, chapter 49, does, and that chapter's series estimates each lunation's new moon to within seconds near the present. The estimates of two consecutive lunations bracket the date, and each is refined to the moment the phase passes 0° by the secant method, which converges in a few steps, to under a millisecond. The phase comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Returns

  • {:ok, date_time}, the UTC DateTime.t/0 at which the new moon occurs.

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000, beyond which the Meeus chapter 49 estimates can no longer tell the lunations apart.

Accuracy

For the new moon of 18 February 1977, Meeus gives 3h37m40s TD as the correct time (Astronomical Algorithms, example 49.a); Astro gives 3h37m39.9s TD. Outside the ephemeris the series place new moons within 6.0 s RMS of JPL over 1900–2100, and 20 s at most. Far from the present the result is limited by ΔT, uncertain by minutes before about 1000 and after about 2500; the accuracy guide gives the details.

Examples

iex> Astro.date_time_new_moon_before(~D[2021-08-23])
{:ok, ~U[2021-08-08 13:50:08.051917Z]}

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 49 (phases of the Moon).

  • New moon and Secant method, Wikipedia.

date_time_new_moon_nearest(date)

(since 2.0.0)
@spec date_time_new_moon_nearest(date()) ::
  {:ok, DateTime.t()} | {:error, :invalid_date | :year_out_of_range}

Returns the date time of the new moon nearest to a given date or date time.

A new moon is the moment the lunar phase (see lunar_phase_at/1) passes 0°: the Moon's apparent ecliptic longitude equals the Sun's. The Moon is then between the Earth and the Sun with its dark side towards us. This is a conjunction in longitude; the Moon can pass up to about 5° north or south of the Sun, which is why most new moons are not eclipses.

It is the nearer of the new moons found by the lunar phase searches either side of the date (see date_time_lunar_phase_at_or_before/2 and date_time_lunar_phase_at_or_after/2), and agrees with date_time_new_moon_before/1 and date_time_new_moon_at_or_after/1 to a millisecond.

Arguments

Returns

  • {:ok, date_time}, the UTC DateTime.t/0 at which the new moon occurs.

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000, beyond which the Meeus chapter 49 estimates can no longer tell the lunations apart.

Accuracy

For the new moon of 18 February 1977, Meeus gives 3h37m40s TD as the correct time (Astronomical Algorithms, example 49.a); Astro gives 3h37m39.9s TD. Outside the ephemeris the series place new moons within 6.0 s RMS of JPL over 1900–2100, and 20 s at most. Far from the present the result is limited by ΔT, uncertain by minutes before about 1000 and after about 2500; the accuracy guide gives the details.

Examples

iex> Astro.date_time_new_moon_nearest(~D[2021-08-23])
{:ok, ~U[2021-08-08 13:50:08.051866Z]}

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 49 (phases of the Moon).

  • New moon and Secant method, Wikipedia.

duration_of_daylight(location, date)

(since 2.3.0)
@spec duration_of_daylight(location(), Calendar.date()) ::
  {:ok, Duration.t()} | {:error, atom()}

Returns the duration of daylight for a given location on a given date as a Duration.t/0.

This is the same calculation as hours_of_daylight/2 but, because a Duration.t/0 is not bounded like a Time.t/0, it can represent a full 24 hours of daylight (returned as %Duration{hour: 24}) during the polar summer rather than capping at ~T[23:59:59].

Arguments

  • location is the observer's position as a {longitude, latitude} tuple in degrees, a Geo.Point.t/0, or a Geo.PointZ.t/0 that also carries an elevation in metres. Longitude comes first.

  • date is any Date.t/0 in the Gregorian calendar (for example, Calendar.ISO).

Returns

  • {:ok, duration} where duration is a Duration.t/0 between %Duration{} (no daylight) and %Duration{hour: 24} (24 hours of daylight).

  • {:error, reason} for an invalid location or date, or if the time zone for the location cannot be resolved, as for sunrise/3.

Accuracy

It is taken from sunrise/3 and sunset/3, each within 1.6 s of Skyfield for the same definition of the horizon, and is rounded down to the second. Refraction in the real atmosphere moves both events, by different amounts.

Examples

iex> Astro.duration_of_daylight({151.20666584, -33.8559799094}, ~D[2019-12-10])
{:ok, %Duration{hour: 14, minute: 20, second: 50}}

# 24 hours of daylight in the polar summer, uncapped
iex> Astro.duration_of_daylight({-62.3481, 82.5018}, ~D[2019-06-07])
{:ok, %Duration{hour: 24}}

# No daylight in the polar winter
iex> Astro.duration_of_daylight({-62.3481, 82.5018}, ~D[2019-12-07])
{:ok, %Duration{}}

Reference

equinox(year, event, options \\ [])

@spec equinox(Calendar.year(), :march | :september, options()) ::
  {:ok, DateTime.t()}
  | {:error,
     :invalid_year
     | :invalid_event
     | :year_out_of_range
     | :time_zone_not_found
     | :utc_only_time_zone_database
     | :invalid_time_zone_database}

Returns the datetime of either the March or September equinox, in UTC or a requested time zone.

An equinox is the moment the Sun's apparent ecliptic longitude reaches 0° (the March equinox) or 180° (the September equinox). The Sun is then very nearly on the celestial equator, heading north in March and south in September, and day and night are of almost equal length everywhere.

The moment is found from the Sun's apparent ecliptic longitude (see sun_apparent_longitude/1). Starting 20 days before the 1st of the month, the Sun's mean motion of 360° a tropical year estimates when it reaches the target longitude, and a ten-day bracket around that estimate is bisected until it is narrower than a millisecond. The longitude comes from the JPL DE440s ephemeris where the loaded kernel covers the year, and from the analytical series for any other year.

Arguments

  • year is the Gregorian year for which the equinox is to be calculated, within 50,000 years of 2000.

  • event is either :march or :september indicating which of the two annual equinox datetimes is required.

  • options is a keyword list of options.

Options

  • :time_zone is :utc (the default) or a time zone name such as "Asia/Tokyo". The equinox is one instant everywhere; the time zone decides the local date and time it is given in, and so the civil day it falls on.

  • :time_zone_database is the module implementing Calendar.TimeZoneDatabase in which a named time zone is looked up. The default is the configured database, Calendar.get_time_zone_database/0. UTC needs none.

Returns

  • {:ok, datetime}, the equinox in the requested time zone.

  • {:error, :time_zone_not_found} if the time zone is not known to the time zone database.

  • {:error, :utc_only_time_zone_database} if a time zone other than UTC is requested and no time zone database is configured.

  • {:error, :invalid_time_zone_database} if :time_zone_database is not a time zone database.

  • {:error, :invalid_year} if year is not an integer.

  • {:error, :year_out_of_range} if year is more than 50,000 years from 2000.

  • {:error, :invalid_event} if event is not :march or :september.

Accuracy

The search ends within a millisecond of the crossing. For the June solstice of 1962, Meeus gives 21h24m42s TD from the full VSOP87 theory (Astronomical Algorithms, chapter 27); Astro gives 21:24:07.8 UTC, which is 21h24m42.0s TD. Outside the ephemeris the series put the events 14 s early on average over 1900–2100, 18 s RMS and 50 s at most. Far from the present the time is limited by ΔT, which is uncertain by minutes before about 1000 and after about 2500; the accuracy guide gives the details.

Examples

iex> {:ok, dt} = Astro.equinox 2019, :march
iex> DateTime.truncate(dt, :second)
~U[2019-03-20 21:58:24Z]
iex> {:ok, dt} = Astro.equinox 2019, :september
iex> DateTime.truncate(dt, :second)
~U[2019-09-23 07:50:08Z]
iex> {:ok, dt} = Astro.equinox 900, :march
iex> DateTime.to_date(dt)
~D[0900-03-20]

# The March 2019 equinox is on the 20th in UTC but the 21st in Tokyo
iex> {:ok, dt} = Astro.equinox(2019, :march, time_zone: "Asia/Tokyo")
iex> DateTime.to_date(dt)
~D[2019-03-21]

iex> Astro.equinox(2019, :march,
...>   time_zone: "Asia/Tokyo",
...>   time_zone_database: Calendar.UTCOnlyTimeZoneDatabase
...> )
{:error, :utc_only_time_zone_database}

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 27 (equinoxes and solstices).

  • United States Naval Observatory, Earth's Seasons.

  • Equinox and Tropical year, Wikipedia.

first_visible_crescent_on_or_after(location, date, method \\ :odeh, options \\ [])

(since 2.10.0)
@spec first_visible_crescent_on_or_after(location(), date(), method(), keyword()) ::
  {:ok, Date.t()} | {:error, atom()}

Returns the evening on which the next new crescent is first seen, on or after a date.

See first_visible_crescent_on_or_before/4 for the first sighting. The month after the one date falls in begins the day after the first sighting on or after the day after date.

Arguments

Returns

Accuracy

As for first_visible_crescent_on_or_before/4.

Examples

iex> Astro.first_visible_crescent_on_or_after({39.8262, 21.4225}, ~D[2024-12-20])
{:ok, ~D[2025-01-01]}

Reference

  • Edward M. Reingold and Nachum Dershowitz, Calendrical Calculations, phasis-on-or-after.

first_visible_crescent_on_or_before(location, date, method \\ :odeh, options \\ [])

(since 2.10.0)
@spec first_visible_crescent_on_or_before(location(), date(), method(), keyword()) ::
  {:ok, Date.t()} | {:error, atom()}

Returns the evening on which the last new crescent was first seen, on or before a date.

A lunar month that begins at the first sighting of the new crescent, as an observational Islamic calendar's does, begins the day after the evening it is first seen. A lunation's crescent is first seen on the first evening, from the evening before the day of its new moon, on which new_visible_crescent/4 classes it visible (:A, :B or :C by default). This is the latest such evening on or before date: the month date falls in began the day after it.

Arguments

  • location is the observer's position as a {longitude, latitude} tuple in degrees, a Geo.Point.t/0, or a Geo.PointZ.t/0. Longitude comes first.

  • date is a Date.t/0, the evening to search back from.

  • method is :odeh (the default), :yallop or :schaefer; see new_visible_crescent/3.

  • options is a keyword list of options.

Options

  • :visible is the list of categories that count as a sighting. The default is [:A, :B, :C].

  • :utc_offset and :extinction are as for new_visible_crescent/4.

Returns

  • {:ok, date}, the evening of the first sighting.

  • {:error, :no_visible_crescent} if a lunation's crescent is not seen within 30 evenings of its new moon, as at high latitudes.

  • {:error, :invalid_location}, {:error, :invalid_date}, {:error, :invalid_method}, {:error, :invalid_options}, {:error, :invalid_visible} or {:error, :invalid_utc_offset} for an invalid argument or option.

  • {:error, :year_out_of_range} if date is more than 50,000 years from J2000.

Accuracy

As for new_visible_crescent/3: the evening is the one the criterion predicts, which can differ from an actual sighting by a day when the crescent is near the criterion's boundary.

Examples

iex> Astro.first_visible_crescent_on_or_before({39.8262, 21.4225}, ~D[2024-12-20])
{:ok, ~D[2024-12-02]}

Reference

hours_of_daylight(location, date)

@spec hours_of_daylight(location(), Calendar.date()) ::
  {:ok, Time.t()} | {:error, atom()}

Returns the number of hours of daylight for a given location on a given date.

Daylight is the time the Sun is above the horizon during the local calendar day, from the sunrise and sunset of sunrise/3 and sunset/3 in the location's time zone. It is counted from the events that fall within the day rather than as sunset less sunrise, so a day near a polar circle on which the Sun sets just after midnight and rises again a few hours later is handled. With neither event, the day is all daylight or none, according to whether the Sun is up.

duration_of_daylight/2 is the same calculation returning a Duration.t/0, which can represent a full 24 hours of daylight.

Arguments

  • location is the observer's position as a {longitude, latitude} tuple in degrees, a Geo.Point.t/0, or a Geo.PointZ.t/0 that also carries an elevation in metres. Longitude comes first.

  • date is any Date.t/0 in the Gregorian calendar (for example, Calendar.ISO).

Returns

  • {:ok, time} where time is a Time.t(). The maximum value is ~T[23:59:59], representing 24 hours of daylight (a Time.t() cannot hold 24:00:00).

  • {:error, reason} for an invalid location or date, or if the time zone for the location cannot be resolved, as for sunrise/3.

Accuracy

It is taken from sunrise/3 and sunset/3, each within 1.6 s of Skyfield for the same definition of the horizon, and is rounded down to the second. Refraction in the real atmosphere moves both events, by different amounts.

Examples

iex> Astro.hours_of_daylight({151.20666584, -33.8559799094}, ~D[2019-12-10])
{:ok, ~T[14:20:50]}

# No sunset in summer
iex> Astro.hours_of_daylight({-62.3481, 82.5018}, ~D[2019-06-07])
{:ok, ~T[23:59:59]}

# No sunrise in winter
iex> Astro.hours_of_daylight({-62.3481, 82.5018}, ~D[2019-12-07])
{:ok, ~T[00:00:00]}

Reference

illuminated_fraction_of_moon_at(date_time)

(since 0.6.0)
@spec illuminated_fraction_of_moon_at(date()) :: number()

Returns the fraction of the Moon's disc that is illuminated at a date or date time.

The illuminated fraction k is the part of the Moon's apparent disc lit by the Sun, as seen from the centre of the Earth: 0 at new moon and 1 at full moon. It depends on the phase angle i, the angle at the Moon between the directions to the Sun and to the Earth:

k = (1 + cos i) / 2

The phase angle is found from the Sun's and the Moon's apparent positions (see sun_position_at/1 and moon_position_at/1), as in Meeus, Astronomical Algorithms, chapter 48: their elongation ψ from

cos ψ = sin δ₀ sin δ + cos δ₀ cos δ cos(α₀ − α)

and then

tan i = R sin ψ / (Δ − R cos ψ)

where α₀ and δ₀ are the Sun's right ascension and declination, α and δ the Moon's, R the Sun's distance and Δ the Moon's.

Arguments

Returns

  • a float value between 0.0 and 1.0 representing the fractional illumination of the moon.

Accuracy

For Meeus's example 48.a (1992 April 12, 0h TD) the result is 0.67856, against Meeus's 0.6786. Outside the ephemeris the series agree with JPL over 1900–2100 to 0.0000045 RMS and 0.000018 at most. The accuracy guide gives the details.

Examples

iex> fraction = Astro.illuminated_fraction_of_moon_at(~D[2017-03-16])
iex> Float.round(fraction, 4)
0.8884

iex> fraction = Astro.illuminated_fraction_of_moon_at(~D[1992-04-12])
iex> Float.round(fraction, 4)
0.6786

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 48 (illuminated fraction of the Moon's disk).

  • Phase angle and Lunar phase, Wikipedia.

is_lunar_phase(phase)

(macro)

Guards that a value is a lunar phase angle.

Arguments

  • phase is the value to test.

Returns

  • true if phase is a number from 0.0 to 360.0 inclusive, otherwise false.

Examples

iex> require Astro
iex> Astro.is_lunar_phase(90.0)
true
iex> Astro.is_lunar_phase(400.0)
false

lunar_eclipse_at(location, eclipse, options \\ [])

(since 2.10.0)
@spec lunar_eclipse_at(location(), Astro.Lunar.Eclipse.t() | date(), options()) ::
  {:ok, Astro.Lunar.Eclipse.Local.t()}
  | {:error,
     :not_visible
     | :invalid_location
     | :invalid_date
     | :year_out_of_range
     | :invalid_shadow
     | :invalid_options
     | :time_zone_not_found
     | :time_zone_not_resolved
     | :tz_world_data_not_installed
     | :tz_world_backend_not_running
     | :invalid_time_zone_resolver
     | :invalid_time_zone_database
     | :utc_only_time_zone_database}

Returns the local circumstances of a lunar eclipse at a location: the eclipse's contacts and greatest eclipse, and the Moon's altitude at each.

A lunar eclipse is the Moon passing through the Earth's shadow, so its contacts happen at the same instants for everyone who can see the Moon; what depends on the place is whether the Moon is above the horizon while it happens. The Moon's altitude is topocentric, allowing for its parallax of up to about 1° from the observer's position on the Earth, and geometric, without refraction. Astro.Lunar.Eclipse.Local describes the calculation.

Arguments

Options

  • :shadow is the convention for enlarging the Earth's shadow for its atmosphere, :danjon (the default) or :chauvenet, as for lunar_eclipse_at_or_after/2. An eclipse given as eclipse is found again with this convention.

  • :time_zone is the time zone of the returned date times: :default, the time zone of the location, which is the default; :utc; or a time zone name.

  • :time_zone_database is the module implementing the Calendar.TimeZoneDatabase behaviour. The default is the configured Elixir time zone database.

  • :time_zone_resolver is a 1-arity function that receives a %Geo.Point{coordinates: {lng, lat}} and returns {:ok, time_zone_name} or {:error, reason}. The default is TzWorld.timezone_at/1 when :tz_world is a dependency.

Returns

  • {:ok, local}, an Astro.Lunar.Eclipse.Local.t/0 with the fields of Astro.Lunar.Eclipse.t/0, its instants as date times in the requested time zone, and the Moon's altitude in degrees at greatest eclipse and at each contact the eclipse has. A contact the Moon is below the horizon for has a negative altitude.

  • {:error, :not_visible} if the Moon is below the horizon from the first penumbral contact to the last. The Moon has risen when its upper limb is above the horizon, allowing for standard refraction, as for moonrise/3.

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if eclipse is neither an eclipse nor a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if eclipse is a date more than 50,000 years from J2000.

  • {:error, :invalid_shadow} if :shadow is not :danjon or :chauvenet.

  • {:error, :invalid_options} if options is not a keyword list.

  • The time zone errors of sunrise/3.

Accuracy

The instants are those of lunar_eclipse_at_or_after/2. The Moon's altitude at the instants of moonrise/3 and moonset/3 is the horizon they use to within 0.00001°, and it is as accurate as the Moon's position otherwise. Refraction near the horizon departs from the standard 34′ with the weather, so whether a Moon that rises or sets during an eclipse is seen at a contact within a few minutes of it is uncertain by that much.

Examples

iex> {:ok, local} = Astro.lunar_eclipse_at({-96.80, 32.78}, ~D[2025-03-01], time_zone: "America/Chicago")
iex> {local.type, Float.round(local.moon_altitudes.greatest, 1)}
{:total, 59.0}
iex> DateTime.truncate(local.greatest, :second)
#DateTime<2025-03-14 01:58:46-05:00 CDT America/Chicago>

iex> Astro.lunar_eclipse_at({77.21, 28.61}, ~D[2025-03-01], time_zone: :utc)
{:error, :not_visible}

Reference

  • Fred Espenak and Jean Meeus, Five Millennium Canon of Lunar Eclipses: −1999 to +3000, NASA/TP-2009-214172 (2009).

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 40 (correction for parallax).

lunar_eclipse_at_or_after(date, options \\ [])

(since 2.10.0)
@spec lunar_eclipse_at_or_after(date(), Keyword.t()) ::
  {:ok, Astro.Lunar.Eclipse.t()}
  | {:error,
     :invalid_date | :year_out_of_range | :invalid_shadow | :invalid_options}

Returns the first lunar eclipse whose greatest eclipse is at or after a date or date time.

An eclipse is found at a full moon near a node of the Moon's orbit. At each full moon the Moon's angular distance from the axis of the Earth's shadow, the point opposite the Sun, is compared with the radius of the penumbra; where it can reach it, greatest eclipse is the moment of least distance, found by golden section search, and each contact the moment the Moon's limb meets a shadow's edge, found by bisection. Astro.Lunar.Eclipse describes the geometry. The positions come from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Options

  • :shadow is the convention for enlarging the Earth's shadow for its atmosphere: :danjon (the default, as NASA's eclipse catalogues use it) or :chauvenet (the older almanac convention, which makes the umbral magnitude about 0.006 larger).

Returns

  • {:ok, eclipse}, an Astro.Lunar.Eclipse.t/0 with the type (:penumbral, :partial or :total), gamma, the penumbral and umbral magnitudes, greatest eclipse and the contacts as UTC DateTime.t/0s, nil for the contacts the eclipse does not have, and its Saros series as NASA numbers it (Astro.Saros).

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000.

  • {:error, :invalid_shadow} if :shadow is not :danjon or :chauvenet.

  • {:error, :invalid_options} if options is not a keyword list.

Accuracy

Against NASA's catalogue for 1901–2100, compared in Terrestrial Time so that ΔT does not enter, greatest eclipse is within 3 s (0.5 s on average), gamma within 0.00015, both magnitudes within 0.0004, the partial and total phases' durations within 0.1 minute, and every eclipse's type is the same. Outside the ephemeris the series place greatest eclipse within 15 s of NASA's in the nineteenth century, 40 s in the thirtieth and two and a half minutes around 950 BCE, with magnitudes within 0.002 and the same types. In Universal Time every instant also carries ΔT's uncertainty; the accuracy guide gives it.

Examples

iex> {:ok, eclipse} = Astro.lunar_eclipse_at_or_after(~D[2025-03-01])
iex> {eclipse.type, DateTime.truncate(eclipse.greatest, :second)}
{:total, ~U[2025-03-14 06:58:46Z]}

iex> {:ok, eclipse} = Astro.lunar_eclipse_at_or_after(~D[2025-03-15])
iex> {eclipse.type, DateTime.to_date(eclipse.greatest)}
{:total, ~D[2025-09-07]}

Reference

lunar_eclipse_before(date, options \\ [])

(since 2.10.0)
@spec lunar_eclipse_before(date(), Keyword.t()) ::
  {:ok, Astro.Lunar.Eclipse.t()}
  | {:error,
     :invalid_date | :year_out_of_range | :invalid_shadow | :invalid_options}

Returns the last lunar eclipse whose greatest eclipse is before a date or date time.

An eclipse is found at a full moon near a node of the Moon's orbit. At each full moon the Moon's angular distance from the axis of the Earth's shadow, the point opposite the Sun, is compared with the radius of the penumbra; where it can reach it, greatest eclipse is the moment of least distance, found by golden section search, and each contact the moment the Moon's limb meets a shadow's edge, found by bisection. Astro.Lunar.Eclipse describes the geometry. The positions come from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Options

  • :shadow is the convention for enlarging the Earth's shadow for its atmosphere: :danjon (the default, as NASA's eclipse catalogues use it) or :chauvenet (the older almanac convention, which makes the umbral magnitude about 0.006 larger).

Returns

  • {:ok, eclipse}, an Astro.Lunar.Eclipse.t/0 with the type (:penumbral, :partial or :total), gamma, the penumbral and umbral magnitudes, greatest eclipse and the contacts as UTC DateTime.t/0s, nil for the contacts the eclipse does not have, and its Saros series as NASA numbers it (Astro.Saros).

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000.

  • {:error, :invalid_shadow} if :shadow is not :danjon or :chauvenet.

  • {:error, :invalid_options} if options is not a keyword list.

Accuracy

Against NASA's catalogue for 1901–2100, compared in Terrestrial Time so that ΔT does not enter, greatest eclipse is within 3 s (0.5 s on average), gamma within 0.00015, both magnitudes within 0.0004, the partial and total phases' durations within 0.1 minute, and every eclipse's type is the same. Outside the ephemeris the series place greatest eclipse within 15 s of NASA's in the nineteenth century, 40 s in the thirtieth and two and a half minutes around 950 BCE, with magnitudes within 0.002 and the same types. In Universal Time every instant also carries ΔT's uncertainty; the accuracy guide gives it.

Examples

iex> {:ok, eclipse} = Astro.lunar_eclipse_before(~D[2025-03-14])
iex> {eclipse.type, DateTime.to_date(eclipse.greatest)}
{:partial, ~D[2024-09-18]}

Reference

lunar_phase_at(date_time)

(since 0.5.0)
@spec lunar_phase_at(date()) :: phase()

Returns the lunar phase in degrees at a date or date time.

The lunar phase is the Moon's apparent ecliptic longitude less the Sun's, from 0° to 360°: 0° at new moon, 90° at first quarter, 180° at full moon and 270° at last quarter. It is how far the Moon has moved ahead of the Sun around the sky since the last new moon, and grows by about 12.2° a day. It differs from the phase angle that sets the illuminated fraction (see illuminated_fraction_of_moon_at/1), because the Moon's orbit is tilted about 5° to the ecliptic.

Both longitudes are apparent ecliptic longitudes of the date (see sun_apparent_longitude/1 and moon_position_at/1): from the JPL DE440s ephemeris where the loaded kernel covers the moment, and from the analytical series elsewhere.

Arguments

Returns

  • the lunar phase in degrees, from 0 to 360.

Accuracy

Outside the ephemeris the series agree with JPL over 1900–2100 to 2.8″ RMS and 11.5″ at most. The phase grows about 30″ a minute, so 2.8″ is about 6 s in the time a phase is reached. The accuracy guide gives the details.

Examples

iex> Astro.lunar_phase_at ~U[2021-08-22 12:02:02.816534Z]
180.00068383330358

iex> Astro.lunar_phase_at(~U[2021-07-10 01:16:37.026816Z])
359.9999651540469

Outside the loaded ephemeris the phase comes from the analytical series:

iex> Astro.lunar_phase_at(~D[1000-01-01]) |> Float.round(2)
202.55

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 47 (position of the Moon) and chapter 49 (phases of the Moon).

  • Lunar phase, Wikipedia.

lunar_phase_emoji(phase)

@spec lunar_phase_emoji(phase()) :: String.t()

Returns the moon phase as a UTF8 binary representing an emoji of the moon phase.

Unicode has eight moon phase symbols, U+1F311 to U+1F318, from 🌑 new moon through 🌓 first quarter, 🌕 full moon and 🌗 last quarter. Each covers 45° of lunar phase centred on its own, so 🌑 is returned from 337.5° to 22.5° and 🌒 from 22.5° to 67.5°. The symbols show the Moon as it is seen from the northern hemisphere, lit on the right while it waxes.

Arguments

Returns

  • A single grapheme string, one of the Unicode moon phase symbols.

Examples

iex> Astro.lunar_phase_emoji 0
"🌑"
iex> Astro.lunar_phase_emoji 45
"🌒"
iex> Astro.lunar_phase_emoji 90
"🌓"
iex> Astro.lunar_phase_emoji 135
"🌔"
iex> Astro.lunar_phase_emoji 180
"🌕"
iex> Astro.lunar_phase_emoji 245
"🌖"
iex> Astro.lunar_phase_emoji 270
"🌗"
iex> Astro.lunar_phase_emoji 320
"🌘"
iex> Astro.lunar_phase_emoji 360
"🌑"

iex> ~U[2021-08-22 12:02:02.816534Z]
...> |> Astro.lunar_phase_at()
...> |> Astro.lunar_phase_emoji()
"🌕"

Reference

moon_position_at(date_time)

(since 0.6.0)
@spec moon_position_at(date()) :: Geo.PointZ.t()

Returns the Moon's apparent right ascension, declination and distance at a date or date time.

This is the Moon's apparent geocentric position: where it is seen from the centre of the Earth, referred to the true equator and equinox of the date. The Moon is close enough that an observer on the Earth's surface sees it displaced by up to about 1° from this position (its parallax); moonrise/3, moonset/3 and the Odeh crescent criterion correct for that.

Where the loaded JPL DE440s ephemeris covers the moment, the Earth-to-Moon vector is read from it 1.282 s earlier, the light time, and rotated from the ICRF to the true equator of date by IAU 1976 precession and IAU 1980 nutation. No annual aberration is applied, because the Moon travels around the Sun with the Earth. Elsewhere the Moon's ecliptic longitude, latitude and distance come from the periodic terms of Meeus, Astronomical Algorithms, chapter 47, abridged from the ELP-2000/82 lunar theory, with nutation in longitude added, and are converted to right ascension and declination with the true obliquity of the ecliptic (chapter 13).

Arguments

Returns

  • a Geo.PointZ.t/0 struct with coordinates {right_ascension, declination, distance} with properties %{reference: :celestial, object: :moon}. Right ascension is in degrees from −180° to 180°, declination in degrees and distance in metres.

Accuracy

Inside the ephemeris, for Meeus's example 47.a (1992 April 12, 0h TD) the result is within 2.1″ in right ascension, 0.3″ in declination and 4 km of Meeus's own series, about the accuracy of that series. Outside it the series agree with JPL over 1900–2100 to 2.7″ RMS in right ascension and 1.3″ in declination (10.1″ and 6.5″ at most), and to 2.9 km in distance. The Moon moves about 0.55″ a second, so far from the present the uncertainty of ΔT dominates: the 265 s uncertainty at the year 0 is about 2.4′ of position. The accuracy guide gives the details.

Examples

iex> Astro.moon_position_at(~D[1992-04-12]) |> Astro.Location.round(6)
%Geo.PointZ{
  coordinates: {134.697251, 13.765115, 368404879.697206},
  properties: %{object: :moon, reference: :celestial},
  srid: nil
}

Reference

moonrise(location, date, options \\ default_options())

(since 2.0.0)
@spec moonrise(location(), date(), options()) ::
  {:ok, DateTime.t()}
  | {:error,
     :no_time
     | :invalid_location
     | :invalid_date
     | :invalid_options
     | :invalid_utc_offset
     | :invalid_limb
     | :invalid_interpolation
     | :time_zone_not_found
     | :time_zone_not_resolved
     | :tz_world_data_not_installed
     | :tz_world_backend_not_running
     | :invalid_time_zone_resolver
     | :invalid_time_zone_database
     | :utc_only_time_zone_database}

Returns the moonrise for a given location and date.

Moonrise is the moment the upper limb of the Moon appears above the horizon.

Refraction lifts the Moon by a standard 34′ at the horizon, and its semi-diameter is 14.7′ to 16.8′ depending on its distance, so this is the moment the Moon's centre, computed without refraction, is 34′ plus its semi-diameter below the horizon.

The Moon is close enough that where it appears depends on where it is seen from: from the Earth's surface it is displaced by up to about 1°, its horizontal parallax, from where it is seen from the Earth's centre. Its altitude is therefore computed from its topocentric position, corrected for parallax in both right ascension and declination (Meeus, Astronomical Algorithms, chapter 40), and the local apparent sidereal time. The altitude is sampled every 24 minutes across the day to bracket the crossing, which is then narrowed by bisection to 0.01 s. The position comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

  • location is the observer's position as a {longitude, latitude} tuple in degrees, a Geo.Point.t/0, or a Geo.PointZ.t/0 that also carries an elevation in metres. Longitude comes first. An elevation enters the parallax but does not lower the horizon.

  • date is a Date.t/0, NaiveDateTime.t/0 or DateTime.t/0 for the day of the moonrise.

  • options is a keyword list of options.

Options

  • :limb is the part of the Moon's disk that defines the event.

    • :upper, the default, puts the upper limb on the apparent horizon, the USNO standard. The event threshold is −(34′ refraction + semi-diameter).

    • :center puts the centre of the disk on the apparent horizon. The event threshold is −34′ of refraction only.

  • :interpolation is how the Moon's position is evaluated while the event is bisected.

    • :direct, the default, evaluates the JPL ephemeris at every step.

    • :lagrange interpolates the geocentric position quadratically from three points, as Meeus Ch. 15 does.

  • :time_zone is the time zone of the returned date time: :default, the time zone of the location, which is the default; :utc; or a time zone name.

  • :utc_offset is a whole number of seconds from UTC, from −50,400 to 50,400. When it is given, the event is the one whose date at that offset is date, whatever time zone it is returned in: the location's own day, for a calendar that keeps local mean time. Otherwise the date is taken in the requested time zone.

  • :time_zone_database is the module implementing the Calendar.TimeZoneDatabase behaviour. The default is the configured Elixir time zone database.

  • :time_zone_resolver is a 1-arity function that receives a %Geo.Point{coordinates: {lng, lat}} and returns {:ok, time_zone_name} or {:error, reason}. The default is TzWorld.timezone_at/1 when :tz_world is a dependency.

Returns

  • {:ok, date_time} where date_time is the moonrise in the requested time zone.

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if date is not a valid date in its calendar.

  • {:error, :invalid_options} if options is not a list.

  • {:error, :invalid_utc_offset} if :utc_offset is not a whole number of seconds from −50,400 to 50,400.

  • {:error, :no_time} if the Moon does not rise on that date at that location, as it can stay below the horizon for a whole day.

  • {:error, :invalid_limb} if :limb is not :upper or :center.

  • {:error, :invalid_interpolation} if :interpolation is not :direct or :lagrange.

  • {:error, :time_zone_not_found} if the requested time zone is unknown.

  • {:error, :time_zone_not_resolved} if no time zone can be resolved for the location, which happens when :tz_world is not a dependency and no :time_zone_resolver is given, or when the resolver returns neither {:ok, time_zone} nor {:error, reason}.

  • {:error, :tz_world_data_not_installed} if :tz_world is a dependency but its data has not been installed. Run mix tz_world.update to install it.

  • {:error, :tz_world_backend_not_running} if :tz_world is a dependency but none of its backends is running; see Astro.Supervisor.

  • {:error, :invalid_time_zone_resolver} if :time_zone_resolver is not a 1-arity function.

  • {:error, :invalid_time_zone_database} if :time_zone_database is not a time zone database.

  • {:error, :utc_only_time_zone_database} if the moonrise is requested in a time zone other than UTC and no time zone database is configured.

Accuracy

Against Skyfield, using the same ephemeris, results agree to 0.08 s on average and 0.1 s at most. Against the USNO, which publishes to the minute, they agree to 15.5 s on average and 29 s at most in 70 cases. Outside the ephemeris the series move the result by 0.22 s RMS and 0.93 s at most. As for the Sun, the atmosphere matters more than any of these: refraction at the horizon departs from the standard 34′ with temperature and pressure, and a high observer sees a lower horizon, and neither is modelled. The accuracy guide gives the details.

Examples

iex> {:ok, date_time} = Astro.moonrise({151.20666584, -33.8559799094}, ~D[2019-12-04])
iex> date_time
#DateTime<2019-12-04 12:20:54.388717+11:00 AEDT Australia/Sydney>

iex> Astro.moonrise({-62.3481, 82.5018}, ~D[2024-01-01])
{:error, :no_time}

Reference

moonset(location, date, options \\ default_options())

(since 2.0.0)
@spec moonset(location(), date(), options()) ::
  {:ok, DateTime.t()}
  | {:error,
     :no_time
     | :invalid_location
     | :invalid_date
     | :invalid_options
     | :invalid_utc_offset
     | :invalid_limb
     | :invalid_interpolation
     | :time_zone_not_found
     | :time_zone_not_resolved
     | :tz_world_data_not_installed
     | :tz_world_backend_not_running
     | :invalid_time_zone_resolver
     | :invalid_time_zone_database
     | :utc_only_time_zone_database}

Returns the moonset for a given location and date.

Moonset is the moment the upper limb of the Moon disappears below the horizon.

Refraction lifts the Moon by a standard 34′ at the horizon, and its semi-diameter is 14.7′ to 16.8′ depending on its distance, so this is the moment the Moon's centre, computed without refraction, is 34′ plus its semi-diameter below the horizon.

The Moon is close enough that where it appears depends on where it is seen from: from the Earth's surface it is displaced by up to about 1°, its horizontal parallax, from where it is seen from the Earth's centre. Its altitude is therefore computed from its topocentric position, corrected for parallax in both right ascension and declination (Meeus, Astronomical Algorithms, chapter 40), and the local apparent sidereal time. The altitude is sampled every 24 minutes across the day to bracket the crossing, which is then narrowed by bisection to 0.01 s. The position comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

  • location is the observer's position as a {longitude, latitude} tuple in degrees, a Geo.Point.t/0, or a Geo.PointZ.t/0 that also carries an elevation in metres. Longitude comes first. An elevation enters the parallax but does not lower the horizon.

  • date is a Date.t/0, NaiveDateTime.t/0 or DateTime.t/0 for the day of the moonset.

  • options is a keyword list of options.

Options

  • :limb is the part of the Moon's disk that defines the event.

    • :upper, the default, puts the upper limb on the apparent horizon, the USNO standard. The event threshold is −(34′ refraction + semi-diameter).

    • :center puts the centre of the disk on the apparent horizon. The event threshold is −34′ of refraction only.

  • :interpolation is how the Moon's position is evaluated while the event is bisected.

    • :direct, the default, evaluates the JPL ephemeris at every step.

    • :lagrange interpolates the geocentric position quadratically from three points, as Meeus Ch. 15 does.

  • :time_zone is the time zone of the returned date time: :default, the time zone of the location, which is the default; :utc; or a time zone name.

  • :utc_offset is a whole number of seconds from UTC, from −50,400 to 50,400. When it is given, the event is the one whose date at that offset is date, whatever time zone it is returned in: the location's own day, for a calendar that keeps local mean time. Otherwise the date is taken in the requested time zone.

  • :time_zone_database is the module implementing the Calendar.TimeZoneDatabase behaviour. The default is the configured Elixir time zone database.

  • :time_zone_resolver is a 1-arity function that receives a %Geo.Point{coordinates: {lng, lat}} and returns {:ok, time_zone_name} or {:error, reason}. The default is TzWorld.timezone_at/1 when :tz_world is a dependency.

Returns

  • {:ok, date_time} where date_time is the moonset in the requested time zone.

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if date is not a valid date in its calendar.

  • {:error, :invalid_options} if options is not a list.

  • {:error, :invalid_utc_offset} if :utc_offset is not a whole number of seconds from −50,400 to 50,400.

  • {:error, :no_time} if the Moon does not set on that date at that location, as it can stay above the horizon for a whole day.

  • {:error, :invalid_limb} if :limb is not :upper or :center.

  • {:error, :invalid_interpolation} if :interpolation is not :direct or :lagrange.

  • {:error, :time_zone_not_found} if the requested time zone is unknown.

  • {:error, :time_zone_not_resolved} if no time zone can be resolved for the location, which happens when :tz_world is not a dependency and no :time_zone_resolver is given, or when the resolver returns neither {:ok, time_zone} nor {:error, reason}.

  • {:error, :tz_world_data_not_installed} if :tz_world is a dependency but its data has not been installed. Run mix tz_world.update to install it.

  • {:error, :tz_world_backend_not_running} if :tz_world is a dependency but none of its backends is running; see Astro.Supervisor.

  • {:error, :invalid_time_zone_resolver} if :time_zone_resolver is not a 1-arity function.

  • {:error, :invalid_time_zone_database} if :time_zone_database is not a time zone database.

  • {:error, :utc_only_time_zone_database} if the moonset is requested in a time zone other than UTC and no time zone database is configured.

Accuracy

Against Skyfield, using the same ephemeris, results agree to 0.08 s on average and 0.1 s at most. Against the USNO, which publishes to the minute, they agree to 15.5 s on average and 29 s at most in 70 cases. Outside the ephemeris the series move the result by 0.22 s RMS and 0.93 s at most. As for the Sun, the atmosphere matters more than any of these: refraction at the horizon departs from the standard 34′ with temperature and pressure, and a high observer sees a lower horizon, and neither is modelled. The accuracy guide gives the details.

Examples

iex> {:ok, date_time} = Astro.moonset({151.20666584, -33.8559799094}, ~D[2019-12-04])
iex> date_time
#DateTime<2019-12-04 01:13:25.822599+11:00 AEDT Australia/Sydney>

iex> Astro.moonset({-62.3481, 82.5018}, ~D[2024-01-01])
{:error, :no_time}

Reference

new_visible_crescent(location, date, method \\ :odeh)

(since 2.1.0)
@spec new_visible_crescent(location(), date(), method()) ::
  {:ok, Astro.Lunar.CrescentVisibility.visibility()}
  | {:error, :no_sunset | :invalid_location | :invalid_date | :invalid_method}

Predicts the visibility of the new crescent moon at a given location on a given date using one of three published criteria.

A new crescent is first seen in the west after sunset, a day or two after new moon. Whether it can be seen depends on how far the Moon is from the Sun, how wide its lit crescent is, and how dark the sky has become. Each criterion predicts this from the Sun's and the Moon's positions on the evening of date, and classifies the crescent into one of five categories.

Yallop and Odeh both work at the best time to look, sunset plus four-ninths of the interval from sunset to moonset. There they find the arc of vision ARCV, the Moon's altitude less the Sun's, and the width of the crescent W in arcminutes, and compare ARCV with a cubic in W fitted to recorded sightings:

q = (ARCV − (11.8371 − 6.3226 W + 0.7319 W² − 0.1018 W³)) / 10
V = ARCV − (7.1651 − 6.3226 W + 0.7319 W² − 0.1018 W³)

Yallop's q uses the geocentric arc of vision and Odeh's V the topocentric one; Odeh also treats a Moon less than 6.4° from the Sun as invisible, the Danjon limit. The class follows from thresholds on q or V. Schaefer instead models the eye: from sunset to moonset it compares the brightness of the crescent against the twilight sky with the smallest contrast the eye can detect.

Arguments

  • location is the observer's position as a {longitude, latitude} tuple in degrees, a Geo.Point.t/0, or a Geo.PointZ.t/0 that also carries an elevation in metres. Longitude comes first.

  • date is a Date.t/0 or DateTime.t/0 indicating the evening on which crescent visibility is to be evaluated.

  • method selects the prediction criterion. Default :odeh.

    • :odeh — Odeh (2004). Empirical criterion based on 737 observations. Uses topocentric ARCV with a Danjon limit of 6.4°. The most widely used modern criterion.

    • :yallop — Yallop (1997). Empirical criterion based on 295 observations. Uses geocentric ARCV. The original single-parameter approach that Odeh later refined.

    • :schaefer — Schaefer (1988/2000). Physics-based model computing the contrast between crescent brightness and twilight sky brightness against the human contrast detection threshold. The best observation time is found by scanning from sunset to moonset.

Options

When method is :schaefer, the following options are accepted as an optional fourth argument (a keyword list):

  • :extinction — V-band zenith extinction coefficient. Default 0.172 (clean sea-level site). Typical values: 0.12 (high mountain), 0.17 (sea level), 0.25 (hazy conditions).

Returns

  • {:ok, visibility} where visibility is one of:

    • :A — Visible to the naked eye.

    • :B — Visible with optical aid.

    • :C — May need optical aid.

    • :D — Not visible with optical aid.

    • :E — Not visible.

  • {:error, :no_sunset} if no sunset occurs on the given date at the given location (e.g. polar day).

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if date is not a valid date in its calendar.

  • {:error, :invalid_method} if method is not :yallop, :odeh or :schaefer.

Method comparison

AspectYallop (1997)Odeh (2004)Schaefer (1988/2000)
BasisEmpirical polynomialEmpirical polynomialPhysical model
Observations295737N/A (theory)
ARCV typeGeocentricTopocentricN/A
Best timeSunset + 4/9 lagSunset + 4/9 lagScanned (max Rs)
AtmosphereNot modelledNot modelledExtinction coefficient

Accuracy

The categories depend on the Sun's and the Moon's positions far less than on the criteria: outside the ephemeris the series give the same category as JPL in 3,749 of 3,750 evenings. The criteria are fits to sightings made under real skies, and a crescent near the boundary of two categories is as likely to be decided by the evening's weather as by the calculation; Astro's predictions have not themselves been compared with a record of sightings. Far from the present, ΔT moves the Moon against the Sun by about 0.5″ for each second it is wrong, so the 265 s uncertainty at the year 0 changes ARCV by about 2′. The accuracy guide gives the details.

Examples

iex> location = {-0.1275, 51.5072}
iex> Astro.new_visible_crescent(location, ~D[2025-03-31])
{:ok, :A}

iex> location = {-0.1275, 51.5072}
iex> Astro.new_visible_crescent(location, ~D[2025-03-31], :yallop)
{:ok, :A}

Reference

new_visible_crescent(location, date, method, options)

(since 2.1.0)
@spec new_visible_crescent(location(), date(), method(), keyword()) ::
  {:ok, Astro.Lunar.CrescentVisibility.visibility()}
  | {:error,
     :no_sunset
     | :invalid_location
     | :invalid_date
     | :invalid_method
     | :invalid_options
     | :invalid_utc_offset}

Predicts the visibility of the new crescent moon, with options.

This is new_visible_crescent/3 with options: the evening of a location's own day, and for Schaefer's method the clarity of the atmosphere. It takes the same location, date and method and returns the same classes; see new_visible_crescent/3 for how the methods compare.

Arguments

  • location is the observer's position as a {longitude, latitude} tuple in degrees, a Geo.Point.t/0, or a Geo.PointZ.t/0 that also carries an elevation in metres. Longitude comes first.

  • date is a Date.t/0 or DateTime.t/0 indicating the evening on which crescent visibility is to be evaluated.

  • method is :odeh, :yallop or :schaefer.

  • options is a keyword list of options.

Options

  • :utc_offset is a whole number of seconds from UTC, from −50,400 to 50,400. When it is given, the evening is that of date at the offset, as for sunset/3; otherwise it is the evening of the UTC date.

  • :extinction is the V-band zenith extinction coefficient, for :schaefer. The default is 0.172, a clean sea-level site. Typical values are 0.12 on a high mountain, 0.17 at sea level and 0.25 in hazy conditions.

Returns

  • {:ok, visibility} where visibility is one of the classes :A to :E described in new_visible_crescent/3.

  • {:error, :no_sunset} if no sunset occurs on the given date at the given location.

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if date is not a valid date in its calendar.

  • {:error, :invalid_method} if method is not :yallop, :odeh or :schaefer.

  • {:error, :invalid_options} if options is not a list.

  • {:error, :invalid_utc_offset} if :utc_offset is not a whole number of seconds from −50,400 to 50,400.

Examples

iex> location = {39.8579, 21.3891}
iex> Astro.new_visible_crescent(location, ~D[2025-03-31], :schaefer, extinction: 0.25)
{:ok, :A}

Reference

solar_eclipse_at(location, eclipse, options \\ [])

(since 2.10.0)
@spec solar_eclipse_at(location(), Astro.Solar.Eclipse.t() | date(), options()) ::
  {:ok, Astro.Solar.Eclipse.Local.t()}
  | {:error,
     :not_visible
     | :invalid_location
     | :invalid_date
     | :year_out_of_range
     | :invalid_options
     | :time_zone_not_found
     | :time_zone_not_resolved
     | :tz_world_data_not_installed
     | :tz_world_backend_not_running
     | :invalid_time_zone_resolver
     | :invalid_time_zone_database
     | :utc_only_time_zone_database}

Returns the local circumstances of a solar eclipse at a location: when it begins and ends there, its maximum, how much of the Sun is covered, and the Sun's altitude at each contact.

The observer is placed on the fundamental plane of the eclipse's Besselian elements, from the observer's geodetic latitude, longitude and height. Maximum eclipse is the moment the observer is nearest the shadow axis, found by golden section search; the contacts are the moments the observer enters and leaves the penumbra and, for a total or annular eclipse there, the umbra or antumbra, found by bisection. Astro.Solar.Eclipse.Local describes the geometry.

Arguments

Options

  • :time_zone is the time zone of the returned date times: :default, the time zone of the location, which is the default; :utc; or a time zone name.

  • :time_zone_database is the module implementing the Calendar.TimeZoneDatabase behaviour. The default is the configured Elixir time zone database.

  • :time_zone_resolver is a 1-arity function that receives a %Geo.Point{coordinates: {lng, lat}} and returns {:ok, time_zone_name} or {:error, reason}. The default is TzWorld.timezone_at/1 when :tz_world is a dependency.

Returns

  • {:ok, local}, an Astro.Solar.Eclipse.Local.t/0 with the type there (:partial, :annular or :total), the contacts :partial_begins, :annular_begins or :total_begins, :maximum, :total_ends or :annular_ends and :partial_ends as date times in the requested time zone (nil for those the eclipse does not have there), the magnitude, the obscuration, and the Sun's geometric altitude in degrees at each contact. A contact the Sun is below the horizon for has a negative altitude.

  • {:error, :not_visible} if the eclipse is not seen from the location: the location is outside the Moon's penumbra throughout, or the Sun is below the horizon from first contact to last.

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if eclipse is neither an eclipse nor a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if eclipse is a date more than 50,000 years from J2000.

  • {:error, :invalid_options} if options is not a keyword list.

  • The time zone errors of sunrise/3.

Accuracy

Against the USNO's eclipse computer for 17 eclipses and places from 1999 to 2026, with the USNO's ΔT, the contacts agree within 2 s, except where the observer is near the edge of an annular path and the contacts of annularity change quickly with position (4 s at Singapore in 2019). The partial contacts agree within 1.1 s. Totality is about 2 s shorter, and annularity about 2 s longer, than the USNO's, which takes a larger radius for the Moon than NASA's k2 = 0.272281 used here. The magnitude agrees within 0.0005, the obscuration within 0.0012, the Sun's altitudes within 0.05°, and the type and visibility are the same. The USNO's maximum is 1 s earlier than the midpoint of its own second and third contacts, where Astro's is. In Universal Time every instant also carries ΔT's uncertainty, which moves the shadow across the Earth: an error of a second in ΔT moves every contact by about a second.

Examples

iex> dallas = {-96.80, 32.78}
iex> {:ok, local} = Astro.solar_eclipse_at(dallas, ~D[2024-04-01], time_zone: :utc)
iex> {local.type, DateTime.truncate(local.total_begins, :second)}
{:total, ~U[2024-04-08 18:40:43Z]}
iex> {Float.round(local.magnitude, 3), Float.round(local.sun_altitudes.maximum, 1)}
{1.015, 64.6}

iex> {:ok, eclipse} = Astro.solar_eclipse_at_or_after(~D[2024-04-01])
iex> {:ok, local} = Astro.solar_eclipse_at({-74.01, 40.71}, eclipse, time_zone: "America/New_York")
iex> {local.type, Float.round(local.obscuration, 3)}
{:partial, 0.899}
iex> DateTime.truncate(local.maximum, :second)
#DateTime<2024-04-08 15:25:35-04:00 EDT America/New_York>

iex> Astro.solar_eclipse_at({-0.13, 51.50}, ~D[2024-04-01], time_zone: :utc)
{:error, :not_visible}

Reference

  • P. Kenneth Seidelmann (editor), Explanatory Supplement to the Astronomical Almanac (1992), chapter 8 (eclipses of the Sun and Moon).

  • Jean Meeus, Elements of Solar Eclipses 1951–2200 (1989), on local circumstances from Besselian elements.

  • United States Naval Observatory, Solar Eclipse Computer.

solar_eclipse_at_or_after(date)

(since 2.10.0)
@spec solar_eclipse_at_or_after(date()) ::
  {:ok, Astro.Solar.Eclipse.t()} | {:error, :invalid_date | :year_out_of_range}

Returns the first solar eclipse whose greatest eclipse is at or after a date or date time.

An eclipse is found at a new moon near a node of the Moon's orbit. At each new moon the Besselian elements of the Moon's shadow, computed from the Sun's and the Moon's apparent positions, give the distance of the shadow axis from the centre of the Earth; where the shadow can reach the Earth, greatest eclipse is the moment of least distance, found by golden section search. Where the axis meets the Earth the eclipse is central, total or annular by the umbra at the surface, and hybrid if a total eclipse's umbra falls short of the surface at either end of the central line; otherwise the eclipse is partial, or total or annular without a central line where the umbra still reaches the limb. Astro.Solar.Eclipse describes the geometry. The positions come from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Returns

  • {:ok, eclipse}, an Astro.Solar.Eclipse.t/0 with the type (:partial, :annular, :total or :hybrid), greatest eclipse as a UTC DateTime.t/0, gamma and the magnitude: for a central eclipse the ratio of the Moon's apparent diameter to the Sun's at greatest eclipse, and otherwise the fraction of the Sun's diameter covered there; and its Saros series as NASA numbers it (Astro.Saros).

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000.

Accuracy

Against NASA's catalogue for 1901–2100, compared in Terrestrial Time so that ΔT does not enter, greatest eclipse is within 0.6 s, gamma within 0.0001, the magnitude within 0.0001, and every eclipse's type is the same, hybrids included. Outside the ephemeris the series place greatest eclipse within 9 s of NASA's in the nineteenth century, 35 s in the thirtieth and two and a quarter minutes around 950 BCE, with gamma within 0.001, magnitudes within 0.0015 and the same types. In Universal Time every instant also carries ΔT's uncertainty; the accuracy guide gives it.

Examples

iex> {:ok, eclipse} = Astro.solar_eclipse_at_or_after(~D[2024-04-01])
iex> {eclipse.type, DateTime.truncate(eclipse.greatest, :second)}
{:total, ~U[2024-04-08 18:17:20Z]}
iex> {Float.round(eclipse.gamma, 4), Float.round(eclipse.magnitude, 4)}
{0.3431, 1.0565}

iex> {:ok, eclipse} = Astro.solar_eclipse_at_or_after(~D[2024-04-09])
iex> {eclipse.type, DateTime.to_date(eclipse.greatest)}
{:annular, ~D[2024-10-02]}

Reference

  • Fred Espenak and Jean Meeus, Five Millennium Canon of Solar Eclipses: −1999 to +3000, NASA/TP-2006-214141 (2006), and the Five Millennium Catalog of Solar Eclipses.

  • P. Kenneth Seidelmann (editor), Explanatory Supplement to the Astronomical Almanac (1992), chapter 8 (eclipses of the Sun and Moon).

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 54 (eclipses).

solar_eclipse_before(date)

(since 2.10.0)
@spec solar_eclipse_before(date()) ::
  {:ok, Astro.Solar.Eclipse.t()} | {:error, :invalid_date | :year_out_of_range}

Returns the last solar eclipse whose greatest eclipse is before a date or date time.

An eclipse is found at a new moon near a node of the Moon's orbit. At each new moon the Besselian elements of the Moon's shadow, computed from the Sun's and the Moon's apparent positions, give the distance of the shadow axis from the centre of the Earth; where the shadow can reach the Earth, greatest eclipse is the moment of least distance, found by golden section search. Astro.Solar.Eclipse describes the geometry and the types. The positions come from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Returns

  • {:ok, eclipse}, an Astro.Solar.Eclipse.t/0 with the type (:partial, :annular, :total or :hybrid), greatest eclipse as a UTC DateTime.t/0, gamma and the magnitude: for a central eclipse the ratio of the Moon's apparent diameter to the Sun's at greatest eclipse, and otherwise the fraction of the Sun's diameter covered there; and its Saros series as NASA numbers it (Astro.Saros).

  • {:error, :invalid_date} if date_time is not a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if date_time is more than 50,000 years from J2000.

Accuracy

As solar_eclipse_at_or_after/1: within 0.6 s of NASA's greatest eclipse in Terrestrial Time over 1901–2100, gamma and the magnitude within 0.0001, and every type the same.

Examples

iex> {:ok, eclipse} = Astro.solar_eclipse_before(~D[2024-04-01])
iex> {eclipse.type, DateTime.to_date(eclipse.greatest)}
{:annular, ~D[2023-10-14]}

Reference

  • Fred Espenak and Jean Meeus, Five Millennium Canon of Solar Eclipses: −1999 to +3000, NASA/TP-2006-214141 (2006), and the Five Millennium Catalog of Solar Eclipses.

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 54 (eclipses).

solar_eclipse_path(eclipse, options \\ [])

(since 2.10.0)
@spec solar_eclipse_path(Astro.Solar.Eclipse.t() | date(), options()) ::
  {:ok, Astro.Solar.Eclipse.Path.t()}
  | {:error,
     :invalid_date | :year_out_of_range | :invalid_options | :invalid_interval}

Returns the path of a solar eclipse: its central line and the northern and southern limits of the band in which it is total or annular, and the bounds of the region in which it is seen as a partial eclipse.

The central line is where the shadow axis meets the Earth, and a limit is where the edge of the umbra, or of the antumbra for an annular eclipse, just grazes an observer. Each is computed from the eclipse's Besselian elements at a sequence of moments while the shadow is on the Earth. The partial eclipse is seen within the penumbral limits, where the edge of the penumbra grazes an observer, and the sunrise and sunset limits, where the eclipse ends just as the Sun rises or begins just as it sets. Astro.Solar.Eclipse.Path describes the geometry.

The lines are drawn on a map by joining their points in order, and the band by joining the northern limit to the southern limit taken backwards. Where a limit lies beyond the horizon, near the ends of the path and on the open side of an eclipse whose axis misses the Earth, its points follow the horizon, the places having their greatest eclipse at sunrise or sunset, so the two limits begin together where the shadow first touches the Earth and end together where it leaves. Every location is a Geo.Point.t/0 with coordinates {longitude, latitude} in degrees, as Astro takes locations, the longitude from −180° to 180°, so a line that crosses the antimeridian jumps from one end of that range to the other.

Arguments

Options

  • :interval is the time between points in seconds, at least 1. The default is 60. Points are at whole multiples of the interval in Universal Time, and at greatest eclipse.

Returns

  • {:ok, path}, an Astro.Solar.Eclipse.Path.t/0 with:

    • The eclipse's :type.

    • :central_line, :northern_limit and :southern_limit, lists of points in time order. The central line is empty for an eclipse whose shadow axis misses the Earth, and all three for a partial eclipse.

    • :penumbral_northern_limit and :penumbral_southern_limit, lists of points in time order, empty where the limit is never on the Earth with the Sun up.

    • :sunrise_limit, :sunset_limit, :maximum_at_sunrise and :maximum_at_sunset, each a list of lines in the order they begin, each line a list of points in time order: a curve on the horizon can have two branches at once.

    • :first_contact and :last_contact, the points where and when the penumbra first touches the Earth and last leaves it.

    Every point has its :time, a UTC DateTime.t/0 to the millisecond, and its :location, a Geo.Point.t/0. A point of the central line also has the eclipse there (:total or :annular), the path's :width in kilometres, the :duration of totality or annularity in seconds, and the Sun's geometric altitude in degrees.

  • {:error, :invalid_date} if eclipse is neither an eclipse nor a valid date or date time in its calendar.

  • {:error, :year_out_of_range} if eclipse is a date more than 50,000 years from J2000.

  • {:error, :invalid_options} if options is not a keyword list.

  • {:error, :invalid_interval} if :interval is not a number of seconds of at least 1.

Accuracy

Against NASA's path tables for eight eclipses from 1999 to 2027 (total, annular, hybrid, one over Antarctica and one whose axis misses the Earth), 674 instants with NASA's ΔT, every point NASA gives is found. Across the path the central line and the limits lie within 0.7 km of NASA's where the Sun is 20° or more above the horizon, and within 2.6 km over Antarctica with the Sun lower, where the ground is tilted to the shadow. Along the path they are within 2 s of the shadow's motion. The differences follow those of the Besselian elements, which come from different ephemerides. The path width agrees within 1 km and 0.6% (a path 400 km wide curves with the Earth across its width, where the two measure it differently), the duration within 0.7 s, and the Sun's altitude within NASA's rounding to a degree. NASA names a hybrid eclipse's limits by the sign of the umbra's radius, so in the total part of its path NASA's northern limit is the southern one here. In Universal Time the path also carries ΔT's uncertainty: a second of ΔT moves it 0.46 km east or west at the equator.

First and last contact agree with NASA's, as EclipseWise publishes them, within 0.3 s in Terrestrial Time for eight eclipses from 2014 to 2026, total, annular, hybrid and partial. Within each line of the sunrise and sunset limits and of the curves of maximum eclipse on the horizon, every point is where solar_eclipse_at/3 puts that contact, or maximum, within 0.01 s and with the Sun on the horizon within 0.001°; and a point 0.05° inside a penumbral limit sees a partial eclipse of magnitude under 0.003 where a point 0.05° outside sees none. Where lines meet, at first and last contact and where a limit reaches the horizon, the penumbra touches a place for an instant and there is no eclipse to see.

Examples

iex> {:ok, path} = Astro.solar_eclipse_path(~D[2024-04-01])
iex> path.type
:total
iex> {length(path.central_line), length(path.northern_limit)}
{197, 198}
iex> point = Enum.find(path.central_line, &(&1.time == ~U[2024-04-08 18:17:00.000Z]))
iex> {longitude, latitude} = point.location.coordinates
iex> {Float.round(longitude, 2), Float.round(latitude, 2), Float.round(point.width, 0)}
{-104.24, 25.19, 198.0}

A partial eclipse has no central line, but has a region in which it is seen:

iex> {:ok, path} = Astro.solar_eclipse_path(~D[2025-03-01])
iex> {path.type, path.central_line}
{:partial, []}
iex> DateTime.truncate(path.first_contact.time, :second)
~U[2025-03-29 08:50:43Z]

Reference

  • P. Kenneth Seidelmann (editor), Explanatory Supplement to the Astronomical Almanac (1992), chapter 8 (eclipses of the Sun and Moon).

  • Jean Meeus, Elements of Solar Eclipses 1951–2200 (1989), on the central line and the limits from Besselian elements.

  • Fred Espenak, path tables of Central Solar Eclipses: 1991–2050, NASA Goddard Space Flight Center.

solar_noon(location, date)

@spec solar_noon(location(), Calendar.date()) ::
  {:ok, DateTime.t()}
  | {:error, :invalid_location | :invalid_date | :incompatible_calendars}

Returns solar noon at a location on a date, as a UTC date time.

Solar noon is the moment the Sun crosses the local meridian, when it is due north or south and highest in the sky. It is rarely 12:00 on the clock: a mean Sun moving uniformly would cross the meridian at 12:00 local mean time, but the true Sun runs ahead of or behind it by the equation of time, up to about 16 minutes, because the Earth's orbit is elliptical and its axis is tilted.

Solar noon is found as the moment the Sun's local hour angle (the local apparent sidereal time less the Sun's apparent right ascension) is zero, by Newton's method from 12:00 local mean time; see Astro.Solar.solar_noon/2. The Sun's position comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

  • location is the observer's position as a {longitude, latitude} tuple in degrees, a Geo.Point.t/0, or a Geo.PointZ.t/0 that also carries an elevation in metres. Longitude comes first.

  • date is any Calendar.date/0. A date in a calendar other than Calendar.ISO is converted to its ISO date first.

Returns

  • {:ok, datetime}, the UTC date time of solar noon at the given location on the given UTC date.

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if date is not a valid date in its calendar.

  • {:error, :incompatible_calendars} if its calendar cannot be converted to Calendar.ISO.

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. Inside the ephemeris the right ascension is accurate to well under 0.01 s of time; outside it the series move solar noon by 0.06 s RMS and 0.15 s at most over 1900–2100. Sidereal time follows the Earth's rotation, UT1, and Astro takes UTC for it; the two differ by up to 0.9 s. The accuracy guide describes how ΔT limits results far from the present.

Examples

iex> Astro.solar_noon {151.20666584, -33.8559799094}, ~D[2019-12-06]
{:ok, ~U[2019-12-06 01:45:52.977018Z]}

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 15 (rising, transit and setting) and chapter 28 (equation of time).

  • Equation of time and Hour angle, Wikipedia.

solstice(year, event, options \\ [])

@spec solstice(Calendar.year(), :june | :december, options()) ::
  {:ok, DateTime.t()}
  | {:error,
     :invalid_year
     | :invalid_event
     | :year_out_of_range
     | :time_zone_not_found
     | :utc_only_time_zone_database
     | :invalid_time_zone_database}

Returns the datetime of either the June or December solstice, in UTC or a requested time zone.

A solstice is the moment the Sun's apparent ecliptic longitude reaches 90° (the June solstice) or 270° (the December solstice). The Sun is then at its farthest north or south of the celestial equator, and the day is the longest of the year in one hemisphere and the shortest in the other.

The moment is found from the Sun's apparent ecliptic longitude (see sun_apparent_longitude/1). Starting 20 days before the 1st of the month, the Sun's mean motion of 360° a tropical year estimates when it reaches the target longitude, and a ten-day bracket around that estimate is bisected until it is narrower than a millisecond. The longitude comes from the JPL DE440s ephemeris where the loaded kernel covers the year, and from the analytical series for any other year.

Arguments

  • year is the Gregorian year for which the solstice is to be calculated, within 50,000 years of 2000.

  • event is either :june or :december indicating which of the two annual solstice datetimes is required.

  • options is a keyword list of options.

Options

  • :time_zone is :utc (the default) or a time zone name such as "Asia/Tokyo". The solstice is one instant everywhere; the time zone decides the local date and time it is given in, and so the civil day it falls on.

  • :time_zone_database is the module implementing Calendar.TimeZoneDatabase in which a named time zone is looked up. The default is the configured database, Calendar.get_time_zone_database/0. UTC needs none.

Returns

  • {:ok, datetime}, the solstice in the requested time zone.

  • {:error, :time_zone_not_found} if the time zone is not known to the time zone database.

  • {:error, :utc_only_time_zone_database} if a time zone other than UTC is requested and no time zone database is configured.

  • {:error, :invalid_time_zone_database} if :time_zone_database is not a time zone database.

  • {:error, :invalid_year} if year is not an integer.

  • {:error, :year_out_of_range} if year is more than 50,000 years from 2000.

  • {:error, :invalid_event} if event is not :june or :december.

Accuracy

The search ends within a millisecond of the crossing. For the June solstice of 1962, Meeus gives 21h24m42s TD from the full VSOP87 theory (Astronomical Algorithms, chapter 27); Astro gives 21:24:07.8 UTC, which is 21h24m42.0s TD. Outside the ephemeris the series put the events 14 s early on average over 1900–2100, 18 s RMS and 50 s at most. Far from the present the time is limited by ΔT, which is uncertain by minutes before about 1000 and after about 2500; the accuracy guide gives the details.

Examples

iex> {:ok, dt} = Astro.solstice 2019, :december
iex> DateTime.truncate(dt, :second)
~U[2019-12-22 04:19:24Z]
iex> {:ok, dt} = Astro.solstice 2019, :june
iex> DateTime.truncate(dt, :second)
~U[2019-06-21 15:54:13Z]
iex> {:ok, dt} = Astro.solstice 3500, :june
iex> DateTime.to_date(dt)
~D[3500-06-20]

# The June 2021 solstice is on the 21st in UTC but the 20th in Santiago
iex> {:ok, dt} = Astro.solstice(2021, :june, time_zone: "America/Santiago")
iex> DateTime.to_date(dt)
~D[2021-06-20]

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 27 (equinoxes and solstices).

  • United States Naval Observatory, Earth's Seasons.

  • Solstice and Tropical year, Wikipedia.

sun_apparent_longitude(date)

@spec sun_apparent_longitude(date()) :: degrees()

Returns the Sun's apparent ecliptic longitude at a date or date time.

The ecliptic longitude is the Sun's position along the ecliptic, the plane of the Earth's orbit, measured eastward from the equinox of the date. It is 0° at the March equinox, 90° at the June solstice, 180° at the September equinox and 270° at the December solstice, so it measures how far through the tropical year (the year of the seasons) the Earth has moved, without the irregularity of leap years. Solar terms, such as those of the Chinese calendar, are the moments it reaches multiples of 15°.

Where the loaded JPL DE440s ephemeris covers the moment, the apparent position (see sun_position_at/1) is rotated from the true equator to the ecliptic of date by the true obliquity. Elsewhere it is the 49-term series of Bretagnon and Simon, with aberration and nutation in longitude added.

Arguments

Returns

  • the apparent ecliptic longitude in degrees, from 0 to 360.

Accuracy

Outside the ephemeris the series agree with JPL over 1900–2100 to 0.75″ RMS and 2.0″ at most, with a mean bias of +0.56″. The Sun moves about 2.5″ a minute, so that bias puts the moment the Sun reaches a given longitude about 14 s early. The accuracy guide gives the details, and how the series and ΔT drift far from the present.

Examples

iex> Astro.sun_apparent_longitude ~D[2019-03-21]
0.0838600951350463
iex> Astro.sun_apparent_longitude ~D[2019-06-22]
90.32182153965914
iex> Astro.sun_apparent_longitude ~D[2019-09-23]
179.68053562968015
iex> Astro.sun_apparent_longitude ~D[2019-12-23]
270.8352772904792

Reference

  • Jean Meeus, Astronomical Algorithms, 2nd edition (1998), chapter 25 (solar coordinates).

  • Edward M. Reingold and Nachum Dershowitz, Calendrical Calculations, for the series of Bretagnon and Simon.

  • Ecliptic coordinate system, Wikipedia.

sun_azimuth_elevation(location, date_time)

(since 0.11.0)
@spec sun_azimuth_elevation(location(), Calendar.datetime()) ::
  {azimuth :: float(), altitude :: float()}

Returns the Sun's azimuth and elevation as seen from a location at a date time.

Azimuth and elevation are the Sun's horizontal coordinates: its direction around the horizon and its height above it. They are calculated from the Sun's apparent right ascension α and declination δ (see sun_position_at/1) and the local hour angle H, the local apparent sidereal time less α, by the transformation of Meeus, Astronomical Algorithms, chapter 13:

tan A = sin H / (cos H sin φ − tan δ cos φ)
sin h = sin φ sin δ + cos φ cos δ cos H

where φ is the observer's latitude, A is measured from the south and is returned measured from the north, and h is the elevation.

The position is geocentric and airless: it omits the Sun's parallax (at most 8.8″) and atmospheric refraction, which raises the Sun by about 34′ at the horizon and by less than 1′ above an elevation of 45°.

Arguments

Returns

  • {azimuth, elevation} in degrees. Azimuth is measured clockwise from north, from 0° to 360°, and elevation above the horizon, from −90° to 90°.

Accuracy

Inside the loaded JPL ephemeris the Sun's apparent position is accurate to a small fraction of an arcsecond, so the result is limited by what it leaves out: for the reference case of NREL's solar position algorithm (Golden, Colorado, 2003-10-17 19:30:30 UTC) it agrees to 0.05″ in azimuth and is 0.87′ lower, the refraction NREL includes. Outside the ephemeris the analytical series agree with JPL to 0.8″ RMS in azimuth and 0.6″ in elevation. The accuracy guide gives the details.

Examples

iex> {azimuth, elevation} =
...>   Astro.sun_azimuth_elevation({151.20666584, -33.8559799094}, ~U[2019-12-04 02:00:00Z])
iex> {Float.round(azimuth, 4), Float.round(elevation, 4)}
{343.3379, 77.8643}

Reference

sun_position_at(date_time)

(since 0.6.0)
@spec sun_position_at(date()) :: Geo.PointZ.t()

Returns the Sun's apparent right ascension, declination and distance at a date or date time.

This is the Sun's apparent geocentric position: where it is seen from the centre of the Earth, referred to the true equator and equinox of the date. Right ascension is measured eastward along the celestial equator from the equinox and declination north of the equator.

Where the loaded JPL DE440s ephemeris covers the moment, the Earth-to-Sun vector is read from it at the moment less the light time, corrected for aberration, and rotated from the ICRF to the true equator of date by IAU 1976 precession and IAU 1980 nutation. Elsewhere the Sun's longitude comes from the 49-term series of Bretagnon and Simon, with aberration and nutation added, and is converted to right ascension and declination with the true obliquity of the ecliptic (Meeus, Astronomical Algorithms, chapter 13).

Arguments

Returns

  • a Geo.PointZ.t/0 struct with coordinates {right_ascension, declination, distance} with properties %{reference: :celestial, object: :sun}. Right ascension is in degrees from −180° to 180°, declination in degrees and distance in metres.

Accuracy

Inside the ephemeris the reduction reproduces Meeus's full-precision example 25.b to 0.2″. Outside it the series agree with JPL over 1900–2100 to 0.76″ RMS in right ascension and 0.50″ in declination (2.2″ and 1.3″ at most), and to 4,300 km in distance. Far from the present the series drift, and ΔT, which places the moment in Terrestrial Time, becomes uncertain by minutes to hours; the accuracy guide gives the details.

Examples

iex> %Geo.PointZ{coordinates: {right_ascension, declination, distance}, properties: properties} =
...>   Astro.sun_position_at(~D[1992-10-13])
iex> {Float.round(right_ascension, 6), Float.round(declination, 6), round(distance)}
{-161.621245, -7.784069, 149240324715}
iex> properties
%{object: :sun, reference: :celestial}

Reference

sunrise(location, date, options \\ [])

@spec sunrise(location(), date(), options()) ::
  {:ok, DateTime.t()}
  | {:error,
     :no_time
     | :invalid_location
     | :invalid_date
     | :invalid_options
     | :invalid_utc_offset
     | :invalid_limb
     | :invalid_solar_elevation
     | :time_zone_not_found
     | :time_zone_not_resolved
     | :tz_world_data_not_installed
     | :tz_world_backend_not_running
     | :invalid_time_zone_resolver
     | :invalid_time_zone_database
     | :utc_only_time_zone_database}

Calculates the sunrise for a given location and date.

Sunrise is the moment when the upper limb of the sun appears on the horizon in the morning.

Refraction lifts the Sun by a standard 34′ at the horizon and its semi-diameter is about 16′, so this is the moment the Sun's centre, computed without refraction, is 50′ below the horizon. The twilights are the moments the centre is 6°, 12° or 18° below it.

The Sun's altitude is calculated from its apparent geocentric position (see sun_position_at/1) and the local apparent sidereal time; its parallax, at most 8.8″, is ignored. The altitude is sampled every 24 minutes across the day to bracket the crossing, which is then narrowed by bisection to 0.01 s. The position comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Options

  • :solar_elevation is the zenith angle of the sun, in degrees, that marks the sunrise, or one of the names below. The default is :geometric.

    • :geometric is 90°, corrected for refraction and the sun's apparent radius so that it matches the moment the upper limb appears to touch the horizon.

    • :civil is 96°. The sun is below the horizon but there is generally enough natural light for most outdoor activities.

    • :nautical is 102°. The horizon is barely visible, and the moon and stars can still be used for navigation.

    • :astronomical is 108°. Beyond this, astronomical observation becomes impractical.

  • :limb is :upper, the default, for the moment the Sun's upper limb is on the horizon, or :center for its centre (34′ of refraction alone). It applies to :geometric only.

  • :time_zone is the time zone of the returned date time: :default, the time zone of the location, which is the default; :utc; or a time zone name.

  • :utc_offset is a whole number of seconds from UTC, from −50,400 to 50,400. When it is given, the event is the one whose date at that offset is date, whatever time zone it is returned in: the location's own day, for a calendar that keeps local mean time. Otherwise the date is taken in the requested time zone.

  • :time_zone_database is the module implementing the Calendar.TimeZoneDatabase behaviour. The default is the configured Elixir time zone database.

  • :time_zone_resolver is a 1-arity function that receives a %Geo.Point{coordinates: {lng, lat}} and returns {:ok, time_zone_name} or {:error, reason}. The default is TzWorld.timezone_at/1 when :tz_world is a dependency.

Returns

  • {:ok, date_time} where date_time is the sunrise in the requested time zone.

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if date is not a valid date in its calendar.

  • {:error, :invalid_options} if options is not a list.

  • {:error, :invalid_utc_offset} if :utc_offset is not a whole number of seconds from −50,400 to 50,400.

  • {:error, :invalid_limb} if :limb is not :upper or :center.

  • {:error, :no_time} if there is no sunrise on that date at that location, as happens at very high latitudes in summer and winter.

  • {:error, :invalid_solar_elevation} if :solar_elevation is neither a number nor one of the names above.

  • {:error, :time_zone_not_found} if the requested time zone is unknown.

  • {:error, :time_zone_not_resolved} if no time zone can be resolved for the location, which happens when :tz_world is not a dependency and no :time_zone_resolver is given, or when the resolver returns neither {:ok, time_zone} nor {:error, reason}.

  • {:error, :tz_world_data_not_installed} if :tz_world is a dependency but its data has not been installed. Run mix tz_world.update to install it.

  • {:error, :tz_world_backend_not_running} if :tz_world is a dependency but none of its backends is running; see Astro.Supervisor.

  • {:error, :invalid_time_zone_resolver} if :time_zone_resolver is not a 1-arity function.

  • {:error, :invalid_time_zone_database} if :time_zone_database is not a time zone database.

  • {:error, :utc_only_time_zone_database} if the sunrise is requested in a time zone other than UTC and no time zone database is configured.

Accuracy

Against Skyfield, using the same ephemeris, results agree to 0.95 s on average and 1.6 s at most; Skyfield uses the Sun's apparent semi-diameter for the day rather than a fixed 16′. Against timeanddate.com, which publishes to the minute, they agree to 28.8 s on average and within a minute in 307 of 310 cases. Outside the ephemeris the series move the result by 0.07 s RMS and 0.21 s at most. The atmosphere matters more than any of these: refraction at the horizon departs from the standard 34′ with temperature and pressure, and a high observer sees a lower horizon, and neither is modelled. The accuracy guide gives the details.

Examples

iex> {:ok, date_time} = Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04])
iex> date_time
#DateTime<2019-12-04 05:37:06.206432+11:00 AEDT Australia/Sydney>

iex> Astro.sunrise({-62.3481, 82.5018}, ~D[2019-12-04])
{:error, :no_time}

Reference

sunset(location, date, options \\ [])

@spec sunset(location(), date(), options()) ::
  {:ok, DateTime.t()}
  | {:error,
     :no_time
     | :invalid_location
     | :invalid_date
     | :invalid_options
     | :invalid_utc_offset
     | :invalid_limb
     | :invalid_solar_elevation
     | :time_zone_not_found
     | :time_zone_not_resolved
     | :tz_world_data_not_installed
     | :tz_world_backend_not_running
     | :invalid_time_zone_resolver
     | :invalid_time_zone_database
     | :utc_only_time_zone_database}

Calculates the sunset for a given location and date.

Sunset is the moment when the upper limb of the sun disappears below the horizon in the evening.

Refraction lifts the Sun by a standard 34′ at the horizon and its semi-diameter is about 16′, so this is the moment the Sun's centre, computed without refraction, is 50′ below the horizon. The twilights are the moments the centre is 6°, 12° or 18° below it.

The Sun's altitude is calculated from its apparent geocentric position (see sun_position_at/1) and the local apparent sidereal time; its parallax, at most 8.8″, is ignored. The altitude is sampled every 24 minutes across the day to bracket the crossing, which is then narrowed by bisection to 0.01 s. The position comes from the JPL DE440s ephemeris where the loaded kernel covers the date, and from the analytical series elsewhere.

Arguments

Options

  • :solar_elevation is the zenith angle of the sun, in degrees, that marks the sunset, or one of the names below. The default is :geometric.

    • :geometric is 90°, corrected for refraction and the sun's apparent radius so that it matches the moment the upper limb appears to touch the horizon.

    • :civil is 96°. The sun is below the horizon but there is generally enough natural light for most outdoor activities.

    • :nautical is 102°. The horizon is barely visible, and the moon and stars can still be used for navigation.

    • :astronomical is 108°. Beyond this, astronomical observation becomes impractical.

  • :limb is :upper, the default, for the moment the Sun's upper limb is on the horizon, or :center for its centre (34′ of refraction alone). It applies to :geometric only.

  • :time_zone is the time zone of the returned date time: :default, the time zone of the location, which is the default; :utc; or a time zone name.

  • :utc_offset is a whole number of seconds from UTC, from −50,400 to 50,400. When it is given, the event is the one whose date at that offset is date, whatever time zone it is returned in: the location's own day, for a calendar that keeps local mean time. Otherwise the date is taken in the requested time zone.

  • :time_zone_database is the module implementing the Calendar.TimeZoneDatabase behaviour. The default is the configured Elixir time zone database.

  • :time_zone_resolver is a 1-arity function that receives a %Geo.Point{coordinates: {lng, lat}} and returns {:ok, time_zone_name} or {:error, reason}. The default is TzWorld.timezone_at/1 when :tz_world is a dependency.

Returns

  • {:ok, date_time} where date_time is the sunset in the requested time zone.

  • {:error, :invalid_location} if location is not a valid location.

  • {:error, :invalid_date} if date is not a valid date in its calendar.

  • {:error, :invalid_options} if options is not a list.

  • {:error, :invalid_utc_offset} if :utc_offset is not a whole number of seconds from −50,400 to 50,400.

  • {:error, :invalid_limb} if :limb is not :upper or :center.

  • {:error, :no_time} if there is no sunset on that date at that location, as happens at very high latitudes in summer and winter.

  • {:error, :invalid_solar_elevation} if :solar_elevation is neither a number nor one of the names above.

  • {:error, :time_zone_not_found} if the requested time zone is unknown.

  • {:error, :time_zone_not_resolved} if no time zone can be resolved for the location, which happens when :tz_world is not a dependency and no :time_zone_resolver is given, or when the resolver returns neither {:ok, time_zone} nor {:error, reason}.

  • {:error, :tz_world_data_not_installed} if :tz_world is a dependency but its data has not been installed. Run mix tz_world.update to install it.

  • {:error, :tz_world_backend_not_running} if :tz_world is a dependency but none of its backends is running; see Astro.Supervisor.

  • {:error, :invalid_time_zone_resolver} if :time_zone_resolver is not a 1-arity function.

  • {:error, :invalid_time_zone_database} if :time_zone_database is not a time zone database.

  • {:error, :utc_only_time_zone_database} if the sunset is requested in a time zone other than UTC and no time zone database is configured.

Accuracy

Against Skyfield, using the same ephemeris, results agree to 0.95 s on average and 1.6 s at most; Skyfield uses the Sun's apparent semi-diameter for the day rather than a fixed 16′. Against timeanddate.com, which publishes to the minute, they agree to 28.8 s on average and within a minute in 307 of 310 cases. Outside the ephemeris the series move the result by 0.07 s RMS and 0.21 s at most. The atmosphere matters more than any of these: refraction at the horizon departs from the standard 34′ with temperature and pressure, and a high observer sees a lower horizon, and neither is modelled. The accuracy guide gives the details.

Examples

iex> {:ok, date_time} = Astro.sunset({151.20666584, -33.8559799094}, ~D[2019-12-04])
iex> date_time
#DateTime<2019-12-04 19:53:18.155710+11:00 AEDT Australia/Sydney>

iex> Astro.sunset({-62.3481, 82.5018}, ~D[2019-12-04])
{:error, :no_time}

Reference