#[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 rand::prelude::SliceRandom;
use rosomaxa::population::Shuffled;
use rosomaxa::prelude::*;
use std::cmp::Ordering;
use std::collections::HashSet;
use std::fmt::{Debug, Display, Formatter};
use std::ops::ControlFlow;
use std::sync::Arc;
#[derive(Clone)]
pub struct GoalContext {
goal: Goal,
alternative_goals: Vec<(Goal, Float)>,
constraints: Vec<Arc<dyn FeatureConstraint>>,
states: Vec<Arc<dyn FeatureState>>,
}
impl GoalContext {
pub fn with_constraints<Iter>(&self, constraints: Iter) -> Self
where
Iter: Iterator<Item = Arc<dyn FeatureConstraint>>,
{
GoalContext { constraints: constraints.collect(), ..self.clone() }
}
pub fn constraints(&self) -> impl Iterator<Item = Arc<dyn FeatureConstraint>> + '_ {
self.constraints.iter().cloned()
}
}
impl Debug for GoalContext {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct(short_type_name::<Self>())
.field("goal layers", &self.goal.layers.len())
.field("alternatives", &self.alternative_goals.len())
.field("constraints", &self.constraints.len())
.field("states", &self.states.len())
.finish()
}
}
pub struct GoalContextBuilder {
main_goal: Option<Goal>,
alternative_goals: Vec<(Goal, Float)>,
features: Vec<Feature>,
}
impl GoalContextBuilder {
pub fn with_features(features: &[Feature]) -> GenericResult<Self> {
let features = features.to_vec();
let ids_all = features.iter().map(|feature| feature.name.as_str()).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(",")
)
.into());
}
let goal = Goal::simple(&features)?;
Ok(Self { main_goal: Some(goal), alternative_goals: Vec::default(), features })
}
pub fn set_main_goal(mut self, goal: Goal) -> Self {
self.main_goal = Some(goal);
self
}
pub fn add_alternative_goal(mut self, goal: Goal, weight: Float) -> Self {
self.alternative_goals.push((goal, weight));
self
}
pub fn build(self) -> GenericResult<GoalContext> {
let goal = self.main_goal.ok_or_else(|| GenericError::from("missing goal of optimization"))?;
let alternative_goals = self.alternative_goals;
let states = self.features.iter().filter_map(|feature| feature.state.clone()).collect();
let constraints = self.features.iter().filter_map(|feature| feature.constraint.clone()).collect();
Ok(GoalContext { goal, alternative_goals, constraints, states })
}
}
type TotalOrderFn =
Arc<dyn Fn(&[Arc<dyn FeatureObjective>], &InsertionContext, &InsertionContext) -> Ordering + Send + Sync>;
type CostEstimateFn = Arc<dyn Fn(&[Arc<dyn FeatureObjective>], &MoveContext<'_>) -> Cost + Send + Sync>;
type ObjectiveLayer = (TotalOrderFn, CostEstimateFn, Vec<Arc<dyn FeatureObjective>>);
#[derive(Clone)]
pub struct Goal {
layers: Vec<ObjectiveLayer>,
}
impl Goal {
pub fn simple(features: &[Feature]) -> GenericResult<Self> {
let mut builder = GoalBuilder::default();
let names = features.iter().filter(|f| f.objective.is_some()).map(|f| &f.name);
for name in names {
builder = Self::add_with_name(builder, features, name)?;
}
builder.build()
}
pub fn subset_of<S: AsRef<str>>(features: &[Feature], names: &[S]) -> GenericResult<Self> {
let mut builder = GoalBuilder::default();
for name in names {
builder = Self::add_with_name(builder, features, name.as_ref())?;
}
builder.build()
}
fn add_with_name(builder: GoalBuilder, features: &[Feature], name: &str) -> GenericResult<GoalBuilder> {
let feature = features
.iter()
.find(|f| f.name == name)
.ok_or_else(|| GenericError::from(format!("cannot find a feature with given name: '{name}'")))?;
let objective = feature
.objective
.clone()
.take()
.ok_or_else(|| GenericError::from(format!("feature '{name}' has no objective")))?;
Ok(builder.add_single(objective))
}
}
impl Goal {
pub fn total_order(&self, a: &InsertionContext, b: &InsertionContext) -> Ordering {
self.layers
.iter()
.try_fold(Ordering::Equal, |_, (total_order_fn, _, objectives)| {
match (total_order_fn)(objectives.as_slice(), a, b) {
Ordering::Equal => ControlFlow::Continue(Ordering::Equal),
order => ControlFlow::Break(order),
}
})
.unwrap_value()
}
pub fn estimate(&self, move_ctx: &MoveContext<'_>) -> InsertionCost {
self.layers.iter().map(|(_, estimate_fn, objectives)| (estimate_fn)(objectives.as_slice(), move_ctx)).collect()
}
pub fn fitness<'a>(&'a self, solution: &'a InsertionContext) -> impl Iterator<Item = Float> + 'a {
self.layers.iter().flat_map(|(_, _, objectives)| objectives.iter()).map(|objective| objective.fitness(solution))
}
}
#[derive(Default)]
pub struct GoalBuilder {
layers: Vec<ObjectiveLayer>,
}
impl GoalBuilder {
pub fn add_single(mut self, objective: Arc<dyn FeatureObjective>) -> Self {
self.layers.push((
Arc::new(|objectives, a, b| {
let fitness_a = objectives[0].fitness(a);
let fitness_b = objectives[0].fitness(b);
if fitness_a == 0. && fitness_b == 0. {
Ordering::Equal
} else {
fitness_a.total_cmp(&fitness_b)
}
}),
Arc::new(|objectives, move_ctx| objectives[0].estimate(move_ctx)),
vec![objective],
));
self
}
pub fn add_multi<TO, CE>(
mut self,
objectives: &[Arc<dyn FeatureObjective>],
total_order_fn: TO,
cost_estimate_fn: CE,
) -> Self
where
TO: Fn(&[Arc<dyn FeatureObjective>], &InsertionContext, &InsertionContext) -> Ordering + Send + Sync + 'static,
CE: Fn(&[Arc<dyn FeatureObjective>], &MoveContext<'_>) -> Cost + Send + Sync + 'static,
{
self.layers.push((Arc::new(total_order_fn), Arc::new(cost_estimate_fn), objectives.to_vec()));
self
}
pub fn build(self) -> GenericResult<Goal> {
if self.layers.is_empty() {
return Err(GenericError::from("no objectives specified in the goal"));
}
Ok(Goal { layers: self.layers })
}
}
#[derive(Clone, Default)]
pub struct Feature {
pub name: String,
pub constraint: Option<Arc<dyn FeatureConstraint>>,
pub objective: Option<Arc<dyn FeatureObjective>>,
pub state: Option<Arc<dyn FeatureState>>,
}
#[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
}
}
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq)]
pub struct ViolationCode(pub i32);
impl ViolationCode {
pub fn unknown() -> Self {
Self(-1)
}
pub fn is_unknown(&self) -> bool {
self.0 == -1
}
}
impl From<i32> for ViolationCode {
fn from(value: i32) -> Self {
Self(value)
}
}
impl Display for ViolationCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.0))
}
}
#[derive(Default)]
pub struct FeatureBuilder(Feature);
impl FeatureBuilder {
pub fn from_feature(feature: Feature) -> Self {
Self(feature)
}
pub fn with_name(mut self, name: &str) -> Self {
self.0.name = name.to_string();
self
}
pub fn with_constraint<T: FeatureConstraint + 'static>(mut self, constraint: T) -> Self {
self.0.constraint = Some(Arc::new(constraint));
self
}
pub fn with_objective<T: FeatureObjective + 'static>(mut self, objective: T) -> Self {
self.0.objective = Some(Arc::new(objective));
self
}
pub fn with_state<T: FeatureState + 'static>(mut self, state: T) -> Self {
self.0.state = Some(Arc::new(state));
self
}
pub fn build(self) -> Result<Feature, GenericError> {
let feature = self.0;
if feature.name == String::default() {
return Err("features with default id are not allowed".into());
}
if feature.constraint.is_none() && feature.objective.is_none() {
Err("empty feature is not allowed".into())
} else {
Ok(feature)
}
}
}
pub trait FeatureState: Send + Sync {
fn notify_failure(&self, _solution_ctx: &mut SolutionContext, _route_indices: &[usize], _jobs: &[Job]) -> bool {
false
}
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);
}
pub trait FeatureConstraint: Send + Sync {
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation>;
fn merge(&self, _source: Job, _candidate: Job) -> Result<Job, ViolationCode> {
Err(ViolationCode::default())
}
}
pub trait FeatureObjective: Send + Sync {
fn fitness(&self, solution: &InsertionContext) -> Cost;
fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost;
}
impl HeuristicObjective for GoalContext {
type Solution = InsertionContext;
fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering {
self.goal.total_order(a, b)
}
}
impl Shuffled for GoalContext {
fn get_shuffled(&self, random: &(dyn Random)) -> Self {
const RANDOM_ALTERNATIVE_PROBABILITY: Float = 0.05;
const RANDOM_SHUFFLE_PROBABILITY: Float = 0.001;
if !self.alternative_goals.is_empty() && random.is_hit(RANDOM_ALTERNATIVE_PROBABILITY) {
let idx = random.uniform_int(0, self.alternative_goals.len() as i32 - 1) as usize;
return self.get_alternative(idx);
}
if random.is_hit(RANDOM_SHUFFLE_PROBABILITY) {
self.get_shuffled(random)
} else {
self.clone()
}
}
}
impl GoalContext {
fn get_alternative(&self, idx: usize) -> Self {
let (goal, _) = self.alternative_goals[idx].clone();
Self { goal, ..self.clone() }
}
fn get_shuffled(&self, random: &(dyn Random)) -> Self {
let instance = self.clone();
let mut layers = self.goal.layers.clone();
layers.shuffle(&mut random.get_rng());
Self { goal: Goal { layers }, ..instance }
}
pub(crate) fn get_alternatives(&self) -> impl Iterator<Item = Self> + '_ {
self.alternative_goals.iter().enumerate().map(|(idx, _)| self.get_alternative(idx))
}
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 notify_failure(&self, solution_ctx: &mut SolutionContext, route_indices: &[usize], jobs: &[Job]) -> bool {
notify_failure_with_states(&self.states, solution_ctx, route_indices, jobs)
}
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.goal.estimate(move_ctx)
}
pub fn fitness<'a>(&'a self, solution: &'a InsertionContext) -> impl Iterator<Item = Float> + 'a {
self.goal.fitness(solution)
}
}
impl Debug for Feature {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct(short_type_name::<Self>())
.field("name", &self.name)
.field("constraint", &self.constraint.is_some())
.field("objective", &self.objective.is_some())
.field("state", &self.state.is_some())
.finish()
}
}