Astro.Solar.SunRiseSet (Astro v2.0.0)

Copy Markdown View Source

Computes sunrise and sunset times using the JPL DE440s ephemeris and a scan-and-bisect algorithm.

This module is the implementation behind Astro.sunrise/3 and Astro.sunset/3.

Algorithm

The same coarse-scan / binary-search framework used by Astro.Lunar.MoonRiseSet is applied to the Sun. Because the Sun's equatorial horizontal parallax is only ~8.7 arcseconds (≈ 0.002°), no topocentric correction is required; the geocentric position is used directly.

The event condition matches the USNO / timeanddate.com standard:

geometric_alt_centre = 50/60°

where 50′ = 34′ (standard refraction) + 16′ (solar semi-diameter). This fixed threshold is the same constant used by virtually every published sunrise/ sunset table and is independent of the actual solar distance on the day.

Accuracy

Comparison with timeanddate.com

Expected agreement with timeanddate.com to within their 1-minute display resolution for all latitudes where sunrise and sunset occur and where the location has a flat mathematical horizon. The test suite validates 343 cases across five cities (Sydney, Moscow, New York, Beijing, São Paulo) against reference data with a ±1 minute tolerance.

Comparison with Skyfield

Skyfield is a high-accuracy Python astronomy library that also uses JPL ephemerides (DE421/DE440) for solar position and a numerical root-finding approach. The two implementations share the same underlying positional data source and a similar solver strategy (coarse scan then bisection), so they are expected to agree to within a few seconds for standard (geometric) sunrise/sunset. 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. The difference in apparent solar RA is below 0.01 s of time 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.

Comparison with NOAA Solar Calculator

The NOAA Solar Calculator uses the Meeus analytical polynomial series for solar position and an iterative formula for the rise/set time. Astro.Solar (lib/astro/solar.ex) implements this same NOAA/Meeus algorithm. Differences between this module and the NOAA approach are typically under 30 seconds and arise from:

  • This module evaluates solar positions from the JPL DE440s numerical ephemeris (Chebyshev polynomials fitted to a full n-body integration), while the NOAA algorithm uses truncated analytical series from Meeus.
  • This module applies a variable ΔT correction based on IERS observations (1972–2025) and Meeus polynomial approximations for historical dates, while the NOAA calculator uses a simpler ΔT model.
  • This module uses a scan-and-bisect solver with 0.01 s tolerance, while the NOAA algorithm uses an iterative analytical formula.

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.

Solar elevation options

The :solar_elevation option controls which event is computed. The terminology can be confusing because different references use "solar elevation", "solar zenith angle", and "solar depression" to describe overlapping concepts.

TermDefinitionRelationship
Geometric altitudeAngle of the Sun's centre above the geometric (airless) horizon, measured from 0° (horizon) to +90° (zenith).
Solar zenith angleComplement of altitude: 90° − altitude. 0° at the zenith, 90° at the horizon.zenith = 90° − altitude
Solar depressionAngle below the horizon, i.e. the negation of a negative altitude. Used for twilight thresholds.depression = −altitude (when Sun is below horizon)

Sunrise and sunset occur when the Sun's geometric altitude crosses a threshold that accounts for atmospheric refraction (34′) and the Sun's angular semi-diameter (16′). The named :solar_elevation values and their corresponding thresholds are:

OptionZenith angleAltitude thresholdDescription
:geometric90°50′−0.8333°Standard sunrise/sunset. Upper limb of the Sun appears to touch the horizon after accounting for standard atmospheric refraction.
:civil96°−6°Civil twilight. Enough light for outdoor activities without artificial lighting. The horizon is clearly visible.
:nautical102°−12°Nautical twilight. The horizon is faintly visible at sea. Bright stars are visible for celestial navigation.
:astronomical108°−18°Astronomical twilight. The sky is dark enough for astronomical observations of faint objects.
Custom number N−(N − 90)°A custom zenith angle in degrees, converted to an altitude threshold.

Note: the :geometric option name is historical. Despite its name, the :geometric threshold does include standard atmospheric refraction (34′) and solar semi-diameter (16′). A truly geometric (airless, centre-of-disk) event would use a custom value of 90.0.

Required setup

The JPL DE440s ephemeris file must be present:

Summary

Functions

Returns the sunrise time for a given location and date.

Returns the sunset time for a given location and date.

Functions

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

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

Returns the sunrise time for a given location and date.

Computes the moment when the upper limb of the Sun appears to cross the horizon (or the configured :solar_elevation threshold) using solar positions derived from the JPL DE440s ephemeris.

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

  • 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

  • :solar_elevation — the type of sunrise to compute:

    • :geometric (default) — standard sunrise where the upper limb of the Sun appears to touch the horizon (zenith 90°50′, accounting for 34′ standard refraction + 16′ solar semi-diameter)
    • :civil — centre of Sun 6° below the horizon (civil twilight boundary)
    • :nautical — centre of Sun 12° below the horizon (nautical twilight boundary)
    • :astronomical — centre of Sun 18° below the horizon (astronomical twilight boundary)
    • a number — custom zenith angle in degrees (90 = geometric horizon with no refraction or semi-diameter correction)
  • :time_zone — the time zone for the returned DateTime. The default is :default which resolves the time zone from the location. :utc returns UTC, or pass a time zone name string (e.g. "America/New_York").

  • :time_zone_database — the module implementing the Calendar.TimeZoneDatabase behaviour. The default is :configured which 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 uses TzWorld.timezone_at/1 if :tz_world is configured.

Returns

  • {:ok, datetime} where datetime is a DateTime.t/0 in the requested time zone.

  • {:error, :no_time} if there is no sunrise on the requested date at the given location (e.g. polar night or midnight sun).

  • {: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[2019-12-04])
iex> {:ok, sunrise} = Astro.Solar.SunRiseSet.sunrise({151.20666584, -33.8559799094}, moment, time_zone: :utc)
iex> sunrise.hour
18
iex> sunrise.minute
37

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

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

Returns the sunset time for a given location and date.

Computes the moment when the upper limb of the Sun appears to cross below the horizon (or the configured :solar_elevation threshold) using solar positions derived from the JPL DE440s ephemeris.

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

  • 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

Accepts the same options as sunrise/3:

  • :solar_elevation — event threshold (default :geometric)
  • :time_zone — time zone for the result (default :default)
  • :time_zone_database — time zone database module (default :configured)
  • :time_zone_resolver — custom location-to-timezone resolver function

Returns

  • {:ok, datetime} where datetime is a DateTime.t/0 in the requested time zone.

  • {:error, :no_time} if there is no sunset on the requested date at the given location (e.g. polar night or midnight sun).

  • {: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[2019-12-04])
iex> {:ok, sunset} = Astro.Solar.SunRiseSet.sunset({151.20666584, -33.8559799094}, moment, time_zone: :utc)
iex> sunset.hour
8
iex> sunset.minute
53