diff --git a/docs/config.md b/docs/config.md index cd74d49b..59003320 100644 --- a/docs/config.md +++ b/docs/config.md @@ -170,6 +170,30 @@ energy_rate_unit = "gallons_gasoline_per_mile" ideal_energy_rate = 0.02857143 # A real world adjustment factor for things like temperature and auxillary loads real_world_energy_adjustment = 1.166 + +# An optional cache that will keep track of recent energy values with the same input. +# If this is ommitted from the config, the model will compute the energy for every link in a search. +# See note below for some considerations when using this. +[traversal.vehicles.float_cache_policy] +cache_size = 10_000 +key_precisions = [ + 2, # speed goes from 71.23 to 7123 + 4, # grade goes from 0.123 (decimal) to 1230 or 123 (millis) to 1230000 +] +``` + +```{note} +When using the float cache it's possible that you might get slightly different final energy values versus the same query run with no caching. + +The reason for this is how the input floating point values get converted into an integer for storage in the cache. + +If your key precision for a grade value is 4, an incoming value of 0.123456 would get converted into 1234 and stored in the cache as such. +This is done to make sure we're actually getting cache hits and improving performance. +If we used a precision of 10, there might not be many other links in the road network that share the same exact properties at that resolution. +But, the tradeoff here is that if you used a key precision of 1, grade values of 0.14 and 0.05 would both result in the integer 1 being stored in the cache. +This would render grades of 5% and 14% to be equal to each other from an energy perspective and they are clearly not. + +So, usage of this cache can result in improved runtimes for the energy traversal model but the user should make sure the precision values are appropriate for the application. ``` ## Plugins @@ -271,7 +295,7 @@ distance_unit = "meters" The load balancer plugin estimates the runtime for each query. That information is used by `CompassApp` in order to best leverage parallelism. -For example, we have configured a parallelism of 2 and have 4 queries, but one query is a cross-country trip and will take a very long time to run. +For example, we have configured a parallelism of 2 and have 4 queries, but one query is a cross-country trip and will take a very long time to run. With the load balancer plugin, Compass will identify this and bundle the three smaller queries together: ``` diff --git a/rust/routee-compass-core/Cargo.toml b/rust/routee-compass-core/Cargo.toml index 25d40c68..0f570b5a 100644 --- a/rust/routee-compass-core/Cargo.toml +++ b/rust/routee-compass-core/Cargo.toml @@ -20,6 +20,7 @@ geo = { workspace = true } ordered-float = { workspace = true } derive_more = "0.99.0" priority-queue = "1.3.2" +lru = "0.12" csv = { workspace = true } kdam = { workspace = true } log = { workspace = true } diff --git a/rust/routee-compass-core/src/model/traversal/traversal_model_error.rs b/rust/routee-compass-core/src/model/traversal/traversal_model_error.rs index 4f3d043e..937e0dc4 100644 --- a/rust/routee-compass-core/src/model/traversal/traversal_model_error.rs +++ b/rust/routee-compass-core/src/model/traversal/traversal_model_error.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use super::state::traversal_state::TraversalState; +use crate::util::cache_policy::cache_error::CacheError; use crate::util::unit::UnitError; #[derive(thiserror::Error, Debug)] @@ -19,6 +20,8 @@ pub enum TraversalModelError { InternalError(String), #[error(transparent)] TraversalUnitsError(#[from] UnitError), + #[error(transparent)] + CacheError(#[from] CacheError), #[error("prediction model failed with error {0}")] PredictionModel(String), } diff --git a/rust/routee-compass-core/src/util/cache_policy/cache_error.rs b/rust/routee-compass-core/src/util/cache_policy/cache_error.rs new file mode 100644 index 00000000..6087fc9f --- /dev/null +++ b/rust/routee-compass-core/src/util/cache_policy/cache_error.rs @@ -0,0 +1,7 @@ +#[derive(thiserror::Error, Debug)] +pub enum CacheError { + #[error("Could not build cache policy due to {0}")] + BuildError(String), + #[error("Could not get value from cache due to {0}")] + RuntimeError(String), +} diff --git a/rust/routee-compass-core/src/util/cache_policy/float_cache_policy.rs b/rust/routee-compass-core/src/util/cache_policy/float_cache_policy.rs new file mode 100644 index 00000000..b55579a4 --- /dev/null +++ b/rust/routee-compass-core/src/util/cache_policy/float_cache_policy.rs @@ -0,0 +1,133 @@ +use std::{num::NonZeroUsize, sync::Mutex}; + +use lru::LruCache; +use serde::{Deserialize, Serialize}; + +use super::cache_error::CacheError; + +fn to_precision(value: f64, precision: i32) -> i64 { + let multiplier = 10f64.powi(precision as i32); + (value * multiplier).round() as i64 +} + +#[derive(Serialize, Deserialize)] +pub struct FloatCachePolicyConfig { + pub cache_size: usize, + pub key_precisions: Vec, +} + +/// A cache policy that uses a float key to store a float value +/// The key is rounded to the specified precision. +/// +/// # Example +/// +/// ``` +/// use routee_compass_core::util::cache_policy::float_cache_policy::{FloatCachePolicy, FloatCachePolicyConfig}; +/// use std::num::NonZeroUsize; +/// +/// let config = FloatCachePolicyConfig { +/// cache_size: 100, +/// key_precisions: vec![2, 2], +/// }; +/// +/// let cache_policy = FloatCachePolicy::from_config(config).unwrap(); +/// +/// // stores keys as [123, 235] +/// cache_policy.update(&[1.234, 2.345], 3.456).unwrap(); +/// +/// let value = cache_policy.get(&[1.234, 2.345]).unwrap().unwrap(); +/// assert_eq!(value, 3.456); +/// +/// // 1.233 rounds to 123 +/// let value = cache_policy.get(&[1.233, 2.345]).unwrap().unwrap(); +/// assert_eq!(value, 3.456); +/// +/// // 1.2 rounds to 120 +/// let value = cache_policy.get(&[1.2, 2.345]).unwrap(); +/// assert_eq!(value, None); +/// +/// // 2.344 rounds to 234 +/// let value = cache_policy.get(&[1.234, 2.344]).unwrap(); +/// assert_eq!(value, None); +pub struct FloatCachePolicy { + cache: Mutex, f64>>, + key_precisions: Vec, +} + +impl FloatCachePolicy { + pub fn from_config(config: FloatCachePolicyConfig) -> Result { + let size = NonZeroUsize::new(config.cache_size).ok_or(CacheError::BuildError( + "maximum_cache_size must be greater than 0".to_string(), + ))?; + let cache = Mutex::new(LruCache::new(size)); + for precision in config.key_precisions.iter() { + if (*precision > 10) || (*precision < -10) { + return Err(CacheError::BuildError( + "key_precision must be betwee -10 and 10".to_string(), + )); + } + } + Ok(Self { + cache, + key_precisions: config.key_precisions, + }) + } + + pub fn float_key_to_int_key(&self, key: &[f64]) -> Vec { + key.iter() + .zip(self.key_precisions.iter()) + .map(|(v, p)| to_precision(*v, *p)) + .collect() + } + + pub fn get(&self, key: &[f64]) -> Result, CacheError> { + let int_key = self.float_key_to_int_key(key); + let mut cache = self.cache.lock().map_err(|e| { + CacheError::RuntimeError(format!("Could not get lock on cache due to {}", e)) + })?; + Ok(cache.get(&int_key).copied()) + } + + pub fn update(&self, key: &[f64], value: f64) -> Result<(), CacheError> { + let int_key = self.float_key_to_int_key(key); + let mut cache = self.cache.lock().map_err(|e| { + CacheError::RuntimeError(format!("Could not get lock on cache due to {}", e)) + })?; + cache.put(int_key, value); + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_float_cache_policy() { + let config = FloatCachePolicyConfig { + cache_size: 100, + key_precisions: vec![1, 3], + }; + + let cache_policy = FloatCachePolicy::from_config(config).unwrap(); + + // should store keys as [12, 2345] + cache_policy.update(&[1.234, 2.345], 3.456).unwrap(); + + // same in same out + let value = cache_policy.get(&[1.234, 2.345]).unwrap().unwrap(); + assert_eq!(value, 3.456); + + // 1.15 rounds to 12 + let value = cache_policy.get(&[1.15, 2.345]).unwrap().unwrap(); + assert_eq!(value, 3.456); + + // 1.14 rounds to 11 + let value = cache_policy.get(&[1.14, 2.345]).unwrap(); + assert_eq!(value, None); + + // 2.344 rounds to 2344 + let value = cache_policy.get(&[1.234, 2.344]).unwrap(); + assert_eq!(value, None); + } +} diff --git a/rust/routee-compass-core/src/util/cache_policy/mod.rs b/rust/routee-compass-core/src/util/cache_policy/mod.rs new file mode 100644 index 00000000..d78087fb --- /dev/null +++ b/rust/routee-compass-core/src/util/cache_policy/mod.rs @@ -0,0 +1,2 @@ +pub mod cache_error; +pub mod float_cache_policy; diff --git a/rust/routee-compass-core/src/util/mod.rs b/rust/routee-compass-core/src/util/mod.rs index 23b962ed..4598b1a3 100644 --- a/rust/routee-compass-core/src/util/mod.rs +++ b/rust/routee-compass-core/src/util/mod.rs @@ -1,3 +1,4 @@ +pub mod cache_policy; pub mod conversion; pub mod duration_extension; pub mod fs; diff --git a/rust/routee-compass-powertrain/Cargo.toml b/rust/routee-compass-powertrain/Cargo.toml index fe0f9979..175fab0c 100644 --- a/rust/routee-compass-powertrain/Cargo.toml +++ b/rust/routee-compass-powertrain/Cargo.toml @@ -27,11 +27,4 @@ rayon = { workspace = true } [features] default = ["onnx"] -onnx = ["ort", "ureq", "flate2", "tar", "zip", "sha256"] - -[build-dependencies] -sha256 = { version = "1.4", optional = true } -ureq = { version = "2.9", optional = true } -flate2 = { workspace = true, optional = true } -tar = { version = "0.4", optional = true } -zip = { version = "0.6", optional = true } +onnx = ["ort"] diff --git a/rust/routee-compass-powertrain/src/routee/energy_traversal_model.rs b/rust/routee-compass-powertrain/src/routee/energy_traversal_model.rs index a43dc69b..021ffaf9 100644 --- a/rust/routee-compass-powertrain/src/routee/energy_traversal_model.rs +++ b/rust/routee-compass-powertrain/src/routee/energy_traversal_model.rs @@ -280,6 +280,7 @@ mod tests { EnergyRateUnit::GallonsGasolinePerMile, None, None, + None, ) .unwrap(); diff --git a/rust/routee-compass-powertrain/src/routee/prediction/onnx/onnx_speed_grade_model.rs b/rust/routee-compass-powertrain/src/routee/prediction/onnx/onnx_speed_grade_model.rs index c7113a7e..4bf174dc 100644 --- a/rust/routee-compass-powertrain/src/routee/prediction/onnx/onnx_speed_grade_model.rs +++ b/rust/routee-compass-powertrain/src/routee/prediction/onnx/onnx_speed_grade_model.rs @@ -23,6 +23,7 @@ impl PredictionModel for OnnxSpeedGradeModel { ) -> Result<(EnergyRate, EnergyRateUnit), TraversalModelError> { let (speed, speed_unit) = speed; let (grade, grade_unit) = grade; + let speed_value: f32 = speed_unit.convert(speed, self.speed_unit).as_f64() as f32; let grade_value: f32 = grade_unit.convert(grade, self.grade_unit).as_f64() as f32; let array = ndarray::Array1::from(vec![speed_value, grade_value]) diff --git a/rust/routee-compass-powertrain/src/routee/prediction/prediction_model_ops.rs b/rust/routee-compass-powertrain/src/routee/prediction/prediction_model_ops.rs index f825f724..14092d21 100644 --- a/rust/routee-compass-powertrain/src/routee/prediction/prediction_model_ops.rs +++ b/rust/routee-compass-powertrain/src/routee/prediction/prediction_model_ops.rs @@ -2,7 +2,10 @@ use std::{path::Path, sync::Arc}; use routee_compass_core::{ model::traversal::traversal_model_error::TraversalModelError, - util::unit::{EnergyRate, EnergyRateUnit, Grade, GradeUnit, Speed, SpeedUnit}, + util::{ + cache_policy::float_cache_policy::FloatCachePolicy, + unit::{EnergyRate, EnergyRateUnit, Grade, GradeUnit, Speed, SpeedUnit}, + }, }; use super::{ @@ -15,7 +18,7 @@ use crate::routee::prediction::onnx::onnx_speed_grade_model::OnnxSpeedGradeModel #[allow(clippy::too_many_arguments)] pub fn load_prediction_model>( - model_name: String, + name: String, model_path: &P, model_type: ModelType, speed_unit: SpeedUnit, @@ -23,6 +26,7 @@ pub fn load_prediction_model>( energy_rate_unit: EnergyRateUnit, ideal_energy_rate_option: Option, real_world_energy_adjustment_option: Option, + cache: Option, ) -> Result { let prediction_model: Arc = match model_type { ModelType::Smartcore => { @@ -58,7 +62,7 @@ pub fn load_prediction_model>( let real_world_energy_adjustment = real_world_energy_adjustment_option.unwrap_or(1.0); Ok(PredictionModelRecord { - name: model_name, + name, prediction_model, model_type, speed_unit, @@ -66,6 +70,7 @@ pub fn load_prediction_model>( energy_rate_unit, ideal_energy_rate, real_world_energy_adjustment, + cache, }) } diff --git a/rust/routee-compass-powertrain/src/routee/prediction/prediction_model_record.rs b/rust/routee-compass-powertrain/src/routee/prediction/prediction_model_record.rs index 1e97490b..ef8b10aa 100644 --- a/rust/routee-compass-powertrain/src/routee/prediction/prediction_model_record.rs +++ b/rust/routee-compass-powertrain/src/routee/prediction/prediction_model_record.rs @@ -2,9 +2,12 @@ use std::sync::Arc; use routee_compass_core::{ model::traversal::traversal_model_error::TraversalModelError, - util::unit::{ - Distance, DistanceUnit, Energy, EnergyRate, EnergyRateUnit, EnergyUnit, Grade, GradeUnit, - Speed, SpeedUnit, + util::{ + cache_policy::float_cache_policy::FloatCachePolicy, + unit::{ + as_f64::AsF64, Distance, DistanceUnit, Energy, EnergyRate, EnergyRateUnit, EnergyUnit, + Grade, GradeUnit, Speed, SpeedUnit, + }, }, }; @@ -19,6 +22,7 @@ pub struct PredictionModelRecord { pub energy_rate_unit: EnergyRateUnit, pub ideal_energy_rate: EnergyRate, pub real_world_energy_adjustment: f64, + pub cache: Option, } impl PredictionModelRecord { @@ -29,7 +33,26 @@ impl PredictionModelRecord { distance: (Distance, DistanceUnit), ) -> Result<(Energy, EnergyUnit), TraversalModelError> { let (distance, distance_unit) = distance; - let (energy_rate, _energy_rate_unit) = self.prediction_model.predict(speed, grade)?; + + let energy_rate = match &self.cache { + Some(cache) => { + let key = vec![speed.0.as_f64(), grade.0.as_f64()]; + match cache.get(&key)? { + Some(er) => EnergyRate::new(er), + None => { + let (energy_rate, _energy_rate_unit) = + self.prediction_model.predict(speed, grade)?; + cache.update(&key, energy_rate.as_f64())?; + energy_rate + } + } + } + None => { + let (energy_rate, _energy_rate_unit) = + self.prediction_model.predict(speed, grade)?; + energy_rate + } + }; let energy_rate_real_world = energy_rate * self.real_world_energy_adjustment; @@ -39,6 +62,7 @@ impl PredictionModelRecord { distance, distance_unit, )?; + Ok((energy, energy_unit)) } } diff --git a/rust/routee-compass-powertrain/src/routee/vehicle/default/dual_fuel_vehicle.rs b/rust/routee-compass-powertrain/src/routee/vehicle/default/dual_fuel_vehicle.rs index 80d32a60..3ab7427b 100644 --- a/rust/routee-compass-powertrain/src/routee/vehicle/default/dual_fuel_vehicle.rs +++ b/rust/routee-compass-powertrain/src/routee/vehicle/default/dual_fuel_vehicle.rs @@ -299,6 +299,7 @@ mod tests { EnergyRateUnit::GallonsGasolinePerMile, Some(EnergyRate::new(0.02)), Some(1.1252), + None, ) .unwrap(); let charge_depleting_model_record = load_prediction_model( @@ -310,6 +311,7 @@ mod tests { EnergyRateUnit::KilowattHoursPerMile, Some(EnergyRate::new(0.2)), Some(1.3958), + None, ) .unwrap(); diff --git a/rust/routee-compass/src/app/compass/config/compass_configuration_error.rs b/rust/routee-compass/src/app/compass/config/compass_configuration_error.rs index 2d4fb498..e430527d 100644 --- a/rust/routee-compass/src/app/compass/config/compass_configuration_error.rs +++ b/rust/routee-compass/src/app/compass/config/compass_configuration_error.rs @@ -5,7 +5,7 @@ use routee_compass_core::{ frontier::frontier_model_error::FrontierModelError, road_network::graph_error::GraphError, traversal::traversal_model_error::TraversalModelError, }, - util::conversion::conversion_error::ConversionError, + util::{cache_policy::cache_error::CacheError, conversion::conversion_error::ConversionError}, }; #[derive(thiserror::Error, Debug)] @@ -62,6 +62,8 @@ pub enum CompassConfigurationError { #[error(transparent)] ConversionError(#[from] ConversionError), #[error(transparent)] + CacheError(#[from] CacheError), + #[error(transparent)] TraversalModelError(#[from] TraversalModelError), #[error(transparent)] FrontierModelError(#[from] FrontierModelError), diff --git a/rust/routee-compass/src/app/compass/config/traversal_model/energy_model_vehicle_builders.rs b/rust/routee-compass/src/app/compass/config/traversal_model/energy_model_vehicle_builders.rs index b1b1bd90..702e0661 100644 --- a/rust/routee-compass/src/app/compass/config/traversal_model/energy_model_vehicle_builders.rs +++ b/rust/routee-compass/src/app/compass/config/traversal_model/energy_model_vehicle_builders.rs @@ -1,7 +1,8 @@ use std::sync::Arc; -use routee_compass_core::util::unit::{ - Energy, EnergyRate, EnergyRateUnit, EnergyUnit, GradeUnit, SpeedUnit, +use routee_compass_core::util::{ + cache_policy::float_cache_policy::{FloatCachePolicy, FloatCachePolicyConfig}, + unit::{Energy, EnergyRate, EnergyRateUnit, EnergyUnit, GradeUnit, SpeedUnit}, }; use routee_compass_powertrain::routee::{ prediction::{load_prediction_model, model_type::ModelType, PredictionModelRecord}, @@ -127,6 +128,16 @@ fn get_model_record_from_params( parent_key.clone(), )?; + let cache_config = parameters.get_config_serde_optional::( + String::from("float_cache_policy"), + parent_key.clone(), + )?; + + let cache = match cache_config { + Some(config) => Some(FloatCachePolicy::from_config(config)?), + None => None, + }; + let model_record = load_prediction_model( name.clone(), &model_path, @@ -136,6 +147,7 @@ fn get_model_record_from_params( energy_rate_unit, ideal_energy_rate_option, real_world_energy_adjustment_option, + cache, )?; Ok(model_record)