# Solar events

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](getting_started.html) if that is not set up.

## Sunrise and sunset

```elixir
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:

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

```elixir
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:

```elixir
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:

```elixir
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.

```elixir
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 `t:Time.t/0`, which cannot exceed a day and so saturates at `~T[23:59:59]` during a polar summer:

```elixir
iex> Astro.hours_of_daylight({151.20666584, -33.8559799094}, ~D[2019-12-07])
{:ok, ~T[14:18:44]}
```

`duration_of_daylight/2` returns a `t:Duration.t/0` and can therefore express a full 24 hours:

```elixir
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.

```elixir
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:

```elixir
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.

```elixir
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:

```elixir
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.

```elixir
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.

```elixir
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:

```elixir
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))
397
```

The 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`:

```elixir
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)
2
```

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

```elixir
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:

```elixir
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:

```elixir
iex> Astro.sun_apparent_longitude(~D[2019-12-04])
251.51997333873487
```
