[ANN] SolarPosition.jl

Can it create plots like this for the position of the sun (at 34 degrees south)
Sunrise at 5:15 and sunset at 18:51

Would this work with TerminalPlots.jl?

With Makie.jl you can, sure.

Here is one for drawing analemmas: Plotting with Makie.jl · SolarPosition.jl

Do you mean UnicodePlots.jl? What do you mean by ‘this’?

[ANN] SolarPosition.jl v0.5.0

I just released SolarPosition.jl v0.5.0 :smiley:

This release is packed with new features:

New positioning algorithms

Two more positioning algorithms have been added, Iqbal, 1983 and Michalsky, 1988. With that we support all the source-provided algorithms from pvlib and solposx.

Algorithm Reference Accuracy Default Refraction Status
PSA Blanco-Muriel et al. ±0.0083° None :white_check_mark:
NOAA Global Monitoring Laboratory ±0.0167° HUGHES :white_check_mark:
Walraven Walraven, 1978 ±0.0100° None :white_check_mark:
USNO U.S. Naval Observatory ±0.0500° None :white_check_mark:
SPA Reda & Andreas, 2004 ±0.0003° SPA :white_check_mark:
Iqbal Iqbal, 1983 ±0.0100° None :white_check_mark:
Michalsky Michalsky, 1988 ±0.0100° MICHALSKY :white_check_mark:

Fast repeated evaluation using Interpolations.jl

We have a new extension for Interpolations.jl:

using Interpolations

alg = Interpolated(SPA(); tspan = (DateTime(2024, 1, 1), DateTime(2025, 1, 1)))
positions = solar_position(obs, times, alg)

This will pre-compute cubic B-splines of intermediate quantities within SPA’s algorithm, and provides an observer-agnostic interpolant that is about 10x faster to evaluate than SPA, while retaining its accuracy. This can be very convenient when you need to compute many locations and/or many time periods with decent accuracy, as you can reuse the interpolant.

Support for Observer{T <: Real}

Previously all algorithms computed internal quantities at fixed Float64. This is now fully type-generic, meaning every algorithm now computes at the element type T of Observer{T <: Real}. Now Float32, BigFloat or even Float128 from QuadMath.jl are all propagated properly. In fact any T <: Real should ‘just work’ now.

On my hardware, Float32 yields modest speedups (10-50%). I think the biggest benefit of this is that now a type like BigFloat can be used to set a reference for other algorithms and implementations, even beyond Julia itself.

Autodiff support

The generalization to T <: Real has also made it possible to compute derivatives with respect to observer coordinates and refraction model parameters:

import DifferentiationInterface as DI
import FiniteDiff

f(x) = solar_position(Observer(x[1], x[2]), dt, SPA(), NoRefraction()).elevation
g_ad = DI.gradient(f, DI.AutoForwardDiff(), [45.0, 10.0])
g_num = DI.gradient(f, DI.AutoFiniteDiff(), [45.0, 10.0])
(g_ad, maximum(abs.(g_ad .- g_num)))

# ([-0.9209770850903363, -0.2755706865807749], 1.9860470068522318e-7)

With that you can do fun things like optimize the orientation of a panel or build an axis tracker, see the docs.

(yes I know, minimization of angle of incidence is simple trig and has an analytical solution, it’s just a demo).

Uncertainty propagation with Measurements.jl

The same genericity means Measurements.jl works too (awesome package!):

using Measurements

pos = solar_position(Observer(52.35888 ± 0.01, 4.88185 ± 0.01), DateTime(2023, 6, 21, 12))
# SolPos(azimuth=188.399 ± 0.019°, 
#   elevation=60.8792 ± 0.0099°, 
#   zenith=29.1208 ± 0.0099°)

Correlations survive as well, so the sum of zenith and elevation has no uncertainty at all:

pos.elevation + pos.zenith
# 90.0 ± 0.0

With this we can answer interesting questions like, what is the impact of GPS accuracy on solar position tracking? (To meet PSA’s ±0.0083°, the margins for azimuth and elevation are 300 m and 920 m. So coordinates are almost never the limiting factor. For SPA the requirements are about 10x stricter.)

Sunrise and sunset at the observer’s element type

There is no obvious way to propagate uncertainty through a DateTime, so I added transit_sunrise_sunset_seconds which returns the event as seconds since midnight UTC:

transit_sunrise_sunset_seconds(Observer(52.35888 ± 0.01, 4.88185 ± 0.01), Date(2023, 6, 21))
# TransitSunriseSunset{Measurement{Float64}}(
# 42134.7 ± 2.4, 11885.5 ± 4.3, 72384.2 ± 4.3)

As a sidenote it also makes event times differentiable, if you ever have a need for that (I can’t think of one right now :slight_smile: ).

TimeZones.jl is now an extension

ZonedDateTime support moved to a package extension. It still works exactly as before. You always needed to import TimeZones to construct a ZonedDateTime anyway, so in practice nothing changes. This also sets up JuliaC support for later, because currently we can’t --trim with TimeZones.jl.

ModelingToolkit.jl v11 support

This release is also compatible with ModelingToolkit.jl v11. The MTK extension gives you a symbolic SolarPositionBlock. This SolarPositionBlock is a component whose azimuth, elevation and zenith are time-varying outputs you can wire into any other system:

using SolarPosition, ModelingToolkit, OrdinaryDiffEq

@named sun = SolarPositionBlock()
sys = mtkcompile(sun)

prob = ODEProblem(sys, [
    sys.observer  => Observer(51.5027, -0.1778, 15.0),
    sys.t0        => DateTime(2024, 6, 21, 12),
    sys.algorithm => PSA(),
    sys.refraction => NoRefraction(),
], (0.0, 86400.0))

sol = solve(prob; saveat = 3600.0)
sol[sys.elevation]

There are two example usecases in the guide: composition with a solar panel power model and a building thermal model with solar gains. If you need fast and accurate simulations, SolarPositionBlock also works beautifully with Interpolated!