From de3540dc2ef89a332f1b7b63e3c4b0ce6b21a69b Mon Sep 17 00:00:00 2001 From: Stefan de Lange Date: Thu, 30 Jul 2026 23:06:17 +0200 Subject: [PATCH 1/2] Move TimeZones support into a package extension TimeZones pulls in TZJData, Downloads and LibCURL, so every user of SolarPosition paid that load cost whether or not they ever touched a ZonedDateTime. Demote it to a weak dependency and move the zoned methods into SolarPositionTimeZonesExt. The relocated methods -- solar_position[!], solar_rate, calculate_deltat, _as_utc, transit_sunrise_sunset and the next_/previous_ event helpers -- all did the same thing: convert to UTC, delegate to the DateTime method, and convert back where the result is itself a time. Nothing in them needs to live in the package proper. Signatures that existed only to name ZonedDateTime are widened to an unconstrained type parameter, so the extension's methods slot in without the package referring to the type: TransitSunriseSunset{T}, the tspan pair of Interpolated, _transit_sunrise_sunset_impl, and the OhMyThreads scheduler methods. This is invisible at the call site. A ZonedDateTime or TimeZone cannot be constructed without TimeZones loaded, which is precisely what triggers the extension. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012Ky1A6fZaCrjV4H3Fzz2u3 --- Project.toml | 3 +- ext/SolarPositionOhMyThreadsExt.jl | 7 +- ext/SolarPositionTimeZonesExt.jl | 193 +++++++++++++++++++++++++++++ src/Positioning/Positioning.jl | 65 ---------- src/Positioning/deltat.jl | 7 -- src/Positioning/interpolated.jl | 7 +- src/Utilities/Utilities.jl | 1 - src/Utilities/spa.jl | 14 +-- src/Utilities/srt.jl | 60 +-------- 9 files changed, 202 insertions(+), 155 deletions(-) create mode 100644 ext/SolarPositionTimeZonesExt.jl diff --git a/Project.toml b/Project.toml index 92a78b58..6ee98071 100644 --- a/Project.toml +++ b/Project.toml @@ -12,9 +12,9 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" [weakdeps] +TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" @@ -26,6 +26,7 @@ SolarPositionInterpolationsExt = "Interpolations" SolarPositionMakieExt = "Makie" SolarPositionModelingToolkitExt = ["ModelingToolkit", "Symbolics"] SolarPositionOhMyThreadsExt = "OhMyThreads" +SolarPositionTimeZonesExt = "TimeZones" [compat] Aqua = "0.8" diff --git a/ext/SolarPositionOhMyThreadsExt.jl b/ext/SolarPositionOhMyThreadsExt.jl index 98a83525..bc84d164 100644 --- a/ext/SolarPositionOhMyThreadsExt.jl +++ b/ext/SolarPositionOhMyThreadsExt.jl @@ -7,7 +7,6 @@ using OhMyThreads using OhMyThreads: tmap, tmap! using StructArrays: StructArrays using Dates: DateTime -using TimeZones: ZonedDateTime using DocStringExtensions: TYPEDSIGNATURES """ @@ -57,7 +56,7 @@ function SolarPosition.solar_position!( alg::SolarAlgorithm, refraction::RefractionAlgorithm, executor::OhMyThreads.Scheduler, - ) where {T <: AbstractSolPos, Z <: Union{DateTime, ZonedDateTime}} + ) where {T <: AbstractSolPos, Z} f = dt -> SolarPosition.solar_position(obs, dt, alg, refraction) tmap!(f, pos, dts; scheduler = executor) return pos @@ -108,7 +107,7 @@ function SolarPosition.solar_position( alg::SolarAlgorithm, refraction::RefractionAlgorithm, executor::OhMyThreads.Scheduler, - ) where {T <: AbstractFloat, Z <: Union{DateTime, ZonedDateTime}} + ) where {T <: AbstractFloat, Z} f = dt -> SolarPosition.solar_position(obs, dt, alg, refraction) results = tmap(f, dts; scheduler = executor) return StructArrays.StructVector(results) @@ -118,7 +117,7 @@ function SolarPosition.solar_position( obs::Observer{T}, dts::AbstractVector{Z}, executor::OhMyThreads.Scheduler, - ) where {T <: AbstractFloat, Z <: Union{DateTime, ZonedDateTime}} + ) where {T <: AbstractFloat, Z} return SolarPosition.solar_position( obs, dts, diff --git a/ext/SolarPositionTimeZonesExt.jl b/ext/SolarPositionTimeZonesExt.jl new file mode 100644 index 00000000..76dfbbc1 --- /dev/null +++ b/ext/SolarPositionTimeZonesExt.jl @@ -0,0 +1,193 @@ +""" +TimeZones support for SolarPosition.jl. + +`ZonedDateTime` inputs and timezone aware sunrise/sunset results live here rather than in +the package proper, so that loading SolarPosition does not pull in TimeZones and, through +it, TZJData, Downloads and LibCURL. This is invisible in practice: a `ZonedDateTime` or a +`TimeZone` cannot be constructed without TimeZones being loaded, which is exactly what +triggers this extension. + +Every method here converts to UTC and delegates to the `DateTime` method in the package, +converting back into the given zone where the result is itself a time. +""" +module SolarPositionTimeZonesExt + +using Dates: DateTime +using SolarPosition: Positioning, Utilities +using StructArrays: StructArrays +using TimeZones: TimeZone, ZonedDateTime, UTC, timezone + +using SolarPosition.Positioning: AbstractObserver, AbstractSolPos, Interpolated, Observer, + PSA, SolarAlgorithm, _result_eltype, calculate_deltat, result_type, solar_position, + solar_position!, solar_rate +using SolarPosition.Refraction: DefaultRefraction, RefractionAlgorithm +using SolarPosition.Utilities: SPA, TransitSunriseSunset, _transit_sunrise_sunset, + _transit_sunrise_sunset_impl, next_solar_noon, next_sunrise, next_sunset, + previous_solar_noon, previous_sunrise, previous_sunset, transit_sunrise_sunset + +# ------------------------------------------------------------------ solar position + +function Positioning.solar_position( + obs::AbstractObserver{T}, + dt::ZonedDateTime, + alg::SolarAlgorithm = PSA(), + refraction::RefractionAlgorithm = DefaultRefraction(), + ) where {T <: Real} + return solar_position(obs, DateTime(dt, UTC), alg, refraction) +end + +function Positioning.solar_position!( + pos::StructArrays.StructVector{T}, + obs::AbstractObserver, + dts::AbstractVector{ZonedDateTime}, + alg::SolarAlgorithm = PSA(), + refraction::RefractionAlgorithm = DefaultRefraction(), + ) where {T <: AbstractSolPos} + @inbounds for i in eachindex(dts, pos) + pos[i] = solar_position(obs, dts[i], alg, refraction) + end + return pos +end + +function Positioning.solar_position!( + pos::StructArrays.StructVector{T}, + obs::AbstractObserver, + dts::AbstractVector{Union{DateTime, ZonedDateTime}}, + alg::SolarAlgorithm = PSA(), + refraction::RefractionAlgorithm = DefaultRefraction(), + ) where {T <: AbstractSolPos} + @inbounds for i in eachindex(dts, pos) + pos[i] = solar_position(obs, dts[i], alg, refraction) + end + return pos +end + +function Positioning.solar_position( + obs::AbstractObserver{T}, + dts::AbstractVector{ZonedDateTime}, + alg::SolarAlgorithm = PSA(), + refraction::RefractionAlgorithm = DefaultRefraction(), + ) where {T <: Real} + RetType = result_type( + typeof(alg), typeof(refraction), _result_eltype(T, refraction), + ) + pos = StructArrays.StructVector{RetType}(undef, length(dts)) + solar_position!(pos, obs, dts, alg, refraction) + return pos +end + +function Positioning.solar_position( + obs::AbstractObserver{T}, + dts::AbstractVector{Union{DateTime, ZonedDateTime}}, + alg::SolarAlgorithm = PSA(), + refraction::RefractionAlgorithm = DefaultRefraction(), + ) where {T <: Real} + RetType = result_type( + typeof(alg), typeof(refraction), _result_eltype(T, refraction), + ) + pos = StructArrays.StructVector{RetType}(undef, length(dts)) + solar_position!(pos, obs, dts, alg, refraction) + return pos +end + +Positioning.solar_rate(obs::AbstractObserver, dt::ZonedDateTime, alg::Interpolated) = + solar_rate(obs, DateTime(dt, UTC), alg) + +# --------------------------------------------------------------------------- delta T + +Positioning.calculate_deltat(datetime::ZonedDateTime) = + calculate_deltat(DateTime(datetime, UTC)) + +Positioning.calculate_deltat(::Type{T}, datetime::ZonedDateTime) where {T <: Real} = + T(calculate_deltat(datetime)) + +# the interpolant span accepts zoned endpoints +Positioning._as_utc(zdt::ZonedDateTime) = DateTime(zdt, UTC) + +# --------------------------------------------------------- sunrise, sunset, transit + +function Utilities.TransitSunriseSunset{ZonedDateTime}( + transit::DateTime, + sunrise::DateTime, + sunset::DateTime, + tz::TimeZone, + ) + return TransitSunriseSunset{ZonedDateTime}( + ZonedDateTime(transit, tz; from_utc = true), + ZonedDateTime(sunrise, tz; from_utc = true), + ZonedDateTime(sunset, tz; from_utc = true), + ) +end + +function Utilities._transit_sunrise_sunset( + tz::TimeZone, + obs::Observer{T}, + dt::DateTime, + alg::SPA, + ) where {T <: Real} + return _transit_sunrise_sunset_impl(ZonedDateTime, obs, dt, alg, tz) +end + +function Utilities.transit_sunrise_sunset( + obs::Observer{T}, + zdt::ZonedDateTime, + alg::SolarAlgorithm = SPA(), + )::TransitSunriseSunset{ZonedDateTime} where {T <: Real} + return _transit_sunrise_sunset(timezone(zdt), obs, DateTime(zdt, UTC), alg) +end + +function Utilities.next_sunrise( + obs::Observer, + zdt::ZonedDateTime, + alg::SolarAlgorithm = SPA(), + ) + result_utc = next_sunrise(obs, DateTime(zdt, UTC), alg) + return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) +end + +function Utilities.next_sunset( + obs::Observer, + zdt::ZonedDateTime, + alg::SolarAlgorithm = SPA(), + ) + result_utc = next_sunset(obs, DateTime(zdt, UTC), alg) + return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) +end + +function Utilities.next_solar_noon( + obs::Observer, + zdt::ZonedDateTime, + alg::SolarAlgorithm = SPA(), + ) + result_utc = next_solar_noon(obs, DateTime(zdt, UTC), alg) + return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) +end + +function Utilities.previous_sunrise( + obs::Observer, + zdt::ZonedDateTime, + alg::SolarAlgorithm = SPA(), + ) + result_utc = previous_sunrise(obs, DateTime(zdt, UTC), alg) + return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) +end + +function Utilities.previous_sunset( + obs::Observer, + zdt::ZonedDateTime, + alg::SolarAlgorithm = SPA(), + ) + result_utc = previous_sunset(obs, DateTime(zdt, UTC), alg) + return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) +end + +function Utilities.previous_solar_noon( + obs::Observer, + zdt::ZonedDateTime, + alg::SolarAlgorithm = SPA(), + ) + result_utc = previous_solar_noon(obs, DateTime(zdt, UTC), alg) + return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) +end + +end # module SolarPositionTimeZonesExt diff --git a/src/Positioning/Positioning.jl b/src/Positioning/Positioning.jl index 21f12fa4..98b52da3 100644 --- a/src/Positioning/Positioning.jl +++ b/src/Positioning/Positioning.jl @@ -10,7 +10,6 @@ module Positioning using Dates: Dates, DateTime, Date, daysinmonth, dayofyear using Dates: year, month, day -using TimeZones: ZonedDateTime, UTC using StructArrays: StructArrays using Tables: Tables using DocStringExtensions: TYPEDFIELDS, TYPEDEF, TYPEDSIGNATURES @@ -358,28 +357,6 @@ function solar_position( return _solar_position(obs, dt, alg, refraction) end -function solar_position( - obs::AbstractObserver{T}, - dt::ZonedDateTime, - alg::SolarAlgorithm = PSA(), - refraction::RefractionAlgorithm = DefaultRefraction(), - ) where {T <: Real} - return solar_position(obs, DateTime(dt, UTC), alg, refraction) -end - -function solar_position!( - pos::StructArrays.StructVector{T}, - obs::AbstractObserver, - dts::AbstractVector{Union{DateTime, ZonedDateTime}}, - alg::SolarAlgorithm = PSA(), - refraction::RefractionAlgorithm = DefaultRefraction(), - ) where {T <: AbstractSolPos} - @inbounds for i in eachindex(dts, pos) - pos[i] = solar_position(obs, dts[i], alg, refraction) - end - return pos -end - function solar_position!( pos::StructArrays.StructVector{T}, obs::AbstractObserver, @@ -393,33 +370,6 @@ function solar_position!( return pos end -function solar_position!( - pos::StructArrays.StructVector{T}, - obs::AbstractObserver, - dts::AbstractVector{ZonedDateTime}, - alg::SolarAlgorithm = PSA(), - refraction::RefractionAlgorithm = DefaultRefraction(), - ) where {T <: AbstractSolPos} - @inbounds for i in eachindex(dts, pos) - pos[i] = solar_position(obs, dts[i], alg, refraction) - end - return pos -end - -function solar_position( - obs::AbstractObserver{T}, - dts::AbstractVector{Union{DateTime, ZonedDateTime}}, - alg::SolarAlgorithm = PSA(), - refraction::RefractionAlgorithm = DefaultRefraction(), - ) where {T <: Real} - RetType = result_type( - typeof(alg), typeof(refraction), _result_eltype(T, refraction), - ) - pos = StructArrays.StructVector{RetType}(undef, length(dts)) - solar_position!(pos, obs, dts, alg, refraction) - return pos -end - function solar_position( obs::AbstractObserver{T}, dts::AbstractVector{DateTime}, @@ -434,21 +384,6 @@ function solar_position( return pos end -function solar_position( - obs::AbstractObserver{T}, - dts::AbstractVector{ZonedDateTime}, - alg::SolarAlgorithm = PSA(), - refraction::RefractionAlgorithm = DefaultRefraction(), - ) where {T <: Real} - RetType = result_type( - typeof(alg), typeof(refraction), _result_eltype(T, refraction), - ) - pos = StructArrays.StructVector{RetType}(undef, length(dts)) - solar_position!(pos, obs, dts, alg, refraction) - return pos -end - - """ $(TYPEDSIGNATURES) diff --git a/src/Positioning/deltat.jl b/src/Positioning/deltat.jl index 53f7bf53..4416248e 100644 --- a/src/Positioning/deltat.jl +++ b/src/Positioning/deltat.jl @@ -197,10 +197,6 @@ function calculate_deltat(date::Union{DateTime, Date}) return calculate_deltat(y, frac_month) end -function calculate_deltat(datetime::ZonedDateTime) - return calculate_deltat(DateTime(datetime, UTC)) -end - # Type-generic entry points: keep the Float64 polynomial value (ΔT is uncertain to ~seconds), # convert to T so the result stays type-stable through the pipeline. calculate_deltat(::Type{T}, year::Real, month::Real) where {T <: Real} = @@ -208,6 +204,3 @@ calculate_deltat(::Type{T}, year::Real, month::Real) where {T <: Real} = calculate_deltat(::Type{T}, date::Union{DateTime, Date}) where {T <: Real} = T(calculate_deltat(date)) - -calculate_deltat(::Type{T}, datetime::ZonedDateTime) where {T <: Real} = - T(calculate_deltat(datetime)) diff --git a/src/Positioning/interpolated.jl b/src/Positioning/interpolated.jl index 93d4ce1c..666c29d2 100644 --- a/src/Positioning/interpolated.jl +++ b/src/Positioning/interpolated.jl @@ -66,7 +66,7 @@ end function Interpolated( algorithm::SPA = SPA(); - tspan::Tuple{<:Union{DateTime, ZonedDateTime}, <:Union{DateTime, ZonedDateTime}}, + tspan::Tuple{Any, Any}, step::Dates.Period = Dates.Hour(1), out_of_range::Symbol = :error, ) @@ -95,7 +95,6 @@ function Interpolated( end _as_utc(dt::DateTime) = dt -_as_utc(zdt::ZonedDateTime) = DateTime(zdt, UTC) # Extension hook. SolarPositionInterpolationsExt defines the working method for SPA; # this fallback exists so construction without the extension fails with a clear message. @@ -216,7 +215,3 @@ function solar_rate( daz = mod(p2.azimuth - p1.azimuth + 180, 360) - 180 return (dazimuth_dt = daz * 1800, delevation_dt = (p2.elevation - p1.elevation) * 1800) end - -function solar_rate(obs::AbstractObserver, dt::ZonedDateTime, alg::Interpolated) - return solar_rate(obs, DateTime(dt, UTC), alg) -end diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 0eff072c..cd5dca1a 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -3,7 +3,6 @@ module Utilities using DocStringExtensions: TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES using ..Positioning: Observer, SPA, SolarAlgorithm, calculate_deltat import Dates: DateTime, Date, Day -import TimeZones: ZonedDateTime, timezone, UTC include("spa.jl") include("srt.jl") diff --git a/src/Utilities/spa.jl b/src/Utilities/spa.jl index 8d659125..3f2821b3 100644 --- a/src/Utilities/spa.jl +++ b/src/Utilities/spa.jl @@ -1,7 +1,6 @@ """SPA sunrise/sunset calculations.""" using Dates: Dates -using TimeZones: TimeZone using ..Positioning: _compute_spa_srt_parameters @@ -23,22 +22,13 @@ function _transit_sunrise_sunset( return _transit_sunrise_sunset_impl(R, obs, dt, alg, nothing) end -function _transit_sunrise_sunset( - tz::TimeZone, - obs::Observer{T}, - dt::DateTime, - alg::SPA, - ) where {T <: Real} - return _transit_sunrise_sunset_impl(ZonedDateTime, obs, dt, alg, tz) -end - function _transit_sunrise_sunset_impl( ::Type{R}, obs::Observer{T}, dt::DateTime, alg::SPA, - tz::Union{Nothing, TimeZone}, - ) where {T <: Real, R <: Union{DateTime, ZonedDateTime}} + tz, + ) where {T <: Real, R} """Calculate the sun transit, sunrise, and sunset for a given date at an Observer location using the SPA algorithm. diff --git a/src/Utilities/srt.jl b/src/Utilities/srt.jl index 6328a1a9..66464e35 100644 --- a/src/Utilities/srt.jl +++ b/src/Utilities/srt.jl @@ -27,7 +27,7 @@ TransitSunriseSunset{ZonedDateTime}( # Fields $(TYPEDFIELDS) """ -struct TransitSunriseSunset{T <: Union{DateTime, ZonedDateTime}} +struct TransitSunriseSunset{T} transit::T sunrise::T sunset::T @@ -43,20 +43,6 @@ function TransitSunriseSunset{DateTime}( return TransitSunriseSunset{DateTime}(transit, sunrise, sunset) end -# Constructor for ZonedDateTime (with timezone conversion) -function TransitSunriseSunset{ZonedDateTime}( - transit::DateTime, - sunrise::DateTime, - sunset::DateTime, - tz::TimeZone, - ) - return TransitSunriseSunset{ZonedDateTime}( - ZonedDateTime(transit, tz; from_utc = true), - ZonedDateTime(sunrise, tz; from_utc = true), - ZonedDateTime(sunset, tz; from_utc = true), - ) -end - """Calculate the sun transit, sunrise, and sunset for a given date at an Observer location. """ @@ -76,14 +62,6 @@ function transit_sunrise_sunset( return transit_sunrise_sunset(obs, DateTime(dt), alg) end -function transit_sunrise_sunset( - obs::Observer{T}, - zdt::ZonedDateTime, - alg::SolarAlgorithm = SPA(), - )::TransitSunriseSunset{ZonedDateTime} where {T <: Real} - return _transit_sunrise_sunset(timezone(zdt), obs, DateTime(zdt, UTC), alg) -end - # Helper function for next_* functions function _next_event(obs::Observer, dt::DateTime, alg::SolarAlgorithm, event_field::Symbol) date_only = Date(dt) @@ -153,12 +131,6 @@ function next_sunrise(obs::Observer, dt::Date, alg::SolarAlgorithm = SPA()) return next_sunrise(obs, DateTime(dt), alg) end -function next_sunrise(obs::Observer, zdt::ZonedDateTime, alg::SolarAlgorithm = SPA()) - dt_utc = DateTime(zdt, UTC) - result_utc = next_sunrise(obs, dt_utc, alg) - return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) -end - """ $(TYPEDSIGNATURES) @@ -180,12 +152,6 @@ function next_sunset(obs::Observer, dt::Date, alg::SolarAlgorithm = SPA()) return next_sunset(obs, DateTime(dt), alg) end -function next_sunset(obs::Observer, zdt::ZonedDateTime, alg::SolarAlgorithm = SPA()) - dt_utc = DateTime(zdt, UTC) - result_utc = next_sunset(obs, dt_utc, alg) - return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) -end - """ $(TYPEDSIGNATURES) @@ -208,12 +174,6 @@ function next_solar_noon(obs::Observer, dt::Date, alg::SolarAlgorithm = SPA()) return next_solar_noon(obs, DateTime(dt), alg) end -function next_solar_noon(obs::Observer, zdt::ZonedDateTime, alg::SolarAlgorithm = SPA()) - dt_utc = DateTime(zdt, UTC) - result_utc = next_solar_noon(obs, dt_utc, alg) - return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) -end - """ $(TYPEDSIGNATURES) @@ -235,12 +195,6 @@ function previous_sunrise(obs::Observer, dt::Date, alg::SolarAlgorithm = SPA()) return previous_sunrise(obs, DateTime(dt), alg) end -function previous_sunrise(obs::Observer, zdt::ZonedDateTime, alg::SolarAlgorithm = SPA()) - dt_utc = DateTime(zdt, UTC) - result_utc = previous_sunrise(obs, dt_utc, alg) - return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) -end - """ $(TYPEDSIGNATURES) @@ -262,12 +216,6 @@ function previous_sunset(obs::Observer, dt::Date, alg::SolarAlgorithm = SPA()) return previous_sunset(obs, DateTime(dt), alg) end -function previous_sunset(obs::Observer, zdt::ZonedDateTime, alg::SolarAlgorithm = SPA()) - dt_utc = DateTime(zdt, UTC) - result_utc = previous_sunset(obs, dt_utc, alg) - return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) -end - """ $(TYPEDSIGNATURES) @@ -288,9 +236,3 @@ end function previous_solar_noon(obs::Observer, dt::Date, alg::SolarAlgorithm = SPA()) return previous_solar_noon(obs, DateTime(dt), alg) end - -function previous_solar_noon(obs::Observer, zdt::ZonedDateTime, alg::SolarAlgorithm = SPA()) - dt_utc = DateTime(zdt, UTC) - result_utc = previous_solar_noon(obs, dt_utc, alg) - return ZonedDateTime(result_utc, timezone(zdt); from_utc = true) -end From 27afd4ad6d58c20830557a6ce58e510f93c37c40 Mon Sep 17 00:00:00 2001 From: Stefan de Lange Date: Thu, 30 Jul 2026 23:27:05 +0200 Subject: [PATCH 2/2] Document the TimeZones extension and sync the docs index Both landing pages had drifted from what the package actually ships. README: add Michalsky to the positioning table, which arrived without the table being updated, and add the new TimeZones row to the extensions table. The refraction table advertised the SPA model as `SPA`, but the exported type is `SPARefraction` -- `SPA` is the positioning algorithm, so a reader copying that name got a working call with the wrong meaning. docs index: the extensions table listed three of the five extensions, missing Interpolations as well as TimeZones. The Interpolated wrapper and the autodiff support had no mention on the page at all, though both are covered in the README and have their own guides; add a section for each, as executable examples reusing the observer the page already defines, and link the guides. Add a one-line example of passing an algorithm explicitly, since the table names algorithms the page never shows being selected. Both pages get a note that loading TimeZones is what enables ZonedDateTime arguments. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012Ky1A6fZaCrjV4H3Fzz2u3 --- README.md | 22 ++++++++++++-------- docs/src/index.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 639d3d2f..53a79340 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ accuracy and implementation status. | USNO | [U.S. Naval Observatory](https://aa.usno.navy.mil/faq/sun_approx) | ±0.0500° | None | ✅ | | SPA | [Reda & Andreas, 2004](https://doi.org/10.1016/j.solener.2003.12.003) | ±0.0003° | Built-in | ✅ | | Iqbal | [Iqbal, 1983](https://doi.org/10.1016/B978-0-12-373750-2.X5001-0) | ±0.0100° | None | ✅ | +| Michalsky | [Michalsky, 1988]() | ±0.0100° | MICHALSKY | ✅ | ## Fast repeated evaluation @@ -141,14 +142,14 @@ tracker example. Atmospheric refraction correction algorithms available in SolarPosition.jl. -| Algorithm | Reference | Atmospheric Parameters | Status | -| --------- | ------------------------------------------------------------------------------------------------ | ---------------------- | ------ | -| HUGHES | [Hughes, 1985](https://pvpmc.sandia.gov/app/uploads/sites/243/2022/10/Engineering-Astronomy.pdf) | Pressure, Temperature | ✅ | -| ARCHER | Archer et al., 1980 | None | ✅ | -| BENNETT | [Bennett, 1982](https://doi.org/10.1017/S0373463300022037) | Pressure, Temperature | ✅ | -| MICHALSKY | [Michalsky, 1988]() | None | ✅ | -| SG2 | [Blanc & Wald, 2012](https://doi.org/10.1016/j.solener.2012.07.018) | Pressure, Temperature | ✅ | -| SPA | [Reda & Andreas, 2004](https://doi.org/10.1016/j.solener.2003.12.003) | Pressure, Temperature | ✅ | +| Algorithm | Reference | Atmospheric Parameters | Status | +| ------------- | ------------------------------------------------------------------------------------------------ | ---------------------- | ------ | +| HUGHES | [Hughes, 1985](https://pvpmc.sandia.gov/app/uploads/sites/243/2022/10/Engineering-Astronomy.pdf) | Pressure, Temperature | ✅ | +| ARCHER | Archer et al., 1980 | None | ✅ | +| BENNETT | [Bennett, 1982](https://doi.org/10.1017/S0373463300022037) | Pressure, Temperature | ✅ | +| MICHALSKY | [Michalsky, 1988]() | None | ✅ | +| SG2 | [Blanc & Wald, 2012](https://doi.org/10.1016/j.solener.2012.07.018) | Pressure, Temperature | ✅ | +| SPARefraction | [Reda & Andreas, 2004](https://doi.org/10.1016/j.solener.2003.12.003) | Pressure, Temperature | ✅ | ## Extensions @@ -161,6 +162,11 @@ import the corresponding packages: | OhMyThreads | [`OhMyThreads.jl`](https://github.com/JuliaFolds2/OhMyThreads.jl) | Parallel computation of solar positions | | ModelingToolkit | [`ModelingToolkit.jl`](https://github.com/SciML/ModelingToolkit.jl) | Symbolic solar position models for simulations | | Interpolations | [`Interpolations.jl`](https://github.com/JuliaMath/Interpolations.jl) | Fast `Interpolated` algorithm construction | +| TimeZones | [`TimeZones.jl`](https://github.com/JuliaTime/TimeZones.jl) | `ZonedDateTime` input and zoned sunrise/sunset | + +Loading `TimeZones.jl` is what enables `ZonedDateTime` arguments. In practice this needs +no thought, since a `ZonedDateTime` cannot be constructed without it, and it means users +who only ever pass a `DateTime` do not pay for TZJData and its download stack. ## Numeric precision diff --git a/docs/src/index.md b/docs/src/index.md index b1dbadd7..55176909 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -88,6 +88,50 @@ accuracy and implementation status. | [`Iqbal`](@ref SolarPosition.Positioning.Iqbal) | [Iqbal, 1983](https://doi.org/10.1016/B978-0-12-373750-2.X5001-0) | ±0.0100° | None | ✅ | | [`Michalsky`](@ref SolarPosition.Positioning.Michalsky) | [Michalsky, 1988](https://doi.org/10.1016/0038-092X(88)90045-X) | ±0.0100° | [`MICHALSKY`](@ref SolarPosition.Refraction.MICHALSKY) | ✅ | +Pass an algorithm as the third argument to pick one; the default is +[`PSA`](@ref SolarPosition.Positioning.PSA). + +```@example srt +solar_position(obs, DateTime(2023, 6, 21, 12), Michalsky()) +``` + +## Fast repeated evaluation + +For dense time series, the [`Interpolated`](@ref SolarPosition.Positioning.Interpolated) +wrapper precomputes cubic B-splines of SPA's geocentric solar coordinates and reconstructs +positions analytically, roughly 10× faster per query at matching accuracy. One interpolant +serves every observer. It activates as a package extension when +[Interpolations.jl](https://github.com/JuliaMath/Interpolations.jl) is loaded: + +```@example srt +using Interpolations + +alg = Interpolated(SPA(); tspan = (DateTime(2023, 1, 1), DateTime(2024, 1, 1))) +solar_position(obs, times, alg) +``` + +See the [Interpolated Solar Position](@ref interpolated-position) guide for accuracy +figures and when the construction cost pays off. + +## Automatic differentiation + +All algorithms are generic over the number type, so solar positions are differentiable +with [ForwardDiff.jl](https://github.com/JuliaDiff/ForwardDiff.jl) out of the box, with no +extension package needed: + +```@example srt +using ForwardDiff + +ForwardDiff.gradient( + x -> solar_position(Observer(x[1], x[2]), DateTime(2023, 6, 21, 12)).elevation, + [52.35888, 4.88185], +) +``` + +The [Automatic Differentiation](@ref automatic-differentiation) guide shows gradients +through refraction models, panel orientation optimization, and a single axis tracker +example. + ## Numeric precision The computation runs at the precision of the @@ -121,12 +165,19 @@ import the corresponding packages: | Makie | [`Makie.jl`](https://github.com/MakieOrg/Makie.jl) | Plotting recipes for solar position visualization | | OhMyThreads | [`OhMyThreads.jl`](https://github.com/JuliaFolds2/OhMyThreads.jl) | Parallel computation of solar positions | | ModelingToolkit | [`ModelingToolkit.jl`](https://github.com/SciML/ModelingToolkit.jl) | Symbolic solar position models for simulations | +| Interpolations | [`Interpolations.jl`](https://github.com/JuliaMath/Interpolations.jl) | Fast `Interpolated` algorithm construction | +| TimeZones | [`TimeZones.jl`](https://github.com/JuliaTime/TimeZones.jl) | `ZonedDateTime` input and zoned sunrise/sunset | + +Loading `TimeZones.jl` is what enables `ZonedDateTime` arguments. In practice this needs +no thought, since a `ZonedDateTime` cannot be constructed without it, and it means users +who only ever pass a `DateTime` do not pay for TZJData and its download stack. !!! note For more details on the extensions, see: - [ModelingToolkit Extension](guides/modelingtoolkit.md) - [Makie Extension](guides/plotting.md) - [OhMyThreads Extension](guides/parallel.md) + - [Interpolated Solar Position](@ref interpolated-position) ## How to Cite