use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::fs::read_dir;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Instant, SystemTime};
use std::{fmt, fs};
use chrono::{DateTime, Utc};
use log::{debug, info};
use rayon::{prelude::*, ThreadPool};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[cfg(feature = "python")]
use crate::algorithms::{NSGA2Arg, NSGA3Arg};
use crate::utils::{elapsed, elapsed_as_string};
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;
use crate::algorithms::StoppingCondition;
use crate::core::{
DataValue, Individual, IndividualExport, OError, ObjectiveDirection, Population, Problem,
ProblemExport,
};
#[derive(Serialize, Deserialize, Clone)]
pub enum NumThreads {
Max,
Use(usize),
Off,
}
impl Default for NumThreads {
fn default() -> Self {
NumThreads::Max
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Elapsed {
pub hours: u64,
pub minutes: u64,
pub seconds: u64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AlgorithmSerialisedExport<T: Serialize> {
pub options: T,
pub problem: ProblemExport,
pub individuals: Vec<IndividualExport>,
pub generation: u32,
#[serde(default)]
pub number_of_function_evaluations: u32,
pub algorithm: String,
pub additional_data: Option<HashMap<String, DataValue>>,
pub took: Elapsed,
pub exported_on: DateTime<Utc>,
}
impl<T: Serialize> AlgorithmSerialisedExport<T> {
pub fn problem(&self) -> Result<Problem, OError> {
self.problem.clone().try_into()
}
pub fn individuals(&self) -> Result<Vec<Individual>, OError> {
let problem = Arc::new(self.problem()?);
let mut individuals: Vec<Individual> = vec![];
for individual_data in &self.individuals {
let mut ind = Individual::new(problem.clone());
for (name, value) in &individual_data.objective_values {
ind.update_objective(name, *value)?;
}
for (name, value) in &individual_data.constraint_values {
ind.update_constraint(name, *value)?;
}
for (name, value) in &individual_data.variable_values {
ind.update_variable(name, value.clone())?;
}
for (name, value) in &individual_data.data {
ind.set_data(name, value.clone());
}
ind.set_evaluated();
individuals.push(ind);
}
Ok(individuals)
}
#[cfg(feature = "plotting")]
pub fn plot_front(
&self,
destination: &PathBuf,
fig_size: Option<[u32; 2]>,
) -> Result<(), OError> {
use gnuplot::AxesCommon;
use gnuplot::{
Figure,
PlotOption::{Color, PointSymbol},
};
let fig_size = fig_size.unwrap_or([800, 500]);
let title = format!(
"Results for {} @ generation={} \nPopulation size={}",
self.algorithm,
self.generation,
self.individuals.len()
);
let obj_count = self.problem.number_of_objectives;
let obj_names = self.problem.objective_names.clone();
let mut fg = Figure::new();
let point_style = [PointSymbol('O'), Color("black".into())];
if obj_count >= 2 && obj_count <= 3 {
let obj1_name = obj_names.get(0).unwrap();
let obj2_name = obj_names.get(1).unwrap();
let obj1: Vec<f64> = self
.individuals()?
.iter()
.map(|i| i.get_objective_value(obj1_name).unwrap())
.collect();
let obj2: Vec<f64> = self
.individuals()?
.iter()
.map(|i| i.get_objective_value(obj2_name).unwrap())
.collect();
if obj_count == 2 {
fg.axes2d()
.points(obj1, obj2, &point_style)
.set_x_label(obj1_name, &[])
.set_y_label(obj2_name, &[])
.set_x_grid(true)
.set_y_grid(true)
.set_title(&title, &[]);
} else {
let obj3_name = obj_names.get(2).unwrap();
let obj3: Vec<f64> = self
.individuals()?
.iter()
.map(|i| i.get_objective_value(obj3_name).unwrap())
.collect();
fg.axes3d()
.points(obj1, obj2, obj3, &point_style)
.set_x_label(obj1_name, &[])
.set_y_label(obj2_name, &[])
.set_z_label(obj3_name, &[])
.set_x_grid(true)
.set_y_grid(true)
.set_z_grid(true)
.set_view(40.0, 110.0)
.set_title(&title, &[]);
}
return fg
.save_to_png(destination, fig_size[0], fig_size[1])
.map_err(|e| {
OError::Generic(format!("Cannot save the chart because {}", e.to_string()))
});
} else {
return Err(OError::Generic(
"Plotting of Pareto front is only supported for a 2 or 3-objective problem"
.to_string(),
));
}
}
}
impl<T: Serialize> TryInto<AlgorithmExport> for AlgorithmSerialisedExport<T> {
type Error = OError;
fn try_into(self) -> Result<AlgorithmExport, Self::Error> {
let data = AlgorithmExport {
problem: Arc::new(self.problem()?),
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(),
};
Ok(data)
}
}
pub enum ExportVecGroupBy {
Individual,
Objective,
}
pub struct ReferencePointType {
pub original: Vec<Vec<f64>>,
pub new: Vec<Vec<f64>>,
}
#[derive(Debug)]
pub struct AlgorithmExport {
pub problem: Arc<Problem>,
pub individuals: Vec<Individual>,
pub generation: u32,
pub number_of_function_evaluations: u32,
pub algorithm: String,
pub took: Elapsed,
pub additional_data: HashMap<String, DataValue>,
}
impl AlgorithmExport {
pub fn get_real_variables(&self, name: &str) -> Result<Vec<f64>, OError> {
self.individuals
.iter()
.map(|i| i.get_real_value(name))
.collect()
}
pub fn get_objective(&self, name: &str) -> Result<Vec<f64>, OError> {
self.individuals
.iter()
.map(|i| i.get_objective_value(name))
.collect()
}
pub fn get_objectives(&self) -> Result<HashMap<String, Vec<f64>>, OError> {
let mut map = HashMap::new();
for name in self.problem.objective_names() {
let data_vec = self
.individuals
.iter()
.map(|i| i.get_objective_value(&name))
.collect::<Result<Vec<f64>, OError>>()?;
map.insert(name, data_vec);
}
Ok(map)
}
fn group_by_objective(&self, vector: Vec<Vec<f64>>) -> Result<Vec<Vec<f64>>, OError> {
let mut vector_by_objectives = vec![vec![]; self.problem.number_of_objectives()];
for ind_objectives in vector.into_iter() {
for obj_num in 0..self.problem.number_of_objectives() {
let value = ind_objectives.get(obj_num);
match value {
Some(v) => vector_by_objectives[obj_num].push(v.clone()),
None => {
return Err(OError::Generic(format!("Cannot group objective {obj_num}")));
}
}
}
}
Ok(vector_by_objectives)
}
pub fn get_nsga3_normalised_objectives(
&self,
group_by: ExportVecGroupBy,
) -> Result<Vec<Vec<f64>>, OError> {
if self.algorithm != "NSGA3" {
return Err(OError::Generic(
"The exported data were not derived using NSGA3".to_string(),
));
}
let mut all_normalised_objectives_per_ind = Vec::new();
for ind in self.individuals.iter() {
all_normalised_objectives_per_ind
.push(ind.get_data("normalised_objectives")?.as_f64_vec()?.clone());
}
let results = match group_by {
ExportVecGroupBy::Individual => all_normalised_objectives_per_ind,
ExportVecGroupBy::Objective => {
let mut all_normalised_objectives =
vec![vec![]; self.problem.number_of_objectives()];
for ind_objectives in all_normalised_objectives_per_ind.into_iter() {
for obj_num in 0..self.problem.number_of_objectives() {
let value = ind_objectives.get(obj_num);
match value {
Some(v) => all_normalised_objectives[obj_num].push(v.clone()),
None => {
return Err(OError::Generic(format!(
"Cannot group objective {obj_num}"
)));
}
}
}
}
all_normalised_objectives
}
};
Ok(results)
}
pub fn get_nsga3_reference_points(
&self,
group_by: ExportVecGroupBy,
) -> Result<Vec<Vec<f64>>, OError> {
if self.algorithm != "NSGA3" {
return Err(OError::Generic(
"The exported data were not derived using NSGA3".to_string(),
));
}
let ref_points_per_ind = match self.additional_data.get("reference_points") {
Some(values) => {
let mut ref_points = vec![];
for point_vec in values.as_data_vec()?.into_iter() {
ref_points.push(point_vec.as_f64_vec()?.clone());
}
ref_points
}
None => {
return Err(OError::Generic(format!("Cannot group reference_points")));
}
};
let results: Vec<Vec<f64>> = match group_by {
ExportVecGroupBy::Individual => ref_points_per_ind,
ExportVecGroupBy::Objective => self.group_by_objective(ref_points_per_ind)?,
};
Ok(results)
}
pub fn get_adaptive_nsga3_reference_points(
&self,
group_by: ExportVecGroupBy,
) -> Result<ReferencePointType, OError> {
let mut ref_points = self.get_nsga3_reference_points(ExportVecGroupBy::Individual)?;
let original_point_count = match self.additional_data.get("number_of_reference_points") {
Some(count) => count.as_real()?, None => {
return Err(OError::Generic(
"Cannot find additional_data.number_of_reference_points".to_string(),
))
}
};
let adaptive_points = ref_points.split_off(original_point_count as usize);
let result = match group_by {
ExportVecGroupBy::Individual => ReferencePointType {
original: ref_points,
new: adaptive_points,
},
ExportVecGroupBy::Objective => ReferencePointType {
original: self.group_by_objective(ref_points)?,
new: self.group_by_objective(adaptive_points)?,
},
};
Ok(result)
}
}
impl Display for AlgorithmExport {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{} at {} generations, took {} hours, {} minutes and {} seconds",
self.algorithm, self.generation, self.took.hours, self.took.minutes, self.took.seconds
)
}
}
#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExportHistory {
generation_step: usize,
destination: PathBuf,
}
impl ExportHistory {
pub fn new(generation_step: usize, destination: &PathBuf) -> Result<Self, OError> {
if !destination.exists() {
return Err(OError::Generic(format!(
"The destination folder '{:?}' does not exist",
destination
)));
}
Ok(Self {
generation_step,
destination: destination.to_owned(),
})
}
pub fn destination(&self) -> &PathBuf {
&self.destination
}
pub fn generation_step(&self) -> usize {
self.generation_step
}
}
#[cfg(feature = "python")]
#[pymethods]
impl ExportHistory {
#[new]
fn py_new(generation_step: usize, destination: PathBuf) -> PyResult<Self> {
ExportHistory::new(generation_step, &destination)
.map_err(|e| PyValueError::new_err(e.to_string()))
}
}
pub trait Algorithm<AlgorithmOptions: Serialize + DeserializeOwned>: Display {
fn initialise(&mut self) -> Result<(), OError>;
fn evolve(&mut self) -> Result<(), OError>;
fn generation(&self) -> u32;
fn number_of_function_evaluations(&self) -> u32;
fn name(&self) -> String;
fn start_time(&self) -> &Instant;
fn stopping_condition(&self) -> &StoppingCondition;
fn population(&self) -> &Population;
fn problem(&self) -> Arc<Problem>;
fn export_history(&self) -> Option<&ExportHistory>;
fn additional_export_data(&self) -> Option<HashMap<String, DataValue>> {
None
}
fn elapsed(&self) -> [u64; 3] {
elapsed(self.start_time().elapsed().as_secs())
}
fn elapsed_as_string(&self) -> String {
elapsed_as_string(self.start_time().elapsed().as_secs())
}
fn build_thread_pool(num_threads: NumThreads) -> Result<Option<ThreadPool>, OError> {
let pool = match num_threads {
NumThreads::Max | NumThreads::Use(_) => {
let mut builder = rayon::ThreadPoolBuilder::new();
if let NumThreads::Use(n) = num_threads {
builder = builder.num_threads(n);
}
let pool = builder.build().map_err(|e| {
OError::Generic(format!("cannot initialise the thread pool because {e}"))
})?;
Some(pool)
}
NumThreads::Off => None,
};
Ok(pool)
}
fn do_evaluation(
individuals: &mut [Individual],
nfe: &mut u32,
thread_pool: &Option<ThreadPool>,
) -> Result<(), OError> {
let delta_nfe = Self::count_unevaluated(individuals);
match thread_pool {
Some(pool) => {
pool.install(|| {
individuals
.into_par_iter()
.enumerate()
.try_for_each(|(idx, i)| Self::evaluate_individual(idx, i))
})?;
}
None => individuals
.iter_mut()
.enumerate()
.try_for_each(|(idx, i)| Self::evaluate_individual(idx, i))?,
}
*nfe += delta_nfe;
Ok(())
}
fn evaluate_individual(idx: usize, i: &mut Individual) -> Result<(), OError> {
debug!("Evaluating individual #{} - {:?}", idx + 1, i.variables());
if i.is_evaluated() {
debug!("Skipping evaluation for individual #{idx}. Already evaluated.");
return Ok(());
}
let problem = i.problem();
let results = problem
.evaluator()
.evaluate(i)
.map_err(|e| OError::Evaluation(e.to_string()))?;
debug!("Updating individual #{idx} objectives and constraints");
for name in problem.objective_names() {
if !results.objectives.contains_key(&name) {
return Err(OError::Evaluation(format!(
"The evaluation function did non return the value for the objective named '{}'",
name
)));
};
i.update_objective(&name, results.objectives[&name])?;
}
if let Some(constraints) = results.constraints {
for name in problem.constraint_names() {
if !constraints.contains_key(&name) {
return Err(OError::Evaluation(format!(
"The evaluation function did non return the value for the constraints named '{}'",
name
)));
};
i.update_constraint(&name, constraints[&name])?;
}
}
i.set_evaluated();
Ok(())
}
fn count_unevaluated(individuals: &[Individual]) -> u32 {
individuals
.iter()
.filter_map(|i| if !i.is_evaluated() { Some(1) } else { None })
.sum()
}
fn run(&mut self) -> Result<(), OError> {
info!("Starting {}", self.name());
self.initialise()?;
if let Some(export) = self.export_history() {
self.save_to_json(&export.destination, Some("Init"))?;
}
let mut history_gen_step: usize = 0;
let mut avg_time = 0.0;
'gen_loop: loop {
if let Some(export) = self.export_history() {
if history_gen_step == export.generation_step - 1 {
self.save_to_json(&export.destination, None)?;
history_gen_step = 0;
} else {
history_gen_step += 1;
}
}
info!("Generation #{}", self.generation());
let now = SystemTime::now();
self.evolve()?;
if let Result::Ok(elapsed) = now.elapsed() {
avg_time = (avg_time + elapsed.as_secs_f64()) / 2.0;
}
info!(
"Evolved generation #{} - Elapsed Time: {}",
self.generation(),
self.elapsed_as_string()
);
match self.stopping_condition() {
StoppingCondition::MaxDurationAsMinutes(max_t) => {
let left = max_t * 60 - self.start_time().elapsed().as_secs() as u32;
info!("Approximate time left: {}", elapsed_as_string(left as u64));
}
StoppingCondition::MaxDurationAsHours(max_t) => {
let left = max_t * 60 * 24 - self.start_time().elapsed().as_secs() as u32;
info!("Approximate time left: {}", elapsed_as_string(left as u64));
}
StoppingCondition::MaxGeneration(gen) => {
let left = (gen - self.generation()) as f64 * avg_time;
info!("Approximate time left: {}", elapsed_as_string(left as u64));
}
StoppingCondition::MaxFunctionEvaluations(nfe) => {
let left = (nfe - self.number_of_function_evaluations()) as f64 * avg_time
/ self.number_of_function_evaluations() as f64;
info!("Approximate time left: {}", elapsed_as_string(left as u64));
}
_ => {}
}
let cond = self.stopping_condition();
let terminate = self.is_stopping_condition_met(cond)?;
if terminate {
if let Some(export) = self.export_history() {
self.save_to_json(&export.destination, Some("Final"))?;
}
info!("Stopping evolution because the {} was reached", cond.name());
info!("Took {}", self.elapsed_as_string());
break 'gen_loop;
}
info!("========================");
debug!("");
debug!("");
}
Ok(())
}
fn is_stopping_condition_met(&self, condition: &StoppingCondition) -> Result<bool, OError> {
let is_met = match condition {
StoppingCondition::MaxDurationAsMinutes(duration) => {
*duration <= ((Instant::now().elapsed().as_secs() / 60) as u32)
}
StoppingCondition::MaxDurationAsHours(duration) => {
*duration <= ((Instant::now().elapsed().as_secs() / 60 / 60) as u32)
}
StoppingCondition::MaxGeneration(generation) => *generation <= self.generation(),
StoppingCondition::MaxFunctionEvaluations(nfe) => {
*nfe <= self.number_of_function_evaluations()
}
StoppingCondition::Any(conditions) => {
if StoppingCondition::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())
}
StoppingCondition::All(conditions) => {
if StoppingCondition::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)
}
fn get_results(&self) -> AlgorithmExport {
let [hours, minutes, seconds] = self.elapsed();
AlgorithmExport {
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,
minutes,
seconds,
},
additional_data: self.additional_export_data().unwrap_or_default(),
}
}
fn algorithm_options(&self) -> AlgorithmOptions;
fn save_to_json(&self, destination: &PathBuf, file_prefix: Option<&str>) -> Result<(), OError> {
let file_prefix = file_prefix.unwrap_or("History");
let [hours, minutes, seconds] = self.elapsed();
let export = AlgorithmSerialisedExport {
options: self.algorithm_options(),
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 {
hours,
minutes,
seconds,
},
exported_on: Utc::now(),
};
let data = serde_json::to_string_pretty(&export).map_err(|e| {
OError::AlgorithmExport(format!(
"The following error occurred while converting the history struct: {e}"
))
})?;
let mut file = destination.to_owned();
file.push(format!(
"{}_{}_gen{}.json",
file_prefix,
self.name(),
self.generation()
));
info!("Saving JSON file {:?}", file);
fs::write(file, data).map_err(|e| {
OError::AlgorithmExport(format!(
"The following error occurred while exporting the history JSON file: {e}",
))
})?;
Ok(())
}
fn read_json_file(
file: &PathBuf,
) -> Result<AlgorithmSerialisedExport<AlgorithmOptions>, OError> {
if !file.exists() {
return Err(OError::File(
file.to_path_buf(),
"the file does not exist".to_string(),
));
}
let data = fs::File::open(file).map_err(|e| {
OError::File(
file.to_path_buf(),
format!("cannot read the JSON file because: {e}"),
)
})?;
let mut history: AlgorithmSerialisedExport<AlgorithmOptions> =
serde_json::from_reader(data).map_err(|e| {
OError::File(
file.to_path_buf(),
format!("cannot parse the JSON file because: {e}"),
)
})?;
for ind in &mut history.individuals {
for (name, value) in ind.objective_values.iter_mut() {
if history.problem.objectives[name].direction() == ObjectiveDirection::Maximise {
*value *= -1.0;
}
}
}
Ok(history)
}
fn read_json_files(
folder: &PathBuf,
) -> Result<Vec<AlgorithmSerialisedExport<AlgorithmOptions>>, OError> {
let json_files: Vec<_> = read_dir(folder)
.map_err(|e| OError::Generic(format!("Cannot read folder because {e}")))?
.filter_map(|res| res.ok())
.map(|dir_entry| dir_entry.path())
.filter_map(|path| {
if path.extension().map_or(false, |ext| ext == "json")
& path.file_name().map_or(false, |name| {
name.to_str().unwrap().starts_with("History_")
| name.to_str().unwrap().starts_with("Final_")
})
{
Some(path)
} else {
None
}
})
.collect();
let results = json_files
.iter()
.map(|file| Self::read_json_file(file))
.collect::<Result<Vec<_>, OError>>()?;
Ok(results)
}
fn seed_population_from_file(
problem: Arc<Problem>,
name: &str,
expected_individuals: usize,
file: &PathBuf,
) -> Result<Population, OError> {
let data = Self::read_json_file(file)?;
if problem.number_of_variables() != data.problem.variables.len() {
return Err(OError::AlgorithmInit(
name.to_string(),
format!(
"The number of variables from the history file ({}) does not \
match the number of variables ({}) defined in the problem",
data.problem.variables.len(),
problem.number_of_variables()
),
));
}
if expected_individuals != data.individuals.len() {
return Err(OError::AlgorithmInit(
name.to_string(),
format!(
"The number of individuals from the history file ({}) does not \
match the population size ({}) used in the algorithm",
data.problem.variables.len(),
problem.number_of_variables()
),
));
}
Population::deserialise(&data.individuals, problem.clone())
}
}
#[cfg(feature = "python")]
#[pyclass(from_py_object, name = "Algorithm")]
#[derive(Clone)]
#[allow(non_camel_case_types)]
pub enum PyAlgorithm {
nsga2 { options: NSGA2Arg },
nsga3 { options: NSGA3Arg },
adaptive_nsga3 { options: NSGA3Arg },
}
#[cfg(feature = "python")]
#[pymethods]
impl PyAlgorithm {
fn __repr__(&self) -> PyResult<String> {
let value = match self {
PyAlgorithm::nsga2 { options } => {
format!("NSGA2(options={:?})", options.__repr__()?)
}
PyAlgorithm::nsga3 { options } => {
format!("NSGA3(options={:?})", options.__repr__()?)
}
PyAlgorithm::adaptive_nsga3 { options } => {
format!("AdaptiveNSGA3(options={:?})", options.__repr__()?)
}
};
Ok(value)
}
fn __str__(&self) -> String {
self.__repr__().unwrap()
}
}
#[cfg(feature = "python")]
macro_rules! create_py_reader_interface {
($name: ident, $type: ident, $ArgType: ident) => {
#[pyclass]
pub struct $name {
export_data: crate::algorithms::AlgorithmExport,
#[pyo3(get)]
problem: crate::core::PyProblem,
#[pyo3(get)]
individuals: Vec<Individual>,
#[pyo3(get)]
took: Py<PyAny>,
#[pyo3(get)]
objectives: std::collections::HashMap<String, Vec<f64>>,
#[pyo3(get)]
additional_data: Option<std::collections::HashMap<String, DataValue>>,
#[pyo3(get)]
exported_on: chrono::DateTime<chrono::Utc>,
}
#[pymethods]
impl $name {
#[new]
pub fn new(file: PathBuf) -> PyResult<Self> {
let path = PathBuf::from(file);
let file_data: crate::algorithms::AlgorithmSerialisedExport<$ArgType> =
$type::read_json_file(&path)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
let additional_data = file_data.additional_data.clone();
let exported_on = file_data.exported_on.clone();
let export_data: crate::algorithms::AlgorithmExport = file_data
.try_into()
.map_err(|e: OError| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
let problem: crate::core::PyProblem = export_data.problem.as_ref().into();
let took = Python::attach(|py| -> PyResult<Py<PyAny>> {
let module = PyModule::import(py, "datetime")?;
let timedelta = module.getattr("timedelta")?;
let kwargs = pyo3::types::IntoPyDict::into_py_dict(
&[
("hours", export_data.took.hours),
("minutes", export_data.took.minutes),
("seconds", export_data.took.seconds),
],
py,
)?;
let result = timedelta.call((), Some(&kwargs))?;
Ok(result.extract::<Py<PyAny>>()?)
})?;
let individuals = export_data.individuals.clone();
let objectives = export_data
.get_objectives()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
Ok(Self {
export_data,
problem,
took,
individuals,
objectives,
additional_data,
exported_on,
})
}
#[getter]
pub fn generation(&self) -> u32 {
self.export_data.generation
}
#[getter]
pub fn algorithm(&self) -> String {
self.export_data.algorithm.clone()
}
pub fn hyper_volume(&mut self, reference_point: Vec<f64>) -> PyResult<f64> {
let hv = crate::metrics::HyperVolume::from_individuals(
&mut self.export_data.individuals,
&reference_point,
)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
Ok(hv)
}
#[pyo3(signature = (offset=None))]
pub fn estimate_reference_point(&self, offset: Option<Vec<f64>>) -> PyResult<Vec<f64>> {
let individuals = &self.export_data.individuals;
let ref_point =
crate::metrics::HyperVolume::estimate_reference_point(individuals, offset)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
Ok(ref_point)
}
#[staticmethod]
#[pyo3(signature = (folder, offset=None))]
pub fn estimate_reference_point_from_files(
folder: PathBuf,
offset: Option<Vec<f64>>,
) -> PyResult<Vec<f64>> {
let all_serialise_data_vec = $type::read_json_files(&folder)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
let ref_point = crate::metrics::HyperVolume::estimate_reference_point_from_files(
&all_serialise_data_vec,
offset,
)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
Ok(ref_point)
}
#[staticmethod]
pub fn convergence_data(
folder: PathBuf,
reference_point: Vec<f64>,
) -> PyResult<(Vec<u32>, Vec<chrono::DateTime<chrono::Utc>>, Vec<f64>)> {
let all_serialise_data = $type::read_json_files(&folder)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
let data =
crate::metrics::HyperVolume::from_files(&all_serialise_data, &reference_point)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
Ok((data.generations(), data.times(), data.values()))
}
}
};
}
#[cfg(feature = "python")]
pub(crate) use create_py_reader_interface;
#[cfg(feature = "python")]
pub mod py {
use crate::algorithms::NumThreads;
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3::types::PyString;
#[pyclass(name = "NumThreads")]
pub struct PyNumThreads;
impl FromPyObject<'_, '_> for NumThreads {
type Error = PyErr;
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
if obj.is_none() {
Ok(NumThreads::default())
} else if let Ok(x) = obj.extract::<usize>() {
Ok(NumThreads::Use(x))
} else if let Ok(x) = obj.extract::<String>() {
if x == "Max" {
Ok(NumThreads::Max)
} else if x == "Off" {
Ok(NumThreads::Off)
} else {
Err(PyTypeError::new_err("Invalid string".to_string()))
}
} else {
Err(PyTypeError::new_err("Invalid type".to_string()))
}
}
}
impl<'py> IntoPyObject<'py> for NumThreads {
type Target = PyAny;
type Output = Bound<'py, PyAny>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match self {
NumThreads::Max => {
let s = PyString::new(py, "Max");
Ok(s.into_any())
}
NumThreads::Use(number) => {
let n = number.into_pyobject(py)?;
Ok(n.into_any())
}
NumThreads::Off => {
let s = PyString::new(py, "Off");
Ok(s.into_any())
}
}
}
}
}
#[cfg(test)]
mod test {
use std::env;
use std::path::Path;
use std::sync::Arc;
use crate::algorithms::{Algorithm, NSGA2Arg, NumThreads, StoppingCondition, NSGA2};
use crate::core::builtin_problems::{SCHProblem, ZTD1Problem};
#[test]
fn test_load_from_file() {
let file = Path::new(&env::current_dir().unwrap())
.join("examples")
.join("results")
.join("SCH_2obj_NSGA2_gen250.json");
let problem = SCHProblem::create().unwrap();
let pop = NSGA2::seed_population_from_file(Arc::new(problem), "NSGA2", 100, &file);
assert!(pop.is_ok());
}
#[test]
fn test_load_from_file_error() {
let file = Path::new(&env::current_dir().unwrap())
.join("examples")
.join("results")
.join("SCH_2obj_NSGA2_gen250.json");
let problem = SCHProblem::create().unwrap();
let pop = NSGA2::seed_population_from_file(Arc::new(problem), "NSGA2", 10, &file);
assert!(pop
.err()
.unwrap()
.to_string()
.contains("number of individuals from the history file"));
}
#[test]
fn test_load_from_file_wrong_problem() {
let file = Path::new(&env::current_dir().unwrap())
.join("examples")
.join("results")
.join("SCH_2obj_NSGA2_gen250.json");
let problem = ZTD1Problem::create(30).unwrap();
let pop = NSGA2::seed_population_from_file(Arc::new(problem), "NSGA2", 10, &file);
assert!(pop
.err()
.unwrap()
.to_string()
.contains("number of variables from the history file"));
}
#[test]
fn test_stopping_condition_max_generation() {
let problem = SCHProblem::create().unwrap();
let args = NSGA2Arg {
number_of_individuals: 10,
stopping_condition: StoppingCondition::MaxGeneration(20),
crossover_operator_options: None,
mutation_operator_options: None,
threads: NumThreads::Off,
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]
fn test_stopping_condition_max_nfe() {
let problem = SCHProblem::create().unwrap();
let args = NSGA2Arg {
number_of_individuals: 10,
stopping_condition: StoppingCondition::MaxFunctionEvaluations(20),
crossover_operator_options: None,
mutation_operator_options: None,
threads: NumThreads::Off,
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]
fn test_stopping_condition_any() {
let problem = SCHProblem::create().unwrap();
let args = NSGA2Arg {
number_of_individuals: 10,
stopping_condition: StoppingCondition::Any(vec![
StoppingCondition::MaxFunctionEvaluations(20),
StoppingCondition::MaxGeneration(10),
]),
crossover_operator_options: None,
mutation_operator_options: None,
threads: NumThreads::Off,
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);
}
}
#[cfg(feature = "python")]
#[cfg(test)]
mod test_python_api {
use std::env;
use std::error::Error;
use std::path::Path;
use crate::algorithms::NSGA3Data;
use crate::utils::{DasDarren1998, NumberOfPartitions, TwoLayerPartitions};
use float_cmp::assert_approx_eq;
use pyo3::prelude::*;
use pyo3::types::PyList;
#[test]
fn test_reader() -> Result<(), Box<dyn Error>> {
Python::attach(|py| -> Result<(), Box<dyn Error>> {
let file = Path::new(&env::current_dir()?)
.join("examples")
.join("results")
.join("DTLZ1_3obj_NSGA3_gen400.json");
let reader = Py::new(py, NSGA3Data::new(file)?)?;
let problem = reader.getattr(py, "problem")?;
assert_eq!(
problem
.getattr(py, "number_of_variables")?
.extract::<i32>(py)?,
7
);
assert_eq!(
problem
.getattr(py, "variables")?
.call_method1(py, "get", ("x1".to_string(),))?
.getattr(py, "min_value")?
.extract::<f64>(py)?,
0.0
);
assert_eq!(
problem
.getattr(py, "objectives")?
.call_method0(py, "__len__")?
.extract::<i32>(py)?,
3
);
assert_eq!(
reader.getattr(py, "algorithm")?.extract::<String>(py)?,
"NSGA3".to_string()
);
assert_eq!(reader.getattr(py, "generation")?.extract::<i32>(py)?, 400);
assert_eq!(
reader
.getattr(py, "exported_on")?
.getattr(py, "day")?
.extract::<u32>(py)?,
10
);
let ind = reader
.getattr(py, "individuals")?
.call_method1(py, "pop", (0,))?;
assert_eq!(
ind.call_method0(py, "constraint_violation")?
.extract::<f32>(py)?,
0.0
);
assert_approx_eq!(
f32,
ind.call_method1(py, "get_objective_value", ("f2",))?
.extract::<f32>(py)?,
0.167,
epsilon = 0.001
);
assert!(ind
.call_method0(py, "variables")?
.call_method0(py, "keys")?
.call_method1(py, "__contains__", ("x5".to_string(),))?
.extract::<bool>(py)?);
assert!(ind
.call_method0(py, "data")?
.call_method0(py, "keys")?
.call_method1(py, "__contains__", ("reference_point_index".to_string(),))?
.extract::<bool>(py)?);
assert_approx_eq!(
f32,
reader
.call_method1(py, "hyper_volume", (PyList::new(py, [100, 100, 100])?,),)?
.extract::<f32>(py)?,
999999.97,
epsilon = 0.001
);
Ok(())
})
}
#[test]
fn test_hypervolume() -> Result<(), Box<dyn Error>> {
Python::attach(|py| -> Result<(), Box<dyn Error>> {
let hv = Py::new(py, DasDarren1998::new(3, &NumberOfPartitions::OneLayer(5))?)?;
assert_eq!(
hv.call_method0(py, "calculate")?
.call_method0(py, "__len__")?
.extract::<u32>(py)?,
21
);
let hv = Py::new(
py,
DasDarren1998::new(
3,
&&NumberOfPartitions::TwoLayers(TwoLayerPartitions {
boundary_layer: 3,
inner_layer: 4,
scaling: None,
}),
)?,
)?;
assert_eq!(
hv.call_method0(py, "calculate")?
.call_method0(py, "__len__")?
.extract::<u32>(py)?,
25
);
Ok(())
})
}
}