use astrodyn_quantities::dims::GravParam;
use astrodyn_quantities::frame::SelfPlanet;
use uom::si::f64::Length;
pub const MAX_SH_DEGREE: usize = 4096;
#[inline]
pub(crate) const fn tri_idx(n: usize, m: usize) -> usize {
n * (n + 1) / 2 + m
}
#[inline]
const fn tri_len(degree: usize) -> usize {
(degree + 1) * (degree + 2) / 2
}
#[derive(Debug, Clone)]
pub struct SphericalHarmonicsData {
pub degree: usize,
pub order: usize,
pub radius: f64,
pub mu: f64,
pub(crate) cnm: Vec<f64>,
pub(crate) snm: Vec<f64>,
pub tide_free: bool,
pub tide_free_delta: f64,
pub(crate) alpha: Vec<f64>,
pub(crate) beta: Vec<f64>,
pub(crate) xi: Vec<f64>,
pub(crate) eta: Vec<f64>,
pub(crate) zeta: Vec<f64>,
pub(crate) upsilon: Vec<f64>,
pub(crate) nrdiag: Vec<f64>,
pub(crate) int_to_double: Vec<f64>,
}
impl SphericalHarmonicsData {
#[allow(clippy::too_many_arguments)]
pub fn new(
degree: usize,
order: usize,
radius: f64,
mu: f64,
cnm: Vec<Vec<f64>>,
snm: Vec<Vec<f64>>,
tide_free: bool,
tide_free_delta: f64,
) -> Self {
assert!(degree > 0, "degree must be > 0");
assert!(
degree <= MAX_SH_DEGREE,
"degree ({degree}) exceeds MAX_SH_DEGREE ({MAX_SH_DEGREE}): the triangular index `n*(n+1)/2 + m` and slot count `(degree+1)*(degree+2)/2` must fit in `usize`. Raise MAX_SH_DEGREE if a legitimate gravity model needs it.",
);
assert!(order <= degree, "order must be <= degree");
assert_eq!(cnm.len(), degree + 1);
assert_eq!(snm.len(), degree + 1);
for (n, row) in cnm.iter().enumerate() {
assert_eq!(
row.len(),
n + 1,
"cnm[{n}] must have {} elements, got {}",
n + 1,
row.len()
);
}
for (n, row) in snm.iter().enumerate() {
assert_eq!(
row.len(),
n + 1,
"snm[{n}] must have {} elements, got {}",
n + 1,
row.len()
);
}
let tri_n = tri_len(degree);
let mut cnm_flat = vec![0.0_f64; tri_n];
let mut snm_flat = vec![0.0_f64; tri_n];
for n in 0..=degree {
let base = tri_idx(n, 0);
cnm_flat[base..base + n + 1].copy_from_slice(&cnm[n]);
snm_flat[base..base + n + 1].copy_from_slice(&snm[n]);
}
let mut data = Self {
degree,
order,
radius,
mu,
cnm: cnm_flat,
snm: snm_flat,
tide_free,
tide_free_delta,
alpha: vec![0.0; degree + 1],
beta: vec![0.0; degree + 1],
xi: vec![0.0; tri_n],
eta: vec![0.0; tri_n],
zeta: vec![0.0; tri_n],
upsilon: vec![0.0; tri_n],
nrdiag: vec![0.0; degree + 1],
int_to_double: vec![0.0; degree + 2],
};
data.initialize_body();
data
}
#[allow(clippy::needless_range_loop)]
#[allow(
clippy::cast_precision_loss,
reason = "SH degree counters are <= a few hundred; fit exactly in f64 mantissa"
)]
fn initialize_body(&mut self) {
let degree = self.degree;
for ii in 0..=degree + 1 {
self.int_to_double[ii] = ii as f64;
}
let mut pnm: Vec<Vec<f64>> = Vec::with_capacity(degree + 1);
for ii in 0..=degree {
pnm.push(vec![0.0; ii + 3]);
}
pnm[0][0] = 1.0;
pnm[0][1] = 0.0;
pnm[0][2] = 0.0;
pnm[1][1] = 3.0_f64.sqrt();
pnm[1][2] = 0.0;
pnm[1][3] = 0.0;
let i2d = &self.int_to_double;
for ii in 2..=degree {
let ii_f = i2d[ii];
for jj in 0..=(ii - 1) {
let jj_f = i2d[jj];
let num1 = (2.0 * ii_f - 1.0) * (2.0 * ii_f + 1.0);
let den1 = (ii_f + jj_f) * (ii_f - jj_f);
self.xi[tri_idx(ii, jj)] = (num1 / den1).sqrt();
let num2 = (2.0 * ii_f + 1.0) * (ii_f + jj_f - 1.0) * (ii_f - jj_f - 1.0);
let den2 = (ii_f + jj_f) * (ii_f - jj_f) * (2.0 * ii_f - 3.0);
if num2 == 0.0 {
self.eta[tri_idx(ii, jj)] = 0.0;
} else {
self.eta[tri_idx(ii, jj)] = (num2 / den2).sqrt();
}
}
for jj in 0..=ii {
let jj_f = i2d[jj];
if ii == jj {
self.zeta[tri_idx(ii, jj)] = 0.0;
self.upsilon[tri_idx(ii, jj)] = 0.0;
} else if jj == 0 {
self.zeta[tri_idx(ii, 0)] = (ii_f * (ii_f + 1.0) / 2.0).sqrt();
self.upsilon[tri_idx(ii, 0)] =
(ii_f * (ii_f - 1.0) * (ii_f + 1.0) * (ii_f + 2.0) / 2.0).sqrt();
} else {
self.zeta[tri_idx(ii, jj)] = ((ii_f - jj_f) * (ii_f + jj_f + 1.0)).sqrt();
self.upsilon[tri_idx(ii, jj)] = ((ii_f - jj_f)
* (ii_f + jj_f + 1.0)
* (ii_f - jj_f - 1.0)
* (ii_f + jj_f + 2.0))
.sqrt();
}
}
pnm[ii][ii] = ((2.0 * ii_f + 1.0) / (2.0 * ii_f)).sqrt() * pnm[ii - 1][ii - 1];
pnm[ii][ii + 1] = 0.0;
pnm[ii][ii + 2] = 0.0;
self.nrdiag[ii] = (2.0 * ii_f + 1.0).sqrt() * pnm[ii - 1][ii - 1];
self.alpha[ii] = ((2.0 * ii_f + 1.0) * (2.0 * ii_f - 1.0)).sqrt() / ii_f;
self.beta[ii] = ((2.0 * ii_f + 1.0) / (2.0 * ii_f - 3.0)).sqrt() * (ii_f - 1.0) / ii_f;
}
}
#[inline]
pub fn cnm(&self, n: usize, m: usize) -> f64 {
assert!(
n <= self.degree,
"cnm({n}, {m}): degree n must be <= self.degree ({})",
self.degree
);
assert!(
m <= n,
"cnm({n}, {m}): order m must be <= degree n (triangular index)"
);
self.cnm[tri_idx(n, m)]
}
#[inline]
pub fn snm(&self, n: usize, m: usize) -> f64 {
assert!(
n <= self.degree,
"snm({n}, {m}): degree n must be <= self.degree ({})",
self.degree
);
assert!(
m <= n,
"snm({n}, {m}): order m must be <= degree n (triangular index)"
);
self.snm[tri_idx(n, m)]
}
#[inline]
pub fn cnm_row(&self, n: usize) -> &[f64] {
assert!(
n <= self.degree,
"cnm_row({n}): degree n must be <= self.degree ({})",
self.degree
);
let base = tri_idx(n, 0);
&self.cnm[base..base + n + 1]
}
#[inline]
pub fn snm_row(&self, n: usize) -> &[f64] {
assert!(
n <= self.degree,
"snm_row({n}): degree n must be <= self.degree ({})",
self.degree
);
let base = tri_idx(n, 0);
&self.snm[base..base + n + 1]
}
#[inline]
pub fn mu_typed(&self) -> GravParam<SelfPlanet> {
GravParam::<SelfPlanet>::from_si(self.mu)
}
#[inline]
pub fn radius_typed(&self) -> Length {
Length::new::<uom::si::length::meter>(self.radius)
}
}