Everything Astro derives from the position of the sun: rise and set, the twilights, solar noon, the length of the day, the equinoxes and solstices, and solar eclipses.
Examples use Sydney, {151.20666584, -33.8559799094}, and assume :tz_world is running so times come back in the zone at the location. See Getting started if that is not set up.
Sunrise and sunset
iex> {:ok, datetime} = Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04])
iex> datetime
#DateTime<2019-12-04 05:37:06.206432+11:00 AEDT Australia/Sydney>
iex> {:ok, datetime} = Astro.sunset({151.20666584, -33.8559799094}, ~D[2019-12-04])
iex> datetime
#DateTime<2019-12-04 19:53:18.155710+11:00 AEDT Australia/Sydney>Both return {:error, :no_time} where the event does not happen — inside the Arctic or Antarctic circles around the solstices:
iex> Astro.sunset({-62.3481, 82.5018}, ~D[2019-07-01])
{:error, :no_time}That is a real astronomical answer, not a failure.
Twilight
:solar_elevation sets how far the sun must be below the horizon to count as the event. It is measured as a zenith angle, so larger numbers mean the sun is lower and the event is earlier in the morning and later in the evening.
The default of 90.0 is not geometric sunrise. It includes the standard allowances for atmospheric refraction and the sun's apparent radius, which is what makes it match the moment the disc appears to touch the horizon.
| Value | Zenith | Meaning |
|---|---|---|
90.0 (default) | 90.0° | The disc appears on the horizon |
:civil | 96.0° | Enough natural light for most outdoor activity |
:nautical | 102.0° | Horizon just visible; stars usable for navigation |
:astronomical | 108.0° | Beyond this, astronomical observation is impractical |
iex> {:ok, datetime} = Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04], solar_elevation: :civil)
iex> datetime
#DateTime<2019-12-04 05:08:27.472363+11:00 AEDT Australia/Sydney>
iex> {:ok, datetime} = Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04], solar_elevation: :nautical)
iex> datetime
#DateTime<2019-12-04 04:33:22.755556+11:00 AEDT Australia/Sydney>
iex> {:ok, datetime} = Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04], solar_elevation: :astronomical)
iex> datetime
#DateTime<2019-12-04 03:55:18.104661+11:00 AEDT Australia/Sydney>The same option applies to sunset, where it gives the end of that twilight rather than its start:
iex> {:ok, datetime} = Astro.sunset({151.20666584, -33.8559799094}, ~D[2019-12-04], solar_elevation: :civil)
iex> datetime
#DateTime<2019-12-04 20:21:59.911087+11:00 AEDT Australia/Sydney>Any float works if you need a threshold of your own — a local religious observance, or an aviation rule:
iex> {:ok, datetime} = Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04], solar_elevation: 95.0)
iex> datetime
#DateTime<2019-12-04 05:14:05.867752+11:00 AEDT Australia/Sydney>Solar noon
The moment the sun crosses the local meridian, which is when it is highest. It is not 12:00 local time, and the gap between the two varies through the year by up to about a quarter of an hour, because the Earth's orbit is elliptical and its axis is tilted.
iex> Astro.solar_noon({151.20666584, -33.8559799094}, ~D[2019-12-04])
{:ok, ~U[2019-12-04 01:45:04.043588Z]}Length of the day
Two functions, differing in return type rather than in the calculation.
hours_of_daylight/2 returns a Time.t/0, which cannot exceed a day and so saturates at ~T[23:59:59] during a polar summer:
iex> Astro.hours_of_daylight({151.20666584, -33.8559799094}, ~D[2019-12-07])
{:ok, ~T[14:18:44]}duration_of_daylight/2 returns a Duration.t/0 and can therefore express a full 24 hours:
iex> Astro.duration_of_daylight({151.20666584, -33.8559799094}, ~D[2019-12-07])
{:ok, %Duration{hour: 14, minute: 18, second: 44}}Prefer duration_of_daylight/2 for anywhere inside the polar circles, where the Time version cannot tell a 24-hour day from one a second short of it.
Equinoxes and solstices
These are instants, not dates, and they are the same instant everywhere on Earth — so they are returned in UTC unless you ask for a time zone.
iex> Astro.equinox(2019, :march)
{:ok, ~U[2019-03-20 21:58:24.583335Z]}
iex> Astro.solstice(2019, :december)
{:ok, ~U[2019-12-22 04:19:24.069965Z]}:march and :september are valid for equinox/3; :june and :december for solstice/3. Naming the month rather than "spring" or "summer" avoids the hemisphere ambiguity: the December solstice is midsummer in Sydney and midwinter in London.
The civil date of the instant depends on where you are. Japan's Vernal Equinox Day is the date of the March equinox in Japan, which is a day later than the UTC date in about a third of years. The :time_zone option returns the instant in that zone:
iex> {:ok, equinox} = Astro.equinox(2019, :march, time_zone: "Asia/Tokyo")
iex> DateTime.to_date(equinox)
~D[2019-03-21]A named time zone is looked up in :time_zone_database, which defaults to the configured database (Calendar.get_time_zone_database/0). With none configured only UTC is known, and a named zone returns {:error, :utc_only_time_zone_database}.
Both are the moments the Sun's apparent longitude reaches a multiple of 90°, and work for any year: from the JPL ephemeris where it covers the year, within a second of the full VSOP87 theory, and from the analytical series otherwise.
Solar eclipses
A solar eclipse is the Moon's shadow falling on the Earth. solar_eclipse_at_or_after/1 and solar_eclipse_before/1 find one and return an Astro.Solar.Eclipse: its type (:partial, :annular, :total or :hybrid), the instant of greatest eclipse in UTC, gamma (how far the shadow's axis passes from the Earth's centre, in Earth radii) and the magnitude.
iex> {:ok, eclipse} = Astro.solar_eclipse_at_or_after(~D[2024-04-01])
iex> {eclipse.type, DateTime.truncate(eclipse.greatest, :second)}
{:total, ~U[2024-04-08 18:17:20Z]}
iex> {:ok, eclipse} = Astro.solar_eclipse_before(~D[2024-04-01])
iex> {eclipse.type, DateTime.to_date(eclipse.greatest)}
{:annular, ~D[2023-10-14]}Eclipses recur after a saros of 223 lunations, about 18 years and 11 days, and the eclipses a saros apart form a Saros series, which the :saros field numbers as NASA's catalogues do. The eclipse of 8 April 2024 is in Saros 139, and the next in the series is 18 years later:
iex> {:ok, eclipse} = Astro.solar_eclipse_at_or_after(~D[2024-04-01])
iex> eclipse.saros
139
iex> {:ok, next} = Astro.solar_eclipse_at_or_after(~D[2042-04-01])
iex> {next.saros, DateTime.to_date(next.greatest)}
{139, ~D[2042-04-20]}solar_eclipse_at/3 describes an eclipse as seen from a place: the type there, the contacts and the moment of maximum in the location's time zone, the magnitude, the fraction of the Sun covered, and the Sun's altitude at each contact. It takes the eclipse, or a date for the next one, and returns {:error, :not_visible} where nothing of it is seen.
iex> dallas = {-96.80, 32.78}
iex> {:ok, local} = Astro.solar_eclipse_at(dallas, ~D[2024-04-01], time_zone: "America/Chicago")
iex> local.type
:total
iex> DateTime.truncate(local.total_begins, :second)
#DateTime<2024-04-08 13:40:43-05:00 CDT America/Chicago>
iex> Float.round(local.magnitude, 3)
1.015
iex> Astro.solar_eclipse_at({151.20666584, -33.8559799094}, ~D[2024-04-01])
{:error, :not_visible}solar_eclipse_path/2 gives the band in which the eclipse is total or annular: the central line and the northern and southern limits, each a list of points a minute apart (the :interval option changes that), with the time in UTC and the place as a Geo.Point of {longitude, latitude}. Points on the central line also carry the path's width in kilometres, the duration of totality or annularity in seconds and the Sun's altitude.
iex> {:ok, path} = Astro.solar_eclipse_path(~D[2024-04-01])
iex> length(path.central_line)
197
iex> longest = Enum.max_by(path.central_line, & &1.duration)
iex> {longest.time, Float.round(longest.duration, 1), Float.round(longest.width, 1)}
{~U[2024-04-08 18:20:00.000Z], 268.0, 196.9}To draw the path, make each line a Geo.LineString and the band a polygon from the northern limit and the southern limit taken backwards. Near the ends of the path the limits follow the horizon, so they begin and end together and the polygon closes:
iex> {:ok, path} = Astro.solar_eclipse_path(~D[2024-04-01])
iex> central_line = %Geo.LineString{coordinates: Enum.map(path.central_line, & &1.location.coordinates)}
iex> length(central_line.coordinates)
197
iex> north = Enum.map(path.northern_limit, & &1.location.coordinates)
iex> south = Enum.map(path.southern_limit, & &1.location.coordinates)
iex> band = %Geo.Polygon{coordinates: [north ++ Enum.reverse(south) ++ [hd(north)]]}
iex> length(hd(band.coordinates))
397The path also bounds the region in which the eclipse is seen at all, as a partial eclipse. It lies within the penumbral limits, :penumbral_northern_limit and :penumbral_southern_limit, where the edge of the penumbra just grazes a place, and the :sunrise_limit, where the eclipse ends just as the Sun rises, and :sunset_limit, where it begins just as the Sun sets. Inside it, :maximum_at_sunrise and :maximum_at_sunset are the places that have their greatest eclipse on the horizon, and :first_contact and :last_contact are where and when the penumbra first touches the Earth and finally leaves it. A curve on the horizon can have two branches at once, so each of those four is a list of lines, which a map draws as a Geo.MultiLineString:
iex> {:ok, path} = Astro.solar_eclipse_path(~D[2024-04-01])
iex> DateTime.truncate(path.first_contact.time, :second)
~U[2024-04-08 15:42:14Z]
iex> DateTime.truncate(path.last_contact.time, :second)
~U[2024-04-08 20:52:21Z]
iex> lines = Enum.map(path.sunrise_limit, fn line -> Enum.map(line, & &1.location.coordinates) end)
iex> sunrise = %Geo.MultiLineString{coordinates: lines}
iex> length(sunrise.coordinates)
2A partial eclipse has no central line or umbral limits, but has its region of partial eclipse. Longitudes run from −180° to 180°, so a line that crosses the antimeridian jumps from one end of the range to the other, and a map that draws it may need to split the line there.
Position of the sun
For the geometry itself rather than the events derived from it.
sun_azimuth_elevation/2 gives the sun's position from a place at an instant, as {azimuth, elevation} in degrees. Azimuth is measured clockwise from north; elevation is above the horizon, without refraction.
iex> {azimuth, elevation} =
...> Astro.sun_azimuth_elevation({151.20666584, -33.8559799094}, ~U[2019-12-04 02:00:00Z])
iex> {Float.round(azimuth, 4), Float.round(elevation, 4)}
{343.3379, 77.8643}sun_position_at/1 gives the geocentric celestial position as a Geo.PointZ — right ascension and declination in degrees, with distance in metres:
iex> %Geo.PointZ{coordinates: {right_ascension, declination, distance}} =
...> Astro.sun_position_at(~D[2019-12-04])
iex> {Float.round(right_ascension, 6), Float.round(declination, 6), round(distance)}
{-110.014561, -22.161617, 147457293563}sun_apparent_longitude/1 gives ecliptic longitude in degrees, which is what solar-term and zodiacal calculations are built on:
iex> Astro.sun_apparent_longitude(~D[2019-12-04])
251.51997333873487