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