#[cfg(test)]
#[path = "../../tests/unit/models/goal_test.rs"]
mod goal_test;
use crate::construction::enablers::*;
use crate::construction::heuristics::*;
use crate::models::common::Cost;
use crate::models::problem::Job;
use crate::utils::short_type_name;
use hashbrown::{HashMap, HashSet};
use rand::prelude::SliceRandom;
use rosomaxa::algorithms::nsga2::dominance_order;
use rosomaxa::population::Shuffled;
use rosomaxa::prelude::*;
use std::cmp::Ordering;
use std::fmt::{Debug, Formatter};
use std::slice::Iter;
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct GoalContext {
pub(crate) global_objectives: Vec<Vec<Arc<dyn FeatureObjective<Solution = InsertionContext> + Send + Sync>>>,
pub(crate) flatten_objectives: Vec<Arc<dyn FeatureObjective<Solution = InsertionContext> + Send + Sync>>,
pub(crate) local_objectives: Vec<Vec<Arc<dyn FeatureObjective<Solution = InsertionContext> + Send + Sync>>>,
pub(crate) constraints: Vec<Arc<dyn FeatureConstraint + Send + Sync>>,
pub(crate) states: Vec<Arc<dyn FeatureState + Send + Sync>>,
}
impl GoalContext {
pub fn new(
features: &[Feature],
global_objective_map: &[Vec<String>],
local_objective_map: &[Vec<String>],
) -> Result<Self, String> {
let ids_all = features
.iter()
.filter_map(|feature| feature.objective.as_ref().map(|_| feature.name.clone()))
.collect::<Vec<_>>();
let ids_unique = ids_all.iter().collect::<HashSet<_>>();
if ids_unique.len() != ids_all.len() {
return Err(format!(
"some of the features are defined more than once, check ids list: {}",
ids_all.join(",")
));
}
let check_objective_map = |objective_map: &[Vec<String>]| {
let objective_ids_all = objective_map.iter().flat_map(|objective| objective.iter()).collect::<Vec<_>>();
let objective_ids_unique = objective_ids_all.iter().cloned().collect::<HashSet<_>>();
objective_ids_all.len() == objective_ids_unique.len() && objective_ids_unique.is_subset(&ids_unique)
};
if !check_objective_map(global_objective_map) {
return Err(
"global objective map is invalid: it should contain unique ids of the features specified".to_string()
);
}
if !check_objective_map(local_objective_map) {
return Err(
"local objective map is invalid: it should contain unique ids of the features specified".to_string()
);
}
let feature_map = features
.iter()
.filter_map(|feature| feature.objective.as_ref().map(|objective| (feature.name.clone(), objective.clone())))
.collect::<HashMap<_, _>>();
let remap_objectives = |objective_map: &[Vec<String>]| -> Result<Vec<_>, String> {
objective_map.iter().try_fold(Vec::default(), |mut acc_outer, ids| {
acc_outer.push(ids.iter().try_fold(Vec::default(), |mut acc_inner, id| {
if let Some(objective) = feature_map.get(id) {
acc_inner.push(objective.clone());
Ok(acc_inner)
} else {
Err(format!("cannot find objective for feature with id: {id}"))
}
})?);
Ok(acc_outer)
})
};
let global_objectives = remap_objectives(global_objective_map)?;
let local_objectives = remap_objectives(local_objective_map)?;
let states = features.iter().filter_map(|feature| feature.state.clone()).collect();
let constraints = features.iter().filter_map(|feature| feature.constraint.clone()).collect();
let flatten_objectives = global_objectives.iter().flat_map(|inners| inners.iter()).cloned().collect();
Ok(Self { global_objectives, flatten_objectives, local_objectives, constraints, states })
}
}
impl Debug for GoalContext {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct(short_type_name::<Self>())
.field("global", &self.global_objectives.len())
.field("flatten", &self.flatten_objectives.len())
.field("local", &self.local_objectives.len())
.field("constraints", &self.constraints.len())
.field("states", &self.states.len())
.finish()
}
}
#[derive(Clone, Default)]
pub struct Feature {
pub name: String,
pub constraint: Option<Arc<dyn FeatureConstraint + Send + Sync>>,
pub objective: Option<Arc<dyn FeatureObjective<Solution = InsertionContext> + Send + Sync>>,
pub state: Option<Arc<dyn FeatureState + Send + Sync>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConstraintViolation {
pub code: ViolationCode,
pub stopped: bool,
}
impl ConstraintViolation {
pub fn fail(code: ViolationCode) -> Option<Self> {
Some(ConstraintViolation { code, stopped: true })
}
pub fn skip(code: ViolationCode) -> Option<Self> {
Some(ConstraintViolation { code, stopped: false })
}
pub fn success() -> Option<Self> {
None
}
}
pub type ViolationCode = i32;
pub type StateKey = i32;
#[derive(Default)]
pub struct FeatureBuilder {
feature: Feature,
}
impl FeatureBuilder {
pub fn combine(name: &str, features: &[Feature]) -> Result<Feature, String> {
combine_features(name, features)
}
pub fn from_feature(other: Feature) -> Self {
Self { feature: other }
}
pub fn with_name(mut self, name: &str) -> Self {
self.feature.name = name.to_string();
self
}
pub fn with_constraint<T: FeatureConstraint + Send + Sync + 'static>(mut self, constraint: T) -> Self {
self.feature.constraint = Some(Arc::new(constraint));
self
}
pub fn with_objective<T: FeatureObjective<Solution = InsertionContext> + Send + Sync + 'static>(
mut self,
objective: T,
) -> Self {
self.feature.objective = Some(Arc::new(objective));
self
}
pub fn with_state<T: FeatureState + Send + Sync + 'static>(mut self, state: T) -> Self {
self.feature.state = Some(Arc::new(state));
self
}
pub fn build(self) -> Result<Feature, String> {
let feature = self.feature;
if feature.name == String::default() {
return Err("features with default id are not allowed".to_string());
}
if feature.constraint.is_none() && feature.objective.is_none() {
Err("empty feature is not allowed".to_string())
} else {
Ok(feature)
}
}
}
pub trait FeatureState {
fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, job: &Job);
fn accept_route_state(&self, route_ctx: &mut RouteContext);
fn accept_solution_state(&self, solution_ctx: &mut SolutionContext);
fn state_keys(&self) -> Iter<StateKey>;
}
pub trait FeatureConstraint {
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation>;
fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode>;
}
pub trait FeatureObjective: Objective {
fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost;
}
impl MultiObjective for GoalContext {
type Solution = InsertionContext;
fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering {
unwrap_from_result(self.global_objectives.iter().try_fold(
Ordering::Equal,
|_, objectives| match dominance_order(a, b, objectives.iter().map(|o| o.as_ref())) {
Ordering::Equal => Ok(Ordering::Equal),
order => Err(order),
},
))
}
fn fitness<'a>(&'a self, solution: &'a Self::Solution) -> Box<dyn Iterator<Item = f64> + 'a> {
Box::new(self.flatten_objectives.iter().map(|o| o.fitness(solution)))
}
fn get_order(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result<Ordering, String> {
self.flatten_objectives
.get(idx)
.map(|o| o.total_order(a, b))
.ok_or_else(|| format!("cannot get total_order with index: {idx}"))
}
fn get_distance(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result<f64, String> {
self.flatten_objectives
.get(idx)
.map(|o| o.distance(a, b))
.ok_or_else(|| format!("cannot get distance with index: {idx}"))
}
fn size(&self) -> usize {
self.flatten_objectives.len()
}
}
impl HeuristicObjective for GoalContext {}
impl Shuffled for GoalContext {
fn get_shuffled(&self, random: &(dyn Random + Send + Sync)) -> Self {
let mut global_objectives = self.global_objectives.clone();
let mut flatten_objectives = self.flatten_objectives.clone();
let mut local_objectives = self.local_objectives.clone();
global_objectives.shuffle(&mut random.get_rng());
flatten_objectives.shuffle(&mut random.get_rng());
local_objectives.shuffle(&mut random.get_rng());
Self { global_objectives, flatten_objectives, local_objectives, ..self.clone() }
}
}
impl GoalContext {
pub fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, job: &Job) {
accept_insertion_with_states(&self.states, solution_ctx, route_index, job)
}
pub fn accept_route_state(&self, route_ctx: &mut RouteContext) {
accept_route_state_with_states(&self.states, route_ctx)
}
pub fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
accept_solution_state_with_states(&self.states, solution_ctx);
}
pub fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
merge_with_constraints(&self.constraints, source, candidate)
}
pub fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
evaluate_with_constraints(&self.constraints, move_ctx)
}
pub fn estimate(&self, move_ctx: &MoveContext<'_>) -> InsertionCost {
self.local_objectives
.iter()
.map(|same_level_objectives| {
same_level_objectives.iter().map(|objective| objective.estimate(move_ctx)).sum::<Cost>()
})
.collect()
}
}