Skip to main content

celestial_coords/
solar.rs

1use crate::{CoordResult, ICRSPosition};
2use celestial_core::constants::{ARCSEC_TO_RAD, DEG_TO_RAD, J2000_JD, TWOPI};
3use celestial_core::utils::{normalize_angle_rad, normalize_angle_to_positive};
4use celestial_core::Angle;
5use celestial_time::TT;
6
7const SOLAR_EQUATOR_INCLINATION_DEG: f64 = 7.25;
8const SOLAR_EQUATOR_INCLINATION_RAD: f64 = SOLAR_EQUATOR_INCLINATION_DEG * DEG_TO_RAD;
9
10const SOLAR_ASCENDING_NODE_J2000_DEG: f64 = 75.76;
11
12pub const CARRINGTON_EPOCH_JD: f64 = 2398220.0;
13pub const CARRINGTON_SYNODIC_PERIOD: f64 = 27.2753;
14
15pub struct SolarOrientation {
16    pub b0: Angle,
17    pub l0: Angle,
18    pub p: Angle,
19}
20
21pub fn compute_solar_orientation(epoch: &TT) -> SolarOrientation {
22    let jd = epoch.to_julian_date();
23    let d = (jd.jd1() - J2000_JD) + jd.jd2();
24    let t = d / celestial_core::constants::DAYS_PER_JULIAN_CENTURY;
25
26    let (sun_lon, sun_lat, obliquity) = solar_ecliptic_coords(t);
27    let (b0, l0, p) = heliographic_coords(t, sun_lon, sun_lat, obliquity);
28
29    SolarOrientation {
30        b0: Angle::from_radians(b0),
31        l0: Angle::from_radians(l0),
32        p: Angle::from_radians(p),
33    }
34}
35
36pub fn compute_b0(epoch: &TT) -> Angle {
37    compute_solar_orientation(epoch).b0
38}
39
40pub fn compute_l0(epoch: &TT) -> Angle {
41    compute_solar_orientation(epoch).l0
42}
43
44pub fn compute_p(epoch: &TT) -> Angle {
45    compute_solar_orientation(epoch).p
46}
47
48pub fn carrington_rotation_number(epoch: &TT) -> u32 {
49    let jd = epoch.to_julian_date();
50    let jd_days = (jd.jd1() - CARRINGTON_EPOCH_JD) + jd.jd2();
51    libm::floor(jd_days / CARRINGTON_SYNODIC_PERIOD) as u32 + 1
52}
53
54pub fn sun_earth_distance(epoch: &TT) -> f64 {
55    let jd = epoch.to_julian_date();
56    let d = (jd.jd1() - J2000_JD) + jd.jd2();
57    let t = d / celestial_core::constants::DAYS_PER_JULIAN_CENTURY;
58
59    let m = (357.52911 + 35999.05029 * t - 0.0001537 * t * t) * DEG_TO_RAD;
60    let e = 0.016708634 - 0.000042037 * t - 0.0000001267 * t * t;
61
62    let c_rad = (1.914602 - 0.004817 * t - 0.000014 * t * t) * DEG_TO_RAD * libm::sin(m)
63        + (0.019993 - 0.000101 * t) * DEG_TO_RAD * libm::sin(2.0 * m)
64        + 0.000289 * DEG_TO_RAD * libm::sin(3.0 * m);
65
66    let true_anomaly = m + c_rad;
67    let a = 1.000001018; // semi-major axis in AU
68
69    a * (1.0 - e * e) / (1.0 + e * libm::cos(true_anomaly))
70}
71
72fn heliographic_coords(t: f64, sun_lon: f64, _sun_lat: f64, obliquity: f64) -> (f64, f64, f64) {
73    let i = SOLAR_EQUATOR_INCLINATION_RAD;
74    let k = (SOLAR_ASCENDING_NODE_J2000_DEG + 1.3958333 * t) * DEG_TO_RAD;
75
76    let lambda = sun_lon;
77    let theta = lambda - k;
78    let (sin_theta, cos_theta) = libm::sincos(theta);
79    let (sin_i, cos_i) = libm::sincos(i);
80    let (_sin_obl, cos_obl) = libm::sincos(obliquity);
81
82    let b0 = libm::asin(sin_theta * sin_i);
83
84    let eta = libm::atan2(sin_i * cos_theta, cos_i);
85    let jd_days =
86        t * celestial_core::constants::DAYS_PER_JULIAN_CENTURY + J2000_JD - CARRINGTON_EPOCH_JD;
87    let l0_raw = 360.0 / CARRINGTON_SYNODIC_PERIOD * jd_days;
88    let l0 = normalize_angle_to_positive((l0_raw * DEG_TO_RAD - eta) % TWOPI);
89
90    let rho = libm::atan(cos_theta * sin_i / cos_obl);
91    let sigma = libm::atan(sin_theta * cos_i);
92    let p = normalize_angle_rad(rho + sigma);
93
94    (b0, l0, p)
95}
96
97fn solar_ecliptic_coords(t: f64) -> (f64, f64, f64) {
98    let l0 = 280.46646 + 36000.76983 * t + 0.0003032 * t * t;
99    let m = 357.52911 + 35999.05029 * t - 0.0001537 * t * t;
100    let m_rad = m * DEG_TO_RAD;
101
102    let c = (1.914602 - 0.004817 * t - 0.000014 * t * t) * libm::sin(m_rad)
103        + (0.019993 - 0.000101 * t) * libm::sin(2.0 * m_rad)
104        + 0.000289 * libm::sin(3.0 * m_rad);
105
106    let sun_true_lon = l0 + c;
107
108    let omega = 125.04 - 1934.136 * t;
109    let omega_rad = omega * DEG_TO_RAD;
110    let apparent_lon = sun_true_lon - 0.00569 - 0.00478 * libm::sin(omega_rad);
111
112    let obliquity = mean_obliquity(t);
113
114    (
115        normalize_angle_to_positive(apparent_lon * DEG_TO_RAD),
116        0.0,
117        obliquity,
118    )
119}
120
121fn mean_obliquity(t: f64) -> f64 {
122    let eps0_arcsec = 84381.448 - 46.8150 * t - 0.00059 * t * t + 0.001813 * t * t * t;
123    eps0_arcsec * ARCSEC_TO_RAD
124}
125
126pub(crate) fn get_sun_icrs(epoch: &TT) -> CoordResult<ICRSPosition> {
127    let jd = epoch.to_julian_date();
128    let d = (jd.jd1() - J2000_JD) + jd.jd2();
129    let t = d / celestial_core::constants::DAYS_PER_JULIAN_CENTURY;
130
131    let l0 = 280.46646 + 36000.76983 * t + 0.0003032 * t * t;
132    let m = 357.52911 + 35999.05029 * t - 0.0001537 * t * t;
133    let m_rad = m * DEG_TO_RAD;
134
135    let c = (1.914602 - 0.004817 * t - 0.000014 * t * t) * libm::sin(m_rad)
136        + (0.019993 - 0.000101 * t) * libm::sin(2.0 * m_rad)
137        + 0.000289 * libm::sin(3.0 * m_rad);
138
139    let sun_true_lon = l0 + c;
140    let omega = 125.04 - 1934.136 * t;
141    let omega_rad = omega * DEG_TO_RAD;
142    let apparent_lon = sun_true_lon - 0.00569 - 0.00478 * libm::sin(omega_rad);
143
144    let lambda = apparent_lon * DEG_TO_RAD;
145    let eps = (23.439291 - 0.0130042 * t) * DEG_TO_RAD;
146
147    let (sin_lambda, cos_lambda) = libm::sincos(lambda);
148    let (sin_eps, cos_eps) = libm::sincos(eps);
149
150    let ra = libm::atan2(sin_lambda * cos_eps, cos_lambda);
151    let dec = libm::asin(sin_lambda * sin_eps);
152
153    ICRSPosition::new(
154        Angle::from_radians(normalize_angle_to_positive(ra)),
155        Angle::from_radians(dec),
156    )
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use celestial_time::julian::JulianDate;
163
164    #[test]
165    fn test_b0_range() {
166        let epochs = [
167            TT::j2000(),
168            TT::from_julian_date(JulianDate::new(J2000_JD + 91.0, 0.0)),
169            TT::from_julian_date(JulianDate::new(J2000_JD + 182.0, 0.0)),
170            TT::from_julian_date(JulianDate::new(J2000_JD + 273.0, 0.0)),
171        ];
172
173        for epoch in &epochs {
174            let b0 = compute_b0(epoch);
175            assert!(
176                b0.degrees().abs() <= 7.3,
177                "B0 = {} degrees exceeds expected range ±7.25°",
178                b0.degrees()
179            );
180        }
181    }
182
183    #[test]
184    fn test_l0_range() {
185        let epoch = TT::j2000();
186        let l0 = compute_l0(&epoch);
187        assert!(
188            l0.degrees() >= 0.0 && l0.degrees() < 360.0,
189            "L0 = {} degrees outside [0, 360) range",
190            l0.degrees()
191        );
192    }
193
194    #[test]
195    fn test_p_range() {
196        let epochs = [
197            TT::j2000(),
198            TT::from_julian_date(JulianDate::new(J2000_JD + 91.0, 0.0)),
199            TT::from_julian_date(JulianDate::new(J2000_JD + 182.0, 0.0)),
200            TT::from_julian_date(JulianDate::new(J2000_JD + 273.0, 0.0)),
201        ];
202
203        for epoch in &epochs {
204            let p = compute_p(epoch);
205            assert!(
206                p.degrees().abs() <= 45.0,
207                "P = {} degrees exceeds expected range ±45°",
208                p.degrees()
209            );
210        }
211    }
212
213    #[test]
214    fn test_carrington_rotation_period() {
215        let epoch1 = TT::j2000();
216        let l0_1 = compute_l0(&epoch1);
217
218        let epoch2 =
219            TT::from_julian_date(JulianDate::new(J2000_JD + CARRINGTON_SYNODIC_PERIOD, 0.0));
220        let l0_2 = compute_l0(&epoch2);
221
222        let diff = (l0_2.degrees() - l0_1.degrees()).abs();
223        assert!(
224            (diff - 360.0).abs() < 5.0 || diff < 5.0,
225            "L0 should change by ~360° in one Carrington rotation, got {} degrees",
226            diff
227        );
228    }
229
230    #[test]
231    fn test_solar_orientation_combined() {
232        let epoch = TT::j2000();
233        let orientation = compute_solar_orientation(&epoch);
234
235        assert!(
236            orientation.b0.degrees().abs() <= 7.3,
237            "B0 = {} out of range",
238            orientation.b0.degrees()
239        );
240        assert!(
241            orientation.l0.degrees() >= 0.0 && orientation.l0.degrees() < 360.0,
242            "L0 = {} out of range",
243            orientation.l0.degrees()
244        );
245        assert!(
246            orientation.p.degrees().abs() <= 30.0,
247            "P = {} out of range",
248            orientation.p.degrees()
249        );
250    }
251}