pub(crate) const THETA_SMALL: f64 = 1e-2;
pub(crate) const THETA_NEAR_PI: f64 = 1e-2;
pub(crate) fn sin_over_theta(theta: f64) -> f64 {
if theta < THETA_SMALL {
taylor_sin_over_theta(theta)
} else {
theta.sin() / theta
}
}
pub(crate) fn one_minus_cos_over_theta_sq(theta: f64) -> f64 {
if theta < THETA_SMALL {
taylor_one_minus_cos_over_theta_sq(theta)
} else {
trig_one_minus_cos_over_theta_sq(theta)
}
}
pub(crate) fn theta_minus_sin_over_theta_cubed(theta: f64) -> f64 {
if theta < THETA_SMALL {
taylor_theta_minus_sin_over_theta_cubed(theta)
} else {
(theta - theta.sin()) / (theta * theta * theta)
}
}
pub(crate) fn inverse_left_jacobian_coef(theta: f64) -> f64 {
if theta < THETA_SMALL {
taylor_inverse_left_jacobian_coef(theta)
} else {
trig_inverse_left_jacobian_coef(theta)
}
}
pub(crate) fn theta_over_sin_theta(theta: f64) -> f64 {
if theta < THETA_SMALL {
taylor_theta_over_sin_theta(theta)
} else {
theta / theta.sin()
}
}
pub(crate) fn taylor_sin_over_theta(theta: f64) -> f64 {
let t2 = theta * theta;
1.0 - t2 / 6.0 + t2 * t2 / 120.0
}
pub(crate) fn taylor_one_minus_cos_over_theta_sq(theta: f64) -> f64 {
let t2 = theta * theta;
0.5 - t2 / 24.0 + t2 * t2 / 720.0
}
pub(crate) fn taylor_theta_minus_sin_over_theta_cubed(theta: f64) -> f64 {
let t2 = theta * theta;
1.0 / 6.0 - t2 / 120.0 + t2 * t2 / 5040.0
}
pub(crate) fn taylor_inverse_left_jacobian_coef(theta: f64) -> f64 {
let t2 = theta * theta;
1.0 / 12.0 + t2 / 720.0 + t2 * t2 / 30240.0
}
pub(crate) fn taylor_theta_over_sin_theta(theta: f64) -> f64 {
let t2 = theta * theta;
1.0 + t2 / 6.0 + 7.0 * t2 * t2 / 360.0
}
pub(crate) fn trig_one_minus_cos_over_theta_sq(theta: f64) -> f64 {
let half_sin = (0.5 * theta).sin();
2.0 * half_sin * half_sin / (theta * theta)
}
pub(crate) fn trig_inverse_left_jacobian_coef(theta: f64) -> f64 {
1.0 / (theta * theta) - 1.0 / (2.0 * theta * (0.5 * theta).tan())
}
#[cfg(test)]
mod tests {
use super::*;
fn naive_inverse_left_jacobian_coef(theta: f64) -> f64 {
1.0 / (theta * theta) - (1.0 + theta.cos()) / (2.0 * theta * theta.sin())
}
#[test]
fn half_angle_identity_is_algebraically_correct() {
for theta in [0.5, 1.0, 1.5, 2.0, 2.5] {
let stable = trig_inverse_left_jacobian_coef(theta);
let naive = naive_inverse_left_jacobian_coef(theta);
assert!(
(stable - naive).abs() < 1e-14,
"θ = {theta}: stable {stable:.17e} vs naive {naive:.17e}"
);
}
}
#[test]
fn branches_agree_at_threshold() {
let theta = THETA_SMALL;
let pairs: [(&str, f64, f64); 5] = [
(
"sin θ / θ",
taylor_sin_over_theta(theta),
theta.sin() / theta,
),
(
"(1 − cos θ) / θ²",
taylor_one_minus_cos_over_theta_sq(theta),
trig_one_minus_cos_over_theta_sq(theta),
),
(
"(θ − sin θ) / θ³",
taylor_theta_minus_sin_over_theta_cubed(theta),
(theta - theta.sin()) / theta.powi(3),
),
(
"J⁻¹ coefficient",
taylor_inverse_left_jacobian_coef(theta),
trig_inverse_left_jacobian_coef(theta),
),
(
"θ / sin θ",
taylor_theta_over_sin_theta(theta),
theta / theta.sin(),
),
];
for (name, taylor, trig) in pairs {
assert!(
(taylor - trig).abs() < 1e-10,
"{name}: series {taylor:.17e} vs trigonometry {trig:.17e}, \
difference {:.3e}",
(taylor - trig).abs()
);
}
}
#[test]
fn limits_at_zero() {
assert_eq!(sin_over_theta(0.0), 1.0);
assert_eq!(one_minus_cos_over_theta_sq(0.0), 0.5);
assert_eq!(theta_minus_sin_over_theta_cubed(0.0), 1.0 / 6.0);
assert_eq!(inverse_left_jacobian_coef(0.0), 1.0 / 12.0);
assert_eq!(theta_over_sin_theta(0.0), 1.0);
}
#[test]
fn continuous_across_scales() {
for theta in [0.0, 1e-12, 1e-8, 1e-4, 1e-3] {
assert!((sin_over_theta(theta) - 1.0).abs() < 1e-6);
assert!((one_minus_cos_over_theta_sq(theta) - 0.5).abs() < 1e-6);
assert!((theta_minus_sin_over_theta_cubed(theta) - 1.0 / 6.0).abs() < 1e-6);
assert!((inverse_left_jacobian_coef(theta) - 1.0 / 12.0).abs() < 1e-6);
assert!((theta_over_sin_theta(theta) - 1.0).abs() < 1e-6);
}
}
}