use crate::coordinates::spherical::position;
use crate::time::JulianDate;
use qtty::*;
use std::fmt;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
type DegreePerYear = qtty::Per<Degree, Year>;
type DegreesPerYear = qtty::frequency::Frequency<Degree, Year>;
const COS_DEC_EPSILON: f64 = 1.0e-12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum RaProperMotionConvention {
MuAlpha,
MuAlphaStar,
}
impl std::fmt::Display for RaProperMotionConvention {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MuAlpha => write!(f, "\u{03bc}\u{03b1}"),
Self::MuAlphaStar => write!(f, "\u{03bc}\u{03b1}\u{2a}"),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ProperMotion {
pub pm_ra: DegreesPerYear,
pub pm_dec: DegreesPerYear,
pub ra_convention: RaProperMotionConvention,
}
impl fmt::Display for ProperMotion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"\u{03bc}\u{03b1}{}: {}, \u{03bc}\u{03b4}: {}",
self.ra_convention, self.pm_ra, self.pm_dec
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ProperMotionError {
RightAscensionUndefinedAtPole { dec_deg: f64 },
}
impl fmt::Display for ProperMotionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProperMotionError::RightAscensionUndefinedAtPole { dec_deg } => write!(
f,
"right-ascension proper motion is undefined near the poles (dec={} deg)",
dec_deg
),
}
}
}
impl std::error::Error for ProperMotionError {}
impl RaProperMotionConvention {
fn to_mu_alpha(
self,
pm_ra: DegreesPerYear,
dec: Degrees,
) -> Result<DegreesPerYear, ProperMotionError> {
match self {
RaProperMotionConvention::MuAlpha => Ok(pm_ra),
RaProperMotionConvention::MuAlphaStar => {
let cos_dec = dec.value().to_radians().cos();
if cos_dec.abs() <= COS_DEC_EPSILON {
return Err(ProperMotionError::RightAscensionUndefinedAtPole {
dec_deg: dec.value(),
});
}
Ok(DegreesPerYear::new(pm_ra.value() / cos_dec))
}
}
}
}
impl ProperMotion {
pub fn from_mu_alpha<T>(pm_ra: Quantity<T>, pm_dec: Quantity<T>) -> Self
where
T: FrequencyUnit,
{
Self {
pm_ra: pm_ra.to::<DegreePerYear>(),
pm_dec: pm_dec.to::<DegreePerYear>(),
ra_convention: RaProperMotionConvention::MuAlpha,
}
}
pub fn from_mu_alpha_star<T>(pm_ra_cosdec: Quantity<T>, pm_dec: Quantity<T>) -> Self
where
T: FrequencyUnit,
{
Self {
pm_ra: pm_ra_cosdec.to::<DegreePerYear>(),
pm_dec: pm_dec.to::<DegreePerYear>(),
ra_convention: RaProperMotionConvention::MuAlphaStar,
}
}
fn ra_rate_at_epoch(&self, dec: Degrees) -> Result<DegreesPerYear, ProperMotionError> {
self.ra_convention.to_mu_alpha(self.pm_ra, dec)
}
}
fn set_proper_motion_since_epoch<U: LengthUnit>(
mean_position: position::EquatorialMeanJ2000<U>,
proper_motion: ProperMotion,
jd: JulianDate,
epoch_jd: JulianDate,
) -> Result<position::EquatorialMeanJ2000<U>, ProperMotionError> {
let t: Years =
Years::new((jd / JulianDate::JULIAN_YEAR) - (epoch_jd / JulianDate::JULIAN_YEAR));
let ra_rate = proper_motion.ra_rate_at_epoch(mean_position.dec())?;
Ok(position::EquatorialMeanJ2000::<U>::new(
mean_position.ra() + (ra_rate * t).to(),
mean_position.dec() + (proper_motion.pm_dec * t).to(),
mean_position.distance,
))
}
pub fn set_proper_motion_since_j2000<U: LengthUnit>(
mean_position: position::EquatorialMeanJ2000<U>,
proper_motion: ProperMotion,
jd: JulianDate,
) -> Result<position::EquatorialMeanJ2000<U>, ProperMotionError> {
set_proper_motion_since_epoch(mean_position, proper_motion, jd, JulianDate::J2000)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coordinates::{
centers::Geocentric, frames::EquatorialMeanJ2000, spherical::Position,
};
use crate::time::JulianDate;
use qtty::{AstronomicalUnit, Degrees};
type DegreesPerYear = qtty::Quantity<qtty::Per<Degree, Year>>;
#[test]
fn test_proper_motion_linear_shift_mu_alpha() {
let mean_position = Position::<Geocentric, EquatorialMeanJ2000, AstronomicalUnit>::new(
Degrees::new(10.0), Degrees::new(20.0), 1.0, );
let mu = ProperMotion::from_mu_alpha::<DegreePerYear>(
DegreesPerYear::new(0.01),
DegreesPerYear::new(-0.005),
);
let jd_future = JulianDate::J2000 + 50.0 * JulianDate::JULIAN_YEAR;
let expected_ra = mean_position.ra() + Degrees::new(0.5);
let expected_dec = mean_position.dec() - Degrees::new(0.25);
let shifted = set_proper_motion_since_j2000(mean_position, mu, jd_future).unwrap();
let ra_err = (shifted.ra() - expected_ra).abs();
let dec_err = (shifted.dec() - expected_dec).abs();
assert!(
ra_err < 1e-6,
"RA shifted incorrectly: got {}, expected {}",
shifted.ra(),
expected_ra
);
assert!(
dec_err < 1e-6,
"DEC shifted incorrectly: got {}, expected {}",
shifted.dec(),
expected_dec
);
}
#[test]
fn test_proper_motion_mu_alpha_star_conversion() {
let mean_position = Position::<Geocentric, EquatorialMeanJ2000, AstronomicalUnit>::new(
Degrees::new(10.0), Degrees::new(60.0), 1.0,
);
let mu = ProperMotion::from_mu_alpha_star::<DegreePerYear>(
DegreesPerYear::new(0.005),
DegreesPerYear::new(0.0),
);
let jd_future = JulianDate::J2000 + 10.0 * JulianDate::JULIAN_YEAR;
let shifted = set_proper_motion_since_j2000(mean_position, mu, jd_future).unwrap();
let expected_ra = Degrees::new(10.1);
let ra_err = (shifted.ra() - expected_ra).abs();
assert!(
ra_err < 1e-6,
"RA shifted incorrectly for µα⋆: got {}, expected {}",
shifted.ra(),
expected_ra
);
}
#[test]
fn test_proper_motion_mu_alpha_star_rejects_pole() {
let mean_position = Position::<Geocentric, EquatorialMeanJ2000, AstronomicalUnit>::new(
Degrees::new(30.0),
Degrees::new(90.0),
1.0,
);
let mu = ProperMotion::from_mu_alpha_star::<DegreePerYear>(
DegreesPerYear::new(0.01),
DegreesPerYear::new(0.0),
);
let jd_future = JulianDate::J2000 + JulianDate::JULIAN_YEAR;
assert!(matches!(
set_proper_motion_since_j2000(mean_position, mu, jd_future),
Err(ProperMotionError::RightAscensionUndefinedAtPole { .. })
));
}
}