From 959e4156f9bdda9a2e82975b53b6a872ceb60201 Mon Sep 17 00:00:00 2001 From: Stefan de Lange Date: Tue, 14 Apr 2026 16:28:12 +0200 Subject: [PATCH 1/4] Implement vectorized interface --- CLAUDE.md | 31 +++--- src/Positioning/Positioning.jl | 141 ++++++++++++++----------- src/Positioning/noaa.jl | 157 +++++++++++++++------------- src/Positioning/psa.jl | 87 +++++++++------- src/Positioning/spa.jl | 49 +++++---- src/Positioning/usno.jl | 171 ++++++++++++++++-------------- src/Positioning/walraven.jl | 184 +++++++++++++++++---------------- 7 files changed, 441 insertions(+), 379 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 428c075..7b253e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,30 +40,35 @@ The package provides a unified interface to multiple solar position algorithms. ```text src/ - SolarPosition.jl # Main module, re-exports all submodules + SolarPosition.jl # Main module, re-exports all submodules via @reexport Positioning/ # Solar position algorithms - Positioning.jl # Observer struct, SolPos/ApparentSolPos types, solar_position() API + Positioning.jl # Observer, SolPos/ApparentSolPos types, solar_position() dispatch psa.jl # PSA algorithm (default, ±0.0083°) noaa.jl, spa.jl, # Other algorithms (NOAA, SPA, Walraven, USNO) walraven.jl, usno.jl deltat.jl # Delta T / leap seconds Refraction/ # Atmospheric refraction correction models - Refraction.jl # Abstract base + interface - hughes.jl, bennett.jl, sg2.jl, spa.jl, ... + Refraction.jl # RefractionAlgorithm abstract type + refraction() interface + hughes.jl, archer.jl, bennett.jl, michalsky.jl, sg2.jl, spa.jl Utilities/ # Sunrise/sunset/transit calculations - srt.jl, spa.jl -ext/ # Weak dependency extensions - SolarPositionMakieExt.jl # Sun path plotting - SolarPositionOhMyThreadsExt.jl # Parallel solar position computation - SolarPositionModelingToolkitExt.jl # Symbolic models for MTK + srt.jl # transit_sunrise_sunset(), next_sunrise/sunset/solar_noon, etc. + spa.jl # SPA-based utility helpers +ext/ # Weak dependency extensions (loaded via [weakdeps] in Project.toml) + SolarPositionMakieExt.jl # analemmas!() sun path plotting + SolarPositionOhMyThreadsExt.jl # Parallel solar_position via OhMyThreads + SolarPositionModelingToolkitExt.jl # SolarPositionBlock for MTK symbolic models ``` -**Core API pattern:** `solar_position(algorithm, observer, datetime)` returns a `SolPos` or `ApparentSolPos` (if refraction is included). The `Observer` struct holds location (lat/lon/altitude) and optional atmospheric parameters (pressure, temperature). +**Core API pattern:** `solar_position(observer, datetime, algorithm, refraction)` returns a `SolPos` or `ApparentSolPos` (if refraction model is provided). Also accepts vectors of datetimes (returns `StructVector`) and Tables.jl-compatible tables (adds columns in-place). -**Test discovery:** Test files matching `test-*.jl` under `test/` are automatically discovered and wrapped in `@testset`. Reference values live in `expected-values.jl` files alongside algorithm tests. +**Adding a new algorithm:** Subtype `SolarAlgorithm`, implement `_solar_position(obs, dt, alg::YourAlg)::SolPos`, and the dispatch in `Positioning.jl` handles refraction wrapping automatically. Same pattern for refraction: subtype `RefractionAlgorithm` and implement `_refraction(model, elevation)`. + +**Test discovery:** Test files matching `test-*.jl` under `test/` are automatically discovered and wrapped in `@testset`. Don't add tests to `runtests.jl` directly. Reference values live in `expected-values.jl` files alongside algorithm tests. ## Code Style -- Formatter: **Runic.jl** (enforced via pre-commit). Run before committing. -- Imports: All used symbols must be explicitly imported (checked by ExplicitImports.jl). +- Formatter: **Runic.jl** (enforced via pre-commit). Run `pre-commit run runic --all-files` before committing. +- Imports: All used symbols must be explicitly imported (checked by ExplicitImports.jl pre-commit hook). +- Spelling: **typos** is enforced via pre-commit. If a false positive occurs, add it to `_typos.toml`. - Package quality is checked with **Aqua.jl** and type inference with **JET.jl** (Julia 1.12 only). +- Pre-commit `no-commit-to-branch` blocks direct commits to `main`; work on feature branches. diff --git a/src/Positioning/Positioning.jl b/src/Positioning/Positioning.jl index 1a5f78b..78d44b9 100644 --- a/src/Positioning/Positioning.jl +++ b/src/Positioning/Positioning.jl @@ -263,34 +263,77 @@ See also: [`solar_position!`](@ref), [`Observer`](@ref), [`PSA`](@ref), [`NOAA`] """ function solar_position end -function _solar_position(obs, dt, alg::SolarAlgorithm, ::NoRefraction) - return _solar_position(obs, dt, alg) -end +# Resolve DefaultRefraction to a concrete refraction algorithm. +# Each algorithm file overrides this for its own DefaultRefraction mapping. +# The observer type T is passed to allow type-parameterized refraction models. +_resolve_refraction(::SolarAlgorithm, r::RefractionAlgorithm, ::Type{T}) where {T} = r + +# --- Vectorized internal interface --- -function _solar_position(obs, dt, alg::SolarAlgorithm, refraction::RefractionAlgorithm) - pos = _solar_position(obs, dt, alg) +# Generic fallback: loops over scalar _solar_position for any algorithm that only +# defines the scalar method. Built-in algorithms override with optimized versions. +function _solar_position!( + pos::StructArrays.StructVector{SolPos{T}}, + obs::AbstractObserver{T}, + dts::AbstractVector{DateTime}, + alg::SolarAlgorithm, + ) where {T} + @inbounds for i in eachindex(dts, pos) + pos[i] = _solar_position(obs, dts[i], alg) + end + return pos +end - # apply refraction correction - refraction_correction_deg = Refraction.refraction(refraction, pos.elevation) - apparent_elevation_deg = pos.elevation + refraction_correction_deg - apparent_zenith_deg = 90 - apparent_elevation_deg +# NoRefraction: just compute raw positions +function _solar_position!( + pos::StructArrays.StructVector{SolPos{T}}, + obs::AbstractObserver{T}, + dts::AbstractVector{DateTime}, + alg::SolarAlgorithm, + ::NoRefraction, + ) where {T} + return _solar_position!(pos, obs, dts, alg) +end - return ApparentSolPos( - pos.azimuth, - pos.elevation, - pos.zenith, - apparent_elevation_deg, - apparent_zenith_deg, +# With refraction: compute raw positions into shared columns, then apply refraction +function _solar_position!( + pos::StructArrays.StructVector{ApparentSolPos{T}}, + obs::AbstractObserver{T}, + dts::AbstractVector{DateTime}, + alg::SolarAlgorithm, + refraction::RefractionAlgorithm, + ) where {T} + # Create SolPos view backed by the same column arrays (no heap allocation) + raw = StructArrays.StructVector{SolPos{T}}( + ( + azimuth = pos.azimuth, + elevation = pos.elevation, + zenith = pos.zenith, + ) ) + _solar_position!(raw, obs, dts, alg) + + # Apply refraction in second pass + @inbounds for i in eachindex(pos) + correction = Refraction.refraction(refraction, pos.elevation[i]) + pos.apparent_elevation[i] = pos.elevation[i] + correction + pos.apparent_zenith[i] = T(90) - pos.apparent_elevation[i] + end + return pos end +# Scalar public API routes through the vectorized path function solar_position( obs::AbstractObserver{T}, dt::DateTime, alg::SolarAlgorithm = PSA(), refraction::RefractionAlgorithm = DefaultRefraction(), ) where {T <: AbstractFloat} - return _solar_position(obs, dt, alg, refraction) + resolved = _resolve_refraction(alg, refraction, T) + RetType = result_type(typeof(alg), typeof(resolved), T) + pos = StructArrays.StructVector{RetType}(undef, 1) + _solar_position!(pos, obs, DateTime[dt], alg, resolved) + return pos[1] end function solar_position( @@ -303,77 +346,49 @@ function solar_position( 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, + pos::StructArrays.StructVector{<:AbstractSolPos}, + obs::AbstractObserver{T}, dts::AbstractVector{DateTime}, 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 + ) where {T} + resolved = _resolve_refraction(alg, refraction, T) + _solar_position!(pos, obs, dts, alg, resolved) return pos end function solar_position!( - pos::StructArrays.StructVector{T}, - obs::AbstractObserver, + pos::StructArrays.StructVector{<:AbstractSolPos}, + obs::AbstractObserver{T}, 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 + ) where {T} + utc_dts = DateTime.(dts, Ref(UTC)) + return solar_position!(pos, obs, utc_dts, alg, refraction) end -function solar_position( +function solar_position!( + pos::StructArrays.StructVector{<:AbstractSolPos}, obs::AbstractObserver{T}, dts::AbstractVector{Union{DateTime, ZonedDateTime}}, alg::SolarAlgorithm = PSA(), refraction::RefractionAlgorithm = DefaultRefraction(), - ) where {T <: AbstractFloat} - RetType = result_type(typeof(alg), typeof(refraction), T) - 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}, - alg::SolarAlgorithm = PSA(), - refraction::RefractionAlgorithm = DefaultRefraction(), - ) where {T <: AbstractFloat} - RetType = result_type(typeof(alg), typeof(refraction), T) - pos = StructArrays.StructVector{RetType}(undef, length(dts)) - solar_position!(pos, obs, dts, alg, refraction) - return pos + ) where {T} + utc_dts = [dt isa ZonedDateTime ? DateTime(dt, UTC) : dt for dt in dts] + return solar_position!(pos, obs, utc_dts, alg, refraction) end function solar_position( obs::AbstractObserver{T}, - dts::AbstractVector{ZonedDateTime}, + dts::AbstractVector{<:Union{DateTime, ZonedDateTime}}, alg::SolarAlgorithm = PSA(), refraction::RefractionAlgorithm = DefaultRefraction(), ) where {T <: AbstractFloat} - RetType = result_type(typeof(alg), typeof(refraction), T) + resolved = _resolve_refraction(alg, refraction, T) + RetType = result_type(typeof(alg), typeof(resolved), T) pos = StructArrays.StructVector{RetType}(undef, length(dts)) - solar_position!(pos, obs, dts, alg, refraction) + solar_position!(pos, obs, dts, alg, resolved) return pos end diff --git a/src/Positioning/noaa.jl b/src/Positioning/noaa.jl index a6fee3a..34f3826 100644 --- a/src/Positioning/noaa.jl +++ b/src/Positioning/noaa.jl @@ -34,86 +34,99 @@ end NOAA() = NOAA(67.0) # default delta_t value (2020 default from pvlib) -function _solar_position(obs::Observer{T}, dt::DateTime, alg::NOAA) where {T} +function _solar_position!( + pos::StructArrays.StructVector{SolPos{T}}, + obs::Observer{T}, + dts::AbstractVector{DateTime}, + alg::NOAA, + ) where {T} + # Hoist algorithm and observer constants out of the loop δt = if alg.delta_t === nothing - calculate_deltat(dt) + calculate_deltat(dts[1]) else alg.delta_t end - # convert to Julian date and Julian century - jd = datetime2julian(dt) - jc = (jd - 2451545.0) / 36525.0 - - # mean longitude of the sun [degrees] - mean_long = mod(280.46646 + jc * (36000.76983 + jc * 0.0003032), 360.0) - - # mean anomaly [degrees] - mean_anom = 357.52911 + jc * (35999.05029 - 0.0001537 * jc) - - # cccentricity of Earth's orbit - eccent = 0.016708634 - jc * (0.000042037 + 0.0000001267 * jc) - - # sun equation of center [degrees] - sun_eq_ctr = ( - sind(mean_anom) * (1.914602 - jc * (0.004817 + 0.000014 * jc)) + - sind(2 * mean_anom) * (0.019993 - 0.000101 * jc) + - sind(3 * mean_anom) * 0.000289 - ) - - # sun true/apparent longitude [degrees] - sun_true_long = mean_long + sun_eq_ctr - sun_app_long = sun_true_long - 0.00569 - 0.00478 * sind(125.04 - 1934.136 * jc) - - # mean obliquity of ecliptic [degrees] - mean_obliq = - 23.0 + - (26.0 + (21.448 - jc * (46.815 + jc * (0.00059 - jc * 0.001813))) / 60.0) / 60.0 - - # obliquity correction [degrees] - obliq_corr = mean_obliq + 0.00256 * cosd(125.04 - 1934.136 * jc) - sun_declin = asind(sind(obliq_corr) * sind(sun_app_long)) - - # equation of time [minutes] - var_y = tand(obliq_corr / 2.0)^2 - eot = - 4.0 * rad2deg( - var_y * sind(2.0 * mean_long) - 2.0 * eccent * sind(mean_anom) + - 4.0 * eccent * var_y * sind(mean_anom) * cosd(2.0 * mean_long) - - 0.5 * var_y^2 * sind(4.0 * mean_long) - 1.25 * eccent^2 * sind(2.0 * mean_anom), - ) - - # true solar time [minutes] - hour_frac = fractional_hour(dt) - minutes = hour_frac * 60.0 - true_solar_time = mod(minutes + eot + 4.0 * obs.longitude, 1440.0) - - # hour angle [degrees] - # true_solar_time is in [0, 1440) minutes, so true_solar_time/4 is in [0, 360) degrees - # Convert to standard hour angle range (-180, 180] where 0 is solar noon - hour_angle = true_solar_time / 4.0 - 180.0 - - # zenith angle [degrees] - zenith = acosd( - obs.sin_lat * sind(sun_declin) + obs.cos_lat * cosd(sun_declin) * cosd(hour_angle), - ) - - # azimuth angle [degrees] - azimuth_numerator = obs.sin_lat * cosd(zenith) - sind(sun_declin) - azimuth_denominator = obs.cos_lat * sind(zenith) - - azimuth = if hour_angle > 0.0 - mod(acosd(azimuth_numerator / azimuth_denominator) + 180.0, 360.0) - else - mod(540.0 - acosd(azimuth_numerator / azimuth_denominator), 360.0) + sin_lat = obs.sin_lat + cos_lat = obs.cos_lat + longitude = obs.longitude + + @inbounds for i in eachindex(dts, pos) + dt = dts[i] + + # convert to Julian date and Julian century + jd = datetime2julian(dt) + jc = (jd - 2451545.0) / 36525.0 + + # mean longitude of the sun [degrees] + mean_long = mod(280.46646 + jc * (36000.76983 + jc * 0.0003032), 360.0) + + # mean anomaly [degrees] + mean_anom = 357.52911 + jc * (35999.05029 - 0.0001537 * jc) + + # eccentricity of Earth's orbit + eccent = 0.016708634 - jc * (0.000042037 + 0.0000001267 * jc) + + # sun equation of center [degrees] + sun_eq_ctr = ( + sind(mean_anom) * (1.914602 - jc * (0.004817 + 0.000014 * jc)) + + sind(2 * mean_anom) * (0.019993 - 0.000101 * jc) + + sind(3 * mean_anom) * 0.000289 + ) + + # sun true/apparent longitude [degrees] + sun_true_long = mean_long + sun_eq_ctr + sun_app_long = sun_true_long - 0.00569 - 0.00478 * sind(125.04 - 1934.136 * jc) + + # mean obliquity of ecliptic [degrees] + mean_obliq = + 23.0 + + (26.0 + (21.448 - jc * (46.815 + jc * (0.00059 - jc * 0.001813))) / 60.0) / + 60.0 + + # obliquity correction [degrees] + obliq_corr = mean_obliq + 0.00256 * cosd(125.04 - 1934.136 * jc) + sun_declin = asind(sind(obliq_corr) * sind(sun_app_long)) + + # equation of time [minutes] + var_y = tand(obliq_corr / 2.0)^2 + eot = + 4.0 * rad2deg( + var_y * sind(2.0 * mean_long) - 2.0 * eccent * sind(mean_anom) + + 4.0 * eccent * var_y * sind(mean_anom) * cosd(2.0 * mean_long) - + 0.5 * var_y^2 * sind(4.0 * mean_long) - + 1.25 * eccent^2 * sind(2.0 * mean_anom), + ) + + # true solar time [minutes] + hour_frac = fractional_hour(dt) + minutes = hour_frac * 60.0 + true_solar_time = mod(minutes + eot + 4.0 * longitude, 1440.0) + + # hour angle [degrees] + hour_angle = true_solar_time / 4.0 - 180.0 + + # zenith angle [degrees] + zenith = acosd( + sin_lat * sind(sun_declin) + cos_lat * cosd(sun_declin) * cosd(hour_angle), + ) + + # azimuth angle [degrees] + azimuth_numerator = sin_lat * cosd(zenith) - sind(sun_declin) + azimuth_denominator = cos_lat * sind(zenith) + + azimuth = if hour_angle > 0.0 + mod(acosd(azimuth_numerator / azimuth_denominator) + 180.0, 360.0) + else + mod(540.0 - acosd(azimuth_numerator / azimuth_denominator), 360.0) + end + + pos[i] = SolPos{T}(azimuth, 90.0 - zenith, zenith) end - - return SolPos{T}(azimuth, 90.0 - zenith, zenith) + return pos end -function _solar_position(obs, dt, alg::NOAA, ::DefaultRefraction) - return _solar_position(obs, dt, alg, HUGHES()) -end +_resolve_refraction(::NOAA, ::DefaultRefraction, ::Type{T}) where {T} = HUGHES() # NOAA with DefaultRefraction returns ApparentSolPos (uses HUGHES refraction) result_type(::Type{NOAA}, ::Type{DefaultRefraction}, ::Type{T}) where {T} = diff --git a/src/Positioning/psa.jl b/src/Positioning/psa.jl index ec1da5e..f5d8c99 100644 --- a/src/Positioning/psa.jl +++ b/src/Positioning/psa.jl @@ -64,54 +64,61 @@ PSA() = PSA(2020) end end -function _solar_position(obs::Observer{T}, dt::DateTime, alg::PSA) where {T} - # Get parameters as tuple (allocation-free) +function _solar_position!( + pos::StructArrays.StructVector{SolPos{T}}, + obs::Observer{T}, + dts::AbstractVector{DateTime}, + alg::PSA, + ) where {T} + # Hoist algorithm parameters and observer constants out of the loop p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 = get_psa_params(alg.coeffs) - # elapsed julian days (n) since J2000.0 - jd = datetime2julian(dt) - n = jd - 2451545.0 # Eq. 2 - - # ecliptic coordinates of the sun - # ecliptic longitude (λₑ), and obliquity of the ecliptic (ϵ) - Ω = p1 + p2 * n # Eq. 3 - L = p3 + p4 * n # Eq. 4 - g = p5 + p6 * n # Eq. 5 - (sin_Ω, cos_Ω) = sincos(Ω) - λₑ = L + p7 * sin(g) + p8 * sin(2 * g) + p9 + p10 * sin_Ω # Eq. 6 - ϵ = p11 + p12 * n + p13 * cos_Ω # Eq. 7 - - # celestial right ascension (ra) and declination (d) - (sin_ϵ, cos_ϵ) = sincos(ϵ) - (sin_λₑ, cos_λₑ) = sincos(λₑ) - ra = atan(cos_ϵ * sin_λₑ, cos_λₑ) # Eq. 8 - ra = mod2pi(ra) - δ = asin(sin_ϵ * sin_λₑ) # Eq. 9 - - # computes the local coordinates: azimuth (γ) and zenith angle (θz) - λt = rad2deg(obs.longitude_rad) cos_lat = obs.cos_lat sin_lat = obs.sin_lat + λt = rad2deg(obs.longitude_rad) - hour = fractional_hour(dt) - gmst = p14 + p15 * n + hour # Eq. 10 - lmst = deg2rad(gmst * 15 + λt) # Eq. 11 - ω = lmst - ra # Eq. 12 - (sin_δ, cos_δ) = sincos(δ) - (sin_ω, cos_ω) = sincos(ω) - θz = acos(cos_lat * cos_ω * cos_δ + sin_δ * sin_lat) # Eq. 13 - γ = atan(-sin_ω, (tan(δ) * cos_lat - sin_lat * cos_ω)) # Eq. 14 - - # parallax correction - θz = θz + (EMR / AU) * sin(θz) # Eq. 15,16 - - return SolPos{T}(mod(rad2deg(γ), 360.0), rad2deg(π / 2 - θz), rad2deg(θz)) + @inbounds for i in eachindex(dts, pos) + dt = dts[i] + + # elapsed julian days (n) since J2000.0 + jd = datetime2julian(dt) + n = jd - 2451545.0 + + # ecliptic coordinates + Ω = p1 + p2 * n + L = p3 + p4 * n + g = p5 + p6 * n + (sin_Ω, cos_Ω) = sincos(Ω) + λₑ = L + p7 * sin(g) + p8 * sin(2 * g) + p9 + p10 * sin_Ω + ϵ = p11 + p12 * n + p13 * cos_Ω + + # celestial right ascension and declination + (sin_ϵ, cos_ϵ) = sincos(ϵ) + (sin_λₑ, cos_λₑ) = sincos(λₑ) + ra = atan(cos_ϵ * sin_λₑ, cos_λₑ) + ra = mod2pi(ra) + δ = asin(sin_ϵ * sin_λₑ) + + # local coordinates + hour = fractional_hour(dt) + gmst = p14 + p15 * n + hour + lmst = deg2rad(gmst * 15 + λt) + ω = lmst - ra + (sin_δ, cos_δ) = sincos(δ) + (sin_ω, cos_ω) = sincos(ω) + θz = acos(cos_lat * cos_ω * cos_δ + sin_δ * sin_lat) + γ = atan(-sin_ω, (tan(δ) * cos_lat - sin_lat * cos_ω)) + + # parallax correction + θz = θz + (EMR / AU) * sin(θz) + + pos[i] = SolPos{T}(mod(rad2deg(γ), 360.0), rad2deg(π / 2 - θz), rad2deg(θz)) + end + return pos end -function _solar_position(obs, dt, alg::PSA, ::DefaultRefraction) - return _solar_position(obs, dt, alg, NoRefraction()) -end +_resolve_refraction(::PSA, ::DefaultRefraction, ::Type{T}) where {T} = NoRefraction() # PSA with DefaultRefraction returns SolPos (no refraction by default) result_type(::Type{PSA}, ::Type{DefaultRefraction}, ::Type{T}) where {T} = SolPos{T} diff --git a/src/Positioning/spa.jl b/src/Positioning/spa.jl index 1ed0f8b..af3acb7 100644 --- a/src/Positioning/spa.jl +++ b/src/Positioning/spa.jl @@ -433,43 +433,40 @@ function _solar_position( return SolPos{T}(az, e0, θz0) end -function _solar_position(obs::Observer{T}, dt::DateTime, alg::SPA) where {T <: AbstractFloat} - spa_obs = SPAObserver{T}(obs.latitude, obs.longitude, obs.altitude) - return _solar_position(spa_obs, dt, alg) -end - -function _solar_position( - obs::AbstractObserver{T}, - dt, +function _solar_position!( + pos::StructArrays.StructVector{SolPos{T}}, + obs::Observer{T}, + dts::AbstractVector{DateTime}, alg::SPA, - ::DefaultRefraction, ) where {T <: AbstractFloat} - return _solar_position( - obs, - dt, - alg, - SPARefraction{T}( - pressure = T(alg.pressure), - temperature = T(alg.temperature), - atmos_refract = T(alg.atmos_refract), - ), - ) + # Create SPAObserver once (caches parallax terms) + spa_obs = SPAObserver{T}(obs.latitude, obs.longitude, obs.altitude) + @inbounds for i in eachindex(dts, pos) + pos[i] = _solar_position(spa_obs, dts[i], alg) + end + return pos end -function solar_position!( - pos::StructArrays.StructVector{S}, - obs::AbstractObserver{T}, +function _solar_position!( + pos::StructArrays.StructVector{SolPos{T}}, + obs::SPAObserver{T}, dts::AbstractVector{DateTime}, alg::SPA, - refraction::RefractionAlgorithm = DefaultRefraction(), - ) where {S <: AbstractSolPos, T <: AbstractFloat} - spa_obs = SPAObserver{T}(obs.latitude, obs.longitude, obs.altitude) + ) where {T <: AbstractFloat} @inbounds for i in eachindex(dts, pos) - pos[i] = solar_position(spa_obs, dts[i], alg, refraction) + pos[i] = _solar_position(obs, dts[i], alg) end return pos end +function _resolve_refraction(alg::SPA, ::DefaultRefraction, ::Type{T}) where {T} + return SPARefraction{T}( + pressure = T(alg.pressure), + temperature = T(alg.temperature), + atmos_refract = T(alg.atmos_refract), + ) +end + # SPA returns SolPos with NoRefraction, ApparentSolPos with any refraction result_type(::Type{SPA}, ::Type{NoRefraction}, ::Type{T}) where {T} = SolPos{T} result_type(::Type{SPA}, ::Type{<:RefractionAlgorithm}, ::Type{T}) where {T} = diff --git a/src/Positioning/usno.jl b/src/Positioning/usno.jl index 7d934f2..e2dc5dd 100644 --- a/src/Positioning/usno.jl +++ b/src/Positioning/usno.jl @@ -31,101 +31,116 @@ end USNO() = USNO(67.0, 1) # default delta_t value and gmst_option -function _solar_position(obs::Observer{T}, dt::DateTime, alg::USNO) where {T <: AbstractFloat} +function _solar_position!( + pos::StructArrays.StructVector{SolPos{T}}, + obs::Observer{T}, + dts::AbstractVector{DateTime}, + alg::USNO, + ) where {T <: AbstractFloat} + # Hoist algorithm and observer constants out of the loop δt::T = if alg.delta_t === nothing - calculate_deltat(dt) + calculate_deltat(dts[1]) else alg.delta_t end - # convert to Julian date - jd = datetime2julian(dt) - - # days since J2000.0 - D = jd - 2451545.0 - - # mean anomaly of the sun [deg] - g = 357.529 + 0.98560028 * D - g = mod(g, 360.0) - - # mean longitude of the sun [deg] - q = 280.459 + 0.98564736 * D - q = mod(q, 360.0) - - # geocentric apparent ecliptic longitude of the sun (adjusted for aberration) [deg] - L = q + 1.915 * sind(g) + 0.02 * sind(2 * g) - L = mod(L, 360.0) - - # mean obliquity of the ecliptic [deg] - ϵ = 23.439 - 0.00000036 * D - - # sun's right ascension angle [hours] - ra = rad2deg(atan(cosd(ϵ) * sind(L), cosd(L))) / 15.0 - ra = mod(ra, 24.0) - - # sun's declination angle [deg] - δ = asind(sind(ϵ) * sind(L)) - - # JD_0 is the Julian date of the previous midnight (0h) UT1 - dt_midnight = DateTime(year(dt), month(dt), day(dt), 0, 0, 0) - jd_0 = datetime2julian(dt_midnight) - - # hours of UT1 elapsed since the previous midnight - H = (jd - jd_0) * 24.0 - day_ut = jd_0 - 2451545.0 - jd_tt = jd + δt / 86400.0 - D_tt = jd_tt - 2451545.0 - - # centuries since the year 2000 - t_cent = D_tt / 36525.0 - - # Greenwich mean sidereal time [hours] - gmst = if alg.gmst_option == 1 - ( - 6.697375 + - 0.065707485828 * day_ut + - 1.0027379 * H + - 0.0854103 * t_cent + - 0.0000258 * t_cent^2 - ) - else # gmst_option == 2 - (6.697375 + 0.065709824279 * day_ut + 1.0027379 * H + 0.0000258 * t_cent^2) - end - gmst = mod(gmst, 24.0) + sin_lat = obs.sin_lat + cos_lat = obs.cos_lat + longitude = obs.longitude + gmst_option = alg.gmst_option + + @inbounds for i in eachindex(dts, pos) + dt = dts[i] + + # convert to Julian date + jd = datetime2julian(dt) + + # days since J2000.0 + D = jd - 2451545.0 + + # mean anomaly of the sun [deg] + g = 357.529 + 0.98560028 * D + g = mod(g, 360.0) + + # mean longitude of the sun [deg] + q = 280.459 + 0.98564736 * D + q = mod(q, 360.0) + + # geocentric apparent ecliptic longitude [deg] + L = q + 1.915 * sind(g) + 0.02 * sind(2 * g) + L = mod(L, 360.0) + + # mean obliquity of the ecliptic [deg] + ϵ = 23.439 - 0.00000036 * D + + # sun's right ascension angle [hours] + ra = rad2deg(atan(cosd(ϵ) * sind(L), cosd(L))) / 15.0 + ra = mod(ra, 24.0) + + # sun's declination angle [deg] + δ = asind(sind(ϵ) * sind(L)) + + # JD_0 is the Julian date of the previous midnight (0h) UT1 + dt_midnight = DateTime(year(dt), month(dt), day(dt), 0, 0, 0) + jd_0 = datetime2julian(dt_midnight) + + # hours of UT1 elapsed since the previous midnight + H = (jd - jd_0) * 24.0 + day_ut = jd_0 - 2451545.0 + jd_tt = jd + δt / 86400.0 + D_tt = jd_tt - 2451545.0 + + # centuries since the year 2000 + t_cent = D_tt / 36525.0 + + # Greenwich mean sidereal time [hours] + gmst = if gmst_option == 1 + ( + 6.697375 + + 0.065707485828 * day_ut + + 1.0027379 * H + + 0.0854103 * t_cent + + 0.0000258 * t_cent^2 + ) + else # gmst_option == 2 + (6.697375 + 0.065709824279 * day_ut + 1.0027379 * H + 0.0000258 * t_cent^2) + end + gmst = mod(gmst, 24.0) - # longitude of the ascending node of the moon [deg] - Ω = 125.04 - 0.052954 * D_tt + # longitude of the ascending node of the moon [deg] + Ω = 125.04 - 0.052954 * D_tt - # mean longitude of the sun [deg] - L_s = 280.47 + 0.98565 * D_tt + # mean longitude of the sun [deg] + L_s = 280.47 + 0.98565 * D_tt - # nutation in longitude [hours] - Δψ = -0.000319 * sind(Ω) - 0.000024 * sind(2 * L_s) + # nutation in longitude [hours] + Δψ = -0.000319 * sind(Ω) - 0.000024 * sind(2 * L_s) - # obliquity of the ecliptic [deg] - ε = 23.4393 - 0.0000004 * D_tt + # obliquity of the ecliptic [deg] + ε = 23.4393 - 0.0000004 * D_tt - # equation of equinoxes [hours] - eqeq = Δψ * cosd(ε) + # equation of equinoxes [hours] + eqeq = Δψ * cosd(ε) - # Greenwich apparent sidereal time [hours] - gast = gmst + eqeq + # Greenwich apparent sidereal time [hours] + gast = gmst + eqeq - # local hour angle [deg], longitude is positive if it is east - ha = (gast - ra) * 15.0 + obs.longitude + # local hour angle [deg] + ha = (gast - ra) * 15.0 + longitude - # solar elevation [deg] - elevation = asind(cosd(ha) * cosd(δ) * obs.cos_lat + sind(δ) * obs.sin_lat) + # solar elevation [deg] + elevation = asind(cosd(ha) * cosd(δ) * cos_lat + sind(δ) * sin_lat) - # azimuth [deg] - azimuth = rad2deg(atan(-sind(ha), (tand(δ) * obs.cos_lat - obs.sin_lat * cosd(ha)))) + # azimuth [deg] + azimuth = + rad2deg(atan(-sind(ha), (tand(δ) * cos_lat - sin_lat * cosd(ha)))) - return SolPos{T}(mod(azimuth, 360.0), elevation, 90.0 - elevation) + pos[i] = SolPos{T}(mod(azimuth, 360.0), elevation, 90.0 - elevation) + end + return pos end -function _solar_position(obs, dt, alg::USNO, ::DefaultRefraction) - return _solar_position(obs, dt, alg, NoRefraction()) -end +_resolve_refraction(::USNO, ::DefaultRefraction, ::Type{T}) where {T} = NoRefraction() # USNO with DefaultRefraction returns SolPos (no refraction by default) result_type(::Type{USNO}, ::Type{DefaultRefraction}, ::Type{T}) where {T} = SolPos{T} diff --git a/src/Positioning/walraven.jl b/src/Positioning/walraven.jl index d852261..0a1d877 100644 --- a/src/Positioning/walraven.jl +++ b/src/Positioning/walraven.jl @@ -14,96 +14,106 @@ This algorithm is based on [Wal78](@cite) with corrections from the 1979 Erratum struct Walraven <: SolarAlgorithm end -function _solar_position(obs::Observer{T}, dt::DateTime, ::Walraven) where {T} - # use negative longitude (outdated convention used by Walraven) - longitude = -obs.longitude - - # calculate fractional hour - hour_frac = fractional_hour(dt) - δ = year(dt) - 1980 - - # leap year calculation (round towards zero) - leap = div(δ, 4) - time = δ * 365 + leap + dayofyear(dt) - 1 + hour_frac / 24 - - if δ == (leap * 4) - time -= 1 - end - if (δ < 0) && (δ != (leap * 4)) - time -= 1 - end - - # angular position in orbit [rad] - θ = 2 * T(π) * time / 365.25 - - # mean anomaly [rad] - g = -0.031271 - 4.53963e-7 * time + θ - - # longitude of the sun [rad] - lon_sun = ( - 4.900968 + - 3.67474e-7 * time + - (0.033434 - 2.3e-9 * time) * sin(g) + - 0.000349 * sin(2 * g) + - θ - ) - - # obliquity of ecliptic [rad] - ϵ = deg2rad(T(23.442)) - deg2rad(T(3.56e-7)) * time - (sin_lon_sun, cos_lon_sun) = sincos(lon_sun) - (sin_ϵ, cos_ϵ) = sincos(ϵ) - - # right ascension [rad] - ra = atan(sin_lon_sun * cos_ϵ, cos_lon_sun) - if ra < 0 - ra += 2 * T(π) - end - - # declination [rad] - d = asin(sin_lon_sun * sin_ϵ) - - # sidereal time [rad] - side_t = 1.759335 + 2 * T(π) * (time / 365.25 - δ) + 3.694e-7 * time - if side_t >= 2 * T(π) - side_t -= 2 * T(π) - end - - # local sidereal time [rad] - loc_s = side_t - deg2rad(T(longitude)) + deg2rad(T(hour_frac * 15)) - if loc_s >= 2 * T(π) - loc_s -= 2 * T(π) +function _solar_position!( + pos::StructArrays.StructVector{SolPos{T}}, + obs::Observer{T}, + dts::AbstractVector{DateTime}, + ::Walraven, + ) where {T} + + longitude = -obs.longitude # negative convention used by Walraven + sin_lat = obs.sin_lat + cos_lat = obs.cos_lat + + @inbounds for i in eachindex(dts, pos) + dt = dts[i] + + # calculate fractional hour + hour_frac = fractional_hour(dt) + δ = year(dt) - 1980 + + # leap year calculation (round towards zero) + leap = div(δ, 4) + time = δ * 365 + leap + dayofyear(dt) - 1 + hour_frac / 24 + + if δ == (leap * 4) + time -= 1 + end + if (δ < 0) && (δ != (leap * 4)) + time -= 1 + end + + # angular position in orbit [rad] + θ = 2 * T(π) * time / 365.25 + + # mean anomaly [rad] + g = -0.031271 - 4.53963e-7 * time + θ + + # longitude of the sun [rad] + lon_sun = ( + 4.900968 + + 3.67474e-7 * time + + (0.033434 - 2.3e-9 * time) * sin(g) + + 0.000349 * sin(2 * g) + + θ + ) + + # obliquity of ecliptic [rad] + ϵ = deg2rad(T(23.442)) - deg2rad(T(3.56e-7)) * time + (sin_lon_sun, cos_lon_sun) = sincos(lon_sun) + (sin_ϵ, cos_ϵ) = sincos(ϵ) + + # right ascension [rad] + ra = atan(sin_lon_sun * cos_ϵ, cos_lon_sun) + if ra < 0 + ra += 2 * T(π) + end + + # declination [rad] + d = asin(sin_lon_sun * sin_ϵ) + + # sidereal time [rad] + side_t = 1.759335 + 2 * T(π) * (time / 365.25 - δ) + 3.694e-7 * time + if side_t >= 2 * T(π) + side_t -= 2 * T(π) + end + + # local sidereal time [rad] + loc_s = side_t - deg2rad(T(longitude)) + deg2rad(T(hour_frac * 15)) + if loc_s >= 2 * T(π) + loc_s -= 2 * T(π) + end + + # hour angle [rad] + ha = ra - loc_s + (sin_d, cos_d) = sincos(d) + (sin_ha, cos_ha) = sincos(ha) + + # elevation [rad] + el = asin(sin_lat * sin_d + cos_lat * cos_d * cos_ha) + (sin_el, cos_el) = sincos(el) + + # azimuth [deg] - initial calculation + az = rad2deg(asin(cos_d * sin_ha / cos_el)) + + # azimuth quadrant assignment - Spencer (1989) correction + cos_az = sin_d - sin_el * sin_lat + + if (cos_az >= 0) && (sin(deg2rad(az)) < 0) + az = 360 + az + end + + if cos_az < 0 + az = 180 - az + end + + elevation_deg = rad2deg(el) + pos[i] = SolPos{T}(az, elevation_deg, 90 - elevation_deg) end - - # hour angle [rad] - ha = ra - loc_s - (sin_d, cos_d) = sincos(d) - (sin_ha, cos_ha) = sincos(ha) - - # elevation [rad] - el = asin(obs.sin_lat * sin_d + obs.cos_lat * cos_d * cos_ha) - (sin_el, cos_el) = sincos(el) - - # azimuth [deg] - initial calculation - az = rad2deg(asin(cos_d * sin_ha / cos_el)) - - # azimuth quadrant assignment - Spencer (1989) correction for all longitudes - cos_az = sin_d - sin_el * obs.sin_lat - - if (cos_az >= 0) && (sin(deg2rad(az)) < 0) - az = 360 + az - end - - if cos_az < 0 - az = 180 - az - end - - elevation_deg = rad2deg(el) - return SolPos{T}(az, elevation_deg, 90 - elevation_deg) + return pos end -function _solar_position(obs, dt, alg::Walraven, ::DefaultRefraction) - return _solar_position(obs, dt, alg, NoRefraction()) -end +_resolve_refraction(::Walraven, ::DefaultRefraction, ::Type{T}) where {T} = NoRefraction() # Walraven with DefaultRefraction returns SolPos (no refraction by default) result_type(::Type{Walraven}, ::Type{DefaultRefraction}, ::Type{T}) where {T} = SolPos{T} From d49187b80f57b414bdba666f5988669efbe6be71 Mon Sep 17 00:00:00 2001 From: Stefan de Lange Date: Tue, 14 Apr 2026 16:29:22 +0200 Subject: [PATCH 2/4] Stop tracking .CondaPkg --- .gitignore | 2 +- docs/.CondaPkg/.gitattributes | 2 - docs/.CondaPkg/.gitignore | 4 - docs/.CondaPkg/pixi.lock | 812 ---------------------------------- docs/.CondaPkg/pixi.toml | 21 - 5 files changed, 1 insertion(+), 840 deletions(-) delete mode 100644 docs/.CondaPkg/.gitattributes delete mode 100644 docs/.CondaPkg/.gitignore delete mode 100644 docs/.CondaPkg/pixi.lock delete mode 100644 docs/.CondaPkg/pixi.toml diff --git a/.gitignore b/.gitignore index e4af5c3..2ea6df6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,6 @@ node_modules .vscode/settings.json **.old play/Project.toml -docs/.CondaPkg/meta +.CondaPkg *.mem **/Manifest-*.toml diff --git a/docs/.CondaPkg/.gitattributes b/docs/.CondaPkg/.gitattributes deleted file mode 100644 index 887a2c1..0000000 --- a/docs/.CondaPkg/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# SCM syntax highlighting & preventing 3-way merges -pixi.lock merge=binary linguist-language=YAML linguist-generated=true diff --git a/docs/.CondaPkg/.gitignore b/docs/.CondaPkg/.gitignore deleted file mode 100644 index 740bb7d..0000000 --- a/docs/.CondaPkg/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ - -# pixi environments -.pixi -*.egg-info diff --git a/docs/.CondaPkg/pixi.lock b/docs/.CondaPkg/pixi.lock deleted file mode 100644 index 856bf79..0000000 --- a/docs/.CondaPkg/pixi.lock +++ /dev/null @@ -1,812 +0,0 @@ -version: 6 -environments: - default: - channels: - - url: https://conda.anaconda.org/conda-forge/ - indexes: - - https://pypi.org/simple - packages: - linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-h5347b49_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.11-hc97d973_100_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.9.16-h76e24b7_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - - pypi: https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/fd/04/9aea6b3ed8f1cba49c0f1cab70ff9e0cc5ff03555ffe8a41a2558047358b/pvlib-0.13.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3e/0f/2e2f4ca4eb9ab4d4fa469dd718ef03d13dc76a7b02b382500dec9e6e5665/solposx-1.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl -packages: -- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 - md5: d7c89558ba9fa0495403155b64376d81 - arch: x86_64 - platform: linux - license: None - purls: [] - size: 2562 - timestamp: 1578324546067 -- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 - md5: 73aaf86a425cc6e73fcf236a5a46396d - depends: - - _libgcc_mutex 0.1 conda_forge - - libgomp >=7.5.0 - constrains: - - openmp_impl 9999 - arch: x86_64 - platform: linux - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 23621 - timestamp: 1650670423406 -- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 - md5: 51a19bba1b8ebfb60df25cde030b7ebc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - arch: x86_64 - platform: linux - license: bzip2-1.0.6 - license_family: BSD - purls: [] - size: 260341 - timestamp: 1757437258798 -- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.11.12-hbd8a1cb_0.conda - sha256: b986ba796d42c9d3265602bc038f6f5264095702dd546c14bc684e60c385e773 - md5: f0991f0f84902f6b6009b4d2350a83aa - depends: - - __unix - license: ISC - purls: [] - size: 152432 - timestamp: 1762967197890 -- pypi: https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl - name: certifi - version: 2025.11.12 - sha256: 97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: charset-normalizer - version: 3.4.4 - sha256: a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: contourpy - version: 1.3.3 - sha256: 4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 - requires_dist: - - numpy>=1.25 - - furo ; extra == 'docs' - - sphinx>=7.2 ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - bokeh ; extra == 'bokeh' - - selenium ; extra == 'bokeh' - - contourpy[bokeh,docs] ; extra == 'mypy' - - bokeh ; extra == 'mypy' - - docutils-stubs ; extra == 'mypy' - - mypy==1.17.0 ; extra == 'mypy' - - types-pillow ; extra == 'mypy' - - contourpy[test-no-images] ; extra == 'test' - - matplotlib ; extra == 'test' - - pillow ; extra == 'test' - - pytest ; extra == 'test-no-images' - - pytest-cov ; extra == 'test-no-images' - - pytest-rerunfailures ; extra == 'test-no-images' - - pytest-xdist ; extra == 'test-no-images' - - wurlitzer ; extra == 'test-no-images' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - name: cycler - version: 0.12.1 - sha256: 85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 - requires_dist: - - ipython ; extra == 'docs' - - matplotlib ; extra == 'docs' - - numpydoc ; extra == 'docs' - - sphinx ; extra == 'docs' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/4e/80/c87bc524a90dbeb2a390eea23eae448286983da59b7e02c67fa0ca96a8c5/fonttools-4.61.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - name: fonttools - version: 4.61.0 - sha256: b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b - requires_dist: - - lxml>=4.0 ; extra == 'lxml' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' - - zopfli>=0.1.4 ; extra == 'woff' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'unicode' - - lz4>=1.7.4.2 ; extra == 'graphite' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' - - pycairo ; extra == 'interpolatable' - - matplotlib ; extra == 'plot' - - sympy ; extra == 'symfont' - - xattr ; sys_platform == 'darwin' and extra == 'type1' - - skia-pathops>=0.5.0 ; extra == 'pathops' - - uharfbuzz>=0.45.0 ; extra == 'repacker' - - lxml>=4.0 ; extra == 'all' - - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' - - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' - - zopfli>=0.1.4 ; extra == 'all' - - unicodedata2>=17.0.0 ; python_full_version < '3.15' and extra == 'all' - - lz4>=1.7.4.2 ; extra == 'all' - - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' - - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' - - pycairo ; extra == 'all' - - matplotlib ; extra == 'all' - - sympy ; extra == 'all' - - xattr ; sys_platform == 'darwin' and extra == 'all' - - skia-pathops>=0.5.0 ; extra == 'all' - - uharfbuzz>=0.45.0 ; extra == 'all' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: h5py - version: 3.15.1 - sha256: 121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8 - requires_dist: - - numpy>=1.21.2 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - name: idna - version: '3.11' - sha256: 771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea - requires_dist: - - ruff>=0.6.2 ; extra == 'all' - - mypy>=1.11.2 ; extra == 'all' - - pytest>=8.3.2 ; extra == 'all' - - flake8>=7.1.1 ; extra == 'all' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: kiwisolver - version: 1.4.9 - sha256: b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda - sha256: 9e191baf2426a19507f1d0a17be0fdb7aa155cdf0f61d5a09c808e0a69464312 - md5: a6abd2796fc332536735f68ba23f7901 - depends: - - __glibc >=2.17,<3.0.a0 - - zstd >=1.5.7,<1.6.0a0 - constrains: - - binutils_impl_linux-64 2.45 - arch: x86_64 - platform: linux - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 725545 - timestamp: 1764007826689 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda - sha256: 1e1b08f6211629cbc2efe7a5bca5953f8f6b3cae0eeb04ca4dacee1bd4e2db2f - md5: 8b09ae86839581147ef2e5c5e229d164 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - constrains: - - expat 2.7.3.* - arch: x86_64 - platform: linux - license: MIT - license_family: MIT - purls: [] - size: 76643 - timestamp: 1763549731408 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h9ec8514_0.conda - sha256: 25cbdfa65580cfab1b8d15ee90b4c9f1e0d72128f1661449c9a999d341377d54 - md5: 35f29eec58405aaf55e01cb470d8c26a - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - arch: x86_64 - platform: linux - license: MIT - license_family: MIT - purls: [] - size: 57821 - timestamp: 1760295480630 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda - sha256: 0caed73aac3966bfbf5710e06c728a24c6c138605121a3dacb2e03440e8baa6a - md5: 264fbfba7fb20acf3b29cde153e345ce - depends: - - __glibc >=2.17,<3.0.a0 - - _openmp_mutex >=4.5 - constrains: - - libgomp 15.1.0 h767d61c_5 - - libgcc-ng ==15.1.0=*_5 - arch: x86_64 - platform: linux - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 824191 - timestamp: 1757042543820 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda - sha256: 125051d51a8c04694d0830f6343af78b556dd88cc249dfec5a97703ebfb1832d - md5: dcd5ff1940cd38f6df777cac86819d60 - depends: - - __glibc >=2.17,<3.0.a0 - arch: x86_64 - platform: linux - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 447215 - timestamp: 1757042483384 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 - md5: 1a580f7796c7bf6393fddb8bbbde58dc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - xz 5.8.1.* - arch: x86_64 - platform: linux - license: 0BSD - purls: [] - size: 112894 - timestamp: 1749230047870 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee - md5: c7e925f37e3b40d893459e625f6a53f1 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - arch: x86_64 - platform: linux - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 91183 - timestamp: 1748393666725 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.1-h0c1763c_0.conda - sha256: 6f0e8a812e8e33a4d8b7a0e595efe28373080d27b78ee4828aa4f6649a088454 - md5: 2e1b84d273b01835256e53fd938de355 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux - license: blessing - purls: [] - size: 938979 - timestamp: 1764359444435 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda - sha256: 0f5f61cab229b6043541c13538d75ce11bd96fb2db76f94ecf81997b1fde6408 - md5: 4e02a49aaa9d5190cb630fa43528fbe6 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc 15.1.0 h767d61c_5 - arch: x86_64 - platform: linux - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 3896432 - timestamp: 1757042571458 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda - sha256: 7b8cabbf0ab4fe3581ca28fe8ca319f964078578a51dd2ca3f703c1d21ba23ff - md5: 8bba50c7f4679f08c861b597ad2bda6b - depends: - - libstdcxx 15.1.0 h8f9b012_5 - arch: x86_64 - platform: linux - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 29233 - timestamp: 1757042603319 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-h5347b49_1.conda - sha256: 030447cf827c471abd37092ab9714fde82b8222106f22fde94bc7a64e2704c40 - md5: 41f5c09a211985c3ce642d60721e7c3e - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - arch: x86_64 - platform: linux - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 40235 - timestamp: 1764790744114 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 - md5: edb0dca6bc32e4f4789199455a1dbeb8 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - zlib 1.3.1 *_2 - arch: x86_64 - platform: linux - license: Zlib - license_family: Other - purls: [] - size: 60963 - timestamp: 1727963148474 -- pypi: https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: matplotlib - version: 3.10.7 - sha256: b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1 - requires_dist: - - contourpy>=1.0.1 - - cycler>=0.10 - - fonttools>=4.22.0 - - kiwisolver>=1.3.1 - - numpy>=1.23 - - packaging>=20.0 - - pillow>=8 - - pyparsing>=3 - - python-dateutil>=2.7 - - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' - - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' - - setuptools-scm>=7 ; extra == 'dev' - - setuptools>=64 ; extra == 'dev' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 - md5: 47e340acb35de30501a76c7c799c41d7 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - arch: x86_64 - platform: linux - license: X11 AND BSD-3-Clause - purls: [] - size: 891641 - timestamp: 1738195959188 -- pypi: https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: numpy - version: 2.3.5 - sha256: 11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017 - requires_python: '>=3.11' -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda - sha256: e807f3bad09bdf4075dbb4168619e14b0c0360bacb2e12ef18641a834c8c5549 - md5: 14edad12b59ccbfa3910d42c72adc2a0 - depends: - - __glibc >=2.17,<3.0.a0 - - ca-certificates - - libgcc >=14 - arch: x86_64 - platform: linux - license: Apache-2.0 - license_family: Apache - purls: [] - size: 3119624 - timestamp: 1759324353651 -- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - name: packaging - version: '25.0' - sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - name: pandas - version: 2.3.3 - sha256: 318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac - requires_dist: - - numpy>=1.22.4 ; python_full_version < '3.11' - - numpy>=1.23.2 ; python_full_version == '3.11.*' - - numpy>=1.26.0 ; python_full_version >= '3.12' - - python-dateutil>=2.8.2 - - pytz>=2020.1 - - tzdata>=2022.7 - - hypothesis>=6.46.1 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=2.2.0 ; extra == 'test' - - pyarrow>=10.0.1 ; extra == 'pyarrow' - - bottleneck>=1.3.6 ; extra == 'performance' - - numba>=0.56.4 ; extra == 'performance' - - numexpr>=2.8.4 ; extra == 'performance' - - scipy>=1.10.0 ; extra == 'computation' - - xarray>=2022.12.0 ; extra == 'computation' - - fsspec>=2022.11.0 ; extra == 'fss' - - s3fs>=2022.11.0 ; extra == 'aws' - - gcsfs>=2022.11.0 ; extra == 'gcp' - - pandas-gbq>=0.19.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.0 ; extra == 'excel' - - python-calamine>=0.1.7 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.0.5 ; extra == 'excel' - - pyarrow>=10.0.1 ; extra == 'parquet' - - pyarrow>=10.0.1 ; extra == 'feather' - - tables>=3.8.0 ; extra == 'hdf5' - - pyreadstat>=1.2.0 ; extra == 'spss' - - sqlalchemy>=2.0.0 ; extra == 'postgresql' - - psycopg2>=2.9.6 ; extra == 'postgresql' - - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.0 ; extra == 'mysql' - - pymysql>=1.0.2 ; extra == 'mysql' - - sqlalchemy>=2.0.0 ; extra == 'sql-other' - - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' - - beautifulsoup4>=4.11.2 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'html' - - lxml>=4.9.2 ; extra == 'xml' - - matplotlib>=3.6.3 ; extra == 'plot' - - jinja2>=3.1.2 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.3.0 ; extra == 'clipboard' - - zstandard>=0.19.0 ; extra == 'compression' - - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' - - adbc-driver-postgresql>=0.8.0 ; extra == 'all' - - adbc-driver-sqlite>=0.8.0 ; extra == 'all' - - beautifulsoup4>=4.11.2 ; extra == 'all' - - bottleneck>=1.3.6 ; extra == 'all' - - dataframe-api-compat>=0.1.7 ; extra == 'all' - - fastparquet>=2022.12.0 ; extra == 'all' - - fsspec>=2022.11.0 ; extra == 'all' - - gcsfs>=2022.11.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.46.1 ; extra == 'all' - - jinja2>=3.1.2 ; extra == 'all' - - lxml>=4.9.2 ; extra == 'all' - - matplotlib>=3.6.3 ; extra == 'all' - - numba>=0.56.4 ; extra == 'all' - - numexpr>=2.8.4 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.0 ; extra == 'all' - - pandas-gbq>=0.19.0 ; extra == 'all' - - psycopg2>=2.9.6 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' - - pymysql>=1.0.2 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.0 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=2.2.0 ; extra == 'all' - - python-calamine>=0.1.7 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.3.0 ; extra == 'all' - - scipy>=1.10.0 ; extra == 'all' - - s3fs>=2022.11.0 ; extra == 'all' - - sqlalchemy>=2.0.0 ; extra == 'all' - - tables>=3.8.0 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2022.12.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.0.5 ; extra == 'all' - - zstandard>=0.19.0 ; extra == 'all' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - name: pillow - version: 12.0.0 - sha256: f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 - requires_dist: - - furo ; extra == 'docs' - - olefile ; extra == 'docs' - - sphinx>=8.2 ; extra == 'docs' - - sphinx-autobuild ; extra == 'docs' - - sphinx-copybutton ; extra == 'docs' - - sphinx-inline-tabs ; extra == 'docs' - - sphinxext-opengraph ; extra == 'docs' - - olefile ; extra == 'fpx' - - olefile ; extra == 'mic' - - arro3-compute ; extra == 'test-arrow' - - arro3-core ; extra == 'test-arrow' - - nanoarrow ; extra == 'test-arrow' - - pyarrow ; extra == 'test-arrow' - - check-manifest ; extra == 'tests' - - coverage>=7.4.2 ; extra == 'tests' - - defusedxml ; extra == 'tests' - - markdown2 ; extra == 'tests' - - olefile ; extra == 'tests' - - packaging ; extra == 'tests' - - pyroma>=5 ; extra == 'tests' - - pytest ; extra == 'tests' - - pytest-cov ; extra == 'tests' - - pytest-timeout ; extra == 'tests' - - pytest-xdist ; extra == 'tests' - - trove-classifiers>=2024.10.12 ; extra == 'tests' - - defusedxml ; extra == 'xmp' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/fd/04/9aea6b3ed8f1cba49c0f1cab70ff9e0cc5ff03555ffe8a41a2558047358b/pvlib-0.13.1-py3-none-any.whl - name: pvlib - version: 0.13.1 - sha256: 7de5c71b2f6b92f66eb4ac5d7d26a145c209cc6036dd1c90a7a323bc097b3e46 - requires_dist: - - numpy>=1.21.2 - - pandas>=1.3.3 - - pytz - - requests - - scipy>=1.7.2 - - h5py - - cython ; extra == 'optional' - - ephem ; extra == 'optional' - - nrel-pysam ; extra == 'optional' - - numba>=0.17.0 ; extra == 'optional' - - solarfactors ; extra == 'optional' - - statsmodels ; extra == 'optional' - - ipython ; extra == 'doc' - - pickleshare ; extra == 'doc' - - matplotlib ; extra == 'doc' - - sphinx==7.3.7 ; extra == 'doc' - - pydata-sphinx-theme==0.15.4 ; extra == 'doc' - - sphinx-gallery ; extra == 'doc' - - docutils==0.21 ; extra == 'doc' - - pillow ; extra == 'doc' - - sphinx-toggleprompt==0.5.2 ; extra == 'doc' - - sphinx-favicon ; extra == 'doc' - - solarfactors ; extra == 'doc' - - sphinx-hoverxref~=1.4.2 ; extra == 'doc' - - pytest ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-mock ; extra == 'test' - - requests-mock ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-rerunfailures ; extra == 'test' - - pytest-remotedata ; extra == 'test' - - packaging ; extra == 'test' - - pvlib[doc,optional,test] ; extra == 'all' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - name: pyparsing - version: 3.2.5 - sha256: e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e - requires_dist: - - railroad-diagrams ; extra == 'diagrams' - - jinja2 ; extra == 'diagrams' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.11-hc97d973_100_cp313.conda - build_number: 100 - sha256: 9cf014cf28e93ee242bacfbf664e8b45ae06e50b04291e640abeaeb0cba0364c - md5: 0cbb0010f1d8ecb64a428a8d4214609e - depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - ld_impl_linux-64 >=2.36.1 - - libexpat >=2.7.3,<3.0a0 - - libffi >=3.5.2,<3.6.0a0 - - libgcc >=14 - - liblzma >=5.8.1,<6.0a0 - - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.51.1,<4.0a0 - - libuuid >=2.41.2,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.5.4,<4.0a0 - - python_abi 3.13.* *_cp313 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - arch: x86_64 - platform: linux - license: Python-2.0 - purls: [] - size: 37226336 - timestamp: 1765021889577 - python_site_packages_path: lib/python3.13/site-packages -- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - name: python-dateutil - version: 2.9.0.post0 - sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - requires_dist: - - six>=1.5 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' -- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - build_number: 8 - sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 - md5: 94305520c52a4aa3f6c2b1ff6008d9f8 - constrains: - - python 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 7002 - timestamp: 1752805902938 -- pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - name: pytz - version: '2025.2' - sha256: 5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 -- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c - md5: 283b96675859b20a825f8fa30f311446 - depends: - - libgcc >=13 - - ncurses >=6.5,<7.0a0 - arch: x86_64 - platform: linux - license: GPL-3.0-only - license_family: GPL - purls: [] - size: 282480 - timestamp: 1740379431762 -- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - name: requests - version: 2.32.5 - sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 - requires_dist: - - charset-normalizer>=2,<4 - - idna>=2.5,<4 - - urllib3>=1.21.1,<3 - - certifi>=2017.4.17 - - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - name: scipy - version: 1.16.3 - sha256: 7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88 - requires_dist: - - numpy>=1.25.2,<2.6 - - pytest>=8.0.0 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-timeout ; extra == 'test' - - pytest-xdist ; extra == 'test' - - asv ; extra == 'test' - - mpmath ; extra == 'test' - - gmpy2 ; extra == 'test' - - threadpoolctl ; extra == 'test' - - scikit-umfpack ; extra == 'test' - - pooch ; extra == 'test' - - hypothesis>=6.30 ; extra == 'test' - - array-api-strict>=2.3.1 ; extra == 'test' - - cython ; extra == 'test' - - meson ; extra == 'test' - - ninja ; sys_platform != 'emscripten' and extra == 'test' - - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' - - intersphinx-registry ; extra == 'doc' - - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' - - sphinx-copybutton ; extra == 'doc' - - sphinx-design>=0.4.0 ; extra == 'doc' - - matplotlib>=3.5 ; extra == 'doc' - - numpydoc ; extra == 'doc' - - jupytext ; extra == 'doc' - - myst-nb>=1.2.0 ; extra == 'doc' - - pooch ; extra == 'doc' - - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' - - jupyterlite-pyodide-kernel ; extra == 'doc' - - linkify-it-py ; extra == 'doc' - - mypy==1.10.0 ; extra == 'dev' - - typing-extensions ; extra == 'dev' - - types-psutil ; extra == 'dev' - - pycodestyle ; extra == 'dev' - - ruff>=0.0.292 ; extra == 'dev' - - cython-lint>=0.12.2 ; extra == 'dev' - - rich-click ; extra == 'dev' - - doit>=0.36.0 ; extra == 'dev' - - pydevtool ; extra == 'dev' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - name: six - version: 1.17.0 - sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' -- pypi: https://files.pythonhosted.org/packages/3e/0f/2e2f4ca4eb9ab4d4fa469dd718ef03d13dc76a7b02b382500dec9e6e5665/solposx-1.0.1-py3-none-any.whl - name: solposx - version: 1.0.1 - sha256: 8ef2d363724c6ef6ba12684cfab2a3e038e235d6b640c9ec615a0c342667bd9a - requires_dist: - - numpy - - matplotlib - - pandas - - pvlib - - skyfield>=1.51 ; extra == 'optional' - - sg2 ; extra == 'optional' - - pytest>=7 ; extra == 'test' - - pytest-cov ; extra == 'test' - - packaging ; extra == 'test' - - solposx[optional] ; extra == 'doc' - - sphinx==8.2.3 ; extra == 'doc' - - myst-nb==1.1.2 ; extra == 'doc' - - sphinx-book-theme==1.1.4 ; extra == 'doc' - - pvlib==0.13.0 ; extra == 'doc' - - pandas==2.3.0 ; extra == 'doc' - - solposx[doc,optional,test] ; extra == 'all' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda - sha256: 1544760538a40bcd8ace2b1d8ebe3eb5807ac268641f8acdc18c69c5ebfeaf64 - md5: 86bc20552bf46075e3d92b67f089172d - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - constrains: - - xorg-libx11 >=1.8.12,<2.0a0 - arch: x86_64 - platform: linux - license: TCL - license_family: BSD - purls: [] - size: 3284905 - timestamp: 1763054914403 -- pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - name: tzdata - version: '2025.2' - sha256: 1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 - requires_python: '>=2' -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 - md5: 4222072737ccff51314b5ece9c7d6f5a - license: LicenseRef-Public-Domain - purls: [] - size: 122968 - timestamp: 1742727099393 -- pypi: https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl - name: urllib3 - version: 2.6.1 - sha256: e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b - requires_dist: - - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' - - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' - - h2>=4,<5 ; extra == 'h2' - - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' - - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.9.16-h76e24b7_0.conda - sha256: ecb1c52867be0e7a6854d83cb8618bfbcbe77989e947f9abb04d63bb2a57cbe3 - md5: 9092334a4b811b29749fa8822f4535b9 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - constrains: - - __glibc >=2.17 - arch: x86_64 - platform: linux - license: Apache-2.0 OR MIT - purls: [] - size: 17634430 - timestamp: 1765049000112 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 - md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 - depends: - - __glibc >=2.17,<3.0.a0 - - libzlib >=1.3.1,<2.0a0 - arch: x86_64 - platform: linux - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 601375 - timestamp: 1764777111296 diff --git a/docs/.CondaPkg/pixi.toml b/docs/.CondaPkg/pixi.toml deleted file mode 100644 index 09daa2d..0000000 --- a/docs/.CondaPkg/pixi.toml +++ /dev/null @@ -1,21 +0,0 @@ -[dependencies] -openssl = ">=3, <3.6" -libstdcxx = ">=3.4,<=15.1" -uv = ">=0.4" -libstdcxx-ng = ">=3.4,<=15.1" - - [dependencies.python] - channel = "conda-forge" - build = "*cp*" - version = ">=3.10,!=3.14.0,!=3.14.1,<4" - -[project] -name = ".CondaPkg" -platforms = ["linux-64"] -channels = ["conda-forge"] -channel-priority = "strict" -description = "automatically generated by CondaPkg.jl" - -[pypi-dependencies] -pandas = "*" -solposx = "*" From e84883fb340eee8dc9fd00b9c975c4d3f6c2d7f4 Mon Sep 17 00:00:00 2001 From: Stefan de Lange Date: Tue, 14 Apr 2026 16:34:57 +0200 Subject: [PATCH 3/4] Remove comments --- src/Positioning/noaa.jl | 2 +- src/Positioning/psa.jl | 2 +- src/Positioning/usno.jl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Positioning/noaa.jl b/src/Positioning/noaa.jl index 34f3826..abb7f0b 100644 --- a/src/Positioning/noaa.jl +++ b/src/Positioning/noaa.jl @@ -40,7 +40,7 @@ function _solar_position!( dts::AbstractVector{DateTime}, alg::NOAA, ) where {T} - # Hoist algorithm and observer constants out of the loop + δt = if alg.delta_t === nothing calculate_deltat(dts[1]) else diff --git a/src/Positioning/psa.jl b/src/Positioning/psa.jl index f5d8c99..db3edcc 100644 --- a/src/Positioning/psa.jl +++ b/src/Positioning/psa.jl @@ -70,7 +70,7 @@ function _solar_position!( dts::AbstractVector{DateTime}, alg::PSA, ) where {T} - # Hoist algorithm parameters and observer constants out of the loop + p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 = get_psa_params(alg.coeffs) diff --git a/src/Positioning/usno.jl b/src/Positioning/usno.jl index e2dc5dd..95cc92a 100644 --- a/src/Positioning/usno.jl +++ b/src/Positioning/usno.jl @@ -37,7 +37,7 @@ function _solar_position!( dts::AbstractVector{DateTime}, alg::USNO, ) where {T <: AbstractFloat} - # Hoist algorithm and observer constants out of the loop + δt::T = if alg.delta_t === nothing calculate_deltat(dts[1]) else From aa9eb8e0e89508139f7a77c12efaab036dea6040 Mon Sep 17 00:00:00 2001 From: Stefan de Lange Date: Tue, 14 Apr 2026 16:52:13 +0200 Subject: [PATCH 4/4] Add single path back --- src/Positioning/Positioning.jl | 28 ++++- src/Positioning/noaa.jl | 155 +++++++++++++-------------- src/Positioning/psa.jl | 91 +++++++++------- src/Positioning/spa.jl | 5 + src/Positioning/usno.jl | 187 ++++++++++++++++++--------------- src/Positioning/walraven.jl | 175 +++++++++++++++--------------- 6 files changed, 351 insertions(+), 290 deletions(-) diff --git a/src/Positioning/Positioning.jl b/src/Positioning/Positioning.jl index 78d44b9..7e9a096 100644 --- a/src/Positioning/Positioning.jl +++ b/src/Positioning/Positioning.jl @@ -322,7 +322,28 @@ function _solar_position!( return pos end -# Scalar public API routes through the vectorized path +# --- Scalar internal interface --- + +# Scalar refraction wrappers (zero-allocation path for single datetimes) +function _solar_position(obs, dt, alg::SolarAlgorithm, ::NoRefraction) + return _solar_position(obs, dt, alg) +end + +function _solar_position(obs, dt, alg::SolarAlgorithm, refraction::RefractionAlgorithm) + pos = _solar_position(obs, dt, alg) + correction = Refraction.refraction(refraction, pos.elevation) + apparent_elevation_deg = pos.elevation + correction + apparent_zenith_deg = 90 - apparent_elevation_deg + return ApparentSolPos( + pos.azimuth, + pos.elevation, + pos.zenith, + apparent_elevation_deg, + apparent_zenith_deg, + ) +end + +# Scalar public API (zero-allocation) function solar_position( obs::AbstractObserver{T}, dt::DateTime, @@ -330,10 +351,7 @@ function solar_position( refraction::RefractionAlgorithm = DefaultRefraction(), ) where {T <: AbstractFloat} resolved = _resolve_refraction(alg, refraction, T) - RetType = result_type(typeof(alg), typeof(resolved), T) - pos = StructArrays.StructVector{RetType}(undef, 1) - _solar_position!(pos, obs, DateTime[dt], alg, resolved) - return pos[1] + return _solar_position(obs, dt, alg, resolved) end function solar_position( diff --git a/src/Positioning/noaa.jl b/src/Positioning/noaa.jl index abb7f0b..41e732e 100644 --- a/src/Positioning/noaa.jl +++ b/src/Positioning/noaa.jl @@ -33,6 +33,83 @@ end NOAA() = NOAA(67.0) # default delta_t value (2020 default from pvlib) +# Core computation shared by scalar and vectorized paths +@inline function _noaa_kernel( + dt::DateTime, sin_lat, cos_lat, longitude, ::Type{T}, + ) where {T} + # convert to Julian date and Julian century + jd = datetime2julian(dt) + jc = (jd - 2451545.0) / 36525.0 + + # mean longitude of the sun [degrees] + mean_long = mod(280.46646 + jc * (36000.76983 + jc * 0.0003032), 360.0) + + # mean anomaly [degrees] + mean_anom = 357.52911 + jc * (35999.05029 - 0.0001537 * jc) + + # eccentricity of Earth's orbit + eccent = 0.016708634 - jc * (0.000042037 + 0.0000001267 * jc) + + # sun equation of center [degrees] + sun_eq_ctr = ( + sind(mean_anom) * (1.914602 - jc * (0.004817 + 0.000014 * jc)) + + sind(2 * mean_anom) * (0.019993 - 0.000101 * jc) + + sind(3 * mean_anom) * 0.000289 + ) + + # sun true/apparent longitude [degrees] + sun_true_long = mean_long + sun_eq_ctr + sun_app_long = sun_true_long - 0.00569 - 0.00478 * sind(125.04 - 1934.136 * jc) + + # mean obliquity of ecliptic [degrees] + mean_obliq = + 23.0 + + (26.0 + (21.448 - jc * (46.815 + jc * (0.00059 - jc * 0.001813))) / 60.0) / + 60.0 + + # obliquity correction [degrees] + obliq_corr = mean_obliq + 0.00256 * cosd(125.04 - 1934.136 * jc) + sun_declin = asind(sind(obliq_corr) * sind(sun_app_long)) + + # equation of time [minutes] + var_y = tand(obliq_corr / 2.0)^2 + eot = + 4.0 * rad2deg( + var_y * sind(2.0 * mean_long) - 2.0 * eccent * sind(mean_anom) + + 4.0 * eccent * var_y * sind(mean_anom) * cosd(2.0 * mean_long) - + 0.5 * var_y^2 * sind(4.0 * mean_long) - + 1.25 * eccent^2 * sind(2.0 * mean_anom), + ) + + # true solar time [minutes] + hour_frac = fractional_hour(dt) + minutes = hour_frac * 60.0 + true_solar_time = mod(minutes + eot + 4.0 * longitude, 1440.0) + + # hour angle [degrees] + hour_angle = true_solar_time / 4.0 - 180.0 + + # zenith angle [degrees] + zenith = acosd( + sin_lat * sind(sun_declin) + cos_lat * cosd(sun_declin) * cosd(hour_angle), + ) + + # azimuth angle [degrees] + azimuth_numerator = sin_lat * cosd(zenith) - sind(sun_declin) + azimuth_denominator = cos_lat * sind(zenith) + + azimuth = if hour_angle > 0.0 + mod(acosd(azimuth_numerator / azimuth_denominator) + 180.0, 360.0) + else + mod(540.0 - acosd(azimuth_numerator / azimuth_denominator), 360.0) + end + + return SolPos{T}(azimuth, 90.0 - zenith, zenith) +end + +function _solar_position(obs::Observer{T}, dt::DateTime, alg::NOAA) where {T} + return _noaa_kernel(dt, obs.sin_lat, obs.cos_lat, obs.longitude, T) +end function _solar_position!( pos::StructArrays.StructVector{SolPos{T}}, @@ -40,88 +117,12 @@ function _solar_position!( dts::AbstractVector{DateTime}, alg::NOAA, ) where {T} - - δt = if alg.delta_t === nothing - calculate_deltat(dts[1]) - else - alg.delta_t - end - sin_lat = obs.sin_lat cos_lat = obs.cos_lat longitude = obs.longitude @inbounds for i in eachindex(dts, pos) - dt = dts[i] - - # convert to Julian date and Julian century - jd = datetime2julian(dt) - jc = (jd - 2451545.0) / 36525.0 - - # mean longitude of the sun [degrees] - mean_long = mod(280.46646 + jc * (36000.76983 + jc * 0.0003032), 360.0) - - # mean anomaly [degrees] - mean_anom = 357.52911 + jc * (35999.05029 - 0.0001537 * jc) - - # eccentricity of Earth's orbit - eccent = 0.016708634 - jc * (0.000042037 + 0.0000001267 * jc) - - # sun equation of center [degrees] - sun_eq_ctr = ( - sind(mean_anom) * (1.914602 - jc * (0.004817 + 0.000014 * jc)) + - sind(2 * mean_anom) * (0.019993 - 0.000101 * jc) + - sind(3 * mean_anom) * 0.000289 - ) - - # sun true/apparent longitude [degrees] - sun_true_long = mean_long + sun_eq_ctr - sun_app_long = sun_true_long - 0.00569 - 0.00478 * sind(125.04 - 1934.136 * jc) - - # mean obliquity of ecliptic [degrees] - mean_obliq = - 23.0 + - (26.0 + (21.448 - jc * (46.815 + jc * (0.00059 - jc * 0.001813))) / 60.0) / - 60.0 - - # obliquity correction [degrees] - obliq_corr = mean_obliq + 0.00256 * cosd(125.04 - 1934.136 * jc) - sun_declin = asind(sind(obliq_corr) * sind(sun_app_long)) - - # equation of time [minutes] - var_y = tand(obliq_corr / 2.0)^2 - eot = - 4.0 * rad2deg( - var_y * sind(2.0 * mean_long) - 2.0 * eccent * sind(mean_anom) + - 4.0 * eccent * var_y * sind(mean_anom) * cosd(2.0 * mean_long) - - 0.5 * var_y^2 * sind(4.0 * mean_long) - - 1.25 * eccent^2 * sind(2.0 * mean_anom), - ) - - # true solar time [minutes] - hour_frac = fractional_hour(dt) - minutes = hour_frac * 60.0 - true_solar_time = mod(minutes + eot + 4.0 * longitude, 1440.0) - - # hour angle [degrees] - hour_angle = true_solar_time / 4.0 - 180.0 - - # zenith angle [degrees] - zenith = acosd( - sin_lat * sind(sun_declin) + cos_lat * cosd(sun_declin) * cosd(hour_angle), - ) - - # azimuth angle [degrees] - azimuth_numerator = sin_lat * cosd(zenith) - sind(sun_declin) - azimuth_denominator = cos_lat * sind(zenith) - - azimuth = if hour_angle > 0.0 - mod(acosd(azimuth_numerator / azimuth_denominator) + 180.0, 360.0) - else - mod(540.0 - acosd(azimuth_numerator / azimuth_denominator), 360.0) - end - - pos[i] = SolPos{T}(azimuth, 90.0 - zenith, zenith) + pos[i] = _noaa_kernel(dts[i], sin_lat, cos_lat, longitude, T) end return pos end diff --git a/src/Positioning/psa.jl b/src/Positioning/psa.jl index db3edcc..a7a84e7 100644 --- a/src/Positioning/psa.jl +++ b/src/Positioning/psa.jl @@ -64,56 +64,73 @@ PSA() = PSA(2020) end end +# Core computation shared by scalar and vectorized paths +@inline function _psa_kernel( + dt::DateTime, + p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + cos_lat, sin_lat, λt, ::Type{T}, + ) where {T} + # elapsed julian days (n) since J2000.0 + jd = datetime2julian(dt) + n = jd - 2451545.0 + + # ecliptic coordinates + Ω = p1 + p2 * n + L = p3 + p4 * n + g = p5 + p6 * n + (sin_Ω, cos_Ω) = sincos(Ω) + λₑ = L + p7 * sin(g) + p8 * sin(2 * g) + p9 + p10 * sin_Ω + ϵ = p11 + p12 * n + p13 * cos_Ω + + # celestial right ascension and declination + (sin_ϵ, cos_ϵ) = sincos(ϵ) + (sin_λₑ, cos_λₑ) = sincos(λₑ) + ra = atan(cos_ϵ * sin_λₑ, cos_λₑ) + ra = mod2pi(ra) + δ = asin(sin_ϵ * sin_λₑ) + + # local coordinates + hour = fractional_hour(dt) + gmst = p14 + p15 * n + hour + lmst = deg2rad(gmst * 15 + λt) + ω = lmst - ra + (sin_δ, cos_δ) = sincos(δ) + (sin_ω, cos_ω) = sincos(ω) + θz = acos(cos_lat * cos_ω * cos_δ + sin_δ * sin_lat) + γ = atan(-sin_ω, (tan(δ) * cos_lat - sin_lat * cos_ω)) + + # parallax correction + θz = θz + (EMR / AU) * sin(θz) + + return SolPos{T}(mod(rad2deg(γ), 360.0), rad2deg(π / 2 - θz), rad2deg(θz)) +end + +function _solar_position(obs::Observer{T}, dt::DateTime, alg::PSA) where {T} + p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 = + get_psa_params(alg.coeffs) + return _psa_kernel( + dt, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + obs.cos_lat, obs.sin_lat, rad2deg(obs.longitude_rad), T, + ) +end + function _solar_position!( pos::StructArrays.StructVector{SolPos{T}}, obs::Observer{T}, dts::AbstractVector{DateTime}, alg::PSA, ) where {T} - p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 = get_psa_params(alg.coeffs) - cos_lat = obs.cos_lat sin_lat = obs.sin_lat λt = rad2deg(obs.longitude_rad) @inbounds for i in eachindex(dts, pos) - dt = dts[i] - - # elapsed julian days (n) since J2000.0 - jd = datetime2julian(dt) - n = jd - 2451545.0 - - # ecliptic coordinates - Ω = p1 + p2 * n - L = p3 + p4 * n - g = p5 + p6 * n - (sin_Ω, cos_Ω) = sincos(Ω) - λₑ = L + p7 * sin(g) + p8 * sin(2 * g) + p9 + p10 * sin_Ω - ϵ = p11 + p12 * n + p13 * cos_Ω - - # celestial right ascension and declination - (sin_ϵ, cos_ϵ) = sincos(ϵ) - (sin_λₑ, cos_λₑ) = sincos(λₑ) - ra = atan(cos_ϵ * sin_λₑ, cos_λₑ) - ra = mod2pi(ra) - δ = asin(sin_ϵ * sin_λₑ) - - # local coordinates - hour = fractional_hour(dt) - gmst = p14 + p15 * n + hour - lmst = deg2rad(gmst * 15 + λt) - ω = lmst - ra - (sin_δ, cos_δ) = sincos(δ) - (sin_ω, cos_ω) = sincos(ω) - θz = acos(cos_lat * cos_ω * cos_δ + sin_δ * sin_lat) - γ = atan(-sin_ω, (tan(δ) * cos_lat - sin_lat * cos_ω)) - - # parallax correction - θz = θz + (EMR / AU) * sin(θz) - - pos[i] = SolPos{T}(mod(rad2deg(γ), 360.0), rad2deg(π / 2 - θz), rad2deg(θz)) + pos[i] = _psa_kernel( + dts[i], p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, + cos_lat, sin_lat, λt, T, + ) end return pos end diff --git a/src/Positioning/spa.jl b/src/Positioning/spa.jl index af3acb7..5590bd1 100644 --- a/src/Positioning/spa.jl +++ b/src/Positioning/spa.jl @@ -433,6 +433,11 @@ function _solar_position( return SolPos{T}(az, e0, θz0) end +function _solar_position(obs::Observer{T}, dt::DateTime, alg::SPA) where {T <: AbstractFloat} + spa_obs = SPAObserver{T}(obs.latitude, obs.longitude, obs.altitude) + return _solar_position(spa_obs, dt, alg) +end + function _solar_position!( pos::StructArrays.StructVector{SolPos{T}}, obs::Observer{T}, diff --git a/src/Positioning/usno.jl b/src/Positioning/usno.jl index 95cc92a..fe22d83 100644 --- a/src/Positioning/usno.jl +++ b/src/Positioning/usno.jl @@ -30,6 +30,105 @@ end USNO() = USNO(67.0, 1) # default delta_t value and gmst_option +# Core computation shared by scalar and vectorized paths +@inline function _usno_kernel( + dt::DateTime, δt::T, sin_lat, cos_lat, longitude, gmst_option, + ::Type{T}, + ) where {T <: AbstractFloat} + # convert to Julian date + jd = datetime2julian(dt) + + # days since J2000.0 + D = jd - 2451545.0 + + # mean anomaly of the sun [deg] + g = 357.529 + 0.98560028 * D + g = mod(g, 360.0) + + # mean longitude of the sun [deg] + q = 280.459 + 0.98564736 * D + q = mod(q, 360.0) + + # geocentric apparent ecliptic longitude [deg] + L = q + 1.915 * sind(g) + 0.02 * sind(2 * g) + L = mod(L, 360.0) + + # mean obliquity of the ecliptic [deg] + ϵ = 23.439 - 0.00000036 * D + + # sun's right ascension angle [hours] + ra = rad2deg(atan(cosd(ϵ) * sind(L), cosd(L))) / 15.0 + ra = mod(ra, 24.0) + + # sun's declination angle [deg] + δ = asind(sind(ϵ) * sind(L)) + + # JD_0 is the Julian date of the previous midnight (0h) UT1 + dt_midnight = DateTime(year(dt), month(dt), day(dt), 0, 0, 0) + jd_0 = datetime2julian(dt_midnight) + + # hours of UT1 elapsed since the previous midnight + H = (jd - jd_0) * 24.0 + day_ut = jd_0 - 2451545.0 + jd_tt = jd + δt / 86400.0 + D_tt = jd_tt - 2451545.0 + + # centuries since the year 2000 + t_cent = D_tt / 36525.0 + + # Greenwich mean sidereal time [hours] + gmst = if gmst_option == 1 + ( + 6.697375 + + 0.065707485828 * day_ut + + 1.0027379 * H + + 0.0854103 * t_cent + + 0.0000258 * t_cent^2 + ) + else # gmst_option == 2 + (6.697375 + 0.065709824279 * day_ut + 1.0027379 * H + 0.0000258 * t_cent^2) + end + gmst = mod(gmst, 24.0) + + # longitude of the ascending node of the moon [deg] + Ω = 125.04 - 0.052954 * D_tt + + # mean longitude of the sun [deg] + L_s = 280.47 + 0.98565 * D_tt + + # nutation in longitude [hours] + Δψ = -0.000319 * sind(Ω) - 0.000024 * sind(2 * L_s) + + # obliquity of the ecliptic [deg] + ε = 23.4393 - 0.0000004 * D_tt + + # equation of equinoxes [hours] + eqeq = Δψ * cosd(ε) + + # Greenwich apparent sidereal time [hours] + gast = gmst + eqeq + + # local hour angle [deg] + ha = (gast - ra) * 15.0 + longitude + + # solar elevation [deg] + elevation = asind(cosd(ha) * cosd(δ) * cos_lat + sind(δ) * sin_lat) + + # azimuth [deg] + azimuth = + rad2deg(atan(-sind(ha), (tand(δ) * cos_lat - sin_lat * cosd(ha)))) + + return SolPos{T}(mod(azimuth, 360.0), elevation, 90.0 - elevation) +end + +function _solar_position(obs::Observer{T}, dt::DateTime, alg::USNO) where {T <: AbstractFloat} + δt::T = if alg.delta_t === nothing + calculate_deltat(dt) + else + alg.delta_t + end + return _usno_kernel(dt, δt, obs.sin_lat, obs.cos_lat, obs.longitude, alg.gmst_option, T) +end function _solar_position!( pos::StructArrays.StructVector{SolPos{T}}, @@ -37,7 +136,6 @@ function _solar_position!( dts::AbstractVector{DateTime}, alg::USNO, ) where {T <: AbstractFloat} - δt::T = if alg.delta_t === nothing calculate_deltat(dts[1]) else @@ -50,92 +148,7 @@ function _solar_position!( gmst_option = alg.gmst_option @inbounds for i in eachindex(dts, pos) - dt = dts[i] - - # convert to Julian date - jd = datetime2julian(dt) - - # days since J2000.0 - D = jd - 2451545.0 - - # mean anomaly of the sun [deg] - g = 357.529 + 0.98560028 * D - g = mod(g, 360.0) - - # mean longitude of the sun [deg] - q = 280.459 + 0.98564736 * D - q = mod(q, 360.0) - - # geocentric apparent ecliptic longitude [deg] - L = q + 1.915 * sind(g) + 0.02 * sind(2 * g) - L = mod(L, 360.0) - - # mean obliquity of the ecliptic [deg] - ϵ = 23.439 - 0.00000036 * D - - # sun's right ascension angle [hours] - ra = rad2deg(atan(cosd(ϵ) * sind(L), cosd(L))) / 15.0 - ra = mod(ra, 24.0) - - # sun's declination angle [deg] - δ = asind(sind(ϵ) * sind(L)) - - # JD_0 is the Julian date of the previous midnight (0h) UT1 - dt_midnight = DateTime(year(dt), month(dt), day(dt), 0, 0, 0) - jd_0 = datetime2julian(dt_midnight) - - # hours of UT1 elapsed since the previous midnight - H = (jd - jd_0) * 24.0 - day_ut = jd_0 - 2451545.0 - jd_tt = jd + δt / 86400.0 - D_tt = jd_tt - 2451545.0 - - # centuries since the year 2000 - t_cent = D_tt / 36525.0 - - # Greenwich mean sidereal time [hours] - gmst = if gmst_option == 1 - ( - 6.697375 + - 0.065707485828 * day_ut + - 1.0027379 * H + - 0.0854103 * t_cent + - 0.0000258 * t_cent^2 - ) - else # gmst_option == 2 - (6.697375 + 0.065709824279 * day_ut + 1.0027379 * H + 0.0000258 * t_cent^2) - end - gmst = mod(gmst, 24.0) - - # longitude of the ascending node of the moon [deg] - Ω = 125.04 - 0.052954 * D_tt - - # mean longitude of the sun [deg] - L_s = 280.47 + 0.98565 * D_tt - - # nutation in longitude [hours] - Δψ = -0.000319 * sind(Ω) - 0.000024 * sind(2 * L_s) - - # obliquity of the ecliptic [deg] - ε = 23.4393 - 0.0000004 * D_tt - - # equation of equinoxes [hours] - eqeq = Δψ * cosd(ε) - - # Greenwich apparent sidereal time [hours] - gast = gmst + eqeq - - # local hour angle [deg] - ha = (gast - ra) * 15.0 + longitude - - # solar elevation [deg] - elevation = asind(cosd(ha) * cosd(δ) * cos_lat + sind(δ) * sin_lat) - - # azimuth [deg] - azimuth = - rad2deg(atan(-sind(ha), (tand(δ) * cos_lat - sin_lat * cosd(ha)))) - - pos[i] = SolPos{T}(mod(azimuth, 360.0), elevation, 90.0 - elevation) + pos[i] = _usno_kernel(dts[i], δt, sin_lat, cos_lat, longitude, gmst_option, T) end return pos end diff --git a/src/Positioning/walraven.jl b/src/Positioning/walraven.jl index 0a1d877..0ef63ea 100644 --- a/src/Positioning/walraven.jl +++ b/src/Positioning/walraven.jl @@ -13,6 +13,96 @@ This algorithm is based on [Wal78](@cite) with corrections from the 1979 Erratum """ struct Walraven <: SolarAlgorithm end +# Core computation shared by scalar and vectorized paths +@inline function _walraven_kernel( + dt::DateTime, longitude, sin_lat, cos_lat, ::Type{T}, + ) where {T} + # calculate fractional hour + hour_frac = fractional_hour(dt) + δ = year(dt) - 1980 + + # leap year calculation (round towards zero) + leap = div(δ, 4) + time = δ * 365 + leap + dayofyear(dt) - 1 + hour_frac / 24 + + if δ == (leap * 4) + time -= 1 + end + if (δ < 0) && (δ != (leap * 4)) + time -= 1 + end + + # angular position in orbit [rad] + θ = 2 * T(π) * time / 365.25 + + # mean anomaly [rad] + g = -0.031271 - 4.53963e-7 * time + θ + + # longitude of the sun [rad] + lon_sun = ( + 4.900968 + + 3.67474e-7 * time + + (0.033434 - 2.3e-9 * time) * sin(g) + + 0.000349 * sin(2 * g) + + θ + ) + + # obliquity of ecliptic [rad] + ϵ = deg2rad(T(23.442)) - deg2rad(T(3.56e-7)) * time + (sin_lon_sun, cos_lon_sun) = sincos(lon_sun) + (sin_ϵ, cos_ϵ) = sincos(ϵ) + + # right ascension [rad] + ra = atan(sin_lon_sun * cos_ϵ, cos_lon_sun) + if ra < 0 + ra += 2 * T(π) + end + + # declination [rad] + d = asin(sin_lon_sun * sin_ϵ) + + # sidereal time [rad] + side_t = 1.759335 + 2 * T(π) * (time / 365.25 - δ) + 3.694e-7 * time + if side_t >= 2 * T(π) + side_t -= 2 * T(π) + end + + # local sidereal time [rad] + loc_s = side_t - deg2rad(T(longitude)) + deg2rad(T(hour_frac * 15)) + if loc_s >= 2 * T(π) + loc_s -= 2 * T(π) + end + + # hour angle [rad] + ha = ra - loc_s + (sin_d, cos_d) = sincos(d) + (sin_ha, cos_ha) = sincos(ha) + + # elevation [rad] + el = asin(sin_lat * sin_d + cos_lat * cos_d * cos_ha) + (sin_el, cos_el) = sincos(el) + + # azimuth [deg] - initial calculation + az = rad2deg(asin(cos_d * sin_ha / cos_el)) + + # azimuth quadrant assignment - Spencer (1989) correction + cos_az = sin_d - sin_el * sin_lat + + if (cos_az >= 0) && (sin(deg2rad(az)) < 0) + az = 360 + az + end + + if cos_az < 0 + az = 180 - az + end + + elevation_deg = rad2deg(el) + return SolPos{T}(az, elevation_deg, 90 - elevation_deg) +end + +function _solar_position(obs::Observer{T}, dt::DateTime, ::Walraven) where {T} + return _walraven_kernel(dt, -obs.longitude, obs.sin_lat, obs.cos_lat, T) +end function _solar_position!( pos::StructArrays.StructVector{SolPos{T}}, @@ -20,95 +110,12 @@ function _solar_position!( dts::AbstractVector{DateTime}, ::Walraven, ) where {T} - longitude = -obs.longitude # negative convention used by Walraven sin_lat = obs.sin_lat cos_lat = obs.cos_lat @inbounds for i in eachindex(dts, pos) - dt = dts[i] - - # calculate fractional hour - hour_frac = fractional_hour(dt) - δ = year(dt) - 1980 - - # leap year calculation (round towards zero) - leap = div(δ, 4) - time = δ * 365 + leap + dayofyear(dt) - 1 + hour_frac / 24 - - if δ == (leap * 4) - time -= 1 - end - if (δ < 0) && (δ != (leap * 4)) - time -= 1 - end - - # angular position in orbit [rad] - θ = 2 * T(π) * time / 365.25 - - # mean anomaly [rad] - g = -0.031271 - 4.53963e-7 * time + θ - - # longitude of the sun [rad] - lon_sun = ( - 4.900968 + - 3.67474e-7 * time + - (0.033434 - 2.3e-9 * time) * sin(g) + - 0.000349 * sin(2 * g) + - θ - ) - - # obliquity of ecliptic [rad] - ϵ = deg2rad(T(23.442)) - deg2rad(T(3.56e-7)) * time - (sin_lon_sun, cos_lon_sun) = sincos(lon_sun) - (sin_ϵ, cos_ϵ) = sincos(ϵ) - - # right ascension [rad] - ra = atan(sin_lon_sun * cos_ϵ, cos_lon_sun) - if ra < 0 - ra += 2 * T(π) - end - - # declination [rad] - d = asin(sin_lon_sun * sin_ϵ) - - # sidereal time [rad] - side_t = 1.759335 + 2 * T(π) * (time / 365.25 - δ) + 3.694e-7 * time - if side_t >= 2 * T(π) - side_t -= 2 * T(π) - end - - # local sidereal time [rad] - loc_s = side_t - deg2rad(T(longitude)) + deg2rad(T(hour_frac * 15)) - if loc_s >= 2 * T(π) - loc_s -= 2 * T(π) - end - - # hour angle [rad] - ha = ra - loc_s - (sin_d, cos_d) = sincos(d) - (sin_ha, cos_ha) = sincos(ha) - - # elevation [rad] - el = asin(sin_lat * sin_d + cos_lat * cos_d * cos_ha) - (sin_el, cos_el) = sincos(el) - - # azimuth [deg] - initial calculation - az = rad2deg(asin(cos_d * sin_ha / cos_el)) - - # azimuth quadrant assignment - Spencer (1989) correction - cos_az = sin_d - sin_el * sin_lat - - if (cos_az >= 0) && (sin(deg2rad(az)) < 0) - az = 360 + az - end - - if cos_az < 0 - az = 180 - az - end - - elevation_deg = rad2deg(el) - pos[i] = SolPos{T}(az, elevation_deg, 90 - elevation_deg) + pos[i] = _walraven_kernel(dts[i], longitude, sin_lat, cos_lat, T) end return pos end