use crate::experiment::traits::ExperimentalCase;
use crate::problem::traits::Problem;
use crate::solution::traits::Dominance;
use crate::ImprovementDirection;
use std::cmp::Ordering;
use std::fmt::Display;
use super::parallel::{parallel_collect_by_range, ParallelConfig};
use super::report::{ExperimentFailure, ExperimentReport, ExperimentRunResult, ExperimentSummary};
use super::utils::{best_and_worst, mean, variance};
#[derive(Clone)]
struct CaseMetadata {
algorithm_name: String,
case_name: String,
parameters_text: String,
}
struct WorkerOutput {
run_results: Vec<ExperimentRunResult>,
failures: Vec<ExperimentFailure>,
}
enum JobOutcome {
Success(ExperimentRunResult),
Failure(ExperimentFailure),
}
pub struct Experiment<T, Q, P>
where
T: Clone + Send + 'static,
Q: Clone + Default + Dominance + Send + 'static + Copy + Into<f64>,
P: Problem<T, Q> + Sync,
{
problem: P,
runs: usize,
parallel_threads: Option<usize>,
objective: ImprovementDirection,
cases: Vec<Box<dyn ExperimentalCase<T, Q, P>>>,
}
impl<T, Q, P> Experiment<T, Q, P>
where
T: Clone + Send + 'static + Display,
Q: Clone + Default + Dominance + Send + 'static + Copy + Into<f64> + Display,
P: Problem<T, Q> + Sync,
{
fn collect_case_metadata(&self) -> Vec<CaseMetadata> {
self.cases
.iter()
.map(|case| CaseMetadata {
algorithm_name: case.algorithm_name().to_string(),
case_name: case.case_name(),
parameters_text: case.parameters_as_text(),
})
.collect()
}
fn execute_single_job(
&self,
case_idx: usize,
run_index: usize,
metadata: &CaseMetadata,
) -> JobOutcome {
let case = &self.cases[case_idx];
match case.run(&self.problem) {
Ok(solution_set) => {
if let Some(best_value) = solution_set.best_solution_value() {
JobOutcome::Success(ExperimentRunResult {
algorithm_name: metadata.algorithm_name.clone(),
case_name: metadata.case_name.clone(),
run_index,
best_value,
})
} else {
JobOutcome::Failure(ExperimentFailure {
algorithm_name: metadata.algorithm_name.clone(),
case_name: metadata.case_name.clone(),
run_index,
error: "algorithm returned an empty solution set".to_string(),
})
}
}
Err(error) => JobOutcome::Failure(ExperimentFailure {
algorithm_name: metadata.algorithm_name.clone(),
case_name: metadata.case_name.clone(),
run_index,
error,
}),
}
}
fn execute_jobs_parallel(&self, case_metadata: &[CaseMetadata]) -> WorkerOutput {
let total_jobs = self.cases.len().saturating_mul(self.runs);
let worker_outputs = parallel_collect_by_range(
total_jobs,
ParallelConfig::new(self.parallel_threads).with_min_chunk_size(1),
|_, range| {
let mut run_results = Vec::<ExperimentRunResult>::new();
let mut failures = Vec::<ExperimentFailure>::new();
for flat_idx in range {
let case_idx = flat_idx / self.runs;
let run_index = flat_idx % self.runs;
let metadata = &case_metadata[case_idx];
match self.execute_single_job(case_idx, run_index, metadata) {
JobOutcome::Success(result) => run_results.push(result),
JobOutcome::Failure(failure) => failures.push(failure),
}
}
WorkerOutput {
run_results,
failures,
}
},
);
let mut run_results = Vec::<ExperimentRunResult>::new();
let mut failures = Vec::<ExperimentFailure>::new();
for mut worker in worker_outputs {
run_results.append(&mut worker.run_results);
failures.append(&mut worker.failures);
}
WorkerOutput {
run_results,
failures,
}
}
fn build_summaries(
&self,
case_metadata: &[CaseMetadata],
run_results: &[ExperimentRunResult],
) -> Vec<ExperimentSummary> {
let mut summaries = Vec::new();
for metadata in case_metadata {
let case_name = &metadata.case_name;
let algorithm_name = &metadata.algorithm_name;
let mut values: Vec<f64> = run_results
.iter()
.filter(|r| r.algorithm_name == *algorithm_name && r.case_name == *case_name)
.map(|r| r.best_value)
.collect();
if values.is_empty() {
continue;
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let runs_ok = values.len();
let mean = mean(&values);
let variance = variance(&values, mean);
let (best, worst) = best_and_worst(&values, self.objective);
summaries.push(ExperimentSummary {
algorithm_name: algorithm_name.clone(),
case_name: case_name.clone(),
parameters_text: metadata.parameters_text.clone(),
runs_ok,
best,
mean,
worst,
std_dev: variance.sqrt(),
});
}
summaries.sort_by(|a, b| {
let ord = match self.objective {
ImprovementDirection::Maximize => {
b.best.partial_cmp(&a.best).unwrap_or(Ordering::Equal)
}
ImprovementDirection::Minimize => {
a.best.partial_cmp(&b.best).unwrap_or(Ordering::Equal)
}
};
if ord == Ordering::Equal {
a.case_name.cmp(&b.case_name)
} else {
ord
}
});
summaries
}
pub fn new(problem: P) -> Self {
let objective = problem.get_improvement_direction().clone();
Self {
problem,
runs: 30,
parallel_threads: None,
objective: objective,
cases: Vec::new(),
}
}
pub fn with_runs(mut self, runs: usize) -> Self {
self.runs = runs.max(1);
self
}
pub fn with_threads(mut self, threads: usize) -> Self {
self.parallel_threads = Some(threads.max(1));
self
}
pub fn sequential(mut self) -> Self {
self.parallel_threads = Some(1);
self
}
pub fn with_parallel(mut self) -> Self {
self.parallel_threads = None;
self
}
pub fn add_case(mut self, case: impl ExperimentalCase<T, Q, P> + 'static) -> Self {
self.cases.push(Box::new(case));
self
}
pub fn execute(&self) -> Result<ExperimentReport, String> {
if self.cases.is_empty() {
return Err("experiment has no algorithms/configurations to execute".to_string());
}
let case_metadata = self.collect_case_metadata();
let worker_output = self.execute_jobs_parallel(&case_metadata);
let summaries = self.build_summaries(&case_metadata, &worker_output.run_results);
Ok(ExperimentReport {
objective: self.objective,
runs_per_case: self.runs,
run_results: worker_output.run_results,
failures: worker_output.failures,
summaries,
})
}
}