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
75 changes: 75 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.0] - 2026-07-28

**"Evolutionary algorithms as probabilistic programs"** — the two-layer
refactor (cross-repo plan, tracking issue
[#18](https://github.com/alexnodeland/fugue-evo/issues/18); upstream
primitives in fugue-ppl 0.2.1 / fugue#45). fugue-evo is now explicitly two
layers: a standalone classic EC layer with **no** fugue dependency, and a
fugue-native inference layer where the Boltzmann target is literally a fugue
program and every sampler is fugue's own inference machinery.

### Changed (breaking)

- **Trait split**: `EvolutionaryGenome` no longer has `to_trace` /
`from_trace` / `trace_prefix`. They moved to the new `TraceGenome`
extension trait (`genome::trace_genome`, behind the `ppl` feature); bring
it into scope with `use fugue_evo::genome::trace_genome::TraceGenome`.
The `ChoiceValue` re-export moved there too.
- **`ppl` feature (default on)**: `fugue-ppl` is now optional. With
`--no-default-features --features std,parallel,checkpoint` the entire
classic layer (all 8 algorithms, operators, wasm crate) compiles with no
fugue dependency.
- **`fugue_integration` renamed to `inference`** (deprecated alias kept for
one release).
- **`Prior` enum removed** — priors are programs now. `GenomePrior::model()
-> fugue::Model<G>` returns the decoded genome; built-in constructors:
`UniformBoxPrior`, `GaussianPrior`, `BitStringPrior`, `PermutationPrior`,
and the PCFG `ArithmeticGrammarPrior`. All hand-written density code
(`log_prior_density`, `log_boltzmann_target` internals) is deleted;
scoring is `ScoreGivenTrace` replay of the target program.
- **`EvolutionModel<G, F>` is now `EvolutionModel<P: GenomePrior, F>`**:
`EvolutionModel::new(prior, fitness)`. `target_model()` builds the fixed-β
Boltzmann program for MH; `smc_model()` builds the untempered program for
SMC (β applied exactly once by fugue's adaptive tempering — fixing the old
hand-rolled SMC's β double-counting).
- **`EvolutionStep` removed** → `EvolutionChain`, a thin wrapper over
`fugue::adaptive_single_site_mh`. Typed proposals move **every** site kind;
the old proposal only perturbed `F64` choices, so BitString/Permutation
chains silently never moved (new regressions:
`test_bitstring_chain_moves`, `test_permutation_chain_moves`).
- **`Permutation`'s trace encoding is now the Lehmer code** (ranks against
the shrinking available-value list) instead of raw values, matching the
sequential-categorical `PermutationPrior` so single-site MH moves decode to
valid, distinct permutations.
- **`EvolutionarySMC` removed** → `EvolutionSMC::run` /
`run_with_kernel` over `fugue::adaptive_smc_with_kernel`: adaptive
ESS-driven β ladder, systematic resampling, per-particle rejuvenation, the
population-coupled `CrossoverKernel`, and an unbiased **log-evidence**
estimate. Results are `EvolutionPosterior` (fugue particles); genomes are
recovered by decode-replay (`best`, `genomes`, `weighted_mean/variance`).
- **`BayesianAdaptiveGA::new(prior, fitness, pop, gens)`** (was
`(fitness, bounds, ..)`); its conjugate `Beta`/`Gamma` machinery now uses
`rand_distr` instead of fugue distributions.

### Added

- **`ArithmeticGrammarPrior`** (`inference::grammar`): expression trees as a
probabilistic context-free grammar program with tree-path addresses
(`node/0/1#leaf`, `#func`, `#const`, …). Structure lives in the choices, so
fugue's generic machinery becomes genetic programming: single-site MH on a
`#leaf`/`#func` site births/kills subtrees with automatic reversible-jump
corrections (subtree regeneration), and `subtree_crossover_mask()` +
`fugue::CrossoverKernel` grafts subtrees between particles (subtree
crossover). Parsimony is the grammar prior itself.
- **Flagship example** `examples/symbolic_regression_inference.rs`: symbolic
regression posed as exact Bayesian inference — PCFG prior, Gaussian
likelihood factor, tempered SMC with both genetic moves, MAP program by
decode-replay, posterior-predictive readout, and grammar comparison by
Bayes factor. Pinned by `test_symreg_recovers_known_expression` (recovers
`x² + 1`).
- Analytic regression anchors kept green through the rewrite: EV-16
(conjugate SMC posterior, now with an added analytic *evidence* check),
EV-52 (weighted trace = β·f), EV-90 (MH truncated-exponential mean),
EV-53 (conjugate updates / Thompson preference).


## [0.1.1] - 2026-07-21

### Added
Expand Down
21 changes: 13 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ cargo run --example sphere_optimization

## Architecture

fugue-evo is a probabilistic genetic algorithm library that treats evolution as Bayesian inference. It integrates with `fugue-ppl` (a probabilistic programming library) for trace-based evolutionary operators.
fugue-evo is a **two-layer** evolutionary-computation library:

### Core Abstraction: `EvolutionaryGenome` Trait
1. **Classic EC layer** (no fugue dependency): all algorithms, operators, population machinery, checkpointing, WASM. Compiles with `--no-default-features --features std,parallel,checkpoint`.
2. **Inference layer** (`ppl` feature, default on; `src/inference/`): evolutionary algorithms as probabilistic programs. The prior over genomes is a fugue `Model<G>` (`GenomePrior`), fitness enters as `factor(β·f)`, and the Boltzmann posterior is sampled by fugue's own MH/SMC engines (`EvolutionChain`, `EvolutionSMC`). `ArithmeticGrammarPrior` does genetic programming over a probabilistic grammar — subtree mutation/crossover are generic trace moves.

The central abstraction is `EvolutionaryGenome` (src/genome/traits.rs), which requires genomes to convert to/from Fugue traces. This enables:
- **Trace-based mutation**: Selective resampling of addresses
- **Trace-based crossover**: Merging parent traces with constraints
### Core Abstractions

- `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`.

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

Expand All @@ -52,7 +55,7 @@ Built-in genome types: `RealVector`, `BitString`, `Permutation`, `TreeGenome`
- **operators/**: Selection, crossover, mutation operators with trait bounds
- **fitness/**: `Fitness` trait and benchmark functions (Sphere, Rastrigin, Rosenbrock)
- **hyperparameter/**: Adaptive and Bayesian hyperparameter tuning (schedules, self-adaptive, conjugate priors)
- **fugue_integration/**: Trace operators and effect handlers for probabilistic evolution
- **inference/**: (`ppl`) priors as programs, `EvolutionModel` (Boltzmann target as a fugue program), `EvolutionChain` (MH), `EvolutionSMC` (tempered SMC + crossover kernel + log-evidence), `ArithmeticGrammarPrior` (grammar GP), effect handlers, trace operators
- **checkpoint/**: State serialization for pausing/resuming evolution
- **termination/**: Convergence criteria (max generations, fitness threshold, stagnation)

Expand All @@ -75,6 +78,8 @@ SimpleGABuilder::<RealVector, f64, _, _, _, _, _>::new()

Operators implement traits like `SelectionOperator`, `CrossoverOperator`, `MutationOperator`. Bounded variants (`BoundedCrossoverOperator`, `BoundedMutationOperator`) receive bounds information for constraint handling.

### Fugue Integration
### Inference layer invariants

Genomes convert to `fugue::Trace` objects where genes are stored at indexed addresses (e.g., `addr!("gene", 0)`). This enables probabilistic interpretation of genetic operators through the `fugue_integration` module's effect handlers.
- The SMC path uses `EvolutionModel::smc_model()` (untempered `factor(f)`): β is applied exactly once by fugue's adaptive tempering. Never bake β into the SMC factor.
- All densities come from running/replaying the target program (`ScoreGivenTrace`); there is deliberately no hand-written density code in this crate.
- Regression anchors that must stay green: EV-16 (conjugate SMC posterior + analytic evidence), EV-52 (weighted trace = β·f), EV-90 (MH truncated-exponential mean), the dead-chain regressions (`test_bitstring_chain_moves`, `test_permutation_chain_moves`), and `test_symreg_recovers_known_expression`.
4 changes: 2 additions & 2 deletions Cargo.lock

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

22 changes: 18 additions & 4 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.1.1"
version = "0.2.0"
edition = "2021"
authors = ["Alex Nodeland"]
description = "A Probabilistic Genetic Algorithm Library for Rust"
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)"
license = "MIT"
repository = "https://github.com/alexnodeland/fugue-evo"
documentation = "https://docs.rs/fugue-evo"
Expand All @@ -24,10 +24,14 @@ keywords = ["genetic-algorithm", "evolution", "optimization", "bayesian", "ppl"]
categories = ["algorithms", "science"]

[features]
default = ["std", "parallel", "checkpoint"]
default = ["std", "parallel", "checkpoint", "ppl"]
std = []
parallel = ["rayon"]
checkpoint = [] # File-based checkpointing (requires std)
# 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.
ppl = ["dep:fugue-ppl"]

[dependencies]
# Linear algebra for CMA-ES
Expand All @@ -52,7 +56,9 @@ thiserror = "2.0"

# Observability
tracing = "0.1"
fugue-ppl = { path = "../fugue", version = "0.2.0" }

# Probabilistic-programming bridge (optional; enabled by the `ppl` feature)
fugue-ppl = { path = "../fugue", version = "0.2.1", optional = true }

# WASM support (enabled via getrandom js feature when targeting wasm32)
[target.'cfg(target_arch = "wasm32")'.dependencies]
Expand Down Expand Up @@ -86,3 +92,11 @@ tempfile = "3.10"
[[example]]
name = "island_model"
required-features = ["parallel"]

[[example]]
name = "bayesian_evolution"
required-features = ["ppl"]

[[example]]
name = "symbolic_regression_inference"
required-features = ["ppl"]
44 changes: 28 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Fugue Evo

**Evolution as Bayesian inference — a probabilistic, type-safe evolutionary computation library for Rust**
**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))**

*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,16 +16,17 @@

</div>

A broad evolutionary-computation library for Rust, with an optional probabilistic-programming bridge to [Fugue](https://github.com/alexnodeland/fugue).
An evolutionary-computation library for Rust with an architectural split that keeps both halves honest:

The default flagship algorithms (SimpleGA, CMA-ES, NSGA-II, Island Model, Evolution Strategy, EDA/UMDA, SteadyState) are standalone evolutionary computation: they use Fugue's `Trace` only as an address→value data container for the optional `to_trace`/`from_trace` round-trip, not for inference. The genuine "evolution as Bayesian inference over solution spaces" story — a tempered Sequential Monte Carlo pipeline over Fugue's `Model`/`Handler`/`factor` machinery — lives in the `fugue_integration` module (`EvolutionarySMC`/`EvolutionStep`/`BayesianAdaptiveGA`), demonstrated by `examples/bayesian_evolution.rs`. Reach for that module, not the default algorithms, when you want the PPL-powered inference path (EV-17).
- **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.

## Features

- **Multiple Algorithms**: Simple GA, CMA-ES, NSGA-II, Island Model
- **Flexible Genomes**: Real-valued vectors, bit strings, permutations, and GP trees
- **Rich Operators**: SBX crossover, polynomial mutation, tournament selection, and more
- **Probabilistic Integration**: a genuine tempered Sequential Monte Carlo pipeline over Fugue's `Model`/`Handler`/`factor` machinery, targeting the Boltzmann/Gibbs posterior `π_β(x) ∝ p(x) · exp(β·f(x))` (see `examples/bayesian_evolution.rs`)
- **Evolutionary inference**: priors as programs (`GenomePrior` → `fugue::Model<G>`), adaptive tempered SMC over the Boltzmann posterior with log-evidence, typed-proposal MH, and grammar-based GP as exact inference (`examples/bayesian_evolution.rs`, `examples/symbolic_regression_inference.rs`)
- **Bayesian Learning**: opt-in online hyperparameter tuning via a Thompson-sampling multi-armed bandit over conjugate `Beta`/`Gamma` posteriors (`SimpleGABuilder::adaptive_operators` + `run_adaptive`; see `examples/hyperparameter_learning.rs`)
- **Production Ready**: checkpointing with bit-identical resume (ChaCha RNG family), convergence detection, parallel evaluation

Expand Down Expand Up @@ -78,7 +79,8 @@ The `examples/` directory contains demonstrations of various features:
- `checkpointing.rs` - Save and restore evolution state with bit-identical resume
- `symbolic_regression.rs` - Genetic programming with tree genomes
- `hyperparameter_learning.rs` - Opt-in Thompson-sampling operator-parameter tuning
- `bayesian_evolution.rs` - Flagship end-to-end pipeline: tempered SMC over the Boltzmann posterior, plus the Bayesian adaptive GA
- `bayesian_evolution.rs` - End-to-end inference pipeline: tempered SMC over the Boltzmann posterior, MH chain, plus the Bayesian adaptive GA
- `symbolic_regression_inference.rs` - **Flagship**: symbolic regression as exact Bayesian inference over a probabilistic grammar (subtree moves as generic trace machinery, model comparison by Bayes factor)

Run an example:

Expand All @@ -96,7 +98,7 @@ cargo run --example sphere_optimization

### Fitness as Likelihood

The `exp(f/T)` selection weight corresponds to Bayesian conditioning on fitness. In this crate that correspondence is realized concretely in two places: `BoltzmannSelection` (a standalone softmax-of-`f/T` selection operator), and the tempered-SMC path in `fugue_integration`, which targets the Boltzmann/Gibbs posterior `π_β(x) ∝ p(x)·exp(β·f(x))` using Fugue's `factor` machinery. The other default selection operators (tournament, roulette, rank) are ordinary EC and do not perform inference.
The `exp(f/T)` selection weight corresponds to Bayesian conditioning on fitness. In this crate that correspondence is realized concretely in two places: `BoltzmannSelection` (a standalone softmax-of-`f/T` selection operator in the classic layer), and the `inference` module, where the Boltzmann/Gibbs posterior `π_β(x) ∝ p(x)·exp(β·f(x))` is assembled as a literal fugue program (`prior.model().bind(|g| factor(β·f(g)))`) and sampled by fugue's MH and tempered-SMC engines. The other default selection operators (tournament, roulette, rank) are ordinary EC and do not perform inference.

### Learnable Operators

Expand All @@ -110,22 +112,32 @@ The `EvolutionaryGenome` trait provides a unified abstraction supporting:
- `Permutation` - Ordering problems (TSP, scheduling)
- `TreeGenome` - Genetic programming

### Fugue Integration
### Evolution as inference (`ppl`)

Genomes can be converted to Fugue PPL traces for probabilistic operations:
Genomes implementing the `TraceGenome` extension trait can be encoded as fugue
traces (`use fugue_evo::genome::trace_genome::TraceGenome`):

```rust
let trace = genome.to_trace();
let recovered = RealVector::from_trace(&trace)?;
```

Beyond trace conversion, the `fugue_integration` module runs a genuine tempered
Sequential Monte Carlo sampler (`EvolutionarySMC`) over Fugue's
`Model`/`Handler`/`factor` machinery, targeting the Boltzmann/Gibbs posterior
`π_β(x) ∝ p(x) · exp(β·f(x))` from the prior (`β = 0`) to the full posterior
(`β = 1`), using trace-based mutation/crossover as `π_β`-invariant
Metropolis–Hastings rejuvenation moves. See `examples/bayesian_evolution.rs`
for the end-to-end pipeline.
The real story is the `inference` module: the prior is any fugue program
returning the decoded genome, fitness is a likelihood factor, and the
posterior is sampled by fugue's engines —

```rust
let model = EvolutionModel::new(GaussianPrior::new(0.0, 2.0, DIM), fitness);
let posterior = EvolutionSMC::run(&mut rng, &model, EvoSmcConfig::default());
// posterior.weighted_mean(0), posterior.log_evidence, posterior.best(..)
```

Adaptive ESS-driven tempering from the prior (β = 0) to the posterior
(β = 1), typed single-site MH rejuvenation (all site kinds move, including
bits and permutation ranks), a population-coupled crossover kernel, decode-
replay genome recovery, and an unbiased log-evidence estimate for Bayesian
model comparison. See `examples/bayesian_evolution.rs` and the flagship
`examples/symbolic_regression_inference.rs`.

## Algorithms

Expand Down Expand Up @@ -154,7 +166,7 @@ crates were never exercised against together:

```toml
[dependencies]
fugue-ppl = { path = "../fugue", version = "0.2.0" }
fugue-ppl = { path = "../fugue", version = "0.2.1", optional = true }
```

Both crates live side by side under the same `fugue-ecosystem` parent
Expand Down
2 changes: 1 addition & 1 deletion docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
# Architecture

- [Design Philosophy](./architecture/philosophy.md)
- [Fugue Integration](./architecture/fugue-integration.md)
- [Evolution as Inference](./architecture/fugue-integration.md)
- [Type System](./architecture/type-system.md)

-----------
Expand Down
Loading
Loading