celestial_coords/
distance.rs1use crate::{CoordError, CoordResult};
2use celestial_core::Angle;
3
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9pub struct Distance {
10 parsecs: f64,
11}
12
13impl Distance {
14 pub fn from_parsecs(parsecs: f64) -> CoordResult<Self> {
22 if !parsecs.is_finite() || parsecs <= 0.0 {
23 return Err(CoordError::invalid_distance(format!(
24 "Distance must be positive and finite, got {}",
25 parsecs
26 )));
27 }
28 Ok(Self { parsecs })
29 }
30
31 pub fn from_light_years(ly: f64) -> CoordResult<Self> {
36 const LY_TO_PC: f64 = 0.3066013937;
37 Self::from_parsecs(ly * LY_TO_PC)
38 }
39
40 pub fn from_au(au: f64) -> CoordResult<Self> {
45 const AU_TO_PC: f64 = 4.84813681109536e-6;
46 Self::from_parsecs(au * AU_TO_PC)
47 }
48
49 pub fn from_kilometers(km: f64) -> CoordResult<Self> {
54 const KM_TO_PC: f64 = 3.24077929e-14;
55 Self::from_parsecs(km * KM_TO_PC)
56 }
57
58 pub fn from_parallax_arcsec(parallax_arcsec: f64) -> CoordResult<Self> {
66 if !parallax_arcsec.is_finite() || parallax_arcsec <= 0.0 {
67 return Err(CoordError::invalid_distance(format!(
68 "Parallax must be positive and finite, got {} arcsec",
69 parallax_arcsec
70 )));
71 }
72 Self::from_parsecs(1.0 / parallax_arcsec)
73 }
74
75 pub fn from_parallax_milliarcsec(parallax_mas: f64) -> CoordResult<Self> {
76 Self::from_parallax_arcsec(parallax_mas / 1000.0)
77 }
78
79 pub fn from_parallax_angle(parallax: Angle) -> CoordResult<Self> {
80 Self::from_parallax_arcsec(parallax.arcseconds())
81 }
82
83 pub fn parsecs(self) -> f64 {
84 self.parsecs
85 }
86
87 pub fn light_years(self) -> f64 {
88 const PC_TO_LY: f64 = 3.2615637769;
89 self.parsecs * PC_TO_LY
90 }
91
92 pub fn au(self) -> f64 {
93 const PC_TO_AU: f64 = 206264.806247096;
94 self.parsecs * PC_TO_AU
95 }
96
97 pub fn kilometers(self) -> f64 {
98 #[allow(clippy::excessive_precision)]
99 const PC_TO_KM: f64 = 3.0856775814913673e13;
100 self.parsecs * PC_TO_KM
101 }
102
103 pub fn parallax_arcsec(self) -> f64 {
104 1.0 / self.parsecs
105 }
106
107 pub fn parallax_milliarcsec(self) -> f64 {
108 self.parallax_arcsec() * 1000.0
109 }
110
111 pub fn parallax_angle(self) -> Angle {
112 Angle::from_arcseconds(self.parallax_arcsec())
113 }
114
115 pub fn distance_modulus(self) -> f64 {
116 5.0 * libm::log10(self.parsecs) - 5.0
117 }
118
119 pub fn from_distance_modulus(dm: f64) -> CoordResult<Self> {
120 let parsecs = 10.0_f64.powf((dm + 5.0) / 5.0);
121 Self::from_parsecs(parsecs)
122 }
123
124 pub fn is_galactic(self) -> bool {
125 self.parsecs < 100_000.0
126 }
127
128 pub fn is_local_group(self) -> bool {
129 self.parsecs < 2_000_000.0
130 }
131
132 pub fn parallax_uncertainty_mas(self, relative_error: f64) -> f64 {
133 let parallax_mas = self.parallax_milliarcsec();
134 parallax_mas * relative_error
135 }
136
137 pub fn proper_motion_distance_au(self, pm_mas_per_year: f64, dt_years: f64) -> f64 {
138 let pm_rad_per_year =
139 pm_mas_per_year * 1e-3 * (celestial_core::constants::PI / (180.0 * 3600.0));
140 let angular_distance_rad = pm_rad_per_year * dt_years;
141 self.au() * angular_distance_rad
142 }
143}
144
145impl std::ops::Add for Distance {
146 type Output = CoordResult<Self>;
147
148 fn add(self, other: Self) -> Self::Output {
149 Self::from_parsecs(self.parsecs + other.parsecs)
150 }
151}
152
153impl std::ops::Sub for Distance {
154 type Output = CoordResult<Self>;
155
156 fn sub(self, other: Self) -> Self::Output {
157 Self::from_parsecs(self.parsecs - other.parsecs)
158 }
159}
160
161impl std::ops::Mul<f64> for Distance {
162 type Output = CoordResult<Self>;
163
164 fn mul(self, factor: f64) -> Self::Output {
165 Self::from_parsecs(self.parsecs * factor)
166 }
167}
168
169impl std::ops::Div<f64> for Distance {
170 type Output = CoordResult<Self>;
171
172 fn div(self, divisor: f64) -> Self::Output {
173 Self::from_parsecs(self.parsecs / divisor)
174 }
175}
176
177impl PartialOrd for Distance {
178 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
179 self.parsecs.partial_cmp(&other.parsecs)
180 }
181}
182
183impl std::fmt::Display for Distance {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 if self.parsecs < 1e-3 {
186 write!(f, "{:.3} AU", self.au())
187 } else if self.parsecs < 1000.0 {
188 write!(f, "{:.3} pc", self.parsecs)
189 } else if self.parsecs < 1e6 {
190 write!(f, "{:.3} kpc", self.parsecs / 1000.0)
191 } else {
192 write!(f, "{:.3} Mpc", self.parsecs / 1e6)
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_distance_creation() {
203 let d1 = Distance::from_parsecs(10.0).unwrap();
204 assert_eq!(d1.parsecs(), 10.0);
205
206 let d2 = Distance::from_parallax_arcsec(0.1).unwrap();
207 assert_eq!(d2.parsecs(), 10.0);
208
209 assert!(Distance::from_parsecs(-1.0).is_err());
210 assert!(Distance::from_parsecs(0.0).is_err());
211 assert!(Distance::from_parallax_arcsec(0.0).is_err());
212 }
213
214 #[test]
215 fn test_from_light_years() {
216 let d = Distance::from_light_years(1.0).unwrap();
217 assert!((d.parsecs() - 0.3066013937).abs() < 1e-9);
218 }
219
220 #[test]
221 fn test_parallax_angle() {
222 let angle = Angle::from_arcseconds(0.1);
223 let d = Distance::from_parallax_angle(angle).unwrap();
224 assert!((d.parsecs() - 10.0).abs() < 1e-12);
225 }
226
227 #[test]
228 fn test_parallax_uncertainty_mas() {
229 let d = Distance::from_parsecs(100.0).unwrap();
230 let unc = d.parallax_uncertainty_mas(0.01);
231 assert!((unc - 0.1).abs() < 1e-6);
232 }
233
234 #[test]
235 fn test_partial_ord() {
236 let d1 = Distance::from_parsecs(10.0).unwrap();
237 let d2 = Distance::from_parsecs(20.0).unwrap();
238 assert!(d1 < d2);
239 }
240
241 #[test]
242 fn test_unit_conversions() {
243 let distance = Distance::from_parsecs(1.0).unwrap();
244
245 #[allow(clippy::excessive_precision)]
246 {
247 assert!((distance.light_years() - 3.261_563_776_9).abs() < 1e-9);
248 assert!((distance.au() - 206264.806_247_096).abs() < 1e-6);
249 assert!((distance.kilometers() - 3.085_677_581_491_367_3e13).abs() < 1e6);
250 }
251 }
252
253 #[test]
254 fn test_parallax_calculations() {
255 let proxima = Distance::from_parallax_arcsec(0.7687).unwrap();
256 assert!((proxima.parsecs() - 1.3009).abs() < 0.001);
257
258 let distance = Distance::from_parallax_milliarcsec(768.7).unwrap();
259 assert!((distance.parsecs() - 1.3009).abs() < 0.001);
260 }
261
262 #[test]
263 fn test_distance_modulus() {
264 let distance = Distance::from_parsecs(10.0).unwrap();
265 let dm = distance.distance_modulus();
266 assert!((dm - 0.0).abs() < 1e-12);
267
268 let recovered = Distance::from_distance_modulus(dm).unwrap();
269 assert!((recovered.parsecs() - 10.0).abs() < 1e-12);
270 }
271
272 #[test]
273 fn test_distance_scales() {
274 let galactic = Distance::from_parsecs(1000.0).unwrap();
275 assert!(galactic.is_galactic());
276 assert!(galactic.is_local_group());
277
278 let extragalactic = Distance::from_parsecs(10_000_000.0).unwrap();
279 assert!(!extragalactic.is_galactic());
280 assert!(!extragalactic.is_local_group());
281 }
282
283 #[test]
284 fn test_proper_motion_distance() {
285 let distance = Distance::from_parsecs(1.0).unwrap();
286
287 let linear_dist = distance.proper_motion_distance_au(1.0, 1.0);
288
289 assert!(linear_dist > 0.0);
290 assert!(linear_dist < 10.0);
291 }
292
293 #[test]
294 fn test_arithmetic_operations() {
295 let d1 = Distance::from_parsecs(10.0).unwrap();
296 let d2 = Distance::from_parsecs(5.0).unwrap();
297
298 let sum = (d1 + d2).unwrap();
299 assert_eq!(sum.parsecs(), 15.0);
300
301 let diff = (d1 - d2).unwrap();
302 assert_eq!(diff.parsecs(), 5.0);
303
304 let doubled = (d1 * 2.0).unwrap();
305 assert_eq!(doubled.parsecs(), 20.0);
306
307 let halved = (d1 / 2.0).unwrap();
308 assert_eq!(halved.parsecs(), 5.0);
309 }
310
311 #[test]
312 fn test_display() {
313 let close = Distance::from_au(1.0).unwrap();
314 assert!(close.to_string().contains("AU"));
315
316 let nearby = Distance::from_parsecs(10.0).unwrap();
317 assert!(nearby.to_string().contains("pc"));
318
319 let distant = Distance::from_parsecs(10000.0).unwrap();
320 assert!(distant.to_string().contains("kpc"));
321
322 let very_distant = Distance::from_parsecs(10_000_000.0).unwrap();
323 assert!(very_distant.to_string().contains("Mpc"));
324 }
325}