Skip to content

feat: enhance with genetic algorithm framework, selection, crossover, mutation & convergence#3

Closed
SuperInstance wants to merge 1 commit into
mainfrom
superz/evolve-enhance
Closed

feat: enhance with genetic algorithm framework, selection, crossover, mutation & convergence#3
SuperInstance wants to merge 1 commit into
mainfrom
superz/evolve-enhance

Conversation

@SuperInstance
Copy link
Copy Markdown
Owner

@SuperInstance SuperInstance commented Apr 13, 2026

Enhancements

Genetic Algorithm Framework

  • GeneticAlgorithm engine with configurable population, chromosomes, and operators
  • Individual with chromosome and fitness tracking
  • GAConfig for tuning all parameters (population size, rates, elitism, convergence)
  • GAResult with best individual, generation count, convergence flag, and stats

Fitness Function Abstraction

  • FitnessFn trait for pluggable fitness evaluation
  • Works with closures and custom types
  • BoxFitnessFn for dynamic dispatch

Selection Strategies

  • Roulette Wheel: fitness-proportionate selection
  • Tournament: configurable tournament size, fittest wins
  • Rank-Based: linear rank weighting for selective pressure

Crossover Operators

  • Single-point: split and swap tails
  • Two-point: split at two points, swap middle segment
  • Uniform: each gene randomly from either parent

Mutation Operators

  • Random Replace: replace gene with random value in range
  • Swap: swap two random genes
  • Scramble: shuffle a random segment
  • Invert: reverse a random segment
  • Gaussian: add normally-distributed noise

Elitism Support

  • Configurable elitism count preserves top performers
  • Elites pass directly to next generation without mutation
  • Ensures best fitness is non-decreasing

Convergence Detection

  • Window-based convergence check
  • Configurable window size and threshold
  • Early termination when fitness plateaus
  • Target fitness support for goal-oriented runs

Generation Statistics

  • Per-generation tracking: best/worst/avg fitness, diversity (std dev)
  • Useful for analysis and visualization

Backward Compatibility

  • Original Engine with behavior evolution fully preserved
  • All 21 original tests pass

Tests

  • 42 tests passing (21 new + 21 original)

Staging: Open in Devin

Copy link
Copy Markdown

@beta-devin-ai-integration beta-devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

View 5 additional findings in Devin Review.

Staging: Open in Devin

Comment thread src/lib.rs
}

fn roulette_pick(population: &[Individual], total_fitness: f64) -> usize {
let mut r = pseudo_random_ga(total_fitness);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 roulette_pick always selects the first individual due to incorrect scaling of random value

pseudo_random_ga() returns a value in [0, 1), but roulette_pick subtracts raw fitness values (which can be >> 1.0) from it. For any population where individual fitnesses are ≥ 1.0, r -= f makes r go negative on the very first iteration, so the first individual is always selected regardless of fitness distribution. The fix is to scale the random value to [0, total_fitness): let mut r = pseudo_random_ga(total_fitness) * total_fitness;.

Suggested change
let mut r = pseudo_random_ga(total_fitness);
let mut r = pseudo_random_ga(total_fitness) * total_fitness;
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment thread src/lib.rs
let total_weight = n as f64 * (n as f64 + 1.0) / 2.0;

let pick = |total: f64| -> usize {
let mut r = pseudo_random_ga(total);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 RankBasedSelection always picks the lowest-fitness individual due to same scaling bug

Same issue as in roulette_pick: pseudo_random_ga(total) returns [0, 1) but the code subtracts rank weights starting at 1.0. Since r is always < 1.0, r -= 1.0 is immediately ≤ 0 on the first iteration (rank 0, weight 1), so the individual at rank 0 (the worst fitness) is always selected. This inverts the intended selection pressure — the GA will consistently prefer worse individuals.

Fix

Change line 409 to: let mut r = pseudo_random_ga(total) * total;

Suggested change
let mut r = pseudo_random_ga(total);
let mut r = pseudo_random_ga(total) * total;
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment thread src/lib.rs
Comment on lines +473 to +474
child1.extend_from_slice(&parent2[p2..]);
child2.extend_from_slice(&parent1[p2..]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 crossover_two_point swaps tails from wrong parents, degenerating to single-point crossover

In two-point crossover, the tails (after p2) should come from the same parent as the head (before p1). The code gives child1 parent2[p2..] and child2 parent1[p2..], which means child1 = parent1[..p1] + parent2[p1..] — effectively single-point crossover at p1, completely ignoring p2.

Correct implementation

Lines 473-474 should be:

child1.extend_from_slice(&parent1[p2..]);
child2.extend_from_slice(&parent2[p2..]);
Suggested change
child1.extend_from_slice(&parent2[p2..]);
child2.extend_from_slice(&parent1[p2..]);
child1.extend_from_slice(&parent1[p2..]);
child2.extend_from_slice(&parent2[p2..]);
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Comment thread src/lib.rs
Comment on lines +572 to +580
let u1 = pseudo_random_ga(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as f64 + 0.123);
let u2 = pseudo_random_ga(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as f64 + 0.456);
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 gaussian_noise can produce infinity when pseudo_random_ga returns 0.0

pseudo_random_ga() returns (h.finish() % 10000) as f64 / 10000.0, which can be exactly 0.0. The Box-Muller transform at src/lib.rs:580 computes (-2.0 * u1.ln()).sqrt() — when u1 == 0.0, u1.ln() is -inf, producing inf. This propagates through mutate_gaussian into chromosome gene values as inf or -inf, corrupting data for all subsequent fitness evaluations, crossover, and selection. The fix is to clamp u1 away from zero, e.g. let u1 = pseudo_random_ga(...).max(1e-10);.

Suggested change
let u1 = pseudo_random_ga(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as f64 + 0.123);
let u2 = pseudo_random_ga(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as f64 + 0.456);
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
let u1 = pseudo_random_ga(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as f64 + 0.123).max(1e-10);
let u2 = pseudo_random_ga(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as f64 + 0.456);
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

… mutation, elitism, convergence

- Genetic algorithm framework (population, individuals, chromosomes)
- Fitness function abstraction (trait-based, pluggable)
- Selection strategies: roulette wheel, tournament, rank-based
- Crossover operators: single-point, two-point, uniform
- Mutation operators: random replace, swap, scramble, invert, gaussian
- Elitism support (preserve top performers each generation)
- Convergence detection (fitness diversity threshold)
- GA config with tunable parameters
- Generation stats tracking (best/worst/avg fitness, diversity)
- Custom operator support via run_with_operators
- Backward-compatible Engine preserved
- 42 tests passing (21 new + 21 original)
@SuperInstance SuperInstance force-pushed the superz/evolve-enhance branch from 104c625 to df3332d Compare April 18, 2026 18:38
@SuperInstance
Copy link
Copy Markdown
Owner Author

Closing: superseded by merged work on main. The changes from this PR have been incorporated through other merged PRs. Thank you for the contribution! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant