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
:limbis 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).:centerputs the centre of the disk on the apparent horizon. The event threshold is−34′of refraction only.
:interpolationis how the Moon's position is evaluated while the event is bisected.:direct, the default, evaluates the JPL ephemeris at every step.:lagrangeinterpolates the geocentric position quadratically from three points, as Meeus Ch. 15 does.
:time_zoneis 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_databaseis the module implementing theCalendar.TimeZoneDatabasebehaviour. The default is the configured Elixir time zone database.:time_zone_resolveris a 1-arity function that receives a%Geo.Point{coordinates: {lng, lat}}and returns{:ok, time_zone_name}or{:error, reason}. The default isTzWorld.timezone_at/1when:tz_worldis a dependency.
Returns
{:ok, date_time}wheredate_timeis 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:limbis not:upperor:center.{:error, :invalid_interpolation}if:interpolationis not:director: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_worldis not a dependency and no:time_zone_resolveris given.{:error, :tz_world_data_not_installed}if:tz_worldis a dependency but its data has not been installed. Runmix tz_world.updateto 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
@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
:limbis 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).:centerputs the centre of the disk on the apparent horizon. The event threshold is−34′of refraction only.
:interpolationis how the Moon's position is evaluated while the event is bisected.:direct, the default, evaluates the JPL ephemeris at every step.:lagrangeinterpolates the geocentric position quadratically from three points, as Meeus Ch. 15 does.
:time_zoneis 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_databaseis the module implementing theCalendar.TimeZoneDatabasebehaviour. The default is the configured Elixir time zone database.:time_zone_resolveris a 1-arity function that receives a%Geo.Point{coordinates: {lng, lat}}and returns{:ok, time_zone_name}or{:error, reason}. The default isTzWorld.timezone_at/1when:tz_worldis a dependency.
Returns
{:ok, date_time}wheredate_timeis 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:limbis not:upperor:center.{:error, :invalid_interpolation}if:interpolationis not:director: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_worldis not a dependency and no:time_zone_resolveris given.{:error, :tz_world_data_not_installed}if:tz_worldis a dependency but its data has not been installed. Runmix tz_world.updateto 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