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:
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.
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:
- Download
de440s.bspfrom https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de440s.bsp to theprivdirectory.
Summary
Functions
Returns the moonrise time for a given location and date.
Returns the moonset time for a given location and date.
Functions
@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
locationis a{longitude, latitude}tuple, aGeo.Point.t/0, or aGeo.PointZ.t/0. Longitude and latitude are in degrees (west/south negative). Observer elevation is taken fromGeo.PointZif provided; it affects the geocentric parallax factors (Meeus Ch.11).momentis a moment (float Gregorian days since 0000-01-01) representing UTC midnight of the requested date. UseAstro.Time.date_time_to_moment/1to convert from aDateorDateTime.optionsis a keyword list of options.
Options
:limb— which part of the Moon's disk defines the event::upper(default) — upper limb on the apparent horizon (USNO standard). The event threshold is−(34′ refraction + semi-diameter).:center— centre of disk on the apparent horizon. The event threshold is−34′ refractiononly.
:interpolation— how the Moon's position is evaluated during bisection::direct(default) — evaluate the JPL ephemeris at every bisection probe.:lagrange— three-point Lagrange quadratic interpolation of the geocentric position, mimicking the Meeus Ch.15 approach.
:time_zone— the time zone for the returnedDateTime. The default is:defaultwhich resolves the time zone from the location.:utcreturns UTC, or pass a time zone name string (e.g."Asia/Tokyo").:time_zone_database— the module implementing theCalendar.TimeZoneDatabasebehaviour. The default is:configuredwhich uses the application's configured time zone database.:time_zone_resolver— a 1-arity function that receives a%Geo.Point{}and returns{:ok, time_zone_name}or{:error, reason}. The default usesTzWorld.timezone_at/1if:tz_worldis configured.
Returns
{:ok, datetime}wheredatetimeis aDateTime.t/0in the requested time zone.{:error, :no_time}if the Moon does not rise on the requested date at the given location (the Moon can remain below the horizon for an entire calendar day).{:error, :time_zone_not_found}if the requested time zone is unknown.{:error, :time_zone_not_resolved}if the time zone cannot be resolved from the location.
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
@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
locationis a{longitude, latitude}tuple, aGeo.Point.t/0, or aGeo.PointZ.t/0. Longitude and latitude are in degrees (west/south negative). Observer elevation is taken fromGeo.PointZif provided; it affects the geocentric parallax factors (Meeus Ch.11).momentis a moment (float Gregorian days since 0000-01-01) representing UTC midnight of the requested date. UseAstro.Time.date_time_to_moment/1to convert from aDateorDateTime.optionsis a keyword list of options.
Options
:limb— which part of the Moon's disk defines the event::upper(default) — upper limb on the apparent horizon (USNO standard). The event threshold is−(34′ refraction + semi-diameter).:center— centre of disk on the apparent horizon. The event threshold is−34′ refractiononly.
:interpolation— how the Moon's position is evaluated during bisection::direct(default) — evaluate the JPL ephemeris at every bisection probe.:lagrange— three-point Lagrange quadratic interpolation of the geocentric position, mimicking the Meeus Ch.15 approach.
:time_zone— the time zone for the returnedDateTime. The default is:defaultwhich resolves the time zone from the location.:utcreturns UTC, or pass a time zone name string (e.g."Asia/Tokyo").:time_zone_database— the module implementing theCalendar.TimeZoneDatabasebehaviour. The default is:configuredwhich uses the application's configured time zone database.:time_zone_resolver— a 1-arity function that receives a%Geo.Point{}and returns{:ok, time_zone_name}or{:error, reason}. The default usesTzWorld.timezone_at/1if:tz_worldis configured.
Returns
{:ok, datetime}wheredatetimeis aDateTime.t/0in the requested time zone.{:error, :no_time}if the Moon does not set on the requested date at the given location (the Moon can remain above the horizon for an entire calendar day).{:error, :time_zone_not_found}if the requested time zone is unknown.{:error, :time_zone_not_resolved}if the time zone cannot be resolved from the location.
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