solaris/
lib.rs

1use num::{Float, NumCast};
2
3// (1) atan2, but the undefined (0, 0) is defined as 0
4pub fn fast_atan2<F: Float>(y: F, x: F) -> F {
5    let zero: F = NumCast::from(0).unwrap();
6    let pi: F = NumCast::from(std::f64::consts::PI).unwrap();
7
8    if x > zero {
9        return (y / x).atan();
10    } else if x == zero {
11        if y < zero {
12            return NumCast::from(-std::f64::consts::PI / 2.).unwrap();
13        } else if y == zero {
14            return zero;
15        } else if y > zero {
16            return NumCast::from(std::f64::consts::PI / 2.).unwrap();
17        }
18    } else if x < zero {
19        if y < zero {
20            return (y / x).atan() - pi;
21        } else if y >= zero {
22            return (y / x).atan() + pi;
23        }
24    }
25
26    return zero;
27}
28
29// TODO: placeholder
30/// Julian days since epoch year 2000.0.
31type JD2000 = f64;
32//// Centuries since epoch year 2000.0
33type JC2000 = f64;
34
35pub struct SunPosition {
36    /// Compass direction
37    azimuth: f64,
38    /// altitude angle
39    altitude: f64,
40}
41
42// placeholder for organization
43pub fn solaris_base_impl(
44    t_Jd: JD2000,
45    t_Jc: JC2000,
46    use_approx_R: bool,
47    obs_latitude: f64,
48    obs_longitude: f64,
49) -> SunPosition {
50    // (2) Simon et al., 1994
51    // mean latitude
52    let λ0 = 4.895063168 + 628.331966786 * t_Jc + 5.291838e-6 * (t_Jc * t_Jc);
53
54    // (3) Chapront-Touze and Chapront, 1988
55    // mean anomaly
56    let M = 6.240060141 + 628.301955152 * t_Jc + -2.682571e-6 * (t_Jc * t_Jc);
57
58    // (4) Meeus 1998
59    // Sun's equation of the centre
60    let C = (3.34161088e-2 - 8.40725e-5 * t_Jc - 2.443e-7 * (t_Jc * t_Jc)) * M.sin()
61        + (3.489437e-4 - 1.76278e-6 * t_Jc) * (2.0 * M).sin();
62
63    // (5) true longitude
64    let λ = λ0 + C;
65
66    // (6) Chapront-Touze and Chapront, 1988
67    // longitude of the Moon's mean ascending node
68    let Ω = 2.1824390725 - 33.7570464271 * t_Jc + 3.622256e-5 * (t_Jc * t_Jc);
69
70    // (7) Seidelmann, 1982
71    // Nutation in longitude
72    let delta_ψ = -8.338601e-5 * Ω.sin();
73
74    // (8) (9) (10) Simon et al., 1994
75    let e = 0.016708634 - 4.2037e-5 * t_Jc - 1.267e-7 * (t_Jc * t_Jc);
76    let v = M + C;
77    let R = if use_approx_R {
78        1.0000010178 // (11)
79    } else {
80        1.0000010178 * (1. - e * e) / (1. + e * v.cos())
81    };
82
83    // (12) Kovalevsky and Seidelmann, 2004
84    // Annual aberration
85    let delta_λ = (-9.93087e-5) / R;
86
87    // (13)
88    // Sun's actual correct longitude
89    let λ = λ + delta_λ + delta_ψ;
90    // Sometimes longitude can go out of range of radians and mess up later calculations
91    let λ = λ % (2. * std::f64::consts::PI);
92
93    // (14)
94    // Approximated ecliptical latitude of the sun
95    // GOOD
96    const β: f64 = 0.0;
97
98    // (15) Meeus, 1998
99    // Mean obliquity
100    let ε0 = 0.409092804222 - 2.26965525e-4 * t_Jc - 2.86e-9 * (t_Jc * t_Jc);
101
102    // (16) Seidelmann, 1982 leading term only
103    // nutation in obliquity
104    let delta_ε = 4.4615e-5 * Ω.cos();
105
106    // (17)
107    // obliquity of the ecliptic
108    let ε = ε0 + delta_ε;
109
110    // (18) (19) Urban and Seidelmann, 2012
111    let cos_ε = ε.cos();
112    let sin_ε = ε.sin();
113    let tan_β = β.tan();
114    let cos_β = β.cos();
115    let sin_β = β.sin();
116    let sin_λ = λ.sin();
117    let cos_λ = λ.cos();
118
119    // right ascension
120    // let α = fast_atan2(sin_λ * cos_ε - tan_β * sin_ε, cos_λ);
121    let α = fast_atan2(sin_λ * cos_ε - tan_β * sin_ε, cos_λ);
122    // hack: handle < 0
123    let α = if α < 0. {
124        α + (2. * std::f64::consts::PI)
125    } else {
126        α
127    };
128    // declination
129    let δ = (sin_β * cos_ε + cos_β * sin_ε * sin_λ).asin();
130
131    // (21)
132    // Local sidereal time
133    let delta_θ = delta_ψ * cos_ε;
134    let θ0 = 4.89496121 + 6.300388098985 * t_Jd + 6.77e-6 * (t_Jc * t_Jc);
135    let θ = θ0 + delta_θ + obs_longitude;
136
137    // (20)
138    // Parallactic coordinate hour angle
139    let H = θ - α;
140
141    let cos_H = H.cos();
142    let cos_δ = δ.cos();
143    let sin_δ = δ.sin();
144    let cos_lat = obs_latitude.cos();
145    let sin_lat = obs_latitude.sin();
146    // (24) Urban and Seidalmann, 2012
147    // altitude h
148    let h = (sin_δ * sin_lat + cos_H * cos_δ * cos_lat).asin();
149
150    let sin_H = H.sin();
151    // (25) Urban and Seidalmann, 2012
152    // azimuth
153    let A = fast_atan2(sin_H, cos_H * sin_lat - (sin_δ / cos_δ) * cos_lat);
154
155    // (26) uncorrected zenith angle
156    let ζ = (std::f64::consts::PI / 2.) - h;
157
158    // (27) Saemundsson 1986
159    // Atmospheric refraction correction
160    // Air temperature, (283 K)
161    const T: f64 = 283.;
162    // Air pressure (101 kPa)
163    const P: f64 = 101.;
164    let delta_h = 0.0002967 * (1.0 / (h + (0.0031376 / (h + 0.0892)))).tan() * (283. / T) * (P / 101.);
165
166    // Correct altitude
167    let h = h + delta_h;
168
169    return SunPosition {
170        azimuth: A,
171        altitude: h,
172    };
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn initial_check() {
181        // 2000-01-01 as julian day number
182        const jd2000_epoch: f64 = 2451545.00;
183        // ~2024-03-17T14:48
184        const now: f64 = 2460387.166666667;
185
186        // Somewhere on the road in central PA where I'm running this code :)
187        const obs_lat: f64 = 40.884305 * (std::f64::consts::PI / 180.);
188        const obs_lon: f64 = -77.779766 * (std::f64::consts::PI / 180.);
189
190        const t_Jd: f64 = now - jd2000_epoch;
191        const t_Jc: f64 = t_Jd / (365.25 * 100.);
192        println!("jc = {}, jd = {}", t_Jc, t_Jd);
193
194        let sun_pos = solaris_base_impl(t_Jd, t_Jc, false, obs_lat, obs_lon);
195        println!(
196            "altitude = {}, azimuth = {}",
197            sun_pos.altitude * (180. / std::f64::consts::PI),
198            sun_pos.azimuth * (180. / std::f64::consts::PI),
199        );
200
201        let approx_eq = |x: f64, y: f64| -> bool {
202            let diff = (y - x).abs();
203            diff < 1e-6
204        };
205
206        assert!(approx_eq(sun_pos.azimuth * (180. / std::f64::consts::PI), -28.33772429847502));
207        assert!(approx_eq(sun_pos.altitude * (180. / std::f64::consts::PI), 44.4880091652758));
208    }
209
210    #[test]
211    fn fast_atan2_test() {
212        let approx_eq = |x: f64, y: f64| -> bool {
213            let diff = (y - x).abs();
214            diff < 1e-6
215        };
216
217        // x < 0
218        assert!(approx_eq(fast_atan2::<f64>(-3., -4.), -2.4980915448));
219        assert!(approx_eq(fast_atan2::<f64>(0., -4.), 3.14159265359));
220        assert!(approx_eq(fast_atan2::<f64>(3., -4.), 2.4980915448));
221
222        // x == 0
223        assert!(approx_eq(fast_atan2::<f64>(-3., 0.), -1.57079632679));
224        assert!(approx_eq(fast_atan2::<f64>(0., 0.), 0.));
225        assert!(approx_eq(fast_atan2::<f64>(3., 0.), 1.57079632679));
226
227        // x > 0
228        assert!(approx_eq(fast_atan2::<f64>(-3., 4.), -0.643501108793));
229        assert!(approx_eq(fast_atan2::<f64>(0., 4.), 0.));
230        assert!(approx_eq(fast_atan2::<f64>(3., 4.), 0.643501108793));
231    }
232}