1use 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 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 a_nm[(0, 0)] = 1.0;
62 for n in 1..=degree_np2 {
63 let nf64 = n as f64;
64 a_nm[(n, n)] = (1.0 + 1.0 / (2.0 * nf64)).sqrt() * a_nm[(n - 1, n - 1)];
66 }
67
68 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 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 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 a_nm_h[(0, 0)] = OHyperdual::from(1.0);
104 for n in 1..=degree_np2 {
105 a_nm_h[(n, n)] = OHyperdual::from(a_nm[(n, n)]);
107 }
108
109 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 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 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(); let max_order = self.grav_data.max_order_m(); let mut a_nm = self.a_nm.clone();
166
167 a_nm[(1, 0)] = u_ * 3.0f64.sqrt();
169 for n in 1..=max_degree + 1 {
170 let nf64 = n as f64;
171 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 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
196 .grav_data
197 .frame
198 .mean_equatorial_radius_km()
199 .context(AstroPhysicsSnafu)
200 .context(DynamicsAstroSnafu)?;
201
202 let mu_km3_s2 = self
203 .grav_data
204 .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.grav_data.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 let dcm = almanac
259 .rotate(self.grav_data.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 fn gradient(
274 &self,
275 osc: &Orbit,
276 almanac: &Almanac,
277 ) -> Result<(Vector3<f64>, Matrix3<f64>), DynamicsError> {
278 let state = almanac
280 .transform_to(*osc, self.grav_data.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 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.grav_data.max_degree_n(); let max_order = self.grav_data.max_order_m(); let mut a_nm = DMatrix::from_element(max_degree + 3, max_degree + 3, OHyperdual::from(0.0));
297 for i in 0..=max_degree + 1 {
299 a_nm[(i, i)] = self.a_nm_h[(i, i)];
300 }
301
302 a_nm[(1, 0)] = u_ * 3.0f64.sqrt();
304 for n in 1..=max_degree + 1 {
305 let nf64 = n as f64;
306 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 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 .grav_data
332 .frame
333 .mean_equatorial_radius_km()
334 .context(AstroPhysicsSnafu)
335 .context(DynamicsAstroSnafu)?;
336
337 let real_mu_km3_s2 = self
338 .grav_data
339 .frame
340 .mu_km3_s2()
341 .context(AstroPhysicsSnafu)
342 .context(DynamicsAstroSnafu)?;
343
344 let eq_radius = OHyperdual::<f64, U7>::from(real_eq_radius_km);
345 let rho = eq_radius / r_;
346 let mut rho_np1 = OHyperdual::<f64, U7>::from(real_mu_km3_s2) / r_ * rho;
347
348 let mut a0 = OHyperdual::<f64, U7>::from(0.0);
349 let mut a1 = OHyperdual::<f64, U7>::from(0.0);
350 let mut a2 = OHyperdual::<f64, U7>::from(0.0);
351 let mut a3 = OHyperdual::<f64, U7>::from(0.0);
352 let sqrt2 = OHyperdual::<f64, U7>::from(2.0.sqrt());
353
354 for n in 1..=max_degree {
355 let mut sum0 = OHyperdual::from(0.0);
356 let mut sum1 = OHyperdual::from(0.0);
357 let mut sum2 = OHyperdual::from(0.0);
358 let mut sum3 = OHyperdual::from(0.0);
359 rho_np1 *= rho;
360
361 for m in 0..=min(n, max_order) {
362 let (c_valf64, s_valf64) = self.grav_data.cs_nm(n, m);
363 let c_val = OHyperdual::<f64, U7>::from(c_valf64);
364 let s_val = OHyperdual::<f64, U7>::from(s_valf64);
365
366 let d_ = unsafe {
367 (c_val * r_m.get_unchecked(m) + s_val * i_m.get_unchecked(m)) * sqrt2
368 };
369 let e_ = if m == 0 {
370 OHyperdual::from(0.0)
371 } else {
372 unsafe {
373 (c_val * r_m.get_unchecked(m - 1) + s_val * i_m.get_unchecked(m - 1))
374 * sqrt2
375 }
376 };
377 let f_ = if m == 0 {
378 OHyperdual::from(0.0)
379 } else {
380 unsafe {
381 (s_val * r_m.get_unchecked(m - 1) - c_val * i_m.get_unchecked(m - 1))
382 * sqrt2
383 }
384 };
385
386 unsafe {
387 sum0 += OHyperdual::from(m as f64) * a_nm.get_unchecked((n, m)) * e_;
388 sum1 += OHyperdual::from(m as f64) * a_nm.get_unchecked((n, m)) * f_;
389 sum2 +=
390 *self.vr01_h.get_unchecked((n, m)) * *a_nm.get_unchecked((n, m + 1)) * d_;
391 sum3 += *self.vr11_h.get_unchecked((n, m))
392 * *a_nm.get_unchecked((n + 1, m + 1))
393 * d_;
394 }
395 }
396 let rr = rho_np1 / eq_radius;
397 a0 += rr * sum0;
398 a1 += rr * sum1;
399 a2 += rr * sum2;
400 a3 -= rr * sum3;
401 }
402
403 let dcm = almanac
404 .rotate(self.grav_data.frame, osc.frame, osc.epoch)
405 .context(OrientationSnafu {
406 action: "transform state dcm",
407 })
408 .context(DynamicsAlmanacSnafu {
409 action: "transforming into gravity field frame",
410 })?
411 .rot_mat;
412
413 let accel_local = Vector3::new(a0 + a3 * s_, a1 + a3 * t_, a2 + a3 * u_);
414 let dx = dcm
415 * Vector3::new(
416 accel_local[0].real(),
417 accel_local[1].real(),
418 accel_local[2].real(),
419 );
420
421 let mut grad_local = Matrix3::zeros();
422 for i in 0..3 {
423 for j in 1..4 {
425 grad_local[(i, j - 1)] += accel_local[i][j];
427 }
428 }
429 let grad = dcm * grad_local * dcm.transpose();
430 Ok((dx, grad))
431 }
432}