1use crate::astro::constants::earth::WGS84_A_KM;
10use crate::astro::constants::units::DEGREES_PER_SEMICIRCLE;
11use crate::astro::math::vec3;
12
13const RIGHT_ANGLE_DEG: f64 = 90.0;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
18pub enum AngleError {
19 #[error("invalid angle input {field}: {reason}")]
20 InvalidInput {
21 field: &'static str,
22 reason: &'static str,
23 },
24}
25
26#[inline]
30pub fn rad_to_deg_ref(rad: f64) -> f64 {
31 rad * DEGREES_PER_SEMICIRCLE / std::f64::consts::PI
32}
33
34#[inline]
42pub fn normalize_geodetic_lon_rad(lon_rad: f64) -> f64 {
43 if lon_rad <= -std::f64::consts::PI {
44 std::f64::consts::PI
45 } else {
46 lon_rad
47 }
48}
49
50#[inline]
53fn angle_between(
54 a: [f64; 3],
55 a_field: &'static str,
56 b: [f64; 3],
57 b_field: &'static str,
58) -> Result<f64, AngleError> {
59 validate_nonzero_vec3(a, a_field)?;
60 validate_nonzero_vec3(b, b_field)?;
61 let cos_theta = vec3::dot3(a, b) / (vec3::norm3(a) * vec3::norm3(b));
62 let cos_theta = cos_theta.clamp(-1.0, 1.0);
64 Ok(rad_to_deg_ref(cos_theta.acos()))
65}
66
67#[inline]
70fn nadir_body_angle(
71 sat_pos: [f64; 3],
72 body_pos: [f64; 3],
73 body_field: &'static str,
74) -> Result<f64, AngleError> {
75 validate_nonzero_vec3(sat_pos, "sat_pos")?;
76 validate_nonzero_vec3(body_pos, body_field)?;
77 let nadir = vec3::neg3(sat_pos);
78 let body_from_sat = vec3::sub3(body_pos, sat_pos);
79 angle_between(nadir, "sat_pos", body_from_sat, body_field)
80}
81
82pub fn sun_angle(sat_pos: [f64; 3], sun_pos: [f64; 3]) -> Result<f64, AngleError> {
87 nadir_body_angle(sat_pos, sun_pos, "sun_pos")
88}
89
90pub fn moon_angle(sat_pos: [f64; 3], moon_pos: [f64; 3]) -> Result<f64, AngleError> {
92 nadir_body_angle(sat_pos, moon_pos, "moon_pos")
93}
94
95pub fn sun_elevation(sat_pos: [f64; 3], sun_pos: [f64; 3]) -> Result<f64, AngleError> {
100 validate_nonzero_vec3(sat_pos, "sat_pos")?;
101 validate_nonzero_vec3(sun_pos, "sun_pos")?;
102 let sun_from_sat = vec3::sub3(sun_pos, sat_pos);
103 let zenith_angle = angle_between(sat_pos, "sat_pos", sun_from_sat, "sun_pos")?;
104 Ok(RIGHT_ANGLE_DEG - zenith_angle)
105}
106
107pub fn phase_angle(
110 sat_pos: [f64; 3],
111 sun_pos: [f64; 3],
112 observer_pos: [f64; 3],
113) -> Result<f64, AngleError> {
114 validate_nonzero_vec3(sat_pos, "sat_pos")?;
115 validate_nonzero_vec3(sun_pos, "sun_pos")?;
116 validate_nonzero_vec3(observer_pos, "observer_pos")?;
117 let sun_from_sat = vec3::sub3(sun_pos, sat_pos);
118 let observer_from_sat = vec3::sub3(observer_pos, sat_pos);
119 angle_between(sun_from_sat, "sun_pos", observer_from_sat, "observer_pos")
120}
121
122pub fn earth_angular_radius(sat_pos: [f64; 3]) -> Result<f64, AngleError> {
125 validate_nonzero_vec3(sat_pos, "sat_pos")?;
126 let distance = vec3::norm3(sat_pos);
127 let ratio = (WGS84_A_KM / distance).min(1.0);
128 Ok(rad_to_deg_ref(ratio.asin()))
129}
130
131fn validate_nonzero_vec3(v: [f64; 3], field: &'static str) -> Result<(), AngleError> {
132 if !v.iter().all(|value| value.is_finite()) {
133 return Err(invalid_angle_input(field, "not finite"));
134 }
135 let norm = vec3::norm3(v);
136 if norm == 0.0 {
137 return Err(invalid_angle_input(field, "zero vector"));
138 }
139 if !norm.is_finite() {
140 return Err(invalid_angle_input(field, "out of range"));
141 }
142 Ok(())
143}
144
145fn invalid_angle_input(field: &'static str, reason: &'static str) -> AngleError {
146 AngleError::InvalidInput { field, reason }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
157 fn sun_angle_matches_reference_bits() {
158 let sat = [6778.0, 0.0, 0.0];
159 let skew_sat = [6778.0, 123.0, -456.0];
160 assert_eq!(
161 sun_angle(sat, [149_597_870.0, 0.0, 0.0])
162 .expect("valid angle geometry")
163 .to_bits(),
164 0x4066_8000_0000_0000
165 );
166 assert_eq!(
167 sun_angle(sat, [-149_597_870.0, 0.0, 0.0])
168 .expect("valid angle geometry")
169 .to_bits(),
170 0x0000_0000_0000_0000
171 );
172 assert_eq!(
173 sun_angle(skew_sat, [149_597_870.0, 1_000_000.0, -500_000.0])
174 .expect("valid angle geometry")
175 .to_bits(),
176 0x4066_091c_484a_7158
177 );
178 }
179
180 #[test]
181 fn moon_angle_matches_reference_bits() {
182 let sat = [6778.0, 0.0, 0.0];
183 let skew_sat = [6778.0, 123.0, -456.0];
184 assert_eq!(
185 moon_angle(sat, [200_000.0, 300_000.0, 50_000.0])
186 .expect("valid angle geometry")
187 .to_bits(),
188 0x405e_9b67_b2be_cf9b
189 );
190 assert_eq!(
191 moon_angle(skew_sat, [-384_400.0, 12_345.0, 6_789.0])
192 .expect("valid angle geometry")
193 .to_bits(),
194 0x400f_c228_50bd_874f
195 );
196 }
197
198 #[test]
199 fn sun_elevation_matches_reference_bits() {
200 let sat = [6778.0, 0.0, 0.0];
201 let skew_sat = [6778.0, 123.0, -456.0];
202 assert_eq!(
203 sun_elevation(sat, [149_597_870.0, 0.0, 0.0])
204 .expect("valid angle geometry")
205 .to_bits(),
206 0x4056_8000_0000_0000
207 );
208 assert_eq!(
209 sun_elevation(sat, [0.0, 149_597_870.0, 0.0])
210 .expect("valid angle geometry")
211 .to_bits(),
212 0xbf65_4421_f2e3_8000
213 );
214 assert_eq!(
215 sun_elevation(skew_sat, [149_597_870.0, 1_000_000.0, -500_000.0])
216 .expect("valid angle geometry")
217 .to_bits(),
218 0x4055_9238_9094_e2b1
219 );
220 }
221
222 #[test]
223 fn phase_angle_matches_reference_bits() {
224 let sat = [6778.0, 0.0, 0.0];
225 let skew_sat = [6778.0, 123.0, -456.0];
226 assert_eq!(
227 phase_angle(sat, [149_597_870.0, 1_000_000.0, 0.0], [0.0, 6378.0, 0.0],)
228 .expect("valid angle geometry")
229 .to_bits(),
230 0x4061_0b78_cc20_1866
231 );
232 assert_eq!(
233 phase_angle(
234 skew_sat,
235 [149_597_870.0, 1_000_000.0, -500_000.0],
236 [-6378.0, 100.0, 50.0]
237 )
238 .expect("valid angle geometry")
239 .to_bits(),
240 0x4066_3f01_b89b_b002
241 );
242 }
243
244 #[test]
245 fn earth_angular_radius_matches_reference_bits() {
246 assert_eq!(
247 earth_angular_radius([6778.0, 0.0, 0.0])
248 .expect("valid angle geometry")
249 .to_bits(),
250 0x4051_8e27_583c_2f41
251 );
252 assert_eq!(
253 earth_angular_radius([42_164.0, 0.0, 0.0])
254 .expect("valid angle geometry")
255 .to_bits(),
256 0x4021_66aa_1bd9_bda5
257 );
258 assert_eq!(
259 earth_angular_radius([7000.0, 1234.0, -567.0])
260 .expect("valid angle geometry")
261 .to_bits(),
262 0x404f_b89e_165a_1133
263 );
264 }
265
266 #[test]
267 fn angle_helpers_reject_invalid_vectors() {
268 assert_invalid_angle_field(
269 sun_angle([0.0, 0.0, 0.0], [149_597_870.0, 0.0, 0.0]).unwrap_err(),
270 "sat_pos",
271 "zero vector",
272 );
273 assert_invalid_angle_field(
274 moon_angle([6778.0, 0.0, 0.0], [f64::NAN, 0.0, 0.0]).unwrap_err(),
275 "moon_pos",
276 "not finite",
277 );
278 assert_invalid_angle_field(
279 sun_elevation([6778.0, 0.0, 0.0], [6778.0, 0.0, 0.0]).unwrap_err(),
280 "sun_pos",
281 "zero vector",
282 );
283 assert_invalid_angle_field(
284 phase_angle(
285 [6778.0, 0.0, 0.0],
286 [149_597_870.0, 0.0, 0.0],
287 [6778.0, 0.0, 0.0],
288 )
289 .unwrap_err(),
290 "observer_pos",
291 "zero vector",
292 );
293 assert_invalid_angle_field(
294 earth_angular_radius([f64::INFINITY, 0.0, 0.0]).unwrap_err(),
295 "sat_pos",
296 "not finite",
297 );
298 }
299
300 fn assert_invalid_angle_field(
301 error: AngleError,
302 expected: &'static str,
303 expected_reason: &'static str,
304 ) {
305 let AngleError::InvalidInput { field, reason } = error;
306 assert_eq!(field, expected);
307 assert_eq!(reason, expected_reason);
308 }
309}