Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-07-28

**Inference-first.** fugue-evo's identity is now "an implementation of fugue
for running evolutionary algorithms as Bayesian inference"; the classic EC
toolkit is a standalone, feature-gated companion. This release closes the
three caveats left by 0.2.0: black-box-only fitness, the classic layer's
monopoly on optimization/multi-objective, and the tree-encoding seam.

### Added

- **Likelihoods as programs** (`inference::likelihood`): the new
`GenomeLikelihood<G>` trait — an observation program `p(data|x)` that may
contain per-datum `observe` statements, `factor`s, and **latent nuisance
parameters jointly inferred with the genome**. `tempered_observe` helper;
`FactorFitness` adapter keeps the classical black-box mode as an explicit
Gibbs / generalized-Bayes posterior; `NoLikelihood` for prior-only runs.
`GaussianRegression` (`inference::grammar`) demonstrates the payoff: the
observation noise is a latent site (`NoiseSpec::Infer`), and its posterior
is read off the particle traces — pinned by
`test_symreg_infers_noise_jointly` (recovers a true sigma of 0.3).
- **Optimizer mode** (`EvolutionSMC::anneal`): continue the tempering ladder
past beta = 1 toward `beta_max` (incremental reweight + resample +
pi_beta-invariant rejuvenation + optional crossover sweeps, all fugue
primitives), concentrating the population on the optima — a principled,
uncertainty-carrying single-objective optimizer. Pinned by
`test_anneal_concentrates_on_optimum`; head-to-head with SimpleGA in
`examples/optimize_by_inference.rs`.
- **Multi-objective as inference** (`inference::pareto`):
`ParetoScalarization` puts the scalarization weight *inside the model*
(uniform-simplex stick-breaking Beta sites), so the joint posterior's
marginal traces the Pareto front and `particle_weights` reads each
particle's front position off its trace. Pinned analytically by
`test_pareto_posterior_traces_the_front` (biobjective with Pareto set
[0,2]: mass on the set, both ends covered, particles near their weight's
scalarized optimum x* = 2(1-w)).
- **Prior-owned encodings** (`GenomePrior::trace_of`): encode a genome under
*the prior's* address scheme (default: the canonical `TraceGenome`
encoding; `ArithmeticGrammarPrior` overrides with the exact inverse of its
generative walk — pinned by `test_trace_of_inverts_generative_run` and a
hand-computed PCFG score). `EvolutionModel::score`/`to_weighted_trace` now
work for grammar trees, and the new `EvolutionChain::init_from(genome)`
warm-starts a chain from any in-support genome — including a classic GA/GP
result.
- **`MemoizedFitness`**: exact-key (bincode) shared-cache fitness wrapper,
removing repeated evaluations under replay-heavy inference.

### Changed (breaking)

- `EvolutionModel<P, F>` is now `EvolutionModel<P, L: GenomeLikelihood>`.
`EvolutionModel::new(prior, fitness)` still works (it now returns
`EvolutionModel<P, FactorFitness<F>>`); explicit type annotations need the
`FactorFitness` wrapper. `from_likelihood(prior, likelihood)` accepts any
observation program. `fitness_value`/`log_weight`/`to_weighted_trace` are
specific to the `FactorFitness` mode (EV-52 unchanged and green).
- **`classic` feature (default on)**: `algorithms`, `operators`,
`population`, `hyperparameter`, `interactive`, `checkpoint`,
`diagnostics`, `termination` are now gated. `--no-default-features
--features std,ppl` builds the inference layer with no classic EC code;
`--features std,parallel,checkpoint,classic` builds classic with no fugue.
`MultiObjectiveFitness`/`ClosureMultiObjective` moved to the core
`fitness::multi_objective` (re-exported from `algorithms::nsga2`).
- Crate description and README lead with the inference identity.


## [0.2.0] - 2026-07-28

**"Evolutionary algorithms as probabilistic programs"** — the two-layer
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ fugue-evo is a **two-layer** evolutionary-computation library:

