Utilities

SolarPosition.jl provides several utility functions for common solar position-related calculations, such as determining solar transit, sunrise, and sunset times for a given location and date.

For now, only the analytical SPA algorithm is supported for these utility functions. This algorithm was developed by Jean Meeus in his book Astronomical Algorithms [Mee91] and is widely used in the astronomical community for solar position calculations, particularly because it is relatively accurate but computationally efficient and simple to implement.

Sunrise, Sunset, and Solar Noon

The module exports the following convenience functions:

Info

Solar noon is defined as the time when the sun reaches its highest elevation for the day. It is also referred to as the solar transit, the point the sun crosses the local meridian. The next/previous sunrise and sunset functions allow you to find the next or previous occurrence of these events relative to a given input time.

Example: Sunrise and Sunset

As an example, let's calculate the solar noon, sunrise, and sunset times for the Van Gogh museum in Amsterdam on June 21, 2023:

using SolarPosition, Dates, TimeZonestz_amsterdam = TimeZone("Europe/Brussels")obs = Observer(52.35888, 4.88185, 100.0)# Summer solsticezdt = ZonedDateTime(2023, 6, 21, 12, 0, tz_amsterdam)events = transit_sunrise_sunset(obs, zdt)println("Solar noon: ", events.transit)println("Sunrise:    ", events.sunrise)println("Sunset:     ", events.sunset)
Solar noon: 2023-06-21T13:42:15+02:00
Sunrise:    2023-06-21T05:18:05+02:00
Sunset:     2023-06-21T22:06:24+02:00

We can confirm these results by consulting timeanddate.com for our location and date.

Another option is to use the next_sunrise and next_sunset functions to return the sunrise and sunset times directly:

using SolarPosition, Dates, TimeZonesnext_sunrise_time = next_sunrise(obs, zdt)next_sunset_time = next_sunset(obs, zdt)println("Next Sunrise: ", next_sunrise_time)println("Next Sunset:  ", next_sunset_time)
Next Sunrise: 2023-06-22T05:18:19+02:00
Next Sunset:  2023-06-21T22:06:24+02:00

Plotting the Solar Altitude

To visualize the solar altitude throughout the day, we can use the solar_position function to compute the solar positions at regular intervals and plot the results. We will make use of next_sunrise and next_sunset to mark the sunrise and sunset times on the plot.

Visualization
using CairoMakie# Define time range for the entire day (every 5 minutes)start_time = ZonedDateTime(2023, 6, 21, 0, 0, tz_amsterdam)end_time = ZonedDateTime(2023, 6, 21, 23, 59, tz_amsterdam)times = collect(start_time:Minute(5):end_time)# Compute solar positions for all timespositions = solar_position(obs, times)# Get key eventsevents = transit_sunrise_sunset(obs, zdt)sunrise_elev = solar_position(obs, events.sunrise).elevationsunset_elev = solar_position(obs, events.sunset).elevationtransit_elev = solar_position(obs, events.transit).elevation# Convert times to hours for plottingtimes_hours = hour.(times) .+ minute.(times) ./ 60# Create the plot with stylingfig = Figure(backgroundcolor=:transparent, textcolor="#f5ab35", size=(800, 400))ax = Axis(fig[1, 1],    xlabel="Time (hours)",    ylabel="Solar Altitude (°)",    title="Solar Altitude - Amsterdam, June 21, 2023",    backgroundcolor=:transparent,    xticks=0:3:24)# Plot the solar altitude curvelines!(ax, times_hours, positions.elevation,    linewidth=2, color="#f5ab35")# Add vertical markers and labels for eventssunrise_hour = hour(events.sunrise) + minute(events.sunrise) / 60transit_hour = hour(events.transit) + minute(events.transit) / 60sunset_hour = hour(events.sunset) + minute(events.sunset) / 60vlines!(ax, sunrise_hour, linestyle=:dash, color=:gold, linewidth=1.5)text!(ax, sunrise_hour, sunrise_elev + 5,    text=Dates.format(events.sunrise, "HH:MM"),    align=(:center, :bottom), color=:gold, fontsize=12)vlines!(ax, transit_hour, linestyle=:dash, color=:red, linewidth=1.5)text!(ax, transit_hour, transit_elev + 5,    text=Dates.format(events.transit, "HH:MM"),    align=(:center, :bottom), color=:red, fontsize=12)vlines!(ax, sunset_hour, linestyle=:dash, color=:purple, linewidth=1.5)text!(ax, sunset_hour, sunset_elev + 5,    text=Dates.format(events.sunset, "HH:MM"),    align=(:center, :bottom), color=:purple, fontsize=12)# Add horizontal line at horizonhlines!(ax, 0, linestyle=:dash, color=:gray, linewidth=1)
Makie.HLines{Tuple{Int64}}
Example block output

