mwa_rust_core/pos/
xyz.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5//! Handle (x,y,z) coordinates of an antenna (a.k.a. tile or station), geodetic
6//! or geocentric.
7//!
8//! hyperdrive prefers to keep track of [XyzGeodetic] coordinates, as these are
9//! what are needed to calculate [UVW]s.
10//!
11//! This coordinate system is discussed at length in Interferometry and
12//! Synthesis in Radio Astronomy, Third Edition, Section 4: Geometrical
13//! Relationships, Polarimetry, and the Measurement Equation.
14
15// TODO: Account for northing and eastings. Australia drifts by ~7cm/year, and
16// the ellipsoid model probably need to be changed too!
17
18use rayon::prelude::*;
19
20use super::ErfaError;
21use crate::{
22    constants::*, math::cross_correlation_baseline_to_tiles, HADec, LatLngHeight, ENH, UVW,
23};
24
25/// The geodetic (x,y,z) coordinates of an antenna (a.k.a. tile or station). All
26/// units are in metres.
27///
28/// This coordinate system is discussed at length in Interferometry and
29/// Synthesis in Radio Astronomy, Third Edition, Section 4: Geometrical
30/// Relationships, Polarimetry, and the Measurement Equation.
31#[derive(Clone, Copy, Debug, Default, PartialEq)]
32pub struct XyzGeodetic {
33    /// x-coordinate \[meters\]
34    pub x: f64,
35    /// y-coordinate \[meters\]
36    pub y: f64,
37    /// z-coordinate \[meters\]
38    pub z: f64,
39}
40
41impl XyzGeodetic {
42    /// Convert [XyzGeodetic] coordinates at a latitude to [ENH] coordinates.
43    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    /// Convert [XyzGeodetic] coordinates at a latitude to [ENH] coordinates.
49    /// This function is less convenient than [XyzGeodetic::to_enh()], but is
50    /// slightly more efficient because the caller can prevent needless `sin`
51    /// and `cos` calculations.
52    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    /// Convert [XyzGeodetic] coordinates at the MWA's latitude to [ENH]
61    /// coordinates.
62    pub fn to_enh_mwa(self) -> ENH {
63        self.to_enh(MWA_LAT_RAD)
64    }
65
66    /// Convert a [XyzGeodetic] coordinate to [XyzGeocentric].
67    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    /// Convert a [XyzGeodetic] coordinate to [XyzGeocentric]. This function is
79    /// less convenient than [XyzGeodetic::to_geocentric], but may be better in
80    /// tight loops as the arguments to this function don't need to be uselessly
81    /// re-calculated.
82    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    /// Convert a [XyzGeodetic] coordinate to [XyzGeocentric], using the MWA's
100    /// location.
101    pub fn to_geocentric_mwa(self) -> Result<XyzGeocentric, ErfaError> {
102        self.to_geocentric(LatLngHeight::new_mwa())
103    }
104
105    /// For each tile listed in an [`mwalib::MetafitsContext`], calculate a
106    /// [XyzGeodetic] coordinate.
107    ///
108    /// Note that the RF inputs are ordered by antenna number, **not** the
109    /// "input"; e.g. in the metafits file, Tile104 is often the first tile
110    /// listed ("input" 0), Tile103 second ("input" 2), so the first baseline
111    /// would naively be between Tile104 and Tile103.
112    #[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            // There is an RF input for both tile polarisations. The ENH
119            // coordinates are the same for both polarisations of a tile; ignore
120            // the RF input if it's associated with Y.
121            .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    /// For each tile listed in an [`mwalib::MetafitsContext`], calculate a
134    /// [XyzGeodetic] coordinate assuming the MWA's latitude.
135    ///
136    /// Note that the RF inputs are ordered by antenna number, **not** the
137    /// "input"; e.g. in the metafits file, Tile104 is often the first tile
138    /// listed ("input" 0), Tile103 second ("input" 2), so the first baseline
139    /// would naively be between Tile104 and Tile103.
140    #[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
146/// Convert [XyzGeodetic] tile coordinates to [UVW] baseline coordinates without
147/// having to form [XyzGeodetic] baselines first.
148pub 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    // Get a UVW for each tile.
152    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    // Take the difference of every pair of UVWs.
157    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
168/// Convert [XyzGeodetic] tile coordinates to [UVW] baseline coordinates without
169/// having to form [XyzGeodetic] baselines first. This function performs
170/// calculations in parallel.
171pub 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    // Get a UVW for each tile.
175    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    // Take the difference of every pair of UVWs.
180    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/// The geocentric (x,y,z) coordinates of an antenna (a.k.a. tile or station).
204/// All units are in metres.
205///
206/// This coordinate system is discussed at length in Interferometry and
207/// Synthesis in Radio Astronomy, Third Edition, Section 4: Geometrical
208/// Relationships, Polarimetry, and the Measurement Equation.
209#[derive(Clone, Copy, Debug, Default, PartialEq)]
210pub struct XyzGeocentric {
211    /// x-coordinate \[meters\]
212    pub x: f64,
213    /// y-coordinate \[meters\]
214    pub y: f64,
215    /// z-coordinate \[meters\]
216    pub z: f64,
217}
218
219impl XyzGeocentric {
220    /// Get a geocentric coordinate vector with the given geodetic coordinates
221    /// (longitude, latitude and height). The ellipsoid model is WGS84.
222    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,    // ellipsoid identifier (Note 1)
227                earth_pos.longitude_rad,        // longitude (radians, east +ve)
228                earth_pos.latitude_rad,         // latitude (geodetic, radians, Note 3)
229                earth_pos.height_metres,        // height above ellipsoid (geodetic, Notes 2,3)
230                geocentric_vector.as_mut_ptr(), // geocentric vector (Note 2)
231            )
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    /// Get a geocentric coordinate vector with the MWA's location. This
249    /// function just calls [XyzGeocentric::get_geocentric_vector] with
250    /// [MWA_LONG_RAD], [MWA_LAT_RAD] and [MWA_HEIGHT_M].
251    pub fn get_geocentric_vector_mwa() -> Result<XyzGeocentric, ErfaError> {
252        Self::get_geocentric_vector(LatLngHeight::new_mwa())
253    }
254
255    /// Convert a [XyzGeocentric] coordinate to [XyzGeodetic].
256    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    /// Convert a [XyzGeocentric] coordinate to [XyzGeodetic]. This function is
265    /// less convenient than [XyzGeocentric::to_geodetic()], but may be better
266    /// in tight loops as the arguments to this function don't need to be
267    /// uselessly re-calculated.
268    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    /// Convert a [XyzGeocentric] coordinate to [XyzGeodetic], using the MWA's
291    /// location.
292    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        // Do everything manually.
340        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        // Do everything automatically.
361        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        // These geodetic XYZ positions are taken from a uvfits made from cotter
374        // for Tile011.
375        let uvfits_xyz = XyzGeodetic {
376            x: 4.56250049e+02,
377            y: -1.49785004e+02,
378            z: 6.80459899e+01,
379        };
380        // These geocentric XYZ positions are taken from a MS made from cotter
381        // for Tile011.
382        let ms_xyz = XyzGeocentric {
383            x: -2559524.23682043,
384            y: 5095846.67363471,
385            z: -2848988.72758185,
386        };
387
388        // Check the conversion of geocentric to geodetic.
389        let result = ms_xyz.to_geodetic_mwa();
390        assert!(result.is_ok());
391        let local_xyz = result.unwrap();
392
393        // cotter's MWA coordinates are a little off of what is in mwalib.
394        // Verify that the transformation isn't quite right.
395        assert_abs_diff_eq!(uvfits_xyz, local_xyz, epsilon = 1e0);
396
397        // Now verify cotter's ms XYZ with the constants it uses.
398        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        // Now check the conversion of geodetic to geocentric.
409        let result = uvfits_xyz.to_geocentric_mwa();
410        assert!(result.is_ok());
411        let geocentric_xyz = result.unwrap();
412        // cotter's MWA coordinates are a little off of what is in mwalib.
413        // Verify that the transformation isn't quite right.
414        assert_abs_diff_eq!(ms_xyz, geocentric_xyz, epsilon = 1e0);
415
416        // Now verify cotter's ms XYZ with the constants it uses.
417        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}