1use 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, 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 frame: Frame,
39 stor: GravityFieldData,
40 a_nm: DMatrix<f64>,
41 b_nm: DMatrix<f64>,
42 c_nm: DMatrix<f64>,
43 vr01: DMatrix<f64>,
44 vr11: DMatrix<f64>,
45 a_nm_h: DMatrix<OHyperdual<f64, U7>>,
46 b_nm_h: DMatrix<OHyperdual<f64, U7>>,
47 c_nm_h: DMatrix<OHyperdual<f64, U7>>,
48 vr01_h: DMatrix<OHyperdual<f64, U7>>,
49 vr11_h: DMatrix<OHyperdual<f64, U7>>,
50}
51
52impl GravityField {
53 pub fn from_stor(frame: Frame, stor: GravityFieldData) -> Arc<Self> {
55 let degree_np2 = stor.max_degree_n() + 2;
56 let mut a_nm = DMatrix::from_element(degree_np2 + 1, degree_np2 + 1, 0.0);
57 let mut b_nm = DMatrix::from_element(degree_np2, degree_np2, 0.0);
58 let mut c_nm = DMatrix::from_element(degree_np2, degree_np2, 0.0);
59 let mut vr01 = DMatrix::from_element(degree_np2, degree_np2, 0.0);
60 let mut vr11 = DMatrix::from_element(degree_np2, degree_np2, 0.0);
61
62 a_nm[(0, 0)] = 1.0;
64 for n in 1..=degree_np2 {
65 let nf64 = n as f64;
66 a_nm[(n, n)] = (1.0 + 1.0 / (2.0 * nf64)).sqrt() * a_nm[(n - 1, n - 1)];
68 }
69
70 for n in 0..degree_np2 {
72 for m in 0..degree_np2 {
73 let nf64 = n as f64;
74 let mf64 = m as f64;
75 c_nm[(n, m)] = (((2.0 * nf64 + 1.0) * (nf64 + mf64 - 1.0) * (nf64 - mf64 - 1.0))
77 / ((nf64 - mf64) * (nf64 + mf64) * (2.0 * nf64 - 3.0)))
78 .sqrt();
79
80 b_nm[(n, m)] = (((2.0 * nf64 + 1.0) * (2.0 * nf64 - 1.0))
81 / ((nf64 + mf64) * (nf64 - mf64)))
82 .sqrt();
83
84 vr01[(n, m)] = ((nf64 - mf64) * (nf64 + mf64 + 1.0)).sqrt();
85 vr11[(n, m)] = (((2.0 * nf64 + 1.0) * (nf64 + mf64 + 2.0) * (nf64 + mf64 + 1.0))
86 / (2.0 * nf64 + 3.0))
87 .sqrt();
88
89 if m == 0 {
90 vr01[(n, m)] /= 2.0_f64.sqrt();
91 vr11[(n, m)] /= 2.0_f64.sqrt();
92 }
93 }
94 }
95
96 let mut a_nm_h =
98 DMatrix::from_element(degree_np2 + 1, degree_np2 + 1, OHyperdual::from(0.0));
99 let mut b_nm_h = DMatrix::from_element(degree_np2, degree_np2, OHyperdual::from(0.0));
100 let mut c_nm_h = DMatrix::from_element(degree_np2, degree_np2, OHyperdual::from(0.0));
101 let mut vr01_h = DMatrix::from_element(degree_np2, degree_np2, OHyperdual::from(0.0));
102 let mut vr11_h = DMatrix::from_element(degree_np2, degree_np2, OHyperdual::from(0.0));
103
104 a_nm_h[(0, 0)] = OHyperdual::from(1.0);
106 for n in 1..=degree_np2 {
107 a_nm_h[(n, n)] = OHyperdual::from(a_nm[(n, n)]);
109 }
110
111 for n in 0..degree_np2 {
113 for m in 0..degree_np2 {
114 vr01_h[(n, m)] = OHyperdual::from(vr01[(n, m)]);
115 vr11_h[(n, m)] = OHyperdual::from(vr11[(n, m)]);
116 b_nm_h[(n, m)] = OHyperdual::from(b_nm[(n, m)]);
117 c_nm_h[(n, m)] = OHyperdual::from(c_nm[(n, m)]);
118 }
119 }
120
121 Arc::new(Self {
122 frame,
123 stor,
124 a_nm,
125 b_nm,
126 c_nm,
127 vr01,
128 vr11,
129 a_nm_h,
130 b_nm_h,
131 c_nm_h,
132 vr01_h,
133 vr11_h,
134 })
135 }
136}
137
138impl fmt::Display for GravityField {
139 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
140 write!(
141 f,
142 "{} gravity field {}x{} (order x degree)",
143 self.frame,
144 self.stor.max_order_m(),
145 self.stor.max_degree_n(),
146 )
147 }
148}
149
150impl AccelModel for GravityField {
151 fn eom(&self, osc: &Orbit, almanac: Arc<Almanac>) -> Result<Vector3<f64>, DynamicsError> {
152 let state = almanac
154 .transform_to(*osc, self.frame, None)
155 .context(DynamicsAlmanacSnafu {
156 action: "transforming into gravity field frame",
157 })?;
158
159 let r_ = state.rmag_km();
161 let s_ = state.radius_km.x / r_;
162 let t_ = state.radius_km.y / r_;
163 let u_ = state.radius_km.z / r_;
164 let max_degree = self.stor.max_degree_n(); let max_order = self.stor.max_order_m(); let mut a_nm = self.a_nm.clone();
169
170 a_nm[(1, 0)] = u_ * 3.0f64.sqrt();
172 for n in 1..=max_degree + 1 {
173 let nf64 = n as f64;
174 a_nm[(n + 1, n)] = (2.0 * nf64 + 3.0).sqrt() * u_ * a_nm[(n, n)];
176 }
177
178 for m in 0..=max_order + 1 {
179 for n in (m + 2)..=max_degree + 1 {
180 let hm_idx = (n, m);
181 a_nm[hm_idx] = u_ * self.b_nm[hm_idx] * a_nm[(n - 1, m)]
182 - self.c_nm[hm_idx] * a_nm[(n - 2, m)];
183 }
184 }
185
186 let mut r_m = Vec::with_capacity(min(max_degree, max_order) + 1);
188 let mut i_m = Vec::with_capacity(min(max_degree, max_order) + 1);
189
190 r_m.push(1.0);
191 i_m.push(0.0);
192
193 for m in 1..=min(max_degree, max_order) {
194 r_m.push(s_ * r_m[m - 1] - t_ * i_m[m - 1]);
195 i_m.push(s_ * i_m[m - 1] + t_ * r_m[m - 1]);
196 }
197
198 let eq_radius_km = self
199 .frame
200 .mean_equatorial_radius_km()
201 .context(AstroPhysicsSnafu)
202 .context(DynamicsAstroSnafu)?;
203
204 let mu_km3_s2 = self
205 .frame
206 .mu_km3_s2()
207 .context(AstroPhysicsSnafu)
208 .context(DynamicsAstroSnafu)?;
209
210 let rho = eq_radius_km / r_;
211 let mut rho_np1 = mu_km3_s2 / r_ * rho;
212 let mut accel4: Vector4<f64> = Vector4::zeros();
213
214 for n in 1..=max_degree {
215 let mut sum: Vector4<f64> = Vector4::zeros();
216 rho_np1 *= rho;
217
218 for m in 0..=min(n, max_order) {
219 let (c_val, s_val) = self.stor.cs_nm(n, m);
220 let d_ = unsafe {
221 (c_val * r_m.get_unchecked(m) + s_val * i_m.get_unchecked(m)) * 2.0.sqrt()
222 };
223 let e_ = if m == 0 {
224 0.0
225 } else {
226 unsafe {
227 (c_val * r_m.get_unchecked(m - 1) + s_val * i_m.get_unchecked(m - 1))
228 * 2.0.sqrt()
229 }
230 };
231 let f_ = if m == 0 {
232 0.0
233 } else {
234 unsafe {
235 (s_val * r_m.get_unchecked(m - 1) - c_val * i_m.get_unchecked(m - 1))
236 * 2.0.sqrt()
237 }
238 };
239
240 unsafe {
241 sum.x += (m as f64) * a_nm.get_unchecked((n, m)) * e_;
242 sum.y += (m as f64) * a_nm.get_unchecked((n, m)) * f_;
243 sum.z += self.vr01.get_unchecked((n, m)) * a_nm.get_unchecked((n, m + 1)) * d_;
244 sum.w -=
245 self.vr11.get_unchecked((n, m)) * a_nm.get_unchecked((n + 1, m + 1)) * d_;
246 }
247 }
248 let rr = rho_np1 / eq_radius_km;
249 accel4 += rr * sum;
250 }
251 let accel = Vector3::new(
252 accel4.x + accel4.w * s_,
253 accel4.y + accel4.w * t_,
254 accel4.z + accel4.w * u_,
255 );
256 let dcm = almanac
260 .rotate(self.frame, osc.frame, osc.epoch)
261 .context(OrientationSnafu {
262 action: "transform state dcm",
263 })
264 .context(DynamicsAlmanacSnafu {
265 action: "transforming into gravity field frame",
266 })?;
267
268 Ok(dcm.rot_mat * accel)
269 }
270
271 fn gradient(
275 &self,
276 osc: &Orbit,
277 almanac: Arc<Almanac>,
278 ) -> Result<(Vector3<f64>, Matrix3<f64>), DynamicsError> {
279 let state = almanac
281 .transform_to(*osc, self.frame, None)
282 .context(DynamicsAlmanacSnafu {
283 action: "transforming into gravity field frame",
284 })?;
285
286 let radius: Vector3<OHyperdual<f64, U7>> = hyperspace_from_vector(&state.radius_km);
287
288 let r_ = norm(&radius);
290 let s_ = radius[0] / r_;
291 let t_ = radius[1] / r_;
292 let u_ = radius[2] / r_;
293 let max_degree = self.stor.max_degree_n(); let max_order = self.stor.max_order_m(); let mut a_nm = DMatrix::from_element(max_degree + 3, max_degree + 3, OHyperdual::from(0.0));
298 for i in 0..=max_degree + 1 {
300 a_nm[(i, i)] = self.a_nm_h[(i, i)];
301 }
302
303 a_nm[(1, 0)] = u_ * 3.0f64.sqrt();
305 for n in 1..=max_degree + 1 {
306 let nf64 = n as f64;
307 a_nm[(n + 1, n)] = OHyperdual::from((2.0 * nf64 + 3.0).sqrt()) * u_ * a_nm[(n, n)];
309 }
310
311 for m in 0..=max_order + 1 {
312 for n in (m + 2)..=max_degree + 1 {
313 let hm_idx = (n, m);
314 a_nm[hm_idx] = u_ * self.b_nm_h[hm_idx] * a_nm[(n - 1, m)]
315 - self.c_nm_h[hm_idx] * a_nm[(n - 2, m)];
316 }
317 }
318
319 let mut r_m = Vec::with_capacity(min(max_degree, max_order) + 1);
321 let mut i_m = Vec::with_capacity(min(max_degree, max_order) + 1);
322
323 r_m.push(OHyperdual::<f64, U7>::from(1.0));
324 i_m.push(OHyperdual::<f64, U7>::from(0.0));
325
326 for m in 1..=min(max_degree, max_order) {
327 r_m.push(s_ * r_m[m - 1] - t_ * i_m[m - 1]);
328 i_m.push(s_ * i_m[m - 1] + t_ * r_m[m - 1]);
329 }
330
331 let real_eq_radius_km = self
332 .frame
333 .mean_equatorial_radius_km()
334 .context(AstroPhysicsSnafu)
335 .context(DynamicsAstroSnafu)?;
336
337 let real_mu_km3_s2 = self
338 .frame
339 .mu_km3_s2()
340 .context(AstroPhysicsSnafu)
341 .context(DynamicsAstroSnafu)?;
342
343 let eq_radius = OHyperdual::<f64, U7>::from(real_eq_radius_km);
344 let rho = eq_radius / r_;
345 let mut rho_np1 = OHyperdual::<f64, U7>::from(real_mu_km3_s2) / r_ * rho;
346
347 let mut a0 = OHyperdual::<f64, U7>::from(0.0);
348 let mut a1 = OHyperdual::<f64, U7>::from(0.0);
349 let mut a2 = OHyperdual::<f64, U7>::from(0.0);
350 let mut a3 = OHyperdual::<f64, U7>::from(0.0);
351 let sqrt2 = OHyperdual::<f64, U7>::from(2.0.sqrt());
352
353 for n in 1..=max_degree {
354 let mut sum0 = OHyperdual::from(0.0);
355 let mut sum1 = OHyperdual::from(0.0);
356 let mut sum2 = OHyperdual::from(0.0);
357 let mut sum3 = OHyperdual::from(0.0);
358 rho_np1 *= rho;
359
360 for m in 0..=min(n, max_order) {
361 let (c_valf64, s_valf64) = self.stor.cs_nm(n, m);
362 let c_val = OHyperdual::<f64, U7>::from(c_valf64);
363 let s_val = OHyperdual::<f64, U7>::from(s_valf64);
364
365 let d_ = unsafe {
366 (c_val * r_m.get_unchecked(m) + s_val * i_m.get_unchecked(m)) * sqrt2
367 };
368 let e_ = if m == 0 {
369 OHyperdual::from(0.0)
370 } else {
371 unsafe {
372 (c_val * r_m.get_unchecked(m - 1) + s_val * i_m.get_unchecked(m - 1))
373 * sqrt2
374 }
375 };
376 let f_ = if m == 0 {
377 OHyperdual::from(0.0)
378 } else {
379 unsafe {
380 (s_val * r_m.get_unchecked(m - 1) - c_val * i_m.get_unchecked(m - 1))
381 * sqrt2
382 }
383 };
384
385 unsafe {
386 sum0 += OHyperdual::from(m as f64) * a_nm.get_unchecked((n, m)) * e_;
387 sum1 += OHyperdual::from(m as f64) * a_nm.get_unchecked((n, m)) * f_;
388 sum2 +=
389 *self.vr01_h.get_unchecked((n, m)) * *a_nm.get_unchecked((n, m + 1)) * d_;
390 sum3 += *self.vr11_h.get_unchecked((n, m))
391 * *a_nm.get_unchecked((n + 1, m + 1))
392 * d_;
393 }
394 }
395 let rr = rho_np1 / eq_radius;
396 a0 += rr * sum0;
397 a1 += rr * sum1;
398 a2 += rr * sum2;
399 a3 -= rr * sum3;
400 }
401
402 let dcm = almanac
403 .rotate(self.frame, osc.frame, osc.epoch)
404 .context(OrientationSnafu {
405 action: "transform state dcm",
406 })
407 .context(DynamicsAlmanacSnafu {
408 action: "transforming into gravity field frame",
409 })?
410 .rot_mat;
411
412 let accel_local = Vector3::new(a0 + a3 * s_, a1 + a3 * t_, a2 + a3 * u_);
413 let dx = dcm
414 * Vector3::new(
415 accel_local[0].real(),
416 accel_local[1].real(),
417 accel_local[2].real(),
418 );
419
420 let mut grad_local = Matrix3::zeros();
421 for i in 0..3 {
422 for j in 1..4 {
424 grad_local[(i, j - 1)] += accel_local[i][j];
426 }
427 }
428 let grad = dcm * grad_local * dcm.transpose();
429 Ok((dx, grad))
430 }
431}