As you can see, the sunrise and sunset events occur slightly below the horizon line (0° elevation). This is due to atmospheric refraction effects, which cause the sun to appear slightly higher in the sky when it is near the horizon.

Sun Graph

We can plot the sunrise, sunset, and solar noon times on a sun graph to visualize the number of daylight hours throughout the day for our location in an entire year.

Visualization
# Generate dates for the entire year 2023 (every day)year_start = ZonedDateTime(2023, 1, 1, 12, 0, tz_amsterdam)year_dates = [year_start + Day(i) for i in 0:364]# Calculate sunrise, sunset, and solar noon for each daysunrise_times = Float64[]sunset_times = Float64[]solar_noon_times = Float64[]for date in year_dates    events = transit_sunrise_sunset(obs, date)    # Convert to hours since midnight    push!(sunrise_times, hour(events.sunrise) + minute(events.sunrise) / 60)    push!(sunset_times, hour(events.sunset) + minute(events.sunset) / 60)    push!(solar_noon_times, hour(events.transit) + minute(events.transit) / 60)end# Calculate daylight hours for each daydaylight_hours = sunset_times .- sunrise_times# Find solstices (longest and shortest days)summer_solstice_idx = argmax(daylight_hours)winter_solstice_idx = argmin(daylight_hours)# Create day of year array for x-axisday_of_year = 1:365# Create the sun graphfig = Figure(backgroundcolor=:transparent, textcolor="#f5ab35", size=(900, 500))ax = Axis(fig[1, 1],    xlabel="Month",    ylabel="Time (hours)",    title="Sun Graph 2023 - Amsterdam (52.36°N, 4.88°E)",    backgroundcolor=:transparent,    yticks=0:2:24,    xticks=(        [1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335],        ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]    ))xlims!(ax, 1, 365)# Fill night time (top part: midnight to sunrise)band!(ax, day_of_year, sunrise_times, fill(24.0, length(day_of_year)),    color=(:darkslategray, 0.8))# Fill night time (bottom part: sunset to midnight)band!(ax, day_of_year, fill(0.0, length(day_of_year)), sunset_times,    color=(:darkslategray, 0.8))# Fill daylight timeband!(ax, day_of_year, sunrise_times, sunset_times,    color=(:lightblue, 0.6))# Plot solar noon linelines!(ax, day_of_year, solar_noon_times,    color=:red, linewidth=2, label="Solar Noon")# Mark solsticesvlines!(ax, summer_solstice_idx, linestyle=:dash, color=:orange, linewidth=2)text!(ax, summer_solstice_idx, 12,    text="Summer\nSolstice",    align=(:center, :bottom), color=:orange, fontsize=10, rotation=π/2)vlines!(ax, winter_solstice_idx, linestyle=:dash, color=:steelblue, linewidth=2)text!(ax, winter_solstice_idx, 12,    text="Winter\nSolstice",    align=(:center, :bottom), color=:steelblue, fontsize=10, rotation=π/2)ylims!(ax, 0, 24)
┌ Warning: Assignment to `events` in soft scope is ambiguous because a global variable by the same name exists: `events` will be treated as a new local. Disambiguate by using `local events` to suppress this warning or `global events` to assign to the existing global variable.
└ @ utilities.md:154
Example block output

