diff --git a/docs/src/internals/algorithms/heuristics.md b/docs/src/internals/algorithms/heuristics.md index 5d179b5f2..2d9a40445 100644 --- a/docs/src/internals/algorithms/heuristics.md +++ b/docs/src/internals/algorithms/heuristics.md @@ -53,8 +53,7 @@ Currently available heuristics: Each heuristic accepts one of solutions from the population (not necessary the best known) and tries to improve it (or diversify). During one of refinement iterations, many solutions are picked at the same time and many heuristics are called then in parallel. Such incremental step is called a `generation`. Once it is completed, all found solutions are introduced to the population, -which decides how to store them. With elitism/rosomaxa population types, to order solution from Pareto optimal front, -`Non-Dominated Sorting Genetic Algorithm II` is used. +which decides how to store them. [Related documentation](https://docs.rs/vrp-core/latest/vrp_core/solver/search/index.html) diff --git a/docs/src/internals/algorithms/index.md b/docs/src/internals/algorithms/index.md index dd3ac2e46..0a15c12ee 100644 --- a/docs/src/internals/algorithms/index.md +++ b/docs/src/internals/algorithms/index.md @@ -14,7 +14,6 @@ An incomplete list of important references: - Thibaut Vidal: `Hybrid Genetic Search for the CVRP: Open-Source Implementation and SWAP* Neighborhood` - Richard F. Hartl, Thibaut Vidal: `Workload Equity in Vehicle Routing Problems: A Survey and Analysis` -- K. Deb; A. Pratap; S. Agarwal; T. Meyarivan: `A fast and elitist multiobjective genetic algorithm: NSGA-II` - Damminda Alahakoon, Saman K Halgamuge, Srinivasan Bala: `Dynamic self-organizing maps with controlled growth for knowledge discovery` - Daniel J. Russo, Benjamin Van Roy, Abbas Kazerouni, Ian Osband and Zheng Wen: `A Tutorial on Thompson Sampling` https://web.stanford.edu/~bvr/pubs/TS_Tutorial.pdf diff --git a/docs/src/internals/overview.md b/docs/src/internals/overview.md index 4c9f85662..111916471 100644 --- a/docs/src/internals/overview.md +++ b/docs/src/internals/overview.md @@ -113,8 +113,7 @@ right side * objective * kind - *_ multi (NSGA-II) - *_ hierarchical + *_ lexicographical * types *_ minimize/maximize routes *_ minimize cost diff --git a/experiments/heuristic-research/src/solver/mod.rs b/experiments/heuristic-research/src/solver/mod.rs index f4f60c44f..6ecd62c2a 100644 --- a/experiments/heuristic-research/src/solver/mod.rs +++ b/experiments/heuristic-research/src/solver/mod.rs @@ -23,7 +23,7 @@ fn get_population( ) -> Box + Send + Sync> where O: HeuristicObjective + Shuffled + 'static, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered + 'static, + S: HeuristicSolution + RosomaxaWeighted + 'static, { match population_type { "greedy" => Box::new(ProxyPopulation::new(Greedy::new(objective, 1, None))), diff --git a/experiments/heuristic-research/src/solver/proxies.rs b/experiments/heuristic-research/src/solver/proxies.rs index e946a1a6b..ff7ebc3bd 100644 --- a/experiments/heuristic-research/src/solver/proxies.rs +++ b/experiments/heuristic-research/src/solver/proxies.rs @@ -1,6 +1,6 @@ use crate::*; use rosomaxa::example::VectorSolution; -use rosomaxa::population::{DominanceOrdered, RosomaxaWeighted, Shuffled}; +use rosomaxa::population::{RosomaxaWeighted, Shuffled}; use rosomaxa::prelude::*; use std::any::TypeId; use std::cmp::Ordering; @@ -46,7 +46,7 @@ impl<'a> TryFrom<&'a str> for ExperimentData { impl From<&S> for ObservationData where - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered + 'static, + S: HeuristicSolution + RosomaxaWeighted + 'static, { fn from(solution: &S) -> Self { if TypeId::of::() == TypeId::of::() { @@ -85,7 +85,7 @@ pub struct ProxyPopulation where P: HeuristicPopulation + 'static, O: HeuristicObjective + Shuffled + 'static, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered + 'static, + S: HeuristicSolution + RosomaxaWeighted + 'static, { generation: usize, inner: P, @@ -95,7 +95,7 @@ impl ProxyPopulation where P: HeuristicPopulation + 'static, O: HeuristicObjective + Shuffled + 'static, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered + 'static, + S: HeuristicSolution + RosomaxaWeighted + 'static, { /// Creates a new instance of `ProxyPopulation`. pub fn new(inner: P) -> Self { @@ -112,7 +112,7 @@ impl HeuristicPopulation for ProxyPopulation where P: HeuristicPopulation + 'static, O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { type Objective = O; type Individual = S; @@ -153,7 +153,7 @@ where })) } - fn ranked<'a>(&'a self) -> Box + 'a> { + fn ranked<'a>(&'a self) -> Box + 'a> { self.inner.ranked() } @@ -174,7 +174,7 @@ impl Display for ProxyPopulation where P: HeuristicPopulation + 'static, O: HeuristicObjective + Shuffled + 'static, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered + 'static, + S: HeuristicSolution + RosomaxaWeighted + 'static, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.inner.fmt(f) diff --git a/experiments/heuristic-research/src/solver/state.rs b/experiments/heuristic-research/src/solver/state.rs index d08c8240f..5bff45b3f 100644 --- a/experiments/heuristic-research/src/solver/state.rs +++ b/experiments/heuristic-research/src/solver/state.rs @@ -1,6 +1,6 @@ use crate::{Coordinate, MatrixData}; use rosomaxa::algorithms::gsom::NetworkState; -use rosomaxa::population::{DominanceOrdered, Rosomaxa, RosomaxaWeighted, Shuffled}; +use rosomaxa::population::{Rosomaxa, RosomaxaWeighted, Shuffled}; use rosomaxa::prelude::*; use serde::{Deserialize, Serialize}; use std::any::TypeId; @@ -44,10 +44,10 @@ pub fn get_population_state(population: &P) -> PopulationState where P: HeuristicPopulation + 'static, O: HeuristicObjective + Shuffled + 'static, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered + 'static, + S: HeuristicSolution + RosomaxaWeighted + 'static, { let fitness_values = - population.ranked().next().map(|(solution, _)| solution.fitness().collect::>()).unwrap_or_default(); + population.ranked().next().map(|solution| solution.fitness().collect::>()).unwrap_or_default(); if TypeId::of::

() == TypeId::of::>() { let rosomaxa = unsafe { std::mem::transmute::<&P, &Rosomaxa>(population) }; diff --git a/rosomaxa/src/algorithms/mod.rs b/rosomaxa/src/algorithms/mod.rs index 55dddc907..11f66e4f1 100644 --- a/rosomaxa/src/algorithms/mod.rs +++ b/rosomaxa/src/algorithms/mod.rs @@ -2,5 +2,4 @@ pub mod gsom; pub mod math; -pub mod nsga2; pub mod rl; diff --git a/rosomaxa/src/algorithms/nsga2/crowding_distance.rs b/rosomaxa/src/algorithms/nsga2/crowding_distance.rs deleted file mode 100644 index 72b0c8927..000000000 --- a/rosomaxa/src/algorithms/nsga2/crowding_distance.rs +++ /dev/null @@ -1,82 +0,0 @@ -#[cfg(test)] -#[path = "../../../tests/unit/algorithms/nsga2/crowding_distance_test.rs"] -mod crowding_distance_test; - -use crate::algorithms::nsga2::non_dominated_sort::Front; -use crate::algorithms::nsga2::*; - -pub struct AssignedCrowdingDistance<'a, S> -where - S: 'a, -{ - pub index: usize, - pub solution: &'a S, - pub rank: usize, - pub crowding_distance: f64, -} - -pub struct ObjectiveStat { - pub spread: f64, -} - -/// Assigns a crowding distance to each solution in `front`. -pub fn assign_crowding_distance<'a, S>( - front: &Front<'a, S>, - multi_objective: &impl MultiObjective, -) -> (Vec>, Vec) { - let mut a: Vec<_> = front - .iter() - .map(|(solution, index)| AssignedCrowdingDistance { - index, - solution, - rank: front.rank(), - crowding_distance: 0.0, - }) - .collect(); - - let objective_count = multi_objective.size(); - - let objective_stat: Vec<_> = (0..objective_count) - .map(|objective_idx| { - // first, sort according to the corresponding objective - a.sort_by(|a, b| { - multi_objective - .get_order(a.solution, b.solution, objective_idx) - .expect("get_order: invalid multi objective") - }); - - // assign infinite crowding distance to the extremes - { - a.first_mut().unwrap().crowding_distance = f64::INFINITY; - a.last_mut().unwrap().crowding_distance = f64::INFINITY; - } - - // the distance between the "best" and "worst" solution according to "objective" - let spread = multi_objective - .get_distance(a.first().unwrap().solution, a.last().unwrap().solution, objective_idx) - .expect("get_distance: invalid multi objective") - .abs(); - debug_assert!(spread >= 0.0); - - if spread > 0.0 { - let norm = 1.0 / (spread * (objective_count as f64)); - debug_assert!(norm > 0.0); - - for i in 1..a.len() - 1 { - debug_assert!(i >= 1 && i + 1 < a.len()); - - let distance = multi_objective - .get_distance(a[i + 1].solution, a[i - 1].solution, objective_idx) - .expect("get_distance: invalid multi objective") - .abs(); - debug_assert!(distance >= 0.0); - a[i].crowding_distance += distance * norm; - } - } - - ObjectiveStat { spread } - }) - .collect(); - - (a, objective_stat) -} diff --git a/rosomaxa/src/algorithms/nsga2/mod.rs b/rosomaxa/src/algorithms/nsga2/mod.rs deleted file mode 100644 index e54f113e6..000000000 --- a/rosomaxa/src/algorithms/nsga2/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! This module contains a logic for processing multiple solutions and multi objective optimization -//! based on `Non Dominated Sorting Genetic Algorithm II` algorithm. -//! -//! A Non Dominated Sorting Genetic Algorithm II (NSGA-II) is a popular multi objective optimization -//! algorithm with three special characteristics: -//! -//! - fast non-dominated sorting approach -//! - fast crowded distance estimation procedure -//! - simple crowded comparison operator -//! -//! For more details regarding NSGA-II algorithm details, check original paper "A fast and elitist -//! multiobjective genetic algorithm: NSGA-II", Kalyanmoy Deb et al. DOI: `0.1109/4235.996017` -//! -//! A NSGA-II implementation in this module is based on the source code from the following repositories: -//! -//! - [dominance order trait](https://github.com/mneumann/dominance-ord-rs) -//! - [fast non-dominated sort algorithm](https://github.com/mneumann/non-dominated-sort-rs) -//! - [NSGA-II implementation](https://github.com/mneumann/nsga2-rs) -//! -//! which are released under MIT License (MIT), copyright (c) 2016 Michael Neumann -//! - -mod crowding_distance; -use self::crowding_distance::*; - -mod non_dominated_sort; -use self::non_dominated_sort::*; - -mod nsga2_sort; -pub use self::nsga2_sort::select_and_rank; - -mod objective; -pub use self::objective::*; diff --git a/rosomaxa/src/algorithms/nsga2/non_dominated_sort.rs b/rosomaxa/src/algorithms/nsga2/non_dominated_sort.rs deleted file mode 100644 index e8c9b01a9..000000000 --- a/rosomaxa/src/algorithms/nsga2/non_dominated_sort.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! Implementation of the [Fast Non-Dominated Sort Algorithm][1] as used by NSGA-II. -//! Time complexity is `O(K * N^2)`, where `K` is the number of objectives and `N` the number of solutions. -//! -//! Non-dominated sorting is used in multi-objective (multivariate) optimization to group solutions -//! into non-dominated Pareto fronts according to their objectives. In the existence of multiple -//! objectives, a solution can happen to be better in one objective while at the same time worse in -//! another objective, and as such none of the two solutions _dominates_ the other. -//! -//! [1]: https://www.iitk.ac.in/kangal/Deb_NSGA-II.pdf "A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II)" - -#[cfg(test)] -#[path = "../../../tests/unit/algorithms/nsga2/non_dominated_sort_test.rs"] -mod non_dominated_sort_test; - -use crate::MultiObjective; -use std::cmp::Ordering; -use std::collections::HashSet; - -type SolutionIdx = usize; - -#[derive(Debug, Clone)] -pub struct Front<'s, S: 's> { - dominated_solutions: Vec>, - domination_count: Vec, - previous_front: Vec, - current_front: Vec, - rank: usize, - solutions: &'s [S], -} - -impl<'f, 's: 'f, S: 's> Front<'s, S> { - pub fn rank(&self) -> usize { - self.rank - } - - /// Iterates over the elements of the front. - pub fn iter(&'f self) -> FrontElemIter<'f, 's, S> { - FrontElemIter { front: self, next_idx: 0 } - } - - pub fn is_empty(&self) -> bool { - self.current_front.is_empty() - } - - pub fn next_front(self) -> Self { - let Front { dominated_solutions, mut domination_count, previous_front, current_front, rank, solutions } = self; - - // reuse the previous_front - let mut next_front = previous_front; - next_front.clear(); - - // NOTE: loop to handle non transient relationship in solutions introduced by hierarchical objective - loop { - for &p_i in current_front.iter() { - for &q_i in dominated_solutions[p_i].iter() { - if domination_count[q_i] == 0 { - // TODO investigate why this happens - continue; - } - - domination_count[q_i] -= 1; - if domination_count[q_i] == 0 { - // q_i is not dominated by any other solution. it belongs to the next front. - next_front.push(q_i); - } - } - } - - if !next_front.is_empty() || domination_count.iter().all(|v| *v == 0) { - break; - } - } - - Self { - dominated_solutions, - domination_count, - previous_front: current_front, - current_front: next_front, - rank: rank + 1, - solutions, - } - } -} - -pub struct FrontElemIter<'f, 's: 'f, S: 's> { - front: &'f Front<'s, S>, - next_idx: SolutionIdx, -} - -impl<'f, 's: 'f, S: 's> Iterator for FrontElemIter<'f, 's, S> { - type Item = (&'s S, usize); - - fn next(&mut self) -> Option { - match self.front.current_front.get(self.next_idx) { - Some(&solution_idx) => { - self.next_idx += 1; - Some((&self.front.solutions[solution_idx], solution_idx)) - } - None => None, - } - } -} - -/// Performs a non-dominated sort of `solutions`. Returns the first Pareto front. -pub fn non_dominated_sort<'s, S, O>(solutions: &'s [S], objective: &O) -> Front<'s, S> -where - O: MultiObjective, -{ - // the indices of the solutions that are dominated by this `solution` - let mut dominated_solutions: Vec> = solutions.iter().map(|_| Vec::new()).collect(); - - // for each solutions, we keep a domination count, i.e. the number of solutions that dominate the solution - let mut domination_count: Vec = solutions.iter().map(|_| 0).collect(); - - let mut current_front: Vec = Vec::new(); - - // initial pass over each combination: O(n*n / 2) - let mut iter = solutions.iter().enumerate(); - while let Some((p_i, p)) = iter.next() { - for (q_i, q) in iter.clone() { - match objective.total_order(p, q) { - Ordering::Less => { - // p dominates q, add `q` to the set of solutions dominated by `p` - dominated_solutions[p_i].push(q_i); - // q is dominated by p - domination_count[q_i] += 1; - } - Ordering::Greater => { - // p is dominated by q, add `p` to the set of solutions dominated by `q` - dominated_solutions[q_i].push(p_i); - // q dominates p, increment domination counter of `p` - domination_count[p_i] += 1 - } - Ordering::Equal => {} - } - } - // if domination_count drops to zero, push index to front - if domination_count[p_i] == 0 { - current_front.push(p_i); - } - } - - // non transient relationship in solutions, e.g.: - // - // A < B B > A C < A - // A > C B > C C > B - // - // this might occur with hierarchical objective - if current_front.is_empty() { - let min = *domination_count.iter().min().expect("domination count should not be empty"); - let ids = domination_count - .iter() - .enumerate() - .filter(|(_, count)| **count == min) - .map(|(idx, _)| idx) - .collect::>(); - - dominated_solutions.iter_mut().enumerate().filter(|(idx, _)| ids.contains(idx)).for_each(|(_, domindated)| { - domindated.retain(|idx| !ids.contains(idx)); - }); - - current_front.extend(ids); - } - - Front { dominated_solutions, domination_count, previous_front: Vec::new(), current_front, rank: 0, solutions } -} diff --git a/rosomaxa/src/algorithms/nsga2/nsga2_sort.rs b/rosomaxa/src/algorithms/nsga2/nsga2_sort.rs deleted file mode 100644 index 136564b8e..000000000 --- a/rosomaxa/src/algorithms/nsga2/nsga2_sort.rs +++ /dev/null @@ -1,57 +0,0 @@ -#[cfg(test)] -#[path = "../../../tests/unit/algorithms/nsga2/nsga2_sort_test.rs"] -mod nsga2_sort_test; - -use super::*; - -/// Select `n` solutions using the approach taken by NSGA2. -/// -/// We first sort the solutions into their corresponding pareto fronts using a non-dominated sort -/// algorithm. Then, we put as many "complete" fronts into the result set, until we cannot fit in a -/// whole front anymore, without exceeding `n` solutions in the result set. For this last front, -/// that does not completely fit into the result set, we sort it's solutions according to their -/// crowding distance (higher crowding distance is "better"), and prefer those solutions with the -/// higher crowding distance until we have exactly `n` solutions in the result set. -pub fn select_and_rank<'a, S: 'a>( - solutions: &'a [S], - n: usize, - multi_objective: &impl MultiObjective, -) -> Vec> { - // cannot select more solutions than we actually have - let n = solutions.len().min(n); - debug_assert!(n <= solutions.len()); - - let mut result = Vec::with_capacity(n); - let mut missing_solutions = n; - - let mut front = non_dominated_sort(solutions, multi_objective); - - while !front.is_empty() { - let (mut assigned_crowding, _) = assign_crowding_distance(&front, multi_objective); - - if assigned_crowding.len() > missing_solutions { - // the front does not fit in total. sort it's solutions according to the crowding - // distance and take the best solutions until we have "n" solutions in the result - assigned_crowding.sort_by(|a, b| { - debug_assert_eq!(a.rank, b.rank); - a.crowding_distance.partial_cmp(&b.crowding_distance).unwrap().reverse() - }); - } - - // take no more than `missing_solutions` - let take = assigned_crowding.len().min(missing_solutions); - - result.extend(assigned_crowding.into_iter().take(take)); - - missing_solutions -= take; - if missing_solutions == 0 { - break; - } - - front = front.next_front(); - } - - debug_assert_eq!(n, result.len()); - - result -} diff --git a/rosomaxa/src/algorithms/nsga2/objective.rs b/rosomaxa/src/algorithms/nsga2/objective.rs deleted file mode 100644 index efe1dec40..000000000 --- a/rosomaxa/src/algorithms/nsga2/objective.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::prelude::GenericError; -use crate::utils::compare_floats; -use std::cmp::Ordering; - -/// An *objective* defines a *total ordering relation* and a *distance metric* on a set of -/// `solutions`. Given any two solutions, an objective answers the following two questions: -/// -/// - "which solution is the better one" (total order) -/// - "how similar are the two solutions" (distance metric) -pub trait Objective { - /// The solution value type that we define the objective on. - type Solution; - - /// An objective defines a total ordering between any two solution values. - /// - /// This answers the question, is solution `a` better, equal or worse than solution `b`, - /// according to the objective. - fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { - let fitness_a = self.fitness(a); - let fitness_b = self.fitness(b); - - compare_floats(fitness_a, fitness_b) - } - - /// An objective defines a distance metric between any two solution values. - /// - /// The distance metric answer the question, how similar the solutions `a` and `b` are, - /// according to the objective. A zero value would mean, that both solutions are in fact the same, - /// according to the objective. Larger magnitudes would mean "less similar". - fn distance(&self, a: &Self::Solution, b: &Self::Solution) -> f64 { - let fitness_a = self.fitness(a); - let fitness_b = self.fitness(b); - - fitness_a - fitness_b - } - - /// An objective fitness value for given `solution`. - fn fitness(&self, solution: &Self::Solution) -> f64; -} - -/// A multi objective. -pub trait MultiObjective { - /// The solution value type that we define the objective on. - type Solution; - - /// An objective defines a total ordering between any two solution values. - fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering; - - /// An objective fitness values for given `solution`. - fn fitness<'a>(&'a self, solution: &'a Self::Solution) -> Box + 'a>; - - /// Get solution order for individual objective. - fn get_order(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result; - - /// Gets solution distance for individual objective. - fn get_distance(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result; - - /// Returns total number of inner objectives. - fn size(&self) -> usize; -} - -/// Calculates dominance order of two solutions using multiple objectives. -pub fn dominance_order<'a, T, O: Objective + ?Sized + 'a, Iter: Iterator>( - a: &'a T, - b: &'a T, - objectives: Iter, -) -> Ordering { - let mut less_cnt = 0; - let mut greater_cnt = 0; - - for objective in objectives { - match objective.total_order(a, b) { - Ordering::Less => { - less_cnt += 1; - } - Ordering::Greater => { - greater_cnt += 1; - } - Ordering::Equal => {} - } - } - - if less_cnt > 0 && greater_cnt == 0 { - Ordering::Less - } else if greater_cnt > 0 && less_cnt == 0 { - Ordering::Greater - } else { - debug_assert!((less_cnt > 0 && greater_cnt > 0) || (less_cnt == 0 && greater_cnt == 0)); - Ordering::Equal - } -} diff --git a/rosomaxa/src/evolution/mod.rs b/rosomaxa/src/evolution/mod.rs index ca10f8d15..6f0e48168 100644 --- a/rosomaxa/src/evolution/mod.rs +++ b/rosomaxa/src/evolution/mod.rs @@ -8,9 +8,10 @@ pub use self::config::*; mod simulator; pub use self::simulator::*; -pub mod telemetry; +mod telemetry; pub use self::telemetry::*; +pub mod objectives; pub mod strategies; /// Defines evolution result type. diff --git a/rosomaxa/src/evolution/objectives.rs b/rosomaxa/src/evolution/objectives.rs new file mode 100644 index 000000000..a974f064b --- /dev/null +++ b/rosomaxa/src/evolution/objectives.rs @@ -0,0 +1,53 @@ +//! Specifies objective functions. + +use std::cmp::Ordering; + +/// An *objective* function defines a *total ordering relation* and a *fitness metrics* on a set of +/// `solutions`. Given any two solutions, an objective answers the following two questions: +/// +/// - "which solution is the better one" (total order) +/// - "how are two solutions close to each other" (fitness vector metrics) +pub trait HeuristicObjective: Send + Sync { + /// The solution value type that we define the objective on. + type Solution; + + /// An objective defines a total ordering between any two solution values. + /// + /// This answers the question, is solution `a` better, equal or worse than solution `b`, + /// according to the objective. + fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering; + + /// An objective fitness values for given `solution`. + fn fitness<'a>(&'a self, solution: &'a Self::Solution) -> Box + 'a>; +} + +/// Calculates dominance order of two solutions using ordering functions. +pub fn dominance_order<'a, T: 'a, Order, Iter>(a: &'a T, b: &'a T, ordering_fns: Iter) -> Ordering +where + Order: Fn(&'a T, &'a T) -> Ordering, + Iter: Iterator, +{ + let mut less_cnt = 0; + let mut greater_cnt = 0; + + for ordering_fn in ordering_fns { + match ordering_fn(a, b) { + Ordering::Less => { + less_cnt += 1; + } + Ordering::Greater => { + greater_cnt += 1; + } + Ordering::Equal => {} + } + } + + if less_cnt > 0 && greater_cnt == 0 { + Ordering::Less + } else if greater_cnt > 0 && less_cnt == 0 { + Ordering::Greater + } else { + debug_assert!((less_cnt > 0 && greater_cnt > 0) || (less_cnt == 0 && greater_cnt == 0)); + Ordering::Equal + } +} diff --git a/rosomaxa/src/evolution/strategies/iterative.rs b/rosomaxa/src/evolution/strategies/iterative.rs index c47118777..d9bcc7f13 100644 --- a/rosomaxa/src/evolution/strategies/iterative.rs +++ b/rosomaxa/src/evolution/strategies/iterative.rs @@ -78,7 +78,7 @@ where let (population, telemetry_metrics) = heuristic_ctx.on_result()?; let solutions = - population.ranked().map(|(solution, _)| solution.deep_copy()).take(self.desired_solutions_amount).collect(); + population.ranked().map(|solution| solution.deep_copy()).take(self.desired_solutions_amount).collect(); Ok((solutions, telemetry_metrics)) } diff --git a/rosomaxa/src/evolution/telemetry.rs b/rosomaxa/src/evolution/telemetry.rs index 42cc2460b..3339f3374 100644 --- a/rosomaxa/src/evolution/telemetry.rs +++ b/rosomaxa/src/evolution/telemetry.rs @@ -42,8 +42,6 @@ pub struct TelemetryGeneration { /// Keeps essential information about particular individual in population. pub struct TelemetryIndividual { - /// Rank in population. - pub rank: usize, /// Solution difference from best individual. pub difference: f64, /// Objectives fitness values. @@ -181,14 +179,14 @@ where } }; - if let Some((best_individual, rank)) = population.ranked().next() { + if let Some(best_individual) = population.ranked().next() { let should_log_best = generation % *log_best.unwrap_or(&usize::MAX) == 0; let should_log_population = generation % *log_population.unwrap_or(&usize::MAX) == 0; let should_track_population = generation % *track_population.unwrap_or(&usize::MAX) == 0; if should_log_best { self.log_individual( - &self.get_individual_metrics(objective, population, best_individual, rank), + &self.get_individual_metrics(objective, population, best_individual), Some((generation, generation_time)), ) } @@ -240,10 +238,8 @@ where ); } - let individuals = population - .ranked() - .map(|(insertion_ctx, rank)| self.get_individual_metrics(objective, population, insertion_ctx, rank)) - .collect::>(); + let individuals = + population.ranked().map(|s| self.get_individual_metrics(objective, population, s)).collect::>(); if should_log_population { individuals.iter().for_each(|metrics| self.log_individual(metrics, None)); @@ -315,13 +311,12 @@ where objective: &O, population: &DynHeuristicPopulation, solution: &S, - rank: usize, ) -> TelemetryIndividual { let fitness = solution.fitness().collect::>(); let difference = get_fitness_change(objective, population, solution); - TelemetryIndividual { rank, difference, fitness } + TelemetryIndividual { difference, fitness } } fn log_individual(&self, metrics: &TelemetryIndividual, gen_info: Option<(usize, Timer)>) { @@ -337,7 +332,7 @@ where fitness ) } else { - format!("\trank: {}, fitness: ({}), difference: {:.3}%", metrics.rank, fitness, metrics.difference) + format!("\tfitness: ({}), difference: {:.3}%", fitness, metrics.difference) }; self.log(value.as_str()); @@ -458,7 +453,7 @@ where let fitness_change = population .ranked() .next() - .map(|(best_ctx, _)| objective.fitness(best_ctx)) + .map(|best_ctx| objective.fitness(best_ctx)) .map(|best_fitness| { let fitness_value = objective.fitness(solution); relative_distance(fitness_value, best_fitness) diff --git a/rosomaxa/src/example.rs b/rosomaxa/src/example.rs index d90262421..0d352ee56 100644 --- a/rosomaxa/src/example.rs +++ b/rosomaxa/src/example.rs @@ -5,9 +5,10 @@ mod example_test; use crate::algorithms::gsom::Input; +use crate::evolution::objectives::HeuristicObjective; use crate::evolution::*; use crate::hyper::*; -use crate::population::{DominanceOrder, DominanceOrdered, RosomaxaWeighted, Shuffled}; +use crate::population::{RosomaxaWeighted, Shuffled}; use crate::prelude::*; use crate::utils::Noise; use crate::*; @@ -45,13 +46,12 @@ pub struct VectorSolution { pub data: Vec, weights: Vec, objective: Arc, - order: DominanceOrder, } impl VectorSolution { /// Returns a fitness value of given solution. pub fn fitness(&self) -> f64 { - Objective::fitness(self.objective.as_ref(), self) + HeuristicObjective::fitness(self.objective.as_ref(), self).next().expect("at least one value should be present") } } @@ -83,7 +83,7 @@ impl HeuristicContext for VectorContext { self.inner_context.population.select() } - fn ranked<'a>(&'a self) -> Box + 'a> { + fn ranked<'a>(&'a self) -> Box + 'a> { self.inner_context.population.ranked() } @@ -135,46 +135,16 @@ impl VectorObjective { } } -impl HeuristicObjective for VectorObjective {} - -impl Objective for VectorObjective { - type Solution = VectorSolution; - - fn fitness(&self, solution: &Self::Solution) -> f64 { - (self.fitness_fn)(solution.data.as_slice()) - } -} - -impl MultiObjective for VectorObjective { +impl HeuristicObjective for VectorObjective { type Solution = VectorSolution; fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { - Objective::total_order(self, a, b) + compare_floats((self.fitness_fn)(a.data.as_slice()), (self.fitness_fn)(b.data.as_slice())) } - fn fitness<'a>(&self, solution: &'a Self::Solution) -> Box + 'a> { + fn fitness<'a>(&'a self, solution: &'a Self::Solution) -> Box + 'a> { Box::new(once((self.fitness_fn)(solution.data.as_slice()))) } - - fn get_order(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result { - if idx == 0 { - Ok(Objective::total_order(self, a, b)) - } else { - Err(format!("objective has only 1 inner, passed index: {idx}").into()) - } - } - - fn get_distance(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result { - if idx == 0 { - Ok(Objective::distance(self, a, b)) - } else { - Err(format!("objective has only 1 inner, passed index: {idx}").into()) - } - } - - fn size(&self) -> usize { - 1 - } } impl Shuffled for VectorObjective { @@ -184,27 +154,12 @@ impl Shuffled for VectorObjective { } impl HeuristicSolution for VectorSolution { - fn fitness<'a>(&'a self) -> Box + 'a> { - MultiObjective::fitness(self.objective.as_ref(), self) + fn fitness(&self) -> impl Iterator { + self.objective.fitness(self) } fn deep_copy(&self) -> Self { - Self { - data: self.data.clone(), - weights: self.weights.clone(), - objective: self.objective.clone(), - order: self.order.clone(), - } - } -} - -impl DominanceOrdered for VectorSolution { - fn get_order(&self) -> &DominanceOrder { - &self.order - } - - fn set_order(&mut self, order: DominanceOrder) { - self.order = order + Self { data: self.data.clone(), weights: self.weights.clone(), objective: self.objective.clone() } } } @@ -223,7 +178,7 @@ impl Input for VectorSolution { impl VectorSolution { /// Creates a new instance of `VectorSolution`. pub fn new(data: Vec, objective: Arc) -> Self { - Self { data, objective, weights: Vec::default(), order: DominanceOrder::default() } + Self { data, objective, weights: Vec::default() } } } diff --git a/rosomaxa/src/hyper/dynamic_selective.rs b/rosomaxa/src/hyper/dynamic_selective.rs index b234f3b6c..9452185c4 100644 --- a/rosomaxa/src/hyper/dynamic_selective.rs +++ b/rosomaxa/src/hyper/dynamic_selective.rs @@ -323,7 +323,7 @@ where heuristic_ctx .ranked() .next() - .map(|(best_known, _)| heuristic_ctx.objective().total_order(solution, best_known)) + .map(|best_known| heuristic_ctx.objective().total_order(solution, best_known)) .unwrap_or(Ordering::Less) } @@ -340,7 +340,7 @@ where heuristic_ctx .ranked() .next() - .map(|(best_known, _)| { + .map(|best_known| { const BEST_DISCOVERY_REWARD_MULTIPLIER: f64 = 2.; let objective = heuristic_ctx.objective(); @@ -412,23 +412,22 @@ where Ordering::Equal => return 0., }; - let total_objectives = objective.size(); + let idx = objective + .fitness(a) + .zip(objective.fitness(b)) + .enumerate() + .find(|(_, (fitness_a, fitness_b))| compare_floats_refs(fitness_a, fitness_b) != Ordering::Equal) + .map(|(idx, _)| idx); - let idx = (0..total_objectives).find(|idx| { - let distance = objective.get_distance(a, b, *idx).expect("cannot get distance by idx"); - compare_floats(distance, 0.) != Ordering::Equal - }); - - // NOTE special case when total order returns non-zero sign when all objectives are the same - // considering their from non-numerical quality point of view let idx = if let Some(idx) = idx { idx } else { return 0.; }; - assert_ne!(total_objectives, 0, "cannot have empty objective here"); - assert_ne!(total_objectives, idx, "cannot have index equal to total amount of objectives"); + let total_objectives = objective.fitness(a).count(); + assert_ne!(total_objectives, 0, "cannot have an empty objective here"); + assert_ne!(total_objectives, idx, "cannot have the index equal to total amount of objectives"); let priority_amplifier = (total_objectives - idx) as f64; let value = a diff --git a/rosomaxa/src/lib.rs b/rosomaxa/src/lib.rs index 38ec66f9f..ed0a54638 100644 --- a/rosomaxa/src/lib.rs +++ b/rosomaxa/src/lib.rs @@ -58,9 +58,9 @@ pub mod termination; pub mod utils; use crate::algorithms::math::RemedianUsize; -use crate::algorithms::nsga2::MultiObjective; use crate::evolution::{Telemetry, TelemetryMetrics, TelemetryMode}; use crate::population::*; +use crate::prelude::HeuristicObjective; use crate::utils::Timer; use crate::utils::{Environment, GenericError}; use std::hash::Hash; @@ -69,13 +69,11 @@ use std::sync::Arc; /// Represents solution in population defined as actual solution. pub trait HeuristicSolution: Send + Sync { /// Get fitness values of a given solution. - fn fitness<'a>(&'a self) -> Box + 'a>; + fn fitness(&self) -> impl Iterator; /// Creates a deep copy of the solution. fn deep_copy(&self) -> Self; } -/// Represents a heuristic objective function. -pub trait HeuristicObjective: MultiObjective + Send + Sync {} /// Specifies a dynamically dispatched type for heuristic population. pub type DynHeuristicPopulation = dyn HeuristicPopulation + Send + Sync; /// Specifies a heuristic result type. @@ -95,7 +93,7 @@ pub trait HeuristicContext: Send + Sync { fn selected<'a>(&'a self) -> Box + 'a>; /// Returns subset of solutions within their rank sorted according their quality. - fn ranked<'a>(&'a self) -> Box + 'a>; + fn ranked<'a>(&'a self) -> Box + 'a>; /// Returns current statistic used to track the search progress. fn statistics(&self) -> &HeuristicStatistics; @@ -201,7 +199,7 @@ where self.population.select() } - fn ranked<'a>(&'a self) -> Box + 'a> { + fn ranked<'a>(&'a self) -> Box + 'a> { self.population.ranked() } @@ -307,7 +305,7 @@ pub fn get_default_population( ) -> Box + Send + Sync> where O: HeuristicObjective + Shuffled + 'static, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered + 'static, + S: HeuristicSolution + RosomaxaWeighted + 'static, { if selection_size == 1 { Box::new(Greedy::new(objective, 1, None)) diff --git a/rosomaxa/src/population/elitism.rs b/rosomaxa/src/population/elitism.rs index 6907bec8a..2b6348660 100644 --- a/rosomaxa/src/population/elitism.rs +++ b/rosomaxa/src/population/elitism.rs @@ -3,7 +3,7 @@ mod elitism_test; use super::*; -use crate::algorithms::nsga2::select_and_rank; +use crate::algorithms::math::relative_distance; use crate::utils::Random; use crate::{HeuristicSpeed, HeuristicStatistics}; use std::cmp::Ordering; @@ -15,16 +15,15 @@ use std::sync::Arc; /// A function type to deduplicate individuals. pub type DedupFn = Box bool + Send + Sync>; -/// A simple evolution aware implementation of `Population` trait with the the following -/// characteristics: -/// -/// - sorting of individuals in population according their objective fitness using `NSGA-II` algorithm -/// - maintaining diversity of population based on their crowding distance -/// +/// Specifies default deduplication threshold used when no dedup function is specified. +const DEFAULT_DEDUP_FN_THRESHOLD: f64 = 0.05; + +/// A simple evolution aware implementation of `Population` trait which keeps predefined amount +/// of best known individuals. pub struct Elitism where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + DominanceOrdered, + S: HeuristicSolution, { objective: Arc, random: Arc, @@ -35,32 +34,16 @@ where dedup_fn: DedupFn, } -/// Keeps track of dominance order in the population for certain individual. -pub trait DominanceOrdered { - /// Gets dominance order in the population. - fn get_order(&self) -> &DominanceOrder; - /// Sets dominance order in the population. - fn set_order(&mut self, order: DominanceOrder); -} - /// Provides way to get a new objective by shuffling existing one. pub trait Shuffled { /// Returns a new objective. fn get_shuffled(&self, random: &(dyn Random + Send + Sync)) -> Self; } -/// Contains ordering information about individual in population. -#[derive(Clone, Debug, Default)] -pub struct DominanceOrder { - orig_index: usize, - seq_index: usize, - rank: usize, -} - impl HeuristicPopulation for Elitism where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + DominanceOrdered, + S: HeuristicSolution, { type Objective = O; type Individual = S; @@ -105,8 +88,8 @@ where } } - fn ranked<'a>(&'a self) -> Box + 'a> { - Box::new(self.individuals.iter().map(|individual| (individual, individual.get_order().rank))) + fn ranked<'a>(&'a self) -> Box + 'a> { + Box::new(self.individuals.iter()) } fn all<'a>(&'a self) -> Box + 'a> { @@ -125,7 +108,7 @@ where impl Elitism where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + DominanceOrdered, + S: HeuristicSolution, { /// Creates a new instance of `Elitism`. pub fn new( @@ -139,17 +122,9 @@ where random, max_population_size, selection_size, - Box::new(|_, a, b| { - if a.get_order().rank == b.get_order().rank { - // NOTE just using crowding distance here does not work - - let fitness_a = a.fitness(); - let fitness_b = b.fitness(); - - fitness_a.zip(fitness_b).all(|(a, b)| compare_floats(a, b) == Ordering::Equal) - } else { - false - } + Box::new(|objective, a, b| { + let distance = relative_distance(objective.fitness(a), objective.fitness(b)); + distance < DEFAULT_DEDUP_FN_THRESHOLD }), ) } @@ -193,20 +168,8 @@ where } fn sort(&mut self) { - let objective = self.objective.clone(); - - // get best order - let best_order = select_and_rank(self.individuals.as_slice(), self.individuals.len(), objective.as_ref()) - .into_iter() - .zip(0..) - .map(|(acc, idx)| DominanceOrder { orig_index: acc.index, seq_index: idx, rank: acc.rank }) - .collect::>(); - - assert_eq!(self.individuals.len(), best_order.len()); - - best_order.into_iter().for_each(|order| self.individuals[order.orig_index].set_order(order)); - self.individuals.sort_by(|a, b| a.get_order().seq_index.cmp(&b.get_order().seq_index)); - self.individuals.dedup_by(|a, b| (self.dedup_fn)(&objective, a, b)); + self.individuals.sort_by(|a, b| self.objective.total_order(a, b)); + self.individuals.dedup_by(|a, b| (self.dedup_fn)(&self.objective, a, b)); } fn ensure_max_population_size(&mut self) { @@ -217,16 +180,10 @@ where fn is_improved(&self, best_known_fitness: Option>) -> bool { best_known_fitness.zip(self.individuals.first()).map_or(true, |(best_known_fitness, new_best_known)| { - let dominance_order = new_best_known.get_order(); - if dominance_order.orig_index != dominance_order.seq_index { - // NOTE: search is unstable, need to check fitness values - best_known_fitness - .into_iter() - .zip(new_best_known.fitness()) - .any(|(a, b)| compare_floats(a, b) != Ordering::Equal) - } else { - false - } + best_known_fitness + .into_iter() + .zip(new_best_known.fitness()) + .any(|(a, b)| compare_floats(a, b) != Ordering::Equal) }) } } @@ -234,7 +191,7 @@ where impl Display for Elitism where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + DominanceOrdered, + S: HeuristicSolution, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let fitness = self.individuals.iter().fold(String::new(), |mut res, individual| { diff --git a/rosomaxa/src/population/greedy.rs b/rosomaxa/src/population/greedy.rs index 07fa7a41d..19100ec79 100644 --- a/rosomaxa/src/population/greedy.rs +++ b/rosomaxa/src/population/greedy.rs @@ -59,8 +59,8 @@ where } } - fn ranked<'a>(&'a self) -> Box + 'a> { - Box::new(self.best_known.iter().map(|individual| (individual, 0))) + fn ranked<'a>(&'a self) -> Box + 'a> { + Box::new(self.best_known.iter()) } fn all<'a>(&'a self) -> Box + 'a> { diff --git a/rosomaxa/src/population/mod.rs b/rosomaxa/src/population/mod.rs index b05b547f6..ef5f2d7bf 100644 --- a/rosomaxa/src/population/mod.rs +++ b/rosomaxa/src/population/mod.rs @@ -1,18 +1,13 @@ //! Specifies population types. mod elitism; -pub use self::elitism::DominanceOrder; -pub use self::elitism::DominanceOrdered; -pub use self::elitism::Elitism; -pub use self::elitism::Shuffled; +pub use self::elitism::{Elitism, Shuffled}; mod greedy; pub use self::greedy::Greedy; mod rosomaxa; -pub use self::rosomaxa::Rosomaxa; -pub use self::rosomaxa::RosomaxaConfig; -pub use self::rosomaxa::RosomaxaWeighted; +pub use self::rosomaxa::{Rosomaxa, RosomaxaConfig, RosomaxaWeighted}; use crate::prelude::*; use std::cmp::Ordering; @@ -55,7 +50,7 @@ pub trait HeuristicPopulation: Display + Send + Sync { fn select<'a>(&'a self) -> Box + 'a>; /// Returns subset of individuals within their rank sorted according their quality. - fn ranked<'a>(&'a self) -> Box + 'a>; + fn ranked<'a>(&'a self) -> Box + 'a>; /// Returns all individuals in arbitrary order. fn all<'a>(&'a self) -> Box + 'a>; diff --git a/rosomaxa/src/population/rosomaxa.rs b/rosomaxa/src/population/rosomaxa.rs index bca422ec3..93212c886 100644 --- a/rosomaxa/src/population/rosomaxa.rs +++ b/rosomaxa/src/population/rosomaxa.rs @@ -5,7 +5,7 @@ mod rosomaxa_test; use super::*; use crate::algorithms::gsom::*; use crate::algorithms::math::relative_distance; -use crate::population::elitism::{DedupFn, DominanceOrdered, Shuffled}; +use crate::population::elitism::{DedupFn, Shuffled}; use crate::utils::{Environment, Random}; use rand::prelude::SliceRandom; use rayon::iter::Either; @@ -60,7 +60,7 @@ pub trait RosomaxaWeighted: Input { pub struct Rosomaxa where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { objective: Arc, environment: Arc, @@ -72,14 +72,14 @@ where impl HeuristicPopulation for Rosomaxa where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { type Objective = O; type Individual = S; fn add_all(&mut self, individuals: Vec) -> bool { // NOTE avoid extra deep copy - let best_known = self.elite.ranked().map(|(i, _)| i).next(); + let best_known = self.elite.ranked().next(); let elite = individuals .iter() .filter(|individual| self.is_comparable_with_best_known(individual, best_known)) @@ -99,7 +99,7 @@ where } fn add(&mut self, individual: Self::Individual) -> bool { - let best_known = self.elite.ranked().map(|(i, _)| i).next(); + let best_known = self.elite.ranked().next(); let individual = init_individual(individual); let is_improved = if self.is_comparable_with_best_known(&individual, best_known) { self.elite.add(individual.deep_copy()) @@ -162,7 +162,7 @@ where } } - fn ranked<'a>(&'a self) -> Box + 'a> { + fn ranked<'a>(&'a self) -> Box + 'a> { self.elite.ranked() } @@ -193,7 +193,7 @@ type IndividualNetwork = Network, IndividualSto impl Rosomaxa where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { /// Creates a new instance of `Rosomaxa`. pub fn new(objective: Arc, environment: Arc, config: RosomaxaConfig) -> Result { @@ -390,7 +390,7 @@ where impl Display for Rosomaxa where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self.phase { @@ -406,7 +406,7 @@ where impl<'a, O, S> TryFrom<&'a Rosomaxa> for NetworkState where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { type Error = String; @@ -422,7 +422,7 @@ where enum RosomaxaPhases where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { Initial { solutions: Vec, @@ -441,7 +441,7 @@ where fn init_individual(individual: S) -> S where - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { let mut individual = individual; individual.init_weights(); @@ -452,7 +452,7 @@ where struct IndividualStorageFactory where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { node_size: usize, random: Arc, @@ -462,7 +462,7 @@ where impl StorageFactory> for IndividualStorageFactory where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { fn eval(&self) -> IndividualStorage { let mut elitism = Elitism::new_with_dedup( @@ -482,7 +482,7 @@ where struct IndividualStorage where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { population: Elitism, } @@ -490,7 +490,7 @@ where impl Storage for IndividualStorage where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { type Item = S; @@ -499,7 +499,7 @@ where } fn iter<'a>(&'a self) -> Box + 'a> { - Box::new(self.population.ranked().map(|(r, _)| r)) + Box::new(self.population.ranked()) } fn drain(&mut self, range: R) -> Vec @@ -521,7 +521,7 @@ where impl Display for IndividualStorage where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.population) @@ -531,7 +531,7 @@ where fn create_dedup_fn(threshold: f64) -> DedupFn where O: HeuristicObjective + Shuffled, - S: HeuristicSolution + RosomaxaWeighted + DominanceOrdered, + S: HeuristicSolution + RosomaxaWeighted, { // NOTE custom dedup rule to increase diversity property Box::new(move |objective, a, b| match objective.total_order(a, b) { diff --git a/rosomaxa/src/prelude.rs b/rosomaxa/src/prelude.rs index 42a9a13ff..7f5808aed 100644 --- a/rosomaxa/src/prelude.rs +++ b/rosomaxa/src/prelude.rs @@ -1,13 +1,13 @@ //! This module reimports a common used types. pub use crate::HeuristicContext; -pub use crate::HeuristicObjective; pub use crate::HeuristicResult; pub use crate::HeuristicSolution; pub use crate::HeuristicSpeed; pub use crate::HeuristicStatistics; pub use crate::Stateful; +pub use crate::evolution::objectives::HeuristicObjective; pub use crate::evolution::strategies::EvolutionStrategy; pub use crate::evolution::EvolutionConfig; pub use crate::evolution::EvolutionConfigBuilder; @@ -25,9 +25,6 @@ pub use crate::hyper::HyperHeuristic; pub use crate::termination::Termination; -pub use crate::algorithms::nsga2::MultiObjective; -pub use crate::algorithms::nsga2::Objective; - pub use crate::utils::DefaultRandom; pub use crate::utils::Environment; pub use crate::utils::GenericError; diff --git a/rosomaxa/src/termination/min_variation.rs b/rosomaxa/src/termination/min_variation.rs index 6f70f08bb..2f115009b 100644 --- a/rosomaxa/src/termination/min_variation.rs +++ b/rosomaxa/src/termination/min_variation.rs @@ -152,7 +152,7 @@ where fn is_termination(&self, heuristic_ctx: &mut Self::Context) -> bool { let first_individual = heuristic_ctx.ranked().next(); - if let Some((first, _)) = first_individual { + if let Some(first) = first_individual { let objective = heuristic_ctx.objective(); let fitness = objective.fitness(first).collect::>(); let result = self.update_and_check(heuristic_ctx, fitness); diff --git a/rosomaxa/src/termination/target_proximity.rs b/rosomaxa/src/termination/target_proximity.rs index 617691ea4..a77d49362 100644 --- a/rosomaxa/src/termination/target_proximity.rs +++ b/rosomaxa/src/termination/target_proximity.rs @@ -45,7 +45,7 @@ where fn is_termination(&self, heuristic_ctx: &mut Self::Context) -> bool { // NOTE ignore pareto front, use the first solution only for comparison - heuristic_ctx.ranked().next().map_or(false, |(solution, _)| { + heuristic_ctx.ranked().next().map_or(false, |solution| { let distance = relative_distance(self.target_fitness.iter().cloned(), solution.fitness()); distance < self.distance_threshold }) diff --git a/rosomaxa/tests/helpers/algorithms/mod.rs b/rosomaxa/tests/helpers/algorithms/mod.rs index 0a5faca71..a12350422 100644 --- a/rosomaxa/tests/helpers/algorithms/mod.rs +++ b/rosomaxa/tests/helpers/algorithms/mod.rs @@ -1,2 +1 @@ pub mod gsom; -pub mod nsga2; diff --git a/rosomaxa/tests/helpers/algorithms/nsga2/mod.rs b/rosomaxa/tests/helpers/algorithms/nsga2/mod.rs deleted file mode 100644 index 5efcfdad1..000000000 --- a/rosomaxa/tests/helpers/algorithms/nsga2/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod objective; -pub use self::objective::*; diff --git a/rosomaxa/tests/helpers/algorithms/nsga2/objective.rs b/rosomaxa/tests/helpers/algorithms/nsga2/objective.rs deleted file mode 100644 index f240b9bd5..000000000 --- a/rosomaxa/tests/helpers/algorithms/nsga2/objective.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::algorithms::nsga2::{dominance_order, MultiObjective, Objective}; -use crate::prelude::*; -use std::cmp::Ordering; -use std::sync::Arc; - -pub type SliceObjective = Arc> + Send + Sync>; - -pub struct SliceDimensionObjective { - dimension: usize, -} - -impl SliceDimensionObjective { - pub fn new(dimension: usize) -> Self { - Self { dimension } - } -} - -impl Objective for SliceDimensionObjective { - type Solution = Vec; - - fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { - compare_floats(a[self.dimension], b[self.dimension]) - } - - fn distance(&self, a: &Self::Solution, b: &Self::Solution) -> f64 { - a[self.dimension] - b[self.dimension] - } - - fn fitness(&self, solution: &Self::Solution) -> f64 { - solution[self.dimension] - } -} - -pub struct SliceSumObjective; - -impl Objective for SliceSumObjective { - type Solution = Vec; - - fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { - compare_floats(self.fitness(a), self.fitness(b)) - } - - fn distance(&self, a: &Self::Solution, b: &Self::Solution) -> f64 { - self.fitness(a) - self.fitness(b) - } - - fn fitness(&self, solution: &Self::Solution) -> f64 { - solution.iter().sum::() - } -} - -#[derive(Default)] -pub struct SliceMultiObjective { - objectives: Vec, -} - -impl SliceMultiObjective { - pub fn new(objectives: Vec) -> Self { - Self { objectives } - } -} - -impl MultiObjective for SliceMultiObjective { - type Solution = Vec; - - fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { - // TODO support more dimensions if necessary - assert_eq!(a.len(), 2); - assert_eq!(a.len(), b.len()); - - if a[0] < b[0] && a[1] <= b[1] || a[0] <= b[0] && a[1] < b[1] { - Ordering::Less - } else if a[0] > b[0] && a[1] >= b[1] || a[0] >= b[0] && a[1] > b[1] { - Ordering::Greater - } else { - Ordering::Equal - } - } - - fn fitness<'a>(&'a self, solution: &'a Self::Solution) -> Box + 'a> { - Box::new(self.objectives.iter().map(|o| o.fitness(solution))) - } - - fn get_order(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result { - self.objectives.get(idx).map(|o| o.total_order(a, b)).ok_or_else(|| format!("wrong index: {idx}").into()) - } - - fn get_distance(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result { - self.objectives.get(idx).map(|o| o.distance(a, b)).ok_or_else(|| format!("wrong index: {idx}").into()) - } - - fn size(&self) -> usize { - self.objectives.len() - } -} - -pub struct SliceHierarchicalObjective { - primary_objectives: Vec, - secondary_objectives: Vec, -} - -impl SliceHierarchicalObjective { - pub fn new(primary_objectives: Vec, secondary_objectives: Vec) -> Self { - Self { primary_objectives, secondary_objectives } - } -} - -impl MultiObjective for SliceHierarchicalObjective { - type Solution = Vec; - - fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { - match dominance_order(a, b, self.primary_objectives.iter().map(|o| o.as_ref())) { - Ordering::Equal => dominance_order(a, b, self.secondary_objectives.iter().map(|o| o.as_ref())), - order => order, - } - } - - fn fitness<'a>(&'a self, solution: &'a Self::Solution) -> Box + 'a> { - Box::new(self.primary_objectives.iter().chain(self.secondary_objectives.iter()).map(|o| o.fitness(solution))) - } - - fn get_order(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result { - self.primary_objectives - .iter() - .chain(self.secondary_objectives.iter()) - .nth(idx) - .map(|o| o.total_order(a, b)) - .ok_or_else(|| format!("wrong index: {idx}").into()) - } - - fn get_distance(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result { - self.primary_objectives - .iter() - .chain(self.secondary_objectives.iter()) - .nth(idx) - .map(|o| o.distance(a, b)) - .ok_or_else(|| format!("wrong index: {idx}").into()) - } - - fn size(&self) -> usize { - self.primary_objectives.len() + self.secondary_objectives.len() - } -} diff --git a/rosomaxa/tests/unit/algorithms/nsga2/crowding_distance_test.rs b/rosomaxa/tests/unit/algorithms/nsga2/crowding_distance_test.rs deleted file mode 100644 index 21a6f014d..000000000 --- a/rosomaxa/tests/unit/algorithms/nsga2/crowding_distance_test.rs +++ /dev/null @@ -1,59 +0,0 @@ -use super::*; -use crate::algorithms::nsga2::non_dominated_sort::non_dominated_sort; -use crate::helpers::algorithms::nsga2::*; -use std::sync::Arc; - -#[test] -fn can_get_crowding_distance() { - let mo = SliceMultiObjective::new(vec![ - Arc::new(SliceDimensionObjective::new(0)), - Arc::new(SliceDimensionObjective::new(1)), - ]); - - let a = vec![1., 3.]; - let b = vec![3., 1.]; - let c = vec![3., 3.]; - let d = vec![2., 2.]; - - let solutions = vec![a.clone(), b.clone(), c.clone(), d.clone()]; - - let f0 = non_dominated_sort(&solutions, &mo); - - let solutions = f0.iter().collect::>(); - assert_eq!(3, solutions.len()); - assert_eq!(&a, solutions[0].0); - assert_eq!(&b, solutions[1].0); - assert_eq!(&d, solutions[2].0); - - let (crowding, stat) = assign_crowding_distance(&f0, &mo); - - assert_eq!(2, stat.len()); - assert_eq!(2.0, stat[0].spread); - assert_eq!(2.0, stat[1].spread); - - // Same number as solutions in front - assert_eq!(3, crowding.len()); - // All have rank 0 - assert_eq!(0, crowding[0].rank); - assert_eq!(0, crowding[1].rank); - assert_eq!(0, crowding[2].rank); - - let ca = crowding.iter().find(|i| i.solution.eq(&a)).unwrap(); - let cb = crowding.iter().find(|i| i.solution.eq(&b)).unwrap(); - let cd = crowding.iter().find(|i| i.solution.eq(&d)).unwrap(); - - assert_eq!(f64::INFINITY, ca.crowding_distance); - assert_eq!(f64::INFINITY, cb.crowding_distance); - - // only cd is in the middle. spread is in both dimensions the same - // (2.0). norm is 1.0 / (spread * #objectives) = 1.0 / 4.0. As we - // add two times 0.5, the crowding distance should be 1.0. - assert_eq!(1.0, cd.crowding_distance); - - let f1 = f0.next_front(); - let solutions = f1.iter().collect::>(); - assert_eq!(1, solutions.len()); - assert_eq!(&c, solutions[0].0); - - assert!(f1.next_front().is_empty()); -} diff --git a/rosomaxa/tests/unit/algorithms/nsga2/non_dominated_sort_test.rs b/rosomaxa/tests/unit/algorithms/nsga2/non_dominated_sort_test.rs deleted file mode 100644 index ba5d5becf..000000000 --- a/rosomaxa/tests/unit/algorithms/nsga2/non_dominated_sort_test.rs +++ /dev/null @@ -1,117 +0,0 @@ -use super::*; -use crate::helpers::algorithms::nsga2::*; -use crate::prelude::Objective; - -/// Creates `n_fronts` with each having `n` solutions in it. -pub fn create_solutions_with_n_fronts(n: usize, n_fronts: usize) -> (Vec>, Vec>) { - let mut solutions = Vec::with_capacity(n * n_fronts); - let mut expected_fronts = Vec::with_capacity(n_fronts); - - for front in 0..n_fronts { - let mut current_front = Vec::with_capacity(n); - for i in 0..n { - solutions.push(vec![(front + i) as f64, (front + n - i) as f64]); - current_front.push(front * n + i); - } - expected_fronts.push(current_front); - } - - (solutions, expected_fronts) -} - -#[test] -fn can_compare_dominant_relations() { - let objective = SliceMultiObjective::default(); - let a = vec![1., 2.]; - let b = vec![1., 3.]; - let c = vec![0., 2.]; - - // a < b - assert_eq!(Ordering::Less, objective.total_order(&a, &b)); - // c < a - assert_eq!(Ordering::Less, objective.total_order(&c, &a)); - // transitivity => c < b - assert_eq!(Ordering::Less, objective.total_order(&c, &b)); - - // Just reverse the relation: for all a, b: a < b => b > a - - // b > a - assert_eq!(Ordering::Greater, objective.total_order(&b, &a)); - // a > c - assert_eq!(Ordering::Greater, objective.total_order(&a, &c)); - // transitivity => b > c - assert_eq!(Ordering::Greater, objective.total_order(&b, &c)); -} - -#[test] -fn can_compare_non_dominant_relations() { - let objective = SliceMultiObjective::default(); - let a = vec![1., 2.]; - let b = vec![2., 1.]; - - // Non-domination due to reflexivity - assert_eq!(Ordering::Equal, objective.total_order(&a, &a)); - assert_eq!(Ordering::Equal, objective.total_order(&b, &b)); - - // Non-domination - assert_eq!(Ordering::Equal, objective.total_order(&a, &b)); - assert_eq!(Ordering::Equal, objective.total_order(&b, &a)); -} - -#[test] -fn can_use_simple_objectives() { - let a = vec![1., 2.]; - let b = vec![2., 1.]; - assert_eq!(Ordering::Less, SliceDimensionObjective::new(0).total_order(&a, &b)); - assert_eq!(Ordering::Greater, SliceDimensionObjective::new(1).total_order(&a, &b)); - assert_eq!(Ordering::Equal, SliceSumObjective.total_order(&a, &b)); - - assert_eq!(-1.0, SliceDimensionObjective::new(0).distance(&a, &b)); - assert_eq!(1.0, SliceDimensionObjective::new(1).distance(&a, &b)); - assert_eq!(0.0, SliceSumObjective.distance(&a, &b)); -} - -#[test] -fn test_non_dominated_sort() { - let objective = SliceMultiObjective::default(); - let solutions = vec![vec![1., 2.], vec![1., 2.], vec![2., 1.], vec![1., 3.], vec![0., 2.]]; - - let f0 = non_dominated_sort(&solutions, &objective); - assert_eq!(0, f0.rank()); - assert_eq!(&[2, 4], f0.current_front.as_slice()); - - let f1 = f0.next_front(); - assert_eq!(1, f1.rank()); - assert_eq!(&[0, 1], f1.current_front.as_slice()); - - let f2 = f1.next_front(); - assert_eq!(2, f2.rank()); - assert_eq!(&[3], f2.current_front.as_slice()); - - let f3 = f2.next_front(); - assert_eq!(3, f3.rank()); - assert!(f3.is_empty()); -} - -fn test_fronts(n: usize, n_fronts: usize) { - let objective = SliceMultiObjective::default(); - let (solutions, expected_fronts) = create_solutions_with_n_fronts(n, n_fronts); - - let mut f = non_dominated_sort(&solutions, &objective); - for (expected_rank, expected_front) in expected_fronts.iter().enumerate() { - assert_eq!(expected_rank, f.rank()); - assert_eq!(&expected_front[..], f.current_front.as_slice()); - f = f.next_front(); - } - assert!(f.is_empty()); -} - -#[test] -fn test_non_dominated_sort_5_5() { - test_fronts(5, 5); -} - -#[test] -fn test_non_dominated_sort_1000_5() { - test_fronts(1_000, 5); -} diff --git a/rosomaxa/tests/unit/algorithms/nsga2/nsga2_sort_test.rs b/rosomaxa/tests/unit/algorithms/nsga2/nsga2_sort_test.rs deleted file mode 100644 index 4ad667a84..000000000 --- a/rosomaxa/tests/unit/algorithms/nsga2/nsga2_sort_test.rs +++ /dev/null @@ -1,93 +0,0 @@ -use super::*; -use crate::helpers::algorithms::nsga2::*; -use std::f64::consts::PI; -use std::sync::Arc; - -fn fitness(individual: &[f64]) -> Vec { - const SCALE: f64 = 10.; - - let r = individual[0]; - let h = individual[1]; - - let sh = (r * r + h * h).sqrt(); - - let s = (PI * r * sh) * SCALE; - let t = PI * r * (r + sh) * SCALE; - - vec![s.round(), t.round()] -} - -#[test] -fn can_use_select_and_rank() { - let population = vec![ - vec![10.0, 19.61], - vec![4.99, 5.10], - vec![6.09, 0.79], - vec![6.91, 10.62], - vec![5.21, 18.87], - vec![7.90, 8.98], - vec![9.84, 0.78], - vec![4.96, 0.60], - vec![6.24, 19.66], - vec![6.90, 15.09], - vec![5.20, 18.86], - vec![7.89, 8.97], - ]; - let mo = SliceMultiObjective::new(vec![ - Arc::new(SliceDimensionObjective::new(0)), - Arc::new(SliceDimensionObjective::new(1)), - ]); - - // rate population (calculate fitness) - let rated_population = population.iter().map(|individual| fitness(individual.as_slice())).collect::>(); - let ranked_population = select_and_rank(&rated_population, 7, &mo); - - let results = ranked_population.iter().map(|s| vec![s.index, s.rank]).collect::>(); - - assert_eq!(results.len(), 7); - - assert_eq!(results[0], &[7, 0]); - - assert_eq!(results[1], &[1, 1]); - - assert_eq!(results[2], &[2, 2]); - - assert_eq!(results[3], &[10, 3]); - assert_eq!(results[4], &[3, 3]); - - assert_eq!(results[5], &[4, 4]); - assert_eq!(results[6], &[11, 4]); -} - -parameterized_test! {can_use_select_and_rank_with_non_transient_relationship_by_hierarchical_objective, solutions, { - can_use_select_and_rank_with_non_transient_relationship_by_hierarchical_objective_impl(solutions); -}} - -can_use_select_and_rank_with_non_transient_relationship_by_hierarchical_objective! { - case01: &[ - vec![6., 6., 101.], vec![7., 5., 102.], vec![5., 6., 103.], vec![8., 8., 108.], vec![9., 9., 109.], - ], - case02: &[ - vec![2., 3., 101.], vec![4., 3., 103.], vec![2., 4., 104.], vec![3., 4., 102.], - ], - case03: &[ - vec![2., 5., 102.], vec![3., 5., 101.], vec![2., 5., 102.], - ], - case04: &[ - vec![164., 5., 407.451], vec![166., 5., 393.545], vec![166., 5., 395.197], - vec![166., 5., 395.558], vec![164., 5., 407.451], vec![164., 5., 407.5] - ], -} - -fn can_use_select_and_rank_with_non_transient_relationship_by_hierarchical_objective_impl(solutions: &[Vec]) { - let ranked = select_and_rank( - solutions, - solutions.len(), - &SliceHierarchicalObjective::new( - vec![Arc::new(SliceDimensionObjective::new(0)), Arc::new(SliceDimensionObjective::new(1))], - vec![Arc::new(SliceDimensionObjective::new(2))], - ), - ); - - assert_eq!(ranked.len(), solutions.len()) -} diff --git a/rosomaxa/tests/unit/hyper/dynamic_selective_test.rs b/rosomaxa/tests/unit/hyper/dynamic_selective_test.rs index 625e023ec..bd5620cf2 100644 --- a/rosomaxa/tests/unit/hyper/dynamic_selective_test.rs +++ b/rosomaxa/tests/unit/hyper/dynamic_selective_test.rs @@ -111,7 +111,7 @@ fn can_display_heuristic_info() { fn can_handle_when_objective_lies() { struct LiarObjective; - impl MultiObjective for LiarObjective { + impl HeuristicObjective for LiarObjective { type Solution = TestData; fn total_order(&self, _: &Self::Solution, _: &Self::Solution) -> Ordering { @@ -120,27 +120,14 @@ fn can_handle_when_objective_lies() { } fn fitness<'a>(&'a self, solution: &'a Self::Solution) -> Box + 'a> { - solution.fitness() - } - - fn get_order(&self, _: &Self::Solution, _: &Self::Solution, _: usize) -> Result { - unreachable!() - } - - fn get_distance(&self, _: &Self::Solution, _: &Self::Solution, _: usize) -> Result { - Ok(0.) - } - - fn size(&self) -> usize { - 1 + Box::new(solution.fitness()) } } - impl HeuristicObjective for LiarObjective {} struct TestData; impl HeuristicSolution for TestData { - fn fitness<'a>(&'a self) -> Box + 'a> { + fn fitness(&self) -> impl Iterator { // fitness is the same Box::new(once(1.)) } diff --git a/rosomaxa/tests/unit/population/elitism_test.rs b/rosomaxa/tests/unit/population/elitism_test.rs index 4013f75cf..c5273aed4 100644 --- a/rosomaxa/tests/unit/population/elitism_test.rs +++ b/rosomaxa/tests/unit/population/elitism_test.rs @@ -3,11 +3,11 @@ use crate::example::*; use crate::helpers::example::create_example_objective; fn get_best_fitness(population: &Elitism) -> f64 { - population.ranked().next().unwrap().0.fitness() + population.ranked().next().unwrap().fitness() } fn get_all_fitness(population: &Elitism) -> Vec { - population.ranked().map(|(s, _)| s.fitness()).collect() + population.ranked().map(|s| s.fitness()).collect() } fn create_objective_population( @@ -117,3 +117,35 @@ fn can_handle_empty() { assert!(population.select().next().is_none()); } + +parameterized_test! {can_detect_improvement, (new_individuals, expected), { + can_detect_improvement_impl(new_individuals, expected); +}} + +can_detect_improvement! { + case_01_add_one_same: (vec![vec![0.5, 0.5]], false), + case_02_add_one_worse: (vec![vec![0.7, 0.5]], false), + case_03_add_one_worse: (vec![vec![0.5, 0.7]], false), + + case_04_add_one_better: (vec![vec![0.4, 0.5]], true), + case_05_add_one_better: (vec![vec![0.5, 0.4]], true), + + case_06_add_more_worse: (vec![vec![0.5, 0.7], vec![0.6, 0.6]], false), + case_07_add_more_same: (vec![vec![0.5, 0.5], vec![0.5, 0.5]], false), + + case_08_add_more_mixed: (vec![vec![0.5, 0.4], vec![0.5, 0.7], vec![0.5, 0.5]], true), +} + +fn can_detect_improvement_impl(new_individuals: Vec>, expected: bool) { + let objective = Arc::new(VectorObjective::new( + Arc::new(|data| data.iter().map(|&a| a * a).sum::().sqrt()), + Arc::new(|data: &[f64]| data.to_vec()), + )); + let mut population = Elitism::<_, _>::new(objective.clone(), Environment::default().random, 2, 1); + population.add(VectorSolution::new(vec![0.5, 0.5], objective.clone())); + + assert_eq!( + population.add_with_iter(new_individuals.into_iter().map(|data| VectorSolution::new(data, objective.clone()))), + expected + ) +} diff --git a/rosomaxa/tests/unit/population/greedy_test.rs b/rosomaxa/tests/unit/population/greedy_test.rs index e682735ad..e6f82e981 100644 --- a/rosomaxa/tests/unit/population/greedy_test.rs +++ b/rosomaxa/tests/unit/population/greedy_test.rs @@ -3,7 +3,7 @@ use crate::example::*; use crate::helpers::example::create_example_objective; fn get_best_fitness(population: &Greedy) -> f64 { - population.ranked().next().unwrap().0.fitness() + population.ranked().next().unwrap().fitness() } #[test] diff --git a/vrp-core/src/construction/features/fast_service.rs b/vrp-core/src/construction/features/fast_service.rs index 38a0d9083..64e8a732c 100644 --- a/vrp-core/src/construction/features/fast_service.rs +++ b/vrp-core/src/construction/features/fast_service.rs @@ -53,7 +53,7 @@ struct FastServiceObjective { phantom: PhantomData, } -impl Objective for FastServiceObjective { +impl FeatureObjective for FastServiceObjective { type Solution = InsertionContext; fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { @@ -84,9 +84,7 @@ impl Objective for FastServiceObjective { }) .sum::() } -} -impl FeatureObjective for FastServiceObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { let (route_ctx, activity_ctx) = match move_ctx { MoveContext::Route { .. } => return Cost::default(), diff --git a/vrp-core/src/construction/features/fleet_usage.rs b/vrp-core/src/construction/features/fleet_usage.rs index 013db9be7..54ea478bc 100644 --- a/vrp-core/src/construction/features/fleet_usage.rs +++ b/vrp-core/src/construction/features/fleet_usage.rs @@ -57,15 +57,13 @@ struct FleetUsageObjective { solution_estimate_fn: Box Cost + Send + Sync>, } -impl Objective for FleetUsageObjective { +impl FeatureObjective for FleetUsageObjective { type Solution = InsertionContext; fn fitness(&self, solution: &Self::Solution) -> f64 { (self.solution_estimate_fn)(&solution.solution) } -} -impl FeatureObjective for FleetUsageObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { match move_ctx { MoveContext::Route { route_ctx, .. } => (self.route_estimate_fn)(route_ctx), diff --git a/vrp-core/src/construction/features/minimize_unassigned.rs b/vrp-core/src/construction/features/minimize_unassigned.rs index fadc764d3..23ba6f259 100644 --- a/vrp-core/src/construction/features/minimize_unassigned.rs +++ b/vrp-core/src/construction/features/minimize_unassigned.rs @@ -25,7 +25,7 @@ struct MinimizeUnassignedObjective { unassigned_job_estimator: UnassignedJobEstimator, } -impl Objective for MinimizeUnassignedObjective { +impl FeatureObjective for MinimizeUnassignedObjective { type Solution = InsertionContext; fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { @@ -48,9 +48,7 @@ impl Objective for MinimizeUnassignedObjective { .map(|(job, _)| (self.unassigned_job_estimator)(&solution.solution, job)) .sum::() } -} -impl FeatureObjective for MinimizeUnassignedObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { match move_ctx { MoveContext::Route { solution_ctx, job, .. } => -1. * (self.unassigned_job_estimator)(solution_ctx, job), diff --git a/vrp-core/src/construction/features/total_value.rs b/vrp-core/src/construction/features/total_value.rs index b7e26529f..8a0b50f1d 100644 --- a/vrp-core/src/construction/features/total_value.rs +++ b/vrp-core/src/construction/features/total_value.rs @@ -49,7 +49,7 @@ struct MaximizeTotalValueObjective { estimate_value_fn: EstimateValueFn, } -impl Objective for MaximizeTotalValueObjective { +impl FeatureObjective for MaximizeTotalValueObjective { type Solution = InsertionContext; fn fitness(&self, solution: &Self::Solution) -> f64 { @@ -57,9 +57,7 @@ impl Objective for MaximizeTotalValueObjective { route_ctx.route().tour.jobs().fold(acc, |acc, job| acc + (self.estimate_value_fn)(route_ctx, job)) }) } -} -impl FeatureObjective for MaximizeTotalValueObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { match move_ctx { MoveContext::Route { route_ctx, job, .. } => (self.estimate_value_fn)(route_ctx, job), diff --git a/vrp-core/src/construction/features/tour_compactness.rs b/vrp-core/src/construction/features/tour_compactness.rs index 66a4231ed..562ef2c8f 100644 --- a/vrp-core/src/construction/features/tour_compactness.rs +++ b/vrp-core/src/construction/features/tour_compactness.rs @@ -39,7 +39,7 @@ struct TourCompactnessObjective { thresholds: Option<(f64, f64)>, } -impl Objective for TourCompactnessObjective { +impl FeatureObjective for TourCompactnessObjective { type Solution = InsertionContext; fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { @@ -68,9 +68,7 @@ impl Objective for TourCompactnessObjective { fn fitness(&self, solution: &Self::Solution) -> f64 { solution.solution.state.get(&self.state_key).and_then(|s| s.downcast_ref::()).copied().unwrap_or_default() } -} -impl FeatureObjective for TourCompactnessObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { match move_ctx { MoveContext::Route { solution_ctx, route_ctx, job } => { diff --git a/vrp-core/src/construction/features/tour_order.rs b/vrp-core/src/construction/features/tour_order.rs index f48beaa67..c9e72011d 100644 --- a/vrp-core/src/construction/features/tour_order.rs +++ b/vrp-core/src/construction/features/tour_order.rs @@ -106,7 +106,7 @@ struct TourOrderObjective { order_fn: TourOrderFn, } -impl Objective for TourOrderObjective { +impl FeatureObjective for TourOrderObjective { type Solution = InsertionContext; fn fitness(&self, solution: &Self::Solution) -> f64 { @@ -119,9 +119,7 @@ impl Objective for TourOrderObjective { .cloned() .unwrap_or_else(|| get_violations(solution.routes.as_slice(), &self.order_fn)) as f64 } -} -impl FeatureObjective for TourOrderObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { match move_ctx { MoveContext::Activity { route_ctx, activity_ctx } => { diff --git a/vrp-core/src/construction/features/transport.rs b/vrp-core/src/construction/features/transport.rs index 9bf625bea..5531c01de 100644 --- a/vrp-core/src/construction/features/transport.rs +++ b/vrp-core/src/construction/features/transport.rs @@ -307,15 +307,13 @@ impl TransportObjective { } } -impl Objective for TransportObjective { +impl FeatureObjective for TransportObjective { type Solution = InsertionContext; fn fitness(&self, solution: &Self::Solution) -> f64 { (self.fitness_fn)(solution) } -} -impl FeatureObjective for TransportObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { match move_ctx { MoveContext::Route { route_ctx, .. } => self.estimate_route(route_ctx), diff --git a/vrp-core/src/construction/features/work_balance.rs b/vrp-core/src/construction/features/work_balance.rs index 9e59b9fbf..9034fc5e7 100644 --- a/vrp-core/src/construction/features/work_balance.rs +++ b/vrp-core/src/construction/features/work_balance.rs @@ -152,7 +152,7 @@ struct WorkBalanceObjective { solution_estimate_fn: Arc f64 + Send + Sync>, } -impl Objective for WorkBalanceObjective { +impl FeatureObjective for WorkBalanceObjective { type Solution = InsertionContext; fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { @@ -185,9 +185,7 @@ impl Objective for WorkBalanceObjective { .cloned() .unwrap_or_else(|| (self.solution_estimate_fn)(&solution.solution)) } -} -impl FeatureObjective for WorkBalanceObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { match move_ctx { MoveContext::Route { route_ctx, .. } => { diff --git a/vrp-core/src/construction/heuristics/context.rs b/vrp-core/src/construction/heuristics/context.rs index 9dd9098b9..f56b4f6f0 100644 --- a/vrp-core/src/construction/heuristics/context.rs +++ b/vrp-core/src/construction/heuristics/context.rs @@ -89,7 +89,7 @@ impl InsertionContext { } impl HeuristicSolution for InsertionContext { - fn fitness<'a>(&'a self) -> Box + 'a> { + fn fitness(&self) -> impl Iterator { self.problem.goal.fitness(self) } diff --git a/vrp-core/src/models/goal.rs b/vrp-core/src/models/goal.rs index bf56a276d..3713c7a71 100644 --- a/vrp-core/src/models/goal.rs +++ b/vrp-core/src/models/goal.rs @@ -8,7 +8,7 @@ use crate::models::common::Cost; use crate::models::problem::Job; use hashbrown::{HashMap, HashSet}; use rand::prelude::SliceRandom; -use rosomaxa::algorithms::nsga2::dominance_order; +use rosomaxa::evolution::objectives::dominance_order; use rosomaxa::population::Shuffled; use rosomaxa::prelude::*; use std::cmp::Ordering; @@ -370,19 +370,30 @@ pub trait FeatureConstraint { } /// Defines feature objective behavior. -pub trait FeatureObjective: Objective { +pub trait FeatureObjective { + /// The solution value type that we define the objective on. + type Solution; + + /// An objective defines a total ordering between any two solution values. + fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { + compare_floats(self.fitness(a), self.fitness(b)) + } + + /// An objective fitness values for given `solution`. + fn fitness(&self, solution: &Self::Solution) -> f64; + /// Estimates a cost of insertion. fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost; } -impl MultiObjective for GoalContext { +impl HeuristicObjective for GoalContext { type Solution = InsertionContext; fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { self.global_objectives .iter() .try_fold(Ordering::Equal, |_, objectives| { - match dominance_order(a, b, objectives.iter().map(|o| o.as_ref())) { + match dominance_order(a, b, objectives.iter().map(|o| |a, b| o.total_order(a, b))) { Ordering::Equal => ControlFlow::Continue(Ordering::Equal), order => ControlFlow::Break(order), } @@ -393,28 +404,8 @@ impl MultiObjective for GoalContext { fn fitness<'a>(&'a self, solution: &'a Self::Solution) -> Box + '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 { - self.flatten_objectives - .get(idx) - .map(|o| o.total_order(a, b)) - .ok_or_else(|| format!("cannot get total_order with index: {idx}").into()) - } - - fn get_distance(&self, a: &Self::Solution, b: &Self::Solution, idx: usize) -> Result { - self.flatten_objectives - .get(idx) - .map(|o| o.distance(a, b)) - .ok_or_else(|| format!("cannot get distance with index: {idx}").into()) - } - - fn size(&self) -> usize { - self.flatten_objectives.len() - } } -impl HeuristicObjective for GoalContext {} - impl Shuffled for GoalContext { /// Returns a new instance of `GoalContext` with shuffled objectives. fn get_shuffled(&self, random: &(dyn Random + Send + Sync)) -> Self { diff --git a/vrp-core/src/solver/heuristic.rs b/vrp-core/src/solver/heuristic.rs index b51fbade4..5dc1b7fc7 100644 --- a/vrp-core/src/solver/heuristic.rs +++ b/vrp-core/src/solver/heuristic.rs @@ -230,22 +230,6 @@ impl Input for InsertionContext { } } -impl DominanceOrdered for InsertionContext { - fn get_order(&self) -> &DominanceOrder { - let heuristic_keys = get_heuristic_keys(self); - self.solution - .state - .get(&heuristic_keys.solution_order) - .and_then(|s| s.downcast_ref::()) - .unwrap() - } - - fn set_order(&mut self, order: DominanceOrder) { - let heuristic_keys = get_heuristic_keys(self); - self.solution.state.insert(heuristic_keys.solution_order, Arc::new(order)); - } -} - /// Creates a heuristic operator probability which uses `is_hit` method from passed random object. pub fn create_scalar_operator_probability( scalar_probability: f64, diff --git a/vrp-core/src/solver/mod.rs b/vrp-core/src/solver/mod.rs index 8901c4006..52882aedf 100644 --- a/vrp-core/src/solver/mod.rs +++ b/vrp-core/src/solver/mod.rs @@ -24,20 +24,6 @@ //! objective which, in fact, leads to nontrivial multi-objective optimization problem, where no //! single solution exists that simultaneously optimizes each objective. //! -//! That's why the concept of dominance is introduced: a solution is said to dominate another -//! solution if its quality is at least as good on every objective and better on at least one. -//! The set of all non-dominated solutions of an optimization problem is called the Pareto set and -//! the projection of this set onto the objective function space is called the Pareto front. -//! -//! The aim of multi-objective metaheuristics is to approximate the Pareto front as closely as -//! possible (Zitzler et al., 2004) and therefore generate a set of mutually non-dominated solutions -//! called the Pareto set approximation. -//! -//! This library utilizes `NSGA-II` algorithm to apply Pareto-based ranking over population in order -//! to find Pareto set approximation. However, that Pareto optimality of the solutions cannot be -//! guaranteed: it is only known that none of the generated solutions dominates the others. -//! In the end, the top ranked individual is returned as best known solution. -//! //! # Evolutionary algorithm //! //! An evolutionary algorithm (EA) is a generic population-based metaheuristic optimization algorithm. @@ -159,7 +145,7 @@ impl HeuristicContext for RefinementContext { self.inner_context.selected() } - fn ranked<'a>(&'a self) -> Box + 'a> { + fn ranked<'a>(&'a self) -> Box + 'a> { self.inner_context.ranked() } diff --git a/vrp-core/src/solver/search/decompose_search.rs b/vrp-core/src/solver/search/decompose_search.rs index 74748d398..6d5653a5c 100644 --- a/vrp-core/src/solver/search/decompose_search.rs +++ b/vrp-core/src/solver/search/decompose_search.rs @@ -269,7 +269,7 @@ fn merge_best( accumulated: InsertionContext, ) -> InsertionContext { let (decomposed_ctx, route_indices) = decomposed; - let (decomposed_insertion_ctx, _) = decomposed_ctx.ranked().next().expect(GREEDY_ERROR); + let decomposed_insertion_ctx = decomposed_ctx.ranked().next().expect(GREEDY_ERROR); let environment = original_insertion_ctx.environment.clone(); let (partial_insertion_ctx, _) = create_partial_insertion_ctx(original_insertion_ctx, environment, route_indices); diff --git a/vrp-core/src/solver/search/infeasible_search.rs b/vrp-core/src/solver/search/infeasible_search.rs index eb9aa316d..e6bd5c4ab 100644 --- a/vrp-core/src/solver/search/infeasible_search.rs +++ b/vrp-core/src/solver/search/infeasible_search.rs @@ -81,7 +81,7 @@ impl HeuristicSearchOperator for InfeasibleSearch { } let insertion_ctx = - new_refinement_ctx.ranked().map(|(s, _)| s.deep_copy()).next().unwrap_or_else(|| solution.deep_copy()); + new_refinement_ctx.ranked().map(|s| s.deep_copy()).next().unwrap_or_else(|| solution.deep_copy()); insertion_ctx } diff --git a/vrp-core/tests/unit/models/goal_test.rs b/vrp-core/tests/unit/models/goal_test.rs index c8720ece1..0cfc68efd 100644 --- a/vrp-core/tests/unit/models/goal_test.rs +++ b/vrp-core/tests/unit/models/goal_test.rs @@ -28,15 +28,13 @@ fn create_objective_feature_with_fixed_cost(name: &str, cost: Cost) -> Feature { cost: Cost, } - impl Objective for TestFeatureObjective { + impl FeatureObjective for TestFeatureObjective { type Solution = InsertionContext; fn fitness(&self, _: &Self::Solution) -> f64 { self.cost } - } - impl FeatureObjective for TestFeatureObjective { fn estimate(&self, _: &MoveContext<'_>) -> Cost { self.cost } @@ -53,15 +51,13 @@ fn create_objective_feature_with_dynamic_cost(name: &str, fitness_fn: FitnessFn) fitness_fn: FitnessFn, } - impl Objective for TestFeatureObjective { + impl FeatureObjective for TestFeatureObjective { type Solution = InsertionContext; fn fitness(&self, solution: &Self::Solution) -> f64 { (self.fitness_fn)(self.name.as_str(), solution) } - } - impl FeatureObjective for TestFeatureObjective { fn estimate(&self, _: &MoveContext<'_>) -> Cost { unimplemented!() } diff --git a/vrp-core/tests/unit/models/problem/costs_test.rs b/vrp-core/tests/unit/models/problem/costs_test.rs index 518d7ddae..38069b121 100644 --- a/vrp-core/tests/unit/models/problem/costs_test.rs +++ b/vrp-core/tests/unit/models/problem/costs_test.rs @@ -119,14 +119,14 @@ mod objective { use crate::construction::heuristics::{InsertionContext, MoveContext, StateKeyRegistry}; use crate::helpers::construction::heuristics::{create_state_key, InsertionContextBuilder}; use crate::models::{Feature, FeatureBuilder, FeatureObjective, Goal, GoalContext}; - use rosomaxa::prelude::{compare_floats, MultiObjective, Objective}; + use rosomaxa::prelude::{compare_floats, HeuristicObjective}; use std::cmp::Ordering; struct TestObjective { index: usize, } - impl Objective for TestObjective { + impl FeatureObjective for TestObjective { type Solution = InsertionContext; fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering { @@ -136,10 +136,6 @@ mod objective { compare_floats(a, b) } - fn distance(&self, a: &Self::Solution, b: &Self::Solution) -> f64 { - (self.fitness(a) - self.fitness(b)).abs() - } - fn fitness(&self, solution: &Self::Solution) -> f64 { solution .solution @@ -150,9 +146,7 @@ mod objective { .cloned() .unwrap() } - } - impl FeatureObjective for TestObjective { fn estimate(&self, _: &MoveContext<'_>) -> Cost { Cost::default() } diff --git a/vrp-pragmatic/src/construction/features/breaks.rs b/vrp-pragmatic/src/construction/features/breaks.rs index 77ea25447..eafe187cb 100644 --- a/vrp-pragmatic/src/construction/features/breaks.rs +++ b/vrp-pragmatic/src/construction/features/breaks.rs @@ -11,7 +11,6 @@ use hashbrown::HashSet; use std::iter::once; use vrp_core::construction::enablers::*; use vrp_core::models::solution::Activity; -use vrp_core::rosomaxa::prelude::*; /// Specifies break policy. #[derive(Clone)] @@ -85,7 +84,7 @@ impl FeatureConstraint for OptionalBreakConstraint { struct OptionalBreakObjective {} -impl Objective for OptionalBreakObjective { +impl FeatureObjective for OptionalBreakObjective { type Solution = InsertionContext; fn fitness(&self, solution: &Self::Solution) -> f64 { @@ -97,9 +96,7 @@ impl Objective for OptionalBreakObjective { .filter(|job| job.as_single().filter(|single| is_break_single(single)).is_some()) .count() as f64 } -} -impl FeatureObjective for OptionalBreakObjective { fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { match move_ctx { MoveContext::Route { job, .. } => {