Getting Started

In this tutorial, we introduduce the basics of using SolarPosition.jl to calculate solar positions.

First, we need to import the SolarPosition.jl package along with some supporting packages which we need for handling dates and time zones. We also load DataFrames.jl because it makes it easy to work with tabular data.

Info

The DateTime type in Julia's standard library does not contain time zone information. When using DateTime, it is assumed to be in UTC. Although not necessary, it is safer to work with time zone-aware ZonedDateTime from the TimeZones.jl package.

# mandatory
using SolarPosition
using Dates

# supporting packages
using TimeZones
using DataFrames

Defining a location

We can observe the sun from anywhere on earth. To define an observer location, we use the Observer struct, which takes latitude, longitude, and optionally altitude (in meters) as arguments.

obs = Observer(52.35888, 4.88185, 100.0)  # Van Gogh Museum, Amsterdam
Observer(latitude=52.35888°, longitude=4.88185°, altitude=100.0m)

Computing the solar vector

Finally, we can calculate the solar position for a specific date and time using the solar_position function. The time should be provided as a ZonedDateTime to ensure correct handling of time zones.

tz = TimeZone("Europe/Brussels")
zdt = ZonedDateTime(2023, 6, 21, 12, 0, 0, tz)  # Summer solstice noon
position = solar_position(obs, zdt)
SolPos(azimuth=136.1908215897601°, elevation=55.13208390809107°, zenith=34.86791609190893°)

Choosing a Solar Position Algorithm

By default, solar_position uses the PSA (Plataforma Solar de Almería) algorithm, which has a decent tradeoff between complexity and accuracy. You can choose other algorithms as described in the Solar Positioning Algorithms section.

First, we repeat the previous calculation using the default PSA algorithm:

position_psa = solar_position(obs, zdt, PSA())
SolPos(azimuth=136.1908215897601°, elevation=55.13208390809107°, zenith=34.86791609190893°)

Next, we compute the solar position using the NOAA algorithm:

position_noaa = solar_position(obs, zdt, NOAA())
ApparentSolPos(azimuth=136.18912784953108°, elevation=55.133206173835674°, zenith=34.866793826164326°,
    apparent_elevation=55.144444345946475°, apparent_zenith=34.855555654053525°

As you can see, the results are very similar. With a claimed accuracy of ±0.0083° for PSA and ±0.0167° for NOAA, the differences should be small:

delta_azimuth = abs(position_psa.azimuth - position_noaa.azimuth)
delta_elevation = abs(position_psa.elevation - position_noaa.elevation)
println("Difference in Azimuth: $(round(delta_azimuth, digits=4))°")
println("Difference in Elevation: $(round(delta_elevation, digits=4))°")
Difference in Azimuth: 0.0017°
Difference in Elevation: 0.0011°

Whether the differences are significant depends on your application and required accuracy.

Computing multiple timestamps simultaneously

For more demanding applications, it is often necessary to compute solar positions for multiple timestamps at once. SolarPosition.jl supports this by passing a vector of ZonedDateTime or DateTime objects to the solar_position function. Here, we demonstrate this by calculating solar positions for every hour of a full year.

# generate hourly timestamps for a whole year
dts = collect(ZonedDateTime(DateTime(2023), tz):Hour(1):ZonedDateTime(DateTime(2024), tz))
positions = solar_position(obs, dts)
8761-element StructArray(::Vector{Float64}, ::Vector{Float64}, ::Vector{Float64}) with eltype SolPos{Float64}:
 SolPos(azimuth=339.9139651348899°, elevation=-59.516632497943725°, zenith=149.51663249794373°)
 SolPos(azimuth=7.646701924575278°, elevation=-60.51529149754965°, zenith=150.51529149754964°)
 SolPos(azimuth=33.77454046955175°, elevation=-57.24826008871935°, zenith=147.24826008871935°)
 SolPos(azimuth=54.726212423433985°, elevation=-50.83991842657765°, zenith=140.83991842657764°)
 SolPos(azimuth=71.0627278720897°, elevation=-42.69054124991213°, zenith=132.69054124991212°)
 SolPos(azimuth=84.48676815893062°, elevation=-33.75262519360256°, zenith=123.75262519360257°)
 SolPos(azimuth=96.34620993304598°, elevation=-24.609274875219057°, zenith=114.60927487521906°)
 SolPos(azimuth=107.54782743459084°, elevation=-15.662602359963579°, zenith=105.66260235996357°)
 SolPos(azimuth=118.71570411333808°, elevation=-7.2507276199329365°, zenith=97.25072761993293°)
 SolPos(azimuth=130.29762502910458°, elevation=0.29020843900144433°, zenith=89.70979156099855°)
 ⋮
 SolPos(azimuth=224.18912990939398°, elevation=3.260699920923798°, zenith=86.7393000790762°)
 SolPos(azimuth=236.06457094625256°, elevation=-3.7583471248153963°, zenith=93.7583471248154°)
 SolPos(azimuth=247.37238100897753°, elevation=-11.809776441149609°, zenith=101.80977644114961°)
 SolPos(azimuth=258.49365667002763°, elevation=-20.547744929283503°, zenith=110.54774492928351°)
 SolPos(azimuth=269.9637073278627°, elevation=-29.640398102549234°, zenith=119.64039810254924°)
 SolPos(azimuth=282.5429720048338°, elevation=-38.72506027778869°, zenith=128.72506027778869°)
 SolPos(azimuth=297.3622699585723°, elevation=-47.31978544642601°, zenith=137.319785446426°)
 SolPos(azimuth=316.0254562973933°, elevation=-54.664978115106°, zenith=144.664978115106°)
 SolPos(azimuth=339.9570276073531°, elevation=-59.541354504666444°, zenith=149.54135450466646°)
Info

The returned datastructure is a StructArray from the StructArrays.jl package, which behaves similarly to a vector of SolPos structs but is more convenient to work with.

The returned StructArray can be easily converted to a DataFrame for inspection:

df = DataFrame(positions)
df.datetime = dts  # add datetime information
first(df, 5)  # show first 5 entries
5×4 DataFrame
Rowazimuthelevationzenithdatetime
Float64Float64Float64ZonedDat…
1339.914-59.5166149.5172023-01-01T00:00:00+01:00
27.6467-60.5153150.5152023-01-01T01:00:00+01:00
333.7745-57.2483147.2482023-01-01T02:00:00+01:00
454.7262-50.8399140.842023-01-01T03:00:00+01:00
571.0627-42.6905132.6912023-01-01T04:00:00+01:00

Broadcasting Over Multiple Locations

Thanks to Julia's broadcasting syntax it is trivial to calculate solar positions for multiple locations simultaneously. This can be useful for example when analyzing solar irradiance over a geographic region with multiple measurement stations.

# Create observers at different latitudes
observers = Observer.([10.0, 20.0, 30.0], 10.0)

# Calculate solar position for all locations at a specific time
dt = DateTime(2020)
positions_broadcast = solar_position.(observers, dt)
3-element Vector{SolPos{Float64}}:
 SolPos(azimuth=147.08595651159953°, elevation=-74.2404333846635°, zenith=164.2404333846635°)
 SolPos(azimuth=111.26231927619081°, elevation=-80.88815080646037°, zenith=170.88815080646037°)
 SolPos(azimuth=52.099061930554754°, elevation=-79.220339417286°, zenith=169.220339417286°)