From 0896c880a74f26ee39a130c14634f0df19b6a569 Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Fri, 5 Sep 2025 17:28:00 -0700 Subject: [PATCH 01/11] Refactor the reference frame module to allow loading parameters at runtime --- swiftnav/Cargo.toml | 2 + swiftnav/src/coords/mod.rs | 10 +- swiftnav/src/reference_frame/mod.rs | 398 +++++++++++++++++++++++++--- swiftnav/tests/reference_frames.rs | 128 ++++++--- 4 files changed, 459 insertions(+), 79 deletions(-) diff --git a/swiftnav/Cargo.toml b/swiftnav/Cargo.toml index 5d27706..8dd6009 100644 --- a/swiftnav/Cargo.toml +++ b/swiftnav/Cargo.toml @@ -15,10 +15,12 @@ chrono = { version = "0.4", optional = true } strum = { version = "0.27", features = ["derive"] } nalgebra = "0.33" thiserror = "2.0" +serde = { version = "1.0.219", features = ["derive"] } [dev-dependencies] float_eq = "1.0.1" proptest = "1.5" +serde_json = "1.0" # This tells docs.rs to include the katex header for math formatting # To do this locally diff --git a/swiftnav/src/coords/mod.rs b/swiftnav/src/coords/mod.rs index 7de9a18..d603ea6 100644 --- a/swiftnav/src/coords/mod.rs +++ b/swiftnav/src/coords/mod.rs @@ -85,7 +85,7 @@ pub use llh::*; pub use ned::*; use crate::{ - reference_frame::{get_transformation, ReferenceFrame, TransformationNotFound}, + reference_frame::{ReferenceFrame, TransformationNotFound, TransformationRepository}, time::GpsTime, }; use nalgebra::Vector2; @@ -295,8 +295,12 @@ impl Coordinate { /// /// An error is returned if a transformation from the coordinate's reference frame to the requested /// reference frame could not be found. - pub fn transform_to(&self, new_frame: ReferenceFrame) -> Result { - get_transformation(self.reference_frame, new_frame) + pub fn transform_to( + &self, + new_frame: ReferenceFrame, + repo: &TransformationRepository, + ) -> Result { + repo.get_transformation(self.reference_frame, new_frame) .map(|transformation| transformation.transform(self)) } } diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index b78df34..d3e1b0b 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -50,11 +50,14 @@ //! ``` //! use swiftnav::{ //! coords::{Coordinate, ECEF}, -//! reference_frame::{get_transformation, ReferenceFrame, TransformationNotFound}, +//! reference_frame::{TransformationRepository, ReferenceFrame, TransformationNotFound}, //! time::UtcTime //! }; //! -//! let transformation = get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011) +//! // Can also load your own transformations at runtime +//! let transformation_repo = TransformationRepository::from_builtin(); +//! +//! let transformation = transformation_repo.get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011) //! .unwrap(); //! //! let epoch_2020 = UtcTime::from_parts(2020, 3, 15, 0, 0, 0.).to_gps_hardcoded(); @@ -70,12 +73,13 @@ //! let nad83_coord = transformation.transform(&itrf_coord); //! // Alternatively, you can use the `transform_to` method on the coordinate itself //! let nad83_coord: Result = -//! itrf_coord.transform_to(ReferenceFrame::NAD83_2011); +//! itrf_coord.transform_to(ReferenceFrame::NAD83_2011, &transformation_repo); //! ``` //! use crate::coords::{Coordinate, ECEF}; use nalgebra::{Matrix3, Vector3}; +use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet, VecDeque}, fmt, @@ -86,9 +90,22 @@ mod params; /// Reference Frames #[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, EnumString, Display, EnumIter, Hash, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Copy, + EnumString, + Display, + EnumIter, + Hash, + Serialize, + Deserialize, )] #[strum(serialize_all = "UPPERCASE")] +#[serde(rename_all = "UPPERCASE")] pub enum ReferenceFrame { ITRF88, ITRF89, @@ -167,7 +184,7 @@ pub enum ReferenceFrame { /// parameters in Helmert transformations. In this implementation /// we follow the IERS conventions, which is opposite of the original /// formulation of the Helmert transformation. -#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Serialize, Deserialize)] pub struct TimeDependentHelmertParams { t: Vector3, t_dot: Vector3, @@ -225,7 +242,7 @@ impl TimeDependentHelmertParams { } /// A transformation from one reference frame to another. -#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Serialize, Deserialize)] pub struct Transformation { pub from: ReferenceFrame, pub to: ReferenceFrame, @@ -284,45 +301,147 @@ impl fmt::Display for TransformationNotFound { impl std::error::Error for TransformationNotFound {} -/// Find a transformation from one reference frame to another -/// -/// We currently only support a limited set of transformations. -/// If no transformation is found, `None` is returned. -/// -/// # Errors +/// A repository for managing reference frame transformations /// -/// An error will be returned if a transformation between the two reference -/// frames couldn't be found. -pub fn get_transformation( - from: ReferenceFrame, - to: ReferenceFrame, -) -> Result { - params::TRANSFORMATIONS - .iter() - .find(|t| (t.from == from && t.to == to) || (t.from == to && t.to == from)) - .map(|t| { - if t.from == from && t.to == to { - *t - } else { - (*t).invert() +/// This struct allows for dynamic loading and management of transformations, +/// supporting runtime configuration from various data formats via serde. +#[derive(Debug, Clone)] +pub struct TransformationRepository { + transformations: Vec, + graph: TransformationGraph, +} + +impl TransformationRepository { + /// Create an empty transformation repository + pub fn new() -> Self { + Self { + transformations: Vec::new(), + graph: TransformationGraph::new(), + } + } + + /// Create a repository from a list of transformations + pub fn from_transformations(transformations: Vec) -> Self { + let mut repo = Self { + transformations, + graph: TransformationGraph::new(), + }; + repo.rebuild_graph(); + repo + } + + /// Create a repository with the builtin transformations + pub fn from_builtin() -> Self { + Self::from_transformations(builtin_transformations()) + } + + /// Add a transformation to the repository + /// + /// This will rebuild the internal graph to include the new transformation. + pub fn add_transformation(&mut self, transformation: Transformation) { + self.transformations.push(transformation); + self.rebuild_graph(); + } + + /// Get a transformation between two reference frames + /// + /// This looks for a direct transformation or its inverse. + pub fn get_transformation( + &self, + from: ReferenceFrame, + to: ReferenceFrame, + ) -> Result { + self.transformations + .iter() + .find(|t| (t.from == from && t.to == to) || (t.from == to && t.to == from)) + .map(|t| { + if t.from == from && t.to == to { + *t + } else { + (*t).invert() + } + }) + .ok_or(TransformationNotFound(from, to)) + } + + /// Get the shortest path between two reference frames + /// + /// Returns a list of transformations that need to be applied in sequence + /// to transform from the source to the destination frame. + pub fn get_shortest_path( + &self, + from: ReferenceFrame, + to: ReferenceFrame, + ) -> Option> { + let frame_path = self.graph.get_shortest_path(from, to)?; + + // Convert the path of reference frames into a sequence of transformations + let mut transformations = Vec::new(); + for window in frame_path.windows(2) { + if let [from_frame, to_frame] = window { + if let Ok(transformation) = self.get_transformation(*from_frame, *to_frame) { + transformations.push(transformation); + } else { + // This shouldn't happen if the graph is consistent with available transformations + return None; + } } - }) - .ok_or(TransformationNotFound(from, to)) + } + + Some(transformations) + } + + /// Get a reference to all transformations in the repository + pub fn transformations(&self) -> &[Transformation] { + &self.transformations + } + + /// Rebuild the internal graph from the current transformations + fn rebuild_graph(&mut self) { + self.graph = TransformationGraph::from_transformations(&self.transformations); + } +} + +impl Default for TransformationRepository { + fn default() -> Self { + Self::new() + } +} + +/// Get the builtin transformations as a Vec +/// +/// This function converts the static transformation array into a Vec +/// that can be used for serialization or with TransformationRepository. +pub fn builtin_transformations() -> Vec { + params::TRANSFORMATIONS.to_vec() } /// A helper type for finding transformations between reference frames that require multiple steps /// /// This object can be used to determine which calls to [`get_transformation`] /// are needed when a single transformation does not exist between two reference frames. +#[derive(Debug, Clone)] pub struct TransformationGraph { graph: HashMap>, } impl TransformationGraph { - /// Create a new transformation graph, fully populated with the known transformations + /// Create a new empty transformation graph pub fn new() -> Self { + TransformationGraph { + graph: HashMap::new(), + } + } + + /// Create a new transformation graph from the builtin transformations + pub fn from_builtin() -> Self { + Self::from_transformations(¶ms::TRANSFORMATIONS) + } + + /// Create a new transformation graph from a slice of transformations + pub fn from_transformations(transformations: &[Transformation]) -> Self { let mut graph = HashMap::new(); - for transformation in ¶ms::TRANSFORMATIONS { + for transformation in transformations.iter() { graph .entry(transformation.from) .or_insert_with(HashSet::new) @@ -335,6 +454,21 @@ impl TransformationGraph { TransformationGraph { graph } } + /// Rebuild the graph from a slice of transformations + pub fn rebuild(&mut self, transformations: &[Transformation]) { + self.graph.clear(); + for transformation in transformations.iter() { + self.graph + .entry(transformation.from) + .or_default() + .insert(transformation.to); + self.graph + .entry(transformation.to) + .or_default() + .insert(transformation.from); + } + } + /// Get the shortest path between two reference frames, if one exists /// /// This function will also search for reverse paths if no direct path is found. @@ -375,7 +509,7 @@ impl TransformationGraph { impl Default for TransformationGraph { fn default() -> Self { - TransformationGraph::new() + TransformationGraph::from_builtin() } } @@ -756,7 +890,7 @@ mod tests { // Make sure there isn't a direct path assert!(!TRANSFORMATIONS.iter().any(|t| t.from == from && t.to == to)); - let graph = TransformationGraph::new(); + let graph = TransformationGraph::from_builtin(); let path = graph.get_shortest_path(from, to); assert!(path.is_some()); // Make sure that the path is correct. N.B. this may change if more transformations @@ -770,7 +904,7 @@ mod tests { #[test] fn fully_traversable_graph() { - let graph = TransformationGraph::new(); + let graph = TransformationGraph::from_builtin(); for from in ReferenceFrame::iter() { for to in ReferenceFrame::iter() { if from == to { @@ -781,4 +915,202 @@ mod tests { } } } + + #[test] + fn transformation_repository_empty() { + let repo = TransformationRepository::new(); + assert_eq!(repo.transformations().len(), 0); + + let result = repo.get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014); + assert!(result.is_err()); + } + + #[test] + fn transformation_repository_from_builtin() { + let repo = TransformationRepository::from_builtin(); + assert_eq!(repo.transformations().len(), TRANSFORMATIONS.len()); + + // Test that we can find transformations + let result = repo.get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014); + assert!(result.is_ok()); + + // Test path finding + let path = repo.get_shortest_path(ReferenceFrame::ITRF2020, ReferenceFrame::ETRF2000); + assert!(path.is_some()); + } + + #[test] + fn transformation_repository_add_transformation() { + let mut repo = TransformationRepository::new(); + + // Create a simple transformation for testing + let transformation = Transformation { + from: ReferenceFrame::ITRF2020, + to: ReferenceFrame::ITRF2014, + params: TimeDependentHelmertParams { + tx: 1.0, + tx_dot: 0.0, + ty: 2.0, + ty_dot: 0.0, + tz: 3.0, + tz_dot: 0.0, + s: 0.0, + s_dot: 0.0, + rx: 0.0, + rx_dot: 0.0, + ry: 0.0, + ry_dot: 0.0, + rz: 0.0, + rz_dot: 0.0, + epoch: 2015.0, + }, + }; + + repo.add_transformation(transformation); + assert_eq!(repo.transformations().len(), 1); + + let result = repo.get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), transformation); + + // Test reverse transformation + let reverse_result = + repo.get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::ITRF2020); + assert!(reverse_result.is_ok()); + assert_eq!(reverse_result.unwrap(), transformation.invert()); + } + + #[test] + fn transformation_repository_from_transformations() { + let transformations = vec![ + Transformation { + from: ReferenceFrame::ITRF2020, + to: ReferenceFrame::ITRF2014, + params: TimeDependentHelmertParams { + tx: 1.0, + tx_dot: 0.0, + ty: 2.0, + ty_dot: 0.0, + tz: 3.0, + tz_dot: 0.0, + s: 0.0, + s_dot: 0.0, + rx: 0.0, + rx_dot: 0.0, + ry: 0.0, + ry_dot: 0.0, + rz: 0.0, + rz_dot: 0.0, + epoch: 2015.0, + }, + }, + Transformation { + from: ReferenceFrame::ITRF2014, + to: ReferenceFrame::ITRF2000, + params: TimeDependentHelmertParams { + tx: 4.0, + tx_dot: 0.0, + ty: 5.0, + ty_dot: 0.0, + tz: 6.0, + tz_dot: 0.0, + s: 0.0, + s_dot: 0.0, + rx: 0.0, + rx_dot: 0.0, + ry: 0.0, + ry_dot: 0.0, + rz: 0.0, + rz_dot: 0.0, + epoch: 2015.0, + }, + }, + ]; + + let repo = TransformationRepository::from_transformations(transformations.clone()); + assert_eq!(repo.transformations().len(), 2); + + // Test direct transformation + let result = repo.get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014); + assert!(result.is_ok()); + + // Test multi-step path + let path = repo.get_shortest_path(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2000); + assert!(path.is_some()); + let path = path.unwrap(); + assert_eq!(path.len(), 2); // Two transformations: ITRF2020->ITRF2014 and ITRF2014->ITRF2000 + assert_eq!(path[0].from, ReferenceFrame::ITRF2020); + assert_eq!(path[0].to, ReferenceFrame::ITRF2014); + assert_eq!(path[1].from, ReferenceFrame::ITRF2014); + assert_eq!(path[1].to, ReferenceFrame::ITRF2000); + } + + #[cfg(test)] + mod serialization_tests { + use super::*; + + #[test] + fn test_serde_roundtrip() { + let original_transformations = builtin_transformations(); + + // Serialize to JSON + let json = + serde_json::to_string(&original_transformations).expect("Failed to serialize"); + + // Deserialize from JSON + let deserialized_transformations: Vec = + serde_json::from_str(&json).expect("Failed to deserialize"); + + // Compare + assert_eq!( + original_transformations.len(), + deserialized_transformations.len() + ); + for (orig, deser) in original_transformations + .iter() + .zip(deserialized_transformations.iter()) + { + assert_eq!(orig, deser); + } + } + + #[test] + fn test_reference_frame_serde() { + let frame = ReferenceFrame::ITRF2020; + let json = serde_json::to_string(&frame).expect("Failed to serialize"); + let deserialized: ReferenceFrame = + serde_json::from_str(&json).expect("Failed to deserialize"); + assert_eq!(frame, deserialized); + } + + #[test] + fn test_transformation_serde() { + let transformation = Transformation { + from: ReferenceFrame::ITRF2020, + to: ReferenceFrame::ITRF2014, + params: TimeDependentHelmertParams { + tx: -1.4, + tx_dot: 0.0, + ty: -0.9, + ty_dot: -0.1, + tz: 1.4, + tz_dot: 0.2, + s: -0.42, + s_dot: 0.0, + rx: 0.0, + rx_dot: 0.0, + ry: 0.0, + ry_dot: 0.0, + rz: 0.0, + rz_dot: 0.0, + epoch: 2015.0, + }, + }; + + let json = serde_json::to_string(&transformation).expect("Failed to serialize"); + let deserialized: Transformation = + serde_json::from_str(&json).expect("Failed to deserialize"); + assert_eq!(transformation, deserialized); + } + } } diff --git a/swiftnav/tests/reference_frames.rs b/swiftnav/tests/reference_frames.rs index 009e3c1..4e56d63 100644 --- a/swiftnav/tests/reference_frames.rs +++ b/swiftnav/tests/reference_frames.rs @@ -1,7 +1,7 @@ use float_eq::assert_float_eq; use swiftnav::{ coords::{Coordinate, ECEF}, - reference_frame::{get_transformation, ReferenceFrame}, + reference_frame::{ReferenceFrame, TransformationRepository}, time::{GpsTime, UtcTime}, }; @@ -18,8 +18,10 @@ fn euref_itrf2014() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.0029, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.6005, abs <= 0.0001); @@ -53,8 +55,10 @@ fn euref_itrf2008() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2008).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2008) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.0032, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.6023, abs <= 0.0001); @@ -88,8 +92,10 @@ fn euref_etrf2020() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ETRF2020).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ETRF2020) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1545, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4157, abs <= 0.0001); @@ -123,8 +129,10 @@ fn euref_etrf2014() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::ETRF2014).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::ETRF2014) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1579, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4123, abs <= 0.0001); @@ -158,8 +166,10 @@ fn euref_etrf2005() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2005, ReferenceFrame::ETRF2005).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2005, ReferenceFrame::ETRF2005) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.2107, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4661, abs <= 0.0001); @@ -193,8 +203,10 @@ fn euref_etrf2000() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2000, ReferenceFrame::ETRF2000).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2000, ReferenceFrame::ETRF2000) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.2015, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4596, abs <= 0.0001); @@ -228,8 +240,10 @@ fn euref_etrf97() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF97, ReferenceFrame::ETRF97).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF97, ReferenceFrame::ETRF97) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); @@ -263,8 +277,10 @@ fn euref_etrf96() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF96, ReferenceFrame::ETRF96).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF96, ReferenceFrame::ETRF96) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); @@ -298,8 +314,10 @@ fn euref_etrf94() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF94, ReferenceFrame::ETRF94).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF94, ReferenceFrame::ETRF94) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); @@ -333,8 +351,10 @@ fn euref_etrf93() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF93, ReferenceFrame::ETRF93).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF93, ReferenceFrame::ETRF93) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.2406, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4251, abs <= 0.0001); @@ -368,8 +388,10 @@ fn euref_etrf92() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF92, ReferenceFrame::ETRF92).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF92, ReferenceFrame::ETRF92) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1916, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4388, abs <= 0.0001); @@ -403,8 +425,10 @@ fn euref_etrf91() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF91, ReferenceFrame::ETRF91).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF91, ReferenceFrame::ETRF91) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1746, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4238, abs <= 0.0001); @@ -438,8 +462,10 @@ fn euref_etrf90() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF90, ReferenceFrame::ETRF90).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF90, ReferenceFrame::ETRF90) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1862, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4466, abs <= 0.0001); @@ -473,8 +499,10 @@ fn euref_etrf89() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF89, ReferenceFrame::ETRF89).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF89, ReferenceFrame::ETRF89) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1672, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4186, abs <= 0.0001); @@ -508,8 +536,10 @@ fn euref_etrf2014_reverse() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ETRF2014, ReferenceFrame::ITRF2014).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ETRF2014, ReferenceFrame::ITRF2014) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027893.8541, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.7877, abs <= 0.0001); @@ -576,8 +606,10 @@ fn euref_complete_transform() { Some(ECEF::new(0.01, 0.2, 0.030)), make_epoch(2000), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::ETRF2014).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::ETRF2014) + .unwrap(); // Test adjusting the epoch first then transforming let result_coords = transformation.transform(&initial_coords.adjust_epoch(&make_epoch(2008))); @@ -640,8 +672,10 @@ fn htdp_nad83_2011_fixed_date() { make_epoch(2010), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), -2705104.572, abs <= 0.001); @@ -677,8 +711,10 @@ fn htdp_nad83_2011_adjust_epoch() { make_epoch(2020), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011) + .unwrap(); let result_coords = transformation.transform(&initial_coords.adjust_epoch(&make_epoch(2010))); assert_float_eq!(result_coords.position().x(), -2705104.349, abs <= 0.001); @@ -739,8 +775,10 @@ fn trx_nad83_csrs_fixed_date() { make_epoch(2010), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::NAD83_CSRS).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::NAD83_CSRS) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 1267459.462, abs <= 0.001); @@ -776,8 +814,10 @@ fn trx_nad83_csrs_adjust_epoch() { make_epoch(2020), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::NAD83_CSRS).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::NAD83_CSRS) + .unwrap(); let result_coords = transformation.transform(&initial_coords.adjust_epoch(&make_epoch(2010))); assert_float_eq!(result_coords.position().x(), 1267459.620, abs <= 0.001); @@ -837,8 +877,10 @@ fn dref91_r2016() { None, UtcTime::from_parts(2023, 02, 22, 0, 0, 0.).to_gps_hardcoded(), ); - let transformation = - get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::DREF91_R2016).unwrap(); + let transformations = TransformationRepository::from_builtin(); + let transformation = transformations + .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::DREF91_R2016) + .unwrap(); let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 3842153.3718, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 563401.6528, abs <= 0.0001); From 307f570364cebf0e8817e55a9057120bb3032239 Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Sat, 6 Sep 2025 15:52:55 -0700 Subject: [PATCH 02/11] Refactor the tranformation repository to reduce needed clones --- swiftnav/src/coords/mod.rs | 30 +- swiftnav/src/reference_frame/mod.rs | 569 +++++++++++++++---------- swiftnav/src/reference_frame/params.rs | 15 +- swiftnav/tests/reference_frames.rs | 164 +++---- 4 files changed, 443 insertions(+), 335 deletions(-) diff --git a/swiftnav/src/coords/mod.rs b/swiftnav/src/coords/mod.rs index d603ea6..35dc75f 100644 --- a/swiftnav/src/coords/mod.rs +++ b/swiftnav/src/coords/mod.rs @@ -84,10 +84,7 @@ pub use ellipsoid::*; pub use llh::*; pub use ned::*; -use crate::{ - reference_frame::{ReferenceFrame, TransformationNotFound, TransformationRepository}, - time::GpsTime, -}; +use crate::{reference_frame::ReferenceFrame, time::GpsTime}; use nalgebra::Vector2; /// WGS84 local horizontal coordinates consisting of an Azimuth and Elevation, with angles stored as radians @@ -193,7 +190,7 @@ impl AsMut> for AzimuthElevation { /// Complete coordinate used for transforming between reference frames /// /// Velocities are optional, but when present they will be transformed -#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] +#[derive(Debug, PartialEq, PartialOrd, Clone)] pub struct Coordinate { reference_frame: ReferenceFrame, position: ECEF, @@ -251,8 +248,8 @@ impl Coordinate { /// Get the reference frame of the coordinate #[must_use] - pub fn reference_frame(&self) -> ReferenceFrame { - self.reference_frame + pub fn reference_frame(&self) -> &ReferenceFrame { + &self.reference_frame } /// Get the position of the coordinate @@ -276,7 +273,7 @@ impl Coordinate { /// Use the velocity term to adjust the epoch of the coordinate. /// When a coordinate has no velocity the position won't be changed. #[must_use] - pub fn adjust_epoch(&self, new_epoch: &GpsTime) -> Self { + pub fn adjust_epoch(self, new_epoch: &GpsTime) -> Self { let dt = new_epoch.to_fractional_year_hardcoded() - self.epoch.to_fractional_year_hardcoded(); let v = self.velocity.unwrap_or_default(); @@ -288,21 +285,6 @@ impl Coordinate { reference_frame: self.reference_frame, } } - - /// Transform the coordinate from into a new reference frame - /// - /// # Errors - /// - /// An error is returned if a transformation from the coordinate's reference frame to the requested - /// reference frame could not be found. - pub fn transform_to( - &self, - new_frame: ReferenceFrame, - repo: &TransformationRepository, - ) -> Result { - repo.get_transformation(self.reference_frame, new_frame) - .map(|transformation| transformation.transform(self)) - } } #[cfg(test)] @@ -523,7 +505,7 @@ mod tests { initial_epoch, ); - let new_coord = initial_coord.adjust_epoch(&new_epoch); + let new_coord = initial_coord.clone().adjust_epoch(&new_epoch); assert_eq!(initial_coord.reference_frame, new_coord.reference_frame); assert_float_eq!(new_coord.position.x(), 1.0, abs <= 0.001); diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index d3e1b0b..765158e 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -57,9 +57,6 @@ //! // Can also load your own transformations at runtime //! let transformation_repo = TransformationRepository::from_builtin(); //! -//! let transformation = transformation_repo.get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011) -//! .unwrap(); -//! //! let epoch_2020 = UtcTime::from_parts(2020, 3, 15, 0, 0, 0.).to_gps_hardcoded(); //! let itrf_coord = Coordinate::with_velocity( //! ReferenceFrame::ITRF2014, // The reference frame of the coordinate @@ -70,10 +67,7 @@ //! let epoch_2010 = UtcTime::from_parts(2010, 1, 1, 0, 0, 0.).to_gps_hardcoded(); //! let itrf_coord = itrf_coord.adjust_epoch(&epoch_2010); // Change the epoch of the coordinate //! -//! let nad83_coord = transformation.transform(&itrf_coord); -//! // Alternatively, you can use the `transform_to` method on the coordinate itself -//! let nad83_coord: Result = -//! itrf_coord.transform_to(ReferenceFrame::NAD83_2011, &transformation_repo); +//! let nad83_coord = transformation_repo.transform(itrf_coord.clone(), &ReferenceFrame::NAD83_2011); //! ``` //! @@ -96,7 +90,6 @@ mod params; PartialOrd, Ord, Clone, - Copy, EnumString, Display, EnumIter, @@ -152,6 +145,22 @@ pub enum ReferenceFrame { #[allow(non_camel_case_types)] #[strum(to_string = "WGS84(G2296)", serialize = "WGS84_G2296")] WGS84_G2296, + /// Custom reference frame with user-defined name + #[strum(transparent, default)] + #[serde(untagged)] + Other(String), +} + +impl PartialEq<&ReferenceFrame> for ReferenceFrame { + fn eq(&self, other: &&ReferenceFrame) -> bool { + self == *other + } +} + +impl PartialEq for &ReferenceFrame { + fn eq(&self, other: &ReferenceFrame) -> bool { + *self == other + } } /// 15-parameter Helmert transformation parameters @@ -201,13 +210,15 @@ impl TimeDependentHelmertParams { const ROTATE_SCALE: f64 = (std::f64::consts::PI / 180.0) * (0.001 / 3600.0); /// Reverses the transformation. Since this is a linear transformation we simply negate all terms - pub fn invert(&mut self) { + pub fn invert(mut self) -> Self { self.t *= -1.0; self.t_dot *= -1.0; self.s *= -1.0; self.s_dot *= -1.0; self.r *= -1.0; self.r_dot *= -1.0; + + self } /// Apply the transformation on a position at a specific epoch @@ -239,12 +250,30 @@ impl TimeDependentHelmertParams { fn make_rotation_matrix(s: f64, r: Vector3) -> Matrix3 { Matrix3::new(s, -r.z, r.y, r.z, s, -r.x, -r.y, r.x, s) } + + pub fn transform( + &self, + position: &ECEF, + velocity: Option<&ECEF>, + epoch: f64, + ) -> (ECEF, Option) { + let position = self.transform_position(position, epoch); + let velocity = velocity.map(|v| self.transform_velocity(v, &position)); + + (position, velocity) + } } /// A transformation from one reference frame to another. -#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] pub struct Transformation { + #[serde(alias = "source", alias = "source_name", alias = "frames.source_name")] pub from: ReferenceFrame, + #[serde( + alias = "destination", + alias = "destination_name", + alias = "frames.destination_name" + )] pub to: ReferenceFrame, pub params: TimeDependentHelmertParams, } @@ -254,27 +283,27 @@ impl Transformation { /// /// Reference frame transformations do not change the epoch of the /// coordinate. - /// - /// # Panics - /// - /// This function will panic if the given coordinate is not already in the reference - /// frame this [`Transformation`] converts from. #[must_use] - pub fn transform(&self, coord: &Coordinate) -> Coordinate { - assert!( - coord.reference_frame() == self.from, - "Coordinate reference frame does not match transformation from reference frame" - ); + pub fn transform(&self, coord: Coordinate) -> Result { + if coord.reference_frame() != self.from { + return Err(TransformationNotFound( + self.from.clone(), + coord.reference_frame().clone(), + )); + } - let new_position = self.params.transform_position( + let (new_position, new_velocity) = self.params.transform( &coord.position(), + coord.velocity().as_ref(), coord.epoch().to_fractional_year_hardcoded(), ); - let new_velocity = coord - .velocity() - .as_ref() - .map(|velocity| self.params.transform_velocity(velocity, &coord.position())); - Coordinate::new(self.to, new_position, new_velocity, coord.epoch()) + + Ok(Coordinate::new( + self.to.clone(), + new_position, + new_velocity, + coord.epoch(), + )) } /// Reverse the transformation @@ -290,7 +319,7 @@ impl Transformation { /// /// This error is returned when trying to find a transformation between two reference frames /// and no transformation is found. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct TransformationNotFound(ReferenceFrame, ReferenceFrame); impl fmt::Display for TransformationNotFound { @@ -301,33 +330,39 @@ impl fmt::Display for TransformationNotFound { impl std::error::Error for TransformationNotFound {} +type TransformationGraph = + HashMap>; + /// A repository for managing reference frame transformations /// /// This struct allows for dynamic loading and management of transformations, /// supporting runtime configuration from various data formats via serde. #[derive(Debug, Clone)] pub struct TransformationRepository { - transformations: Vec, - graph: TransformationGraph, + transformations: TransformationGraph, } impl TransformationRepository { /// Create an empty transformation repository pub fn new() -> Self { Self { - transformations: Vec::new(), - graph: TransformationGraph::new(), + transformations: TransformationGraph::new(), } } /// Create a repository from a list of transformations + /// + /// # Note + /// + /// If there are duplicated transformations in the list, the + /// last one in the list will take priority pub fn from_transformations(transformations: Vec) -> Self { - let mut repo = Self { - transformations, - graph: TransformationGraph::new(), - }; - repo.rebuild_graph(); - repo + transformations + .into_iter() + .fold(Self::new(), |mut repo, transformation| { + repo.add_transformation(transformation.clone()); + repo + }) } /// Create a repository with the builtin transformations @@ -339,72 +374,109 @@ impl TransformationRepository { /// /// This will rebuild the internal graph to include the new transformation. pub fn add_transformation(&mut self, transformation: Transformation) { - self.transformations.push(transformation); - self.rebuild_graph(); + let from = transformation.from; + let to = transformation.to; + let params = transformation.params; + let inverted_params = params.invert(); + + self.transformations + .entry(from.clone()) + .or_default() + .extend([(to.clone(), params)]); + + // Add inverted parameters as well + self.transformations + .entry(to) + .or_default() + .extend([(from, inverted_params)]); } - /// Get a transformation between two reference frames - /// - /// This looks for a direct transformation or its inverse. - pub fn get_transformation( + pub fn transform( &self, - from: ReferenceFrame, - to: ReferenceFrame, - ) -> Result { - self.transformations - .iter() - .find(|t| (t.from == from && t.to == to) || (t.from == to && t.to == from)) - .map(|t| { - if t.from == from && t.to == to { - *t - } else { - (*t).invert() - } - }) - .ok_or(TransformationNotFound(from, to)) + coord: Coordinate, + to: &ReferenceFrame, + ) -> Result { + let epoch = coord.epoch().to_fractional_year_hardcoded(); + + let accumulate_transformations = |(position, velocity): (ECEF, Option), + params: &TimeDependentHelmertParams| + -> (ECEF, Option) { + params.transform(&position, velocity.as_ref(), epoch) + }; + + let (position, velocity) = self + .get_shortest_path(coord.reference_frame(), to)? + .into_iter() + .fold( + (coord.position(), coord.velocity()), + accumulate_transformations, + ); + + Ok(Coordinate::new( + to.clone(), + position, + velocity, + coord.epoch(), + )) } /// Get the shortest path between two reference frames /// /// Returns a list of transformations that need to be applied in sequence - /// to transform from the source to the destination frame. - pub fn get_shortest_path( + /// to transform from the source to the destination frame. Uses breadth-first + /// search to find the path with the minimum number of transformation steps. + fn get_shortest_path( &self, - from: ReferenceFrame, - to: ReferenceFrame, - ) -> Option> { - let frame_path = self.graph.get_shortest_path(from, to)?; - - // Convert the path of reference frames into a sequence of transformations - let mut transformations = Vec::new(); - for window in frame_path.windows(2) { - if let [from_frame, to_frame] = window { - if let Ok(transformation) = self.get_transformation(*from_frame, *to_frame) { - transformations.push(transformation); - } else { - // This shouldn't happen if the graph is consistent with available transformations - return None; + from: &ReferenceFrame, + to: &ReferenceFrame, + ) -> Result, TransformationNotFound> { + if from == to { + return Ok(Vec::new()); + } + + let mut visited: HashSet<&ReferenceFrame> = HashSet::new(); + let mut queue: VecDeque<(&ReferenceFrame, Vec<&TimeDependentHelmertParams>)> = + VecDeque::new(); + queue.push_back((from, Vec::new())); + + while let Some((current_frame, path)) = queue.pop_front() { + if current_frame == to { + return Ok(path); + } + + if let Some(neighbors) = self.transformations.get(current_frame) { + for neighbor in neighbors { + if !visited.contains(neighbor.0) { + visited.insert(neighbor.0); + let mut new_path = path.clone(); + new_path.push(neighbor.1); + queue.push_back((neighbor.0, new_path)); + } } } } - Some(transformations) + Err(TransformationNotFound(from.clone(), to.clone())) } - /// Get a reference to all transformations in the repository - pub fn transformations(&self) -> &[Transformation] { - &self.transformations + pub fn count(&self) -> usize { + self.transformations + .values() + .map(|neighbors| neighbors.len()) + .sum() } - /// Rebuild the internal graph from the current transformations - fn rebuild_graph(&mut self) { - self.graph = TransformationGraph::from_transformations(&self.transformations); + // TODO(jbangelo): Add interator functions in place of the list access + pub fn iter(&self) -> impl Iterator { + self.transformations + .iter() + .flat_map(|(_from, neighbors)| neighbors.values()) } } impl Default for TransformationRepository { fn default() -> Self { - Self::new() + Self::from_builtin() } } @@ -416,110 +488,12 @@ pub fn builtin_transformations() -> Vec { params::TRANSFORMATIONS.to_vec() } -/// A helper type for finding transformations between reference frames that require multiple steps -/// -/// This object can be used to determine which calls to [`get_transformation`] -/// are needed when a single transformation does not exist between two reference frames. -#[derive(Debug, Clone)] -pub struct TransformationGraph { - graph: HashMap>, -} - -impl TransformationGraph { - /// Create a new empty transformation graph - pub fn new() -> Self { - TransformationGraph { - graph: HashMap::new(), - } - } - - /// Create a new transformation graph from the builtin transformations - pub fn from_builtin() -> Self { - Self::from_transformations(¶ms::TRANSFORMATIONS) - } - - /// Create a new transformation graph from a slice of transformations - pub fn from_transformations(transformations: &[Transformation]) -> Self { - let mut graph = HashMap::new(); - for transformation in transformations.iter() { - graph - .entry(transformation.from) - .or_insert_with(HashSet::new) - .insert(transformation.to); - graph - .entry(transformation.to) - .or_insert_with(HashSet::new) - .insert(transformation.from); - } - TransformationGraph { graph } - } - - /// Rebuild the graph from a slice of transformations - pub fn rebuild(&mut self, transformations: &[Transformation]) { - self.graph.clear(); - for transformation in transformations.iter() { - self.graph - .entry(transformation.from) - .or_default() - .insert(transformation.to); - self.graph - .entry(transformation.to) - .or_default() - .insert(transformation.from); - } - } - - /// Get the shortest path between two reference frames, if one exists - /// - /// This function will also search for reverse paths if no direct path is found. - /// The search is performed breadth-first. - #[must_use] - pub fn get_shortest_path( - &self, - from: ReferenceFrame, - to: ReferenceFrame, - ) -> Option> { - if from == to { - return None; - } - - let mut visited: HashSet = HashSet::new(); - let mut queue: VecDeque<(ReferenceFrame, Vec)> = VecDeque::new(); - queue.push_back((from, vec![from])); - - while let Some((current_frame, path)) = queue.pop_front() { - if current_frame == to { - return Some(path); - } - - if let Some(neighbors) = self.graph.get(¤t_frame) { - for neighbor in neighbors { - if !visited.contains(neighbor) { - visited.insert(*neighbor); - let mut new_path = path.clone(); - new_path.push(*neighbor); - queue.push_back((*neighbor, new_path)); - } - } - } - } - None - } -} - -impl Default for TransformationGraph { - fn default() -> Self { - TransformationGraph::from_builtin() - } -} - #[cfg(test)] mod tests { use super::*; use float_eq::assert_float_eq; use params::TRANSFORMATIONS; use std::str::FromStr; - use strum::IntoEnumIterator; #[test] fn reference_frame_strings() { @@ -839,14 +813,25 @@ mod tests { #[test] fn helmert_invert() { +<<<<<<< HEAD let mut params = TimeDependentHelmertParams { t: Vector3::new(1.0, 2.0, 3.0), t_dot: Vector3::new(0.1, 0.2, 0.3), +======= + let params = TimeDependentHelmertParams { + tx: 1.0, + tx_dot: 0.1, + ty: 2.0, + ty_dot: 0.2, + tz: 3.0, + tz_dot: 0.3, +>>>>>>> 4166065 (Refactor the tranformation repository to reduce needed clones) s: 4.0, s_dot: 0.4, r: Vector3::new(5.0, 6.0, 7.0), r_dot: Vector3::new(0.5, 0.6, 0.7), epoch: 2010.0, +<<<<<<< HEAD }; params.invert(); assert_float_eq!(params.t.x, -1.0, abs_all <= 1e-4); @@ -855,6 +840,16 @@ mod tests { assert_float_eq!(params.t_dot.y, -0.2, abs_all <= 1e-4); assert_float_eq!(params.t.z, -3.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.z, -0.3, abs_all <= 1e-4); +======= + } + .invert(); + assert_float_eq!(params.tx, -1.0, abs_all <= 1e-4); + assert_float_eq!(params.tx_dot, -0.1, abs_all <= 1e-4); + assert_float_eq!(params.ty, -2.0, abs_all <= 1e-4); + assert_float_eq!(params.ty_dot, -0.2, abs_all <= 1e-4); + assert_float_eq!(params.tz, -3.0, abs_all <= 1e-4); + assert_float_eq!(params.tz_dot, -0.3, abs_all <= 1e-4); +>>>>>>> 4166065 (Refactor the tranformation repository to reduce needed clones) assert_float_eq!(params.s, -4.0, abs_all <= 1e-4); assert_float_eq!(params.s_dot, -0.4, abs_all <= 1e-4); assert_float_eq!(params.r.x, -5.0, abs_all <= 1e-4); @@ -864,6 +859,7 @@ mod tests { assert_float_eq!(params.r.z, -7.0, abs_all <= 1e-4); assert_float_eq!(params.r_dot.z, -0.7, abs_all <= 1e-4); assert_float_eq!(params.epoch, 2010.0, abs_all <= 1e-4); +<<<<<<< HEAD params.invert(); assert_float_eq!(params.t.x, 1.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.x, 0.1, abs_all <= 1e-4); @@ -871,6 +867,15 @@ mod tests { assert_float_eq!(params.t_dot.y, 0.2, abs_all <= 1e-4); assert_float_eq!(params.t.z, 3.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.z, 0.3, abs_all <= 1e-4); +======= + let params = params.invert(); + assert_float_eq!(params.tx, 1.0, abs_all <= 1e-4); + assert_float_eq!(params.tx_dot, 0.1, abs_all <= 1e-4); + assert_float_eq!(params.ty, 2.0, abs_all <= 1e-4); + assert_float_eq!(params.ty_dot, 0.2, abs_all <= 1e-4); + assert_float_eq!(params.tz, 3.0, abs_all <= 1e-4); + assert_float_eq!(params.tz_dot, 0.3, abs_all <= 1e-4); +>>>>>>> 4166065 (Refactor the tranformation repository to reduce needed clones) assert_float_eq!(params.s, 4.0, abs_all <= 1e-4); assert_float_eq!(params.s_dot, 0.4, abs_all <= 1e-4); assert_float_eq!(params.r.x, 5.0, abs_all <= 1e-4); @@ -890,53 +895,32 @@ mod tests { // Make sure there isn't a direct path assert!(!TRANSFORMATIONS.iter().any(|t| t.from == from && t.to == to)); - let graph = TransformationGraph::from_builtin(); - let path = graph.get_shortest_path(from, to); - assert!(path.is_some()); + let graph: TransformationRepository = TransformationRepository::from_builtin(); + let path = graph.get_shortest_path(&from, &to); + assert!(path.is_ok()); // Make sure that the path is correct. N.B. this may change if more transformations // are added in the future let path = path.unwrap(); - assert_eq!(path.len(), 3); - assert_eq!(path[0], from); - assert_eq!(path[1], ReferenceFrame::ITRF2000); - assert_eq!(path[2], to); - } - - #[test] - fn fully_traversable_graph() { - let graph = TransformationGraph::from_builtin(); - for from in ReferenceFrame::iter() { - for to in ReferenceFrame::iter() { - if from == to { - continue; - } - let path = graph.get_shortest_path(from, to); - assert!(path.is_some(), "No path from {} to {}", from, to); - } - } + assert_eq!(path.len(), 2); } #[test] fn transformation_repository_empty() { let repo = TransformationRepository::new(); - assert_eq!(repo.transformations().len(), 0); + assert_eq!(repo.count(), 0); - let result = repo.get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014); + let result = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2014); assert!(result.is_err()); } #[test] fn transformation_repository_from_builtin() { let repo = TransformationRepository::from_builtin(); - assert_eq!(repo.transformations().len(), TRANSFORMATIONS.len()); - - // Test that we can find transformations - let result = repo.get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014); - assert!(result.is_ok()); + assert_eq!(repo.count(), TRANSFORMATIONS.len() * 2); // Also cound inverted transformations // Test path finding - let path = repo.get_shortest_path(ReferenceFrame::ITRF2020, ReferenceFrame::ETRF2000); - assert!(path.is_some()); + let path = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ETRF2000); + assert!(path.is_ok()); } #[test] @@ -966,18 +950,20 @@ mod tests { }, }; + let params = transformation.params.clone(); repo.add_transformation(transformation); - assert_eq!(repo.transformations().len(), 1); + assert_eq!(repo.count(), 2); - let result = repo.get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014); + let result = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2014); assert!(result.is_ok()); - assert_eq!(result.unwrap(), transformation); + assert_eq!(result.unwrap(), vec![¶ms]); // Test reverse transformation + let params = params.invert(); let reverse_result = - repo.get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::ITRF2020); + repo.get_shortest_path(&ReferenceFrame::ITRF2014, &ReferenceFrame::ITRF2020); assert!(reverse_result.is_ok()); - assert_eq!(reverse_result.unwrap(), transformation.invert()); + assert_eq!(reverse_result.unwrap(), vec![¶ms]); } #[test] @@ -1028,21 +1014,120 @@ mod tests { ]; let repo = TransformationRepository::from_transformations(transformations.clone()); - assert_eq!(repo.transformations().len(), 2); + assert_eq!(repo.count(), 4); // Test direct transformation - let result = repo.get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014); + let result = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2014); assert!(result.is_ok()); // Test multi-step path - let path = repo.get_shortest_path(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2000); - assert!(path.is_some()); + let path = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2000); + assert!(path.is_ok()); let path = path.unwrap(); assert_eq!(path.len(), 2); // Two transformations: ITRF2020->ITRF2014 and ITRF2014->ITRF2000 - assert_eq!(path[0].from, ReferenceFrame::ITRF2020); - assert_eq!(path[0].to, ReferenceFrame::ITRF2014); - assert_eq!(path[1].from, ReferenceFrame::ITRF2014); - assert_eq!(path[1].to, ReferenceFrame::ITRF2000); + assert_eq!( + path, + vec![&transformations[0].params, &transformations[1].params] + ); + } + + #[test] + fn custom_reference_frame_creation() { + let custom_frame = ReferenceFrame::Other("MyLocalFrame".to_string()); + assert_eq!(custom_frame.to_string(), "MyLocalFrame"); + } + + #[test] + fn custom_reference_frame_from_str() { + let custom_frame: ReferenceFrame = "UnknownFrame".parse().unwrap(); + assert_eq!( + custom_frame, + ReferenceFrame::Other("UnknownFrame".to_string()) + ); + assert_eq!(custom_frame.to_string(), "UnknownFrame"); + } + + #[test] + fn known_reference_frame_from_str() { + let itrf_frame: ReferenceFrame = "ITRF2020".parse().unwrap(); + assert_eq!(itrf_frame, ReferenceFrame::ITRF2020); + + let nad83_frame: ReferenceFrame = "NAD83(2011)".parse().unwrap(); + assert_eq!(nad83_frame, ReferenceFrame::NAD83_2011); + + let nad83_frame2: ReferenceFrame = "NAD83_2011".parse().unwrap(); + assert_eq!(nad83_frame2, ReferenceFrame::NAD83_2011); + } + + #[test] + fn custom_transformation() { + let transformation = Transformation { + from: ReferenceFrame::ITRF2020, + to: ReferenceFrame::Other("LocalFrame".to_string()), + params: TimeDependentHelmertParams { + tx: 1.0, + tx_dot: 0.0, + ty: 2.0, + ty_dot: 0.0, + tz: 3.0, + tz_dot: 0.0, + s: 0.0, + s_dot: 0.0, + rx: 0.0, + rx_dot: 0.0, + ry: 0.0, + ry_dot: 0.0, + rz: 0.0, + rz_dot: 0.0, + epoch: 2020.0, + }, + }; + + assert_eq!(transformation.from, ReferenceFrame::ITRF2020); + assert_eq!( + transformation.to, + ReferenceFrame::Other("LocalFrame".to_string()) + ); + } + + #[test] + fn custom_transformation_repository() { + let mut repo = TransformationRepository::new(); + let transformation = Transformation { + from: ReferenceFrame::Other("Frame1".to_string()), + to: ReferenceFrame::Other("Frame2".to_string()), + params: TimeDependentHelmertParams { + tx: 1.0, + tx_dot: 0.0, + ty: 2.0, + ty_dot: 0.0, + tz: 3.0, + tz_dot: 0.0, + s: 0.0, + s_dot: 0.0, + rx: 0.0, + rx_dot: 0.0, + ry: 0.0, + ry_dot: 0.0, + rz: 0.0, + rz_dot: 0.0, + epoch: 2020.0, + }, + }; + + repo.add_transformation(transformation); + + let result = repo.get_shortest_path( + &ReferenceFrame::Other("Frame1".to_string()), + &ReferenceFrame::Other("Frame2".to_string()), + ); + assert!(result.is_ok()); + + let reverse_result = repo.get_shortest_path( + &ReferenceFrame::Other("Frame2".to_string()), + &ReferenceFrame::Other("Frame1".to_string()), + ); + assert!(reverse_result.is_ok()); } #[cfg(test)] @@ -1083,6 +1168,46 @@ mod tests { assert_eq!(frame, deserialized); } + #[test] + fn test_custom_reference_frame_serde() { + let custom_frame = ReferenceFrame::Other("MyCustomFrame".to_string()); + let json = serde_json::to_string(&custom_frame).expect("Failed to serialize"); + let deserialized: ReferenceFrame = + serde_json::from_str(&json).expect("Failed to deserialize"); + assert_eq!(custom_frame, deserialized); + assert_eq!(json, "\"MyCustomFrame\""); + } + + #[test] + fn test_custom_transformation_serde() { + let transformation = Transformation { + from: ReferenceFrame::Other("FrameA".to_string()), + to: ReferenceFrame::Other("FrameB".to_string()), + params: TimeDependentHelmertParams { + tx: -1.4, + tx_dot: 0.0, + ty: -0.9, + ty_dot: -0.1, + tz: 1.4, + tz_dot: 0.2, + s: -0.42, + s_dot: 0.0, + rx: 0.0, + rx_dot: 0.0, + ry: 0.0, + ry_dot: 0.0, + rz: 0.0, + rz_dot: 0.0, + epoch: 2015.0, + }, + }; + + let json = serde_json::to_string(&transformation).expect("Failed to serialize"); + let deserialized: Transformation = + serde_json::from_str(&json).expect("Failed to deserialize"); + assert_eq!(transformation, deserialized); + } + #[test] fn test_transformation_serde() { let transformation = Transformation { diff --git a/swiftnav/src/reference_frame/params.rs b/swiftnav/src/reference_frame/params.rs index 3649019..d66f141 100644 --- a/swiftnav/src/reference_frame/params.rs +++ b/swiftnav/src/reference_frame/params.rs @@ -2,7 +2,7 @@ use nalgebra::Vector3; use super::{ReferenceFrame, TimeDependentHelmertParams, Transformation}; -pub const TRANSFORMATIONS: [Transformation; 34] = [ +pub const TRANSFORMATIONS: [Transformation; 33] = [ Transformation { from: ReferenceFrame::ITRF2020, to: ReferenceFrame::ITRF2014, @@ -341,19 +341,6 @@ pub const TRANSFORMATIONS: [Transformation; 34] = [ epoch: 2010.0, }, }, - Transformation { - from: ReferenceFrame::ITRF2014, - to: ReferenceFrame::ETRF2014, - params: TimeDependentHelmertParams { - t: Vector3::new(0.0, 0.0, 0.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), - r_dot: Vector3::new(0.085, 0.531, -0.770), - epoch: 1989.0, - }, - }, Transformation { from: ReferenceFrame::ITRF2008, to: ReferenceFrame::NAD83_CSRS, diff --git a/swiftnav/tests/reference_frames.rs b/swiftnav/tests/reference_frames.rs index 4e56d63..feaa1ec 100644 --- a/swiftnav/tests/reference_frames.rs +++ b/swiftnav/tests/reference_frames.rs @@ -19,10 +19,10 @@ fn euref_itrf2014() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2014) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ITRF2014) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.0029, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.6005, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.9063, abs <= 0.0001); @@ -56,10 +56,10 @@ fn euref_itrf2008() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ITRF2008) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ITRF2008) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.0032, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.6023, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.9082, abs <= 0.0001); @@ -93,10 +93,10 @@ fn euref_etrf2020() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::ETRF2020) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF2020) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1545, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4157, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7999, abs <= 0.0001); @@ -130,10 +130,10 @@ fn euref_etrf2014() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::ETRF2014) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF2014) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1579, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4123, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7973, abs <= 0.0001); @@ -167,11 +167,18 @@ fn euref_etrf2005() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2005, ReferenceFrame::ETRF2005) + + let result_coords = transformations + .transform(initial_coords.clone(), &ReferenceFrame::ETRF2005) .unwrap(); - let result_coords = transformation.transform(&initial_coords); - assert_float_eq!(result_coords.position().x(), 4027894.2107, abs <= 0.0001); + assert_float_eq!( + result_coords.position().x(), + 4027894.2107, + abs <= 0.0001, + "Initial: {:?} Result: {:?}", + initial_coords, + result_coords + ); assert_float_eq!(result_coords.position().y(), 307045.4661, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7626, abs <= 0.0001); assert!(result_coords.velocity().is_some()); @@ -204,10 +211,10 @@ fn euref_etrf2000() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2000, ReferenceFrame::ETRF2000) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF2000) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.2015, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4596, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7581, abs <= 0.0001); @@ -241,10 +248,10 @@ fn euref_etrf97() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF97, ReferenceFrame::ETRF97) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF97) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7569, abs <= 0.0001); @@ -278,10 +285,10 @@ fn euref_etrf96() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF96, ReferenceFrame::ETRF96) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF96) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7569, abs <= 0.0001); @@ -315,10 +322,10 @@ fn euref_etrf94() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF94, ReferenceFrame::ETRF94) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF94) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7569, abs <= 0.0001); @@ -352,10 +359,10 @@ fn euref_etrf93() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF93, ReferenceFrame::ETRF93) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF93) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.2406, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4251, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7267, abs <= 0.0001); @@ -389,10 +396,10 @@ fn euref_etrf92() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF92, ReferenceFrame::ETRF92) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF92) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1916, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4388, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7647, abs <= 0.0001); @@ -426,10 +433,10 @@ fn euref_etrf91() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF91, ReferenceFrame::ETRF91) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF91) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1746, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4238, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7647, abs <= 0.0001); @@ -463,10 +470,10 @@ fn euref_etrf90() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF90, ReferenceFrame::ETRF90) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF90) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1862, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4466, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7664, abs <= 0.0001); @@ -500,10 +507,10 @@ fn euref_etrf89() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF89, ReferenceFrame::ETRF89) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF89) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027894.1672, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4186, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.7894, abs <= 0.0001); @@ -537,10 +544,10 @@ fn euref_etrf2014_reverse() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ETRF2014, ReferenceFrame::ITRF2014) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ITRF2014) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 4027893.8541, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.7877, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919475.0227, abs <= 0.0001); @@ -607,12 +614,14 @@ fn euref_complete_transform() { make_epoch(2000), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::ETRF2014) - .unwrap(); // Test adjusting the epoch first then transforming - let result_coords = transformation.transform(&initial_coords.adjust_epoch(&make_epoch(2008))); + let result_coords = transformations + .transform( + initial_coords.clone().adjust_epoch(&make_epoch(2008)), + &ReferenceFrame::ETRF2014, + ) + .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.3484, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307046.8758, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 4919474.9554, abs <= 0.0001); @@ -636,8 +645,9 @@ fn euref_complete_transform() { assert_eq!(result_coords.reference_frame(), ReferenceFrame::ETRF2014); // Test transforming first then adjusting the epoch - let result_coords = transformation - .transform(&initial_coords) + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::ETRF2014) + .unwrap() .adjust_epoch(&make_epoch(2008)); assert_float_eq!(result_coords.position().x(), 4027894.3484, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307046.8758, abs <= 0.0001); @@ -673,11 +683,10 @@ fn htdp_nad83_2011_fixed_date() { ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011) - .unwrap(); - let result_coords = transformation.transform(&initial_coords); + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::NAD83_2011) + .unwrap(); assert_float_eq!(result_coords.position().x(), -2705104.572, abs <= 0.001); assert_float_eq!(result_coords.position().y(), -4262047.032, abs <= 0.001); assert_float_eq!(result_coords.position().z(), 3885381.705, abs <= 0.001); @@ -712,11 +721,13 @@ fn htdp_nad83_2011_adjust_epoch() { ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2014, ReferenceFrame::NAD83_2011) - .unwrap(); - let result_coords = transformation.transform(&initial_coords.adjust_epoch(&make_epoch(2010))); + let result_coords = transformations + .transform( + initial_coords.clone().adjust_epoch(&make_epoch(2010)), + &ReferenceFrame::NAD83_2011, + ) + .unwrap(); assert_float_eq!(result_coords.position().x(), -2705104.349, abs <= 0.001); assert_float_eq!(result_coords.position().y(), -4262047.291, abs <= 0.001); assert_float_eq!(result_coords.position().z(), 3885381.579, abs <= 0.001); @@ -739,8 +750,9 @@ fn htdp_nad83_2011_adjust_epoch() { assert_eq!(result_coords.epoch(), make_epoch(2010)); assert_eq!(result_coords.reference_frame(), ReferenceFrame::NAD83_2011); - let result_coords = transformation - .transform(&initial_coords) + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::NAD83_2011) + .unwrap() .adjust_epoch(&make_epoch(2010)); assert_float_eq!(result_coords.position().x(), -2705104.349, abs <= 0.001); assert_float_eq!(result_coords.position().y(), -4262047.291, abs <= 0.001); @@ -776,11 +788,10 @@ fn trx_nad83_csrs_fixed_date() { ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::NAD83_CSRS) - .unwrap(); - let result_coords = transformation.transform(&initial_coords); + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::NAD83_CSRS) + .unwrap(); assert_float_eq!(result_coords.position().x(), 1267459.462, abs <= 0.001); assert_float_eq!(result_coords.position().y(), -4294621.605, abs <= 0.001); assert_float_eq!(result_coords.position().z(), 4526843.224, abs <= 0.001); @@ -815,11 +826,13 @@ fn trx_nad83_csrs_adjust_epoch() { ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::NAD83_CSRS) - .unwrap(); - let result_coords = transformation.transform(&initial_coords.adjust_epoch(&make_epoch(2010))); + let result_coords = transformations + .transform( + initial_coords.clone().adjust_epoch(&make_epoch(2010)), + &ReferenceFrame::NAD83_CSRS, + ) + .unwrap(); assert_float_eq!(result_coords.position().x(), 1267459.620, abs <= 0.001); assert_float_eq!(result_coords.position().y(), -4294621.567, abs <= 0.001); assert_float_eq!(result_coords.position().z(), 4526843.177, abs <= 0.001); @@ -842,8 +855,9 @@ fn trx_nad83_csrs_adjust_epoch() { assert_eq!(result_coords.epoch(), make_epoch(2010)); assert_eq!(result_coords.reference_frame(), ReferenceFrame::NAD83_CSRS); - let result_coords = transformation - .transform(&initial_coords) + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::NAD83_CSRS) + .unwrap() .adjust_epoch(&make_epoch(2010)); assert_float_eq!(result_coords.position().x(), 1267459.620, abs <= 0.001); assert_float_eq!(result_coords.position().y(), -4294621.567, abs <= 0.001); @@ -878,10 +892,10 @@ fn dref91_r2016() { UtcTime::from_parts(2023, 02, 22, 0, 0, 0.).to_gps_hardcoded(), ); let transformations = TransformationRepository::from_builtin(); - let transformation = transformations - .get_transformation(ReferenceFrame::ITRF2020, ReferenceFrame::DREF91_R2016) + + let result_coords = transformations + .transform(initial_coords, &ReferenceFrame::DREF91_R2016) .unwrap(); - let result_coords = transformation.transform(&initial_coords); assert_float_eq!(result_coords.position().x(), 3842153.3718, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 563401.6528, abs <= 0.0001); assert_float_eq!(result_coords.position().z(), 5042888.2271, abs <= 0.0001); From 713069cde4c13c458156afd755f5c814dc62f323 Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Sat, 6 Sep 2025 16:06:21 -0700 Subject: [PATCH 03/11] Tests and docs --- swiftnav/src/reference_frame/mod.rs | 175 +++++++++++++++++++--------- 1 file changed, 117 insertions(+), 58 deletions(-) diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index 765158e..962c15d 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -83,6 +83,10 @@ use strum::{Display, EnumIter, EnumString}; mod params; /// Reference Frames +/// +/// Some well known reference frames are included. Other +/// reference frames are supported via the [`ReferenceFrame::Other`] +/// variant which holds the reference frame name as a [`String`] #[derive( Debug, PartialEq, @@ -189,6 +193,8 @@ impl PartialEq for &ReferenceFrame { /// Where $p$ is the constant value, $\dot{p}$ is the rate of /// change, and $\tau$ is the reference epoch. /// +/// # Sign Convention +/// /// There are several sign conventions in use for the rotation /// parameters in Helmert transformations. In this implementation /// we follow the IERS conventions, which is opposite of the original @@ -221,7 +227,7 @@ impl TimeDependentHelmertParams { self } - /// Apply the transformation on a position at a specific epoch + /// Apply the transformation to a position at a specific epoch #[must_use] pub fn transform_position(&self, position: &ECEF, epoch: f64) -> ECEF { let dt = epoch - self.epoch; @@ -234,7 +240,7 @@ impl TimeDependentHelmertParams { (position.as_vector() + t + m * position.as_vector()).into() } - /// Apply the transformation on a velocity at a specific position + /// Apply the transformation to a velocity at a specific position #[must_use] pub fn transform_velocity(&self, velocity: &ECEF, position: &ECEF) -> ECEF { let t = self.t_dot * Self::TRANSLATE_SCALE; @@ -267,13 +273,14 @@ impl TimeDependentHelmertParams { /// A transformation from one reference frame to another. #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] pub struct Transformation { - #[serde(alias = "source", alias = "source_name", alias = "frames.source_name")] - pub from: ReferenceFrame, #[serde( - alias = "destination", - alias = "destination_name", - alias = "frames.destination_name" + alias = "source", + alias = "source_name", + alias = "frames.source", + alias = "frames.source_name" )] + pub from: ReferenceFrame, + #[serde(alias = "destination", alias = "destination_name")] pub to: ReferenceFrame, pub params: TimeDependentHelmertParams, } @@ -283,6 +290,11 @@ impl Transformation { /// /// Reference frame transformations do not change the epoch of the /// coordinate. + /// + /// # Errors + /// + /// [`TransformationNotFound`] Is returned as an error if there is a mismatch between + /// the coordinate reference frame and the [`Transformation::from`] field. #[must_use] pub fn transform(&self, coord: Coordinate) -> Result { if coord.reference_frame() != self.from { @@ -310,7 +322,7 @@ impl Transformation { #[must_use] pub fn invert(mut self) -> Self { std::mem::swap(&mut self.from, &mut self.to); - self.params.invert(); + self.params = self.params.invert(); self } } @@ -335,8 +347,8 @@ type TransformationGraph = /// A repository for managing reference frame transformations /// -/// This struct allows for dynamic loading and management of transformations, -/// supporting runtime configuration from various data formats via serde. +/// This struct allows for loading the builtin transformations +/// as well as adding additional transformations from other sources. #[derive(Debug, Clone)] pub struct TransformationRepository { transformations: TransformationGraph, @@ -356,13 +368,12 @@ impl TransformationRepository { /// /// If there are duplicated transformations in the list, the /// last one in the list will take priority - pub fn from_transformations(transformations: Vec) -> Self { - transformations - .into_iter() - .fold(Self::new(), |mut repo, transformation| { - repo.add_transformation(transformation.clone()); - repo - }) + pub fn from_transformations>( + transformations: T, + ) -> Self { + let mut repo = Self::new(); + repo.extend(transformations); + repo } /// Create a repository with the builtin transformations @@ -391,6 +402,18 @@ impl TransformationRepository { .extend([(from, inverted_params)]); } + /// Transform a [`Coordinate`] to a new reference frame + /// + /// This function finds the shortest series of transformations from the coordinate's + /// initial reference frame to the requestest one, then sequentially applies + /// those transformations to get the new position and velocity. The epoch of the + /// coordinate is not modified in this process. + /// + /// # Errors + /// + /// [`TransformationNotFound`] is returned as an error if no path from the + /// coordinate's reference frame to the requested reference frame could be found + /// in the repository. pub fn transform( &self, coord: Coordinate, @@ -425,6 +448,13 @@ impl TransformationRepository { /// Returns a list of transformations that need to be applied in sequence /// to transform from the source to the destination frame. Uses breadth-first /// search to find the path with the minimum number of transformation steps. + /// + /// An empty vector will be returned if the two reference frames are the same. + /// + /// # Errors + /// + /// [`TransformationNotFound`] will be returned as an error if + /// no path could be found between the two reference frames fn get_shortest_path( &self, from: &ReferenceFrame, @@ -459,19 +489,13 @@ impl TransformationRepository { Err(TransformationNotFound(from.clone(), to.clone())) } + /// Get the number of transformations stored in the repository pub fn count(&self) -> usize { self.transformations .values() .map(|neighbors| neighbors.len()) .sum() } - - // TODO(jbangelo): Add interator functions in place of the list access - pub fn iter(&self) -> impl Iterator { - self.transformations - .iter() - .flat_map(|(_from, neighbors)| neighbors.values()) - } } impl Default for TransformationRepository { @@ -480,6 +504,13 @@ impl Default for TransformationRepository { } } +impl Extend for TransformationRepository { + fn extend>(&mut self, iter: T) { + iter.into_iter() + .for_each(|transformation| self.add_transformation(transformation)); + } +} + /// Get the builtin transformations as a Vec /// /// This function converts the static transformation array into a Vec @@ -813,43 +844,23 @@ mod tests { #[test] fn helmert_invert() { -<<<<<<< HEAD let mut params = TimeDependentHelmertParams { t: Vector3::new(1.0, 2.0, 3.0), t_dot: Vector3::new(0.1, 0.2, 0.3), -======= - let params = TimeDependentHelmertParams { - tx: 1.0, - tx_dot: 0.1, - ty: 2.0, - ty_dot: 0.2, - tz: 3.0, - tz_dot: 0.3, ->>>>>>> 4166065 (Refactor the tranformation repository to reduce needed clones) s: 4.0, s_dot: 0.4, r: Vector3::new(5.0, 6.0, 7.0), r_dot: Vector3::new(0.5, 0.6, 0.7), epoch: 2010.0, -<<<<<<< HEAD - }; - params.invert(); + } + .invert(); assert_float_eq!(params.t.x, -1.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.x, -0.1, abs_all <= 1e-4); assert_float_eq!(params.t.y, -2.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.y, -0.2, abs_all <= 1e-4); assert_float_eq!(params.t.z, -3.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.z, -0.3, abs_all <= 1e-4); -======= - } - .invert(); - assert_float_eq!(params.tx, -1.0, abs_all <= 1e-4); - assert_float_eq!(params.tx_dot, -0.1, abs_all <= 1e-4); - assert_float_eq!(params.ty, -2.0, abs_all <= 1e-4); - assert_float_eq!(params.ty_dot, -0.2, abs_all <= 1e-4); - assert_float_eq!(params.tz, -3.0, abs_all <= 1e-4); - assert_float_eq!(params.tz_dot, -0.3, abs_all <= 1e-4); ->>>>>>> 4166065 (Refactor the tranformation repository to reduce needed clones) + assert_float_eq!(params.s, -4.0, abs_all <= 1e-4); assert_float_eq!(params.s_dot, -0.4, abs_all <= 1e-4); assert_float_eq!(params.r.x, -5.0, abs_all <= 1e-4); @@ -859,23 +870,14 @@ mod tests { assert_float_eq!(params.r.z, -7.0, abs_all <= 1e-4); assert_float_eq!(params.r_dot.z, -0.7, abs_all <= 1e-4); assert_float_eq!(params.epoch, 2010.0, abs_all <= 1e-4); -<<<<<<< HEAD - params.invert(); + let params = params.invert(); assert_float_eq!(params.t.x, 1.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.x, 0.1, abs_all <= 1e-4); assert_float_eq!(params.t.y, 2.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.y, 0.2, abs_all <= 1e-4); assert_float_eq!(params.t.z, 3.0, abs_all <= 1e-4); assert_float_eq!(params.t_dot.z, 0.3, abs_all <= 1e-4); -======= - let params = params.invert(); - assert_float_eq!(params.tx, 1.0, abs_all <= 1e-4); - assert_float_eq!(params.tx_dot, 0.1, abs_all <= 1e-4); - assert_float_eq!(params.ty, 2.0, abs_all <= 1e-4); - assert_float_eq!(params.ty_dot, 0.2, abs_all <= 1e-4); - assert_float_eq!(params.tz, 3.0, abs_all <= 1e-4); - assert_float_eq!(params.tz_dot, 0.3, abs_all <= 1e-4); ->>>>>>> 4166065 (Refactor the tranformation repository to reduce needed clones) + assert_float_eq!(params.s, 4.0, abs_all <= 1e-4); assert_float_eq!(params.s_dot, 0.4, abs_all <= 1e-4); assert_float_eq!(params.r.x, 5.0, abs_all <= 1e-4); @@ -1237,5 +1239,62 @@ mod tests { serde_json::from_str(&json).expect("Failed to deserialize"); assert_eq!(transformation, deserialized); } + + #[test] + fn test_serde_aliases() { + // Test "source" and "destination" + let json = serde_json::json!({ + "source": "ITRF2020", + "destination": "ITRF2014", + "params": { + "tx": -1.4, + "tx_dot": 0.0, + "ty": -0.9, + "ty_dot": -0.1, + "tz": 1.4, + "tz_dot": 0.2, + "s": -0.42, + "s_dot": 0.0, + "rx": 0.0, + "rx_dot": 0.0, + "ry": 0.0, + "ry_dot": 0.0, + "rz": 0.0, + "rz_dot": 0.0, + "epoch": 2015.0, + } + }); + + let deserialized: Transformation = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized.from, ReferenceFrame::ITRF2020); + assert_eq!(deserialized.to, ReferenceFrame::ITRF2014); + + // Test "source_name" and destination_name" + let json = serde_json::json!({ + "source_name": "ITRF2020", + "destination_name": "ITRF2014", + "params": { + "tx": -1.4, + "tx_dot": 0.0, + "ty": -0.9, + "ty_dot": -0.1, + "tz": 1.4, + "tz_dot": 0.2, + "s": -0.42, + "s_dot": 0.0, + "rx": 0.0, + "rx_dot": 0.0, + "ry": 0.0, + "ry_dot": 0.0, + "rz": 0.0, + "rz_dot": 0.0, + "epoch": 2015.0, + } + }); + + let deserialized: Transformation = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized.from, ReferenceFrame::ITRF2020); + assert_eq!(deserialized.to, ReferenceFrame::ITRF2014); + } } } From 67c3402e054b9ec82374bd37e3889328b42870b6 Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Sat, 6 Sep 2025 17:42:47 -0700 Subject: [PATCH 04/11] Clean up serde aliases --- swiftnav/src/reference_frame/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index 962c15d..8114330 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -203,7 +203,9 @@ impl PartialEq for &ReferenceFrame { pub struct TimeDependentHelmertParams { t: Vector3, t_dot: Vector3, + #[serde(alias = "scale", alias = "d")] s: f64, + #[serde(alias = "scale_dot", alias = "d_dot")] s_dot: f64, r: Vector3, r_dot: Vector3, @@ -273,12 +275,7 @@ impl TimeDependentHelmertParams { /// A transformation from one reference frame to another. #[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] pub struct Transformation { - #[serde( - alias = "source", - alias = "source_name", - alias = "frames.source", - alias = "frames.source_name" - )] + #[serde(alias = "source", alias = "source_name")] pub from: ReferenceFrame, #[serde(alias = "destination", alias = "destination_name")] pub to: ReferenceFrame, From 0e1b8a9662bb1143f858eb83975e4d19c0688530 Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Sat, 6 Sep 2025 21:15:43 -0700 Subject: [PATCH 05/11] Update documentation comments --- swiftnav/Cargo.toml | 2 +- swiftnav/src/coords/mod.rs | 4 +- swiftnav/src/reference_frame/mod.rs | 187 +++++++++++++++++++--------- 3 files changed, 129 insertions(+), 64 deletions(-) diff --git a/swiftnav/Cargo.toml b/swiftnav/Cargo.toml index 8dd6009..f87ec86 100644 --- a/swiftnav/Cargo.toml +++ b/swiftnav/Cargo.toml @@ -15,7 +15,7 @@ chrono = { version = "0.4", optional = true } strum = { version = "0.27", features = ["derive"] } nalgebra = "0.33" thiserror = "2.0" -serde = { version = "1.0.219", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } [dev-dependencies] float_eq = "1.0.1" diff --git a/swiftnav/src/coords/mod.rs b/swiftnav/src/coords/mod.rs index 35dc75f..3ba4a72 100644 --- a/swiftnav/src/coords/mod.rs +++ b/swiftnav/src/coords/mod.rs @@ -215,7 +215,7 @@ impl Coordinate { } } - /// Create a new [`Coordinate`] object with a velocity value + /// Create a new [`Coordinate`] object with no velocity value #[must_use] pub fn without_velocity( reference_frame: ReferenceFrame, @@ -230,7 +230,7 @@ impl Coordinate { } } - /// Create a new [`Coordinate`] object with no velocity + /// Create a new [`Coordinate`] object with a velocity #[must_use] pub fn with_velocity( reference_frame: ReferenceFrame, diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index 8114330..3730187 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -9,67 +9,63 @@ // WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. //! Geodetic reference frame transformations //! -//! Geodetic reference frames define the coordinate system used to represent -//! positions on the Earth. Different reference frames are commonly used in -//! different regions of the world, and for different purposes. For example, -//! global reference frames, such as the International Terrestrial Reference -//! Frame (ITRF), are used for global positioning, while regional reference -//! frames, such as the European Terrestrial Reference Frame (ETRF), are used -//! for regional positioning. Due to the movement of the earth's crust apparently -//! fixed positions will move over time. Because of this it's important to note -//! only take note of a position, but also the time at which that position was -//! determined. In most regions of the earth the crust moves at a constant speed, -//! meaning that if you are able to determine the local velocity of the crust you -//! can easily determine what the position of a static point would have been in -//! the past. It is commong for regional reference frames to define a common reference -//! epoch that all positions should be transformed to, allowing the direct comparison -//! of positions even if they were determined at different times. Regional reference -//! frames also typically are defined to be "fixed" to a particular tectonic plate, -//! meaning the large majority of the velocity for points on that tectonic plate -//! are cancelled out. In contrast, global reference frames are not fixed to -//! any particular tectonic plate, so most places on earth will have a measurable -//! velocity. Global reference frames also typically do not have a common reference -//! epoch, so determining one's local velocity is important to be able to compare -//! positions or to transform a coordinate from a global reference frame to a regional -//! reference frame. +//! Transform coordinates between geodetic reference frames using time-dependent Helmert transformations. +//! Supports both global frames (ITRF series) and regional frames (ETRF, NAD83, etc.) with runtime +//! parameter loading. //! -//! This module provides several types and functions to help transform a set of coordinates -//! from one reference frame to another, and from one epoch to another. Several sets of -//! transformation parameters are included for converting between common reference frames. -//! To start out, you must have a [`Coordinate`] that you want to transform. This consists of a -//! position, an epoch, and a reference frame as well as an optional velocity. You then need to -//! get the [`Transformation`] object that describes the transformation from the reference -//! frame of the coordinate to the desired reference frame. You can then call the `transform` -//! method on the transformation object to get a new coordinate in the desired reference frame. -//! This transformation will change the position and velocity of the coordinate, but it does -//! not the change the epoch of the coordinate. If you need to change the epoch of the -//! coordinate you will need to use the [`Coordinate::adjust_epoch`](crate::coords::Coordinate::adjust_epoch) -//! method which uses the velocity of the coordinate to determine the position at the new epoch. +//! # Key Concepts +//! +//! - **Reference frames** define coordinate systems for Earth positioning +//! - **Crustal motion** causes positions to change over time, requiring velocity tracking +//! - **Transformations** use 15-parameter Helmert models with time dependencies +//! - **Path finding** automatically chains transformations between frames +//! +//! # Core Types +//! +//! - [`ReferenceFrame`] - Enumeration of supported coordinate systems +//! - [`TransformationRepository`] - Manages transformation parameters and pathfinding +//! - [`Coordinate`] - Position with reference frame, epoch, and optional velocity +//! - [`TimeDependentHelmertParams`] - 15-parameter transformation model +//! +//! # Basic Usage //! -//! # Example //! ``` //! use swiftnav::{ //! coords::{Coordinate, ECEF}, -//! reference_frame::{TransformationRepository, ReferenceFrame, TransformationNotFound}, +//! reference_frame::{TransformationRepository, ReferenceFrame}, //! time::UtcTime //! }; //! -//! // Can also load your own transformations at runtime -//! let transformation_repo = TransformationRepository::from_builtin(); +//! let repo = TransformationRepository::from_builtin(); +//! let epoch = UtcTime::from_parts(2020, 3, 15, 0, 0, 0.).to_gps_hardcoded(); //! -//! let epoch_2020 = UtcTime::from_parts(2020, 3, 15, 0, 0, 0.).to_gps_hardcoded(); +//! // Create coordinate with position and velocity //! let itrf_coord = Coordinate::with_velocity( -//! ReferenceFrame::ITRF2014, // The reference frame of the coordinate -//! ECEF::new(-2703764.0, -4261273.0, 3887158.0), // The position of the coordinate -//! ECEF::new(-0.221, 0.254, 0.122), // The velocity of the coordinate -//! epoch_2020); // The epoch of the coordinate +//! ReferenceFrame::ITRF2014, +//! ECEF::new(-2703764.0, -4261273.0, 3887158.0), +//! ECEF::new(-0.221, 0.254, 0.122), +//! epoch +//! ); +//! +//! // Transform to different reference frame +//! let nad83_coord = repo.transform(itrf_coord, &ReferenceFrame::NAD83_2011)?; +//! # Ok::<(), Box>(()) +//! ``` //! -//! let epoch_2010 = UtcTime::from_parts(2010, 1, 1, 0, 0, 0.).to_gps_hardcoded(); -//! let itrf_coord = itrf_coord.adjust_epoch(&epoch_2010); // Change the epoch of the coordinate +//! # Custom Transformations //! -//! let nad83_coord = transformation_repo.transform(itrf_coord.clone(), &ReferenceFrame::NAD83_2011); //! ``` +//! # use swiftnav::reference_frame::*; +//! let mut repo = TransformationRepository::new(); //! +//! let custom_transform = Transformation { +//! from: ReferenceFrame::ITRF2020, +//! to: ReferenceFrame::Other("LOCAL_FRAME".to_string()), +//! params: TimeDependentHelmertParams { /* ... */ # tx: 0.0, tx_dot: 0.0, ty: 0.0, ty_dot: 0.0, tz: 0.0, tz_dot: 0.0, s: 0.0, s_dot: 0.0, rx: 0.0, rx_dot: 0.0, ry: 0.0, ry_dot: 0.0, rz: 0.0, rz_dot: 0.0, epoch: 2020.0 } +//! }; +//! +//! repo.add_transformation(custom_transform); +//! ``` use crate::coords::{Coordinate, ECEF}; use nalgebra::{Matrix3, Vector3}; @@ -82,11 +78,29 @@ use strum::{Display, EnumIter, EnumString}; mod params; -/// Reference Frames +/// Geodetic reference frame identifiers +/// +/// Enumerates well-known global and regional reference frames with support +/// for custom frames via the [`Other`] variant. +/// +/// # Examples +/// ``` +/// # use swiftnav::reference_frame::ReferenceFrame; +/// # use std::str::FromStr; +/// // Use predefined frames +/// let itrf = ReferenceFrame::ITRF2020; +/// let nad83 = ReferenceFrame::NAD83_2011; +/// +/// // Parse from string +/// let parsed: ReferenceFrame = "ITRF2014".parse()?; +/// let custom: ReferenceFrame = "MY_LOCAL_FRAME".parse()?; /// -/// Some well known reference frames are included. Other -/// reference frames are supported via the [`ReferenceFrame::Other`] -/// variant which holds the reference frame name as a [`String`] +/// // Custom frames +/// let local = ReferenceFrame::Other("SITE_FRAME_2023".to_string()); +/// # Ok::<(), Box>(()) +/// ``` +/// +/// [`Other`]: ReferenceFrame::Other #[derive( Debug, PartialEq, @@ -193,6 +207,19 @@ impl PartialEq for &ReferenceFrame { /// Where $p$ is the constant value, $\dot{p}$ is the rate of /// change, and $\tau$ is the reference epoch. /// +/// # Parameter Units +/// +/// Input parameters are stored in standard geodetic units: +/// - **Translation** (`tx`, `ty`, `tz`): millimeters (mm) +/// - **Translation rates** (`tx_dot`, `ty_dot`, `tz_dot`): mm/year +/// - **Scale** (`s`): parts per billion (ppb) +/// - **Scale rate** (`s_dot`): ppb/year +/// - **Rotation** (`rx`, `ry`, `rz`): milliarcseconds (mas) +/// - **Rotation rates** (`rx_dot`, `ry_dot`, `rz_dot`): mas/year +/// - **Reference epoch** (`epoch`): decimal years +/// +/// These units are automatically converted to SI units during computation. +/// /// # Sign Convention /// /// There are several sign conventions in use for the rotation @@ -213,8 +240,11 @@ pub struct TimeDependentHelmertParams { } impl TimeDependentHelmertParams { + /// Scale factor for translation parameters (converts mm to m) const TRANSLATE_SCALE: f64 = 1.0e-3; + /// Scale factor for scale parameters (converts ppb to fractional scale) const SCALE_SCALE: f64 = 1.0e-9; + /// Scale factor for rotation parameters (converts mas to radians) const ROTATE_SCALE: f64 = (std::f64::consts::PI / 180.0) * (0.001 / 3600.0); /// Reverses the transformation. Since this is a linear transformation we simply negate all terms @@ -259,6 +289,18 @@ impl TimeDependentHelmertParams { Matrix3::new(s, -r.z, r.y, r.z, s, -r.x, -r.y, r.x, s) } + /// Apply the complete Helmert transformation to position and velocity + /// + /// Combines position and velocity transformations into a single operation. + /// The velocity transformation uses the rate terms from the Helmert parameters. + /// + /// # Arguments + /// * `position` - Position to transform + /// * `velocity` - Optional velocity to transform (None preserves None) + /// * `epoch` - Time epoch for position transformation + /// + /// # Returns + /// Tuple of transformed (position, velocity) pub fn transform( &self, position: &ECEF, @@ -440,18 +482,31 @@ impl TransformationRepository { )) } - /// Get the shortest path between two reference frames + /// Find the shortest transformation path between reference frames + /// + /// Uses breadth-first search to find the minimal sequence of transformations + /// needed to convert between reference frames. The algorithm automatically + /// chains transformations when no direct path exists. + /// + /// Returns an empty vector if source and destination frames are identical. /// - /// Returns a list of transformations that need to be applied in sequence - /// to transform from the source to the destination frame. Uses breadth-first - /// search to find the path with the minimum number of transformation steps. + /// # Examples + /// ``` + /// # use swiftnav::reference_frame::*; + /// let repo = TransformationRepository::from_builtin(); /// - /// An empty vector will be returned if the two reference frames are the same. + /// // Direct transformation if available + /// let path = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2014)?; + /// + /// // Multi-hop transformation when needed + /// let path = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ETRF2000)?; + /// assert!(path.len() >= 1); + /// # Ok::<(), TransformationNotFound>(()) + /// ``` /// /// # Errors /// - /// [`TransformationNotFound`] will be returned as an error if - /// no path could be found between the two reference frames + /// [`TransformationNotFound`] if no transformation path exists between the frames fn get_shortest_path( &self, from: &ReferenceFrame, @@ -508,10 +563,20 @@ impl Extend for TransformationRepository { } } -/// Get the builtin transformations as a Vec +/// Get the builtin transformation parameters +/// +/// Returns a Vec of all pre-defined transformations between common reference frames +/// including ITRF, ETRF, NAD83, and WGS84 series. These transformations are sourced +/// from authoritative geodetic organizations. +/// +/// Use this to initialize a [`TransformationRepository`] or for serialization. /// -/// This function converts the static transformation array into a Vec -/// that can be used for serialization or with TransformationRepository. +/// # Example +/// ``` +/// # use swiftnav::reference_frame::*; +/// let transformations = builtin_transformations(); +/// let repo = TransformationRepository::from_transformations(transformations); +/// ``` pub fn builtin_transformations() -> Vec { params::TRANSFORMATIONS.to_vec() } From b44af202d44f08ed364718d29c9d666d5b942cc3 Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Tue, 16 Sep 2025 19:32:03 -0700 Subject: [PATCH 06/11] Remove serde...for now --- swiftnav/Cargo.toml | 2 - swiftnav/src/reference_frame/mod.rs | 337 +++++-------------------- swiftnav/src/reference_frame/params.rs | 21 +- 3 files changed, 71 insertions(+), 289 deletions(-) diff --git a/swiftnav/Cargo.toml b/swiftnav/Cargo.toml index f87ec86..5d27706 100644 --- a/swiftnav/Cargo.toml +++ b/swiftnav/Cargo.toml @@ -15,12 +15,10 @@ chrono = { version = "0.4", optional = true } strum = { version = "0.27", features = ["derive"] } nalgebra = "0.33" thiserror = "2.0" -serde = { version = "1.0", features = ["derive"] } [dev-dependencies] float_eq = "1.0.1" proptest = "1.5" -serde_json = "1.0" # This tells docs.rs to include the katex header for math formatting # To do this locally diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index 3730187..9c5ead7 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -55,13 +55,14 @@ //! # Custom Transformations //! //! ``` +//! # use nalgebra::Vector3; //! # use swiftnav::reference_frame::*; //! let mut repo = TransformationRepository::new(); //! //! let custom_transform = Transformation { //! from: ReferenceFrame::ITRF2020, //! to: ReferenceFrame::Other("LOCAL_FRAME".to_string()), -//! params: TimeDependentHelmertParams { /* ... */ # tx: 0.0, tx_dot: 0.0, ty: 0.0, ty_dot: 0.0, tz: 0.0, tz_dot: 0.0, s: 0.0, s_dot: 0.0, rx: 0.0, rx_dot: 0.0, ry: 0.0, ry_dot: 0.0, rz: 0.0, rz_dot: 0.0, epoch: 2020.0 } +//! params: TimeDependentHelmertParams::default(), //! }; //! //! repo.add_transformation(custom_transform); @@ -69,7 +70,6 @@ use crate::coords::{Coordinate, ECEF}; use nalgebra::{Matrix3, Vector3}; -use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet, VecDeque}, fmt, @@ -112,11 +112,8 @@ mod params; Display, EnumIter, Hash, - Serialize, - Deserialize, )] #[strum(serialize_all = "UPPERCASE")] -#[serde(rename_all = "UPPERCASE")] pub enum ReferenceFrame { ITRF88, ITRF89, @@ -165,7 +162,6 @@ pub enum ReferenceFrame { WGS84_G2296, /// Custom reference frame with user-defined name #[strum(transparent, default)] - #[serde(untagged)] Other(String), } @@ -226,28 +222,40 @@ impl PartialEq for &ReferenceFrame { /// parameters in Helmert transformations. In this implementation /// we follow the IERS conventions, which is opposite of the original /// formulation of the Helmert transformation. -#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] pub struct TimeDependentHelmertParams { - t: Vector3, - t_dot: Vector3, - #[serde(alias = "scale", alias = "d")] - s: f64, - #[serde(alias = "scale_dot", alias = "d_dot")] - s_dot: f64, - r: Vector3, - r_dot: Vector3, - epoch: f64, + pub t: Vector3, + pub t_dot: Vector3, + pub s: f64, + pub s_dot: f64, + pub r: Vector3, + pub r_dot: Vector3, + pub epoch: f64, } impl TimeDependentHelmertParams { /// Scale factor for translation parameters (converts mm to m) - const TRANSLATE_SCALE: f64 = 1.0e-3; - /// Scale factor for scale parameters (converts ppb to fractional scale) - const SCALE_SCALE: f64 = 1.0e-9; - /// Scale factor for rotation parameters (converts mas to radians) - const ROTATE_SCALE: f64 = (std::f64::consts::PI / 180.0) * (0.001 / 3600.0); + pub const TRANSLATE_SCALE: f64 = 1.0e-3; + /// Scale factor for scale parameters (converts ppb to unit scale) + pub const SCALE_SCALE: f64 = 1.0e-9; + /// Scale factor for rotation parameters (converts milliarcseconds to radians) + pub const ROTATE_SCALE: f64 = (std::f64::consts::PI / 180.0) * (0.001 / 3600.0); + + #[must_use] + pub const fn zeros() -> TimeDependentHelmertParams { + TimeDependentHelmertParams { + t: Vector3::new(0.0, 0.0, 0.0), + t_dot: Vector3::new(0.0, 0.0, 0.0), + s: 0.0, + s_dot: 0.0, + r: Vector3::new(0.0, 0.0, 0.0), + r_dot: Vector3::new(0.0, 0.0, 0.0), + epoch: 0.0, + } + } /// Reverses the transformation. Since this is a linear transformation we simply negate all terms + #[must_use] pub fn invert(mut self) -> Self { self.t *= -1.0; self.t_dot *= -1.0; @@ -301,6 +309,7 @@ impl TimeDependentHelmertParams { /// /// # Returns /// Tuple of transformed (position, velocity) + #[must_use] pub fn transform( &self, position: &ECEF, @@ -314,12 +323,19 @@ impl TimeDependentHelmertParams { } } +impl Default for TimeDependentHelmertParams { + fn default() -> Self { + Self { + epoch: 2020.0, + ..Self::zeros() + } + } +} + /// A transformation from one reference frame to another. -#[derive(Debug, PartialEq, PartialOrd, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, PartialOrd, Clone)] pub struct Transformation { - #[serde(alias = "source", alias = "source_name")] pub from: ReferenceFrame, - #[serde(alias = "destination", alias = "destination_name")] pub to: ReferenceFrame, pub params: TimeDependentHelmertParams, } @@ -334,8 +350,7 @@ impl Transformation { /// /// [`TransformationNotFound`] Is returned as an error if there is a mismatch between /// the coordinate reference frame and the [`Transformation::from`] field. - #[must_use] - pub fn transform(&self, coord: Coordinate) -> Result { + pub fn transform(&self, coord: &Coordinate) -> Result { if coord.reference_frame() != self.from { return Err(TransformationNotFound( self.from.clone(), @@ -395,6 +410,7 @@ pub struct TransformationRepository { impl TransformationRepository { /// Create an empty transformation repository + #[must_use] pub fn new() -> Self { Self { transformations: TransformationGraph::new(), @@ -416,6 +432,7 @@ impl TransformationRepository { } /// Create a repository with the builtin transformations + #[must_use] pub fn from_builtin() -> Self { Self::from_transformations(builtin_transformations()) } @@ -455,7 +472,7 @@ impl TransformationRepository { /// in the repository. pub fn transform( &self, - coord: Coordinate, + coord: &Coordinate, to: &ReferenceFrame, ) -> Result { let epoch = coord.epoch().to_fractional_year_hardcoded(); @@ -490,20 +507,6 @@ impl TransformationRepository { /// /// Returns an empty vector if source and destination frames are identical. /// - /// # Examples - /// ``` - /// # use swiftnav::reference_frame::*; - /// let repo = TransformationRepository::from_builtin(); - /// - /// // Direct transformation if available - /// let path = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2014)?; - /// - /// // Multi-hop transformation when needed - /// let path = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ETRF2000)?; - /// assert!(path.len() >= 1); - /// # Ok::<(), TransformationNotFound>(()) - /// ``` - /// /// # Errors /// /// [`TransformationNotFound`] if no transformation path exists between the frames @@ -542,10 +545,11 @@ impl TransformationRepository { } /// Get the number of transformations stored in the repository + #[must_use] pub fn count(&self) -> usize { self.transformations .values() - .map(|neighbors| neighbors.len()) + .map(HashMap::len) .sum() } } @@ -577,6 +581,7 @@ impl Extend for TransformationRepository { /// let transformations = builtin_transformations(); /// let repo = TransformationRepository::from_transformations(transformations); /// ``` +#[must_use] pub fn builtin_transformations() -> Vec { params::TRANSFORMATIONS.to_vec() } @@ -906,7 +911,7 @@ mod tests { #[test] fn helmert_invert() { - let mut params = TimeDependentHelmertParams { + let params = TimeDependentHelmertParams { t: Vector3::new(1.0, 2.0, 3.0), t_dot: Vector3::new(0.1, 0.2, 0.3), s: 4.0, @@ -996,20 +1001,12 @@ mod tests { from: ReferenceFrame::ITRF2020, to: ReferenceFrame::ITRF2014, params: TimeDependentHelmertParams { - tx: 1.0, - tx_dot: 0.0, - ty: 2.0, - ty_dot: 0.0, - tz: 3.0, - tz_dot: 0.0, + t: Vector3::new(1.0, 2.0, 3.0), + t_dot: Vector3::new(0.0, 0.0, 0.0), s: 0.0, s_dot: 0.0, - rx: 0.0, - rx_dot: 0.0, - ry: 0.0, - ry_dot: 0.0, - rz: 0.0, - rz_dot: 0.0, + r: Vector3::new(0.0, 0.0, 0.0), + r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2015.0, }, }; @@ -1037,20 +1034,12 @@ mod tests { from: ReferenceFrame::ITRF2020, to: ReferenceFrame::ITRF2014, params: TimeDependentHelmertParams { - tx: 1.0, - tx_dot: 0.0, - ty: 2.0, - ty_dot: 0.0, - tz: 3.0, - tz_dot: 0.0, + t: Vector3::new(1.0, 2.0, 3.0), + t_dot: Vector3::new(0.0, 0.0, 0.0), s: 0.0, s_dot: 0.0, - rx: 0.0, - rx_dot: 0.0, - ry: 0.0, - ry_dot: 0.0, - rz: 0.0, - rz_dot: 0.0, + r: Vector3::new(0.0, 0.0, 0.0), + r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2015.0, }, }, @@ -1058,20 +1047,12 @@ mod tests { from: ReferenceFrame::ITRF2014, to: ReferenceFrame::ITRF2000, params: TimeDependentHelmertParams { - tx: 4.0, - tx_dot: 0.0, - ty: 5.0, - ty_dot: 0.0, - tz: 6.0, - tz_dot: 0.0, + t: Vector3::new(4.0,5.0,6.0), + t_dot: Vector3::new(0.0,0.0,0.0), s: 0.0, s_dot: 0.0, - rx: 0.0, - rx_dot: 0.0, - ry: 0.0, - ry_dot: 0.0, - rz: 0.0, - rz_dot: 0.0, + r: Vector3::new(0.0,0.0,0.0), + r_dot: Vector3::new(0.0,0.0,0.0), epoch: 2015.0, }, }, @@ -1129,20 +1110,12 @@ mod tests { from: ReferenceFrame::ITRF2020, to: ReferenceFrame::Other("LocalFrame".to_string()), params: TimeDependentHelmertParams { - tx: 1.0, - tx_dot: 0.0, - ty: 2.0, - ty_dot: 0.0, - tz: 3.0, - tz_dot: 0.0, + t: Vector3::new(1.0,2.0,3.0), + t_dot: Vector3::new(0.0,0.0,0.0), s: 0.0, s_dot: 0.0, - rx: 0.0, - rx_dot: 0.0, - ry: 0.0, - ry_dot: 0.0, - rz: 0.0, - rz_dot: 0.0, + r: Vector3::new(0.0,0.0,0.0), + r_dot: Vector3::new(0.0,0.0,0.0), epoch: 2020.0, }, }; @@ -1161,20 +1134,12 @@ mod tests { from: ReferenceFrame::Other("Frame1".to_string()), to: ReferenceFrame::Other("Frame2".to_string()), params: TimeDependentHelmertParams { - tx: 1.0, - tx_dot: 0.0, - ty: 2.0, - ty_dot: 0.0, - tz: 3.0, - tz_dot: 0.0, + t: Vector3::new(1.0,2.0,3.0), + t_dot: Vector3::new(0.0,0.0,0.0), s: 0.0, s_dot: 0.0, - rx: 0.0, - rx_dot: 0.0, - ry: 0.0, - ry_dot: 0.0, - rz: 0.0, - rz_dot: 0.0, + r: Vector3::new(0.0,0.0,0.0), + r_dot: Vector3::new(0.0,0.0,0.0), epoch: 2020.0, }, }; @@ -1193,170 +1158,4 @@ mod tests { ); assert!(reverse_result.is_ok()); } - - #[cfg(test)] - mod serialization_tests { - use super::*; - - #[test] - fn test_serde_roundtrip() { - let original_transformations = builtin_transformations(); - - // Serialize to JSON - let json = - serde_json::to_string(&original_transformations).expect("Failed to serialize"); - - // Deserialize from JSON - let deserialized_transformations: Vec = - serde_json::from_str(&json).expect("Failed to deserialize"); - - // Compare - assert_eq!( - original_transformations.len(), - deserialized_transformations.len() - ); - for (orig, deser) in original_transformations - .iter() - .zip(deserialized_transformations.iter()) - { - assert_eq!(orig, deser); - } - } - - #[test] - fn test_reference_frame_serde() { - let frame = ReferenceFrame::ITRF2020; - let json = serde_json::to_string(&frame).expect("Failed to serialize"); - let deserialized: ReferenceFrame = - serde_json::from_str(&json).expect("Failed to deserialize"); - assert_eq!(frame, deserialized); - } - - #[test] - fn test_custom_reference_frame_serde() { - let custom_frame = ReferenceFrame::Other("MyCustomFrame".to_string()); - let json = serde_json::to_string(&custom_frame).expect("Failed to serialize"); - let deserialized: ReferenceFrame = - serde_json::from_str(&json).expect("Failed to deserialize"); - assert_eq!(custom_frame, deserialized); - assert_eq!(json, "\"MyCustomFrame\""); - } - - #[test] - fn test_custom_transformation_serde() { - let transformation = Transformation { - from: ReferenceFrame::Other("FrameA".to_string()), - to: ReferenceFrame::Other("FrameB".to_string()), - params: TimeDependentHelmertParams { - tx: -1.4, - tx_dot: 0.0, - ty: -0.9, - ty_dot: -0.1, - tz: 1.4, - tz_dot: 0.2, - s: -0.42, - s_dot: 0.0, - rx: 0.0, - rx_dot: 0.0, - ry: 0.0, - ry_dot: 0.0, - rz: 0.0, - rz_dot: 0.0, - epoch: 2015.0, - }, - }; - - let json = serde_json::to_string(&transformation).expect("Failed to serialize"); - let deserialized: Transformation = - serde_json::from_str(&json).expect("Failed to deserialize"); - assert_eq!(transformation, deserialized); - } - - #[test] - fn test_transformation_serde() { - let transformation = Transformation { - from: ReferenceFrame::ITRF2020, - to: ReferenceFrame::ITRF2014, - params: TimeDependentHelmertParams { - tx: -1.4, - tx_dot: 0.0, - ty: -0.9, - ty_dot: -0.1, - tz: 1.4, - tz_dot: 0.2, - s: -0.42, - s_dot: 0.0, - rx: 0.0, - rx_dot: 0.0, - ry: 0.0, - ry_dot: 0.0, - rz: 0.0, - rz_dot: 0.0, - epoch: 2015.0, - }, - }; - - let json = serde_json::to_string(&transformation).expect("Failed to serialize"); - let deserialized: Transformation = - serde_json::from_str(&json).expect("Failed to deserialize"); - assert_eq!(transformation, deserialized); - } - - #[test] - fn test_serde_aliases() { - // Test "source" and "destination" - let json = serde_json::json!({ - "source": "ITRF2020", - "destination": "ITRF2014", - "params": { - "tx": -1.4, - "tx_dot": 0.0, - "ty": -0.9, - "ty_dot": -0.1, - "tz": 1.4, - "tz_dot": 0.2, - "s": -0.42, - "s_dot": 0.0, - "rx": 0.0, - "rx_dot": 0.0, - "ry": 0.0, - "ry_dot": 0.0, - "rz": 0.0, - "rz_dot": 0.0, - "epoch": 2015.0, - } - }); - - let deserialized: Transformation = serde_json::from_value(json).unwrap(); - assert_eq!(deserialized.from, ReferenceFrame::ITRF2020); - assert_eq!(deserialized.to, ReferenceFrame::ITRF2014); - - // Test "source_name" and destination_name" - let json = serde_json::json!({ - "source_name": "ITRF2020", - "destination_name": "ITRF2014", - "params": { - "tx": -1.4, - "tx_dot": 0.0, - "ty": -0.9, - "ty_dot": -0.1, - "tz": 1.4, - "tz_dot": 0.2, - "s": -0.42, - "s_dot": 0.0, - "rx": 0.0, - "rx_dot": 0.0, - "ry": 0.0, - "ry_dot": 0.0, - "rz": 0.0, - "rz_dot": 0.0, - "epoch": 2015.0, - } - }); - - let deserialized: Transformation = serde_json::from_value(json).unwrap(); - assert_eq!(deserialized.from, ReferenceFrame::ITRF2020); - assert_eq!(deserialized.to, ReferenceFrame::ITRF2014); - } - } } diff --git a/swiftnav/src/reference_frame/params.rs b/swiftnav/src/reference_frame/params.rs index d66f141..2f77fa8 100644 --- a/swiftnav/src/reference_frame/params.rs +++ b/swiftnav/src/reference_frame/params.rs @@ -398,13 +398,8 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ from: ReferenceFrame::ITRF2020, to: ReferenceFrame::WGS84_G2296, params: TimeDependentHelmertParams { - t: Vector3::new(0.0, 0.0, 0.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), - r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2024.0, + ..TimeDependentHelmertParams::zeros() }, }, // WGS84(G2139) is defined to be the same as ITRF2014 at epoch 2016.0 @@ -412,13 +407,8 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ from: ReferenceFrame::ITRF2014, to: ReferenceFrame::WGS84_G2139, params: TimeDependentHelmertParams { - t: Vector3::new(0.0, 0.0, 0.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), - r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2016.0, + ..TimeDependentHelmertParams::zeros() }, }, // WGS84(G1762) is defined to be the same as ITRF2008 at epoch 2005.0 @@ -426,13 +416,8 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ from: ReferenceFrame::ITRF2008, to: ReferenceFrame::WGS84_G1762, params: TimeDependentHelmertParams { - t: Vector3::new(0.0, 0.0, 0.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), - r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2005.0, + ..TimeDependentHelmertParams::zeros() }, }, ]; From be4285bb085ab061c12a8c3bc29675eef727b1af Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Tue, 16 Sep 2025 19:38:28 -0700 Subject: [PATCH 07/11] Fix tests --- swiftnav/src/reference_frame/mod.rs | 44 +++++++++----------------- swiftnav/tests/reference_frames.rs | 48 ++++++++++++++--------------- 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index 9c5ead7..1ca2368 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -48,7 +48,7 @@ //! ); //! //! // Transform to different reference frame -//! let nad83_coord = repo.transform(itrf_coord, &ReferenceFrame::NAD83_2011)?; +//! let nad83_coord = repo.transform(&itrf_coord, &ReferenceFrame::NAD83_2011)?; //! # Ok::<(), Box>(()) //! ``` //! @@ -101,18 +101,7 @@ mod params; /// ``` /// /// [`Other`]: ReferenceFrame::Other -#[derive( - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Clone, - EnumString, - Display, - EnumIter, - Hash, -)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, EnumString, Display, EnumIter, Hash)] #[strum(serialize_all = "UPPERCASE")] pub enum ReferenceFrame { ITRF88, @@ -547,10 +536,7 @@ impl TransformationRepository { /// Get the number of transformations stored in the repository #[must_use] pub fn count(&self) -> usize { - self.transformations - .values() - .map(HashMap::len) - .sum() + self.transformations.values().map(HashMap::len).sum() } } @@ -1047,12 +1033,12 @@ mod tests { from: ReferenceFrame::ITRF2014, to: ReferenceFrame::ITRF2000, params: TimeDependentHelmertParams { - t: Vector3::new(4.0,5.0,6.0), - t_dot: Vector3::new(0.0,0.0,0.0), + t: Vector3::new(4.0, 5.0, 6.0), + t_dot: Vector3::new(0.0, 0.0, 0.0), s: 0.0, s_dot: 0.0, - r: Vector3::new(0.0,0.0,0.0), - r_dot: Vector3::new(0.0,0.0,0.0), + r: Vector3::new(0.0, 0.0, 0.0), + r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2015.0, }, }, @@ -1110,12 +1096,12 @@ mod tests { from: ReferenceFrame::ITRF2020, to: ReferenceFrame::Other("LocalFrame".to_string()), params: TimeDependentHelmertParams { - t: Vector3::new(1.0,2.0,3.0), - t_dot: Vector3::new(0.0,0.0,0.0), + t: Vector3::new(1.0, 2.0, 3.0), + t_dot: Vector3::new(0.0, 0.0, 0.0), s: 0.0, s_dot: 0.0, - r: Vector3::new(0.0,0.0,0.0), - r_dot: Vector3::new(0.0,0.0,0.0), + r: Vector3::new(0.0, 0.0, 0.0), + r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2020.0, }, }; @@ -1134,12 +1120,12 @@ mod tests { from: ReferenceFrame::Other("Frame1".to_string()), to: ReferenceFrame::Other("Frame2".to_string()), params: TimeDependentHelmertParams { - t: Vector3::new(1.0,2.0,3.0), - t_dot: Vector3::new(0.0,0.0,0.0), + t: Vector3::new(1.0, 2.0, 3.0), + t_dot: Vector3::new(0.0, 0.0, 0.0), s: 0.0, s_dot: 0.0, - r: Vector3::new(0.0,0.0,0.0), - r_dot: Vector3::new(0.0,0.0,0.0), + r: Vector3::new(0.0, 0.0, 0.0), + r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2020.0, }, }; diff --git a/swiftnav/tests/reference_frames.rs b/swiftnav/tests/reference_frames.rs index feaa1ec..0bcbf3d 100644 --- a/swiftnav/tests/reference_frames.rs +++ b/swiftnav/tests/reference_frames.rs @@ -21,7 +21,7 @@ fn euref_itrf2014() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ITRF2014) + .transform(&initial_coords, &ReferenceFrame::ITRF2014) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.0029, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.6005, abs <= 0.0001); @@ -58,7 +58,7 @@ fn euref_itrf2008() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ITRF2008) + .transform(&initial_coords, &ReferenceFrame::ITRF2008) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.0032, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.6023, abs <= 0.0001); @@ -95,7 +95,7 @@ fn euref_etrf2020() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF2020) + .transform(&initial_coords, &ReferenceFrame::ETRF2020) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1545, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4157, abs <= 0.0001); @@ -132,7 +132,7 @@ fn euref_etrf2014() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF2014) + .transform(&initial_coords, &ReferenceFrame::ETRF2014) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1579, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4123, abs <= 0.0001); @@ -169,7 +169,7 @@ fn euref_etrf2005() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords.clone(), &ReferenceFrame::ETRF2005) + .transform(&initial_coords.clone(), &ReferenceFrame::ETRF2005) .unwrap(); assert_float_eq!( result_coords.position().x(), @@ -213,7 +213,7 @@ fn euref_etrf2000() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF2000) + .transform(&initial_coords, &ReferenceFrame::ETRF2000) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.2015, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4596, abs <= 0.0001); @@ -250,7 +250,7 @@ fn euref_etrf97() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF97) + .transform(&initial_coords, &ReferenceFrame::ETRF97) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); @@ -287,7 +287,7 @@ fn euref_etrf96() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF96) + .transform(&initial_coords, &ReferenceFrame::ETRF96) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); @@ -324,7 +324,7 @@ fn euref_etrf94() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF94) + .transform(&initial_coords, &ReferenceFrame::ETRF94) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1888, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4489, abs <= 0.0001); @@ -361,7 +361,7 @@ fn euref_etrf93() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF93) + .transform(&initial_coords, &ReferenceFrame::ETRF93) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.2406, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4251, abs <= 0.0001); @@ -398,7 +398,7 @@ fn euref_etrf92() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF92) + .transform(&initial_coords, &ReferenceFrame::ETRF92) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1916, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4388, abs <= 0.0001); @@ -435,7 +435,7 @@ fn euref_etrf91() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF91) + .transform(&initial_coords, &ReferenceFrame::ETRF91) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1746, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4238, abs <= 0.0001); @@ -472,7 +472,7 @@ fn euref_etrf90() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF90) + .transform(&initial_coords, &ReferenceFrame::ETRF90) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1862, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4466, abs <= 0.0001); @@ -509,7 +509,7 @@ fn euref_etrf89() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF89) + .transform(&initial_coords, &ReferenceFrame::ETRF89) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027894.1672, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.4186, abs <= 0.0001); @@ -546,7 +546,7 @@ fn euref_etrf2014_reverse() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ITRF2014) + .transform(&initial_coords, &ReferenceFrame::ITRF2014) .unwrap(); assert_float_eq!(result_coords.position().x(), 4027893.8541, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 307045.7877, abs <= 0.0001); @@ -618,7 +618,7 @@ fn euref_complete_transform() { // Test adjusting the epoch first then transforming let result_coords = transformations .transform( - initial_coords.clone().adjust_epoch(&make_epoch(2008)), + &initial_coords.clone().adjust_epoch(&make_epoch(2008)), &ReferenceFrame::ETRF2014, ) .unwrap(); @@ -646,7 +646,7 @@ fn euref_complete_transform() { // Test transforming first then adjusting the epoch let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::ETRF2014) + .transform(&initial_coords, &ReferenceFrame::ETRF2014) .unwrap() .adjust_epoch(&make_epoch(2008)); assert_float_eq!(result_coords.position().x(), 4027894.3484, abs <= 0.0001); @@ -685,7 +685,7 @@ fn htdp_nad83_2011_fixed_date() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::NAD83_2011) + .transform(&initial_coords, &ReferenceFrame::NAD83_2011) .unwrap(); assert_float_eq!(result_coords.position().x(), -2705104.572, abs <= 0.001); assert_float_eq!(result_coords.position().y(), -4262047.032, abs <= 0.001); @@ -724,7 +724,7 @@ fn htdp_nad83_2011_adjust_epoch() { let result_coords = transformations .transform( - initial_coords.clone().adjust_epoch(&make_epoch(2010)), + &initial_coords.clone().adjust_epoch(&make_epoch(2010)), &ReferenceFrame::NAD83_2011, ) .unwrap(); @@ -751,7 +751,7 @@ fn htdp_nad83_2011_adjust_epoch() { assert_eq!(result_coords.reference_frame(), ReferenceFrame::NAD83_2011); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::NAD83_2011) + .transform(&initial_coords, &ReferenceFrame::NAD83_2011) .unwrap() .adjust_epoch(&make_epoch(2010)); assert_float_eq!(result_coords.position().x(), -2705104.349, abs <= 0.001); @@ -790,7 +790,7 @@ fn trx_nad83_csrs_fixed_date() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::NAD83_CSRS) + .transform(&initial_coords, &ReferenceFrame::NAD83_CSRS) .unwrap(); assert_float_eq!(result_coords.position().x(), 1267459.462, abs <= 0.001); assert_float_eq!(result_coords.position().y(), -4294621.605, abs <= 0.001); @@ -829,7 +829,7 @@ fn trx_nad83_csrs_adjust_epoch() { let result_coords = transformations .transform( - initial_coords.clone().adjust_epoch(&make_epoch(2010)), + &initial_coords.clone().adjust_epoch(&make_epoch(2010)), &ReferenceFrame::NAD83_CSRS, ) .unwrap(); @@ -856,7 +856,7 @@ fn trx_nad83_csrs_adjust_epoch() { assert_eq!(result_coords.reference_frame(), ReferenceFrame::NAD83_CSRS); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::NAD83_CSRS) + .transform(&initial_coords, &ReferenceFrame::NAD83_CSRS) .unwrap() .adjust_epoch(&make_epoch(2010)); assert_float_eq!(result_coords.position().x(), 1267459.620, abs <= 0.001); @@ -894,7 +894,7 @@ fn dref91_r2016() { let transformations = TransformationRepository::from_builtin(); let result_coords = transformations - .transform(initial_coords, &ReferenceFrame::DREF91_R2016) + .transform(&initial_coords, &ReferenceFrame::DREF91_R2016) .unwrap(); assert_float_eq!(result_coords.position().x(), 3842153.3718, abs <= 0.0001); assert_float_eq!(result_coords.position().y(), 563401.6528, abs <= 0.0001); From 4c8c56fe888cff8317366d045a7802c6a24522ea Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Tue, 16 Sep 2025 20:04:10 -0700 Subject: [PATCH 08/11] Clean up comments --- swiftnav/src/reference_frame/mod.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index 1ca2368..addc866 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -230,6 +230,20 @@ impl TimeDependentHelmertParams { /// Scale factor for rotation parameters (converts milliarcseconds to radians) pub const ROTATE_SCALE: f64 = (std::f64::consts::PI / 180.0) * (0.001 / 3600.0); + /// Create a [`TimeDependentHelmertParams`] object with zero in every field. + /// + /// # Note + /// + /// The [`TimeDependentHelmertParams::epoch`] field needs to represent a real time, and an + /// epoch of `0.0` is almost certainly not what you intend. This is best used to initialize + /// all other fields to zero like this: + /// + /// ```rust + /// TimeDependentHelmertParams { + /// epoch: 2020.0, + /// ..TimeDependentHelmertParams::zeros() + /// } + /// ``` #[must_use] pub const fn zeros() -> TimeDependentHelmertParams { TimeDependentHelmertParams { @@ -466,8 +480,8 @@ impl TransformationRepository { ) -> Result { let epoch = coord.epoch().to_fractional_year_hardcoded(); - let accumulate_transformations = |(position, velocity): (ECEF, Option), - params: &TimeDependentHelmertParams| + let apply_transformation = |(position, velocity): (ECEF, Option), + params: &TimeDependentHelmertParams| -> (ECEF, Option) { params.transform(&position, velocity.as_ref(), epoch) }; @@ -475,10 +489,7 @@ impl TransformationRepository { let (position, velocity) = self .get_shortest_path(coord.reference_frame(), to)? .into_iter() - .fold( - (coord.position(), coord.velocity()), - accumulate_transformations, - ); + .fold((coord.position(), coord.velocity()), apply_transformation); Ok(Coordinate::new( to.clone(), @@ -557,9 +568,7 @@ impl Extend for TransformationRepository { /// /// Returns a Vec of all pre-defined transformations between common reference frames /// including ITRF, ETRF, NAD83, and WGS84 series. These transformations are sourced -/// from authoritative geodetic organizations. -/// -/// Use this to initialize a [`TransformationRepository`] or for serialization. +/// from authoritative geodetic organizations.. /// /// # Example /// ``` @@ -568,7 +577,7 @@ impl Extend for TransformationRepository { /// let repo = TransformationRepository::from_transformations(transformations); /// ``` #[must_use] -pub fn builtin_transformations() -> Vec { +fn builtin_transformations() -> Vec { params::TRANSFORMATIONS.to_vec() } From 1fbbc8e55b2237974911effed33319a59d4f53a3 Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Tue, 16 Sep 2025 20:29:04 -0700 Subject: [PATCH 09/11] Fix tests --- swiftnav/src/reference_frame/mod.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index addc866..f4a14ba 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -239,10 +239,11 @@ impl TimeDependentHelmertParams { /// all other fields to zero like this: /// /// ```rust - /// TimeDependentHelmertParams { + /// # use swiftnav::reference_frame::TimeDependentHelmertParams; + /// let params = TimeDependentHelmertParams { /// epoch: 2020.0, /// ..TimeDependentHelmertParams::zeros() - /// } + /// }; /// ``` #[must_use] pub const fn zeros() -> TimeDependentHelmertParams { @@ -569,13 +570,6 @@ impl Extend for TransformationRepository { /// Returns a Vec of all pre-defined transformations between common reference frames /// including ITRF, ETRF, NAD83, and WGS84 series. These transformations are sourced /// from authoritative geodetic organizations.. -/// -/// # Example -/// ``` -/// # use swiftnav::reference_frame::*; -/// let transformations = builtin_transformations(); -/// let repo = TransformationRepository::from_transformations(transformations); -/// ``` #[must_use] fn builtin_transformations() -> Vec { params::TRANSFORMATIONS.to_vec() From 133fc46022f3409d792a89c3e1215faed4c8333c Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Wed, 17 Sep 2025 11:49:59 -0700 Subject: [PATCH 10/11] PR feedback --- swiftnav/src/reference_frame/mod.rs | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index f4a14ba..9b2e95f 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -481,16 +481,13 @@ impl TransformationRepository { ) -> Result { let epoch = coord.epoch().to_fractional_year_hardcoded(); - let apply_transformation = |(position, velocity): (ECEF, Option), - params: &TimeDependentHelmertParams| - -> (ECEF, Option) { - params.transform(&position, velocity.as_ref(), epoch) - }; - let (position, velocity) = self .get_shortest_path(coord.reference_frame(), to)? .into_iter() - .fold((coord.position(), coord.velocity()), apply_transformation); + .fold( + (coord.position(), coord.velocity()), + |(pos, vel), params| params.transform(&pos, vel.as_ref(), epoch), + ); Ok(Coordinate::new( to.clone(), @@ -955,7 +952,6 @@ mod tests { let graph: TransformationRepository = TransformationRepository::from_builtin(); let path = graph.get_shortest_path(&from, &to); - assert!(path.is_ok()); // Make sure that the path is correct. N.B. this may change if more transformations // are added in the future let path = path.unwrap(); @@ -968,7 +964,7 @@ mod tests { assert_eq!(repo.count(), 0); let result = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2014); - assert!(result.is_err()); + result.unwrap_err(); } #[test] @@ -978,7 +974,7 @@ mod tests { // Test path finding let path = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ETRF2000); - assert!(path.is_ok()); + path.unwrap(); } #[test] @@ -1005,14 +1001,12 @@ mod tests { assert_eq!(repo.count(), 2); let result = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2014); - assert!(result.is_ok()); assert_eq!(result.unwrap(), vec![¶ms]); // Test reverse transformation let params = params.invert(); let reverse_result = repo.get_shortest_path(&ReferenceFrame::ITRF2014, &ReferenceFrame::ITRF2020); - assert!(reverse_result.is_ok()); assert_eq!(reverse_result.unwrap(), vec![¶ms]); } @@ -1052,11 +1046,10 @@ mod tests { // Test direct transformation let result = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2014); - assert!(result.is_ok()); + result.unwrap(); // Test multi-step path let path = repo.get_shortest_path(&ReferenceFrame::ITRF2020, &ReferenceFrame::ITRF2000); - assert!(path.is_ok()); let path = path.unwrap(); assert_eq!(path.len(), 2); // Two transformations: ITRF2020->ITRF2014 and ITRF2014->ITRF2000 assert_eq!( @@ -1139,12 +1132,12 @@ mod tests { &ReferenceFrame::Other("Frame1".to_string()), &ReferenceFrame::Other("Frame2".to_string()), ); - assert!(result.is_ok()); + result.unwrap(); let reverse_result = repo.get_shortest_path( &ReferenceFrame::Other("Frame2".to_string()), &ReferenceFrame::Other("Frame1".to_string()), ); - assert!(reverse_result.is_ok()); + reverse_result.unwrap(); } } From b20d8e4aee07a8ac9558c44bed5f6472cc058cb9 Mon Sep 17 00:00:00 2001 From: Joseph Angelo Date: Wed, 17 Sep 2025 11:50:27 -0700 Subject: [PATCH 11/11] Reuse zeros in more built-in transformations --- swiftnav/src/reference_frame/params.rs | 76 ++++++-------------------- 1 file changed, 16 insertions(+), 60 deletions(-) diff --git a/swiftnav/src/reference_frame/params.rs b/swiftnav/src/reference_frame/params.rs index 2f77fa8..fe2bf3f 100644 --- a/swiftnav/src/reference_frame/params.rs +++ b/swiftnav/src/reference_frame/params.rs @@ -10,10 +10,8 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ t: Vector3::new(-1.4, -0.9, 1.4), t_dot: Vector3::new(0.0, -0.1, 0.2), s: -0.42, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), - r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2015.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -24,9 +22,8 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ t_dot: Vector3::new(0.0, -0.1, 0.1), s: -0.29, s_dot: 0.03, - r: Vector3::new(0.0, 0.0, 0.0), - r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2015.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -37,9 +34,8 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ t_dot: Vector3::new(0.3, -0.1, 0.1), s: 0.65, s_dot: 0.03, - r: Vector3::new(0.0, 0.0, 0.0), - r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2015.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -50,9 +46,8 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ t_dot: Vector3::new(0.1, 0.0, -1.7), s: 2.25, s_dot: 0.11, - r: Vector3::new(0.0, 0.0, 0.0), - r_dot: Vector3::new(0.0, 0.0, 0.0), epoch: 2015.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -176,26 +171,18 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ from: ReferenceFrame::ITRF2020, to: ReferenceFrame::ETRF2020, params: TimeDependentHelmertParams { - t: Vector3::new(0.0, 0.0, 0.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.086, 0.519, -0.753), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { from: ReferenceFrame::ITRF2014, to: ReferenceFrame::ETRF2014, params: TimeDependentHelmertParams { - t: Vector3::new(0.0, 0.0, 0.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.085, 0.531, -0.770), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -203,12 +190,9 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF2005, params: TimeDependentHelmertParams { t: Vector3::new(56.0, 48.0, -37.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.054, 0.518, -0.781), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -216,12 +200,9 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF2000, params: TimeDependentHelmertParams { t: Vector3::new(54.0, 51.0, -48.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.081, 0.490, -0.792), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -229,12 +210,9 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF97, params: TimeDependentHelmertParams { t: Vector3::new(41.0, 41.0, -49.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.200, 0.500, -0.650), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -242,12 +220,9 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF96, params: TimeDependentHelmertParams { t: Vector3::new(41.0, 41.0, -49.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.200, 0.500, -0.650), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -255,12 +230,9 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF94, params: TimeDependentHelmertParams { t: Vector3::new(41.0, 41.0, -49.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.200, 0.500, -0.650), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -268,12 +240,9 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF93, params: TimeDependentHelmertParams { t: Vector3::new(19.0, 53.0, -21.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.320, 0.780, -0.670), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -281,12 +250,9 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF92, params: TimeDependentHelmertParams { t: Vector3::new(38.0, 40.0, -37.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.210, 0.520, -0.680), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -294,12 +260,9 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF91, params: TimeDependentHelmertParams { t: Vector3::new(21.0, 25.0, -37.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.210, 0.520, -0.680), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { @@ -307,25 +270,18 @@ pub const TRANSFORMATIONS: [Transformation; 33] = [ to: ReferenceFrame::ETRF90, params: TimeDependentHelmertParams { t: Vector3::new(19.0, 28.0, -23.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.110, 0.570, -0.710), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation { from: ReferenceFrame::ITRF89, to: ReferenceFrame::ETRF89, params: TimeDependentHelmertParams { - t: Vector3::new(0.0, 0.0, 0.0), - t_dot: Vector3::new(0.0, 0.0, 0.0), - s: 0.0, - s_dot: 0.0, - r: Vector3::new(0.0, 0.0, 0.0), r_dot: Vector3::new(0.110, 0.570, -0.710), epoch: 1989.0, + ..TimeDependentHelmertParams::zeros() }, }, Transformation {