use hyperreal::Real;
use crate::{
CandidateId, FitnessComparison, FitnessDirection, FitnessReport, FitnessValue, ParetoRelation,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReplayStatus {
Accepted,
Rejected,
Unknown,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Genome {
pub genes: Vec<Real>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Candidate {
pub id: CandidateId,
pub genome: Genome,
pub replay_policy: ReplayPolicy,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Population {
pub candidates: Vec<Candidate>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReplayPolicy {
pub seed: u64,
pub require_exact_replay: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SelectionPolicy {
ExactBest,
Tournament { size: usize },
}
#[derive(Clone, Debug, PartialEq)]
pub enum MutationPolicy {
ExactDelta { gene: usize, delta: Box<Real> },
StochasticProposal { name: String },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CrossoverPolicy {
OnePoint { index: usize },
None,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SelectionError {
CandidateReportCountMismatch,
EmptyPopulation,
TournamentIndexOutOfBounds {
index: usize,
candidate_count: usize,
},
NoAcceptedReplay,
UnknownComparison,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SelectionReport {
pub candidate: Candidate,
pub fitness: FitnessReport,
pub inspected: usize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VariationError {
GeneIndexOutOfBounds {
index: usize,
len: usize,
},
GenomeLengthMismatch {
left: usize,
right: usize,
},
CrossoverIndexOutOfBounds {
index: usize,
len: usize,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DiversityRelation {
Identical,
Distinct,
ShapeMismatch {
left: usize,
right: usize,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiversityReport {
pub candidate_count: usize,
pub pair_count: usize,
pub identical_pairs: usize,
pub distinct_pairs: usize,
pub shape_mismatch_pairs: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SimulatedAnnealingPolicy {
pub initial_temperature: Real,
pub cooling_ratio: Real,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AnnealingAcceptance {
AcceptImprovement,
AcceptEqual,
RequiresProbabilisticProposal,
RejectUncertifiedReplay,
UnknownComparison,
InvalidSchedule,
}
#[derive(Clone, Debug, PartialEq)]
pub struct HillClimbPolicy {
pub step: Real,
pub max_steps: usize,
pub strategy: HillClimbStrategy,
pub accept_equal_plateau: bool,
pub tabu_memory: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HillClimbStrategy {
FirstImprovement,
BestImprovement,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HillClimbStopReason {
LocalOptimum,
BudgetExhausted,
InitialReplayRejected,
InitialFitnessUnsupported,
}
#[derive(Clone, Debug, PartialEq)]
pub struct HillClimbReport {
pub initial: Candidate,
pub final_candidate: Candidate,
pub final_fitness: FitnessReport,
pub reason: HillClimbStopReason,
pub evaluations: usize,
pub accepted_steps: usize,
pub accepted_reports: Vec<FitnessReport>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Archive {
reports: Vec<FitnessReport>,
}
impl Population {
pub fn push(&mut self, candidate: Candidate) {
self.candidates.push(candidate);
}
}
impl HillClimbPolicy {
pub fn first_improvement(step: Real, max_steps: usize) -> Self {
Self {
step,
max_steps,
strategy: HillClimbStrategy::FirstImprovement,
accept_equal_plateau: false,
tabu_memory: 0,
}
}
pub fn best_improvement(step: Real, max_steps: usize) -> Self {
Self {
step,
max_steps,
strategy: HillClimbStrategy::BestImprovement,
accept_equal_plateau: false,
tabu_memory: 0,
}
}
}
pub fn hill_climb_exact<F>(
initial: Candidate,
policy: &HillClimbPolicy,
direction: FitnessDirection,
mut evaluate: F,
) -> HillClimbReport
where
F: FnMut(&Candidate) -> FitnessReport,
{
let initial_report = evaluate(&initial);
if initial_report.replay != ReplayStatus::Accepted {
return HillClimbReport {
initial: initial.clone(),
final_candidate: initial,
final_fitness: initial_report.clone(),
reason: HillClimbStopReason::InitialReplayRejected,
evaluations: 1,
accepted_steps: 0,
accepted_reports: vec![initial_report],
};
}
if !fitness_supports_total_order(&initial_report.value) {
return HillClimbReport {
initial: initial.clone(),
final_candidate: initial,
final_fitness: initial_report.clone(),
reason: HillClimbStopReason::InitialFitnessUnsupported,
evaluations: 1,
accepted_steps: 0,
accepted_reports: vec![initial_report],
};
}
let mut current = initial.clone();
let mut current_report = initial_report;
let mut accepted_reports = vec![current_report.clone()];
let mut recent = vec![current.genome.clone()];
let mut evaluations = 1_usize;
let mut accepted_steps = 0_usize;
for _ in 0..policy.max_steps {
let mut selected: Option<(Candidate, FitnessReport)> = None;
for neighbor in one_step_neighbors(¤t, &policy.step) {
if recent_contains(&recent, &neighbor.genome) {
continue;
}
let report = evaluate(&neighbor);
evaluations += 1;
if !is_acceptable_neighbor(¤t_report, &report, direction, policy) {
continue;
}
match policy.strategy {
HillClimbStrategy::FirstImprovement => {
selected = Some((neighbor, report));
break;
}
HillClimbStrategy::BestImprovement => {
let replace = selected
.as_ref()
.map(|(_, selected_report)| {
report
.value
.compare_total(&selected_report.value, direction)
== FitnessComparison::Better
})
.unwrap_or(true);
if replace {
selected = Some((neighbor, report));
}
}
}
}
let Some((candidate, report)) = selected else {
return HillClimbReport {
initial,
final_candidate: current,
final_fitness: current_report,
reason: HillClimbStopReason::LocalOptimum,
evaluations,
accepted_steps,
accepted_reports,
};
};
current = candidate;
current_report = report;
accepted_steps += 1;
accepted_reports.push(current_report.clone());
recent.push(current.genome.clone());
if policy.tabu_memory > 0 && recent.len() > policy.tabu_memory + 1 {
recent.remove(0);
}
}
HillClimbReport {
initial,
final_candidate: current,
final_fitness: current_report,
reason: HillClimbStopReason::BudgetExhausted,
evaluations,
accepted_steps,
accepted_reports,
}
}
pub fn classify_simulated_annealing_neighbor(
current: &FitnessReport,
neighbor: &FitnessReport,
policy: &SimulatedAnnealingPolicy,
direction: FitnessDirection,
) -> AnnealingAcceptance {
if policy.initial_temperature <= Real::zero() || policy.cooling_ratio <= Real::zero() {
return AnnealingAcceptance::InvalidSchedule;
}
if neighbor.replay != ReplayStatus::Accepted {
return AnnealingAcceptance::RejectUncertifiedReplay;
}
match neighbor.value.compare_total(¤t.value, direction) {
FitnessComparison::Better => AnnealingAcceptance::AcceptImprovement,
FitnessComparison::Equal => AnnealingAcceptance::AcceptEqual,
FitnessComparison::Worse => AnnealingAcceptance::RequiresProbabilisticProposal,
FitnessComparison::Unknown => AnnealingAcceptance::UnknownComparison,
}
}
pub fn select_exact_best(
candidates: &[Candidate],
reports: &[FitnessReport],
direction: FitnessDirection,
) -> Result<SelectionReport, SelectionError> {
select_best_from_indices(candidates, reports, 0..candidates.len(), direction)
}
pub fn select_tournament_by_indices(
candidates: &[Candidate],
reports: &[FitnessReport],
indices: &[usize],
direction: FitnessDirection,
) -> Result<SelectionReport, SelectionError> {
select_best_from_indices(candidates, reports, indices.iter().copied(), direction)
}
pub fn mutate_exact_delta(
candidate: &Candidate,
gene: usize,
delta: Real,
child_id: CandidateId,
) -> Result<Candidate, VariationError> {
if gene >= candidate.genome.genes.len() {
return Err(VariationError::GeneIndexOutOfBounds {
index: gene,
len: candidate.genome.genes.len(),
});
}
let mut genome = candidate.genome.clone();
genome.genes[gene] = genome.genes[gene].clone() + delta;
Ok(Candidate {
id: child_id,
genome,
replay_policy: candidate.replay_policy.clone(),
})
}
pub fn crossover_one_point(
left: &Candidate,
right: &Candidate,
index: usize,
left_child_id: CandidateId,
right_child_id: CandidateId,
) -> Result<(Candidate, Candidate), VariationError> {
let len = left.genome.genes.len();
if right.genome.genes.len() != len {
return Err(VariationError::GenomeLengthMismatch {
left: len,
right: right.genome.genes.len(),
});
}
if index > len {
return Err(VariationError::CrossoverIndexOutOfBounds { index, len });
}
let mut left_genes = Vec::with_capacity(len);
let mut right_genes = Vec::with_capacity(len);
left_genes.extend_from_slice(&left.genome.genes[..index]);
left_genes.extend_from_slice(&right.genome.genes[index..]);
right_genes.extend_from_slice(&right.genome.genes[..index]);
right_genes.extend_from_slice(&left.genome.genes[index..]);
Ok((
Candidate {
id: left_child_id,
genome: Genome { genes: left_genes },
replay_policy: left.replay_policy.clone(),
},
Candidate {
id: right_child_id,
genome: Genome { genes: right_genes },
replay_policy: right.replay_policy.clone(),
},
))
}
pub fn exact_structural_diversity(candidates: &[Candidate]) -> DiversityReport {
let mut report = DiversityReport {
candidate_count: candidates.len(),
pair_count: 0,
identical_pairs: 0,
distinct_pairs: 0,
shape_mismatch_pairs: 0,
};
for left in 0..candidates.len() {
for right in (left + 1)..candidates.len() {
report.pair_count += 1;
match compare_genome_structure(&candidates[left].genome, &candidates[right].genome) {
DiversityRelation::Identical => report.identical_pairs += 1,
DiversityRelation::Distinct => report.distinct_pairs += 1,
DiversityRelation::ShapeMismatch { .. } => report.shape_mismatch_pairs += 1,
}
}
}
report
}
impl Archive {
pub fn reports(&self) -> &[FitnessReport] {
&self.reports
}
pub fn insert_replayed(&mut self, report: FitnessReport) -> bool {
if report.replay != ReplayStatus::Accepted {
return false;
}
self.reports.push(report);
true
}
pub fn insert_non_dominated(
&mut self,
report: FitnessReport,
direction: FitnessDirection,
) -> bool {
if report.replay != ReplayStatus::Accepted {
return false;
}
if self.reports.iter().any(|existing| {
existing.value.compare_pareto(&report.value, direction) == ParetoRelation::Dominates
}) {
return false;
}
self.reports.retain(|existing| {
report.value.compare_pareto(&existing.value, direction) != ParetoRelation::Dominates
});
self.reports.push(report);
true
}
}
impl FitnessReport {
pub fn scalar(candidate: CandidateId, value: Real, replay: ReplayStatus) -> Self {
Self {
candidate,
value: FitnessValue::Scalar(Box::new(value)),
replay,
evidence: Vec::new(),
}
}
}
fn one_step_neighbors(candidate: &Candidate, step: &Real) -> Vec<Candidate> {
let mut neighbors = Vec::with_capacity(candidate.genome.genes.len() * 2);
for (gene_index, _) in candidate.genome.genes.iter().enumerate() {
for (label, signed_step) in [("minus", -step.clone()), ("plus", step.clone())] {
let mut genome = candidate.genome.clone();
genome.genes[gene_index] = genome.genes[gene_index].clone() + signed_step;
let id = CandidateId::new(format!(
"{}:hill:{gene_index}:{label}",
candidate.id.as_str()
))
.expect("generated hill-climb id is non-empty");
neighbors.push(Candidate {
id,
genome,
replay_policy: candidate.replay_policy.clone(),
});
}
}
neighbors
}
fn is_acceptable_neighbor(
current: &FitnessReport,
neighbor: &FitnessReport,
direction: FitnessDirection,
policy: &HillClimbPolicy,
) -> bool {
if neighbor.replay != ReplayStatus::Accepted {
return false;
}
match neighbor.value.compare_total(¤t.value, direction) {
FitnessComparison::Better => true,
FitnessComparison::Equal => policy.accept_equal_plateau,
FitnessComparison::Worse | FitnessComparison::Unknown => false,
}
}
fn fitness_supports_total_order(value: &FitnessValue) -> bool {
matches!(
value,
FitnessValue::Scalar(_) | FitnessValue::Lexicographic(_) | FitnessValue::Interval(_)
)
}
fn recent_contains(recent: &[Genome], genome: &Genome) -> bool {
recent.iter().any(|existing| existing == genome)
}
fn compare_genome_structure(left: &Genome, right: &Genome) -> DiversityRelation {
if left.genes.len() != right.genes.len() {
return DiversityRelation::ShapeMismatch {
left: left.genes.len(),
right: right.genes.len(),
};
}
if left == right {
DiversityRelation::Identical
} else {
DiversityRelation::Distinct
}
}
fn select_best_from_indices<I>(
candidates: &[Candidate],
reports: &[FitnessReport],
indices: I,
direction: FitnessDirection,
) -> Result<SelectionReport, SelectionError>
where
I: IntoIterator<Item = usize>,
{
if candidates.len() != reports.len() {
return Err(SelectionError::CandidateReportCountMismatch);
}
if candidates.is_empty() {
return Err(SelectionError::EmptyPopulation);
}
let mut inspected = 0_usize;
let mut selected: Option<usize> = None;
for index in indices {
if index >= candidates.len() {
return Err(SelectionError::TournamentIndexOutOfBounds {
index,
candidate_count: candidates.len(),
});
}
inspected += 1;
if reports[index].replay != ReplayStatus::Accepted {
continue;
}
let Some(current) = selected else {
selected = Some(index);
continue;
};
match reports[index]
.value
.compare_total(&reports[current].value, direction)
{
FitnessComparison::Better => selected = Some(index),
FitnessComparison::Equal | FitnessComparison::Worse => {}
FitnessComparison::Unknown => return Err(SelectionError::UnknownComparison),
}
}
let Some(index) = selected else {
return Err(SelectionError::NoAcceptedReplay);
};
Ok(SelectionReport {
candidate: candidates[index].clone(),
fitness: reports[index].clone(),
inspected,
})
}