use crate::functional::Functional;
use crate::legendre::idx;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TideSystem {
TideFree,
ZeroTide,
MeanTide,
Unknown,
}
impl TideSystem {
fn c20_offset(self) -> Option<f64> {
let direct = -3.11080e-8 / 5f64.sqrt();
let k = 0.3;
match self {
TideSystem::TideFree => Some(0.0),
TideSystem::ZeroTide => Some(direct * k),
TideSystem::MeanTide => Some(direct * (1.0 + k)),
TideSystem::Unknown => None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(try_from = "RawCoefficients"))]
pub struct Coefficients {
pub gm: f64,
pub radius: f64,
pub tide_system: TideSystem,
nmax: usize,
pub(crate) c: Vec<f64>,
pub(crate) s: Vec<f64>,
}
impl Coefficients {
pub fn zeros(gm: f64, radius: f64, nmax: usize) -> Self {
let len = idx(nmax, nmax) + 1;
Coefficients {
gm,
radius,
tide_system: TideSystem::Unknown,
nmax,
c: vec![0.0; len],
s: vec![0.0; len],
}
}
pub fn max_degree(&self) -> usize {
self.nmax
}
pub fn c(&self, n: usize, m: usize) -> f64 {
self.c[self.checked(n, m)]
}
pub fn s(&self, n: usize, m: usize) -> f64 {
self.s[self.checked(n, m)]
}
pub fn set(&mut self, n: usize, m: usize, c: f64, s: f64) {
let k = self.checked(n, m);
self.c[k] = c;
self.s[k] = s;
}
fn checked(&self, n: usize, m: usize) -> usize {
assert!(m <= n && n <= self.nmax, "coefficient ({n},{m}) outside degree {}", self.nmax);
idx(n, m)
}
pub fn degree_variances(&self) -> Vec<f64> {
(0..=self.nmax)
.map(|n| (0..=n).map(|m| self.c[idx(n, m)].powi(2) + self.s[idx(n, m)].powi(2)).sum())
.collect()
}
pub fn degree_amplitudes(&self, functional: &Functional) -> Vec<f64> {
self.degree_variances()
.into_iter()
.enumerate()
.map(|(n, cn)| functional.factor(n, self.gm, self.radius, self.radius).abs() * cn.sqrt())
.collect()
}
pub fn truncated(&self, nmax: usize) -> Coefficients {
let nmax = nmax.min(self.nmax);
let len = idx(nmax, nmax) + 1;
Coefficients { nmax, c: self.c[..len].to_vec(), s: self.s[..len].to_vec(), ..self.clone() }
}
pub fn rescaled(&self, gm: f64, radius: f64) -> Coefficients {
let mut out = self.clone();
(out.gm, out.radius) = (gm, radius);
for n in 0..=self.nmax {
let f = self.gm / gm * (self.radius / radius).powi(n as i32);
for m in 0..=n {
out.c[idx(n, m)] *= f;
out.s[idx(n, m)] *= f;
}
}
out
}
pub fn to_tide_system(&self, target: TideSystem) -> Option<Coefficients> {
let delta = target.c20_offset()? - self.tide_system.c20_offset()?;
if self.nmax < 2 {
return None;
}
let mut out = self.clone();
out.c[idx(2, 0)] += delta;
out.tide_system = target;
Some(out)
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
struct RawCoefficients {
gm: f64,
radius: f64,
tide_system: TideSystem,
nmax: usize,
c: Vec<f64>,
s: Vec<f64>,
}
#[cfg(feature = "serde")]
impl TryFrom<RawCoefficients> for Coefficients {
type Error = String;
fn try_from(r: RawCoefficients) -> Result<Self, String> {
let len = r.nmax.checked_add(1).and_then(|a| a.checked_mul(r.nmax + 2)).map(|v| v / 2);
if len != Some(r.c.len()) || r.c.len() != r.s.len() {
return Err(format!("coefficient arrays of length {}/{} do not match degree {}", r.c.len(), r.s.len(), r.nmax));
}
Ok(Coefficients { gm: r.gm, radius: r.radius, tide_system: r.tide_system, nmax: r.nmax, c: r.c, s: r.s })
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TermKind {
Value,
Trend,
Cos {
period: f64,
},
Sin {
period: f64,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct Term {
pub n: usize,
pub m: usize,
pub kind: TermKind,
pub c: f64,
pub s: f64,
pub epoch: f64,
pub valid: core::ops::Range<f64>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TimeVariable {
pub base: Coefficients,
pub terms: Vec<Term>,
}
impl TimeVariable {
pub fn at(&self, t: f64) -> Coefficients {
let mut out = self.base.clone();
for term in self.terms.iter().filter(|term| term.valid.contains(&t)) {
let dt = t - term.epoch;
let w = match term.kind {
TermKind::Value => 1.0,
TermKind::Trend => dt,
TermKind::Cos { period } => (core::f64::consts::TAU * dt / period).cos(),
TermKind::Sin { period } => (core::f64::consts::TAU * dt / period).sin(),
};
let k = out.checked(term.n, term.m);
out.c[k] += w * term.c;
out.s[k] += w * term.s;
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iers_c20_tide_conversion() {
let mut zt = Coefficients::zeros(3.986004415e14, 6378136.3, 2);
zt.tide_system = TideSystem::ZeroTide;
zt.set(2, 0, -0.48416948e-3, 0.0);
let tf = zt.to_tide_system(TideSystem::TideFree).unwrap();
assert!((tf.c(2, 0) - -0.48416531e-3).abs() < 5e-12);
assert!((zt.c(2, 0) - tf.c(2, 0) - -4.1736e-9).abs() < 1e-13);
let back = tf.to_tide_system(TideSystem::MeanTide).unwrap().to_tide_system(TideSystem::ZeroTide).unwrap();
assert!((back.c(2, 0) - zt.c(2, 0)).abs() < 1e-18);
let mut unknown = zt.clone();
unknown.tide_system = TideSystem::Unknown;
assert!(unknown.to_tide_system(TideSystem::TideFree).is_none());
}
#[cfg(feature = "serde")]
#[test]
fn deserialization_checks_array_lengths() {
let raw = |nmax, len| RawCoefficients { gm: 1.0, radius: 1.0, tide_system: TideSystem::Unknown, nmax, c: vec![0.0; len], s: vec![0.0; len] };
assert!(Coefficients::try_from(raw(2, 6)).is_ok());
assert!(Coefficients::try_from(raw(2, 5)).is_err());
assert!(Coefficients::try_from(raw(usize::MAX, 0)).is_err());
}
#[test]
fn degree_variances_sum_orders() {
let mut c = Coefficients::zeros(1.0, 1.0, 3);
c.set(2, 1, 3.0, 4.0);
c.set(3, 3, 1.0, 0.0);
assert_eq!(c.degree_variances(), vec![0.0, 0.0, 25.0, 1.0]);
let t = c.truncated(2);
assert_eq!((t.max_degree(), t.c(2, 1), t.s(2, 1)), (2, 3.0, 4.0));
assert_eq!(c.truncated(9), c);
}
#[test]
#[should_panic(expected = "outside degree")]
fn order_above_degree_is_rejected() {
Coefficients::zeros(1.0, 1.0, 3).set(1, 2, 1.0, 0.0);
}
#[test]
fn time_variable_evaluation() {
let mut base = Coefficients::zeros(1.0, 1.0, 2);
base.set(2, 0, -4.8e-4, 0.0);
let interval = 2010.0..2020.0;
let term = |kind, c, s| Term { n: 2, m: 1, kind, c, s, epoch: 2010.0, valid: interval.clone() };
let model = TimeVariable {
base,
terms: vec![
term(TermKind::Value, 1e-9, 2e-9),
term(TermKind::Trend, 1e-11, -1e-11),
term(TermKind::Cos { period: 1.0 }, 3e-11, 0.0),
term(TermKind::Sin { period: 0.5 }, 0.0, 4e-11),
Term { epoch: 2020.0, valid: 2020.0..2030.0, ..term(TermKind::Value, 9.0, 9.0) },
],
};
let g = model.at(2012.1);
let dt: f64 = 2012.1 - 2010.0;
let tau = core::f64::consts::TAU;
let c21 = 1e-9 + 1e-11 * dt + 3e-11 * (tau * dt).cos();
let s21 = 2e-9 - 1e-11 * dt + 4e-11 * (tau * dt / 0.5).sin();
assert!((g.c(2, 1) - c21).abs() < 1e-24);
assert!((g.s(2, 1) - s21).abs() < 1e-24);
assert_eq!(g.c(2, 0), -4.8e-4);
assert_eq!(model.at(2005.0).c(2, 1), 0.0);
assert_eq!(model.at(2025.0).c(2, 1), 9.0);
}
}