Astro.Lunar.MoonRiseSet (Astro v2.7.0)

Copy Markdown View Source

Computes moonrise and moonset times using the JPL DE440s ephemeris and a fully topocentric bisection algorithm.

This module is the implementation behind Astro.moonrise/3 and Astro.moonset/3.

Algorithm

The classical Meeus Ch.15 three-point geocentric iteration handles parallax-in-altitude via the h0 = 0.7275π − 0.5667° formula but ignores the right-ascension component of lunar parallax (~47 arcmin at the horizon for mid-latitudes). This produces a systematic 2–3 minute error because the apparent Moon is displaced in RA from the geocentric position by the observer's parallax.

This module removes that error entirely by abandoning the interpolation framework. Instead:

  1. Coarse scan — the local day is sampled at 24-minute intervals. At each sample the instantaneous topocentric apparent altitude is evaluated directly from the JPL DE440s ephemeris. Adjacent samples with opposite sign identify a rise or set event bracketed to within one scan step.

  2. Binary search — the bracket is bisected until its width falls below 0.01 seconds. Each probe evaluates one ephemeris position, one Meeus Ch.40 topocentric correction, and one refraction offset — no derivatives, no interpolation error.

The event condition matches the USNO / timeanddate.com standard: the topocentric geometric altitude of the Moon's centre equals −(34′/60° + semi_diameter), where 34′ is a fixed standard-atmosphere refraction constant. This is equivalent to the USNO's published condition zd_centre = 90.5666° + angular_radius − horizontal_parallax, once the horizontal parallax is absorbed by computing the topocentric position directly via Meeus Ch.40.

Unlike the Sun (whose parallax is only ~8.7″), the Moon's parallax can reach ~61′ — comparable to its own angular diameter. This makes the topocentric correction essential for accurate moonrise/moonset times.

Accuracy

Comparison with timeanddate.com

Expected agreement with timeanddate.com to within their 1-minute display resolution for locations with a flat mathematical horizon. The test suite validates 70 cases across four cities (New York, London, Sydney, Tokyo) against USNO reference data with a ±1 minute tolerance.

Comparison with Skyfield

Skyfield is a high-accuracy Python astronomy library that also uses JPL ephemerides for lunar position and a numerical root-finding approach. The two implementations share the same underlying positional data source and a similar solver strategy, so they are expected to agree to within a few seconds. Residual differences arise from:

  • Skyfield uses the IERS-based precession-nutation model (IAU 2000A/2006), while this module uses IAU 1976 precession and IAU 1980 nutation. For the Moon the RA difference is below 0.1″ for modern dates.
  • Skyfield's refraction model optionally accounts for observer elevation and temperature/pressure, whereas this module uses the fixed 34′ standard atmosphere constant.
  • Skyfield computes a fully rigorous topocentric position using the ITRS to GCRS transformation, while this module uses the Meeus Ch.40 approximation. The difference is negligible (< 0.01″) for the Moon.

Comparison with NOAA / Meeus Ch.15

The Meeus Ch.15 algorithm (as used by many online calculators) differs from this module in two significant ways:

  • Geocentric vs topocentric RA — Meeus Ch.15 applies a parallax correction only to altitude, not to right ascension. This ignores the RA component of lunar parallax, which can shift the apparent moonrise/ moonset time by 2–3 minutes at mid-latitudes.
  • Interpolation vs direct evaluation — Meeus Ch.15 interpolates the Moon's position from three daily tabular values, introducing interpolation error. This module evaluates the ephemeris directly at each bisection probe.

For all three references the dominant error source is real-atmosphere refraction variation (±2 arcmin ≈ ±10 s at the horizon), which none of these implementations model.

Required setup

The JPL DE440s ephemeris file must be present:

Summary

Functions

Returns the moonrise time for a given location and date.

Returns the moonset time for a given location and date.

Functions

moonrise(location, moment, options \\ [])

@spec moonrise(Astro.location(), number(), keyword()) ::
  {:ok, DateTime.t()} | {:error, atom()}

Returns the moonrise time for a given location and date.

Computes the moment when the upper limb of the Moon (or centre of disk if :limb is :center) appears to cross above the horizon, using lunar positions derived from the JPL DE440s ephemeris with full topocentric parallax correction.

Arguments

  • location is a {longitude, latitude} tuple, a Geo.Point.t/0, or a Geo.PointZ.t/0. Longitude and latitude are in degrees (west/south negative). Observer elevation is taken from Geo.PointZ if provided; it affects the geocentric parallax factors (Meeus Ch.11).

  • moment is a moment (float Gregorian days since 0000-01-01) representing UTC midnight of the requested date. Use Astro.Time.date_time_to_moment/1 to convert from a Date or DateTime.

  • 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.

  • :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, :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.

  • {: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, :utc_only_time_zone_database} if the moonrise is requested in a time zone other than UTC and no time zone database is configured.

  • {:error, :not_found} if the date is outside the loaded ephemeris.

Examples

iex> moment = Astro.Time.date_time_to_moment(~D[2026-03-01])
iex> {:ok, moonrise} = Astro.Lunar.MoonRiseSet.moonrise({151.20666584, -33.8559799094}, moment, time_zone: :utc)
iex> moonrise.hour
7
iex> moonrise.minute
18

moonset(location, moment, options \\ [])

@spec moonset(Astro.location(), number(), keyword()) ::
  {:ok, DateTime.t()} | {:error, atom()}

Returns the moonset time for a given location and date.

Computes the moment when the upper limb of the Moon (or centre of disk if :limb is :center) appears to cross below the horizon, using lunar positions derived from the JPL DE440s ephemeris with full topocentric parallax correction.

Arguments

  • location is a {longitude, latitude} tuple, a Geo.Point.t/0, or a Geo.PointZ.t/0. Longitude and latitude are in degrees (west/south negative). Observer elevation is taken from Geo.PointZ if provided; it affects the geocentric parallax factors (Meeus Ch.11).

  • moment is a moment (float Gregorian days since 0000-01-01) representing UTC midnight of the requested date. Use Astro.Time.date_time_to_moment/1 to convert from a Date or DateTime.

  • 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.

  • :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, :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.

  • {: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, :utc_only_time_zone_database} if the moonset is requested in a time zone other than UTC and no time zone database is configured.

  • {:error, :not_found} if the date is outside the loaded ephemeris.

Examples

iex> moment = Astro.Time.date_time_to_moment(~D[2026-03-01])
iex> {:ok, moonset} = Astro.Lunar.MoonRiseSet.moonset({151.20666584, -33.8559799094}, moment, time_zone: :utc)
iex> moonset.hour
17
iex> moonset.minute
59