Skip to main content

nyx_space/dynamics/
gravity_field.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::errors::OrientationSnafu;
20use anise::prelude::Almanac;
21use snafu::ResultExt;
22
23use crate::cosmic::{AstroPhysicsSnafu, Orbit};
24use crate::dynamics::AccelModel;
25use crate::io::gravity::GravityFieldData;
26use crate::linalg::{DMatrix, Matrix3, U7, Vector3, Vector4};
27use hyperdual::linalg::norm;
28use hyperdual::{Float, OHyperdual, hyperspace_from_vector};
29use std::cmp::min;
30use std::fmt;
31use std::sync::Arc;
32
33use super::{DynamicsAlmanacSnafu, DynamicsAstroSnafu, DynamicsError};
34
35#[derive(Clone)]
36pub struct GravityField {
37    grav_data: GravityFieldData,
38    a_nm: DMatrix<f64>,
39    b_nm: DMatrix<f64>,
40    c_nm: DMatrix<f64>,
41    vr01: DMatrix<f64>,
42    vr11: DMatrix<f64>,
43    a_nm_h: DMatrix<OHyperdual<f64, U7>>,
44    b_nm_h: DMatrix<OHyperdual<f64, U7>>,
45    c_nm_h: DMatrix<OHyperdual<f64, U7>>,
46    vr01_h: DMatrix<OHyperdual<f64, U7>>,
47    vr11_h: DMatrix<OHyperdual<f64, U7>>,
48}
49
50impl GravityField {
51    /// Create a new Harmonics dynamical model from the provided gravity potential storage instance.
52    pub fn new(stor: GravityFieldData) -> Arc<Self> {
53        let degree_np2 = stor.max_degree_n() + 2;
54        let mut a_nm = DMatrix::from_element(degree_np2 + 1, degree_np2 + 1, 0.0);
55        let mut b_nm = DMatrix::from_element(degree_np2, degree_np2, 0.0);
56        let mut c_nm = DMatrix::from_element(degree_np2, degree_np2, 0.0);
57        let mut vr01 = DMatrix::from_element(degree_np2, degree_np2, 0.0);
58        let mut vr11 = DMatrix::from_element(degree_np2, degree_np2, 0.0);
59
60        // Initialize the diagonal elements (not a function of the input)
61        a_nm[(0, 0)] = 1.0;
62        for n in 1..=degree_np2 {
63            let nf64 = n as f64;
64            // Diagonal element
65            a_nm[(n, n)] = (1.0 + 1.0 / (2.0 * nf64)).sqrt() * a_nm[(n - 1, n - 1)];
66        }
67
68        // Pre-compute the B_nm, C_nm, vr01 and vr11 storages
69        for n in 0..degree_np2 {
70            for m in 0..degree_np2 {
71                let nf64 = n as f64;
72                let mf64 = m as f64;
73                // Compute c_nm, which is B_nm/B_(n-1,m) in Jones' dissertation
74                c_nm[(n, m)] = (((2.0 * nf64 + 1.0) * (nf64 + mf64 - 1.0) * (nf64 - mf64 - 1.0))
75                    / ((nf64 - mf64) * (nf64 + mf64) * (2.0 * nf64 - 3.0)))
76                    .sqrt();
77
78                b_nm[(n, m)] = (((2.0 * nf64 + 1.0) * (2.0 * nf64 - 1.0))
79                    / ((nf64 + mf64) * (nf64 - mf64)))
80                    .sqrt();
81
82                vr01[(n, m)] = ((nf64 - mf64) * (nf64 + mf64 + 1.0)).sqrt();
83                vr11[(n, m)] = (((2.0 * nf64 + 1.0) * (nf64 + mf64 + 2.0) * (nf64 + mf64 + 1.0))
84                    / (2.0 * nf64 + 3.0))
85                    .sqrt();
86
87                if m == 0 {
88                    vr01[(n, m)] /= 2.0_f64.sqrt();
89                    vr11[(n, m)] /= 2.0_f64.sqrt();
90                }
91            }
92        }
93
94        // Repeat for the hyperdual part in case we need to super the partials
95        let mut a_nm_h =
96            DMatrix::from_element(degree_np2 + 1, degree_np2 + 1, OHyperdual::from(0.0));
97        let mut b_nm_h = DMatrix::from_element(degree_np2, degree_np2, OHyperdual::from(0.0));
98        let mut c_nm_h = DMatrix::from_element(degree_np2, degree_np2, OHyperdual::from(0.0));
99        let mut vr01_h = DMatrix::from_element(degree_np2, degree_np2, OHyperdual::from(0.0));
100        let mut vr11_h = DMatrix::from_element(degree_np2, degree_np2, OHyperdual::from(0.0));
101
102        // initialize the diagonal elements (not a function of the input)
103        a_nm_h[(0, 0)] = OHyperdual::from(1.0);
104        for n in 1..=degree_np2 {
105            // Diagonal element
106            a_nm_h[(n, n)] = OHyperdual::from(a_nm[(n, n)]);
107        }
108
109        // Pre-compute the B_nm, C_nm, vr01 and vr11 storages
110        for n in 0..degree_np2 {
111            for m in 0..degree_np2 {
112                vr01_h[(n, m)] = OHyperdual::from(vr01[(n, m)]);
113                vr11_h[(n, m)] = OHyperdual::from(vr11[(n, m)]);
114                b_nm_h[(n, m)] = OHyperdual::from(b_nm[(n, m)]);
115                c_nm_h[(n, m)] = OHyperdual::from(c_nm[(n, m)]);
116            }
117        }
118
119        Arc::new(Self {
120            grav_data: stor,
121            a_nm,
122            b_nm,
123            c_nm,
124            vr01,
125            vr11,
126            a_nm_h,
127            b_nm_h,
128            c_nm_h,
129            vr01_h,
130            vr11_h,
131        })
132    }
133}
134
135impl fmt::Display for GravityField {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        write!(
138            f,
139            "{} gravity field {}x{} (order x degree)",
140            self.grav_data.frame,
141            self.grav_data.max_order_m(),
142            self.grav_data.max_degree_n(),
143        )
144    }
145}
146
147impl AccelModel for GravityField {
148    fn eom(&self, osc: &Orbit, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
149        // Convert the osculating orbit to the correct frame (needed for multiple harmonic fields)
150        let state = almanac
151            .transform_to(*osc, self.grav_data.frame, None)
152            .context(DynamicsAlmanacSnafu {
153                action: "transforming into gravity field frame",
154            })?;
155
156        // Using the GMAT notation, with extra character for ease of highlight
157        let r_ = state.rmag_km();
158        let s_ = state.radius_km.x / r_;
159        let t_ = state.radius_km.y / r_;
160        let u_ = state.radius_km.z / r_;
161        let max_degree = self.grav_data.max_degree_n(); // In GMAT, the degree is NN
162        let max_order = self.grav_data.max_order_m(); // In GMAT, the order is MM
163
164        // Create the associated Legendre polynomials. Note that we add three items as per GMAT (this may be useful for the STM)
165        let mut a_nm = self.a_nm.clone();
166
167        // Initialize the diagonal elements (not a function of the input)
168        a_nm[(1, 0)] = u_ * 3.0f64.sqrt();
169        for n in 1..=max_degree + 1 {
170            let nf64 = n as f64;
171            // Off diagonal
172            a_nm[(n + 1, n)] = (2.0 * nf64 + 3.0).sqrt() * u_ * a_nm[(n, n)];
173        }
174
175        for m in 0..=max_order + 1 {
176            for n in (m + 2)..=max_degree + 1 {
177                let hm_idx = (n, m);
178                a_nm[hm_idx] = u_ * self.b_nm[hm_idx] * a_nm[(n - 1, m)]
179                    - self.c_nm[hm_idx] * a_nm[(n - 2, m)];
180            }
181        }
182
183        // Generate r_m and i_m
184        let mut r_m = Vec::with_capacity(min(max_degree, max_order) + 1);
185        let mut i_m = Vec::with_capacity(min(max_degree, max_order) + 1);
186
187        r_m.push(1.0);
188        i_m.push(0.0);
189
190        for m in 1..=min(max_degree, max_order) {
191            r_m.push(s_ * r_m[m - 1] - t_ * i_m[m - 1]);
192            i_m.push(s_ * i_m[m - 1] + t_ * r_m[m - 1]);
193        }
194
195        let eq_radius_km = self.grav_data.radius_km.unwrap_or(
196            self.grav_data
197                .frame
198                .mean_equatorial_radius_km()
199                .context(AstroPhysicsSnafu)
200                .context(DynamicsAstroSnafu)?,
201        );
202
203        let mu_km3_s2 = self.grav_data.mu_km3_s2.unwrap_or(
204            self.grav_data
205                .frame
206                .mu_km3_s2()
207                .context(AstroPhysicsSnafu)
208                .context(DynamicsAstroSnafu)?,
209        );
210
211        let rho = eq_radius_km / r_;
212        let mut rho_np1 = mu_km3_s2 / r_ * rho;
213        let mut accel4: Vector4<f64> = Vector4::zeros();
214
215        for n in 1..=max_degree {
216            let mut sum: Vector4<f64> = Vector4::zeros();
217            rho_np1 *= rho;
218
219            for m in 0..=min(n, max_order) {
220                let (c_val, s_val) = self.grav_data.cs_nm(n, m);
221                let d_ = unsafe {
222                    (c_val * r_m.get_unchecked(m) + s_val * i_m.get_unchecked(m)) * 2.0.sqrt()
223                };
224                let e_ = if m == 0 {
225                    0.0
226                } else {
227                    unsafe {
228                        (c_val * r_m.get_unchecked(m - 1) + s_val * i_m.get_unchecked(m - 1))
229                            * 2.0.sqrt()
230                    }
231                };
232                let f_ = if m == 0 {
233                    0.0
234                } else {
235                    unsafe {
236                        (s_val * r_m.get_unchecked(m - 1) - c_val * i_m.get_unchecked(m - 1))
237                            * 2.0.sqrt()
238                    }
239                };
240
241                unsafe {
242                    sum.x += (m as f64) * a_nm.get_unchecked((n, m)) * e_;
243                    sum.y += (m as f64) * a_nm.get_unchecked((n, m)) * f_;
244                    sum.z += self.vr01.get_unchecked((n, m)) * a_nm.get_unchecked((n, m + 1)) * d_;
245                    sum.w -=
246                        self.vr11.get_unchecked((n, m)) * a_nm.get_unchecked((n + 1, m + 1)) * d_;
247                }
248            }
249            let rr = rho_np1 / eq_radius_km;
250            accel4 += rr * sum;
251        }
252        let accel = Vector3::new(
253            accel4.x + accel4.w * s_,
254            accel4.y + accel4.w * t_,
255            accel4.z + accel4.w * u_,
256        );
257        // Rotate this acceleration vector back into the integration frame (no center change needed, it's just a vector)
258        // As discussed with Sai, if the Earth was spinning faster, would the acceleration due to the harmonics be any different?
259        // No. Therefore, we do not need to account for the transport theorem here.
260        let dcm = almanac
261            .rotate(self.grav_data.frame, osc.frame, osc.epoch)
262            .context(OrientationSnafu {
263                action: "transform state dcm",
264            })
265            .context(DynamicsAlmanacSnafu {
266                action: "transforming into gravity field frame",
267            })?;
268
269        Ok(dcm.rot_mat * accel)
270    }
271
272    /// The gradient computation of the gravity field skip bound checks on the matrix fetching via the get_unchecked.
273    /// This approach allows the gradient to be calculated to full machine precision while, surprisingly, being slightly
274    /// faster than a single forward different gradient approach.
275    fn gradient(
276        &self,
277        osc: &Orbit,
278        almanac: &Almanac,
279    ) -> Result<(Vector3<f64>, Matrix3<f64>), DynamicsError> {
280        // Convert the osculating orbit to the correct frame (needed for multiple harmonic fields)
281        let state = almanac
282            .transform_to(*osc, self.grav_data.frame, None)
283            .context(DynamicsAlmanacSnafu {
284                action: "transforming into gravity field frame",
285            })?;
286
287        let radius: Vector3<OHyperdual<f64, U7>> = hyperspace_from_vector(&state.radius_km);
288
289        // Using the GMAT notation, with extra character for ease of highlight
290        let r_ = norm(&radius);
291        let s_ = radius[0] / r_;
292        let t_ = radius[1] / r_;
293        let u_ = radius[2] / r_;
294        let max_degree = self.grav_data.max_degree_n(); // In GMAT, the order is NN
295        let max_order = self.grav_data.max_order_m(); // In GMAT, the order is MM
296
297        // Create the associated Legendre polynomials. Note that we add three items as per GMAT (this may be useful for the STM)
298        let mut a_nm = DMatrix::from_element(max_degree + 3, max_degree + 3, OHyperdual::from(0.0));
299        // Copy only the pre-computed diagonals from self.a_nm_h manually
300        for i in 0..=max_degree + 1 {
301            a_nm[(i, i)] = self.a_nm_h[(i, i)];
302        }
303
304        // Initialize the diagonal elements (not a function of the input)
305        a_nm[(1, 0)] = u_ * 3.0f64.sqrt();
306        for n in 1..=max_degree + 1 {
307            let nf64 = n as f64;
308            // Off diagonal
309            a_nm[(n + 1, n)] = OHyperdual::from((2.0 * nf64 + 3.0).sqrt()) * u_ * a_nm[(n, n)];
310        }
311
312        for m in 0..=max_order + 1 {
313            for n in (m + 2)..=max_degree + 1 {
314                let hm_idx = (n, m);
315                a_nm[hm_idx] = u_ * self.b_nm_h[hm_idx] * a_nm[(n - 1, m)]
316                    - self.c_nm_h[hm_idx] * a_nm[(n - 2, m)];
317            }
318        }
319
320        // Generate r_m and i_m
321        let mut r_m = Vec::with_capacity(min(max_degree, max_order) + 1);
322        let mut i_m = Vec::with_capacity(min(max_degree, max_order) + 1);
323
324        r_m.push(OHyperdual::<f64, U7>::from(1.0));
325        i_m.push(OHyperdual::<f64, U7>::from(0.0));
326
327        for m in 1..=min(max_degree, max_order) {
328            r_m.push(s_ * r_m[m - 1] - t_ * i_m[m - 1]);
329            i_m.push(s_ * i_m[m - 1] + t_ * r_m[m - 1]);
330        }
331
332        let real_eq_radius_km = self.grav_data.radius_km.unwrap_or(
333            self.grav_data
334                .frame
335                .mean_equatorial_radius_km()
336                .context(AstroPhysicsSnafu)
337                .context(DynamicsAstroSnafu)?,
338        );
339
340        let real_mu_km3_s2 = self.grav_data.mu_km3_s2.unwrap_or(
341            self.grav_data
342                .frame
343                .mu_km3_s2()
344                .context(AstroPhysicsSnafu)
345                .context(DynamicsAstroSnafu)?,
346        );
347
348        let eq_radius = OHyperdual::<f64, U7>::from(real_eq_radius_km);
349        let rho = eq_radius / r_;
350        let mut rho_np1 = OHyperdual::<f64, U7>::from(real_mu_km3_s2) / r_ * rho;
351
352        let mut a0 = OHyperdual::<f64, U7>::from(0.0);
353        let mut a1 = OHyperdual::<f64, U7>::from(0.0);
354        let mut a2 = OHyperdual::<f64, U7>::from(0.0);
355        let mut a3 = OHyperdual::<f64, U7>::from(0.0);
356        let sqrt2 = OHyperdual::<f64, U7>::from(2.0.sqrt());
357
358        for n in 1..=max_degree {
359            let mut sum0 = OHyperdual::from(0.0);
360            let mut sum1 = OHyperdual::from(0.0);
361            let mut sum2 = OHyperdual::from(0.0);
362            let mut sum3 = OHyperdual::from(0.0);
363            rho_np1 *= rho;
364
365            for m in 0..=min(n, max_order) {
366                let (c_valf64, s_valf64) = self.grav_data.cs_nm(n, m);
367                let c_val = OHyperdual::<f64, U7>::from(c_valf64);
368                let s_val = OHyperdual::<f64, U7>::from(s_valf64);
369
370                let d_ = unsafe {
371                    (c_val * r_m.get_unchecked(m) + s_val * i_m.get_unchecked(m)) * sqrt2
372                };
373                let e_ = if m == 0 {
374                    OHyperdual::from(0.0)
375                } else {
376                    unsafe {
377                        (c_val * r_m.get_unchecked(m - 1) + s_val * i_m.get_unchecked(m - 1))
378                            * sqrt2
379                    }
380                };
381                let f_ = if m == 0 {
382                    OHyperdual::from(0.0)
383                } else {
384                    unsafe {
385                        (s_val * r_m.get_unchecked(m - 1) - c_val * i_m.get_unchecked(m - 1))
386                            * sqrt2
387                    }
388                };
389
390                unsafe {
391                    sum0 += OHyperdual::from(m as f64) * a_nm.get_unchecked((n, m)) * e_;
392                    sum1 += OHyperdual::from(m as f64) * a_nm.get_unchecked((n, m)) * f_;
393                    sum2 +=
394                        *self.vr01_h.get_unchecked((n, m)) * *a_nm.get_unchecked((n, m + 1)) * d_;
395                    sum3 += *self.vr11_h.get_unchecked((n, m))
396                        * *a_nm.get_unchecked((n + 1, m + 1))
397                        * d_;
398                }
399            }
400            let rr = rho_np1 / eq_radius;
401            a0 += rr * sum0;
402            a1 += rr * sum1;
403            a2 += rr * sum2;
404            a3 -= rr * sum3;
405        }
406
407        let dcm = almanac
408            .rotate(self.grav_data.frame, osc.frame, osc.epoch)
409            .context(OrientationSnafu {
410                action: "transform state dcm",
411            })
412            .context(DynamicsAlmanacSnafu {
413                action: "transforming into gravity field frame",
414            })?
415            .rot_mat;
416
417        let accel_local = Vector3::new(a0 + a3 * s_, a1 + a3 * t_, a2 + a3 * u_);
418        let dx = dcm
419            * Vector3::new(
420                accel_local[0].real(),
421                accel_local[1].real(),
422                accel_local[2].real(),
423            );
424
425        let mut grad_local = Matrix3::zeros();
426        for i in 0..3 {
427            // For each acceleration component
428            for j in 1..4 {
429                // For each derivative wrt to the position
430                grad_local[(i, j - 1)] += accel_local[i][j];
431            }
432        }
433        let grad = dcm * grad_local * dcm.transpose();
434        Ok((dx, grad))
435    }
436}