Design Philosophy

Fugue-evo is built on several core design principles that distinguish it from traditional GA libraries.

Evolution as Bayesian Inference

The central insight of fugue-evo is that evolutionary algorithms can be understood through a probabilistic lens:

Fitness as Likelihood

In Bayesian terms:

  • Prior: Initial population distribution
  • Likelihood: Fitness function (how well does this solution explain our objective?)
  • Posterior: Population after selection

Selection acts as conditioning: we observe that solutions should have high fitness, and update our population distribution accordingly.

Learnable Operators

Traditional GAs use fixed operators with hand-tuned parameters. Fugue-evo treats operator parameters as uncertain quantities that can be learned:

// Traditional: fixed mutation rate
let mutation_rate = 0.1;

// Fugue-evo: learned mutation rate
let mut posterior = BetaPosterior::new(2.0, 2.0);
// ... observe outcomes, update posterior
let learned_rate = posterior.mean();

Type Safety

Rust's type system enables compile-time correctness guarantees.

Generic Constraints

Operators are typed to their applicable genomes:

// SBX only works with real-valued genomes
impl CrossoverOperator<RealVector> for SbxCrossover { ... }

// Order crossover only works with permutations
impl CrossoverOperator<Permutation> for OrderCrossover { ... }

Builder Pattern

Configuration errors are caught at compile time:

let ga = SimpleGABuilder::new()
    .population_size(100)
    // Missing required configuration = compile error
    .build()?;  // Only compiles when complete

Trait-Based Abstraction

Core abstractions are defined as traits, enabling extensibility.

EvolutionaryGenome

Any type can be a genome if it implements:

pub trait EvolutionaryGenome: Clone + Send + Sync {
    fn to_trace(&self) -> Trace;
    fn from_trace(trace: &Trace) -> Result<Self, GenomeError>;
}

Operator Traits

Custom operators implement standard traits:

pub trait MutationOperator<G>: Send + Sync {
    fn mutate<R: Rng>(&self, genome: &mut G, rng: &mut R);
}

Composability

Components are designed to compose cleanly.

Operator Composition

// Combine mutations
let mutation = CompositeMutation::new(
    GaussianMutation::new(0.1),
    PolynomialMutation::new(20.0),
    0.5, // 50% chance of each
);

Termination Composition

// Complex stopping conditions
let term = AnyOf::new(vec![
    Box::new(MaxGenerations::new(1000)),
    Box::new(AllOf::new(vec![
        Box::new(MinGenerations::new(100)),
        Box::new(FitnessStagnation::new(20)),
    ])),
]);

Reproducibility

Scientific use requires reproducibility.

Seeded RNG

All randomness flows through explicit RNG:

let mut rng = StdRng::seed_from_u64(42);
let result = ga.run(&mut rng)?;
// Same seed = same result

Checkpointing

State can be saved and restored:

manager.save(&checkpoint)?;
// Later...
let checkpoint = load_checkpoint(&path)?;

Performance

Optional Parallelism

Parallelism is opt-in and doesn't affect correctness:

// Sequential (deterministic)
.parallel(false)

// Parallel (faster, same final result distribution)
.parallel(true)

Efficient Representations

Genomes use efficient storage:

// RealVector: contiguous Vec<f64>
// BitString: Vec<bool> (could use bitpacking)
// Permutation: Vec<usize>

Interoperability

Fugue Integration

Deep integration with probabilistic programming:

// Genomes convert to traces
let trace = genome.to_trace();

// Enables trace-based operators
let mutated = trace_mutation(&trace, &mut rng);

WASM Support

Same algorithms run in browser:

const optimizer = new SimpleGAOptimizer(config);
const result = optimizer.run(fitnessFunction);

Simplicity

Avoid Over-Engineering

  • Minimal dependencies
  • Clear, focused APIs
  • Documentation over abstraction

Progressive Disclosure

Simple cases are simple:

// Simplest usage
let result = SimpleGABuilder::new()
    .population_size(100)
    .bounds(bounds)
    .fitness(fitness)
    .max_generations(200)
    .defaults()  // Use sensible defaults
    .run(&mut rng)?;

Advanced features are available when needed:

// Full control
let result = SimpleGABuilder::new()
    .population_size(100)
    .bounds(bounds)
    .selection(custom_selection)
    .crossover(custom_crossover)
    .mutation(custom_mutation)
    .fitness(custom_fitness)
    .termination(complex_termination)
    .parallel(true)
    .elitism(true)
    .elite_count(5)
    .build()?
    .run(&mut rng)?;