diff --git a/vrp-cli/src/extensions/generate/plan.rs b/vrp-cli/src/extensions/generate/plan.rs index 1a3bbf8b9..859e06c21 100644 --- a/vrp-cli/src/extensions/generate/plan.rs +++ b/vrp-cli/src/extensions/generate/plan.rs @@ -5,8 +5,8 @@ mod plan_test; use super::get_random_item; use vrp_core::prelude::{Float, GenericError}; use vrp_core::utils::{DefaultRandom, Random}; -use vrp_pragmatic::format::Location; use vrp_pragmatic::format::problem::{Job, JobPlace, JobTask, Plan, Problem}; +use vrp_pragmatic::format::Location; /// Generates a new plan for given problem with amount of jobs specified by`jobs_size` and /// bounding box of size `area_size` (half size in meters). When not specified, jobs bounding @@ -73,7 +73,7 @@ pub(crate) fn generate_plan( }) .collect(); - Ok(Plan { jobs, relations: None, clustering: None }) + Ok(Plan { jobs, relations: None, clustering: None, strict_departure: None }) } type LocationFn = Box Location>; diff --git a/vrp-cli/src/extensions/import/csv.rs b/vrp-cli/src/extensions/import/csv.rs index ca283f9ba..bc3476333 100644 --- a/vrp-cli/src/extensions/import/csv.rs +++ b/vrp-cli/src/extensions/import/csv.rs @@ -78,7 +78,11 @@ mod actual { let get_tasks = |jobs: &Vec<&CsvJob>, filter: Box bool>| { let tasks = jobs.iter().filter(|j| (filter)(j)).map(|job| get_task(job)).collect::>(); - if tasks.is_empty() { None } else { Some(tasks) } + if tasks.is_empty() { + None + } else { + Some(tasks) + } }; let jobs = read_csv_entries::(reader)? @@ -155,7 +159,7 @@ mod actual { let matrix_profile_names = vehicles.iter().map(|v| v.profile.matrix.clone()).collect::>(); Ok(Problem { - plan: Plan { jobs, relations: None, clustering: None }, + plan: Plan { jobs, relations: None, clustering: None, strict_departure: None }, fleet: Fleet { vehicles, profiles: matrix_profile_names.into_iter().map(|name| MatrixProfile { name, speed: None }).collect(), @@ -169,8 +173,8 @@ mod actual { #[cfg(not(feature = "csv-format"))] mod actual { use std::io::{BufReader, Read}; - use vrp_pragmatic::format::FormatError; use vrp_pragmatic::format::problem::Problem; + use vrp_pragmatic::format::FormatError; /// A stub method for reading problem from csv format. pub fn read_csv_problem( diff --git a/vrp-core/src/construction/features/mod.rs b/vrp-core/src/construction/features/mod.rs index f54ca429f..42d7c1e32 100644 --- a/vrp-core/src/construction/features/mod.rs +++ b/vrp-core/src/construction/features/mod.rs @@ -15,7 +15,7 @@ pub(crate) use self::capacity::MaxVehicleLoadTourState; pub use self::capacity::{CapacityFeatureBuilder, JobDemandDimension, VehicleCapacityDimension}; mod compatibility; -pub use self::compatibility::{JobCompatibilityDimension, create_compatibility_feature}; +pub use self::compatibility::{create_compatibility_feature, JobCompatibilityDimension}; mod fast_service; pub use self::fast_service::FastServiceFeatureBuilder; @@ -24,7 +24,7 @@ mod fleet_usage; pub use self::fleet_usage::*; mod groups; -pub use self::groups::{JobGroupDimension, create_group_feature}; +pub use self::groups::{create_group_feature, JobGroupDimension}; mod hierarchical_areas; pub use self::hierarchical_areas::*; @@ -48,7 +48,7 @@ mod reloads; pub use self::reloads::{ReloadFeatureFactory, ReloadIntervalsTourState, SharedResource, SharedResourceId}; mod skills; -pub use self::skills::{JobSkills, JobSkillsDimension, VehicleSkillsDimension, create_skills_feature}; +pub use self::skills::{create_skills_feature, JobSkills, JobSkillsDimension, VehicleSkillsDimension}; mod total_value; pub use self::total_value::*; @@ -70,3 +70,6 @@ pub use self::work_balance::{ create_activity_balanced_feature, create_distance_balanced_feature, create_duration_balanced_feature, create_max_load_balanced_feature, }; + +mod strict_departure; +pub use self::strict_departure::create_strict_departure_feature; diff --git a/vrp-core/src/construction/features/strict_departure.rs b/vrp-core/src/construction/features/strict_departure.rs new file mode 100644 index 000000000..581e994ba --- /dev/null +++ b/vrp-core/src/construction/features/strict_departure.rs @@ -0,0 +1,47 @@ +//! This module provides functionailty to reject plans where +//! departures leave after a place's available times, instead +//! of merely having the restriction apply for arrival times +use std::sync::Arc; + +use rosomaxa::utils::GenericError; + +use crate::{ + models::{ConstraintViolation, Feature, FeatureBuilder, FeatureConstraint, ViolationCode}, + prelude::ActivityCost, +}; + +struct StrictDepartureConstraint { + time_constraint_code: ViolationCode, + activity: Arc, +} + +impl FeatureConstraint for StrictDepartureConstraint { + fn evaluate(&self, move_ctx: &crate::prelude::MoveContext<'_>) -> Option { + match move_ctx { + crate::prelude::MoveContext::Activity { activity_ctx, route_ctx, .. } => { + let activity_departure = self.activity.estimate_departure( + route_ctx.route(), + activity_ctx.target, + activity_ctx.target.schedule.arrival, + ); + let place_closing = activity_ctx.target.place.time.end; + if activity_departure > place_closing { + ConstraintViolation::fail(self.time_constraint_code) + } else { + None + } + } + _ => None, + } + } +} +pub fn create_strict_departure_feature( + name: &str, + activity: Arc, + time_constraint_code: ViolationCode, +) -> Result { + FeatureBuilder::default() + .with_name(name) + .with_constraint(StrictDepartureConstraint { activity, time_constraint_code }) + .build() +} diff --git a/vrp-pragmatic/src/format/problem/goal_reader.rs b/vrp-pragmatic/src/format/problem/goal_reader.rs index 1724b7efe..ae8331590 100644 --- a/vrp-pragmatic/src/format/problem/goal_reader.rs +++ b/vrp-pragmatic/src/format/problem/goal_reader.rs @@ -23,6 +23,13 @@ pub(super) fn create_goal_context( features.push(create_reachable_feature("reachable", blocks.transport.clone(), REACHABLE_CONSTRAINT_CODE)?) } + if props.has_strict_departure { + features.push(create_strict_departure_feature( + "strict_departure", + blocks.activity.clone(), + TIME_CONSTRAINT_CODE, + )?) + } features.push(get_capacity_feature("capacity", api_problem, blocks, props)?); if props.has_tour_travel_limits { diff --git a/vrp-pragmatic/src/format/problem/mod.rs b/vrp-pragmatic/src/format/problem/mod.rs index 114500a61..8899b6ace 100644 --- a/vrp-pragmatic/src/format/problem/mod.rs +++ b/vrp-pragmatic/src/format/problem/mod.rs @@ -4,8 +4,8 @@ use super::*; use crate::parse_time; use std::io::{BufReader, Read}; use std::sync::Arc; -use vrp_core::models::Lock; use vrp_core::models::common::TimeWindow; +use vrp_core::models::Lock; use vrp_core::prelude::{ActivityCost, Fleet as CoreFleet, Jobs as CoreJobs, TransportCost}; use vrp_core::utils::*; @@ -91,7 +91,11 @@ impl PragmaticProblem for ApiProblem { impl PragmaticProblem for (ApiProblem, Option>) { fn read_pragmatic(self) -> Result { - if let Some(matrices) = self.1 { (self.0, matrices).read_pragmatic() } else { self.0.read_pragmatic() } + if let Some(matrices) = self.1 { + (self.0, matrices).read_pragmatic() + } else { + self.0.read_pragmatic() + } } } @@ -109,6 +113,7 @@ struct ProblemProperties { has_compatibility: bool, has_tour_size_limits: bool, has_tour_travel_limits: bool, + has_strict_departure: bool, } /// Keeps track of materialized problem building blocks. diff --git a/vrp-pragmatic/src/format/problem/model.rs b/vrp-pragmatic/src/format/problem/model.rs index 341c09a87..444ac4e16 100644 --- a/vrp-pragmatic/src/format/problem/model.rs +++ b/vrp-pragmatic/src/format/problem/model.rs @@ -214,6 +214,7 @@ pub struct VicinityFilteringPolicy { /// A plan specifies work which has to be done. #[derive(Clone, Deserialize, Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct Plan { /// List of jobs. pub jobs: Vec, @@ -225,6 +226,10 @@ pub struct Plan { /// Specifies clustering parameters. #[serde(skip_serializing_if = "Option::is_none")] pub clustering: Option, + + /// whether to use strict departure logic + #[serde(skip_serializing_if = "Option::is_none")] + pub strict_departure: Option, } // endregion diff --git a/vrp-pragmatic/src/format/problem/problem_reader.rs b/vrp-pragmatic/src/format/problem/problem_reader.rs index b1aaf2628..b2f9a72c6 100644 --- a/vrp-pragmatic/src/format/problem/problem_reader.rs +++ b/vrp-pragmatic/src/format/problem/problem_reader.rs @@ -5,10 +5,10 @@ use crate::format::problem::goal_reader::create_goal_context; use crate::format::problem::job_reader::{read_jobs_with_extra_locks, read_locks}; use crate::format::{FormatError, JobIndex}; use crate::validation::ValidationContext; -use crate::{CoordIndex, parse_time}; +use crate::{parse_time, CoordIndex}; use vrp_core::construction::enablers::*; -use vrp_core::models::Extras; use vrp_core::models::common::{TimeOffset, TimeSpan, TimeWindow}; +use vrp_core::models::Extras; use vrp_core::solver::processing::{ClusterConfigExtraProperty, ReservedTimesExtraProperty}; pub(super) fn map_to_problem_with_approx(problem: ApiProblem) -> Result { @@ -104,7 +104,11 @@ fn read_reserved_times_index(api_problem: &ApiProblem, fleet: &CoreFleet) -> Res }) .collect::>(); - if times.is_empty() { None } else { Some((actor.clone(), times)) } + if times.is_empty() { + None + } else { + Some((actor.clone(), times)) + } }) .collect() } @@ -169,6 +173,7 @@ fn get_problem_properties(api_problem: &ApiProblem, matrices: &[Matrix]) -> Prob has_compatibility, has_tour_size_limits, has_tour_travel_limits, + has_strict_departure: api_problem.plan.strict_departure == Some(true), } }