We also marked the summer and winter solstices, which correspond to the longest and shortest days of the year, respectively.

Note

Note the two discontinuities in March and October. These are due to the start and end of Daylight Saving Time (DST). The DST period starts on the last Sunday of March and ends on the last Sunday of October. Clocks are set one hour ahead in March, meaning sunrise and sunset times are later by one hour. This effect is reversed in October when clocks are set back one hour. This effectively turns the UTC offset from +1 hour to +2 hours during the DST period.

Date, DateTime and ZonedDateTime

The utility functions accept three different time input types:

  • ZonedDateTime — Recommended for timezone-aware calculations. The functions will return results in the same timezone as the input.
  • DateTime — Assumed to be in UTC. Results will be returned as DateTime in UTC.
  • Date — Assumed to be in UTC. Results will be returned as DateTime in UTC.
Tip

When working with specific geographic locations, it's best to use ZonedDateTime to ensure results are in the local timezone and to correctly handle Daylight Saving Time transitions.

Here's an example showing the different input types:

# Using ZonedDateTime (recommended - timezone aware)zdt = ZonedDateTime(2023, 6, 21, 12, 0, tz_amsterdam)events_zdt = transit_sunrise_sunset(obs, zdt)println("ZonedDateTime input:")println("  Sunrise: ", events_zdt.sunrise)# Using DateTime (assumed UTC at 00:00)dt = DateTime(2023, 6, 21, 12, 0)events_dt = transit_sunrise_sunset(obs, dt)println("\nDateTime input (UTC):")println("  Sunrise: ", events_dt.sunrise)# Using Date (assumed UTC at 00:00)d = Date(2023, 6, 21)events_d = transit_sunrise_sunset(obs, d)println("\nDate input (UTC 00:00):")println("  Sunrise: ", events_d.sunrise)
ZonedDateTime input:
  Sunrise: 2023-06-21T05:18:05+02:00

DateTime input (UTC):
  Sunrise: 2023-06-21T03:18:05

Date input (UTC 00:00):
  Sunrise: 2023-06-21T03:18:05

Note that DateTime and Date inputs produce results in UTC, while ZonedDateTime preserves the input timezone. For Amsterdam in summer, the local time is UTC+2 (CEST), which explains the 2-hour difference in the sunrise times shown above.

Forward looking functions

SolarPosition.Utilities.next_sunrise — Function
next_sunrise(
    obs::Observer,
    dt::Dates.DateTime
) -> Dates.DateTime
next_sunrise(
    obs::Observer,
    dt::Dates.DateTime,
    alg::SolarAlgorithm
) -> Dates.DateTime

Calculate the next sunrise after a given DateTime at an Observer location.

Arguments

  • obs: Observer location
  • dt: DateTime to start searching from
  • alg: Solar algorithm to use (default: SPA())

Returns

  • DateTime of the next sunrise after dt
source
SolarPosition.Utilities.next_sunset — Function
next_sunset(
    obs::Observer,
    dt::Dates.DateTime
) -> Dates.DateTime
next_sunset(
    obs::Observer,
    dt::Dates.DateTime,
    alg::SolarAlgorithm
) -> Dates.DateTime

Calculate the next sunset after a given DateTime at an Observer location.

Arguments

  • obs: Observer location
  • dt: DateTime to start searching from
  • alg: Solar algorithm to use (default: SPA())

Returns

  • DateTime of the next sunset after dt
source
SolarPosition.Utilities.next_solar_noon — Function
next_solar_noon(
    obs::Observer,
    dt::Dates.DateTime
) -> Dates.DateTime
next_solar_noon(
    obs::Observer,
    dt::Dates.DateTime,
    alg::SolarAlgorithm
) -> Dates.DateTime

