Skip to content

Commit

Permalink
Move skills feature to core
Browse files Browse the repository at this point in the history
  • Loading branch information
reinterpretcat committed Jun 10, 2024
1 parent 119e29a commit 21c1faa
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 104 deletions.
2 changes: 1 addition & 1 deletion vrp-core/src/construction/features/breaks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl BreakCandidate<'_> {
}
}

/// Provides way to work with a break job.
/// Provides a way to work with a break job.
pub trait BreakAspects: Clone + Send + Sync {
/// Checks whether the job is a break job and it can be assigned to the given route.
fn belongs_to_route(&self, route_ctx: &RouteContext, candidate: BreakCandidate<'_>) -> bool;
Expand Down
3 changes: 3 additions & 0 deletions vrp-core/src/construction/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ pub use self::reloads::*;
mod shared_resource;
pub use self::shared_resource::*;

mod skills;
pub use self::skills::*;

mod total_value;
pub use self::total_value::*;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@
mod skills_test;

use super::*;
use crate::construction::enablers::{JobTie, VehicleTie};
use hashbrown::HashSet;

/// Provides a way to work with the job-vehicle skills feature.
pub trait JobSkillsAspects: Clone + Send + Sync {
/// Returns job skills if defined.
fn get_job_skills<'a>(&self, job: &'a Job) -> Option<&'a JobSkills>;

/// Returns job skills for vehicle if defined.
fn get_vehicle_skills<'a>(&self, vehicle: &'a Vehicle) -> Option<&'a HashSet<String>>;

/// Returns a violation code.
fn get_violation_code(&self) -> ViolationCode;
}

