use super::*;
use crate::construction::enablers::{JobTie, VehicleTie};
use crate::construction::features::*;
use vrp_core::construction::clustering::vicinity::ClusterDimension;
use vrp_core::construction::features::*;
use vrp_core::models::common::{LoadOps, MultiDimLoad, SingleDimLoad};
use vrp_core::models::problem::{ActivityCost, Actor, Jobs, Single, TransportCost};
use vrp_core::models::{Feature, GoalContext, Lock};
#[allow(clippy::too_many_arguments)]
pub(crate) fn create_goal_context(
api_problem: &ApiProblem,
jobs: &Jobs,
job_index: &JobIndex,
fleet: &CoreFleet,
transport: Arc<dyn TransportCost + Send + Sync>,
activity: Arc<dyn ActivityCost + Send + Sync>,
props: &ProblemProperties,
locks: &[Arc<Lock>],
) -> Result<GoalContext, String> {
let mut features = Vec::new();
let objective_features = get_objective_features(api_problem, props, transport.clone(), activity.clone())?;
let (global_objective_map, local_objective_map) = extract_feature_map(objective_features.as_slice())?;
features.extend(objective_features.into_iter().flat_map(|features| features.into_iter()));
if props.has_unreachable_locations {
features.push(create_reachable_feature("reachable", transport.clone(), REACHABLE_CONSTRAINT_CODE)?)
}
features.push(get_capacity_feature("capacity", api_problem, jobs, job_index, props)?);
if props.has_tour_travel_limits {
features.push(get_tour_limit_feature("tour_limit", api_problem, transport.clone())?)
}
if props.has_breaks {
features.push(create_optional_break_feature("break", BREAK_CONSTRAINT_CODE)?)
}
if props.has_order && !global_objective_map.iter().flat_map(|o| o.iter()).any(|name| *name == "tour_order") {
features.push(create_tour_order_hard_feature("tour_order", TOUR_ORDER_CONSTRAINT_CODE, get_tour_order_fn())?)
}
if props.has_compatibility {
features.push(create_compatibility_feature("compatibility", COMPATIBILITY_CONSTRAINT_CODE, COMPATIBILITY_KEY)?);
}
if props.has_group {
features.push(create_group_feature("group", jobs.size(), GROUP_CONSTRAINT_CODE, GROUP_KEY)?);
}
if props.has_skills {
features.push(create_skills_feature("skills", SKILL_CONSTRAINT_CODE)?)
}
if props.has_dispatch {
features.push(create_dispatch_feature("dispatch", DISPATCH_CONSTRAINT_CODE)?)
}
if !locks.is_empty() {
features.push(create_locked_jobs_feature("locked_jobs", fleet, locks, LOCKING_CONSTRAINT_CODE)?);
}
if props.has_tour_size_limits {
features.push(create_activity_limit_feature(
"activity_limit",
TOUR_SIZE_CONSTRAINT_CODE,
Arc::new(|actor| actor.vehicle.dimens.get_tour_size()),
)?);
}
GoalContext::new(features.as_slice(), global_objective_map.as_slice(), local_objective_map.as_slice())
}
fn get_objective_features(
api_problem: &ApiProblem,
props: &ProblemProperties,
transport: Arc<dyn TransportCost + Send + Sync>,
activity: Arc<dyn ActivityCost + Send + Sync>,
) -> Result<Vec<Vec<Feature>>, String> {
let objectives = if let Some(objectives) = api_problem.objectives.clone() {
objectives
} else {
let mut objectives = vec![
vec![Objective::MinimizeUnassignedJobs { breaks: Some(1.) }],
vec![Objective::MinimizeTours],
vec![Objective::MinimizeCost],
];
if props.has_value {
objectives.insert(0, vec![Objective::MaximizeValue { breaks: None }])
}
objectives
};
objectives
.iter()
.map(|objectives| {
objectives
.iter()
.map(|objective| match objective {
Objective::MinimizeCost => create_minimize_transport_costs_feature(
"min_cost",
transport.clone(),
activity.clone(),
TIME_CONSTRAINT_CODE,
),
Objective::MinimizeDistance => create_minimize_distance_feature(
"min_distance",
transport.clone(),
activity.clone(),
TIME_CONSTRAINT_CODE,
),
Objective::MinimizeDuration => create_minimize_duration_feature(
"min_duration",
transport.clone(),
activity.clone(),
TIME_CONSTRAINT_CODE,
),
Objective::MinimizeTours => create_minimize_tours_feature("min_tours"),
Objective::MaximizeTours => create_maximize_tours_feature("max_tours"),
Objective::MaximizeValue { breaks } => create_maximize_total_job_value_feature(
"max_value",
JobReadValueFn::Left(Arc::new({
let break_value = *breaks;
move |job| {
job.dimens().get_job_value().unwrap_or_else(|| {
job.dimens()
.get_job_type()
.zip(break_value)
.filter(|(job_type, _)| *job_type == "break")
.map(|(_, break_value)| break_value)
.unwrap_or(0.)
})
}
})),
Arc::new(|job, value| match job {
CoreJob::Single(single) => {
let mut dimens = single.dimens.clone();
dimens.set_job_value(Some(value));
CoreJob::Single(Arc::new(Single { places: single.places.clone(), dimens }))
}
_ => job.clone(),
}),
-1,
),
Objective::MinimizeUnassignedJobs { breaks } => create_minimize_unassigned_jobs_feature(
"min_unassigned",
Arc::new({
let break_value = *breaks;
let default_value = 1.;
move |_, job| {
if let Some(clusters) = job.dimens().get_cluster() {
clusters.len() as f64 * default_value
} else {
job.dimens().get_job_type().map_or(default_value, |job_type| {
match job_type.as_str() {
"break" => break_value.unwrap_or(default_value),
"reload" => 0.,
_ => default_value,
}
})
}
}
}),
),
Objective::MinimizeArrivalTime => create_minimize_arrival_time_feature("min_arrival_time"),
Objective::BalanceMaxLoad { options } => {
if props.has_multi_dimen_capacity {
create_max_load_balanced_feature::<MultiDimLoad>(
"max_load_balance",
get_threshold(options),
Arc::new(|loaded, capacity| {
let mut max_ratio = 0_f64;
for (idx, value) in capacity.load.iter().enumerate() {
let ratio = loaded.load[idx] as f64 / *value as f64;
max_ratio = max_ratio.max(ratio);
}
max_ratio
}),
)
} else {
create_max_load_balanced_feature::<SingleDimLoad>(
"max_load_balance",
get_threshold(options),
Arc::new(|loaded, capacity| loaded.value as f64 / capacity.value as f64),
)
}
}
Objective::BalanceActivities { options } => {
create_activity_balanced_feature("activity_balance", get_threshold(options))
}
Objective::BalanceDistance { options } => {
create_distance_balanced_feature("distance_balance", get_threshold(options))
}
Objective::BalanceDuration { options } => {
create_duration_balanced_feature("duration_balance", get_threshold(options))
}
Objective::TourOrder => {
create_tour_order_soft_feature("tour_order", TOUR_ORDER_KEY, get_tour_order_fn())
}
})
.collect()
})
.collect()
}
fn get_capacity_feature(
name: &str,
api_problem: &ApiProblem,
jobs: &Jobs,
job_index: &JobIndex,
props: &ProblemProperties,
) -> Result<Feature, String> {
if props.has_reloads {
let threshold = 0.9;
if props.has_multi_dimen_capacity {
get_capacity_with_reload_feature::<MultiDimLoad>(
name,
api_problem,
jobs,
job_index,
MultiDimLoad::new,
Box::new(move |capacity| *capacity * threshold),
)
} else {
get_capacity_with_reload_feature::<SingleDimLoad>(
name,
api_problem,
jobs,
job_index,
|capacity| SingleDimLoad::new(capacity.first().cloned().unwrap_or_default()),
Box::new(move |capacity| *capacity * threshold),
)
}
} else if props.has_multi_dimen_capacity {
create_capacity_limit_feature::<MultiDimLoad>(name, CAPACITY_CONSTRAINT_CODE)
} else {
create_capacity_limit_feature::<SingleDimLoad>(name, CAPACITY_CONSTRAINT_CODE)
}
}
fn get_capacity_with_reload_feature<T: LoadOps + SharedResource>(
name: &str,
api_problem: &ApiProblem,
jobs: &Jobs,
job_index: &JobIndex,
capacity_map: fn(Vec<i32>) -> T,
load_schedule_threshold_fn: LoadScheduleThresholdFn<T>,
) -> Result<Feature, String> {
let reload_resources = get_reload_resources(api_problem, job_index, capacity_map);
let capacity_feature_factory: CapacityFeatureFactoryFn<T> = Box::new(|name, multi_trip| {
create_capacity_limit_with_multi_trip_feature(name, CAPACITY_CONSTRAINT_CODE, multi_trip)
});
if reload_resources.is_empty() {
create_simple_reload_multi_trip_feature(name, capacity_feature_factory, load_schedule_threshold_fn)
} else {
create_shared_reload_multi_trip_feature(
name,
capacity_feature_factory,
load_schedule_threshold_fn,
reload_resources,
jobs.size(),
RELOAD_RESOURCE_CONSTRAINT_CODE,
RELOAD_RESOURCE_KEY,
)
}
}
fn get_tour_limit_feature(
name: &str,
api_problem: &ApiProblem,
transport: Arc<dyn TransportCost + Send + Sync>,
) -> Result<Feature, String> {
let (distances, durations) = api_problem
.fleet
.vehicles
.iter()
.filter_map(|vehicle| vehicle.limits.as_ref().map(|limits| (vehicle, limits)))
.fold((HashMap::new(), HashMap::new()), |(mut distances, mut durations), (vehicle, limits)| {
limits.max_distance.iter().for_each(|max_distance| {
distances.insert(vehicle.type_id.clone(), *max_distance);
});
limits.shift_time.iter().for_each(|shift_time| {
durations.insert(vehicle.type_id.clone(), *shift_time);
});
(distances, durations)
});
let get_limit = |limit_map: HashMap<String, f64>| {
Arc::new(move |actor: &Actor| {
actor.vehicle.dimens.get_vehicle_type().and_then(|v_type| limit_map.get(v_type)).cloned()
})
};
create_travel_limit_feature(
name,
transport.clone(),
get_limit(distances),
get_limit(durations),
DISTANCE_LIMIT_CONSTRAINT_CODE,
DURATION_LIMIT_CONSTRAINT_CODE,
)
}
fn get_reload_resources<T>(
api_problem: &ApiProblem,
job_index: &JobIndex,
capacity_map: fn(Vec<i32>) -> T,
) -> HashMap<CoreJob, (T, SharedResourceId)>
where
T: LoadOps + SharedResource,
{
let available_resources = api_problem
.fleet
.resources
.as_ref()
.iter()
.flat_map(|resources| resources.iter())
.map(|resource| match resource {
VehicleResource::Reload { id, capacity } => (id.clone(), capacity.clone()),
})
.collect::<Vec<_>>();
let total_resources_specified = available_resources.len();
let available_resources = available_resources
.into_iter()
.enumerate()
.map(|(idx, (id, capacity))| (id, (idx, capacity)))
.collect::<HashMap<_, _>>();
assert_eq!(total_resources_specified, available_resources.len());
api_problem
.fleet
.vehicles
.iter()
.flat_map(|vehicle| {
vehicle
.shifts
.iter()
.enumerate()
.flat_map(|(shift_idx, vehicle_shift)| {
vehicle_shift
.reloads
.iter()
.flatten()
.enumerate()
.map(move |(reload_idx, reload)| (shift_idx, reload_idx + 1, reload))
})
.filter_map(|(shift_idx, place_idx, reload)| {
reload
.resource_id
.as_ref()
.and_then(|resource_id| available_resources.get(resource_id))
.map(|(resource_id, capacity)| (shift_idx, place_idx, *resource_id, capacity.clone()))
})
.flat_map(move |(shift_idx, place_idx, resource_id, capacity)| {
vehicle.vehicle_ids.iter().filter_map(move |vehicle_id| {
let job_id = format!("{}_reload_{}_{}", vehicle_id, shift_idx, place_idx);
let capacity = capacity_map(capacity.clone());
job_index.get(&job_id).map(|job| (job.clone(), (capacity, resource_id)))
})
})
})
.collect()
}
#[allow(clippy::type_complexity)]
fn extract_feature_map(features: &[Vec<Feature>]) -> Result<(Vec<Vec<String>>, Vec<Vec<String>>), String> {
let objective_map: Vec<Vec<String>> = features
.iter()
.map(|features| features.iter().filter_map(|f| f.objective.as_ref().map(|_| f.name.clone())).collect())
.collect();
Ok((objective_map.clone(), objective_map))
}
fn get_threshold(options: &Option<BalanceOptions>) -> Option<f64> {
options.as_ref().and_then(|o| o.threshold)
}
fn get_tour_order_fn() -> TourOrderFn {
TourOrderFn::Left(Arc::new(|single| {
single
.as_ref()
.map(|single| &single.dimens)
.map(|dimens| {
dimens.get_job_order().map(|order| OrderResult::Value(order as f64)).unwrap_or_else(|| {
dimens.get_job_type().map_or(OrderResult::Default, |v| {
match v.as_str() {
"break" | "reload" | "dispatch" => OrderResult::Ignored,
_ => OrderResult::Default,
}
})
})
})
.unwrap_or(OrderResult::Ignored)
}))
}