Skip to main content

nyx_space/dynamics/
solid_tides.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use 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;
35
36#[cfg(feature = "python")]
37use pyo3::prelude::*;
38#[cfg(feature = "python")]
39use pyo3::types::PyType;
40
41/// `SolidTides` implements the solid tide acceleration model.
42/// It accounts for the crust deformation due to the configured tidal perturbers.
43/// Formulas are based on IERS 2010 Conventions.
44#[derive(Clone, Debug, Serialize, Deserialize, StaticType, TypedBuilder, PartialEq)]
45#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
46pub struct SolidTides {
47    /// The body-fixed frame of the central body being deformed.
48    pub frame: Frame,
49    /// 2nd degree Love number
50    pub k2: f64,
51    /// 3rd degree Love number
52    pub k3: f64,
53    /// The collection of celestial bodies raising the tide.
54    pub perturbers: Vec<TidalPerturber>,
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize, StaticType, TypedBuilder, PartialEq)]
58#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
59pub struct TidalPerturber {
60    /// The frame used to resolve the state of the perturber relative to central_frame.
61    pub frame: Frame,
62    /// Optimization flag: true only if (R_eq / r_j)^4 is large enough to warrant k3.
63    /// Set to True for the Earth system
64    pub compute_degree_3: bool,
65}
66
67impl TidalPerturber {
68    fn compute_pert(
69        &self,
70        epoch: Epoch,
71        tidal_model: &SolidTides,
72        almanac: &Almanac,
73        delta_c: &mut [[f64; 4]; 4],
74        delta_s: &mut [[f64; 4]; 4],
75    ) -> Result<(), DynamicsError> {
76        let radius_km = almanac
77            .transform(self.frame, tidal_model.frame, epoch, None)
78            .context(DynamicsAlmanacSnafu {
79                action: "Moon position in ECEF",
80            })?
81            .radius_km;
82
83        let r_body = radius_km.norm();
84        let s_body = radius_km.x / r_body;
85        let t_body = radius_km.y / r_body;
86        let u_body = radius_km.z / r_body;
87
88        let sin_phi = u_body;
89        let cos_phi = (1.0 - sin_phi.powi(2)).max(0.0).sqrt();
90        let cos_lambda = if cos_phi > 1e-12 {
91            s_body / cos_phi
92        } else {
93            1.0
94        };
95        let sin_lambda = if cos_phi > 1e-12 {
96            t_body / cos_phi
97        } else {
98            0.0
99        };
100
101        // Fully normalized Associated Legendre Polynomials P_nm(sin_phi) for n=2,3
102        let p20 = 0.5 * (3.0 * sin_phi.powi(2) - 1.0) * 5.0f64.sqrt();
103        let p21 = 3.0 * sin_phi * cos_phi * (5.0 / 3.0f64).sqrt();
104        let p22 = 3.0 * cos_phi.powi(2) * (5.0 / 12.0f64).sqrt();
105
106        let p30 = 0.5 * (5.0 * sin_phi.powi(3) - 3.0 * sin_phi) * 7.0f64.sqrt();
107        let p31 = 1.5 * (5.0 * sin_phi.powi(2) - 1.0) * cos_phi * (7.0 / 6.0f64).sqrt();
108        let p32 = 15.0 * sin_phi * cos_phi.powi(2) * (7.0 / 60.0f64).sqrt();
109        let p33 = 15.0 * cos_phi.powi(3) * (7.0 / 360.0f64).sqrt();
110
111        let primary_mu_km3_s2 = tidal_model
112            .frame
113            .mu_km3_s2()
114            .context(AstroPhysicsSnafu)
115            .context(DynamicsAstroSnafu)?;
116
117        let primary_eq_radius_km = tidal_model
118            .frame
119            .mean_equatorial_radius_km()
120            .context(AstroPhysicsSnafu)
121            .context(DynamicsAstroSnafu)?;
122
123        let secondary_mu_km3_s2 = self
124            .frame
125            .mu_km3_s2()
126            .context(AstroPhysicsSnafu)
127            .context(DynamicsAstroSnafu)?;
128
129        let gm_ratio = secondary_mu_km3_s2 / primary_mu_km3_s2;
130        let r_ratio = primary_eq_radius_km / r_body;
131
132        let m = if self.compute_degree_3 { 3 } else { 2 };
133
134        for n in 2..=m {
135            let kn = if n == 2 {
136                tidal_model.k2
137            } else {
138                tidal_model.k3
139            };
140            let common = kn / (2.0 * n as f64 + 1.0) * gm_ratio * r_ratio.powi(n as i32 + 1);
141
142            for m in 0..=n {
143                let p_nm = match (n, m) {
144                    (2, 0) => p20,
145                    (2, 1) => p21,
146                    (2, 2) => p22,
147                    (3, 0) => p30,
148                    (3, 1) => p31,
149                    (3, 2) => p32,
150                    (3, 3) => p33,
151                    _ => 0.0,
152                };
153
154                let (cos_ml, sin_ml) = match m {
155                    0 => (1.0, 0.0),
156                    1 => (cos_lambda, sin_lambda),
157                    2 => (
158                        cos_lambda.powi(2) - sin_lambda.powi(2),
159                        2.0 * sin_lambda * cos_lambda,
160                    ),
161                    3 => (
162                        cos_lambda * (cos_lambda.powi(2) - 3.0 * sin_lambda.powi(2)),
163                        sin_lambda * (3.0 * cos_lambda.powi(2) - sin_lambda.powi(2)),
164                    ),
165                    _ => (0.0, 0.0),
166                };
167
168                delta_c[n][m] += common * p_nm * cos_ml;
169                delta_s[n][m] += common * p_nm * sin_ml;
170            }
171        }
172
173        Ok(())
174    }
175}
176
177impl SolidTides {
178    /// Initializes solid tides with the Moon and the Sun, where the k3 is only computed for the Moon.
179    /// Sets the k2 Love number to 0.3019 and the k3 Love number to 0.093
180    pub fn earth_moon_system(
181        mut earth_frame: Frame,
182        mut moon_frame: Frame,
183        almanac: &Almanac,
184    ) -> Result<Self, DynamicsError> {
185        let mut sun_j2k = almanac
186            .frame_info(SUN_J2000)
187            .context(DynamicsPlanetarySnafu {
188                action: "fetching sun frame",
189            })?;
190
191        // Repeat for the Earth and Moon
192        for frame in [&mut earth_frame, &mut moon_frame, &mut sun_j2k] {
193            if frame.mu_km3_s2.is_none() {
194                *frame = almanac.frame_info(*frame).context(DynamicsPlanetarySnafu {
195                    action: "fetching sun frame",
196                })?;
197            }
198
199            // Ensure the gravitational parameter is set.
200            frame
201                .mu_km3_s2()
202                .context(AstroPhysicsSnafu)
203                .context(DynamicsAstroSnafu)?;
204
205            // Ensure the equatorial radius is set.
206            frame
207                .mean_equatorial_radius_km()
208                .context(AstroPhysicsSnafu)
209                .context(DynamicsAstroSnafu)?;
210        }
211
212        let me = Self::builder()
213            .k2(0.3019)
214            .k3(0.093)
215            .frame(earth_frame)
216            .perturbers(vec![
217                TidalPerturber::builder()
218                    .frame(moon_frame)
219                    .compute_degree_3(true)
220                    .build(),
221                TidalPerturber::builder()
222                    .frame(sun_j2k)
223                    .compute_degree_3(false)
224                    .build(),
225            ])
226            .build();
227
228        Ok(me)
229    }
230
231    /// Internal helper to compute tidal delta coefficients
232    fn accumulate_deltas(
233        &self,
234        epoch: Epoch,
235        almanac: &Almanac,
236    ) -> Result<([[f64; 4]; 4], [[f64; 4]; 4]), DynamicsError> {
237        let mut delta_c = [[0.0f64; 4]; 4];
238        let mut delta_s = [[0.0f64; 4]; 4];
239
240        for pert in &self.perturbers {
241            pert.compute_pert(epoch, self, almanac, &mut delta_c, &mut delta_s)?;
242        }
243
244        Ok((delta_c, delta_s))
245    }
246}
247
248#[allow(clippy::needless_range_loop)]
249impl AccelModel for SolidTides {
250    fn eom(&self, osc: &Orbit, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
251        let (delta_c, delta_s) = self.accumulate_deltas(osc.epoch, almanac)?;
252
253        // Convert the osculating orbit to the correct frame (needed for multiple harmonic fields)
254        let state = almanac
255            .transform_to(*osc, self.frame, None)
256            .context(DynamicsAlmanacSnafu {
257                action: "transforming into solid tides frame",
258            })?;
259
260        // Using the GMAT notation, with extra character for ease of highlight
261        let r_ = state.rmag_km();
262        let s_ = state.radius_km.x / r_;
263        let t_ = state.radius_km.y / r_;
264        let u_ = state.radius_km.z / r_;
265
266        // Associated Legendre polynomials a_nm (scaled as in sph_harmonics.rs)
267        let mut a_nm = [[0.0f64; 6]; 6];
268        a_nm[0][0] = 1.0;
269        for n in 1..=4 {
270            a_nm[n][n] = (1.0 + 1.0 / (2.0 * n as f64)).sqrt() * a_nm[n - 1][n - 1];
271        }
272        a_nm[1][0] = u_ * 3.0f64.sqrt();
273        for n in 1..=4 {
274            a_nm[n + 1][n] = (2.0 * n as f64 + 3.0).sqrt() * u_ * a_nm[n][n];
275        }
276
277        let b_nm = |n: usize, m: usize| {
278            (((2.0 * n as f64 + 1.0) * (2.0 * n as f64 - 1.0))
279                / ((n as f64 + m as f64) * (n as f64 - m as f64)))
280                .sqrt()
281        };
282        let c_nm = |n: usize, m: usize| {
283            (((2.0 * n as f64 + 1.0) * (n as f64 + m as f64 - 1.0) * (n as f64 - m as f64 - 1.0))
284                / ((n as f64 - m as f64) * (n as f64 + m as f64) * (2.0 * n as f64 - 3.0)))
285                .sqrt()
286        };
287
288        for m in 0..=3 {
289            for n in (m + 2)..=4 {
290                a_nm[n][m] = u_ * b_nm(n, m) * a_nm[n - 1][m] - c_nm(n, m) * a_nm[n - 2][m];
291            }
292        }
293
294        let mut r_m = [0.0f64; 4];
295        let mut i_m = [0.0f64; 4];
296        r_m[0] = 1.0;
297        i_m[0] = 0.0;
298        for m in 1..=3 {
299            r_m[m] = s_ * r_m[m - 1] - t_ * i_m[m - 1];
300            i_m[m] = s_ * i_m[m - 1] + t_ * r_m[m - 1];
301        }
302
303        let eq_radius_km = self
304            .frame
305            .mean_equatorial_radius_km()
306            .context(AstroPhysicsSnafu)
307            .context(DynamicsAstroSnafu)?;
308
309        let mu_km3_s2 = self
310            .frame
311            .mu_km3_s2()
312            .context(AstroPhysicsSnafu)
313            .context(DynamicsAstroSnafu)?;
314
315        let rho = eq_radius_km / r_;
316
317        let mut rho_np1 = mu_km3_s2 / r_ * rho;
318        let mut accel4 = Vector4::zeros();
319
320        let vr01 = |n: usize, m: usize| {
321            let mut val = ((n as f64 - m as f64) * (n as f64 + m as f64 + 1.0)).sqrt();
322            if m == 0 {
323                val /= 2.0f64.sqrt();
324            }
325            val
326        };
327        let vr11 = |n: usize, m: usize| {
328            let mut val = (((2.0 * n as f64 + 1.0)
329                * (n as f64 + m as f64 + 2.0)
330                * (n as f64 + m as f64 + 1.0))
331                / (2.0 * n as f64 + 3.0))
332                .sqrt();
333            if m == 0 {
334                val /= 2.0f64.sqrt();
335            }
336            val
337        };
338
339        let sqrt2 = 2.0f64.sqrt();
340
341        for n in 1..=3 {
342            rho_np1 *= rho;
343            if n < 2 {
344                continue;
345            } // only degree 2 and 3
346
347            let mut sum = Vector4::zeros();
348            for m in 0..=n {
349                let c_val = delta_c[n][m];
350                let s_val = delta_s[n][m];
351
352                let d_ = (c_val * r_m[m] + s_val * i_m[m]) * sqrt2;
353                let e_ = if m == 0 {
354                    0.0
355                } else {
356                    (c_val * r_m[m - 1] + s_val * i_m[m - 1]) * sqrt2
357                };
358                let f_ = if m == 0 {
359                    0.0
360                } else {
361                    (s_val * r_m[m - 1] - c_val * i_m[m - 1]) * sqrt2
362                };
363
364                sum.x += (m as f64) * a_nm[n][m] * e_;
365                sum.y += (m as f64) * a_nm[n][m] * f_;
366                sum.z += vr01(n, m) * a_nm[n][m + 1] * d_;
367                sum.w -= vr11(n, m) * a_nm[n + 1][m + 1] * d_;
368            }
369            accel4 += (rho_np1 / eq_radius_km) * sum;
370        }
371
372        let accel_ecef = Vector3::new(
373            accel4.x + accel4.w * s_,
374            accel4.y + accel4.w * t_,
375            accel4.z + accel4.w * u_,
376        );
377
378        let dcm = almanac
379            .rotate(self.frame, osc.frame, osc.epoch)
380            .context(OrientationSnafu {
381                action: "rotating accel back to integration frame",
382            })
383            .context(DynamicsAlmanacSnafu {
384                action: "rotating accel back to integration frame",
385            })?
386            .rot_mat;
387
388        Ok(dcm * accel_ecef)
389    }
390
391    fn gradient(
392        &self,
393        osc: &Orbit,
394        almanac: &Almanac,
395    ) -> Result<(Vector3<f64>, Matrix3<f64>), DynamicsError> {
396        let (delta_c, delta_s) = self.accumulate_deltas(osc.epoch, almanac)?;
397
398        // Convert the osculating orbit to the correct frame (needed for multiple harmonic fields)
399        let state = almanac
400            .transform_to(*osc, self.frame, None)
401            .context(DynamicsAlmanacSnafu {
402                action: "transforming into gravity field frame",
403            })?;
404
405        let radius: Vector3<OHyperdual<f64, U7>> = hyperspace_from_vector(&state.radius_km);
406
407        // Using the GMAT notation, with extra character for ease of highlight
408        let r_ = norm(&radius);
409        let s_ = radius[0] / r_;
410        let t_ = radius[1] / r_;
411        let u_ = radius[2] / r_;
412
413        // Legendre polynomials recursion in Hyperdual
414        let mut a_nm = [[OHyperdual::<f64, U7>::from(0.0); 6]; 6];
415        a_nm[0][0] = OHyperdual::from(1.0);
416        for n in 1..=4 {
417            a_nm[n][n] =
418                OHyperdual::from((1.0 + 1.0 / (2.0 * n as f64)).sqrt()) * a_nm[n - 1][n - 1];
419        }
420        a_nm[1][0] = u_ * OHyperdual::from(3.0f64.sqrt());
421        for n in 1..=4 {
422            a_nm[n + 1][n] = OHyperdual::from((2.0 * n as f64 + 3.0).sqrt()) * u_ * a_nm[n][n];
423        }
424
425        let b_nm = |n: usize, m: usize| {
426            (((2.0 * n as f64 + 1.0) * (2.0 * n as f64 - 1.0))
427                / ((n as f64 + m as f64) * (n as f64 - m as f64)))
428                .sqrt()
429        };
430        let c_nm = |n: usize, m: usize| {
431            (((2.0 * n as f64 + 1.0) * (n as f64 + m as f64 - 1.0) * (n as f64 - m as f64 - 1.0))
432                / ((n as f64 - m as f64) * (n as f64 + m as f64) * (2.0 * n as f64 - 3.0)))
433                .sqrt()
434        };
435
436        for m in 0..=3 {
437            for n in (m + 2)..=4 {
438                a_nm[n][m] = u_ * OHyperdual::from(b_nm(n, m)) * a_nm[n - 1][m]
439                    - OHyperdual::from(c_nm(n, m)) * a_nm[n - 2][m];
440            }
441        }
442
443        let mut r_m = [OHyperdual::<f64, U7>::from(0.0); 4];
444        let mut i_m = [OHyperdual::<f64, U7>::from(0.0); 4];
445        r_m[0] = OHyperdual::from(1.0);
446        i_m[0] = OHyperdual::from(0.0);
447        for m in 1..=3 {
448            r_m[m] = s_ * r_m[m - 1] - t_ * i_m[m - 1];
449            i_m[m] = s_ * i_m[m - 1] + t_ * r_m[m - 1];
450        }
451
452        let real_eq_radius_km = self
453            .frame
454            .mean_equatorial_radius_km()
455            .context(AstroPhysicsSnafu)
456            .context(DynamicsAstroSnafu)?;
457
458        let real_mu_km3_s2 = self
459            .frame
460            .mu_km3_s2()
461            .context(AstroPhysicsSnafu)
462            .context(DynamicsAstroSnafu)?;
463
464        let eq_radius = OHyperdual::<f64, U7>::from(real_eq_radius_km);
465        let rho = eq_radius / r_;
466        let mut rho_np1 = OHyperdual::<f64, U7>::from(real_mu_km3_s2) / r_ * rho;
467
468        let mut a0 = OHyperdual::<f64, U7>::from(0.0);
469        let mut a1 = OHyperdual::<f64, U7>::from(0.0);
470        let mut a2 = OHyperdual::<f64, U7>::from(0.0);
471        let mut a3 = OHyperdual::<f64, U7>::from(0.0);
472
473        let vr01 = |n: usize, m: usize| {
474            let mut val = ((n as f64 - m as f64) * (n as f64 + m as f64 + 1.0)).sqrt();
475            if m == 0 {
476                val /= 2.0f64.sqrt();
477            }
478            val
479        };
480        let vr11 = |n: usize, m: usize| {
481            let mut val = (((2.0 * n as f64 + 1.0)
482                * (n as f64 + m as f64 + 2.0)
483                * (n as f64 + m as f64 + 1.0))
484                / (2.0 * n as f64 + 3.0))
485                .sqrt();
486            if m == 0 {
487                val /= 2.0f64.sqrt();
488            }
489            val
490        };
491        let sqrt2 = OHyperdual::<f64, U7>::from(2.0f64.sqrt());
492
493        for n in 1..=3 {
494            rho_np1 *= rho;
495            if n < 2 {
496                continue;
497            }
498
499            let mut sum0 = OHyperdual::from(0.0);
500            let mut sum1 = OHyperdual::from(0.0);
501            let mut sum2 = OHyperdual::from(0.0);
502            let mut sum3 = OHyperdual::from(0.0);
503
504            for m in 0..=n {
505                let c_val = OHyperdual::from(delta_c[n][m]);
506                let s_val = OHyperdual::from(delta_s[n][m]);
507
508                let d_ = (c_val * r_m[m] + s_val * i_m[m]) * sqrt2;
509                let e_ = if m == 0 {
510                    OHyperdual::from(0.0)
511                } else {
512                    (c_val * r_m[m - 1] + s_val * i_m[m - 1]) * sqrt2
513                };
514                let f_ = if m == 0 {
515                    OHyperdual::from(0.0)
516                } else {
517                    (s_val * r_m[m - 1] - c_val * i_m[m - 1]) * sqrt2
518                };
519
520                sum0 += OHyperdual::from(m as f64) * a_nm[n][m] * e_;
521                sum1 += OHyperdual::from(m as f64) * a_nm[n][m] * f_;
522                sum2 += OHyperdual::from(vr01(n, m)) * a_nm[n][m + 1] * d_;
523                sum3 += OHyperdual::from(vr11(n, m)) * a_nm[n + 1][m + 1] * d_;
524            }
525            let rr = rho_np1 / eq_radius;
526            a0 += rr * sum0;
527            a1 += rr * sum1;
528            a2 += rr * sum2;
529            a3 -= rr * sum3;
530        }
531
532        let accel_local = Vector3::new(a0 + a3 * s_, a1 + a3 * t_, a2 + a3 * u_);
533
534        let dcm = almanac
535            .rotate(self.frame, osc.frame, osc.epoch)
536            .context(OrientationSnafu {
537                action: "rotating accel back to integration frame",
538            })
539            .context(DynamicsAlmanacSnafu {
540                action: "rotating accel back to integration frame",
541            })?
542            .rot_mat;
543
544        let dx = dcm
545            * Vector3::new(
546                accel_local[0].real(),
547                accel_local[1].real(),
548                accel_local[2].real(),
549            );
550
551        let mut grad_local = Matrix3::zeros();
552        for i in 0..3 {
553            for j in 1..4 {
554                grad_local[(i, j - 1)] += accel_local[i][j];
555            }
556        }
557        let grad = dcm * grad_local * dcm.transpose();
558        Ok((dx, grad))
559    }
560}
561
562impl fmt::Display for SolidTides {
563    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
564        write!(
565            f,
566            "Solid tides for {} with {} (k2={:.6}, k3={:.6})",
567            self.frame,
568            self.perturbers
569                .iter()
570                .map(|pert| pert.to_string())
571                .collect::<Vec<String>>()
572                .join(", "),
573            self.k2,
574            self.k3
575        )
576    }
577}
578
579impl fmt::Display for TidalPerturber {
580    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
581        write!(
582            f,
583            "TidalPerturber of {} (3rd deg= {})",
584            self.frame, self.compute_degree_3
585        )
586    }
587}
588
589#[cfg(feature = "python")]
590#[cfg_attr(feature = "python", pymethods)]
591impl SolidTides {
592    #[new]
593    fn py_new(frame: Frame, k2: f64, k3: f64, perturbers: Vec<TidalPerturber>) -> Self {
594        Self {
595            frame,
596            k2,
597            k3,
598            perturbers,
599        }
600    }
601
602    #[classmethod]
603    #[pyo3(name = "earth_moon_system")]
604    fn py_earth_moon_system(
605        _cls: &Bound<'_, PyType>,
606        earth_frame: Frame,
607        moon_frame: Frame,
608        almanac: &Almanac,
609    ) -> Result<Self, DynamicsError> {
610        Self::earth_moon_system(earth_frame, moon_frame, almanac)
611    }
612
613    fn __str__(&self) -> String {
614        format!("{self}")
615    }
616
617    fn __repr__(&self) -> String {
618        format!("{self} @ {self:p}")
619    }
620
621    fn __eq__(&self, other: &Self) -> bool {
622        self == other
623    }
624}
625
626#[cfg(feature = "python")]
627#[cfg_attr(feature = "python", pymethods)]
628impl TidalPerturber {
629    #[new]
630    fn py_new(frame: Frame, compute_degree_3: bool) -> Self {
631        Self {
632            frame,
633            compute_degree_3,
634        }
635    }
636
637    fn __str__(&self) -> String {
638        format!("{self}")
639    }
640
641    fn __repr__(&self) -> String {
642        format!("{self} @ {self:p}")
643    }
644
645    fn __eq__(&self, other: &Self) -> bool {
646        self == other
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653    use crate::cosmic::Orbit;
654    use anise::constants::frames::{EARTH_J2000, IAU_EARTH_FRAME, IAU_MOON_FRAME};
655    use std::path::PathBuf;
656    use std::str::FromStr;
657
658    #[test]
659    fn test_solid_tides_earth() {
660        let data_folder: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../data/01_planetary"]
661            .iter()
662            .collect();
663        let mut almanac = Almanac::default();
664        // Load kernels
665        almanac = almanac
666            .load(data_folder.join("de440s.bsp").to_str().unwrap())
667            .unwrap();
668        almanac = almanac
669            .load(data_folder.join("pck08.pca").to_str().unwrap())
670            .unwrap();
671
672        let epoch = Epoch::from_str("2024-01-01T12:00:00 UTC").unwrap();
673        let sc_orbit = Orbit::cartesian(7000.0, 0.0, 0.0, 0.0, 7.5, 0.0, epoch, EARTH_J2000);
674
675        let tides =
676            SolidTides::earth_moon_system(IAU_EARTH_FRAME, IAU_MOON_FRAME, &almanac.clone())
677                .expect("could not init solid tides");
678        let acc = tides.eom(&sc_orbit, &almanac).unwrap();
679
680        println!("Solid tides acceleration: {:?}", acc);
681        // Typical solid tide acceleration for LEO is around 1e-9 to 1e-7 km/s^2
682        assert!(acc.norm() > 0.0);
683        assert!(acc.norm() < 1e-6);
684
685        let (acc_grad, grad) = tides.gradient(&sc_orbit, &almanac).unwrap();
686        assert!((acc - acc_grad).norm() < 1e-12);
687        assert!(grad.norm() > 0.0);
688    }
689}