Calculate the solar noon (transit) time for a given DateTime at an Observer location. If the solar noon for the current day has already passed, returns the solar noon for the next day.

Arguments

  • obs: Observer location
  • dt: DateTime to start searching from
  • alg: Solar algorithm to use (default: SPA())

Returns

  • DateTime of the next solar noon (transit) after dt
source

Backward looking functions

SolarPosition.Utilities.previous_sunrise — Function
previous_sunrise(
    obs::Observer,
    dt::Dates.DateTime
) -> Dates.DateTime
previous_sunrise(
    obs::Observer,
    dt::Dates.DateTime,
    alg::SolarAlgorithm
) -> Dates.DateTime

Calculate the previous sunrise before a given DateTime at an Observer location.

Arguments

  • obs: Observer location
  • dt: DateTime to start searching from
  • alg: Solar algorithm to use (default: SPA())

Returns

  • DateTime of the previous sunrise before dt
source
SolarPosition.Utilities.previous_sunset — Function
previous_sunset(
    obs::Observer,
    dt::Dates.DateTime
) -> Dates.DateTime
previous_sunset(
    obs::Observer,
    dt::Dates.DateTime,
    alg::SolarAlgorithm
) -> Dates.DateTime

Calculate the previous sunset before a given DateTime at an Observer location.

Arguments

  • obs: Observer location
  • dt: DateTime to start searching from
  • alg: Solar algorithm to use (default: SPA())

Returns

  • DateTime of the previous sunset before dt
source
SolarPosition.Utilities.previous_solar_noon — Function
previous_solar_noon(
    obs::Observer,
    dt::Dates.DateTime
) -> Dates.DateTime
previous_solar_noon(
    obs::Observer,
    dt::Dates.DateTime,
    alg::SolarAlgorithm
) -> Dates.DateTime

Calculate the previous solar noon (transit) before a given DateTime at an Observer location.

Arguments

  • obs: Observer location
  • dt: DateTime to start searching from
  • alg: Solar algorithm to use (default: SPA())

Returns

  • DateTime of the previous solar noon (transit) before dt
source

Docs

SolarPosition.Utilities.transit_sunrise_sunset_seconds — Function
transit_sunrise_sunset_seconds(
    obs::Observer{T<:Real},
    dt::Dates.DateTime
) -> TransitSunriseSunset{T} where T<:Real
transit_sunrise_sunset_seconds(
    obs::Observer{T<:Real},
    dt::Dates.DateTime,
    alg::SolarAlgorithm
) -> TransitSunriseSunset{T} where T<:Real

Calculate the sun transit, sunrise, and sunset as seconds since midnight UTC.

transit_sunrise_sunset rounds to a whole second in order to return a DateTime. This variant keeps the event times at the observer's element type instead, so sub-second precision survives, and so does anything the element type carries beyond a value. A Measurement observer therefore yields event times with an uncertainty, and a dual-valued observer yields differentiable ones.

Arguments

  • obs: Observer location
  • dt: date of interest, any time of day is ignored
  • alg: Solar algorithm to use (default: SPA())

Returns

  • TransitSunriseSunset{T} whose fields are seconds since midnight UTC
source
SolarPosition.Utilities.TransitSunriseSunset — Type
struct TransitSunriseSunset{T}

Struct to hold the results of sun transit, sunrise, and sunset calculations.

The datetime fields are in UTC unless a TimeZone is provided, in which case they are converted to that timezone assuming the input DateTime was in UTC.

Constructors

TransitSunriseSunset{DateTime}(    transit::DateTime,    sunrise::DateTime,    sunset::DateTime,    ::Nothing,)TransitSunriseSunset{ZonedDateTime}(    transit::DateTime,    sunrise::DateTime,    sunset::DateTime,    tz::TimeZone,)

Fields

  • transit::Any

  • sunrise::Any

  • sunset::Any

source