Skip to content

Commit

Permalink
Introduce minimize arrival time objective
Browse files Browse the repository at this point in the history
  • Loading branch information
reinterpretcat committed Mar 9, 2022
1 parent 858b8b6 commit 31ac6ad
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 14 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ All notable changes to this project will be documented in this file.

### Added

* add stopping by terminal signal
* stopping by terminal signal
* `minimize-arrival-time` objective to prefer solutions where work is finished earlier.


## [v1.16.0] - 2022-03-03
Expand Down
3 changes: 2 additions & 1 deletion docs/src/concepts/pragmatic/problem/objectives.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ split into two groups.

### Scalar objectives

This objectives targeting for some scalar characteristic of solution:
These objectives targeting for some scalar characteristic of solution:

* `minimize-cost`: minimizes total transport cost calculated for all routes
* `minimize-distance`: minimizes total distance of all routes
Expand All @@ -33,6 +33,7 @@ constraints such as time windows. The objective has the following optional param
assignment leads to more jobs unassigned.
* `minimize-tours`: minimizes total amount of tours present in solution
* `maximize-tours`: maximizes total amount of tours present in solution
* `minimize-arrival-time`: prefers solutions where work is finished earlier
* `maximize-value`: maximizes total value of served jobs. It has optional parameters:
* `reductionFactor`: a factor to reduce value cost compared to max routing costs
* `breaks`: a value penalty for skipping a break. Default value is 100.
Expand Down
26 changes: 17 additions & 9 deletions vrp-core/src/construction/constraints/fleet_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::construction::constraints::{ConstraintModule, ConstraintVariant, Soft
use crate::construction::heuristics::{RouteContext, SolutionContext};
use crate::models::common::Cost;
use crate::models::problem::Job;
use std::ops::Deref;
use std::slice::Iter;
use std::sync::Arc;

Expand Down Expand Up @@ -34,31 +35,38 @@ impl ConstraintModule for FleetUsageConstraintModule {
impl FleetUsageConstraintModule {
/// Creates `FleetUsageConstraintModule` to minimize used fleet size.
pub fn new_minimized() -> Self {
Self::new_with_cost(1E12)
Self::new_with_cost(Box::new(|_| 1E12))
}

/// Creates `FleetUsageConstraintModule` to maximize used fleet size.
pub fn new_maximized() -> Self {
Self::new_with_cost(-1E12)
Self::new_with_cost(Box::new(|_| -1E12))
}

/// Creates `FleetUsageConstraintModule` with custom extra cost.
pub fn new_with_cost(extra_cost: Cost) -> Self {
/// Creates `FleetUsageConstraintModule` to minimize total arrival time.
pub fn new_earliest() -> Self {
Self::new_with_cost(Box::new(|route_ctx| {
// TODO find better approach to penalize later departures
route_ctx.route.actor.detail.time.start
}))
}

fn new_with_cost(extra_cost_fn: Box<dyn Fn(&RouteContext) -> Cost + Send + Sync>) -> Self {
Self {
state_keys: vec![],
constraints: vec![ConstraintVariant::SoftRoute(Arc::new(FleetCostSoftRouteConstraint { extra_cost }))],
constraints: vec![ConstraintVariant::SoftRoute(Arc::new(FleetCostSoftRouteConstraint { extra_cost_fn }))],
}
}
}

struct FleetCostSoftRouteConstraint {
extra_cost: Cost,
extra_cost_fn: Box<dyn Fn(&RouteContext) -> Cost + Send + Sync>,
}

impl SoftRouteConstraint for FleetCostSoftRouteConstraint {
fn estimate_job(&self, _: &SolutionContext, ctx: &RouteContext, _job: &Job) -> Cost {
if ctx.route.tour.job_count() == 0 {
self.extra_cost
fn estimate_job(&self, _: &SolutionContext, route_ctx: &RouteContext, _job: &Job) -> Cost {
if route_ctx.route.tour.job_count() == 0 {
self.extra_cost_fn.deref()(route_ctx)
} else {
0.
}
Expand Down
30 changes: 30 additions & 0 deletions vrp-core/src/solver/objectives/minimize_arrival_time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#[cfg(test)]
#[path = "../../../tests/unit/solver/objectives/minimize_arrival_time_test.rs"]
mod minimize_arrival_time_test;

use super::*;
use rosomaxa::prelude::*;

/// An objective function which prefers solution with less total arrival time.
#[derive(Default)]
pub struct MinimizeArrivalTime {}

impl Objective for MinimizeArrivalTime {
type Solution = InsertionContext;

fn fitness(&self, solution: &Self::Solution) -> f64 {
if solution.solution.routes.is_empty() {
0.
} else {
let total: f64 = solution
.solution
.routes
.iter()
.filter_map(|route_ctx| route_ctx.route.tour.end())
.map(|end| end.schedule.arrival)
.sum();

total / solution.solution.routes.len() as f64
}
}
}
3 changes: 3 additions & 0 deletions vrp-core/src/solver/objectives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use std::cmp::Ordering;
mod generic_value;
pub use self::generic_value::*;

mod minimize_arrival_time;
pub use self::minimize_arrival_time::*;

mod total_routes;
pub use self::total_routes::TotalRoutes;

Expand Down
4 changes: 2 additions & 2 deletions vrp-core/src/solver/objectives/total_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ impl Objective for TotalRoutes {
type Solution = InsertionContext;

fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering {
let fitness_a = a.solution.routes.len() as f64;
let fitness_b = b.solution.routes.len() as f64;
let fitness_a = self.fitness(a);
let fitness_b = self.fitness(b);

let (fitness_a, fitness_b) =
if self.is_minimization { (fitness_a, fitness_b) } else { (-1. * fitness_a, -1. * fitness_b) };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use super::*;
use crate::helpers::models::domain::*;
use crate::helpers::models::solution::*;
use crate::solver::objectives::MinimizeArrivalTime;

fn create_test_insertion_ctx(routes: &[f64]) -> InsertionContext {
let mut insertion_ctx = create_empty_insertion_context();
let problem = insertion_ctx.problem.clone();

routes.iter().for_each(|arrival| {
let mut route_ctx = create_route_context_with_activities(problem.fleet.as_ref(), "v1", vec![]);
route_ctx.route_mut().tour.all_activities_mut().last().unwrap().schedule.arrival = *arrival;

insertion_ctx.solution.routes.push(route_ctx);
});

insertion_ctx
}

#[test]
fn can_properly_estimate_empty_solution() {
let empty = create_empty_insertion_context();
let non_empty = create_test_insertion_ctx(&[10.]);

let result = MinimizeArrivalTime::default().total_order(&empty, &non_empty);

assert_eq!(result, Ordering::Less);
}

parameterized_test! {can_properly_estimate_solutions, (left, right, expected), {
can_properly_estimate_solutions_impl(left, right, expected);
}}

can_properly_estimate_solutions! {
case_01: (&[10.], &[10.], Ordering::Equal),
case_02: (&[10.], &[11.], Ordering::Less),
case_03: (&[10.], &[9.], Ordering::Greater),
case_04: (&[10.], &[10., 10.], Ordering::Equal),
case_05: (&[10.], &[10., 9.], Ordering::Greater),
case_06: (&[10.], &[10., 11.], Ordering::Less),
}

fn can_properly_estimate_solutions_impl(left: &[f64], right: &[f64], expected: Ordering) {
let left = create_test_insertion_ctx(left);
let right = create_test_insertion_ctx(right);

let result = MinimizeArrivalTime::default().total_order(&left, &right);

assert_eq!(result, expected);
}
4 changes: 4 additions & 0 deletions vrp-pragmatic/src/format/problem/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,10 @@ pub enum Objective {
breaks: Option<f64>,
},

/// An objective to minimize sum of arrival times from all routes.
#[serde(rename(deserialize = "minimize-arrival-time", serialize = "minimize-arrival-time"))]
MinimizeArrivalTime,

/// An objective to balance max load across all tours.
#[serde(rename(deserialize = "balance-max-load", serialize = "balance-max-load"))]
BalanceMaxLoad {
Expand Down
5 changes: 5 additions & 0 deletions vrp-pragmatic/src/format/problem/objective_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use vrp_core::models::common::ValueDimension;
use vrp_core::models::common::{MultiDimLoad, SingleDimLoad};
use vrp_core::models::problem::Job;
use vrp_core::models::problem::{ProblemObjective, Single, TargetConstraint, TargetObjective};
use vrp_core::solver::objectives::MinimizeArrivalTime as CoreMinimizeArrivalTime;
use vrp_core::solver::objectives::TourOrder as CoreTourOrder;
use vrp_core::solver::objectives::*;

Expand Down Expand Up @@ -58,6 +59,10 @@ pub fn create_objective(
core_objectives.push(Arc::new(get_unassigned_objective(1.)))
}
}
MinimizeArrivalTime => {
constraint.add_module(Arc::new(FleetUsageConstraintModule::new_earliest()));
core_objectives.push(Arc::new(CoreMinimizeArrivalTime::default()))
}
BalanceMaxLoad { options } => {
let (module, objective) = get_load_balance(props, options);
constraint.add_module(module);
Expand Down
1 change: 1 addition & 0 deletions vrp-pragmatic/src/validation/objectives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fn check_e1601_duplicate_objectives(objectives: &[&Objective]) -> Result<(), For
MaximizeTours => acc.entry("maximize-tours"),
MaximizeValue { .. } => acc.entry("maximize-value"),
MinimizeUnassignedJobs { .. } => acc.entry("minimize-unassigned"),
MinimizeArrivalTime => acc.entry("minimize-arrival-time"),
BalanceMaxLoad { .. } => acc.entry("balance-max-load"),
BalanceActivities { .. } => acc.entry("balance-activities"),
BalanceDistance { .. } => acc.entry("balance-distance"),
Expand Down
50 changes: 49 additions & 1 deletion vrp-pragmatic/tests/features/fleet/basic_multi_shift.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::format::problem::*;
use crate::format::solution::*;
use crate::format_time;
use crate::helpers::*;
use crate::{format_time, parse_time};
use vrp_core::utils::compare_floats;

#[test]
fn can_use_multiple_times_from_vehicle_and_job() {
Expand Down Expand Up @@ -139,3 +140,50 @@ fn can_use_multiple_times_from_vehicle_and_job() {
},
);
}

#[test]
fn can_prefer_first_days_with_minimize_arrival_time_objective() {
let problem = Problem {
plan: Plan {
jobs: vec![create_delivery_job("job1", vec![1., 0.]), create_delivery_job("job2", vec![1., 0.])],
..create_empty_plan()
},
objectives: Some(vec![
vec![Objective::MinimizeUnassignedJobs { breaks: None }],
vec![Objective::MinimizeArrivalTime],
vec![Objective::MinimizeCost],
]),
fleet: Fleet {
vehicles: vec![VehicleType {
shifts: vec![0., 100., 200., 300., 400.]
.iter()
.map(|earliest| VehicleShift {
start: ShiftStart {
earliest: format_time(*earliest),
latest: None,
location: vec![0., 0.].to_loc(),
},
end: None,
..create_default_vehicle_shift()
})
.collect(),
capacity: vec![1],
..create_default_vehicle_type()
}],
profiles: create_default_matrix_profiles(),
},
..create_empty_problem()
};
let matrix = create_matrix_from_problem(&problem);

let solution = solve_with_metaheuristic(problem, Some(vec![matrix]));

let mut departures = solution
.tours
.iter()
.filter_map(|tour| tour.stops.first())
.map(|stop| parse_time(&stop.schedule().departure))
.collect::<Vec<_>>();
departures.sort_by(|a, b| compare_floats(*a, *b));
assert_eq!(departures, vec![0., 100.]);
}

0 comments on commit 31ac6ad

Please sign in to comment.