Astro - astronomy calculations (sunrise, sunset, moonrise, moonset, moon phase equinox, solstice, ...)

Please say hi to a new lib, Astro that aims to deliver easy-to-consume astronomy calculations of practical use. For now it only calculates sunrise and sunset. In testing its precision is within 1 minute of the results returns from DateAndTime.com.

It makes use of tz_world to dereference geo coordinates to a timezone and tzdata for time zone conversions.

This lib started our as a requirement to support certain solar, lunar and lunisolar calendars but it has wider applicability.

Examples

# Sunrise in Sydney on December 4th
iex> Astro.sunrise({151.20666584, -33.8559799094}, ~D[2019-12-04])
{:ok, #DateTime<2019-12-04 05:37:00.000000+11:00 AEDT Australia/Sydney>}

# Sunset in Sydney on December 4th
iex> Astro.sunset({151.20666584, -33.8559799094}, ~D[2019-12-04])
{:ok, #DateTime<2019-12-04 19:53:00.000000+11:00 AEDT Australia/Sydney>}

# Sunset in the town of Alert in Nunavut, Canada
# ...doesn't exist since there is no sunset in summer
iex> Astro.sunset({-62.3481, 82.5018}, ~D[2019-07-01])
{:error, :no_time}

# ...or sunrise in winter
iex> Astro.sunrise({-62.3481, 82.5018}, ~D[2019-12-04])
{:error, :no_time}

Next steps (as of 0.5.0)

  • Sunrise
  • Sunset
  • Solstice
  • Equinox
  • Moon phase
  • Moon rise
  • Moon set
39 Likes

New version 0.2.0 is now out:

Enhancements

  • Add Astro.equinox/2and Astro.solstice/2 to calculate solstices and equinoxes for a year. From these can be derived the seasons.

  • Add Astro.Time.datetime_from_julian_days/1

  • Add Astro.Time.utc_datetime_from_terrestrial_datetime/1

Now on to lunar calculations (phase, moonrise, moonset)… And to implement the Persian calendar (which is a Solar based calendar).

5 Likes

New version 0.3.0 is now out:

Change in behaviour

  • Seconds are no longer truncated to zero when calculating datetimes and durations

Enhancements

  • Add Astro.solar_noon/2 to return the true solar noon for a location and date

  • Add Astro.hours_of_daylight/2 to return hours, minutes and seconds as a Time.t() representing the number of daylight hours for a give location and date

  • Add Astro.sun_apparent_longitude/1 to return the apparent solar longitude on a given date. The result, a number of degrees between 0 and 360, can be used to determine the seasons.

This completes the work required to support the Persian calendar which will be landing on hex.pm in the next couple of days.

2 Likes

Astro version 0.5.0 is now out. The release adds functions that calculate the phase of the moon which, apart from anything else, is needed to calculate the Chinese calendar. Please note that Elixir 1.11 or later is required.

Bug Fixes

  • Updates documentation to be clear about installation and setup requirements for tz_world

  • Fixes test data for São Paulo now that it no longer uses DST

  • Ensure :astro is started in test mode

Enhancements

This primary focus of this release is to add lunar calculations for moon phase.

  • Adds Astro.date_time_new_moon_before/1

  • Adds Astro.date_time_new_moon_at_or_after/1

  • Adds Astro.lunar_phase_at/1

  • Adds Astro.date_time_lunar_phase_at_or_before/2

  • Adds Astro.date_time_lunar_phase_at_or_after/2

8 Likes

Astro version 0.6.0 is now out. This version avoids requiring some dependencies which makes the footprint smaller and compilation much faster. I have been using it to support the upcoming Chinese calendar with all tests passing so I consider the basic solar and lunar algorithms to be in solid shape.

Please note that Elixir 1.11 or later is required.

Roadmap

Roadmap to version 1.0 is to add Astro.moonrise/2 and Astro.moonset/2.

Additional development proposals or suggestions are very welcome.

Bug Fixes

  • Fix Astro.Math.atan_r/2

  • Fix Astro.Earth.ephemeris/1

Breaking changes

  • Change Astro.Time.date_time_{from, to}_iso_days/1 to Astro.Time.date_time_{from, to}_moment/1

Enhancements

  • Remove dependency on ex_cldr_calendar and jason

  • Add Astro.sun_position_at/1 which is a public API for returning the right ascension, declination and radius (distance to the sun) at a given date/time.

  • Add Astro.moon_position_at/1 which is a public API for returning the right ascension, declination and radius (distance to the moon) at a given date/time.

  • Add Astro.illuminated_fraction_of_moon_at/1 to return the fraction of the moon that is lit at a given date/time.

11 Likes

A small update resulting in Astro version 0.9.0 that adds Astro.lunar_phase_emoji/1 to return the phase of the moon as an emoji. For example:

iex> Astro.lunar_phase_emoji 0
"🌑"
iex> Astro.lunar_phase_emoji 45
"🌒"
iex> Astro.lunar_phase_emoji 90
"🌓"
iex> Astro.lunar_phase_emoji 135
"🌔"
iex> Astro.lunar_phase_emoji 180
"🌕"
iex> Astro.lunar_phase_emoji 245
"🌖"
iex> Astro.lunar_phase_emoji 270
"🌗"
iex> Astro.lunar_phase_emoji 320
"🌘"
iex> Astro.lunar_phase_emoji 360
"🌑"

iex> ~U[2021-08-22 12:01:02.170362Z]
...> |> Astro.lunar_phase_at()
...> |> Astro.lunar_phase_emoji()
"🌕"
16 Likes

@habeebkhan, happy to help if I can but I’m not sure what you’re asking? Can you describe your use case for me?

Man, there are so many cool things built with Elixir.

1 Like

I’ve released Astro 1.0 almost exactly a year after the last published version. This indicates API stability for existing functions (stable in most cases for at least 3 years). There is only one new function in this release:

Enhancements

  • Release 1.0. This library was started four years ago and the public API has been stable for at least three of those years.

  • Adds Astro.sun_azimuth_altitude/2. Thanks to @kimlai for the suggestion. Closes #3.

16 Likes

Nice lib! :glowing_star: Is it possible to use it with tz instead of tzdata?

Edit: never mind, I should have checked the docs :slight_smile: Astro — Astro v1.0.0

1 Like

I’ve published Astro version 1.0.2. The primary fix is to calculate astronomical, nautical and civil sunrise/sunset correctly. The changelog entry is:

Bug Fixes

  • Fix sunrise/sunset calculations when the solar elevation isn’t 90 degrees. Thanks to @cloud8421 for the issue. Closes #5.

Elixir 1.17 support

Astro compiles and runs without warnings on Elixir 1.17.

The difference between dawn and sunrise

There are four main daily milestones in the progress from darkness to light each day.

  • Astronomical twilight is calculated when the geometric centre of the sun is 18 degrees below the horizon. This is the solar elevation where stars start to disappear agains the sky’s background. Therefore typically the time at which optical observation will stop.

  • Nautical twilight is calculated when the geometric centre of the sun is 12 degrees below the horizon. This is the time at which the horizon can be observed by eye although it is not generally considered light enough for outdoor activity. The important of seeing the horizon is understandable for mariners.

  • Civil twilight is what is generally considered to be dawn and is calculated when the geometric centre of the sun is 6 degrees below the horizon. This is the point at which outdoor activity is possible due to light from the sun below the horizon refracting in the atmosphere.

  • Sunrise is calculated when the upper edge of the sun breaks the horizon, adapted to account for refraction of the suns rays.

Each of these - and indeed any arbitrary solar elevation - can be calculated in Astro by:

iex> Astro.sunrise(location, date, solar_elevation: :civil | :nautical | :astronomical | :geometric | angle_in_degrees)

Example

iex> london_z = %Geo.PointZ{coordinates: {-0.1276, 51.5072, 21.0}}
iex> Astro.sunrise(london_z, ~D[2024-05-26], solar_elevation: :civil)
DateTime.new(~D[2024-05-26], ~T[04:08:37.000000], "Europe/London")
5 Likes

I’ve just published Astro 1.1.0 with the following changelog entry. Please note the breaking change to configuration (there are no breaking changes to application code).

Breaking Change

  • :tz_world is no longer a required dependency - it is now an optional dependency. This library is used to resolve a time zone name from a given latitude and longitude. When configured, it becomes the default method of resolving time zone names from a location. However it is no possible to provide alternative implementations for this resolution using the :time_zone_resolver option.

  • To retain the previous behaviour, applications should add {:tz_world, "~> 1.0"} to their dependencies.

Enhancements

  • Adds an option :time_zone_resolver to Astro.sunrise/3 and Astro.sunset/3 that is a 1-arity function that is invoked to resolve the time zone name from a given latitude and longitude. The default is to use TzWorld.timezone_at/1 if TzWorld is configured, otherwise an error is returned.

  • The default time zone database is now detected in the following order:

    1. Application.get_env(:elixir, :time_zone_database)
    2. TzData.TimeZoneDatabase if TzData is configured
    3. Tz.TimeZoneDatabase if Tz is configured

Thanks to @cloud8421 for the motivation and encouragement to get this done.

3 Likes

I’ve just published Astro 2.0. Being a new major release there are some breaking changes and some important enhancements. The biggest changes are documented below, see the changelog for complete details.

Core Algorithm Upgrade

The biggest change is a shift from NOAA/Meeus analytical polynomial series to JPL DE440s numerical ephemeris for sunrise/sunset calculations. Astro.sunrise/3 and Astro.sunset/3 now use a scan-and-bisect solver with positions computed directly from the JPL DE440s ephemeris file, and bisection tolerance tightened from 1.0s to 0.01s (sub-second precision).

Improved Delta-T

Variable ΔT based on IERS observations (1972–2025) and Meeus polynomials replaces the previous fixed value. This shifts computed times for equinoxes, solstices, and lunar phases by up to ~22 seconds vs 1.x.

New Features

  • Moonrise/moonset: Astro.moonrise/3, Astro.moonset/3, and Astro.Lunar.MoonRiseSet (fully topocentric, correcting the ~2–3 min RA-parallax error in Meeus Ch.15)
  • Astro.date_time_new_moon_nearest/1
  • Astro.Coordinates module for coordinate system conversions
  • Astro.Time.date_from_julian_days/1
  • Improved and more consistent documentation.

Accuracy

With the new numerical engine, accuracy has improved. The main comparison is with Skyfield a well-regarded Astronomy library for Python. Other comparisons are against timeanddate.com and USNO.

A more complete version of the comparison data is here.

Sunrise / Sunset

Both Astro and Skyfield use JPL DE440s ephemerides, which explains their near-exact agreement. timeanddate.com agrees with both to within ±1 minute.

Comparison Max diff Mean diff Within ±1 min
Astro vs Skyfield 7 s 3.8 s 310/310 (100%)
Astro vs timeanddate.com 61 s ~29 s 308/310 (99.4%)
timeanddate.com vs Skyfield 61 s 28.5 s 308/310 (99.4%)

Moonrise / Moonset

Both Astro and Skyfield use JPL DE440s ephemerides, which explains their near-exact agreement again. timeanddate.com agrees with both to within ±1 minute.

The ~16s mean difference against USNO is explained by two factors: USNO uses DE430 (vs the JPL DE440s that Astro uses), and USNO rounds to the nearest minute. Skyfield shows the same ~16s offset against USNO, suggesting this is an ephemeris version difference rather than an
algorithmic error.

Comparison Max diff Mean diff Within ±1 min
Astro vs Skyfield 6 s 2.5 s 240/240 (100%)
Astro vs USNO 32 s 15.5 s 67/67 (100%)
Skyfield vs USNO 35 s 15.6 s 67/67 (100%)

Migration from Astro 1.x

The public API in the Astro module is compatible with that in Astro 1.x with any differences being due to the improved numerical engine. Functions in other modules have changed and are documented in the changelog.

In Astro 2.x, the JPL ephemeris needs to be installed. A new mix astro.download_ephemeris is provided to make this easier.

What’s next?

Some of the motivation for this update was to finally deliver moonrise and moonset which are required to support the upcoming implementations of the Islamic Umm al-Qura calendar and the Hebrew calendar.

Feature requests and contributions

Astro welcomes feature requests and contributions no matter how big or small. Just head on over to the github repo.

7 Likes

I’ve published Astro version 2.5.0.

The primary goal of this release is to remove the requirement to download the full JPL DE440s ephemeris.

That’s been achieved by vendoring a subset of the JPL ephemeris for the date range 1900 to 2100 which probably meets the requirements of most consumers. Downloading the full ephemeris automatically opens the range to 1849 to 2150.

Enhancements

  • A compact ephemeris covering 1900 to 2100 now ships with the package, so Astro works immediately after installation with no download step. It is extracted from JPL’s DE440s kernel and yields identical results; mix astro.download_ephemeris remains available for the full 1849 to 2150 range and takes precedence when present.

  • mix astro.build_ephemeris builds a compact ephemeris from a JPL DE-series kernel, keeping only the Sun, Moon and Earth segments over a chosen span of years at roughly 42 KB per year. Options include --from, --to, --source and --keep-earth.

3 Likes