/// A job skills limitation for a vehicle.
pub struct JobSkills {
/// Vehicle should have all of these skills defined.
Expand All @@ -29,25 +40,28 @@ impl JobSkills {
}

/// Creates a skills feature as hard constraint.
pub fn create_skills_feature(name: &str, code: ViolationCode) -> Result<Feature, GenericError> {
FeatureBuilder::default().with_name(name).with_constraint(SkillsConstraint { code }).build()
pub fn create_skills_feature<A>(name: &str, aspects: A) -> Result<Feature, GenericError>
where
A: JobSkillsAspects + 'static,
{
FeatureBuilder::default().with_name(name).with_constraint(SkillsConstraint { aspects }).build()
}

struct SkillsConstraint {
code: ViolationCode,
struct SkillsConstraint<A: JobSkillsAspects> {
aspects: A,
}

impl FeatureConstraint for SkillsConstraint {
impl<A: JobSkillsAspects> FeatureConstraint for SkillsConstraint<A> {
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
match move_ctx {
MoveContext::Route { route_ctx, job, .. } => {
if let Some(job_skills) = job.dimens().get_job_skills() {
let vehicle_skills = route_ctx.route().actor.vehicle.dimens.get_vehicle_skills();
if let Some(job_skills) = self.aspects.get_job_skills(job) {
let vehicle_skills = self.aspects.get_vehicle_skills(&route_ctx.route().actor.vehicle);
let is_ok = check_all_of(job_skills, &vehicle_skills)
&& check_one_of(job_skills, &vehicle_skills)
&& check_none_of(job_skills, &vehicle_skills);
if !is_ok {
return ConstraintViolation::fail(self.code);
return ConstraintViolation::fail(self.aspects.get_violation_code());
}
}

Expand All @@ -58,8 +72,8 @@ impl FeatureConstraint for SkillsConstraint {
}

fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
let source_skills = source.dimens().get_job_skills();
let candidate_skills = candidate.dimens().get_job_skills();
let source_skills = self.aspects.get_job_skills(&source);
let candidate_skills = self.aspects.get_job_skills(&candidate);

let check_skill_sets = |source_set: Option<&HashSet<String>>, candidate_set: Option<&HashSet<String>>| match (
source_set,
Expand All @@ -83,7 +97,7 @@ impl FeatureConstraint for SkillsConstraint {
if has_comparable_skills {
Ok(source)
} else {
Err(self.code)
Err(self.aspects.get_violation_code())
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions vrp-core/tests/helpers/models/problem/fleet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ impl VehicleBuilder {
self
}

pub fn property<T: 'static + Sync + Send>(&mut self, key: &str, value: T) -> &mut Self {
self.0.dimens.insert(key.to_string(), Arc::new(value));
self
}

pub fn build(&mut self) -> Vehicle {
std::mem::replace(&mut self.0, test_vehicle(0))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,54 @@
use crate::construction::enablers::{JobTie, VehicleTie};
use crate::construction::features::skills::create_skills_feature;
use crate::construction::features::JobSkills;
use crate::helpers::*;
use crate::construction::features::{JobSkills, JobSkillsAspects};
use crate::construction::heuristics::MoveContext;
use crate::helpers::construction::heuristics::InsertionContextBuilder;
use crate::helpers::models::problem::{test_driver, FleetBuilder, SingleBuilder, VehicleBuilder};
use crate::helpers::models::solution::{RouteBuilder, RouteContextBuilder};
use crate::models::common::ValueDimension;
use crate::models::problem::{Job, Vehicle};
use crate::models::{ConstraintViolation, ViolationCode};
use hashbrown::HashSet;
use std::iter::FromIterator;
use std::sync::Arc;
use vrp_core::construction::enablers::create_typed_actor_groups;
use vrp_core::construction::heuristics::{MoveContext, RouteContext, RouteState};
use vrp_core::models::problem::{Fleet, Job, Vehicle};
use vrp_core::models::{ConstraintViolation, ViolationCode};

const VIOLATION_CODE: ViolationCode = 1;

#[derive(Clone)]
struct TestJobSkillsAspects;

impl JobSkillsAspects for TestJobSkillsAspects {
fn get_job_skills<'a>(&self, job: &'a Job) -> Option<&'a JobSkills> {
job.dimens().get_value("skills")
}

fn get_vehicle_skills<'a>(&self, vehicle: &'a Vehicle) -> Option<&'a HashSet<String>> {
vehicle.dimens.get_value("skills")
}

fn get_violation_code(&self) -> ViolationCode {
VIOLATION_CODE
}
}

fn create_job_with_skills(all_of: Option<Vec<&str>>, one_of: Option<Vec<&str>>, none_of: Option<Vec<&str>>) -> Job {
let mut single = create_single_with_location(None);
single.dimens.set_job_skills(Some(JobSkills {
all_of: all_of.map(|skills| skills.iter().map(|s| s.to_string()).collect()),
one_of: one_of.map(|skills| skills.iter().map(|s| s.to_string()).collect()),
none_of: none_of.map(|skills| skills.iter().map(|s| s.to_string()).collect()),
}));

Job::Single(Arc::new(single))
SingleBuilder::default()
.property(
"skills",
JobSkills {
all_of: all_of.map(|skills| skills.iter().map(|s| s.to_string()).collect()),
one_of: one_of.map(|skills| skills.iter().map(|s| s.to_string()).collect()),
none_of: none_of.map(|skills| skills.iter().map(|s| s.to_string()).collect()),
},
)
.build_as_job_ref()
}

fn create_vehicle_with_skills(skills: Option<Vec<&str>>) -> Vehicle {
let mut vehicle = test_vehicle("v1");

let mut builder = VehicleBuilder::default();
if let Some(skills) = skills {
vehicle.dimens.set_vehicle_skills(HashSet::<String>::from_iter(skills.iter().map(|s| s.to_string())));
builder.property("skills", HashSet::<String>::from_iter(skills.iter().map(|s| s.to_string())));
}

vehicle
builder.id("v1").build()
}

fn failure() -> Option<ConstraintViolation> {
Expand Down Expand Up @@ -81,21 +99,17 @@ fn can_check_skills_impl(
vehicle_skills: Option<Vec<&str>>,
expected: Option<ConstraintViolation>,
) {
let fleet = Fleet::new(
vec![Arc::new(test_driver())],
vec![Arc::new(create_vehicle_with_skills(vehicle_skills))],
|actors| {
create_typed_actor_groups(actors, |a| {
a.vehicle.dimens.get_vehicle_type().cloned().expect("no vehicle type set")
})
},
);
let fleet = FleetBuilder::default()
.add_driver(test_driver())
.add_vehicle(create_vehicle_with_skills(vehicle_skills))
.build();
let route_ctx =
RouteContext::new_with_state(create_route_with_activities(&fleet, "v1", vec![]), RouteState::default());
let constraint = create_skills_feature("skills", VIOLATION_CODE).unwrap().constraint.unwrap();
RouteContextBuilder::default().with_route(RouteBuilder::default().with_vehicle(&fleet, "v1").build()).build();

let constraint = create_skills_feature("skills", TestJobSkillsAspects).unwrap().constraint.unwrap();

let actual = constraint.evaluate(&MoveContext::route(
&create_solution_context_for_fleet(&fleet),
&InsertionContextBuilder::default().build().solution,
&route_ctx,
&create_job_with_skills(all_of, one_of, none_of),
));
Expand Down Expand Up @@ -125,7 +139,7 @@ can_merge_skills! {
}

fn can_merge_skills_impl(source: Job, candidate: Job, expected: Result<(), i32>) {
let constraint = create_skills_feature("skills", VIOLATION_CODE).unwrap().constraint.unwrap();
let constraint = create_skills_feature("skills", TestJobSkillsAspects).unwrap().constraint.unwrap();

let result = constraint.merge(source, candidate).map(|_| ());

Expand Down
3 changes: 1 addition & 2 deletions vrp-pragmatic/src/construction/enablers/entities.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Specifies different entities as extension points on Dimensions type.
use crate::construction::features::JobSkills;
use hashbrown::HashSet;
use vrp_core::construction::features::BreakPolicy;
use vrp_core::construction::features::{BreakPolicy, JobSkills};
use vrp_core::models::common::{Dimensions, ValueDimension};

/// Specifies vehicle entity.
Expand Down
9 changes: 0 additions & 9 deletions vrp-pragmatic/src/construction/features/mod.rs

This file was deleted.

1 change: 0 additions & 1 deletion vrp-pragmatic/src/construction/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//! Contains a building blocks to create a different VRP variants.
pub mod enablers;
pub mod features;
31 changes: 29 additions & 2 deletions vrp-pragmatic/src/format/problem/aspects.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::construction::enablers::{BreakTie, JobTie, VehicleTie};
use hashbrown::HashSet;
use std::marker::PhantomData;
use vrp_core::construction::features::{
BreakAspects, BreakCandidate, BreakPolicy, CompatibilityAspects, GroupAspects, RechargeAspects,
RechargeDistanceLimitFn, RechargeKeys, ReloadAspects,
BreakAspects, BreakCandidate, BreakPolicy, CompatibilityAspects, GroupAspects, JobSkills, JobSkillsAspects,
RechargeAspects, RechargeDistanceLimitFn, RechargeKeys, ReloadAspects,
};
use vrp_core::construction::heuristics::{RouteContext, StateKey};
use vrp_core::models::common::{CapacityDimension, Demand, DemandDimension, IdDimension, LoadOps};
Expand Down Expand Up @@ -158,6 +159,32 @@ impl<T: LoadOps> ReloadAspects<T> for PragmaticReloadAspects<T> {
}
}

#[derive(Clone)]
pub struct PragmaticJobSkillsAspects {
violation_code: ViolationCode,
}

impl PragmaticJobSkillsAspects {
/// Creates a new instance of `PragmaticJobSkillsAspects`.
pub fn new(violation_code: ViolationCode) -> Self {
Self { violation_code }
}
}

impl JobSkillsAspects for PragmaticJobSkillsAspects {
fn get_job_skills<'a>(&self, job: &'a Job) -> Option<&'a JobSkills> {
job.dimens().get_job_skills()
}

fn get_vehicle_skills<'a>(&self, vehicle: &'a Vehicle) -> Option<&'a HashSet<String>> {
vehicle.dimens.get_vehicle_skills()
}

fn get_violation_code(&self) -> ViolationCode {
self.violation_code
}
}

fn is_correct_vehicle(route: &Route, single: &Single) -> bool {
let job_vehicle_id = single.dimens.get_vehicle_id();
let job_shift_idx = single.dimens.get_shift_index();
Expand Down
3 changes: 1 addition & 2 deletions vrp-pragmatic/src/format/problem/goal_reader.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use super::*;
use crate::construction::enablers::{JobTie, VehicleTie};
use crate::construction::features::*;
use crate::format::problem::aspects::*;
use hashbrown::HashSet;
use vrp_core::construction::clustering::vicinity::ClusterDimension;
Expand Down Expand Up @@ -67,7 +66,7 @@ pub(super) fn create_goal_context(
}

if props.has_skills {
features.push(create_skills_feature("skills", SKILL_CONSTRAINT_CODE)?)
features.push(create_skills_feature("skills", PragmaticJobSkillsAspects::new(SKILL_CONSTRAINT_CODE))?)
}

if !blocks.locks.is_empty() {
Expand Down
2 changes: 1 addition & 1 deletion vrp-pragmatic/src/format/problem/job_reader.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::construction::enablers::{BreakTie, JobTie, VehicleTie};
use crate::construction::features::JobSkills as FeatureJobSkills;
use crate::format::coord_index::CoordIndex;
use crate::format::problem::JobSkills as ApiJobSkills;
use crate::format::problem::*;
Expand All @@ -8,6 +7,7 @@ use crate::utils::VariableJobPermutation;
use hashbrown::HashMap;
use std::sync::Arc;
use vrp_core::construction::features::BreakPolicy;
use vrp_core::construction::features::JobSkills as FeatureJobSkills;
use vrp_core::models::common::*;
use vrp_core::models::problem::{Actor, Fleet, Job, Jobs, Multi, Place, Single, TransportCost};
use vrp_core::models::{Lock, LockDetail, LockOrder, LockPosition};
Expand Down
35 changes: 1 addition & 34 deletions vrp-pragmatic/tests/helpers/core.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
use crate::construction::enablers::{JobTie, VehicleTie};
use crate::helpers::TestTransportCost;
use std::sync::Arc;
use vrp_core::construction::enablers::{create_typed_actor_groups, ScheduleKeys};
use vrp_core::construction::features::create_minimize_transport_costs_feature;
use vrp_core::construction::heuristics::{RegistryContext, SolutionContext, StateKeyRegistry};
use vrp_core::construction::enablers::create_typed_actor_groups;
use vrp_core::models::common::*;
use vrp_core::models::problem::*;
use vrp_core::models::solution::*;
use vrp_core::models::GoalContextBuilder;
use vrp_core::utils::DefaultRandom;

const DEFAULT_VEHICLE_COSTS: Costs =
Costs { fixed: 100.0, per_distance: 1.0, per_driving_time: 1.0, per_waiting_time: 1.0, per_service_time: 1.0 };
Expand Down Expand Up @@ -107,31 +102,3 @@ pub fn single_demand_as_multi(pickup: (i32, i32), delivery: (i32, i32)) -> Deman

Demand { pickup: (make(pickup.0), make(pickup.1)), delivery: (make(delivery.0), make(delivery.1)) }
}

pub fn create_solution_context_for_fleet(fleet: &Fleet) -> SolutionContext {
let feature = create_minimize_transport_costs_feature(
"min-costs",
TestTransportCost::new_shared(),
Arc::new(SimpleActivityCost::default()),
ScheduleKeys::from(&mut StateKeyRegistry::default()),
1,
)
.expect("cannot create transport cost feature");
let goal = GoalContextBuilder::with_features(vec![feature])
.expect("cannot create builder")
.set_goal(&["min-costs"], &["min-costs"])
.expect("cannot set goal")
.build()
.expect("cannot build goal context");
let registry = Registry::new(fleet, Arc::new(DefaultRandom::default()));

SolutionContext {
required: vec![],
ignored: vec![],
unassigned: Default::default(),
locked: Default::default(),
state: Default::default(),
routes: Default::default(),
registry: RegistryContext::new(&goal, registry),
}
}
7 changes: 0 additions & 7 deletions vrp-pragmatic/tests/helpers/problem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use crate::format::problem::*;
use crate::format::{CoordIndex, Location};
use crate::format_time;
use crate::helpers::ToLocation;
use std::sync::Arc;
use vrp_core::models::common::{Distance, Duration, Location as CoreLocation, Profile};
use vrp_core::models::problem::{TransportCost, TravelTime};
use vrp_core::models::solution::Route;
Expand Down Expand Up @@ -351,12 +350,6 @@ impl TransportCost for TestTransportCost {
}
}

impl TestTransportCost {
pub fn new_shared() -> Arc<dyn TransportCost + Sync + Send> {
Arc::new(Self::default())
}
}

fn fake_routing(from: CoreLocation, to: CoreLocation) -> f64 {
(if to > from { to - from } else { from - to }) as f64
}

0 comments on commit 21c1faa

Please sign in to comment.