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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ accuracy and implementation status.
| Walraven | [Walraven, 1978](<https://doi.org/10.1016/0038-092X(78)90155-X>) | ±0.0100° | None | ✅ |
| USNO | [U.S. Naval Observatory](https://aa.usno.navy.mil/faq/sun_approx) | ±0.0500° | None | ✅ |
| SPA | [Reda & Andreas, 2004](https://doi.org/10.1016/j.solener.2003.12.003) | ±0.0003° | Built-in | ✅ |
| Iqbal | [Iqbal, 1983](https://doi.org/10.1016/B978-0-12-373750-2.X5001-0) | ±0.0100° | None | ✅ |
| Michalsky | [Michalsky, 1988](<https://doi.org/10.1016/0038-092X(88)90045-X>) | ±0.0100° | MICHALSKY | ✅ |
| SG2 | [Blanc & Wald, 2012](https://doi.org/10.1016/j.solener.2012.07.018) | ±0.0030° | SG2Refraction | ✅ |

## Refraction correction algorithms

Expand Down
5 changes: 4 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ accuracy and implementation status.
| [`Walraven`](@ref SolarPosition.Positioning.Walraven) | [Walraven, 1978](https://doi.org/10.1016/0038-092X(78)90155-X) | ±0.0100° | None | ✅ |
| [`USNO`](@ref SolarPosition.Positioning.USNO) | [U.S. Naval Observatory](https://aa.usno.navy.mil/faq/sun_approx) | ±0.0500° | None | ✅ |
| [`SPA`](@ref SolarPosition.Positioning.SPA) | [Reda & Andreas, 2004](https://doi.org/10.1016/j.solener.2003.12.003) | ±0.0003° | Built-in | ✅ |
| [`Iqbal`](@ref SolarPosition.Positioning.Iqbal) | [Iqbal, 1983](https://doi.org/10.1016/B978-0-12-373750-2.X5001-0) | ±0.0100° | None | ✅ |
| [`Michalsky`](@ref SolarPosition.Positioning.Michalsky) | [Michalsky, 1988](https://doi.org/10.1016/0038-092X(88)90045-X) | ±0.0100° | [`MICHALSKY`](@ref SolarPosition.Refraction.MICHALSKY) | ✅ |
| [`SG2`](@ref SolarPosition.Positioning.SG2) | [Blanc & Wald, 2012](https://doi.org/10.1016/j.solener.2012.07.018) | ±0.0030° | [`SG2Refraction`](@ref SolarPosition.Refraction.SG2Refraction) | ✅ |

## Refraction correction algorithms

Expand All @@ -96,7 +99,7 @@ Atmospheric refraction correction algorithms available in SolarPosition.jl.
| [`ARCHER`](@ref SolarPosition.Refraction.ARCHER) | Archer et al., 1980 | None | ✅ |
| [`BENNETT`](@ref SolarPosition.Refraction.BENNETT) | [Bennett, 1982](https://doi.org/10.1017/S0373463300022037) | Pressure, Temperature | ✅ |
| [`MICHALSKY`](@ref SolarPosition.Refraction.MICHALSKY) | [Michalsky, 1988](https://doi.org/10.1016/0038-092X(88)90045-X) | None | ✅ |
| [`SG2`](@ref SolarPosition.Refraction.SG2) | [Blanc & Wald, 2012](https://doi.org/10.1016/j.solener.2012.07.018) | Pressure, Temperature | ✅ |
| [`SG2Refraction`](@ref SolarPosition.Refraction.SG2Refraction) | [Blanc & Wald, 2012](https://doi.org/10.1016/j.solener.2012.07.018) | Pressure, Temperature | ✅ |
| [`SPARefraction`](@ref SolarPosition.Refraction.SPARefraction) | [Reda & Andreas, 2004](https://doi.org/10.1016/j.solener.2003.12.003) | Pressure, Temperature | ✅ |

## Extensions
Expand Down
40 changes: 40 additions & 0 deletions docs/src/positioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ The following solar position algorithms are currently implemented in SolarPositi
| [`Walraven`](@ref SolarPosition.Positioning.Walraven) | [Wal78](@cite) | ±0.0100° | None | ✅ |
| [`USNO`](@ref SolarPosition.Positioning.USNO) | [USNO](@cite) | ±0.0500° | None | ✅ |
| [`SPA`](@ref SolarPosition.Positioning.SPA) | [RA04](@cite) | ±0.0003° | Built-in | ✅ |
| [`Iqbal`](@ref SolarPosition.Positioning.Iqbal) | [Iqb83](@cite) | ±0.0100° | None | ✅ |
| [`Michalsky`](@ref SolarPosition.Positioning.Michalsky) | [Mic88](@cite) | ±0.0100° | [`MICHALSKY`](@ref SolarPosition.Refraction.MICHALSKY) | ✅ |
| [`SG2`](@ref SolarPosition.Positioning.SG2) | [BW12](@cite) | ±0.0030° | [`SG2Refraction`](@ref SolarPosition.Refraction.SG2Refraction) | ✅ |

## [PSA](@id psa-algorithm)

Expand Down Expand Up @@ -121,3 +124,40 @@ position calculation with periodic terms for Earth heliocentric longitude and la
```@docs
SolarPosition.Positioning.SPA
```

## [Iqbal](@id iqbal-algorithm)

The Iqbal algorithm is a fast, low-complexity method that derives the solar declination
and equation of time from a truncated Fourier series in the day angle.

The formulation was compiled by [Iqb83](@cite) and builds on the Fourier-series
representation of [Spe71](@cite).

```@docs
SolarPosition.Positioning.Iqbal
```

## [Michalsky](@id michalsky-algorithm)

The Michalsky algorithm implements the Astronomical Almanac's approximate solar position
algorithm, with a stated accuracy of ±0.01° between 1950 and 2050. It exposes options for
the azimuth-quadrant correction and the Julian date formulation.

The algorithm was published by [Mic88](@cite); the azimuth correction that makes it valid
for all latitudes is from [Spe89](@cite).

```@docs
SolarPosition.Positioning.Michalsky
```

## [SG2](@id sg2-algorithm)

The SG2 (Second Generation) algorithm is optimized for fast and accurate computation over
multi-decadal periods, with a stated accuracy of ±0.003° between 1980 and 2030. Dates
outside this range raise an `ArgumentError`.

The algorithm was published by [BW12](@cite).

```@docs
SolarPosition.Positioning.SG2
```
8 changes: 4 additions & 4 deletions docs/src/refraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ of the available algorithms:
| [`ARCHER`](@ref SolarPosition.Refraction.ARCHER) | [Arc80](@cite) | None | ✅ |
| [`BENNETT`](@ref SolarPosition.Refraction.BENNETT) | [Ben82](@cite) | Pressure, Temperature | ✅ |
| [`MICHALSKY`](@ref SolarPosition.Refraction.MICHALSKY) | [Mic88](@cite) | None | ✅ |
| [`SG2`](@ref SolarPosition.Refraction.SG2) | [BW12](@cite) | Pressure, Temperature | ✅ |
| [`SG2Refraction`](@ref SolarPosition.Refraction.SG2Refraction) | [BW12](@cite) | Pressure, Temperature | ✅ |
| [`SPARefraction`](@ref SolarPosition.Refraction.SPARefraction) | [RA04](@cite) | Pressure, Temperature | ✅ |

To calculate refraction, we can use the [`refraction`](@ref SolarPosition.Refraction.refraction) function:
Expand Down Expand Up @@ -83,7 +83,7 @@ using CairoMakie
# Define models and elevation range
models = [("Archer", SolarPosition.Refraction.ARCHER()), ("Bennett", SolarPosition.Refraction.BENNETT()),
("Hughes", SolarPosition.Refraction.HUGHES()), ("Michalsky", SolarPosition.Refraction.MICHALSKY()),
("SG2", SolarPosition.Refraction.SG2()), ("SPA", SolarPosition.Refraction.SPARefraction())]
("SG2", SolarPosition.Refraction.SG2Refraction()), ("SPA", SolarPosition.Refraction.SPARefraction())]
elevation = -1.5:0.1:90.0

# Create figure with two subplots
Expand Down Expand Up @@ -161,15 +161,15 @@ solar position calculations. It includes special handling for very low elevation
SolarPosition.Refraction.MICHALSKY
```

## [SG2](@id sg2-refraction)
## [SG2Refraction](@id sg2-refraction)

The SG2 (Second Generation) refraction algorithm is optimized for fast computation over multi-decadal periods.

Developed by [BW12](@cite), this algorithm uses a two-regime approach with different formulas
for elevations above and below a threshold. It accounts for atmospheric pressure and temperature.

```@docs
SolarPosition.Refraction.SG2
SolarPosition.Refraction.SG2Refraction
```

## [SPARefraction](@id spa-refraction)
Expand Down
19 changes: 19 additions & 0 deletions docs/src/refs.bib
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,25 @@ @article{Spe89
url = {https://doi.org/10.1016/0038-092X(89)90039-X}
}

@book{Iqb83,
author = {Iqbal, M.},
title = {An Introduction to Solar Radiation},
publisher = {Academic Press},
year = {1983},
doi = {10.1016/B978-0-12-373750-2.X5001-0},
url = {https://doi.org/10.1016/B978-0-12-373750-2.X5001-0}
}

@article{Spe71,
author = {Spencer, J. W.},
title = {Fourier series representation of the position of the Sun},
journal = {Search},
volume = {2},
number = {5},
pages = {172},
year = {1971}
}

@article{Ben82,
author = {Bennett, G. G.},
title = {The Calculation of Astronomical Refraction in Marine Navigation},
Expand Down
6 changes: 5 additions & 1 deletion src/Positioning/Positioning.jl
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,13 @@ include("noaa.jl")
include("walraven.jl")
include("usno.jl")
include("spa.jl")
include("iqbal.jl")
include("michalsky.jl")
include("sg2.jl")

export Observer,
PSA, NOAA, Walraven, USNO, SPA, solar_position, solar_position!, SolPos, ApparentSolPos
PSA, NOAA, Walraven, USNO, SPA, Iqbal, Michalsky, SG2,
solar_position, solar_position!, SolPos, ApparentSolPos
export SolarAlgorithm, AbstractSolPos, AbstractApparentSolPos
export calculate_deltat

Expand Down
72 changes: 72 additions & 0 deletions src/Positioning/iqbal.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
$(TYPEDEF)

Iqbal solar position algorithm.

A lightweight algorithm that obtains the solar declination and equation of time from a
truncated Fourier series in the day angle, then derives the zenith and azimuth from the
standard spherical-trigonometry relations. No atmospheric refraction is applied by default.

# Accuracy
The truncated Fourier expansion gives a declination accurate to about ±0.01°, which makes
this algorithm a good choice when speed matters more than sub-arcminute precision.

# Literature
Based on the formulation compiled by [Iqb83](@cite), which builds on the Fourier-series
representation of [Spe71](@cite).

# Fields
$(TYPEDFIELDS)
"""
struct Iqbal <: SolarAlgorithm end

function _solar_position(obs::Observer{T}, dt::DateTime, ::Iqbal) where {T}
# day angle [radians]
day_angle = 2π * (dayofyear(dt) - 1) / 365

(sin_Γ, cos_Γ) = sincos(day_angle)
(sin_2Γ, cos_2Γ) = sincos(2 * day_angle)
(sin_3Γ, cos_3Γ) = sincos(3 * day_angle)

# solar declination [degrees]
declination = rad2deg(
0.006918 - 0.399912 * cos_Γ + 0.070257 * sin_Γ -
0.006758 * cos_2Γ + 0.000907 * sin_2Γ -
0.002697 * cos_3Γ + 0.00148 * sin_3Γ,
)

# equation of time [minutes]
eot =
(
0.0000075 + 0.001868 * cos_Γ - 0.032077 * sin_Γ -
0.014615 * cos_2Γ - 0.040849 * sin_2Γ
) * 1440 / (2π)

# hour angle [degrees]
hour_angle = (fractional_hour(dt) - 12) * 15 + obs.longitude + eot / 4

(sin_δ, cos_δ) = sincosd(declination)
(sin_ω, cos_ω) = sincosd(hour_angle)

# zenith angle [degrees]
zenith = acosd(sin_δ * obs.sin_lat + cos_δ * obs.cos_lat * cos_ω)

# azimuth [degrees], measured eastward from north. The arctan form selects the correct
# quadrant where the closed-form expression would be ambiguous.
azimuth = mod(
atand(
sin_ω * cos_δ,
cos_ω * obs.sin_lat * cos_δ - obs.cos_lat * sin_δ,
) + 180,
360,
)

return SolPos{T}(azimuth, 90 - zenith, zenith)
end

function _solar_position(obs, dt, alg::Iqbal, ::DefaultRefraction)
return _solar_position(obs, dt, alg, NoRefraction())
end

# Iqbal with DefaultRefraction returns SolPos (no refraction by default)
result_type(::Type{Iqbal}, ::Type{DefaultRefraction}, ::Type{T}) where {T} = SolPos{T}
117 changes: 117 additions & 0 deletions src/Positioning/michalsky.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using ..Refraction: MICHALSKY

"""
$(TYPEDEF)

Michalsky solar position algorithm.

Implements the Astronomical Almanac's approximate solar position algorithm. By default the
[`MICHALSKY`](@ref) atmospheric refraction model is applied, matching the original
publication.

# Accuracy
Claimed accuracy: ±0.01° between 1950 and 2050 when using the original Julian date
formulation.

# Options
- `spencer_correction`: when `true` (default), applies the azimuth-quadrant correction of
[Spe89](@cite) so the algorithm is valid at all latitudes. The original formulation
(`false`) is only valid in the northern hemisphere.
- `julian_date`: `:original` (default) uses the integer-based Julian date from the original
paper, which guarantees the stated accuracy only between 1950 and 2050; `:standard` uses
the exact Julian date and remains usable outside that window.

# Literature
Based on the algorithm of [Mic88](@cite) with the azimuth correction of [Spe89](@cite).

# Fields
$(TYPEDFIELDS)
"""
struct Michalsky <: SolarAlgorithm
"Apply the Spencer (1989) azimuth-quadrant correction (valid for all latitudes)."
spencer_correction::Bool
"Julian date formulation: `:original` or `:standard`."
julian_date::Symbol
end

Michalsky(; spencer_correction::Bool = true, julian_date::Symbol = :original) =
Michalsky(spencer_correction, julian_date)

function _solar_position(obs::Observer{T}, dt::DateTime, alg::Michalsky) where {T}
hour = fractional_hour(dt)

# Julian date
jd = if alg.julian_date === :original
delta = year(dt) - 1949
leap = floor(delta / 4)
2432916.5 + delta * 365 + leap + dayofyear(dt) + hour / 24
elseif alg.julian_date === :standard
datetime2julian(dt)
else
throw(
ArgumentError(
"`julian_date` must be :original or :standard, got :$(alg.julian_date)",
),
)
end

# days since J2000.0
n = jd - 2451545.0

# mean longitude, mean anomaly and ecliptic longitude [degrees]
L = mod(280.46 + 0.9856474 * n, 360)
g = mod(357.528 + 0.9856003 * n, 360)
l = mod(L + 1.915 * sind(g) + 0.02 * sind(2 * g), 360)

# obliquity of the ecliptic [degrees]
ep = 23.439 - 0.0000004 * n

# declination and right ascension [degrees]
dec = asind(sind(ep) * sind(l))
ra = mod(rad2deg(atan(cosd(ep) * sind(l), cosd(l))), 360)

# Greenwich and local mean sidereal time [hours]
gmst = mod(6.697375 + 0.0657098242 * n + hour, 24)
lmst = mod(gmst + obs.longitude / 15, 24)

# hour angle [hours], wrapped to [-12, 12)
ha = mod(lmst - ra / 15 + 12, 24) - 12

# elevation and azimuth [degrees]
el = asind(sind(dec) * obs.sin_lat + cosd(dec) * obs.cos_lat * cosd(15 * ha))
az = asind(-cosd(dec) * sind(15 * ha) / cosd(el))

if alg.spencer_correction
# Spencer (1989) quadrant correction
cos_az = sind(dec) - sind(el) * obs.sin_lat
if cos_az >= 0 && sind(az) < 0
az += 360
end
if cos_az < 0
az = 180 - az
end
else
# Original Michalsky quadrant assignment via the critical elevation. The ratio can
# exceed the asin domain (e.g. at the equator); treat that as undefined so neither
# correction applies, matching the reference behaviour.
ratio = sind(dec) / obs.sin_lat
elc = abs(ratio) <= 1 ? asind(ratio) : T(NaN)
if el >= elc
az = 180 - az
end
if el <= elc && ha > 0
az += 360
end
az = mod(az, 360)
end

return SolPos{T}(az, el, 90 - el)
end

function _solar_position(obs, dt, alg::Michalsky, ::DefaultRefraction)
return _solar_position(obs, dt, alg, MICHALSKY())
end

# Michalsky with DefaultRefraction returns ApparentSolPos (uses MICHALSKY refraction)
result_type(::Type{Michalsky}, ::Type{DefaultRefraction}, ::Type{T}) where {T} =
ApparentSolPos{T}
Loading
Loading