Fugue Integration
Fugue-evo integrates deeply with fugue-ppl, a probabilistic programming library. This enables novel trace-based genetic operators.
Core Concept: Genomes as Traces
In fugue-ppl, a trace records all random choices made during program execution. Fugue-evo represents genomes as traces:
// A genome's genes become trace entries
genome.to_trace() → {
addr!("gene", 0) → 1.234,
addr!("gene", 1) → -0.567,
addr!("gene", 2) → 3.890,
// ...
}
Why Traces?
1. Selective Resampling
Mutation becomes selective resampling of addresses:
// Traditional mutation: perturb random genes
genes[i] += noise;
// Trace mutation: resample selected addresses
let addresses_to_mutate = select_addresses(&trace, probability);
let mutated_trace = resample(trace, addresses_to_mutate);
2. Structured Crossover
Crossover can respect genome structure:
// Exchange subtrees of traces
let child_trace = merge_traces(
parent1_trace,
parent2_trace,
&crossover_points,
);
3. Probabilistic Interpretation
Genetic operators have probabilistic semantics:
- Selection = Conditioning on high fitness
- Mutation = Partial resampling from prior
- Crossover = Trace merging
The EvolutionaryGenome Trait
pub trait EvolutionaryGenome: Clone + Send + Sync {
/// Convert genome to Fugue trace
fn to_trace(&self) -> Trace;
/// Reconstruct genome from trace
fn from_trace(trace: &Trace) -> Result<Self, GenomeError>;
}
Example: RealVector Implementation
impl EvolutionaryGenome for RealVector {
fn to_trace(&self) -> Trace {
let mut trace = Trace::new();
for (i, &gene) in self.genes.iter().enumerate() {
trace.insert(addr!("gene", i), gene);
}
trace
}
fn from_trace(trace: &Trace) -> Result<Self, GenomeError> {
let mut genes = Vec::new();
let mut i = 0;
while let Some(&value) = trace.get(&addr!("gene", i)) {
genes.push(value);
i += 1;
}
Ok(RealVector::new(genes))
}
}
Trace-Based Operators
Trace Mutation
use fugue_evo::fugue_integration::trace_operators::TraceMutation;
let mutation = TraceMutation::new(mutation_probability);
let mutated_genome = mutation.mutate_via_trace(&genome, &bounds, &mut rng);
How it works:
- Convert genome to trace
- For each address, decide whether to resample
- Resample selected addresses from prior (uniform within bounds)
- Reconstruct genome from mutated trace
Trace Crossover
use fugue_evo::fugue_integration::trace_operators::TraceCrossover;
let crossover = TraceCrossover::new(crossover_type);
let (child1, child2) = crossover.crossover_via_trace(&p1, &p2, &mut rng);
How it works:
- Convert both parents to traces
- For each address, select source parent
- Merge into child traces
- Reconstruct child genomes
Effect Handlers
Fugue uses effect handlers (poutine-style) for program transformation. Fugue-evo provides evolution-specific handlers:
Conditioning Handler
Interprets selection as conditioning:
use fugue_evo::fugue_integration::effect_handlers::ConditioningHandler;
// Selection biases toward high fitness
let handler = ConditioningHandler::new(fitness_function);
let selected_trace = handler.condition(trace, fitness_threshold);
Resampling Handler
Implements mutation as partial resampling:
use fugue_evo::fugue_integration::effect_handlers::ResamplingHandler;
let handler = ResamplingHandler::new(resample_probability);
let mutated_trace = handler.resample(trace, &prior, &mut rng);
Evolution Model
The full probabilistic evolution model:
use fugue_evo::fugue_integration::evolution_model::EvolutionModel;
let model = EvolutionModel::new()
.with_prior(UniformPrior::new(&bounds))
.with_likelihood(fitness_function)
.with_mutation_kernel(GaussianKernel::new(sigma));
// One generation = one inference step
let posterior_population = model.step(prior_population, &mut rng);
Advanced: Custom Trace Structures
For complex genomes, design meaningful trace structures:
// Neural network genome
impl EvolutionaryGenome for NeuralNetwork {
fn to_trace(&self) -> Trace {
let mut trace = Trace::new();
// Layer structure
for (layer_idx, layer) in self.layers.iter().enumerate() {
for (neuron_idx, neuron) in layer.neurons.iter().enumerate() {
// Weight addresses
for (weight_idx, &weight) in neuron.weights.iter().enumerate() {
trace.insert(
addr!("layer", layer_idx, "neuron", neuron_idx, "weight", weight_idx),
weight,
);
}
// Bias address
trace.insert(
addr!("layer", layer_idx, "neuron", neuron_idx, "bias"),
neuron.bias,
);
}
}
trace
}
}
This enables:
- Layer-aware mutation (mutate one layer at a time)
- Structural crossover (exchange layers between parents)
- Hierarchical analysis of evolved networks