1pub fn miss_distance_km(accel_m_s2: f64, burn_time_s: f64, pointing_error_rad: f64) -> f64 {
15 let lateral_accel = accel_m_s2 * pointing_error_rad.sin();
16 let miss_m = 0.5 * lateral_accel * burn_time_s * burn_time_s;
17 miss_m / 1000.0 }
19
20pub fn required_pointing_rad(accel_m_s2: f64, burn_time_s: f64, max_miss_km: f64) -> f64 {
26 let sin_theta = 2.0 * max_miss_km * 1000.0 / (accel_m_s2 * burn_time_s * burn_time_s);
27 if sin_theta.abs() > 1.0 {
28 std::f64::consts::FRAC_PI_2
30 } else {
31 sin_theta.asin()
32 }
33}
34
35pub fn flip_angular_rate(flip_duration_s: f64) -> f64 {
41 std::f64::consts::PI / flip_duration_s
42}
43
44pub fn flip_angular_momentum(mass_kg: f64, radius_m: f64, angular_rate_rad_s: f64) -> f64 {
52 let moment_of_inertia = 0.5 * mass_kg * radius_m * radius_m;
53 moment_of_inertia * angular_rate_rad_s
54}
55
56pub fn flip_rcs_torque(mass_kg: f64, radius_m: f64, flip_duration_s: f64, ramp_time_s: f64) -> f64 {
63 let moment_of_inertia = 0.5 * mass_kg * radius_m * radius_m;
64 let coast_duration = flip_duration_s - 2.0 * ramp_time_s;
67 if coast_duration <= 0.0 {
68 let omega_peak = 2.0 * std::f64::consts::PI / flip_duration_s;
70 let angular_accel = omega_peak / (flip_duration_s / 2.0);
71 return moment_of_inertia * angular_accel;
72 }
73 let omega_peak = std::f64::consts::PI / (coast_duration + ramp_time_s);
74 let angular_accel = omega_peak / ramp_time_s;
75 moment_of_inertia * angular_accel
76}
77
78pub fn velocity_error_from_pointing(
83 accel_m_s2: f64,
84 burn_time_s: f64,
85 pointing_error_rad: f64,
86) -> f64 {
87 let v_error_m_s = accel_m_s2 * burn_time_s * pointing_error_rad.sin();
88 v_error_m_s / 1000.0 }
90
91pub fn accuracy_to_pointing_error_rad(accuracy_fraction: f64) -> f64 {
98 accuracy_fraction.clamp(-1.0, 1.0).acos()
99}
100
101pub fn gravity_gradient_torque(
111 gm_m3_s2: f64,
112 distance_m: f64,
113 mass_kg: f64,
114 length_m: f64,
115 angle_rad: f64,
116) -> f64 {
117 let delta_i = mass_kg * length_m * length_m / 12.0;
119 let r3 = distance_m * distance_m * distance_m;
120 3.0 * gm_m3_s2 * delta_i * (2.0 * angle_rad).sin() / (2.0 * r3)
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_miss_distance_small_angle() {
129 let miss = miss_distance_km(20.0, 3600.0, 0.001);
131 assert!((miss - 129.6).abs() < 0.1, "miss = {miss}");
133 }
134
135 #[test]
136 fn test_miss_distance_zero_error() {
137 assert_eq!(miss_distance_km(20.0, 3600.0, 0.0), 0.0);
138 }
139
140 #[test]
141 fn test_required_pointing_inverse() {
142 let accel = 20.0;
143 let burn_time = 3600.0;
144 let target_miss = 10.0; let theta = required_pointing_rad(accel, burn_time, target_miss);
146 let actual_miss = miss_distance_km(accel, burn_time, theta);
147 assert!(
148 (actual_miss - target_miss).abs() < 0.01,
149 "actual = {actual_miss}"
150 );
151 }
152
153 #[test]
154 fn test_flip_angular_rate() {
155 let rate = flip_angular_rate(60.0);
157 assert!((rate - std::f64::consts::PI / 60.0).abs() < 1e-10);
158 }
159
160 #[test]
161 fn test_flip_angular_momentum() {
162 let h = flip_angular_momentum(300_000.0, 5.0, 0.05);
164 assert!((h - 187_500.0).abs() < 1.0, "h = {h}");
167 }
168
169 #[test]
170 fn test_flip_rcs_torque() {
171 let torque = flip_rcs_torque(300_000.0, 5.0, 60.0, 10.0);
173 assert!((torque - 23_562.0).abs() < 100.0, "torque = {torque}");
179 }
180
181 #[test]
182 fn test_velocity_error() {
183 let v_err = velocity_error_from_pointing(20.0, 3600.0, 0.001);
185 assert!((v_err - 0.072).abs() < 0.001, "v_err = {v_err}");
187 }
188
189 #[test]
190 fn test_accuracy_to_pointing() {
191 let theta = accuracy_to_pointing_error_rad(0.998);
193 assert!((theta - 0.0632).abs() < 0.001, "theta = {theta}");
195 }
196
197 #[test]
198 fn test_gravity_gradient_torque_zero_angle() {
199 let torque = gravity_gradient_torque(3.986e14, 7_000_000.0, 300_000.0, 100.0, 0.0);
201 assert!(torque.abs() < 1e-6, "torque = {torque}");
202 }
203
204 #[test]
205 fn test_gravity_gradient_torque_nonzero() {
206 let torque = gravity_gradient_torque(
208 3.986e14, 7_000_000.0, 300_000.0, 100.0, std::f64::consts::FRAC_PI_4,
213 );
214 assert!(torque > 0.0, "torque = {torque}");
216 assert!((torque - 436.0).abs() < 10.0, "torque = {torque}");
220 }
221
222 #[test]
225 fn required_pointing_clamp_to_frac_pi_2() {
226 let theta = required_pointing_rad(0.001, 1.0, 1_000_000.0);
228 assert_eq!(theta, std::f64::consts::FRAC_PI_2);
229 }
230
231 #[test]
232 fn flip_rcs_torque_triangular_profile() {
233 let torque_tri = flip_rcs_torque(300_000.0, 5.0, 60.0, 30.0);
235 let expected = 3_750_000.0 * 2.0 * std::f64::consts::PI / 1800.0;
240 assert!((torque_tri - expected).abs() < 1.0, "torque = {torque_tri}");
241 }
242
243 #[test]
244 fn flip_rcs_torque_overlong_ramp() {
245 let torque = flip_rcs_torque(300_000.0, 5.0, 60.0, 40.0);
247 assert!(torque > 0.0, "torque should be positive: {torque}");
248 }
249
250 #[test]
251 fn gravity_gradient_max_at_45_degrees() {
252 let gm = 3.986e14;
253 let r = 7_000_000.0;
254 let m = 300_000.0;
255 let l = 100.0;
256 let torque_45 = gravity_gradient_torque(gm, r, m, l, std::f64::consts::FRAC_PI_4);
257 let torque_30 = gravity_gradient_torque(gm, r, m, l, std::f64::consts::FRAC_PI_6);
258 let torque_60 = gravity_gradient_torque(gm, r, m, l, std::f64::consts::FRAC_PI_3);
259 assert!(torque_45 > torque_30, "45° > 30°");
261 assert!(torque_45 > torque_60, "45° > 60°");
262 }
263
264 #[test]
265 fn velocity_error_zero_burn_time() {
266 assert_eq!(velocity_error_from_pointing(20.0, 0.0, 0.1), 0.0);
267 }
268
269 #[test]
270 fn velocity_error_zero_accel() {
271 assert_eq!(velocity_error_from_pointing(0.0, 3600.0, 0.1), 0.0);
272 }
273
274 #[test]
275 fn miss_distance_large_angle() {
276 let miss = miss_distance_km(20.0, 3600.0, std::f64::consts::FRAC_PI_2);
278 assert!((miss - 129_600.0).abs() < 1.0, "miss = {miss}");
280 }
281
282 #[test]
283 fn accuracy_100_percent_is_zero_error() {
284 let theta = accuracy_to_pointing_error_rad(1.0);
285 assert!(theta.abs() < 1e-15, "theta = {theta}");
286 }
287
288 #[test]
289 fn accuracy_0_percent_is_pi_over_2() {
290 let theta = accuracy_to_pointing_error_rad(0.0);
291 assert!(
292 (theta - std::f64::consts::FRAC_PI_2).abs() < 1e-15,
293 "theta = {theta}"
294 );
295 }
296}