1use anise::constants::frames::SUN_J2000;
20use anise::errors::OrientationSnafu;
21use anise::prelude::Almanac;
22use serde::{Deserialize, Serialize};
23use serde_dhall::StaticType;
24use snafu::ResultExt;
25use typed_builder::TypedBuilder;
26
27use crate::cosmic::{AstroPhysicsSnafu, Epoch, Frame, Orbit};
28use crate::dynamics::{
29 AccelModel, DynamicsAlmanacSnafu, DynamicsAstroSnafu, DynamicsError, DynamicsPlanetarySnafu,
30};
31use crate::linalg::{Matrix3, U7, Vector3, Vector4};
32use hyperdual::linalg::norm;
33use hyperdual::{OHyperdual, hyperspace_from_vector};
34use std::fmt;
35use std::sync::Arc;
36
37#[derive(Clone, Debug, Serialize, Deserialize, StaticType, TypedBuilder)]
41pub struct SolidTides {
42 pub frame: Frame,
44 pub k2: f64,
46 pub k3: f64,
48 pub perturbers: Vec<TidalPerturber>,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize, StaticType, TypedBuilder)]
53pub struct TidalPerturber {
54 pub frame: Frame,
56 pub compute_degree_3: bool,
59}
60
61impl TidalPerturber {
62 fn compute_pert(
63 &self,
64 epoch: Epoch,
65 tidal_model: &SolidTides,
66 almanac: &Almanac,
67 delta_c: &mut [[f64; 4]; 4],
68 delta_s: &mut [[f64; 4]; 4],
69 ) -> Result<(), DynamicsError> {
70 let radius_km = almanac
71 .transform(self.frame, tidal_model.frame, epoch, None)
72 .context(DynamicsAlmanacSnafu {
73 action: "Moon position in ECEF",
74 })?
75 .radius_km;
76
77 let r_body = radius_km.norm();
78 let s_body = radius_km.x / r_body;
79 let t_body = radius_km.y / r_body;
80 let u_body = radius_km.z / r_body;
81
82 let sin_phi = u_body;
83 let cos_phi = (1.0 - sin_phi.powi(2)).max(0.0).sqrt();
84 let cos_lambda = if cos_phi > 1e-12 {
85 s_body / cos_phi
86 } else {
87 1.0
88 };
89 let sin_lambda = if cos_phi > 1e-12 {
90 t_body / cos_phi
91 } else {
92 0.0
93 };
94
95 let p20 = 0.5 * (3.0 * sin_phi.powi(2) - 1.0) * 5.0f64.sqrt();
97 let p21 = 3.0 * sin_phi * cos_phi * (5.0 / 3.0f64).sqrt();
98 let p22 = 3.0 * cos_phi.powi(2) * (5.0 / 12.0f64).sqrt();
99
100 let p30 = 0.5 * (5.0 * sin_phi.powi(3) - 3.0 * sin_phi) * 7.0f64.sqrt();
101 let p31 = 1.5 * (5.0 * sin_phi.powi(2) - 1.0) * cos_phi * (7.0 / 6.0f64).sqrt();
102 let p32 = 15.0 * sin_phi * cos_phi.powi(2) * (7.0 / 60.0f64).sqrt();
103 let p33 = 15.0 * cos_phi.powi(3) * (7.0 / 360.0f64).sqrt();
104
105 let primary_mu_km3_s2 = tidal_model
106 .frame
107 .mu_km3_s2()
108 .context(AstroPhysicsSnafu)
109 .context(DynamicsAstroSnafu)?;
110
111 let primary_eq_radius_km = tidal_model
112 .frame
113 .mean_equatorial_radius_km()
114 .context(AstroPhysicsSnafu)
115 .context(DynamicsAstroSnafu)?;
116
117 let secondary_mu_km3_s2 = self
118 .frame
119 .mu_km3_s2()
120 .context(AstroPhysicsSnafu)
121 .context(DynamicsAstroSnafu)?;
122
123 let gm_ratio = secondary_mu_km3_s2 / primary_mu_km3_s2;
124 let r_ratio = primary_eq_radius_km / r_body;
125
126 let m = if self.compute_degree_3 { 3 } else { 2 };
127
128 for n in 2..=m {
129 let kn = if n == 2 {
130 tidal_model.k2
131 } else {
132 tidal_model.k3
133 };
134 let common = kn / (2.0 * n as f64 + 1.0) * gm_ratio * r_ratio.powi(n as i32 + 1);
135
136 for m in 0..=n {
137 let p_nm = match (n, m) {
138 (2, 0) => p20,
139 (2, 1) => p21,
140 (2, 2) => p22,
141 (3, 0) => p30,
142 (3, 1) => p31,
143 (3, 2) => p32,
144 (3, 3) => p33,
145 _ => 0.0,
146 };
147
148 let (cos_ml, sin_ml) = match m {
149 0 => (1.0, 0.0),
150 1 => (cos_lambda, sin_lambda),
151 2 => (
152 cos_lambda.powi(2) - sin_lambda.powi(2),
153 2.0 * sin_lambda * cos_lambda,
154 ),
155 3 => (
156 cos_lambda * (cos_lambda.powi(2) - 3.0 * sin_lambda.powi(2)),
157 sin_lambda * (3.0 * cos_lambda.powi(2) - sin_lambda.powi(2)),
158 ),
159 _ => (0.0, 0.0),
160 };
161
162 delta_c[n][m] += common * p_nm * cos_ml;
163 delta_s[n][m] += common * p_nm * sin_ml;
164 }
165 }
166
167 Ok(())
168 }
169}
170
171impl SolidTides {
172 pub fn earth_moon_system(
175 mut earth_frame: Frame,
176 mut moon_frame: Frame,
177 almanac: Arc<Almanac>,
178 ) -> Result<Arc<Self>, DynamicsError> {
179 let mut sun_j2k = almanac
180 .frame_info(SUN_J2000)
181 .context(DynamicsPlanetarySnafu {
182 action: "fetching sun frame",
183 })?;
184
185 for frame in [&mut earth_frame, &mut moon_frame, &mut sun_j2k] {
187 if frame.mu_km3_s2.is_none() {
188 *frame = almanac.frame_info(*frame).context(DynamicsPlanetarySnafu {
189 action: "fetching sun frame",
190 })?;
191 }
192
193 frame
195 .mu_km3_s2()
196 .context(AstroPhysicsSnafu)
197 .context(DynamicsAstroSnafu)?;
198
199 frame
201 .mean_equatorial_radius_km()
202 .context(AstroPhysicsSnafu)
203 .context(DynamicsAstroSnafu)?;
204 }
205
206 let me = Self::builder()
207 .k2(0.3019)
208 .k3(0.093)
209 .frame(earth_frame)
210 .perturbers(vec![
211 TidalPerturber::builder()
212 .frame(moon_frame)
213 .compute_degree_3(true)
214 .build(),
215 TidalPerturber::builder()
216 .frame(sun_j2k)
217 .compute_degree_3(true)
218 .build(),
219 ])
220 .build();
221
222 Ok(Arc::new(me))
223 }
224
225 fn accumulate_deltas(
227 &self,
228 epoch: Epoch,
229 almanac: Arc<Almanac>,
230 ) -> Result<([[f64; 4]; 4], [[f64; 4]; 4]), DynamicsError> {
231 let mut delta_c = [[0.0f64; 4]; 4];
232 let mut delta_s = [[0.0f64; 4]; 4];
233
234 for pert in &self.perturbers {
235 pert.compute_pert(epoch, self, &almanac, &mut delta_c, &mut delta_s)?;
236 }
237
238 Ok((delta_c, delta_s))
239 }
240}
241
242#[allow(clippy::needless_range_loop)]
243impl AccelModel for SolidTides {
244 fn eom(&self, osc: &Orbit, almanac: Arc<Almanac>) -> Result<Vector3<f64>, DynamicsError> {
245 let (delta_c, delta_s) = self.accumulate_deltas(osc.epoch, almanac.clone())?;
246
247 let state = almanac
249 .transform_to(*osc, self.frame, None)
250 .context(DynamicsAlmanacSnafu {
251 action: "transforming into solid tides frame",
252 })?;
253
254 let r_ = state.rmag_km();
256 let s_ = state.radius_km.x / r_;
257 let t_ = state.radius_km.y / r_;
258 let u_ = state.radius_km.z / r_;
259
260 let mut a_nm = [[0.0f64; 6]; 6];
262 a_nm[0][0] = 1.0;
263 for n in 1..=4 {
264 a_nm[n][n] = (1.0 + 1.0 / (2.0 * n as f64)).sqrt() * a_nm[n - 1][n - 1];
265 }
266 a_nm[1][0] = u_ * 3.0f64.sqrt();
267 for n in 1..=4 {
268 a_nm[n + 1][n] = (2.0 * n as f64 + 3.0).sqrt() * u_ * a_nm[n][n];
269 }
270
271 let b_nm = |n: usize, m: usize| {
272 (((2.0 * n as f64 + 1.0) * (2.0 * n as f64 - 1.0))
273 / ((n as f64 + m as f64) * (n as f64 - m as f64)))
274 .sqrt()
275 };
276 let c_nm = |n: usize, m: usize| {
277 (((2.0 * n as f64 + 1.0) * (n as f64 + m as f64 - 1.0) * (n as f64 - m as f64 - 1.0))
278 / ((n as f64 - m as f64) * (n as f64 + m as f64) * (2.0 * n as f64 - 3.0)))
279 .sqrt()
280 };
281
282 for m in 0..=3 {
283 for n in (m + 2)..=4 {
284 a_nm[n][m] = u_ * b_nm(n, m) * a_nm[n - 1][m] - c_nm(n, m) * a_nm[n - 2][m];
285 }
286 }
287
288 let mut r_m = [0.0f64; 4];
289 let mut i_m = [0.0f64; 4];
290 r_m[0] = 1.0;
291 i_m[0] = 0.0;
292 for m in 1..=3 {
293 r_m[m] = s_ * r_m[m - 1] - t_ * i_m[m - 1];
294 i_m[m] = s_ * i_m[m - 1] + t_ * r_m[m - 1];
295 }
296
297 let eq_radius_km = self
298 .frame
299 .mean_equatorial_radius_km()
300 .context(AstroPhysicsSnafu)
301 .context(DynamicsAstroSnafu)?;
302
303 let mu_km3_s2 = self
304 .frame
305 .mu_km3_s2()
306 .context(AstroPhysicsSnafu)
307 .context(DynamicsAstroSnafu)?;
308
309 let rho = eq_radius_km / r_;
310
311 let mut rho_np1 = mu_km3_s2 / r_ * rho;
312 let mut accel4 = Vector4::zeros();
313
314 let vr01 = |n: usize, m: usize| {
315 let mut val = ((n as f64 - m as f64) * (n as f64 + m as f64 + 1.0)).sqrt();
316 if m == 0 {
317 val /= 2.0f64.sqrt();
318 }
319 val
320 };
321 let vr11 = |n: usize, m: usize| {
322 let mut val = (((2.0 * n as f64 + 1.0)
323 * (n as f64 + m as f64 + 2.0)
324 * (n as f64 + m as f64 + 1.0))
325 / (2.0 * n as f64 + 3.0))
326 .sqrt();
327 if m == 0 {
328 val /= 2.0f64.sqrt();
329 }
330 val
331 };
332
333 let sqrt2 = 2.0f64.sqrt();
334
335 for n in 1..=3 {
336 rho_np1 *= rho;
337 if n < 2 {
338 continue;
339 } let mut sum = Vector4::zeros();
342 for m in 0..=n {
343 let c_val = delta_c[n][m];
344 let s_val = delta_s[n][m];
345
346 let d_ = (c_val * r_m[m] + s_val * i_m[m]) * sqrt2;
347 let e_ = if m == 0 {
348 0.0
349 } else {
350 (c_val * r_m[m - 1] + s_val * i_m[m - 1]) * sqrt2
351 };
352 let f_ = if m == 0 {
353 0.0
354 } else {
355 (s_val * r_m[m - 1] - c_val * i_m[m - 1]) * sqrt2
356 };
357
358 sum.x += (m as f64) * a_nm[n][m] * e_;
359 sum.y += (m as f64) * a_nm[n][m] * f_;
360 sum.z += vr01(n, m) * a_nm[n][m + 1] * d_;
361 sum.w -= vr11(n, m) * a_nm[n + 1][m + 1] * d_;
362 }
363 accel4 += (rho_np1 / eq_radius_km) * sum;
364 }
365
366 let accel_ecef = Vector3::new(
367 accel4.x + accel4.w * s_,
368 accel4.y + accel4.w * t_,
369 accel4.z + accel4.w * u_,
370 );
371
372 let dcm = almanac
373 .rotate(self.frame, osc.frame, osc.epoch)
374 .context(OrientationSnafu {
375 action: "rotating accel back to integration frame",
376 })
377 .context(DynamicsAlmanacSnafu {
378 action: "rotating accel back to integration frame",
379 })?
380 .rot_mat;
381
382 Ok(dcm * accel_ecef)
383 }
384
385 fn gradient(
386 &self,
387 osc: &Orbit,
388 almanac: Arc<Almanac>,
389 ) -> Result<(Vector3<f64>, Matrix3<f64>), DynamicsError> {
390 let (delta_c, delta_s) = self.accumulate_deltas(osc.epoch, almanac.clone())?;
391
392 let state = almanac
394 .transform_to(*osc, self.frame, None)
395 .context(DynamicsAlmanacSnafu {
396 action: "transforming into gravity field frame",
397 })?;
398
399 let radius: Vector3<OHyperdual<f64, U7>> = hyperspace_from_vector(&state.radius_km);
400
401 let r_ = norm(&radius);
403 let s_ = radius[0] / r_;
404 let t_ = radius[1] / r_;
405 let u_ = radius[2] / r_;
406
407 let mut a_nm = [[OHyperdual::<f64, U7>::from(0.0); 6]; 6];
409 a_nm[0][0] = OHyperdual::from(1.0);
410 for n in 1..=4 {
411 a_nm[n][n] =
412 OHyperdual::from((1.0 + 1.0 / (2.0 * n as f64)).sqrt()) * a_nm[n - 1][n - 1];
413 }
414 a_nm[1][0] = u_ * OHyperdual::from(3.0f64.sqrt());
415 for n in 1..=4 {
416 a_nm[n + 1][n] = OHyperdual::from((2.0 * n as f64 + 3.0).sqrt()) * u_ * a_nm[n][n];
417 }
418
419 let b_nm = |n: usize, m: usize| {
420 (((2.0 * n as f64 + 1.0) * (2.0 * n as f64 - 1.0))
421 / ((n as f64 + m as f64) * (n as f64 - m as f64)))
422 .sqrt()
423 };
424 let c_nm = |n: usize, m: usize| {
425 (((2.0 * n as f64 + 1.0) * (n as f64 + m as f64 - 1.0) * (n as f64 - m as f64 - 1.0))
426 / ((n as f64 - m as f64) * (n as f64 + m as f64) * (2.0 * n as f64 - 3.0)))
427 .sqrt()
428 };
429
430 for m in 0..=3 {
431 for n in (m + 2)..=4 {
432 a_nm[n][m] = u_ * OHyperdual::from(b_nm(n, m)) * a_nm[n - 1][m]
433 - OHyperdual::from(c_nm(n, m)) * a_nm[n - 2][m];
434 }
435 }
436
437 let mut r_m = [OHyperdual::<f64, U7>::from(0.0); 4];
438 let mut i_m = [OHyperdual::<f64, U7>::from(0.0); 4];
439 r_m[0] = OHyperdual::from(1.0);
440 i_m[0] = OHyperdual::from(0.0);
441 for m in 1..=3 {
442 r_m[m] = s_ * r_m[m - 1] - t_ * i_m[m - 1];
443 i_m[m] = s_ * i_m[m - 1] + t_ * r_m[m - 1];
444 }
445
446 let real_eq_radius_km = self
447 .frame
448 .mean_equatorial_radius_km()
449 .context(AstroPhysicsSnafu)
450 .context(DynamicsAstroSnafu)?;
451
452 let real_mu_km3_s2 = self
453 .frame
454 .mu_km3_s2()
455 .context(AstroPhysicsSnafu)
456 .context(DynamicsAstroSnafu)?;
457
458 let eq_radius = OHyperdual::<f64, U7>::from(real_eq_radius_km);
459 let rho = eq_radius / r_;
460 let mut rho_np1 = OHyperdual::<f64, U7>::from(real_mu_km3_s2) / r_ * rho;
461
462 let mut a0 = OHyperdual::<f64, U7>::from(0.0);
463 let mut a1 = OHyperdual::<f64, U7>::from(0.0);
464 let mut a2 = OHyperdual::<f64, U7>::from(0.0);
465 let mut a3 = OHyperdual::<f64, U7>::from(0.0);
466
467 let vr01 = |n: usize, m: usize| {
468 let mut val = ((n as f64 - m as f64) * (n as f64 + m as f64 + 1.0)).sqrt();
469 if m == 0 {
470 val /= 2.0f64.sqrt();
471 }
472 val
473 };
474 let vr11 = |n: usize, m: usize| {
475 let mut val = (((2.0 * n as f64 + 1.0)
476 * (n as f64 + m as f64 + 2.0)
477 * (n as f64 + m as f64 + 1.0))
478 / (2.0 * n as f64 + 3.0))
479 .sqrt();
480 if m == 0 {
481 val /= 2.0f64.sqrt();
482 }
483 val
484 };
485 let sqrt2 = OHyperdual::<f64, U7>::from(2.0f64.sqrt());
486
487 for n in 1..=3 {
488 rho_np1 *= rho;
489 if n < 2 {
490 continue;
491 }
492
493 let mut sum0 = OHyperdual::from(0.0);
494 let mut sum1 = OHyperdual::from(0.0);
495 let mut sum2 = OHyperdual::from(0.0);
496 let mut sum3 = OHyperdual::from(0.0);
497
498 for m in 0..=n {
499 let c_val = OHyperdual::from(delta_c[n][m]);
500 let s_val = OHyperdual::from(delta_s[n][m]);
501
502 let d_ = (c_val * r_m[m] + s_val * i_m[m]) * sqrt2;
503 let e_ = if m == 0 {
504 OHyperdual::from(0.0)
505 } else {
506 (c_val * r_m[m - 1] + s_val * i_m[m - 1]) * sqrt2
507 };
508 let f_ = if m == 0 {
509 OHyperdual::from(0.0)
510 } else {
511 (s_val * r_m[m - 1] - c_val * i_m[m - 1]) * sqrt2
512 };
513
514 sum0 += OHyperdual::from(m as f64) * a_nm[n][m] * e_;
515 sum1 += OHyperdual::from(m as f64) * a_nm[n][m] * f_;
516 sum2 += OHyperdual::from(vr01(n, m)) * a_nm[n][m + 1] * d_;
517 sum3 += OHyperdual::from(vr11(n, m)) * a_nm[n + 1][m + 1] * d_;
518 }
519 let rr = rho_np1 / eq_radius;
520 a0 += rr * sum0;
521 a1 += rr * sum1;
522 a2 += rr * sum2;
523 a3 -= rr * sum3;
524 }
525
526 let accel_local = Vector3::new(a0 + a3 * s_, a1 + a3 * t_, a2 + a3 * u_);
527
528 let dcm = almanac
529 .rotate(self.frame, osc.frame, osc.epoch)
530 .context(OrientationSnafu {
531 action: "rotating accel back to integration frame",
532 })
533 .context(DynamicsAlmanacSnafu {
534 action: "rotating accel back to integration frame",
535 })?
536 .rot_mat;
537
538 let dx = dcm
539 * Vector3::new(
540 accel_local[0].real(),
541 accel_local[1].real(),
542 accel_local[2].real(),
543 );
544
545 let mut grad_local = Matrix3::zeros();
546 for i in 0..3 {
547 for j in 1..4 {
548 grad_local[(i, j - 1)] += accel_local[i][j];
549 }
550 }
551 let grad = dcm * grad_local * dcm.transpose();
552 Ok((dx, grad))
553 }
554}
555
556impl fmt::Display for SolidTides {
557 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
558 write!(f, "Solid tides for {}", self.frame)
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use crate::cosmic::Orbit;
566 use anise::constants::frames::{EARTH_J2000, IAU_EARTH_FRAME, IAU_MOON_FRAME};
567 use std::path::PathBuf;
568 use std::str::FromStr;
569
570 #[test]
571 fn test_solid_tides_earth() {
572 let data_folder: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../data/01_planetary"]
573 .iter()
574 .collect();
575 let mut almanac = Almanac::default();
576 almanac = almanac
578 .load(data_folder.join("de440s.bsp").to_str().unwrap())
579 .unwrap();
580 almanac = almanac
581 .load(data_folder.join("pck08.pca").to_str().unwrap())
582 .unwrap();
583 let almanac = Arc::new(almanac);
584
585 let epoch = Epoch::from_str("2024-01-01T12:00:00 UTC").unwrap();
586 let frame = Frame::from(EARTH_J2000);
587 let sc_orbit = Orbit::cartesian(7000.0, 0.0, 0.0, 0.0, 7.5, 0.0, epoch, frame);
588
589 let tides = SolidTides::earth_moon_system(IAU_EARTH_FRAME, IAU_MOON_FRAME, almanac.clone())
590 .expect("could not init solid tides");
591 let acc = tides.eom(&sc_orbit, almanac.clone()).unwrap();
592
593 println!("Solid tides acceleration: {:?}", acc);
594 assert!(acc.norm() > 0.0);
596 assert!(acc.norm() < 1e-6);
597
598 let (acc_grad, grad) = tides.gradient(&sc_orbit, almanac).unwrap();
599 assert!((acc - acc_grad).norm() < 1e-12);
600 assert!(grad.norm() > 0.0);
601 }
602}