use affn::centers::ReferenceCenter;
use affn::frames::ReferenceFrame;
use affn::matrix3::FrameMatrix3;
use qtty::dynamics::GravitationalParameter;
use qtty::length::Kilometers;
use tempoch::ContinuousScale;
use crate::error::PrincipiaError;
use crate::models::{AccelerationModel, AccelerationPartials};
use crate::state::{Acceleration, DynamicsState};
const DEGENERATE_RADIUS_KM: f64 = 100.0;
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct J2 {
pub mu: GravitationalParameter,
pub r_ref: Kilometers,
pub j2: f64,
min_radius: f64,
}
impl J2 {
#[inline]
pub const fn new(mu: GravitationalParameter, r_ref: Kilometers, j2: f64) -> Self {
Self {
mu,
r_ref,
j2,
min_radius: DEGENERATE_RADIUS_KM,
}
}
pub fn try_new(
mu: GravitationalParameter,
r_ref: Kilometers,
j2: f64,
) -> Result<Self, PrincipiaError> {
if !mu.value().is_finite() || mu.value() <= 0.0 {
return Err(PrincipiaError::NonPositiveValue {
context: "J2: gravitational parameter must be finite and positive",
});
}
if !r_ref.value().is_finite() || r_ref.value() <= 0.0 {
return Err(PrincipiaError::NonPositiveValue {
context: "J2: reference radius must be finite and positive",
});
}
if !j2.is_finite() {
return Err(PrincipiaError::NonFiniteValue {
context: "J2: j2 coefficient must be finite",
});
}
Ok(Self {
mu,
r_ref,
j2,
min_radius: DEGENERATE_RADIUS_KM,
})
}
pub fn try_with_min_radius(self, min_radius_km: f64) -> Result<Self, PrincipiaError> {
if !min_radius_km.is_finite() || min_radius_km <= 0.0 {
return Err(PrincipiaError::NonPositiveValue {
context: "J2: min_radius_km must be finite and positive",
});
}
Ok(Self {
min_radius: min_radius_km,
..self
})
}
pub fn with_min_radius(self, min_radius_km: f64) -> Self {
self.try_with_min_radius(min_radius_km)
.expect("min_radius_km must be positive and finite")
}
}
impl<Ctx, S, C, F> AccelerationModel<Ctx, S, C, F> for J2
where
S: ContinuousScale,
C: ReferenceCenter,
F: ReferenceFrame,
{
fn name(&self) -> &'static str {
"j2"
}
fn acceleration(
&self,
state: &DynamicsState<S, C, F>,
_ctx: &Ctx,
) -> Result<Acceleration<F>, PrincipiaError> {
let rx = state.position.x().value();
let ry = state.position.y().value();
let rz = state.position.z().value();
let r2 = rx * rx + ry * ry + rz * rz;
let r = r2.sqrt();
if r < self.min_radius {
return Err(PrincipiaError::DegenerateGeometry {
reason: "radial magnitude below J2 degeneracy threshold",
});
}
let mu = self.mu.value();
let r_eq = self.r_ref.value();
let coef = 1.5 * self.j2 * mu * r_eq * r_eq / (r2 * r2 * r);
let zr2 = (rz * rz) / r2;
let common = 5.0 * zr2 - 1.0;
Ok(Acceleration::<F>::new(
coef * common * rx,
coef * common * ry,
coef * (common - 2.0) * rz,
))
}
fn partials(
&self,
state: &DynamicsState<S, C, F>,
_ctx: &Ctx,
) -> Result<AccelerationPartials<F>, PrincipiaError> {
let x = state.position.x().value();
let y = state.position.y().value();
let z = state.position.z().value();
let r2 = x * x + y * y + z * z;
let r = r2.sqrt();
if r < self.min_radius {
return Err(PrincipiaError::DegenerateGeometry {
reason: "radial magnitude below J2 degeneracy threshold",
});
}
let req = self.r_ref.value();
let mu = self.mu.value();
let j2 = self.j2;
let c = 1.5 * j2 * mu * req * req;
let r7 = r2 * r2 * r2 * r;
let d = c / r7;
let q = (z * z) / r2;
let diag_xy_coeff = (5.0 * q - 1.0) * r2;
let off_xy = 5.0 * (7.0 * q - 1.0);
let dxx = d * (diag_xy_coeff - off_xy * x * x);
let dyy = d * (diag_xy_coeff - off_xy * y * y);
let dzz = d * r2 * (30.0 * q - 3.0 - 35.0 * q * q);
let dxy = d * 5.0 * (1.0 - 7.0 * q) * x * y;
let dxz = d * 5.0 * (3.0 - 7.0 * q) * x * z;
let dyz = d * 5.0 * (3.0 - 7.0 * q) * y * z;
Ok(AccelerationPartials {
d_acc_d_pos: FrameMatrix3::from_array([
[dxx, dxy, dxz],
[dxy, dyy, dyz],
[dxz, dyz, dzz],
]),
d_acc_d_vel: FrameMatrix3::zero(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use affn::centers::ReferenceCenter;
use affn::frames::ReferenceFrame;
use qtty::length::Kilometers;
use qtty::unit::Kilometer;
use qtty::{GravitationalParameter, KmPerSecond, Second};
use tempoch::{Time, TT};
#[derive(Debug, Clone, Copy)]
struct Inertial;
impl ReferenceFrame for Inertial {
fn frame_name() -> &'static str {
"Inertial"
}
}
#[derive(Debug, Clone, Copy)]
struct Center;
impl ReferenceCenter for Center {
type Params = ();
fn center_name() -> &'static str {
"Center"
}
}
fn model() -> J2 {
J2::new(
GravitationalParameter::new(398_600.441_8),
Kilometers::new(6_378.137),
1.082_626_68e-3,
)
}
fn state_equatorial(r: f64) -> crate::state::DynamicsState<TT, Center, Inertial> {
crate::state::DynamicsState::new(
Time::<TT>::from_raw_j2000_seconds(Second::new(0.0)).unwrap(),
affn::cartesian::Position::<Center, Inertial, Kilometer>::new(r, 0.0, 0.0),
affn::cartesian::Velocity::<Inertial, KmPerSecond>::new(0.0, 7.5, 0.0),
)
}
fn state_degenerate() -> crate::state::DynamicsState<TT, Center, Inertial> {
crate::state::DynamicsState::new(
Time::<TT>::from_raw_j2000_seconds(Second::new(0.0)).unwrap(),
affn::cartesian::Position::<Center, Inertial, Kilometer>::new(0.0, 0.0, 0.0),
affn::cartesian::Velocity::<Inertial, KmPerSecond>::new(0.0, 7.5, 0.0),
)
}
#[test]
fn try_new_rejects_invalid_parameters() {
assert!(matches!(
J2::try_new(
GravitationalParameter::new(0.0),
Kilometers::new(6_378.137),
1.0
),
Err(PrincipiaError::NonPositiveValue { .. })
));
assert!(matches!(
J2::try_new(
GravitationalParameter::new(398_600.441_8),
Kilometers::new(0.0),
1.0
),
Err(PrincipiaError::NonPositiveValue { .. })
));
assert!(matches!(
J2::try_new(
GravitationalParameter::new(398_600.441_8),
Kilometers::new(6_378.137),
f64::NAN,
),
Err(PrincipiaError::NonFiniteValue { .. })
));
}
#[test]
fn try_with_min_radius_rejects_invalid_threshold() {
for min_radius in [0.0, -1.0, f64::INFINITY, f64::NAN] {
let result = model().try_with_min_radius(min_radius);
assert!(matches!(
result,
Err(PrincipiaError::NonPositiveValue { .. })
));
}
}
#[test]
fn name_is_j2() {
let m = model();
assert_eq!(
<J2 as AccelerationModel<(), TT, Center, Inertial>>::name(&m),
"j2"
);
}
#[test]
fn acceleration_equatorial_z_component_zero() {
let a = model()
.acceleration(&state_equatorial(7000.0), &())
.unwrap();
assert!(a.z().value().abs() < 1e-20);
}
#[test]
fn acceleration_degenerate_returns_error() {
assert!(model().acceleration(&state_degenerate(), &()).is_err());
}
#[test]
fn partials_d_acc_d_vel_is_zero() {
let p = model().partials(&state_equatorial(7000.0), &()).unwrap();
let v = p.d_acc_d_vel.as_array();
for row in v {
for val in row {
assert_eq!(*val, 0.0);
}
}
}
#[test]
fn partials_degenerate_returns_error() {
assert!(model().partials(&state_degenerate(), &()).is_err());
}
}