From 8113b6f8aae6e71ada06250f9f1fc0f4921063d4 Mon Sep 17 00:00:00 2001 From: rfitzger Date: Fri, 10 Nov 2023 11:53:19 -0700 Subject: [PATCH 1/5] optional distance tolerance for nearest neighbor query --- .../src/plugin/input/default/rtree/builder.rs | 18 ++- .../src/plugin/input/default/rtree/plugin.rs | 136 ++++++++++++++---- .../routee-compass/src/plugin/plugin_error.rs | 4 +- 3 files changed, 125 insertions(+), 33 deletions(-) diff --git a/rust/routee-compass/src/plugin/input/default/rtree/builder.rs b/rust/routee-compass/src/plugin/input/default/rtree/builder.rs index 3b0bcde4..812d0c76 100644 --- a/rust/routee-compass/src/plugin/input/default/rtree/builder.rs +++ b/rust/routee-compass/src/plugin/input/default/rtree/builder.rs @@ -1,3 +1,5 @@ +use routee_compass_core::util::unit::{Distance, DistanceUnit}; + use crate::{ app::compass::config::{ builders::InputPluginBuilder, compass_configuration_error::CompassConfigurationError, @@ -15,13 +17,19 @@ impl InputPluginBuilder for VertexRTreeBuilder { &self, parameters: &serde_json::Value, ) -> Result, CompassConfigurationError> { + let parent_key = String::from("Vertex RTree Input Plugin"); let vertex_filename_key = String::from("vertices_input_file"); - let vertex_path = parameters.get_config_path( - vertex_filename_key, - String::from("Vertex RTree Input Plugin"), + let vertex_path = parameters.get_config_path(vertex_filename_key, parent_key.clone())?; + let tolerance_distance = parameters.get_config_serde_optional::( + String::from("distance_tolerance"), + parent_key.clone(), + )?; + let distance_unit = parameters.get_config_serde_optional::( + String::from("distance_unit"), + parent_key.clone(), )?; - let rtree = - RTreePlugin::from_file(&vertex_path).map_err(CompassConfigurationError::PluginError)?; + let rtree = RTreePlugin::new(&vertex_path, tolerance_distance, distance_unit) + .map_err(CompassConfigurationError::PluginError)?; let m: Box = Box::new(rtree); return Ok(m); } diff --git a/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs b/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs index 0e2ffdf5..e7d069f6 100644 --- a/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs +++ b/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs @@ -3,10 +3,13 @@ use std::path::Path; use crate::plugin::input::input_json_extensions::InputJsonExtensions; use crate::plugin::input::input_plugin::InputPlugin; use crate::plugin::plugin_error::PluginError; -use geo::{coord, Coord}; +use geo::{coord, Coord, Point}; use routee_compass_core::{ model::{graph::graph::Graph, property::vertex::Vertex}, - util::fs::read_utils, + util::{ + fs::read_utils, + unit::{Distance, DistanceUnit, BASE_DISTANCE_UNIT}, + }, }; use rstar::{PointDistance, RTree, RTreeObject, AABB}; @@ -88,42 +91,80 @@ impl PointDistance for RTreeVertex { /// * An input plugin that uses an RTree to find the nearest vertex to the origin and destination coordinates. pub struct RTreePlugin { vertex_rtree: VertexRTree, + tolerance: Option<(Distance, DistanceUnit)>, } impl RTreePlugin { - pub fn new(vertices: Box<[Vertex]>) -> Self { - Self { - vertex_rtree: VertexRTree::new(vertices.to_vec()), - } - } - pub fn from_file(vertex_file: &Path) -> Result { + /// creates a new R Tree input plugin instance. + /// + /// # Arguments + /// + /// * `vertex_file` - file containing vertices + /// * `tolerance_distance` - optional max distance to nearest vertex (assumed infinity if not included) + /// * `distance_unit` - distance unit for tolerance, assumed BASE_DISTANCE_UNIT if not provided + /// + /// # Returns + /// + /// * a plugin instance or an error from file loading + pub fn new( + vertex_file: &Path, + tolerance_distance: Option, + distance_unit: Option, + ) -> Result { let vertices: Box<[Vertex]> = read_utils::from_csv(&vertex_file, true, None).map_err(PluginError::CsvReadError)?; - Ok(Self::new(vertices)) + let vertex_rtree = VertexRTree::new(vertices.to_vec()); + let tolerance = match (tolerance_distance, distance_unit) { + (None, None) => None, + (None, Some(_)) => None, + (Some(t), None) => Some((t, BASE_DISTANCE_UNIT)), + (Some(t), Some(u)) => Some((t, u)), + }; + Ok(RTreePlugin { + vertex_rtree, + tolerance, + }) } } impl InputPlugin for RTreePlugin { - fn process(&self, input: &serde_json::Value) -> Result, PluginError> { - let mut updated = input.clone(); - let origin_coord = input.get_origin_coordinate()?; - let destination_coord_option = input.get_destination_coordinate()?; + /// finds the nearest graph vertex to the user-provided origin (and optionally, destination) coordinates. + /// + /// # Arguments + /// + /// * `query` - search query assumed to have at least an origin coordinate entry + /// + /// # Returns + /// + /// * either vertex ids for the nearest coordinates to the the origin (and optionally destination), + /// or, an error if not found or not within tolerance + fn process(&self, query: &serde_json::Value) -> Result, PluginError> { + let mut updated = query.clone(); + let src_coord = query.get_origin_coordinate()?; + let dst_coord_option = query.get_destination_coordinate()?; - let origin_vertex = self - .vertex_rtree - .nearest_vertex(origin_coord) - .ok_or(PluginError::NearestVertexNotFound(origin_coord))?; + let src_vertex = + self.vertex_rtree + .nearest_vertex(src_coord) + .ok_or(PluginError::PluginFailed(format!( + "nearest vertex not found for origin coordinate {:?}", + src_coord + )))?; - updated.add_origin_vertex(origin_vertex.vertex_id)?; + validate_tolerance(src_coord, src_vertex.coordinate, &self.tolerance)?; + updated.add_origin_vertex(src_vertex.vertex_id)?; - match destination_coord_option { + match dst_coord_option { None => {} - Some(destination_coord) => { - let destination_vertex = self - .vertex_rtree - .nearest_vertex(destination_coord) - .ok_or(PluginError::NearestVertexNotFound(destination_coord))?; - updated.add_destination_vertex(destination_vertex.vertex_id)?; + Some(dst_coord) => { + let dst_vertex = self.vertex_rtree.nearest_vertex(dst_coord).ok_or( + PluginError::PluginFailed(format!( + "nearest vertex not found for destination coordinate {:?}", + dst_coord + )), + )?; + validate_tolerance(dst_coord, dst_vertex.coordinate, &self.tolerance)?; + updated.add_destination_vertex(dst_vertex.vertex_id)?; } } @@ -131,6 +172,49 @@ impl InputPlugin for RTreePlugin { } } +/// confirms that two coordinates are within some stated distance tolerance. +/// if no tolerance is provided, the dst coordinate is assumed to be a valid distance. +/// +/// # Arguments +/// +/// * `src` - source coordinate +/// * `dst` - destination coordinate that may or may not be within some distance +/// tolerance of the src coordinate +/// * `tolerance` - tolerance parameters set by user for the rtree plugin. if this is None, +/// all coordinate pairs are assumed to be within distance tolerance, but this +/// may lead to unexpected behavior where far away coordinates are considered "matched". +/// +/// # Returns +/// +/// * nothing, or an error if the coordinates are not within tolerance +fn validate_tolerance( + src: Coord, + dst: Coord, + tolerance: &Option<(Distance, DistanceUnit)>, +) -> Result<(), PluginError> { + match tolerance { + Some((tolerance_distance, tolerance_distance_unit)) => { + let distance_base = Distance::new(src.distance_2(&dst)); + let distance = BASE_DISTANCE_UNIT.convert(distance_base, *tolerance_distance_unit); + if &distance >= tolerance_distance { + Err(PluginError::PluginFailed( + format!( + "coord {:?} nearest vertex coord is {:?} which is {} {} away, exceeding the distance tolerance of {}", + src, + dst, + distance, + tolerance_distance, + tolerance_distance_unit + ) + )) + } else { + Ok(()) + } + } + None => Ok(()), + } +} + #[cfg(test)] mod test { use std::{ @@ -160,7 +244,7 @@ mod test { .join("test") .join("rtree_query.json"); let query_str = fs::read_to_string(query_filepath).unwrap(); - let rtree_plugin = RTreePlugin::from_file(&vertices_filepath).unwrap(); + let rtree_plugin = RTreePlugin::new(&vertices_filepath, None, None).unwrap(); let query: serde_json::Value = serde_json::from_str(&query_str).unwrap(); let processed_query = rtree_plugin.process(&query).unwrap(); diff --git a/rust/routee-compass/src/plugin/plugin_error.rs b/rust/routee-compass/src/plugin/plugin_error.rs index d3817a76..5395625b 100644 --- a/rust/routee-compass/src/plugin/plugin_error.rs +++ b/rust/routee-compass/src/plugin/plugin_error.rs @@ -15,8 +15,8 @@ pub enum PluginError { InputError(String), #[error("error with building plugin")] BuildError, - #[error("nearest vertex not found for coord {0:?}")] - NearestVertexNotFound(Coord), + #[error("{0}")] + PluginFailed(String), #[error("unable to read file {0} due to {1}")] FileReadError(PathBuf, String), #[error(transparent)] From ab8b77c6210cb9a5d7844fe9b7e88924dc0b4e06 Mon Sep 17 00:00:00 2001 From: rfitzger Date: Fri, 10 Nov 2023 12:34:06 -0700 Subject: [PATCH 2/5] remove " characters from display --- rust/routee-compass-core/src/util/unit/distance_unit.rs | 4 +++- rust/routee-compass-core/src/util/unit/energy_rate_unit.rs | 4 +++- rust/routee-compass-core/src/util/unit/energy_unit.rs | 4 +++- rust/routee-compass-core/src/util/unit/grade_unit.rs | 4 +++- rust/routee-compass-core/src/util/unit/speed_unit.rs | 4 +++- rust/routee-compass-core/src/util/unit/time_unit.rs | 4 +++- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/rust/routee-compass-core/src/util/unit/distance_unit.rs b/rust/routee-compass-core/src/util/unit/distance_unit.rs index 4ba65b69..ead885a2 100644 --- a/rust/routee-compass-core/src/util/unit/distance_unit.rs +++ b/rust/routee-compass-core/src/util/unit/distance_unit.rs @@ -28,7 +28,9 @@ impl DistanceUnit { impl std::fmt::Display for DistanceUnit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + let s = serde_json::to_string(self) + .map_err(|_| std::fmt::Error)? + .replace("\"", ""); write!(f, "{}", s) } } diff --git a/rust/routee-compass-core/src/util/unit/energy_rate_unit.rs b/rust/routee-compass-core/src/util/unit/energy_rate_unit.rs index 9c8d83d3..effc0305 100644 --- a/rust/routee-compass-core/src/util/unit/energy_rate_unit.rs +++ b/rust/routee-compass-core/src/util/unit/energy_rate_unit.rs @@ -38,7 +38,9 @@ impl EnergyRateUnit { impl std::fmt::Display for EnergyRateUnit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + let s = serde_json::to_string(self) + .map_err(|_| std::fmt::Error)? + .replace("\"", ""); write!(f, "{}", s) } } diff --git a/rust/routee-compass-core/src/util/unit/energy_unit.rs b/rust/routee-compass-core/src/util/unit/energy_unit.rs index d0c78118..e10a7fa6 100644 --- a/rust/routee-compass-core/src/util/unit/energy_unit.rs +++ b/rust/routee-compass-core/src/util/unit/energy_unit.rs @@ -11,7 +11,9 @@ impl EnergyUnit {} impl std::fmt::Display for EnergyUnit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + let s = serde_json::to_string(self) + .map_err(|_| std::fmt::Error)? + .replace("\"", ""); write!(f, "{}", s) } } diff --git a/rust/routee-compass-core/src/util/unit/grade_unit.rs b/rust/routee-compass-core/src/util/unit/grade_unit.rs index ef62e344..78be23ac 100644 --- a/rust/routee-compass-core/src/util/unit/grade_unit.rs +++ b/rust/routee-compass-core/src/util/unit/grade_unit.rs @@ -28,7 +28,9 @@ impl GradeUnit { impl std::fmt::Display for GradeUnit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + let s = serde_json::to_string(self) + .map_err(|_| std::fmt::Error)? + .replace("\"", ""); write!(f, "{}", s) } } diff --git a/rust/routee-compass-core/src/util/unit/speed_unit.rs b/rust/routee-compass-core/src/util/unit/speed_unit.rs index a9ca89f8..8f60aed1 100644 --- a/rust/routee-compass-core/src/util/unit/speed_unit.rs +++ b/rust/routee-compass-core/src/util/unit/speed_unit.rs @@ -12,7 +12,9 @@ pub enum SpeedUnit { impl std::fmt::Display for SpeedUnit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + let s = serde_json::to_string(self) + .map_err(|_| std::fmt::Error)? + .replace("\"", ""); write!(f, "{}", s) } } diff --git a/rust/routee-compass-core/src/util/unit/time_unit.rs b/rust/routee-compass-core/src/util/unit/time_unit.rs index 7f069386..98f63169 100644 --- a/rust/routee-compass-core/src/util/unit/time_unit.rs +++ b/rust/routee-compass-core/src/util/unit/time_unit.rs @@ -36,7 +36,9 @@ impl TimeUnit { impl std::fmt::Display for TimeUnit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + let s = serde_json::to_string(self) + .map_err(|_| std::fmt::Error)? + .replace("\"", ""); write!(f, "{}", s) } } From dd750318b711111f1955fe3bb21be1c28bfae66d Mon Sep 17 00:00:00 2001 From: rfitzger Date: Fri, 10 Nov 2023 12:34:28 -0700 Subject: [PATCH 3/5] error message for invalid distance tolerance --- .../src/plugin/input/default/rtree/plugin.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs b/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs index e7d069f6..c49a80a9 100644 --- a/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs +++ b/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs @@ -8,6 +8,7 @@ use routee_compass_core::{ model::{graph::graph::Graph, property::vertex::Vertex}, util::{ fs::read_utils, + geo::haversine, unit::{Distance, DistanceUnit, BASE_DISTANCE_UNIT}, }, }; @@ -194,17 +195,19 @@ fn validate_tolerance( ) -> Result<(), PluginError> { match tolerance { Some((tolerance_distance, tolerance_distance_unit)) => { - let distance_base = Distance::new(src.distance_2(&dst)); - let distance = BASE_DISTANCE_UNIT.convert(distance_base, *tolerance_distance_unit); + let distance_meters = haversine::coord_distance_meters(src, dst) + .map_err(|s| PluginError::PluginFailed(s))?; + let distance = BASE_DISTANCE_UNIT.convert(distance_meters, *tolerance_distance_unit); if &distance >= tolerance_distance { Err(PluginError::PluginFailed( format!( - "coord {:?} nearest vertex coord is {:?} which is {} {} away, exceeding the distance tolerance of {}", + "coord {:?} nearest vertex coord is {:?} which is {} {} away, exceeding the distance tolerance of {} {}", src, dst, distance, + tolerance_distance_unit, tolerance_distance, - tolerance_distance_unit + tolerance_distance_unit, ) )) } else { From b238316ad7258588f9e40660e9b8e2203c4a64b7 Mon Sep 17 00:00:00 2001 From: rfitzger Date: Fri, 10 Nov 2023 12:39:44 -0700 Subject: [PATCH 4/5] set rtree distance tolerance of 0.2km --- python/nrel/routee/compass/resources/osm_default_distance.toml | 2 +- python/nrel/routee/compass/resources/osm_default_energy.toml | 2 +- python/nrel/routee/compass/resources/osm_default_speed.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/nrel/routee/compass/resources/osm_default_distance.toml b/python/nrel/routee/compass/resources/osm_default_distance.toml index 31e3cf34..59a8dd74 100644 --- a/python/nrel/routee/compass/resources/osm_default_distance.toml +++ b/python/nrel/routee/compass/resources/osm_default_distance.toml @@ -12,7 +12,7 @@ distance_unit = "miles" [plugin] input_plugins = [ { type = "grid_search" }, - { type = "vertex_rtree", vertices_input_file = "vertices-compass.csv.gz" }, + { type = "vertex_rtree", distance_tolerance = 0.2, distance_unit = "kilometers", vertices_input_file = "vertices-compass.csv.gz" }, ] output_plugins = [ { type = "summary" }, diff --git a/python/nrel/routee/compass/resources/osm_default_energy.toml b/python/nrel/routee/compass/resources/osm_default_energy.toml index 0d1926a3..b41281a4 100644 --- a/python/nrel/routee/compass/resources/osm_default_energy.toml +++ b/python/nrel/routee/compass/resources/osm_default_energy.toml @@ -8,7 +8,7 @@ verbose = true [plugin] input_plugins = [ { type = "grid_search" }, - { type = "vertex_rtree", vertices_input_file = "vertices-compass.csv.gz" }, + { type = "vertex_rtree", distance_tolerance = 0.2, distance_unit = "kilometers", vertices_input_file = "vertices-compass.csv.gz" }, ] output_plugins = [ { type = "summary" }, diff --git a/python/nrel/routee/compass/resources/osm_default_speed.toml b/python/nrel/routee/compass/resources/osm_default_speed.toml index af226e80..9be4ce53 100644 --- a/python/nrel/routee/compass/resources/osm_default_speed.toml +++ b/python/nrel/routee/compass/resources/osm_default_speed.toml @@ -15,7 +15,7 @@ output_time_unit = "minutes" [plugin] input_plugins = [ { type = "grid_search" }, - { type = "vertex_rtree", vertices_input_file = "vertices-compass.csv.gz" }, + { type = "vertex_rtree", distance_tolerance = 0.2, distance_unit = "kilometers", vertices_input_file = "vertices-compass.csv.gz" }, ] output_plugins = [ { type = "summary" }, From 83e73543dc446f12ebca2f680d0e7bbc10286de7 Mon Sep 17 00:00:00 2001 From: rfitzger Date: Fri, 10 Nov 2023 12:50:16 -0700 Subject: [PATCH 5/5] reference meters explicitly --- rust/routee-compass/src/plugin/input/default/rtree/plugin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs b/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs index c49a80a9..f2876de2 100644 --- a/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs +++ b/rust/routee-compass/src/plugin/input/default/rtree/plugin.rs @@ -197,7 +197,7 @@ fn validate_tolerance( Some((tolerance_distance, tolerance_distance_unit)) => { let distance_meters = haversine::coord_distance_meters(src, dst) .map_err(|s| PluginError::PluginFailed(s))?; - let distance = BASE_DISTANCE_UNIT.convert(distance_meters, *tolerance_distance_unit); + let distance = DistanceUnit::Meters.convert(distance_meters, *tolerance_distance_unit); if &distance >= tolerance_distance { Err(PluginError::PluginFailed( format!(