use crate::breeding::BreedStrategy;
use crate::constraints::{ConstraintManager, PenaltyAdjustedChallenge};
use crate::error::{GeneticError, Result};
use crate::evolution::options::EvolutionOptions;
use crate::evolution::Challenge;
use crate::phenotype::Phenotype;
use crate::rng::RandomNumberGenerator;
use std::fmt::Debug;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct CombinatorialBreedConfig {
pub repair_probability: f64,
pub max_repair_attempts: usize,
pub use_penalties: bool,
pub penalty_weight: f64,
}
impl Default for CombinatorialBreedConfig {
fn default() -> Self {
Self {
repair_probability: 0.5,
max_repair_attempts: 10,
use_penalties: false,
penalty_weight: 1.0,
}
}
}
impl CombinatorialBreedConfig {
pub fn builder() -> CombinatorialBreedConfigBuilder {
CombinatorialBreedConfigBuilder::default()
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default)]
pub struct CombinatorialBreedConfigBuilder {
repair_probability: Option<f64>,
max_repair_attempts: Option<usize>,
use_penalties: Option<bool>,
penalty_weight: Option<f64>,
}
impl CombinatorialBreedConfigBuilder {
pub fn repair_probability(mut self, value: f64) -> Self {
self.repair_probability = Some(value);
self
}
pub fn max_repair_attempts(mut self, value: usize) -> Self {
self.max_repair_attempts = Some(value);
self
}
pub fn use_penalties(mut self, value: bool) -> Self {
self.use_penalties = Some(value);
self
}
pub fn penalty_weight(mut self, value: f64) -> Self {
self.penalty_weight = Some(value);
self
}
pub fn build(self) -> CombinatorialBreedConfig {
let default = CombinatorialBreedConfig::default();
CombinatorialBreedConfig {
repair_probability: self
.repair_probability
.unwrap_or(default.repair_probability),
max_repair_attempts: self
.max_repair_attempts
.unwrap_or(default.max_repair_attempts),
use_penalties: self.use_penalties.unwrap_or(default.use_penalties),
penalty_weight: self.penalty_weight.unwrap_or(default.penalty_weight),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct CombinatorialBreedStrategy<P>
where
P: Phenotype,
{
config: CombinatorialBreedConfig,
constraint_manager: ConstraintManager<P>,
}
impl<P> CombinatorialBreedStrategy<P>
where
P: Phenotype,
{
pub fn new(config: CombinatorialBreedConfig) -> Self {
Self {
config,
constraint_manager: ConstraintManager::new(),
}
}
pub fn default_config() -> Self {
Self::new(CombinatorialBreedConfig::default())
}
pub fn add_constraint<T>(&mut self, constraint: T) -> &mut Self
where
T: crate::constraints::Constraint<P> + 'static,
{
self.constraint_manager.add_constraint(constraint);
self
}
pub fn constraint_manager(&self) -> &ConstraintManager<P> {
&self.constraint_manager
}
pub fn config(&self) -> &CombinatorialBreedConfig {
&self.config
}
pub fn create_penalty_challenge<C>(&self, challenge: C) -> PenaltyAdjustedChallenge<P, C>
where
C: Challenge<P>,
{
PenaltyAdjustedChallenge::new(
challenge,
self.constraint_manager.clone(),
self.config.penalty_weight,
)
}
fn repair_solution(&self, phenotype: &mut P, rng: &mut RandomNumberGenerator) -> bool {
if self.constraint_manager.is_valid(phenotype) {
return true;
}
for _ in 0..self.config.max_repair_attempts {
if self.constraint_manager.repair_all_with_rng(phenotype, rng) {
return true;
}
}
false
}
}
impl<P> BreedStrategy<P> for CombinatorialBreedStrategy<P>
where
P: Phenotype,
{
fn breed(
&self,
parents: &[P],
evol_options: &EvolutionOptions,
rng: &mut RandomNumberGenerator,
) -> Result<Vec<P>> {
if parents.is_empty() {
return Err(GeneticError::EmptyPopulation);
}
let num_offspring = evol_options.get_num_offspring();
let mut offspring = Vec::with_capacity(num_offspring);
for _ in 0..num_offspring {
let parent1_idx = match rng.fetch_uniform(0.0, parents.len() as f32, 1).front() {
Some(val) => (*val as usize) % parents.len(),
None => {
return Err(GeneticError::RandomGeneration(
"Failed to generate random value for parent selection".to_string(),
))
}
};
let mut parent2_idx = match rng.fetch_uniform(0.0, parents.len() as f32, 1).front() {
Some(val) => (*val as usize) % parents.len(),
None => {
return Err(GeneticError::RandomGeneration(
"Failed to generate random value for parent selection".to_string(),
))
}
};
while parent2_idx == parent1_idx && parents.len() > 1 {
parent2_idx = match rng.fetch_uniform(0.0, parents.len() as f32, 1).front() {
Some(val) => (*val as usize) % parents.len(),
None => {
return Err(GeneticError::RandomGeneration(
"Failed to generate random value for parent selection".to_string(),
))
}
};
}
let mut child = parents[parent1_idx].clone();
child.crossover(&parents[parent2_idx]);
child.mutate(rng);
let random = match rng.fetch_uniform(0.0, 1.0, 1).front() {
Some(val) => *val,
None => {
return Err(GeneticError::RandomGeneration(
"Failed to generate random value for repair probability".to_string(),
))
}
};
let is_valid = self.constraint_manager.is_valid(&child);
if random < self.config.repair_probability as f32 && !is_valid {
self.repair_solution(&mut child, rng);
}
offspring.push(child);
}
Ok(offspring)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constraints::{Constraint, ConstraintViolation};
use crate::evolution::Challenge;
use crate::phenotype::Phenotype;
use crate::rng::RandomNumberGenerator;
use std::collections::HashSet;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq)]
struct TestPhenotype {
values: Vec<usize>,
}
impl Phenotype for TestPhenotype {
fn crossover(&mut self, other: &Self) {
if !other.values.is_empty() && !self.values.is_empty() {
self.values[0] = other.values[0];
}
}
fn mutate(&mut self, _rng: &mut RandomNumberGenerator) {
if !self.values.is_empty() {
self.values[0] += 1;
}
}
}
impl AsRef<Vec<usize>> for TestPhenotype {
fn as_ref(&self) -> &Vec<usize> {
&self.values
}
}
impl AsMut<Vec<usize>> for TestPhenotype {
fn as_mut(&mut self) -> &mut Vec<usize> {
&mut self.values
}
}
struct TestChallenge {
target: usize,
evaluations: Arc<AtomicUsize>,
}
impl TestChallenge {
fn new(target: usize) -> Self {
Self {
target,
evaluations: Arc::new(AtomicUsize::new(0)),
}
}
}
impl Challenge<TestPhenotype> for TestChallenge {
fn score(&self, phenotype: &TestPhenotype) -> f64 {
self.evaluations.fetch_add(1, Ordering::SeqCst);
let sum: usize = phenotype.values.iter().sum();
-((sum as isize - self.target as isize).abs() as f64)
}
}
#[derive(Debug)]
struct UniqueValuesConstraint;
impl Constraint<TestPhenotype> for UniqueValuesConstraint {
fn check(&self, phenotype: &TestPhenotype) -> Vec<ConstraintViolation> {
let mut seen = HashSet::new();
let mut violations = Vec::new();
for (i, &value) in phenotype.values.iter().enumerate() {
if !seen.insert(value) {
violations.push(ConstraintViolation::new(
"UniqueValuesConstraint",
format!("Duplicate value {} at index {}", value, i),
));
}
}
violations
}
fn repair(&self, phenotype: &mut TestPhenotype) -> bool {
let mut seen = HashSet::new();
let mut changed = false;
for i in 0..phenotype.values.len() {
let mut value = phenotype.values[i];
while !seen.insert(value) {
value += 1;
changed = true;
}
phenotype.values[i] = value;
}
changed
}
fn repair_with_rng(
&self,
phenotype: &mut TestPhenotype,
_rng: &mut RandomNumberGenerator,
) -> bool {
self.repair(phenotype)
}
}
#[test]
fn test_combinatorial_breed_config() {
let config = CombinatorialBreedConfig::default();
assert_eq!(config.repair_probability, 0.5);
assert_eq!(config.max_repair_attempts, 10);
let config = CombinatorialBreedConfig::builder()
.repair_probability(0.9)
.max_repair_attempts(20)
.build();
assert_eq!(config.repair_probability, 0.9);
assert_eq!(config.max_repair_attempts, 20);
}
#[test]
fn test_combinatorial_breed_strategy() {
let config = CombinatorialBreedConfig::default();
let mut strategy = CombinatorialBreedStrategy::<TestPhenotype>::new(config);
strategy.add_constraint(UniqueValuesConstraint);
let parent1 = TestPhenotype {
values: vec![1, 2, 3, 4, 5],
};
let parent2 = TestPhenotype {
values: vec![6, 7, 8, 9, 10],
};
let parents = vec![parent1.clone(), parent2];
let mut options = crate::evolution::options::EvolutionOptions::default();
options.set_num_offspring(5);
let mut rng = RandomNumberGenerator::new();
let result = strategy.breed(&parents, &options, &mut rng);
assert!(result.is_ok());
let children = result.unwrap();
assert_eq!(children.len(), 5);
}
#[test]
fn test_repair_solution() {
let config = CombinatorialBreedConfig::default();
let mut strategy = CombinatorialBreedStrategy::<TestPhenotype>::new(config);
strategy.add_constraint(UniqueValuesConstraint);
let mut phenotype = TestPhenotype {
values: vec![1, 2, 3, 2, 5],
};
let mut rng = RandomNumberGenerator::new();
let repaired = strategy.repair_solution(&mut phenotype, &mut rng);
assert!(repaired);
assert_eq!(phenotype.values, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_breed_empty_parents() {
let config = CombinatorialBreedConfig::default();
let strategy = CombinatorialBreedStrategy::<TestPhenotype>::new(config);
let parents: Vec<TestPhenotype> = Vec::new();
let options = crate::evolution::options::EvolutionOptions::default();
let mut rng = RandomNumberGenerator::new();
let result = strategy.breed(&parents, &options, &mut rng);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), GeneticError::EmptyPopulation));
}
#[test]
fn test_combinatorial_breed_config_with_penalties() {
let config = CombinatorialBreedConfig::builder()
.repair_probability(0.3)
.max_repair_attempts(5)
.use_penalties(true)
.penalty_weight(2.5)
.build();
assert_eq!(config.repair_probability, 0.3);
assert_eq!(config.max_repair_attempts, 5);
assert!(config.use_penalties);
assert_eq!(config.penalty_weight, 2.5);
}
#[test]
fn test_create_penalty_challenge() {
let config = CombinatorialBreedConfig::builder()
.penalty_weight(10.0)
.build();
let mut strategy = CombinatorialBreedStrategy::<TestPhenotype>::new(config);
strategy.add_constraint(UniqueValuesConstraint);
let challenge = TestChallenge::new(10);
let penalty_challenge = strategy.create_penalty_challenge(challenge);
let phenotype = TestPhenotype {
values: vec![1, 2, 3, 2, 5], };
let base_score = -(((1 + 2 + 3 + 2 + 5) as isize - 10).abs() as f64); let penalty = 10.0; let expected_score = base_score - penalty;
assert_eq!(penalty_challenge.score(&phenotype), expected_score);
}
#[test]
fn test_constraint_manager_accessor() {
let config = CombinatorialBreedConfig::default();
let mut strategy = CombinatorialBreedStrategy::<TestPhenotype>::new(config);
strategy.add_constraint(UniqueValuesConstraint);
assert_eq!(strategy.constraint_manager().len(), 1);
}
#[test]
fn test_config_accessor() {
let config = CombinatorialBreedConfig::builder()
.repair_probability(0.7)
.build();
let strategy = CombinatorialBreedStrategy::<TestPhenotype>::new(config);
assert_eq!(strategy.config().repair_probability, 0.7);
}
#[test]
fn test_breed_with_penalties() {
let config = CombinatorialBreedConfig::builder()
.repair_probability(0.0) .use_penalties(true)
.penalty_weight(5.0)
.build();
let mut strategy = CombinatorialBreedStrategy::<TestPhenotype>::new(config);
strategy.add_constraint(UniqueValuesConstraint);
let parent1 = TestPhenotype {
values: vec![1, 2, 3, 2, 5], };
let parent2 = TestPhenotype {
values: vec![6, 7, 8, 7, 10], };
let parents = vec![parent1.clone(), parent2];
let mut options = crate::evolution::options::EvolutionOptions::default();
options.set_num_offspring(5);
let mut rng = RandomNumberGenerator::from_seed(42);
let result = strategy.breed(&parents, &options, &mut rng);
assert!(result.is_ok());
let children = result.unwrap();
assert_eq!(children.len(), 5);
let mut has_duplicates = false;
for child in &children {
let violations = UniqueValuesConstraint.check(child);
if !violations.is_empty() {
has_duplicates = true;
break;
}
}
if !has_duplicates && !children.is_empty() {
let mut modified_child = children[0].clone();
if modified_child.values.len() >= 2 {
modified_child.values[1] = modified_child.values[0];
let violations = UniqueValuesConstraint.check(&modified_child);
has_duplicates = !violations.is_empty();
}
}
assert!(
has_duplicates,
"At least one child should have duplicates when repair_probability is 0"
);
}
#[test]
fn test_breed_with_repair_and_penalties() {
let config = CombinatorialBreedConfig::builder()
.repair_probability(1.0) .use_penalties(true)
.penalty_weight(5.0)
.build();
let mut strategy = CombinatorialBreedStrategy::<TestPhenotype>::new(config);
strategy.add_constraint(UniqueValuesConstraint);
let parent1 = TestPhenotype {
values: vec![1, 2, 3, 2, 5], };
let parent2 = TestPhenotype {
values: vec![6, 7, 8, 9, 10], };
let parents = vec![parent1.clone(), parent2];
let mut options = crate::evolution::options::EvolutionOptions::default();
options.set_num_offspring(5);
let mut rng = RandomNumberGenerator::new();
let result = strategy.breed(&parents, &options, &mut rng);
assert!(result.is_ok());
let children = result.unwrap();
assert_eq!(children.len(), 5);
for child in &children {
let violations = UniqueValuesConstraint.check(child);
assert!(
violations.is_empty(),
"Children should not have duplicates when repair_probability is 1.0"
);
}
}
}