use kinavis_kernel::math;
pub const MAX_DEGREE: usize = 12;
pub(crate) const TABLE_LEN: usize = (MAX_DEGREE + 1) * (MAX_DEGREE + 2) / 2;
const DEGREES: usize = MAX_DEGREE + 1;
const fn slot(n: usize, m: usize) -> usize {
(n.saturating_mul(n.saturating_add(1)) / 2).saturating_add(m)
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Table([f64; TABLE_LEN]);
impl Table {
const ZERO: Self = Self([0.0; TABLE_LEN]);
pub(crate) fn at(&self, n: usize, m: usize) -> f64 {
self.0.get(slot(n, m)).copied().unwrap_or(0.0)
}
fn set(&mut self, n: usize, m: usize, value: f64) {
if let Some(entry) = self.0.get_mut(slot(n, m)) {
*entry = value;
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Legendre {
pub(crate) value: Table,
pub(crate) derivative: Table,
pub(crate) over_cosine: Table,
}
impl Legendre {
#[allow(clippy::many_single_char_names, clippy::range_plus_one)]
pub(crate) fn at(phi: f64) -> Self {
let x = math::sin(phi);
let c = math::cos(phi);
let mut p = Table::ZERO;
let mut dp = Table::ZERO;
let mut q = Table::ZERO;
p.set(0, 0, 1.0);
for n in 1..DEGREES {
let (n_1, n_2) = (n - 1, n.saturating_sub(2));
for m in 0..n + 1 {
let (value, deriv, quot) = if m == n {
let k = 2.0 * count(m) - 1.0;
let seed = if m == 1 { 1.0 } else { k * c * q.at(n_1, n_1) };
(
k * c * p.at(n_1, n_1),
k * (c * dp.at(n_1, n_1) + x * p.at(n_1, n_1)),
seed,
)
} else if m == n_1 {
let k = 2.0 * count(m) + 1.0;
(
k * x * p.at(m, m),
k * (x * dp.at(m, m) - c * p.at(m, m)),
k * x * q.at(m, m),
)
} else {
let a = 2.0 * count(n) - 1.0;
let b = count(n + m) - 1.0;
let d = count(n - m);
(
(a * x * p.at(n_1, m) - b * p.at(n_2, m)) / d,
(a * (x * dp.at(n_1, m) - c * p.at(n_1, m)) - b * dp.at(n_2, m)) / d,
(a * x * q.at(n_1, m) - b * q.at(n_2, m)) / d,
)
};
p.set(n, m, value);
dp.set(n, m, deriv);
if m >= 1 {
q.set(n, m, quot);
}
}
}
let mut value = Table::ZERO;
let mut derivative = Table::ZERO;
let mut over_cosine = Table::ZERO;
value.set(0, 0, 1.0);
for n in 1..DEGREES {
for m in 0..n + 1 {
let factor = schmidt_factor(n, m);
value.set(n, m, factor * p.at(n, m));
derivative.set(n, m, -factor * dp.at(n, m));
over_cosine.set(n, m, factor * q.at(n, m));
}
}
Self {
value,
derivative,
over_cosine,
}
}
}
fn schmidt_factor(n: usize, m: usize) -> f64 {
if m == 0 {
return 1.0;
}
let mut ratio = 2.0;
for k in (n - m + 1)..=(n + m) {
ratio /= count(k);
}
math::sqrt(ratio)
}
fn count(k: usize) -> f64 {
math::count_to_f64(k)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn low_degrees_match_the_closed_forms() {
let phi = math::to_radians(37.0);
let (x, c) = (math::sin(phi), math::cos(phi));
let tables = Legendre::at(phi);
assert!((tables.value.at(1, 0) - x).abs() < 1e-15);
assert!((tables.value.at(1, 1) - c).abs() < 1e-15);
assert!((tables.value.at(2, 0) - (3.0 * x * x - 1.0) / 2.0).abs() < 1e-15);
assert!((tables.value.at(2, 1) - math::sqrt(3.0) * x * c).abs() < 1e-15);
assert!((tables.value.at(2, 2) - math::sqrt(3.0) / 2.0 * c * c).abs() < 1e-15);
assert!((tables.derivative.at(1, 0) - c).abs() < 1e-15);
assert!((tables.derivative.at(1, 1) + x).abs() < 1e-15);
assert!((tables.derivative.at(2, 0) - 3.0 * x * c).abs() < 1e-15);
assert!((tables.derivative.at(2, 1) - math::sqrt(3.0) * (c * c - x * x)).abs() < 1e-15);
assert!((tables.over_cosine.at(2, 1) - tables.value.at(2, 1) / c).abs() < 1e-15);
assert!((tables.over_cosine.at(2, 2) - tables.value.at(2, 2) / c).abs() < 1e-15);
}
#[test]
fn the_quotient_is_finite_at_the_pole_and_agrees_just_short_of_it() {
let pole = Legendre::at(math::to_radians(90.0));
let near = Legendre::at(math::to_radians(89.999_999));
for n in 1..=MAX_DEGREE {
for m in 1..=n {
assert!(pole.over_cosine.at(n, m).is_finite());
let c = math::cos(math::to_radians(89.999_999));
let by_division = near.value.at(n, m) / c;
assert!(
(near.over_cosine.at(n, m) - by_division).abs() < 1e-9,
"n={n} m={m}"
);
}
}
assert!(pole.over_cosine.at(1, 1).abs() > 0.5);
assert!(pole.over_cosine.at(2, 2).abs() < 1e-12);
}
#[test]
fn the_derivative_is_the_slope_of_the_function() {
let phi = math::to_radians(-23.0);
let h = 1e-6;
let tables = Legendre::at(phi);
let up = Legendre::at(phi + h);
let down = Legendre::at(phi - h);
for n in 1..=MAX_DEGREE {
for m in 0..=n {
let numerical = (up.value.at(n, m) - down.value.at(n, m)) / (2.0 * h);
assert!(
(tables.derivative.at(n, m) - numerical).abs() < 1e-6,
"n={n} m={m}"
);
}
}
}
}