diff --git a/CHANGELOG.md b/CHANGELOG.md index 67f313c..d5b12dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 1.1.0 + +- Added `number_of_function_evaluations` field in algorithms and serialised data. This fields + calculate the number of times individual's objectives and constraints are evaluated during an + evolution. +- Renamed struct to specify stopping condition values. For example, + the `StoppingConditionType::MaxGeneration(MaxGeneration(250))` + can be defined as `StoppingConditionType::MaxGeneration(MaxGenerationValue(250))`. This is done to avoid confusion + between the enum `StoppingConditionType` value and the value of the stopping condition. +- Added the following new stopping conditions: `MaxFunctionEvaluations`, `Any` and `All`. The first one stops the + evolution + after a maximum number of functon evaluations. The second and third allows to combine multiple condition. For example, + they allow to stop the algorithm when either a specific duration or evolution number are reached (using `Any`). + ## 1.0.0 - Added new Python API to generate reference points with `DasDarren1998`. The new class diff --git a/examples/convergence.rs b/examples/convergence.rs index 4041165..cf71c6a 100644 --- a/examples/convergence.rs +++ b/examples/convergence.rs @@ -2,7 +2,7 @@ use std::env; use std::path::PathBuf; use optirustic::algorithms::{ - Algorithm, ExportHistory, MaxGeneration, NSGA2Arg, StoppingConditionType, NSGA2, + Algorithm, ExportHistory, MaxGenerationValue, NSGA2Arg, StoppingConditionType, NSGA2, }; use optirustic::core::builtin_problems::SCHProblem; use optirustic::core::OError; @@ -35,7 +35,7 @@ fn main() -> Result<(), OError> { let export_history = ExportHistory::new(100, &out_path)?; let args = NSGA2Arg { number_of_individuals: 10, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(1000)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(1000)), crossover_operator_options: None, mutation_operator_options: None, parallel: Some(false), diff --git a/examples/nsga2_sch.rs b/examples/nsga2_sch.rs index 81cb653..f66f80e 100644 --- a/examples/nsga2_sch.rs +++ b/examples/nsga2_sch.rs @@ -4,7 +4,9 @@ use std::path::PathBuf; use log::LevelFilter; -use optirustic::algorithms::{Algorithm, MaxGeneration, NSGA2Arg, StoppingConditionType, NSGA2}; +use optirustic::algorithms::{ + Algorithm, MaxGenerationValue, NSGA2Arg, StoppingConditionType, NSGA2, +}; use optirustic::core::builtin_problems::SCHProblem; /// Solve the Schaffer’s problem (SCH) where the following 2 objectives are minimised: @@ -28,7 +30,7 @@ fn main() -> Result<(), Box> { let args = NSGA2Arg { // use 100 individuals and stop the algorithm at 250 generations number_of_individuals: 100, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(250)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(250)), // use default options for the SBX and PM operators crossover_operator_options: None, mutation_operator_options: None, diff --git a/examples/nsga2_zdt1.rs b/examples/nsga2_zdt1.rs index 7de0d80..6464efe 100644 --- a/examples/nsga2_zdt1.rs +++ b/examples/nsga2_zdt1.rs @@ -4,7 +4,9 @@ use std::path::PathBuf; use log::LevelFilter; -use optirustic::algorithms::{Algorithm, MaxGeneration, NSGA2Arg, StoppingConditionType, NSGA2}; +use optirustic::algorithms::{ + Algorithm, MaxGenerationValue, NSGA2Arg, StoppingConditionType, NSGA2, +}; use optirustic::core::builtin_problems::ZTD1Problem; /// Solve the ZDT1 problem (SCH) where the following 2 objectives are minimised: @@ -29,7 +31,7 @@ fn main() -> Result<(), Box> { // Setup and run the NSGA2 algorithm let args = NSGA2Arg { number_of_individuals, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(1000)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(1000)), // use default options for the SBX and PM operators crossover_operator_options: None, mutation_operator_options: None, diff --git a/examples/nsga3_dtlz1.rs b/examples/nsga3_dtlz1.rs index ea351fc..9fb16fa 100644 --- a/examples/nsga3_dtlz1.rs +++ b/examples/nsga3_dtlz1.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use log::LevelFilter; use optirustic::algorithms::{ - Algorithm, MaxGeneration, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, NSGA3, + Algorithm, MaxGenerationValue, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, NSGA3, }; use optirustic::core::builtin_problems::DTLZ1Problem; use optirustic::operators::SimulatedBinaryCrossoverArgs; @@ -56,7 +56,7 @@ fn main() -> Result<(), Box> { crossover_operator_options: Some(crossover_operator_options), mutation_operator_options: None, // stop at generation 400 - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(400)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(400)), parallel: None, export_history: None, // to reproduce results diff --git a/examples/nsga3_dtlz1_8obj.rs b/examples/nsga3_dtlz1_8obj.rs index 46f8143..a826055 100644 --- a/examples/nsga3_dtlz1_8obj.rs +++ b/examples/nsga3_dtlz1_8obj.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use log::LevelFilter; use optirustic::algorithms::{ - Algorithm, MaxGeneration, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, NSGA3, + Algorithm, MaxGenerationValue, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, NSGA3, }; use optirustic::core::builtin_problems::DTLZ1Problem; use optirustic::operators::{PolynomialMutationArgs, SimulatedBinaryCrossoverArgs}; @@ -47,7 +47,7 @@ fn main() -> Result<(), Box> { number_of_partitions, crossover_operator_options: Some(crossover_operator_options), mutation_operator_options: Some(mutation_operator_options), - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(750)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(750)), parallel: None, export_history: None, seed: Some(1), diff --git a/examples/nsga3_dtlz2.rs b/examples/nsga3_dtlz2.rs index 7933dbe..d8e2610 100644 --- a/examples/nsga3_dtlz2.rs +++ b/examples/nsga3_dtlz2.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use log::LevelFilter; use optirustic::algorithms::{ - Algorithm, MaxGeneration, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, NSGA3, + Algorithm, MaxGenerationValue, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, NSGA3, }; use optirustic::core::builtin_problems::DTLZ2Problem; use optirustic::operators::SimulatedBinaryCrossoverArgs; @@ -54,7 +54,7 @@ fn main() -> Result<(), Box> { crossover_operator_options: Some(crossover_operator_options), mutation_operator_options: None, // stop at generation 400 - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(400)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(400)), parallel: None, export_history: None, // to reproduce results diff --git a/examples/nsga3_inverted_dtlz1.rs b/examples/nsga3_inverted_dtlz1.rs index e17a74e..f5b1ee3 100644 --- a/examples/nsga3_inverted_dtlz1.rs +++ b/examples/nsga3_inverted_dtlz1.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use log::LevelFilter; use optirustic::algorithms::{ - AdaptiveNSGA3, Algorithm, MaxGeneration, NSGA3Arg, Nsga3NumberOfIndividuals, + AdaptiveNSGA3, Algorithm, MaxGenerationValue, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, }; use optirustic::core::builtin_problems::DTLZ1Problem; @@ -50,7 +50,7 @@ fn main() -> Result<(), Box> { crossover_operator_options: Some(crossover_operator_options), mutation_operator_options: None, // stop at generation 400 - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(400)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(400)), parallel: None, export_history: None, // to reproduce results diff --git a/libs/optirustic-macros/src/lib.rs b/libs/optirustic-macros/src/lib.rs index b820266..8ddc464 100644 --- a/libs/optirustic-macros/src/lib.rs +++ b/libs/optirustic-macros/src/lib.rs @@ -92,7 +92,7 @@ pub fn as_algorithm_args(_attrs: TokenStream, input: TokenStream) -> TokenStream } /// This macro adds the following private fields to the struct defining an algorithm: -/// `problem`, `number_of_individuals`, `population`, `generation`,`stopping_condition`, +/// `problem`, `number_of_individuals`, `population`, `generation`,`stopping_condition`, `number_of_function_evaluations`, /// `start_time`, `export_history` and `parallel`. /// /// It also implements the `Display` trait. @@ -141,6 +141,14 @@ pub fn as_algorithm(attrs: TokenStream, input: TokenStream) -> TokenStream { }) .expect("Cannot add `generation` field"), ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { + /// The number of function evaluations. + number_of_function_evaluations: usize + }) + .expect("Cannot add `number_of_function_evaluations` field"), + ); fields.named.push( syn::Field::parse_named .parse2(quote! { @@ -205,7 +213,7 @@ pub fn as_algorithm(attrs: TokenStream, input: TokenStream) -> TokenStream { /// This macro adds common items when the `Algorithm` trait is implemented for a new algorithm /// struct. This adds the following items: `Algorithm::name()`, `Algorithm::stopping_condition()` /// `Algorithm::start_time()`, `Algorithm::problem()`, `Algorithm::population()`, -/// `Algorithm::generation()` and `Algorithm::export_history()`. +/// `Algorithm::generation()`, `Algorithm::number_of_function_evaluations()` and `Algorithm::export_history()`. /// #[proc_macro_attribute] pub fn impl_algorithm_trait_items(attrs: TokenStream, input: TokenStream) -> TokenStream { @@ -282,7 +290,16 @@ pub fn impl_algorithm_trait_items(attrs: TokenStream, input: TokenStream) -> Tok ) .into(), ) - .expect("Failed to parse `export_history` item"), + .expect("Failed to parse `generation` item"), + syn::parse::( + quote!( + fn number_of_function_evaluations(&self) -> usize { + self.number_of_function_evaluations + } + ) + .into(), + ) + .expect("Failed to parse `number_of_function_evaluations` item"), syn::parse::( quote!( fn algorithm_options(&self) -> #arg_type { diff --git a/optirustic-py/README.md b/optirustic-py/README.md index 01049f6..440a7df 100644 --- a/optirustic-py/README.md +++ b/optirustic-py/README.md @@ -12,7 +12,7 @@ exported with the `optirustic` crate. It lets you: The package can be installed from [PyPi](https://pypi.org/project/optirustic/): ``` -pip install optirustic_py +pip install optirustic ``` # Usage diff --git a/src/algorithms/a_nsga3.rs b/src/algorithms/a_nsga3.rs index 742481a..b5252af 100644 --- a/src/algorithms/a_nsga3.rs +++ b/src/algorithms/a_nsga3.rs @@ -32,7 +32,7 @@ mod test_problems { use optirustic_macros::test_with_retries; use crate::algorithms::{ - AdaptiveNSGA3, Algorithm, MaxGeneration, NSGA3Arg, Nsga3NumberOfIndividuals, + AdaptiveNSGA3, Algorithm, MaxGenerationValue, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, }; use crate::core::builtin_problems::DTLZ1Problem; @@ -61,7 +61,7 @@ mod test_problems { number_of_partitions, crossover_operator_options: Some(crossover_operator_options), mutation_operator_options: Some(mutation_operator_options), - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(400)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(400)), parallel: None, export_history: None, seed: Some(1), diff --git a/src/algorithms/algorithm.rs b/src/algorithms/algorithm.rs index 75ade2e..a340241 100644 --- a/src/algorithms/algorithm.rs +++ b/src/algorithms/algorithm.rs @@ -40,6 +40,9 @@ pub struct AlgorithmSerialisedExport { pub individuals: Vec, /// The generation the export was collected at. pub generation: usize, + /// The number of function evaluations + #[serde(default)] + pub number_of_function_evaluations: usize, /// The algorithm name. pub algorithm: String, /// Any additional data exported by the algorithm. @@ -99,6 +102,7 @@ impl TryInto for AlgorithmSerialisedExport { individuals: self.individuals()?, generation: self.generation, algorithm: self.algorithm, + number_of_function_evaluations: self.number_of_function_evaluations, took: self.took, additional_data: self.additional_data.unwrap_or_default(), }; @@ -115,6 +119,8 @@ pub struct AlgorithmExport { pub individuals: Vec, /// The generation number. pub generation: usize, + /// The number of function evaluations + pub number_of_function_evaluations: usize, /// The algorithm name used to evolve the individuals. pub algorithm: String, /// The time the algorithm took to reach the current generation. @@ -222,6 +228,14 @@ pub trait Algorithm: Display { /// return: `usize`. fn generation(&self) -> usize; + /// Return the number of function evaluations. This is the number of times the algorithm evaluates + /// an individual's objectives and constraints using [`Algorithm::evaluate_individual`]. If no + /// new solutions/individuals are chosen by an algorithm, this counter will not increase, as past + /// solutions are already evaluated. + /// + /// return: `usize`. + fn number_of_function_evaluations(&self) -> usize; + /// Return the algorithm name. /// /// return: `String`. @@ -282,39 +296,50 @@ pub trait Algorithm: Display { } /// Evaluate the objectives and constraints for unevaluated individuals in the population. This - /// updates the individual data only and runs the evaluation function in threads. This returns - /// an error if the evaluation function fails or the evaluation function does not provide a - /// value for a problem constraints or objectives for one individual. + /// updates the individual data only, runs the evaluation function in threads and increase the + /// `nfe` counter by the number of evaluated individuals. + /// This returns an error if the evaluation function fails or the evaluation function does not + /// provide a value for a problem constraints or objectives for one individual. /// /// # Arguments /// /// * `individuals`: The individuals to evaluate. + /// * `nfe`: The reference to the number of function evaluation counter. /// /// return `Result<(), OError>` - fn do_parallel_evaluation(individuals: &mut [Individual]) -> Result<(), OError> { + fn do_parallel_evaluation( + individuals: &mut [Individual], + nfe: &mut usize, + ) -> Result<(), OError> { + let delta_nfe = Self::count_unevaluated(individuals); individuals .into_par_iter() .enumerate() .try_for_each(|(idx, i)| Self::evaluate_individual(idx, i))?; + *nfe += delta_nfe; Ok(()) } /// Evaluate the objectives and constraints for unevaluated individuals in the population. This - /// updates the individual data only and runs the evaluation function in a plain loop. This - /// returns an error if the evaluation function fails or the evaluation function does not + /// updates the individual data only, runs the evaluation function in a plain loop and increase + /// the `nfe` counter by the number of evaluated individuals. + /// This returns an error if the evaluation function fails or the evaluation function does not /// provide a value for a problem constraints or objectives for one individual. /// Evaluation may be performed in threads using [`Self::do_parallel_evaluation`]. /// /// # Arguments /// /// * `individuals`: The individuals to evaluate. + /// * `nfe`: The reference to the number of function evaluation counter. /// - /// return `Result<(), OError>` - fn do_evaluation(individuals: &mut [Individual]) -> Result<(), OError> { + /// return `Result`. + fn do_evaluation(individuals: &mut [Individual], nfe: &mut usize) -> Result<(), OError> { + let delta_nfe = Self::count_unevaluated(individuals); individuals .iter_mut() .enumerate() .try_for_each(|(idx, i)| Self::evaluate_individual(idx, i))?; + *nfe += delta_nfe; Ok(()) } @@ -369,6 +394,25 @@ pub trait Algorithm: Display { Ok(()) } + /// Count the number on unevaluated individuals. + /// + /// # Arguments + /// + /// * `individuals`: The individuals to check. + /// + /// returns: `usize` + fn count_unevaluated(individuals: &[Individual]) -> usize { + individuals + .iter() + .filter_map(|i| { + if !i.is_evaluated() { + Some(1_usize) + } else { + None + } + }) + .sum() + } /// Run the algorithm. /// /// return: `Result<(), OError>` @@ -406,10 +450,7 @@ pub trait Algorithm: Display { // Termination let cond = self.stopping_condition(); - let terminate = match &cond { - StoppingConditionType::MaxDuration(t) => t.is_met(Instant::now().elapsed()), - StoppingConditionType::MaxGeneration(t) => t.is_met(self.generation()), - }; + let terminate = self.is_stopping_condition_met(cond)?; if terminate { // save last file if let Some(export) = self.export_history() { @@ -425,6 +466,47 @@ pub trait Algorithm: Display { Ok(()) } + /// Check if the given stopping condition is met. + /// + /// # Arguments + /// + /// * `condition`: The stopping condition type. + /// + /// returns: `Result` + fn is_stopping_condition_met(&self, condition: &StoppingConditionType) -> Result { + let is_met = match condition { + StoppingConditionType::MaxDuration(cond) => cond.is_met(Instant::now().elapsed()), + StoppingConditionType::MaxGeneration(cond) => cond.is_met(self.generation()), + StoppingConditionType::MaxFunctionEvaluations(cond) => { + cond.is_met(self.number_of_function_evaluations()) + } + StoppingConditionType::Any(conditions) => { + if StoppingConditionType::has_nested_vector(conditions) { + return Err(OError::AlgorithmRun( + self.name(), + "A vector of stopping condition vector is not allowed".to_string(), + )); + } + conditions + .iter() + .any(|c| self.is_stopping_condition_met(c).unwrap()) + } + StoppingConditionType::All(conditions) => { + if StoppingConditionType::has_nested_vector(conditions) { + return Err(OError::AlgorithmRun( + self.name(), + "A vector of stopping condition vector is not allowed".to_string(), + )); + } + + conditions + .iter() + .all(|c| self.is_stopping_condition_met(c).unwrap()) + } + }; + Ok(is_met) + } + /// Get the results of the run. /// /// return: `AlgorithmExport`. @@ -434,6 +516,7 @@ pub trait Algorithm: Display { problem: self.problem(), individuals: self.population().individuals().to_vec(), generation: self.generation(), + number_of_function_evaluations: self.number_of_function_evaluations(), algorithm: self.name(), took: Elapsed { hours, @@ -464,6 +547,7 @@ pub trait Algorithm: Display { problem: self.problem().serialise(), individuals: self.population().serialise(), generation: self.generation(), + number_of_function_evaluations: self.number_of_function_evaluations(), algorithm: self.name(), additional_data: self.additional_export_data(), took: Elapsed { @@ -627,7 +711,10 @@ mod test { use std::path::Path; use std::sync::Arc; - use crate::algorithms::{Algorithm, NSGA2}; + use crate::algorithms::stopping_condition::MaxFunctionEvaluationValue; + use crate::algorithms::{ + Algorithm, MaxGenerationValue, NSGA2Arg, StoppingConditionType, NSGA2, + }; use crate::core::builtin_problems::{SCHProblem, ZTD1Problem}; #[test] @@ -677,4 +764,74 @@ mod test { .to_string() .contains("number of variables from the history file")); } + + #[test] + /// Test StoppingConditionType::MaxGeneration + fn test_stopping_condition_max_generation() { + let problem = SCHProblem::create().unwrap(); + let args = NSGA2Arg { + number_of_individuals: 10, + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(20)), + crossover_operator_options: None, + mutation_operator_options: None, + parallel: Some(false), + export_history: None, + resume_from_file: None, + seed: Some(10), + }; + let mut algo = NSGA2::new(problem, args).unwrap(); + algo.run().unwrap(); + let results = algo.get_results(); + + assert_eq!(results.generation, 20); + } + + #[test] + /// Test StoppingConditionType::MaxFunctionEvaluations + fn test_stopping_condition_max_nfe() { + let problem = SCHProblem::create().unwrap(); + let args = NSGA2Arg { + number_of_individuals: 10, + stopping_condition: StoppingConditionType::MaxFunctionEvaluations( + MaxFunctionEvaluationValue(20), + ), + crossover_operator_options: None, + mutation_operator_options: None, + parallel: Some(false), + export_history: None, + resume_from_file: None, + seed: Some(10), + }; + let mut algo = NSGA2::new(problem, args).unwrap(); + algo.run().unwrap(); + let results = algo.get_results(); + + assert_eq!(results.number_of_function_evaluations, 20); + assert_eq!(results.generation, 2); + } + + #[test] + /// Test StoppingConditionType::Any + fn test_stopping_condition_any() { + let problem = SCHProblem::create().unwrap(); + let args = NSGA2Arg { + number_of_individuals: 10, + stopping_condition: StoppingConditionType::Any(vec![ + StoppingConditionType::MaxFunctionEvaluations(MaxFunctionEvaluationValue(20)), + StoppingConditionType::MaxGeneration(MaxGenerationValue(10)), + ]), + crossover_operator_options: None, + mutation_operator_options: None, + parallel: Some(false), + export_history: None, + resume_from_file: None, + seed: Some(10), + }; + let mut algo = NSGA2::new(problem, args).unwrap(); + algo.run().unwrap(); + let results = algo.get_results(); + + assert_eq!(results.number_of_function_evaluations, 20); + assert_eq!(results.generation, 2); + } } diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index 703780c..8a5825f 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -3,7 +3,7 @@ pub use algorithm::{Algorithm, AlgorithmExport, AlgorithmSerialisedExport, Expor pub use nsga2::{NSGA2Arg, NSGA2}; pub use nsga3::{NSGA3Arg, Nsga3NumberOfIndividuals, NSGA3}; pub use stopping_condition::{ - MaxDuration, MaxGeneration, StoppingCondition, StoppingConditionType, + MaxDurationValue, MaxGenerationValue, StoppingCondition, StoppingConditionType, }; mod a_nsga3; diff --git a/src/algorithms/nsga2.rs b/src/algorithms/nsga2.rs index a9540d3..3517992 100644 --- a/src/algorithms/nsga2.rs +++ b/src/algorithms/nsga2.rs @@ -142,6 +142,7 @@ impl NSGA2 { crossover_operator, mutation_operator, generation: 0, + number_of_function_evaluations: 0, stopping_condition: options.stopping_condition, start_time: Instant::now(), parallel: options.parallel.unwrap_or(true), @@ -280,9 +281,15 @@ impl Algorithm for NSGA2 { fn initialise(&mut self) -> Result<(), OError> { info!("Evaluating initial population"); if self.parallel { - NSGA2::do_parallel_evaluation(self.population.individuals_as_mut())?; + NSGA2::do_parallel_evaluation( + self.population.individuals_as_mut(), + &mut self.number_of_function_evaluations, + )?; } else { - NSGA2::do_evaluation(self.population.individuals_as_mut())?; + NSGA2::do_evaluation( + self.population.individuals_as_mut(), + &mut self.number_of_function_evaluations, + )?; } debug!("Calculating rank"); @@ -330,9 +337,15 @@ impl Algorithm for NSGA2 { debug!("Evaluating population"); if self.parallel { - NSGA2::do_parallel_evaluation(self.population.individuals_as_mut())?; + NSGA2::do_parallel_evaluation( + self.population.individuals_as_mut(), + &mut self.number_of_function_evaluations, + )?; } else { - NSGA2::do_evaluation(self.population.individuals_as_mut())?; + NSGA2::do_evaluation( + self.population.individuals_as_mut(), + &mut self.number_of_function_evaluations, + )?; } debug!("Evaluation done"); @@ -645,7 +658,9 @@ mod test_sorting { mod test_problems { use optirustic_macros::test_with_retries; - use crate::algorithms::{Algorithm, MaxGeneration, NSGA2Arg, StoppingConditionType, NSGA2}; + use crate::algorithms::{ + Algorithm, MaxGenerationValue, NSGA2Arg, StoppingConditionType, NSGA2, + }; use crate::core::builtin_problems::{ SCHProblem, ZTD1Problem, ZTD2Problem, ZTD3Problem, ZTD4Problem, }; @@ -660,7 +675,7 @@ mod test_problems { let problem = SCHProblem::create().unwrap(); let args = NSGA2Arg { number_of_individuals: 10, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(1000)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(1000)), crossover_operator_options: None, mutation_operator_options: None, parallel: Some(false), @@ -688,7 +703,7 @@ mod test_problems { let problem = ZTD1Problem::create(number_of_individuals).unwrap(); let args = NSGA2Arg { number_of_individuals, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(2500)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(2500)), crossover_operator_options: None, mutation_operator_options: None, parallel: Some(false), @@ -735,7 +750,7 @@ mod test_problems { let problem = ZTD2Problem::create(number_of_individuals).unwrap(); let args = NSGA2Arg { number_of_individuals, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(2500)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(2500)), crossover_operator_options: None, mutation_operator_options: None, parallel: Some(false), @@ -787,7 +802,7 @@ mod test_problems { let problem = ZTD3Problem::create(number_of_individuals).unwrap(); let args = NSGA2Arg { number_of_individuals, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(2500)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(2500)), crossover_operator_options: None, mutation_operator_options: None, parallel: Some(false), @@ -838,7 +853,7 @@ mod test_problems { let number_of_individuals: usize = 10; let args = NSGA2Arg { number_of_individuals, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(3000)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(3000)), crossover_operator_options: None, mutation_operator_options: None, parallel: Some(false), @@ -892,7 +907,7 @@ mod test_problems { let problem = ZTD4Problem::create(number_of_individuals).unwrap(); let args = NSGA2Arg { number_of_individuals, - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(1000)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(1000)), crossover_operator_options: None, mutation_operator_options: None, parallel: Some(false), diff --git a/src/algorithms/nsga3/mod.rs b/src/algorithms/nsga3/mod.rs index 37fa621..9999074 100644 --- a/src/algorithms/nsga3/mod.rs +++ b/src/algorithms/nsga3/mod.rs @@ -226,6 +226,7 @@ impl NSGA3 { crossover_operator, mutation_operator, generation: 0, + number_of_function_evaluations: 0, stopping_condition: options.stopping_condition, start_time: Instant::now(), parallel: options.parallel.unwrap_or(true), @@ -298,9 +299,15 @@ impl Algorithm for NSGA3 { fn initialise(&mut self) -> Result<(), OError> { info!("Evaluating initial population"); if self.parallel { - NSGA3::do_parallel_evaluation(self.population.individuals_as_mut())?; + NSGA3::do_parallel_evaluation( + self.population.individuals_as_mut(), + &mut self.number_of_function_evaluations, + )?; } else { - NSGA3::do_evaluation(self.population.individuals_as_mut())?; + NSGA3::do_evaluation( + self.population.individuals_as_mut(), + &mut self.number_of_function_evaluations, + )?; } info!("Initial evaluation completed"); @@ -344,9 +351,15 @@ impl Algorithm for NSGA3 { debug!("Evaluating population"); if self.parallel { - NSGA3::do_parallel_evaluation(self.population.individuals_as_mut())?; + NSGA3::do_parallel_evaluation( + self.population.individuals_as_mut(), + &mut self.number_of_function_evaluations, + )?; } else { - NSGA3::do_evaluation(self.population.individuals_as_mut())?; + NSGA3::do_evaluation( + self.population.individuals_as_mut(), + &mut self.number_of_function_evaluations, + )?; } debug!("Evaluation done"); @@ -474,7 +487,8 @@ mod test_problems { use optirustic_macros::test_with_retries; use crate::algorithms::{ - Algorithm, MaxGeneration, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, NSGA3, + Algorithm, MaxGenerationValue, NSGA3Arg, Nsga3NumberOfIndividuals, StoppingConditionType, + NSGA3, }; use crate::core::builtin_problems::{DTLZ1Problem, DTLZ2Problem}; use crate::core::test_utils::{assert_approx_array_eq, check_value_in_range}; @@ -538,7 +552,7 @@ mod test_problems { crossover_operator_options: Some(crossover_operator_options), mutation_operator_options: Some(mutation_operator_options), // see Table III - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(max_gen)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(max_gen)), parallel: None, export_history: None, seed: Some(1), @@ -637,7 +651,7 @@ mod test_problems { crossover_operator_options: Some(crossover_operator_options), mutation_operator_options: Some(mutation_operator_options), // see Table III - stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(400)), + stopping_condition: StoppingConditionType::MaxGeneration(MaxGenerationValue(400)), parallel: None, export_history: None, seed: Some(1), diff --git a/src/algorithms/stopping_condition.rs b/src/algorithms/stopping_condition.rs index 0b32c19..6b186ea 100644 --- a/src/algorithms/stopping_condition.rs +++ b/src/algorithms/stopping_condition.rs @@ -18,9 +18,9 @@ pub trait StoppingCondition { /// Number of generations after which a genetic algorithm terminates. #[derive(Serialize, Deserialize, Clone)] -pub struct MaxGeneration(pub usize); +pub struct MaxGenerationValue(pub usize); -impl StoppingCondition for MaxGeneration { +impl StoppingCondition for MaxGenerationValue { fn target(&self) -> usize { self.0 } @@ -30,11 +30,25 @@ impl StoppingCondition for MaxGeneration { } } +/// Number of function evaluations after which a genetic algorithm terminates. +#[derive(Serialize, Deserialize, Clone)] +pub struct MaxFunctionEvaluationValue(pub usize); + +impl StoppingCondition for MaxFunctionEvaluationValue { + fn target(&self) -> usize { + self.0 + } + + fn name() -> String { + "maximum number of function evaluations".to_string() + } +} + /// Elapsed time after which a genetic algorithm terminates. #[derive(Serialize, Deserialize, Clone)] -pub struct MaxDuration(pub Duration); +pub struct MaxDurationValue(pub Duration); -impl StoppingCondition for MaxDuration { +impl StoppingCondition for MaxDurationValue { fn target(&self) -> Duration { self.0 } @@ -49,17 +63,50 @@ impl StoppingCondition for MaxDuration { #[derive(Serialize, Deserialize, Clone)] pub enum StoppingConditionType { /// Set a maximum duration - MaxDuration(MaxDuration), + MaxDuration(MaxDurationValue), /// Set a maximum number of generations - MaxGeneration(MaxGeneration), + MaxGeneration(MaxGenerationValue), + /// Set a maximum number of function evaluations + MaxFunctionEvaluations(MaxFunctionEvaluationValue), + /// Stop when at least on condition is met (this acts as an OR operator) + Any(Vec), + /// Stop when all conditions are met (this acts as an AND operator) + All(Vec), } impl StoppingConditionType { /// A name describing the stopping condition. + /// + /// returns: `String` pub fn name(&self) -> String { match self { - StoppingConditionType::MaxDuration(_) => MaxDuration::name(), - StoppingConditionType::MaxGeneration(_) => MaxGeneration::name(), + StoppingConditionType::MaxDuration(_) => MaxDurationValue::name(), + StoppingConditionType::MaxGeneration(_) => MaxGenerationValue::name(), + StoppingConditionType::MaxFunctionEvaluations(_) => MaxFunctionEvaluationValue::name(), + StoppingConditionType::Any(s) => s + .iter() + .map(|cond| cond.name()) + .collect::>() + .join(" OR "), + StoppingConditionType::All(s) => s + .iter() + .map(|cond| cond.name()) + .collect::>() + .join(" AND "), } } + + /// Check whether the stopping condition is a vector and has nested vector in it. + /// + /// # Arguments + /// + /// * `conditions`: A vector of stopping conditions. + /// + /// returns: `bool` + pub fn has_nested_vector(conditions: &[StoppingConditionType]) -> bool { + conditions.iter().any(|c| match c { + StoppingConditionType::Any(_) | StoppingConditionType::All(_) => true, + _ => false, + }) + } }