use genetic_algorithms::error::GaError;
use genetic_algorithms::gp::{
ramped_half_and_half, BoolNode, GpChromosome, GpConfiguration, GpCrossover, GpGa, GpMutation,
GpNode, MathNode, Node, TreeChromosome,
};
use rand::rngs::SmallRng;
use rand::SeedableRng;
use std::fmt;
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum TestNode {
Add,
Mul,
#[default]
X,
Const(i32),
}
impl fmt::Display for TestNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TestNode::Add => write!(f, "add"),
TestNode::Mul => write!(f, "mul"),
TestNode::X => write!(f, "x"),
TestNode::Const(n) => write!(f, "{}", n),
}
}
}
impl GpNode for TestNode {
fn arity(&self) -> usize {
match self {
TestNode::Add | TestNode::Mul => 2,
TestNode::X | TestNode::Const(_) => 0,
}
}
fn evaluate(&self, args: &[f64]) -> f64 {
match self {
TestNode::Add => args[0] + args[1],
TestNode::Mul => args[0] * args[1],
TestNode::X => 0.0,
TestNode::Const(n) => *n as f64,
}
}
fn sample_random_terminal(_rng: &mut impl rand::Rng) -> Self {
TestNode::X
}
fn all_functions() -> Vec<Self> {
vec![TestNode::Add, TestNode::Mul]
}
}
#[test]
fn test_gp_node_trait() {
assert_eq!(TestNode::Add.arity(), 2);
assert_eq!(TestNode::Mul.arity(), 2);
assert_eq!(TestNode::X.arity(), 0);
assert_eq!(TestNode::Const(42).arity(), 0);
assert!(!TestNode::Add.is_terminal());
assert!(!TestNode::Mul.is_terminal());
assert!(TestNode::X.is_terminal());
assert!(TestNode::Const(7).is_terminal());
let fns = TestNode::all_functions();
assert_eq!(fns.len(), 2);
}
#[test]
fn test_tree_chromosome_not_linear() {
let chr: GpChromosome<TestNode> =
GpChromosome::with_root(Box::new(Node::Terminal(TestNode::X)));
assert_eq!(chr.depth(), 1);
assert_eq!(chr.node_count(), 1);
}
#[test]
fn test_node_drop_iterative() {
let mut root: Box<Node<TestNode>> = Box::new(Node::Terminal(TestNode::Const(0)));
for _ in 0..10 {
root = Box::new(Node::Function {
value: TestNode::Add,
children: vec![Box::new(Node::Terminal(TestNode::X)), root],
});
}
drop(root);
}
#[test]
fn test_display_prefix_sexpr() {
let root = Box::new(Node::Function {
value: TestNode::Add,
children: vec![
Box::new(Node::Terminal(TestNode::X)),
Box::new(Node::Terminal(TestNode::X)),
],
});
let chr = GpChromosome::with_root(root);
let s = chr.to_string();
assert!(
s.starts_with('('),
"expected S-expr to start with '(', got: {}",
s
);
}
#[test]
fn test_display_nested() {
let inner = Box::new(Node::Function {
value: TestNode::Mul,
children: vec![
Box::new(Node::Terminal(TestNode::X)),
Box::new(Node::Terminal(TestNode::Const(3))),
],
});
let root = Box::new(Node::Function {
value: TestNode::Add,
children: vec![inner],
});
let chr = GpChromosome::with_root(root);
assert_eq!(chr.to_string(), "(add (mul x 3))");
}
#[test]
fn test_math_node_gp_node_impl() {
let mut rng = SmallRng::seed_from_u64(42);
assert_eq!(MathNode::Add.arity(), 2);
assert!(!MathNode::Add.is_terminal());
assert_eq!(MathNode::Const(7.0).arity(), 0);
assert!(MathNode::Const(7.0).is_terminal());
assert_eq!(MathNode::Var(0).arity(), 0);
assert!(MathNode::Var(0).is_terminal());
let fns = MathNode::all_functions();
assert_eq!(fns.len(), 4);
let t = MathNode::sample_random_terminal(&mut rng);
assert!(
t.is_terminal(),
"sample_random_terminal must return a terminal"
);
assert_eq!(MathNode::ProtectedDiv.evaluate(&[5.0, 0.0]), 1.0);
assert!((MathNode::ProtectedDiv.evaluate(&[6.0, 2.0]) - 3.0).abs() < 1e-10);
assert_eq!(MathNode::Add.evaluate(&[1.0, 2.0]), 3.0);
assert_eq!(MathNode::Sub.evaluate(&[5.0, 3.0]), 2.0);
assert_eq!(MathNode::Mul.evaluate(&[3.0, 4.0]), 12.0);
assert_eq!(MathNode::Const(7.0).evaluate(&[]), 7.0);
}
#[test]
fn test_bool_node_gp_node_impl() {
assert_eq!(BoolNode::And.arity(), 2);
assert_eq!(BoolNode::Or.arity(), 2);
assert_eq!(BoolNode::Not.arity(), 1);
assert_eq!(BoolNode::Gt.arity(), 2);
assert_eq!(BoolNode::Lt.arity(), 2);
let fns = BoolNode::all_functions();
assert_eq!(fns.len(), 5);
assert_eq!(BoolNode::And.evaluate(&[1.0, 1.0]), 1.0);
assert_eq!(BoolNode::And.evaluate(&[1.0, 0.0]), 0.0);
assert_eq!(BoolNode::Or.evaluate(&[0.0, 0.0]), 0.0);
assert_eq!(BoolNode::Or.evaluate(&[1.0, 0.0]), 1.0);
assert_eq!(BoolNode::Not.evaluate(&[0.0]), 1.0);
assert_eq!(BoolNode::Not.evaluate(&[1.0]), 0.0);
assert_eq!(BoolNode::Gt.evaluate(&[2.0, 1.0]), 1.0);
assert_eq!(BoolNode::Gt.evaluate(&[1.0, 2.0]), 0.0);
assert_eq!(BoolNode::Lt.evaluate(&[1.0, 2.0]), 1.0);
assert_eq!(BoolNode::Lt.evaluate(&[2.0, 1.0]), 0.0);
}
fn build_tree(depth: usize) -> Box<Node<TestNode>> {
if depth <= 1 {
Box::new(Node::Terminal(TestNode::X))
} else {
Box::new(Node::Function {
value: TestNode::Add,
children: vec![build_tree(depth - 1), build_tree(depth - 1)],
})
}
}
#[test]
fn test_subtree_crossover() {
let mut rng = SmallRng::seed_from_u64(42);
let p1 = GpChromosome::with_root(build_tree(2));
let p2 = GpChromosome::with_root(build_tree(2));
let result = GpCrossover::SubtreeCrossover.apply(&p1, &p2, 10, 100, &mut rng);
assert!(
result.is_ok(),
"Expected Ok from crossover, got: {:?}",
result
);
let (c1, c2): (GpChromosome<TestNode>, GpChromosome<TestNode>) = result.unwrap();
assert!(
c1.depth() <= 10,
"child1 depth {} exceeds limit",
c1.depth()
);
assert!(
c2.depth() <= 10,
"child2 depth {} exceeds limit",
c2.depth()
);
assert!(
c1.node_count() <= 100,
"child1 node_count {} exceeds limit",
c1.node_count()
);
assert!(
c2.node_count() <= 100,
"child2 node_count {} exceeds limit",
c2.node_count()
);
}
#[test]
fn test_bloat_limit_crossover() {
let mut rng = SmallRng::seed_from_u64(0);
let found_depth_error = (0u64..20).any(|seed| {
let mut r = SmallRng::seed_from_u64(seed);
let p1 = GpChromosome::with_root(build_tree(3));
let p2 = GpChromosome::with_root(build_tree(3));
matches!(
GpCrossover::SubtreeCrossover.apply(&p1, &p2, 2, 1000, &mut r),
Err(GaError::TreeDepthExceeded(_))
)
});
assert!(
found_depth_error,
"Expected at least one TreeDepthExceeded across seeds 0-19"
);
let found_size_error = (0u64..20).any(|seed| {
let mut r = SmallRng::seed_from_u64(seed);
let p1 = GpChromosome::with_root(build_tree(4));
let p2 = GpChromosome::with_root(build_tree(4));
matches!(
GpCrossover::SubtreeCrossover.apply(&p1, &p2, 1000, 5, &mut r),
Err(GaError::TreeSizeExceeded(_))
)
});
assert!(
found_size_error,
"Expected at least one TreeSizeExceeded across seeds 0-19"
);
let p1 = GpChromosome::with_root(build_tree(2));
let p2 = GpChromosome::with_root(build_tree(2));
assert!(GpCrossover::SubtreeCrossover
.apply(&p1, &p2, 100, 1000, &mut rng)
.is_ok());
}
#[test]
fn test_point_mutation() {
let mut rng = SmallRng::seed_from_u64(7);
let chr = GpChromosome::with_root(build_tree(2));
let before_count = chr.node_count();
let before_depth = chr.depth();
let mut chr = chr;
let result = GpMutation::PointMutation { p_per_node: 1.0 }.apply(&mut chr, 100, 1000, &mut rng);
assert!(result.is_ok(), "PointMutation returned error: {:?}", result);
assert_eq!(
chr.node_count(),
before_count,
"PointMutation changed node_count"
);
assert_eq!(chr.depth(), before_depth, "PointMutation changed depth");
}
#[test]
fn test_hoist_mutation() {
let mut rng = SmallRng::seed_from_u64(13);
let chr = GpChromosome::with_root(build_tree(3));
let before_count = chr.node_count();
assert!(before_count > 1, "need a multi-node tree for hoist");
let mut chr = chr;
let result = GpMutation::HoistMutation.apply(&mut chr, 100, 1000, &mut rng);
assert!(result.is_ok(), "HoistMutation returned error: {:?}", result);
assert!(
chr.node_count() <= before_count,
"HoistMutation grew tree from {} to {} nodes",
before_count,
chr.node_count()
);
let mut terminal_chr =
GpChromosome::<TestNode>::with_root(Box::new(Node::Terminal(TestNode::X)));
let result2 = GpMutation::HoistMutation.apply(&mut terminal_chr, 100, 1000, &mut rng);
assert!(result2.is_ok());
assert_eq!(terminal_chr.node_count(), 1);
}
#[test]
fn test_bloat_limit_mutation() {
let rng = SmallRng::seed_from_u64(99);
let found_error = (0u64..50).any(|seed| {
let mut r = SmallRng::seed_from_u64(seed);
let mut chr = GpChromosome::with_root(build_tree(2));
matches!(
GpMutation::SubtreeMutation {
mutation_max_depth: 5
}
.apply(&mut chr, 1, 1000, &mut r),
Err(GaError::TreeDepthExceeded(_))
)
});
assert!(
found_error,
"Expected SubtreeMutation to return TreeDepthExceeded for max_depth=1 across seeds 0-49"
);
let _ = rng;
}
#[test]
fn test_gpga_ramp_half_and_half() {
let mut rng = SmallRng::seed_from_u64(77);
let pop_size = 20;
let init_max_depth = 4;
let pop: Vec<GpChromosome<TestNode>> =
ramped_half_and_half::<TestNode>(pop_size, init_max_depth, &mut rng);
assert_eq!(pop.len(), pop_size, "population size should equal pop_size");
for (i, chr) in pop.iter().enumerate() {
let d = chr.depth();
assert!(d >= 1, "chromosome {} has depth {} < 1", i, d);
assert!(
d <= init_max_depth,
"chromosome {} has depth {} > init_max_depth {}",
i,
d,
init_max_depth
);
let n = chr.node_count();
assert!(n >= 1, "chromosome {} has 0 nodes", i);
}
}
#[test]
fn test_gpga_run_symbolic_regression() {
let config = GpConfiguration::new()
.with_population_size(20)
.with_max_generations(10)
.with_init_max_depth(3)
.with_max_depth(6)
.with_max_node_count(50);
let mut engine = GpGa::<TestNode>::with_ramped_half_and_half(config, |_tree| {
1.0
});
let result = engine.run();
assert!(
result.is_ok(),
"GpGa::run() returned Err: {:?}",
result.err()
);
let result = result.unwrap();
assert!(
!result.best_fitness.is_nan(),
"best_fitness should not be NaN"
);
assert_eq!(result.generations, 10, "should complete all generations");
assert_eq!(
result.population.len(),
20,
"final population should have pop_size individuals"
);
}
#[test]
fn test_generation_stats_avg_node_count() {
use genetic_algorithms::observer::GaObserver;
use genetic_algorithms::stats::GenerationStats;
use std::sync::{Arc, Mutex};
let collected: Arc<Mutex<Vec<GenerationStats>>> = Arc::new(Mutex::new(Vec::new()));
let collected_clone = Arc::clone(&collected);
struct StatsCollector {
stats: Arc<Mutex<Vec<GenerationStats>>>,
}
impl GaObserver<GpChromosome<TestNode>> for StatsCollector {
fn on_generation_end(&self, stats: &GenerationStats) {
self.stats.lock().unwrap().push(stats.clone());
}
}
let observer = Arc::new(StatsCollector {
stats: collected_clone,
});
let config = GpConfiguration::new()
.with_population_size(10)
.with_max_generations(3)
.with_init_max_depth(3)
.with_max_depth(6)
.with_max_node_count(50);
let mut engine =
GpGa::<TestNode>::with_ramped_half_and_half(config, |_tree| 1.0).with_observer(observer);
engine.run().expect("run should succeed");
let stats = collected.lock().unwrap();
assert_eq!(stats.len(), 3, "should have 3 generation stats entries");
for (i, s) in stats.iter().enumerate() {
assert!(
s.avg_node_count > 0.0,
"generation {} avg_node_count should be > 0.0, got {}",
i,
s.avg_node_count
);
}
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_deep_tree() {
let mut root: Node<TestNode> = Node::Terminal(TestNode::X);
for _ in 0..63 {
root = Node::Function {
value: TestNode::Add,
children: vec![Box::new(root), Box::new(Node::Terminal(TestNode::X))],
};
}
assert_eq!(
root.depth(),
64,
"tree must be exactly depth 64 before serialization"
);
let chr = GpChromosome::<TestNode>::with_root(Box::new(root));
let mut out = Vec::<u8>::new();
{
let mut json_ser = serde_json::Serializer::new(&mut out);
let stacker_ser = serde_stacker::Serializer::new(&mut json_ser);
use serde::Serialize as _;
chr.serialize(stacker_ser)
.expect("serialize must not overflow");
}
let json = String::from_utf8(out).expect("output must be valid UTF-8");
let restored: GpChromosome<TestNode> = {
let mut json_de = serde_json::Deserializer::from_str(&json);
json_de.disable_recursion_limit();
let stacker_de = serde_stacker::Deserializer::new(&mut json_de);
use serde::Deserialize as _;
GpChromosome::<TestNode>::deserialize(stacker_de).expect("deserialize must not overflow")
};
assert_eq!(restored.depth(), 64, "round-trip must preserve tree depth");
}