Skip to main content

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