- `EvolutionaryGenome` (src/genome/traits.rs): the classic, fugue-free genome trait (decode/dimension/generate/distance).
- `TraceGenome` (src/genome/trace_genome.rs, `ppl`): extension trait adding `to_trace`/`from_trace`/`trace_prefix` — the boundary into the inference layer. `Permutation` uses a Lehmer-code (rank) encoding so single-site MH moves stay valid.
- `GenomePrior` (src/inference/prior.rs): a prior as a program — `fn model(&self) -> fugue::Model<G>` returning the decoded genome. Built-ins: `UniformBoxPrior`, `GaussianPrior`, `BitStringPrior`, `PermutationPrior`, `ArithmeticGrammarPrior`.
- `GenomePrior` (src/inference/prior.rs): a prior as a program — `fn model(&self) -> fugue::Model<G>` returning the decoded genome, plus `trace_of` (encode a genome under the prior's address scheme; grammar prior overrides it). Built-ins: `UniformBoxPrior`, `GaussianPrior`, `BitStringPrior`, `PermutationPrior`, `ArithmeticGrammarPrior`.
- `GenomeLikelihood` (src/inference/likelihood.rs): an observation program `p(data|g)` — observes, factors, latent nuisance sites (jointly inferred). `FactorFitness` is the black-box Gibbs-posterior adapter; `MemoizedFitness` caches expensive evaluations.
- Optimizer mode: `EvolutionSMC::anneal` tempers past beta=1. Multi-objective: `ParetoScalarization` (src/inference/pareto.rs) — scalarization weight as a latent site; posterior traces the Pareto front.
- Feature matrix: `classic` gates the EC toolkit; `ppl` gates inference; each builds without the other (`std,ppl` and `std,parallel,checkpoint,classic` are both CI-relevant configs).

Built-in genome types: `RealVector`, `BitString`, `Permutation`, `TreeGenome`

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 43 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ lto = true

[package]
name = "fugue-evo"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = ["Alex Nodeland"]
description = "Evolutionary computation for Rust: classical EC algorithms plus evolutionary inference - evolutionary algorithms as probabilistic programs (tempered SMC in trace space, built on fugue-ppl)"
description = "An implementation of fugue for running evolutionary algorithms as Bayesian inference: priors and likelihoods as probabilistic programs, tempered SMC in trace space, annealed optimization, Pareto posteriors - plus a standalone classical EC toolkit"
license = "MIT"
repository = "https://github.com/alexnodeland/fugue-evo"
documentation = "https://docs.rs/fugue-evo"
Expand All @@ -24,10 +24,16 @@ keywords = ["genetic-algorithm", "evolution", "optimization", "bayesian", "ppl"]
categories = ["algorithms", "science"]

[features]
default = ["std", "parallel", "checkpoint", "ppl"]
default = ["std", "parallel", "checkpoint", "ppl", "classic"]
std = []
parallel = ["rayon"]
checkpoint = [] # File-based checkpointing (requires std)
# Classic EC layer: algorithms, operators, population machinery, checkpointing,
# hyperparameter tuning, interactive GA, diagnostics, termination. Off => only
# the core (error/fitness/genome) and, with `ppl`, the inference layer compile.
# `checkpoint` and `parallel` only affect classic code paths, so they are inert
# without `classic`.
classic = []
# Probabilistic-programming bridge: the TraceGenome extension trait and the
# fugue-native inference layer. Off => the classic EC layer compiles with no
# fugue-ppl dependency at all.
Expand Down Expand Up @@ -88,10 +94,38 @@ criterion = "0.5"
approx = "0.5"
tempfile = "3.10"

# Examples requiring parallel feature
# Examples gated on the features they exercise
[[example]]
name = "island_model"
required-features = ["parallel"]
required-features = ["parallel", "classic"]

[[example]]
name = "sphere_optimization"
required-features = ["classic"]

[[example]]
name = "rastrigin_benchmark"
required-features = ["classic"]

[[example]]
name = "cma_es_example"
required-features = ["classic"]

[[example]]
name = "hyperparameter_learning"
required-features = ["classic"]

[[example]]
name = "symbolic_regression"
required-features = ["classic"]

[[example]]
name = "checkpointing"
required-features = ["classic", "checkpoint"]

[[example]]
name = "interactive_evolution"
required-features = ["classic"]

[[example]]
name = "bayesian_evolution"
Expand All @@ -100,3 +134,7 @@ required-features = ["ppl"]
[[example]]
name = "symbolic_regression_inference"
required-features = ["ppl"]

[[example]]
name = "optimize_by_inference"
required-features = ["ppl", "classic"]
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Fugue Evo

**Two layers: classical evolutionary algorithms (standalone), and evolutionary inference — evolutionary algorithms *as* probabilistic programs (tempered SMC in trace space, built on [Fugue](https://github.com/alexnodeland/fugue))**
**An implementation of [Fugue](https://github.com/alexnodeland/fugue) for running evolutionary algorithms as Bayesian inference — priors and likelihoods as probabilistic programs, tempered SMC in trace space — plus a standalone classical EC toolkit**

*Populations hunting real landscapes, live in your browser: every figure in the docs at [evo.fugue.run](https://evo.fugue.run) runs the actual crate, compiled to WASM.*

Expand All @@ -16,10 +16,14 @@

</div>

An evolutionary-computation library for Rust with an architectural split that keeps both halves honest:
fugue-evo runs evolution as inference. The prior over genomes is a user-written fugue `Model<G>` (a `GenomePrior`); the data enter through a `GenomeLikelihood` — an *observation program* that may contain per-datum `observe` statements, latent nuisance parameters (e.g. an unknown noise scale, jointly inferred with the genome), or a black-box `factor(β·f(x))` (the classical Gibbs-posterior mode). The target `π_β(x) ∝ p(x)·p(data|x)^β` **is a fugue program**, and every sampler is fugue's own inference machinery:

- **Classic EC (no fugue dependency).** SimpleGA, CMA-ES, NSGA-II, Island Model, Evolution Strategy, EDA/UMDA, SteadyState, all operators, checkpointing, and the WASM surface. Build with `--no-default-features --features std,parallel,checkpoint` and there is no probabilistic-programming dependency at all.
- **Evolutionary inference (`ppl` feature, on by default): evolutionary algorithms *as* probabilistic programs.** The prior over genomes is a user-written fugue `Model<G>` (a `GenomePrior`), fitness enters as `factor(β·f(x))`, so the Boltzmann posterior `π_β(x) ∝ p(x)·exp(β·f(x))` **is a fugue program** — and every sampler is fugue's own inference machinery: `EvolutionChain` (typed single-site MH), `EvolutionSMC` (adaptive tempered SMC with a population-coupled crossover kernel and an unbiased log-evidence estimate), and `ArithmeticGrammarPrior` (genetic programming over a probabilistic grammar, where subtree mutation and crossover are generic trace moves). See `examples/symbolic_regression_inference.rs` — symbolic regression as exact Bayesian inference.
- **`EvolutionChain`** — typed single-site MH (every site kind moves: reals, bits, permutation ranks, tree structure with automatic reversible-jump corrections), warm-startable from any genome via `init_from`.
- **`EvolutionSMC`** — adaptive tempered SMC with a population-coupled crossover kernel, decode-replay genome recovery, and an unbiased **log-evidence** estimate for Bayesian model comparison. `EvolutionSMC::anneal` keeps tempering past β = 1 for **optimizer mode** — a principled single-objective optimizer with uncertainty attached.
- **`ArithmeticGrammarPrior`** — genetic programming over a probabilistic grammar: subtree mutation and crossover are generic trace moves. `examples/symbolic_regression_inference.rs` does symbolic regression as exact Bayesian inference.
- **`ParetoScalarization`** — multi-objective optimization as inference: the scalarization weight is a latent model site, so the posterior marginal *traces the Pareto front* and each particle knows where on the front it lives.

The **classic EC toolkit** (`classic` feature, on by default) — SimpleGA, CMA-ES, NSGA-II, Island Model, ES, EDA/UMDA, operators, checkpointing, the WASM surface — remains fully standalone: build with `--no-default-features --features std,parallel,checkpoint,classic` and there is no probabilistic-programming dependency at all. Conversely, `--features std,ppl` builds the inference layer with no classic EC code. CMA-ES and NSGA-II are deliberately *not* reframed as inference (CMA-ES is not a posterior sampler); they serve as baselines.

## Features

Expand Down
4 changes: 2 additions & 2 deletions crates/fugue-evo-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ publish = false
crate-type = ["cdylib", "rlib"]

[dependencies]
# Core library without parallel/checkpoint features
fugue-evo = { path = "../..", default-features = false, features = ["std"] }
# Classic EC layer without parallel/checkpoint/ppl features
fugue-evo = { path = "../..", default-features = false, features = ["std", "classic"] }

# WASM bindings
wasm-bindgen = "0.2"
Expand Down
73 changes: 73 additions & 0 deletions examples/optimize_by_inference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Optimizer mode: annealed inference vs a classic GA on the same problem.
//!
//! `EvolutionSMC::anneal` keeps tempering past the posterior (β = 1) toward
//! `β_max`, so the particle population concentrates on the optima — a
//! principled, uncertainty-aware single-objective optimizer built entirely
//! from inference machinery. This example runs it head-to-head with the
//! classic `SimpleGA` on the sphere benchmark and prints what each layer
//! gives you.
//!
//! Run with: `cargo run --example optimize_by_inference`

use fugue_evo::prelude::*;
use rand::rngs::StdRng;
use rand::SeedableRng;

const DIM: usize = 4;

fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Single-objective optimization: annealed inference vs classic GA ===\n");
let mut rng = StdRng::seed_from_u64(20260728);

// Sphere: f(x) = -Σx², optimum 0 at the origin.
let fitness = Sphere::new(DIM);
let bounds = MultiBounds::symmetric(5.12, DIM);

// ---------------- Classic GA ----------------
let ga_result = SimpleGABuilder::real_valued()
.population_size(100)
.bounds(bounds.clone())
.fitness(Sphere::new(DIM))
.max_generations(200)
.build()?
.run(&mut rng)?;
println!("-- SimpleGA (classic layer) --");
println!(
" best fitness: {:.6} (~{} fitness evaluations)",
ga_result.best_fitness,
100 * 200
);

// ---------------- Annealed inference ----------------
let model = EvolutionModel::new(UniformBoxPrior::new(bounds), fitness.clone());
let annealed = EvolutionSMC::anneal(
&mut rng,
&model,
EvoSmcConfig {
num_particles: 300,
rejuvenation_steps: 4,
crossover: Some(CrossoverConfig::default()),
..Default::default()
},
500.0, // β_max: how hard to anneal
15, // annealing rungs past β = 1
);
let model_fn = model.smc_model();
let (best, best_f) = annealed.best(&fitness, &model_fn).unwrap();
println!("\n-- EvolutionSMC::anneal (inference layer) --");
println!(" best fitness: {:.6}", best_f);
println!(" best genome: {:?}", best.genes());
println!(
" population spread at β=500: {:.4} (posterior-style uncertainty, not a point)",
(0..DIM)
.map(|i| annealed.weighted_variance(i))
.sum::<f64>()
.sqrt()
);
println!(
" log evidence (β ≤ 1 ladder): {:.3} — a model score no GA can report",
annealed.log_evidence
);

Ok(())
}
15 changes: 7 additions & 8 deletions examples/symbolic_regression_inference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,13 @@ impl Fitness for SymRegFit {
}
}

fn run_inference(
rng: &mut StdRng,
fitness: &SymRegFit,
n_functions: usize,
) -> (
EvolutionPosterior<TreeGenome<ArithmeticTerminal, ArithmeticFunction>>,
EvolutionModel<ArithmeticGrammarPrior, SymRegFit>,
) {
type SymRegTree = TreeGenome<ArithmeticTerminal, ArithmeticFunction>;
type SymRegResult = (
EvolutionPosterior<SymRegTree>,
EvolutionModel<ArithmeticGrammarPrior, FactorFitness<SymRegFit>>,
);

fn run_inference(rng: &mut StdRng, fitness: &SymRegFit, n_functions: usize) -> SymRegResult {
let prior = ArithmeticGrammarPrior {
terminal_prob: 0.35,
max_depth: 5,
Expand Down
Loading
Loading