1use kiddo::ImmutableKdTree;
2use kiddo::SquaredEuclidean;
3use libm::{asin, atan2, hypot};
4
5pub fn convert_equitorial_to_cartesian(ra_deg: &f64, dec_deg: &f64) -> [f64; 3] {
7 let ra_radians = ra_deg.to_radians();
8 let dec_radians = dec_deg.to_radians();
9 let x = dec_radians.cos() * ra_radians.cos();
10 let y = dec_radians.cos() * ra_radians.sin();
11 let z = dec_radians.sin();
12 [x, y, z]
13}
14
15pub fn convert_cartesian_to_equitorial(x: &f64, y: &f64, z: &f64) -> [f64; 2] {
17 let radius = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt();
18 let ra_radian = atan2(*y, *x);
19 let dec_radian = asin(z / radius);
20 [ra_radian.to_degrees(), dec_radian.to_degrees()]
21}
22
23pub fn convert_equitorial_to_cartesian_scaled(
25 ra_deg: f64,
26 dec_deg: f64,
27 distance: f64,
28) -> [f64; 3] {
29 let ra_rad = ra_deg.to_radians();
30 let dec_rad = dec_deg.to_radians();
31 let x = distance * dec_rad.cos() * ra_rad.cos();
32 let y = distance * dec_rad.cos() * ra_rad.sin();
33 let z = distance * dec_rad.sin();
34 [x, y, z]
35}
36
37pub fn euclidean_distance_3d(point_1: &[f64; 3], point_2: &[f64; 3]) -> f64 {
39 ((point_1[0] - point_2[0]).powi(2)
40 + (point_1[1] - point_2[1]).powi(2)
41 + (point_1[2] - point_2[2]).powi(2))
42 .sqrt()
43}
44
45pub fn chord_distance(angular_separation_degrees: f64) -> f64 {
47 assert!((0. ..=180.).contains(&angular_separation_degrees));
48 2. * (angular_separation_degrees.to_radians() / 2.).sin()
49}
50
51pub fn angular_separation(lon_1: f64, lat_1: f64, lon_2: f64, lat_2: f64) -> f64 {
56 let lon_1_rad = lon_1.to_radians();
57 let lat_1_rad = lat_1.to_radians();
58 let lon_2_rad = lon_2.to_radians();
59 let lat_2_rad = lat_2.to_radians();
60 let sdlon = (lon_2_rad - lon_1_rad).sin();
61 let cdlon = (lon_2_rad - lon_1_rad).cos();
62 let slat1 = (lat_1_rad).sin();
63 let slat2 = (lat_2_rad).sin();
64 let clat1 = (lat_1_rad).cos();
65 let clat2 = (lat_2_rad).cos();
66
67 let num1 = clat2 * sdlon;
68 let num2 = clat1 * slat2 - slat1 * clat2 * cdlon;
69 let denominator = slat1 * slat2 + clat1 * clat2 * cdlon;
70 atan2(hypot(num1, num2), denominator).to_degrees()
71}
72
73pub fn angular_separation_haversine(
79 lon1_deg: &f64,
80 lat1_deg: &f64,
81 lon2_deg: &f64,
82 lat2_deg: &f64,
83) -> f64 {
84 let lon1 = lon1_deg.to_radians();
86 let lat1 = lat1_deg.to_radians();
87 let lon2 = lon2_deg.to_radians();
88 let lat2 = lat2_deg.to_radians();
89
90 let dlat = lat2 - lat1;
92 let dlon = lon2 - lon1;
93
94 let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
96 let c = 2.0 * a.sqrt().asin();
97
98 c.to_degrees()
100}
101
102pub fn angular_separation_small_angle(
106 lon1_deg: &f64,
107 lat1_deg: &f64,
108 lon2_deg: &f64,
109 lat2_deg: &f64,
110) -> f64 {
111 let lon1 = lon1_deg.to_radians();
113 let lat1 = lat1_deg.to_radians();
114 let lon2 = lon2_deg.to_radians();
115 let lat2 = lat2_deg.to_radians();
116
117 let dlat = lat2 - lat1;
119 let dlon = lon2 - lon1;
120
121 let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
123 let c = 2.0 * a.sqrt().asin();
124
125 c.to_degrees()
127}
128
129#[derive(Debug)]
130pub struct Point {
131 pub ra_deg: f64,
132 pub dec_deg: f64,
133}
134
135pub fn build_kd_tree(ra_array_deg: &[f64], dec_array_deg: &[f64]) -> ImmutableKdTree<f64, 3> {
136 let entries: Vec<[f64; 3]> = ra_array_deg
137 .iter()
138 .zip(dec_array_deg)
139 .map(|(ra, dec)| convert_equitorial_to_cartesian(ra, dec))
140 .collect();
141
142 ImmutableKdTree::new_from_slice(&entries)
143}
144
145pub fn find_idx_within(
146 tree: &ImmutableKdTree<f64, 3>,
147 point: &Point,
148 angular_difference_deg: f64,
149) -> Vec<u64> {
150 let point_cartesian = convert_equitorial_to_cartesian(&point.ra_deg, &point.dec_deg);
151 let dist = chord_distance(angular_difference_deg).powi(2); let neighbors = tree.within_unsorted::<SquaredEuclidean>(&point_cartesian, dist);
153 neighbors.iter().map(|nn| nn.item).collect()
154}
155
156pub fn spherical_mean(ra_points: &[f64], dec_points: &[f64]) -> (f64, f64) {
157 assert_eq!(ra_points.len(), dec_points.len());
158 let mut sum = [0., 0., 0.];
159 for (ra, dec) in ra_points.iter().zip(dec_points.iter()) {
160 let v = convert_equitorial_to_cartesian(ra, dec);
161 sum[0] += v[0];
162 sum[1] += v[1];
163 sum[2] += v[2];
164 }
165
166 let norm = (sum[0].powi(2) + sum[1].powi(2) + sum[2].powi(2)).sqrt();
167 let x = sum[0] / norm;
168 let y = sum[1] / norm;
169 let z = sum[2] / norm;
170
171 let center = convert_cartesian_to_equitorial(&x, &y, &z);
172
173 let mut ra_center = center[0];
174 if ra_center < 0.0 {
175 ra_center += 360.;
176 }
177 (ra_center, center[1])
178}
179
180#[cfg(test)]
181mod test {
182 use super::*;
183 use std::iter::zip;
184
185 const EPSILON: f64 = 1e-6;
186
187 #[test]
188 fn test_kd_tree_building() {
189 let ras = vec![0., 23., 180.];
190 let decs = vec![0., -10., 20.];
191 let tree = build_kd_tree(&ras, &decs);
192
193 let test_point = convert_equitorial_to_cartesian(&ras[1], &decs[1]);
194 let result = tree.within::<SquaredEuclidean>(&test_point, 1e-10);
195 assert_eq!(result.len(), 1);
196 }
197
198 #[test]
199 fn test_finding_within() {
200 let ras = vec![120., 120.9999, 120., 180.];
201 let decs = vec![0., 0., 0., -45.];
202 let tree = build_kd_tree(&ras, &decs);
203 let point = Point {
204 ra_deg: 120.,
205 dec_deg: 0.,
206 };
207 let mut result = find_idx_within(&tree, &point, 1.);
208 assert_eq!(result.len(), 3);
209 let answers = [0, 1, 2];
210 result.sort();
211 for (res, ans) in zip(result, answers) {
212 assert_eq!(res, ans)
213 }
214 }
215
216 #[test]
217 fn test_finding_at_boundaries() {
218 let ras = vec![0., 1., 0., 270.];
219 let decs = vec![0., 0., 1., -45.];
220 let tree = build_kd_tree(&ras, &decs);
221 let point = Point {
222 ra_deg: 0.,
223 dec_deg: 0.,
224 };
225 let mut result = find_idx_within(&tree, &point, 1.);
226 assert_eq!(result.len(), 3);
227 let answers = [0, 1, 2];
228 result.sort();
229 for (res, ans) in zip(result, answers) {
230 assert_eq!(res, ans)
231 }
232 }
233 #[test]
234 fn test_equatorial_to_cartesian_unit_sphere() {
235 let ra = 0.0;
236 let dec = 0.0;
237 let result = convert_equitorial_to_cartesian(&ra, &dec);
238 assert!((result[0] - 1.0).abs() < EPSILON);
239 assert!((result[1]).abs() < EPSILON);
240 assert!((result[2]).abs() < EPSILON);
241 }
242
243 #[test]
244 fn test_cartesian_to_equatorial_unit_sphere() {
245 let x = 1.0;
246 let y = 0.0;
247 let z = 0.0;
248 let result = convert_cartesian_to_equitorial(&x, &y, &z);
249 assert!((result[0] - 0.0).abs() < EPSILON); assert!((result[1] - 0.0).abs() < EPSILON); }
252
253 #[test]
254 fn test_round_trip_conversion() {
255 let ra = 123.4;
256 let dec = -56.78;
257 let cart = convert_equitorial_to_cartesian(&ra, &dec);
258 let equi = convert_cartesian_to_equitorial(&cart[0], &cart[1], &cart[2]);
259
260 let mut ra_norm = equi[0];
262 if ra_norm < 0.0 {
263 ra_norm += 360.0;
264 }
265
266 assert!((ra_norm - ra).abs() < EPSILON);
267 assert!((equi[1] - dec).abs() < EPSILON);
268 }
269
270 #[test]
271 fn test_equatorial_to_cartesian_scaled() {
272 let ra = [120., 122., 124.];
273 let dec = [-56., -34., -30.];
274 let distance = [10., 11., 12.];
275 let result_1 = convert_equitorial_to_cartesian_scaled(ra[0], dec[0], distance[0]);
276 let result_2 = convert_equitorial_to_cartesian_scaled(ra[1], dec[1], distance[1]);
277 let result_3 = convert_equitorial_to_cartesian_scaled(ra[2], dec[2], distance[2]);
278
279 let answer_1 = [-2.795965, 4.842753, -8.290376];
280 let answer_2 = [-4.832553, 7.733701, -6.151122];
281 let answer_3 = [-5.811303, 8.615611, -6.000000];
282
283 for (r, a) in zip(result_1, answer_1) {
284 assert!((r - a).abs() < EPSILON);
285 }
286
287 for (r, a) in zip(result_2, answer_2) {
288 assert!((r - a).abs() < EPSILON);
289 }
290
291 for (r, a) in zip(result_3, answer_3) {
292 assert!((r - a).abs() < EPSILON);
293 }
294 }
295
296 #[test]
297 fn test_euclidean_distance_3d() {
298 let a = [1.0, 0.0, 0.0];
299 let b = [0.0, 0.0, 0.0];
300 let dist = euclidean_distance_3d(&a, &b);
301 assert!((dist - 1.0).abs() < EPSILON);
302
303 let a = [1.0, 2.0, 3.0];
304 let b = [4.0, 6.0, 3.0];
305 let dist = euclidean_distance_3d(&a, &b);
306 assert!((dist - 5.0).abs() < EPSILON);
307 }
308
309 #[test]
310 fn test_chord_distance() {
311 let angular_separation = [0., 20., 30., 180.];
312 let results = angular_separation.iter().map(|&a| chord_distance(a));
313 let answers = [0., 0.347296355, 0.517638090, 2.];
314 for (res, ans) in zip(results, answers) {
315 assert!((res - ans).abs() < 1e-7)
316 }
317 }
318
319 #[test]
320 fn test_projected_separation() {
321 let ra_1s = [-20., -5., 0., 90., 359.];
322 let ra_2s = [-20.5, 20., 30., 40., 50.];
323 let dec_1s = [-90., -45., 0., 45., 90.];
324 let dec_2s = [-45., 0., 5., 10., 20.];
325 let answers = [45., 50.14429257, 30.37550653, 55.22172917, 70.];
326
327 for i in 0..5 {
328 let res = angular_separation(ra_1s[i], dec_1s[i], ra_2s[i], dec_2s[i]);
329 dbg!(res);
330 dbg!(answers[i]);
331 assert!((res - answers[i]).abs() < 1e-5)
332 }
333 }
334 #[test]
335 fn test_separation_over_ra_edge() {
336 let result = angular_separation(1., 0., 359., 0.);
337 assert!((result - 2.).abs() < 1e-9);
338 }
339
340 #[test]
341 fn test_small_angle_approx() {
342 let ra1 = 0.1;
344 let ra2 = 0.2;
345 let dec1 = -20.;
346 let dec2 = -20.;
347 let res = angular_separation_haversine(&ra1, &dec1, &ra2, &dec2);
348 dbg!(res);
349 dbg!(angular_separation(ra1, dec1, ra2, dec2));
350 assert!((angular_separation(ra1, dec1, ra2, dec2) - res).abs() < 1e-5);
351
352 let res = angular_separation_small_angle(&ra1, &dec1, &ra2, &dec2);
353 assert!((angular_separation(ra1, dec1, ra2, dec2) - res).abs() < 1e-5);
354 }
355}