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:

  1. Convert genome to trace
  2. For each address, decide whether to resample
  3. Resample selected addresses from prior (uniform within bounds)
  4. 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:

  1. Convert both parents to traces
  2. For each address, select source parent
  3. Merge into child traces
  4. 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

See Also