Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -26,6 +26,7 @@ SolarPositionInterpolationsExt = "Interpolations"
SolarPositionMakieExt = "Makie"
SolarPositionModelingToolkitExt = ["ModelingToolkit", "Symbolics"]
SolarPositionOhMyThreadsExt = "OhMyThreads"
SolarPositionTimeZonesExt = "TimeZones"

[compat]
Aqua = "0.8"
Expand Down
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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](<https://doi.org/10.1016/0038-092X(88)90045-X>) | ±0.0100° | MICHALSKY | ✅ |

## Fast repeated evaluation

Expand Down Expand Up @@ -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](<https://doi.org/10.1016/0038-092X(88)90045-X>) | 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](<https://doi.org/10.1016/0038-092X(88)90045-X>) | 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

Expand All @@ -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

Expand Down
51 changes: 51 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
7 changes: 3 additions & 4 deletions ext/SolarPositionOhMyThreadsExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ using OhMyThreads
using OhMyThreads: tmap, tmap!
using StructArrays: StructArrays
using Dates: DateTime
using TimeZones: ZonedDateTime
using DocStringExtensions: TYPEDSIGNATURES

"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
193 changes: 193 additions & 0 deletions ext/SolarPositionTimeZonesExt.jl
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading