1use rayon::prelude::*;
19
20use super::ErfaError;
21use crate::{
22 constants::*, math::cross_correlation_baseline_to_tiles, HADec, LatLngHeight, ENH, UVW,
23};
24
25#[derive(Clone, Copy, Debug, Default, PartialEq)]
32pub struct XyzGeodetic {
33 pub x: f64,
35 pub y: f64,
37 pub z: f64,
39}
40
41impl XyzGeodetic {
42 pub fn to_enh(self, latitude: f64) -> ENH {
44 let (s_lat, c_lat) = latitude.sin_cos();
45 Self::to_enh_inner(self, s_lat, c_lat)
46 }
47
48 pub fn to_enh_inner(self, sin_latitude: f64, cos_latitude: f64) -> ENH {
53 ENH {
54 e: self.y,
55 n: -self.x * sin_latitude + self.z * cos_latitude,
56 h: self.x * cos_latitude + self.z * sin_latitude,
57 }
58 }
59
60 pub fn to_enh_mwa(self) -> ENH {
63 self.to_enh(MWA_LAT_RAD)
64 }
65
66 pub fn to_geocentric(self, earth_pos: LatLngHeight) -> Result<XyzGeocentric, ErfaError> {
68 let (sin_longitude, cos_longitude) = earth_pos.longitude_rad.sin_cos();
69 let geocentric_vector = XyzGeocentric::get_geocentric_vector(earth_pos)?;
70 Ok(XyzGeodetic::to_geocentric_inner(
71 self,
72 geocentric_vector,
73 sin_longitude,
74 cos_longitude,
75 ))
76 }
77
78 pub fn to_geocentric_inner(
83 self,
84 geocentric_vector: XyzGeocentric,
85 sin_longitude: f64,
86 cos_longitude: f64,
87 ) -> XyzGeocentric {
88 let xtemp = self.x * cos_longitude - self.y * sin_longitude;
89 let y = self.x * sin_longitude + self.y * cos_longitude;
90 let x = xtemp;
91
92 XyzGeocentric {
93 x: x + geocentric_vector.x,
94 y: y + geocentric_vector.y,
95 z: self.z + geocentric_vector.z,
96 }
97 }
98
99 pub fn to_geocentric_mwa(self) -> Result<XyzGeocentric, ErfaError> {
102 self.to_geocentric(LatLngHeight::new_mwa())
103 }
104
105 #[cfg(feature = "mwalib")]
113 pub fn get_tiles(context: &mwalib::MetafitsContext, latitude_rad: f64) -> Vec<XyzGeodetic> {
114 let (sin_lat, cos_lat) = latitude_rad.sin_cos();
115 context
116 .rf_inputs
117 .iter()
118 .filter(|rf| matches!(rf.pol, mwalib::Pol::Y))
122 .map(|rf| {
123 ENH {
124 e: rf.east_m,
125 n: rf.north_m,
126 h: rf.height_m,
127 }
128 .to_xyz_inner(sin_lat, cos_lat)
129 })
130 .collect()
131 }
132
133 #[cfg(feature = "mwalib")]
141 pub fn get_tiles_mwa(context: &mwalib::MetafitsContext) -> Vec<XyzGeodetic> {
142 Self::get_tiles(context, MWA_LAT_RAD)
143 }
144}
145
146pub fn xyzs_to_uvws(xyzs: &[XyzGeodetic], phase_centre: HADec) -> Vec<UVW> {
149 let (s_ha, c_ha) = phase_centre.ha.sin_cos();
150 let (s_dec, c_dec) = phase_centre.dec.sin_cos();
151 let tile_uvws: Vec<UVW> = xyzs
153 .iter()
154 .map(|&xyz| UVW::from_xyz_inner(xyz, s_ha, c_ha, s_dec, c_dec))
155 .collect();
156 let num_tiles = xyzs.len();
158 let num_baselines = (num_tiles * (num_tiles - 1)) / 2;
159 let mut bl_uvws = Vec::with_capacity(num_baselines);
160 for i in 0..num_tiles {
161 for j in i + 1..num_tiles {
162 bl_uvws.push(tile_uvws[i] - tile_uvws[j]);
163 }
164 }
165 bl_uvws
166}
167
168pub fn xyzs_to_uvws_parallel(xyzs: &[XyzGeodetic], phase_centre: HADec) -> Vec<UVW> {
172 let (s_ha, c_ha) = phase_centre.ha.sin_cos();
173 let (s_dec, c_dec) = phase_centre.dec.sin_cos();
174 let tile_uvws: Vec<UVW> = xyzs
176 .par_iter()
177 .map(|&xyz| UVW::from_xyz_inner(xyz, s_ha, c_ha, s_dec, c_dec))
178 .collect();
179 let num_tiles = xyzs.len();
181 let num_baselines = (num_tiles * (num_tiles - 1)) / 2;
182 (0..num_baselines)
183 .into_par_iter()
184 .map(|i_bl| {
185 let (i, j) = cross_correlation_baseline_to_tiles(num_tiles, i_bl);
186 tile_uvws[i] - tile_uvws[j]
187 })
188 .collect()
189}
190
191impl std::ops::Sub<XyzGeodetic> for XyzGeodetic {
192 type Output = Self;
193
194 fn sub(self, rhs: Self) -> Self {
195 XyzGeodetic {
196 x: self.x - rhs.x,
197 y: self.y - rhs.y,
198 z: self.z - rhs.z,
199 }
200 }
201}
202
203#[derive(Clone, Copy, Debug, Default, PartialEq)]
210pub struct XyzGeocentric {
211 pub x: f64,
213 pub y: f64,
215 pub z: f64,
217}
218
219impl XyzGeocentric {
220 pub fn get_geocentric_vector(earth_pos: LatLngHeight) -> Result<XyzGeocentric, ErfaError> {
223 let mut geocentric_vector: [f64; 3] = [0.0; 3];
224 let status = unsafe {
225 erfa_sys::eraGd2gc(
226 erfa_sys::ERFA_WGS84 as i32, earth_pos.longitude_rad, earth_pos.latitude_rad, earth_pos.height_metres, geocentric_vector.as_mut_ptr(), )
232 };
233 if status != 0 {
234 return Err(ErfaError {
235 source_file: file!(),
236 source_line: line!(),
237 status,
238 function: "eraGd2gc",
239 });
240 }
241 Ok(XyzGeocentric {
242 x: geocentric_vector[0],
243 y: geocentric_vector[1],
244 z: geocentric_vector[2],
245 })
246 }
247
248 pub fn get_geocentric_vector_mwa() -> Result<XyzGeocentric, ErfaError> {
252 Self::get_geocentric_vector(LatLngHeight::new_mwa())
253 }
254
255 pub fn to_geodetic(self, earth_pos: LatLngHeight) -> Result<XyzGeodetic, ErfaError> {
257 let geocentric_vector = XyzGeocentric::get_geocentric_vector(earth_pos)?;
258 let (sin_longitude, cos_longitude) = earth_pos.longitude_rad.sin_cos();
259 let geodetic =
260 XyzGeocentric::to_geodetic_inner(self, geocentric_vector, sin_longitude, cos_longitude);
261 Ok(geodetic)
262 }
263
264 pub fn to_geodetic_inner(
269 self,
270 geocentric_vector: XyzGeocentric,
271 sin_longitude: f64,
272 cos_longitude: f64,
273 ) -> XyzGeodetic {
274 let geodetic = XyzGeodetic {
275 x: self.x - geocentric_vector.x,
276 y: self.y - geocentric_vector.y,
277 z: self.z - geocentric_vector.z,
278 };
279
280 let xtemp = geodetic.x * cos_longitude - geodetic.y * -sin_longitude;
281 let y = geodetic.x * -sin_longitude + geodetic.y * cos_longitude;
282 let x = xtemp;
283 XyzGeodetic {
284 x,
285 y,
286 z: geodetic.z,
287 }
288 }
289
290 pub fn to_geodetic_mwa(self) -> Result<XyzGeodetic, ErfaError> {
293 self.to_geodetic(LatLngHeight::new_mwa())
294 }
295}
296
297#[cfg(test)]
298impl approx::AbsDiffEq for XyzGeodetic {
299 type Epsilon = f64;
300
301 fn default_epsilon() -> f64 {
302 f64::EPSILON
303 }
304
305 fn abs_diff_eq(&self, other: &Self, epsilon: f64) -> bool {
306 f64::abs_diff_eq(&self.x, &other.x, epsilon)
307 && f64::abs_diff_eq(&self.y, &other.y, epsilon)
308 && f64::abs_diff_eq(&self.z, &other.z, epsilon)
309 }
310}
311
312#[cfg(test)]
313impl approx::AbsDiffEq for XyzGeocentric {
314 type Epsilon = f64;
315
316 fn default_epsilon() -> f64 {
317 f64::EPSILON
318 }
319
320 fn abs_diff_eq(&self, other: &Self, epsilon: f64) -> bool {
321 f64::abs_diff_eq(&self.x, &other.x, epsilon)
322 && f64::abs_diff_eq(&self.y, &other.y, epsilon)
323 && f64::abs_diff_eq(&self.z, &other.z, epsilon)
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use approx::*;
331 use ndarray::Array1;
332
333 use crate::constants::{
334 COTTER_MWA_HEIGHT_METRES, COTTER_MWA_LATITUDE_RADIANS, COTTER_MWA_LONGITUDE_RADIANS,
335 };
336
337 #[test]
338 fn test_geocentric_to_geodetic() {
339 let geocentric_vector = XyzGeocentric {
341 x: -2559453.2905955315,
342 y: 5095371.7354411585,
343 z: -2849056.7735717744,
344 };
345 let sin_longitude = 0.8936001831599957;
346 let cos_longitude = -0.44886380190033387;
347 let geocentric = XyzGeocentric {
348 x: -2559043.7415729975,
349 y: 5095823.023550426,
350 z: -2849455.5775171486,
351 };
352 let result = geocentric.to_geodetic_inner(geocentric_vector, sin_longitude, cos_longitude);
353 let expected = XyzGeodetic {
354 x: 219.43940577989025,
355 y: -568.5399780273752,
356 z: -398.80394537420943,
357 };
358 assert_abs_diff_eq!(result, expected);
359
360 let result = geocentric
362 .to_geodetic(LatLngHeight {
363 longitude_rad: COTTER_MWA_LONGITUDE_RADIANS,
364 latitude_rad: COTTER_MWA_LATITUDE_RADIANS,
365 height_metres: COTTER_MWA_HEIGHT_METRES,
366 })
367 .unwrap();
368 assert_abs_diff_eq!(result, expected);
369 }
370
371 #[test]
372 fn test_geocentric_to_geodetic_and_back() {
373 let uvfits_xyz = XyzGeodetic {
376 x: 4.56250049e+02,
377 y: -1.49785004e+02,
378 z: 6.80459899e+01,
379 };
380 let ms_xyz = XyzGeocentric {
383 x: -2559524.23682043,
384 y: 5095846.67363471,
385 z: -2848988.72758185,
386 };
387
388 let result = ms_xyz.to_geodetic_mwa();
390 assert!(result.is_ok());
391 let local_xyz = result.unwrap();
392
393 assert_abs_diff_eq!(uvfits_xyz, local_xyz, epsilon = 1e0);
396
397 let cotter_earth_pos = LatLngHeight {
399 longitude_rad: COTTER_MWA_LONGITUDE_RADIANS,
400 latitude_rad: COTTER_MWA_LATITUDE_RADIANS,
401 height_metres: COTTER_MWA_HEIGHT_METRES,
402 };
403 let result = ms_xyz.to_geodetic(cotter_earth_pos);
404 assert!(result.is_ok());
405 let local_xyz = result.unwrap();
406 assert_abs_diff_eq!(uvfits_xyz, local_xyz, epsilon = 1e-6);
407
408 let result = uvfits_xyz.to_geocentric_mwa();
410 assert!(result.is_ok());
411 let geocentric_xyz = result.unwrap();
412 assert_abs_diff_eq!(ms_xyz, geocentric_xyz, epsilon = 1e0);
415
416 let result = uvfits_xyz.to_geocentric(cotter_earth_pos);
418 assert!(result.is_ok());
419 let geocentric_xyz = result.unwrap();
420 assert_abs_diff_eq!(ms_xyz, geocentric_xyz, epsilon = 1e-6);
421 }
422
423 #[test]
424 fn xyzs_to_uvws_test() {
425 let xyzs = vec![
426 XyzGeodetic {
427 x: 289.5692922664971,
428 y: -585.6749877929688,
429 z: -259.3106530519151,
430 },
431 XyzGeodetic {
432 x: 750.5194624923599,
433 y: -565.4390258789063,
434 z: 665.2348852011041,
435 },
436 ];
437 let phase = HADec::new(6.0163, -0.453121);
438 let result: Vec<UVW> = xyzs_to_uvws(&xyzs, phase);
439 let expected = UVW {
440 u: 102.04605530570603,
441 v: -1028.2293398297727,
442 w: 0.18220641926160397,
443 };
444 assert_abs_diff_eq!(
445 Array1::from(result),
446 Array1::from_elem(1, expected),
447 epsilon = 1e-10
448 );
449 }
450
451 #[test]
452 fn xyzs_to_uvws_parallel_test() {
453 let xyzs = vec![
454 XyzGeodetic {
455 x: 289.5692922664971,
456 y: -585.6749877929688,
457 z: -259.3106530519151,
458 },
459 XyzGeodetic {
460 x: 750.5194624923599,
461 y: -565.4390258789063,
462 z: 665.2348852011041,
463 },
464 XyzGeodetic::default(),
465 XyzGeodetic::default(),
466 XyzGeodetic::default(),
467 ];
468 let phase = HADec::new(6.0163, -0.453121);
469 let serial_result: Vec<UVW> = xyzs_to_uvws(&xyzs, phase);
470 let parallel_result: Vec<UVW> = xyzs_to_uvws_parallel(&xyzs, phase);
471 assert_abs_diff_eq!(Array1::from(serial_result), Array1::from(parallel_result));
472 }
473}