# `Astro.Solar.SunRiseSet`
[🔗](https://github.com/kipcole9/astro/blob/v2.9.0/lib/astro/solar/sun_rise_set.ex#L1)

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](https://www.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](https://rhodesmill.org/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](https://gml.noaa.gov/grad/solcalc/) 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.

| Term | Definition | Relationship |
|---|---|---|
| **Geometric altitude** | Angle of the Sun's centre above the geometric (airless) horizon, measured from 0° (horizon) to +90° (zenith). | — |
| **Solar zenith angle** | Complement of altitude: 90° − altitude. 0° at the zenith, 90° at the horizon. | zenith = 90° − altitude |
| **Solar depression** | Angle 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:

| Option | Zenith angle | Altitude threshold | Description |
|---|---|---|---|
| `:geometric` | 90°50′ | −0.8333° | **Standard sunrise/sunset.** Upper limb of the Sun appears to touch the horizon after accounting for standard atmospheric refraction. |
| `:civil` | 96° | −6° | **Civil twilight.** Enough light for outdoor activities without artificial lighting. The horizon is clearly visible. |
| `:nautical` | 102° | −12° | **Nautical twilight.** The horizon is faintly visible at sea. Bright stars are visible for celestial navigation. |
| `:astronomical` | 108° | −18° | **Astronomical twilight.** The sky is dark enough for astronomical observations of faint objects. |
| Custom number N | 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:

* Download `de440s.bsp` from
  https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de440s.bsp
  to the `priv` directory.

# `sun_above_horizon?`

```elixir
@spec sun_above_horizon?(Astro.location(), number(), keyword()) :: boolean()
```

Returns whether the Sun is above the rise/set horizon at a given instant.

This is used to distinguish polar day from polar night on dates where the
Sun neither rises nor sets within the local calendar day. During such a
period the Sun stays above (polar day) or below (polar night) the horizon
for the entire 24 hours, so any instant within the day yields the correct
answer.

### Arguments

* `location` is the location as a `{longitude, latitude}` tuple,
  a `t:Geo.Point.t/0` or a `t:Geo.PointZ.t/0`.

* `moment` is a moment (float Gregorian days since 0000-01-01).

* `options` is a keyword list of options.

### Options

* `:solar_elevation` is the threshold, as for `sunrise/3` and
  `sunset/3`. The default is `:geometric`. An unknown value raises
  `ArgumentError`, since a predicate has no error to return.

### Returns

* `true` if the Sun's upper limb is at or above the event threshold,
  otherwise `false`.

### Examples

    iex> sydney = {151.20666584, -33.8559799094}
    iex> Astro.Solar.SunRiseSet.sun_above_horizon?(sydney, Astro.Time.date_time_to_moment(~U[2019-12-04 02:00:00Z]))
    true
    iex> Astro.Solar.SunRiseSet.sun_above_horizon?(sydney, Astro.Time.date_time_to_moment(~U[2019-12-04 14:00:00Z]))
    false

# `sunrise`

```elixir
@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 `t:Geo.Point.t/0`,
  or a `t: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` is the threshold that defines the event.

  * `:geometric`, the default, is the standard sunrise, when the upper
    limb of the Sun appears to touch the horizon: a zenith angle of
    90°50′, allowing 34′ for refraction and 16′ for the Sun's
    semi-diameter.

  * `:civil` puts the centre of the Sun 6° below the horizon, the
    boundary of civil twilight.

  * `:nautical` puts it 12° below the horizon, the boundary of nautical
    twilight.

  * `:astronomical` puts it 18° below the horizon, the boundary of
    astronomical twilight.

  * A number is a zenith angle in degrees, where 90 is the geometric
    horizon with no correction for refraction or semi-diameter.

* `: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, `:configured`, is
  the application's configured time zone database.

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

### Returns

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

* `{:error, :no_time}` if there is no sunrise on the requested date at
  the given location, as in polar night or midnight sun.

* `{: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 the time zone cannot be
  resolved from the location.

* `{:error, :tz_world_data_not_installed}` if `:tz_world` is a
  dependency but its time zone data has not been downloaded. Run
  `mix tz_world.update` to install it.

* `{: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.

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

### 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`

```elixir
@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 `t:Geo.Point.t/0`,
  or a `t: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` is the threshold that defines the event.

  * `:geometric`, the default, is the standard sunset, when the upper
    limb of the Sun appears to touch the horizon: a zenith angle of
    90°50′, allowing 34′ for refraction and 16′ for the Sun's
    semi-diameter.

  * `:civil` puts the centre of the Sun 6° below the horizon, the
    boundary of civil twilight.

  * `:nautical` puts it 12° below the horizon, the boundary of nautical
    twilight.

  * `:astronomical` puts it 18° below the horizon, the boundary of
    astronomical twilight.

  * A number is a zenith angle in degrees, where 90 is the geometric
    horizon with no correction for refraction or semi-diameter.

* `: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, `:configured`, is
  the application's configured time zone database.

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

### Returns

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

* `{:error, :no_time}` if there is no sunset on the requested date at
  the given location, as in polar night or midnight sun.

* `{: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 the time zone cannot be
  resolved from the location.

* `{:error, :tz_world_data_not_installed}` if `:tz_world` is a
  dependency but its time zone data has not been downloaded. Run
  `mix tz_world.update` to install it.

* `{: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.

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

### 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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
