use rlevo_core::config::{self, ConfigError, Validate};
use rlevo_core::probability::Probability;
#[derive(Debug, Clone)]
pub struct GepConfig {
pub head_len: usize,
pub tail_len: usize,
pub pop_size: usize,
pub n_vars: usize,
pub mutation_rate: Probability,
pub is_transpose_rate: Probability,
pub ris_transpose_rate: Probability,
pub crossover_1p_rate: Probability,
pub crossover_2p_rate: Probability,
}
impl GepConfig {
pub fn new(
head_len: usize,
max_arity: usize,
n_vars: usize,
pop_size: usize,
) -> Result<Self, ConfigError> {
config::at_least("GepConfig", "max_arity", max_arity, 1)?;
config::at_least("GepConfig", "head_len", head_len, 1)?;
let tail_len = head_len * (max_arity - 1) + 1;
let genome_len = head_len + tail_len;
#[allow(clippy::cast_precision_loss)]
let mutation_rate = Probability::new(2.0 / genome_len as f32);
let config = Self {
head_len,
tail_len,
pop_size,
n_vars,
mutation_rate,
is_transpose_rate: Probability::new(0.1),
ris_transpose_rate: Probability::new(0.1),
crossover_1p_rate: Probability::new(0.3),
crossover_2p_rate: Probability::new(0.3),
};
config.validate()?;
Ok(config)
}
#[must_use]
pub fn genome_len(&self) -> usize {
self.head_len + self.tail_len
}
}
impl Validate for GepConfig {
fn validate(&self) -> Result<(), ConfigError> {
const C: &str = "GepConfig";
config::at_least(C, "head_len", self.head_len, 1)?;
config::at_least(C, "tail_len", self.tail_len, 1)?;
config::at_least(C, "n_vars", self.n_vars, 1)?;
config::at_least(C, "pop_size", self.pop_size, 1)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derives_tail_for_max_arity_two() {
let cfg = GepConfig::new(7, 2, 1, 100).unwrap();
assert_eq!(cfg.tail_len, 8);
assert_eq!(cfg.genome_len(), 15);
}
#[test]
fn derives_tail_for_max_arity_three() {
let cfg = GepConfig::new(5, 3, 2, 50).unwrap();
assert_eq!(cfg.tail_len, 11);
assert_eq!(cfg.genome_len(), 16);
}
#[test]
fn rejects_zero_head() {
let err = GepConfig::new(0, 2, 1, 10).unwrap_err();
assert_eq!(err.field, "head_len");
}
#[test]
fn rejects_zero_vars() {
let err = GepConfig::new(4, 2, 0, 10).unwrap_err();
assert_eq!(err.field, "n_vars");
}
#[test]
fn rejects_zero_arity() {
let err = GepConfig::new(4, 0, 1, 10).unwrap_err();
assert_eq!(err.field, "max_arity");
}
#[test]
fn accepts_valid_config() {
assert!(GepConfig::new(6, 2, 3, 64).unwrap().validate().is_ok());
}
#[test]
fn rejects_zero_pop() {
let err = GepConfig::new(4, 2, 1, 0).unwrap_err();
assert_eq!(err.field, "pop_size");
}
#[test]
fn unary_max_arity_derives_unit_tail() {
for head in [1usize, 2, 5, 7, 20] {
let cfg = GepConfig::new(head, 1, 1, 10).unwrap();
assert_eq!(cfg.tail_len, 1, "unary tail must be 1 for head {head}");
assert_eq!(cfg.genome_len(), head + 1);
assert!(cfg.validate().is_ok());
}
}
#[test]
fn rate_fields_reject_nan_negative_and_above_one() {
assert!(Probability::try_new(f32::NAN).is_err());
assert!(Probability::try_new(-0.1).is_err());
assert!(Probability::try_new(1.5).is_err());
let mut cfg: GepConfig = GepConfig::new(4, 2, 1, 10).unwrap();
cfg.mutation_rate = Probability::try_new(0.05).unwrap();
cfg.crossover_1p_rate = Probability::try_new(0.9).unwrap();
assert_eq!(cfg.mutation_rate, Probability::try_new(0.05).unwrap());
assert_eq!(cfg.crossover_1p_rate, Probability::try_new(0.9).unwrap());
}
}