Skip to content

Commit 0b052b2

Browse files
alexnodelandclaude
andauthored
Inference-first 0.3.0: likelihoods as programs, optimizer mode, Pareto posterior, prior-owned encodings (#20)
* 0.3.0: inference-first — likelihoods as programs, optimizer mode, Pareto posterior, prior-owned encodings Closes the three caveats left by the 0.2.0 refactor, completing the "implementation of fugue for running evolutionary algorithms as Bayesian inference" identity: Fix A — likelihoods as programs (inference/likelihood.rs): - GenomeLikelihood<G>: observation program p(data|g) with observes, factors, and latent nuisance parameters jointly inferred with the genome. - tempered_observe helper; FactorFitness adapter (black-box mode = explicit Gibbs/generalized-Bayes posterior); NoLikelihood; MemoizedFitness (exact-key shared cache). - EvolutionModel<P, L> generalization; new(prior, fitness) still works via FactorFitness; from_likelihood accepts any observation program. - GaussianRegression with NoiseSpec::Infer: observation noise as a latent site, posterior read off particle traces (test recovers sigma=0.3). Fix B1 — optimizer mode: EvolutionSMC::anneal continues the tempering ladder past beta=1 (fugue primitives only: incremental reweight, normalize, resample, rejuvenate, optional crossover sweep), concentrating on optima with uncertainty attached. Head-to-head example vs SimpleGA. Fix B2 — multi-objective as inference (inference/pareto.rs): ParetoScalarization puts the scalarization weight inside the model (uniform-simplex stick-breaking), so the posterior marginal traces the Pareto front; particle_weights locates each particle on it. Analytic biobjective validation (Pareto set [0,2], x*(w) = 2(1-w)). Fix C — prior-owned encodings: GenomePrior::trace_of (grammar prior overrides with the exact inverse of its generative walk; pinned by inversion + hand-computed PCFG score tests); score/to_weighted_trace work for grammar trees; EvolutionChain::init_from warm-starts from any genome. Fix B3 — identity: `classic` feature gates the EC toolkit (MultiObjectiveFitness moved to core fitness::multi_objective); std,ppl builds inference standalone; std,parallel,checkpoint,classic builds classic fugue-free; README/description lead with the inference identity. All 9 suites green in default config; both minimal configs check clean; clippy/fmt clean; wasm unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HmWLBR9Zq6hPG5o7URDuNJ * Fix refactor-induced broken intra-doc links (rustdoc now warning-free) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HmWLBR9Zq6hPG5o7URDuNJ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 6b10766 commit 0b052b2

31 files changed

Lines changed: 1483 additions & 209 deletions

CHANGELOG.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.3.0] - 2026-07-28
11+
12+
**Inference-first.** fugue-evo's identity is now "an implementation of fugue
13+
for running evolutionary algorithms as Bayesian inference"; the classic EC
14+
toolkit is a standalone, feature-gated companion. This release closes the
15+
three caveats left by 0.2.0: black-box-only fitness, the classic layer's
16+
monopoly on optimization/multi-objective, and the tree-encoding seam.
17+
18+
### Added
19+
20+
- **Likelihoods as programs** (`inference::likelihood`): the new
21+
`GenomeLikelihood<G>` trait — an observation program `p(data|x)` that may
22+
contain per-datum `observe` statements, `factor`s, and **latent nuisance
23+
parameters jointly inferred with the genome**. `tempered_observe` helper;
24+
`FactorFitness` adapter keeps the classical black-box mode as an explicit
25+
Gibbs / generalized-Bayes posterior; `NoLikelihood` for prior-only runs.
26+
`GaussianRegression` (`inference::grammar`) demonstrates the payoff: the
27+
observation noise is a latent site (`NoiseSpec::Infer`), and its posterior
28+
is read off the particle traces — pinned by
29+
`test_symreg_infers_noise_jointly` (recovers a true sigma of 0.3).
30+
- **Optimizer mode** (`EvolutionSMC::anneal`): continue the tempering ladder
31+
past beta = 1 toward `beta_max` (incremental reweight + resample +
32+
pi_beta-invariant rejuvenation + optional crossover sweeps, all fugue
33+
primitives), concentrating the population on the optima — a principled,
34+
uncertainty-carrying single-objective optimizer. Pinned by
35+
`test_anneal_concentrates_on_optimum`; head-to-head with SimpleGA in
36+
`examples/optimize_by_inference.rs`.
37+
- **Multi-objective as inference** (`inference::pareto`):
38+
`ParetoScalarization` puts the scalarization weight *inside the model*
39+
(uniform-simplex stick-breaking Beta sites), so the joint posterior's
40+
marginal traces the Pareto front and `particle_weights` reads each
41+
particle's front position off its trace. Pinned analytically by
42+
`test_pareto_posterior_traces_the_front` (biobjective with Pareto set
43+
[0,2]: mass on the set, both ends covered, particles near their weight's
44+
scalarized optimum x* = 2(1-w)).
45+
- **Prior-owned encodings** (`GenomePrior::trace_of`): encode a genome under
46+
*the prior's* address scheme (default: the canonical `TraceGenome`
47+
encoding; `ArithmeticGrammarPrior` overrides with the exact inverse of its
48+
generative walk — pinned by `test_trace_of_inverts_generative_run` and a
49+
hand-computed PCFG score). `EvolutionModel::score`/`to_weighted_trace` now
50+
work for grammar trees, and the new `EvolutionChain::init_from(genome)`
51+
warm-starts a chain from any in-support genome — including a classic GA/GP
52+
result.
53+
- **`MemoizedFitness`**: exact-key (bincode) shared-cache fitness wrapper,
54+
removing repeated evaluations under replay-heavy inference.
55+
56+
### Changed (breaking)
57+
58+
- `EvolutionModel<P, F>` is now `EvolutionModel<P, L: GenomeLikelihood>`.
59+
`EvolutionModel::new(prior, fitness)` still works (it now returns
60+
`EvolutionModel<P, FactorFitness<F>>`); explicit type annotations need the
61+
`FactorFitness` wrapper. `from_likelihood(prior, likelihood)` accepts any
62+
observation program. `fitness_value`/`log_weight`/`to_weighted_trace` are
63+
specific to the `FactorFitness` mode (EV-52 unchanged and green).
64+
- **`classic` feature (default on)**: `algorithms`, `operators`,
65+
`population`, `hyperparameter`, `interactive`, `checkpoint`,
66+
`diagnostics`, `termination` are now gated. `--no-default-features
67+
--features std,ppl` builds the inference layer with no classic EC code;
68+
`--features std,parallel,checkpoint,classic` builds classic with no fugue.
69+
`MultiObjectiveFitness`/`ClosureMultiObjective` moved to the core
70+
`fitness::multi_objective` (re-exported from `algorithms::nsga2`).
71+
- Crate description and README lead with the inference identity.
72+
73+
1074
## [0.2.0] - 2026-07-28
1175

1276
**"Evolutionary algorithms as probabilistic programs"** — the two-layer

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ fugue-evo is a **two-layer** evolutionary-computation library:
4444

4545
- `EvolutionaryGenome` (src/genome/traits.rs): the classic, fugue-free genome trait (decode/dimension/generate/distance).
4646
- `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.
47-
- `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`.
47+
- `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`.
48+
- `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.
49+
- 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.
50+
- 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).
4851

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

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ lto = true
1212

1313
[package]
1414
name = "fugue-evo"
15-
version = "0.2.0"
15+
version = "0.3.0"
1616
edition = "2021"
1717
authors = ["Alex Nodeland"]
18-
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)"
18+
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"
1919
license = "MIT"
2020
repository = "https://github.com/alexnodeland/fugue-evo"
2121
documentation = "https://docs.rs/fugue-evo"
@@ -24,10 +24,16 @@ keywords = ["genetic-algorithm", "evolution", "optimization", "bayesian", "ppl"]
2424
categories = ["algorithms", "science"]
2525

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

91-
# Examples requiring parallel feature
97+
# Examples gated on the features they exercise
9298
[[example]]
9399
name = "island_model"
94-
required-features = ["parallel"]
100+
required-features = ["parallel", "classic"]
101+
102+
[[example]]
103+
name = "sphere_optimization"
104+
required-features = ["classic"]
105+
106+
[[example]]
107+
name = "rastrigin_benchmark"
108+
required-features = ["classic"]
109+
110+
[[example]]
111+
name = "cma_es_example"
112+
required-features = ["classic"]
113+
114+
[[example]]
115+
name = "hyperparameter_learning"
116+
required-features = ["classic"]
117+
118+
[[example]]
119+
name = "symbolic_regression"
120+
required-features = ["classic"]
121+
122+
[[example]]
123+
name = "checkpointing"
124+
required-features = ["classic", "checkpoint"]
125+
126+
[[example]]
127+
name = "interactive_evolution"
128+
required-features = ["classic"]
95129

96130
[[example]]
97131
name = "bayesian_evolution"
@@ -100,3 +134,7 @@ required-features = ["ppl"]
100134
[[example]]
101135
name = "symbolic_regression_inference"
102136
required-features = ["ppl"]
137+
138+
[[example]]
139+
name = "optimize_by_inference"
140+
required-features = ["ppl", "classic"]

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# Fugue Evo
66

7-
**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))**
7+
**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**
88

99
*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.*
1010

@@ -16,10 +16,14 @@
1616

1717
</div>
1818

19-
An evolutionary-computation library for Rust with an architectural split that keeps both halves honest:
19+
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:
2020

21-
- **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.
22-
- **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.
21+
- **`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`.
22+
- **`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.
23+
- **`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.
24+
- **`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.
25+
26+
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.
2327

2428
## Features
2529

crates/fugue-evo-wasm/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ publish = false
1717
crate-type = ["cdylib", "rlib"]
1818

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

2323
# WASM bindings
2424
wasm-bindgen = "0.2"

examples/optimize_by_inference.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//! Optimizer mode: annealed inference vs a classic GA on the same problem.
2+
//!
3+
//! `EvolutionSMC::anneal` keeps tempering past the posterior (β = 1) toward
4+
//! `β_max`, so the particle population concentrates on the optima — a
5+
//! principled, uncertainty-aware single-objective optimizer built entirely
6+
//! from inference machinery. This example runs it head-to-head with the
7+
//! classic `SimpleGA` on the sphere benchmark and prints what each layer
8+
//! gives you.
9+
//!
10+
//! Run with: `cargo run --example optimize_by_inference`
11+
12+
use fugue_evo::prelude::*;
13+
use rand::rngs::StdRng;
14+
use rand::SeedableRng;
15+
16+
const DIM: usize = 4;
17+
18+
fn main() -> Result<(), Box<dyn std::error::Error>> {
19+
println!("=== Single-objective optimization: annealed inference vs classic GA ===\n");
20+
let mut rng = StdRng::seed_from_u64(20260728);
21+
22+
// Sphere: f(x) = -Σx², optimum 0 at the origin.
23+
let fitness = Sphere::new(DIM);
24+
let bounds = MultiBounds::symmetric(5.12, DIM);
25+
26+
// ---------------- Classic GA ----------------
27+
let ga_result = SimpleGABuilder::real_valued()
28+
.population_size(100)
29+
.bounds(bounds.clone())
30+
.fitness(Sphere::new(DIM))
31+
.max_generations(200)
32+
.build()?
33+
.run(&mut rng)?;
34+
println!("-- SimpleGA (classic layer) --");
35+
println!(
36+
" best fitness: {:.6} (~{} fitness evaluations)",
37+
ga_result.best_fitness,
38+
100 * 200
39+
);
40+
41+
// ---------------- Annealed inference ----------------
42+
let model = EvolutionModel::new(UniformBoxPrior::new(bounds), fitness.clone());
43+
let annealed = EvolutionSMC::anneal(
44+
&mut rng,
45+
&model,
46+
EvoSmcConfig {
47+
num_particles: 300,
48+
rejuvenation_steps: 4,
49+
crossover: Some(CrossoverConfig::default()),
50+
..Default::default()
51+
},
52+
500.0, // β_max: how hard to anneal
53+
15, // annealing rungs past β = 1
54+
);
55+
let model_fn = model.smc_model();
56+
let (best, best_f) = annealed.best(&fitness, &model_fn).unwrap();
57+
println!("\n-- EvolutionSMC::anneal (inference layer) --");
58+
println!(" best fitness: {:.6}", best_f);
59+
println!(" best genome: {:?}", best.genes());
60+
println!(
61+
" population spread at β=500: {:.4} (posterior-style uncertainty, not a point)",
62+
(0..DIM)
63+
.map(|i| annealed.weighted_variance(i))
64+
.sum::<f64>()
65+
.sqrt()
66+
);
67+
println!(
68+
" log evidence (β ≤ 1 ladder): {:.3} — a model score no GA can report",
69+
annealed.log_evidence
70+
);
71+
72+
Ok(())
73+
}

examples/symbolic_regression_inference.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,13 @@ impl Fitness for SymRegFit {
6060
}
6161
}
6262

63-
fn run_inference(
64-
rng: &mut StdRng,
65-
fitness: &SymRegFit,
66-
n_functions: usize,
67-
) -> (
68-
EvolutionPosterior<TreeGenome<ArithmeticTerminal, ArithmeticFunction>>,
69-
EvolutionModel<ArithmeticGrammarPrior, SymRegFit>,
70-
) {
63+
type SymRegTree = TreeGenome<ArithmeticTerminal, ArithmeticFunction>;
64+
type SymRegResult = (
65+
EvolutionPosterior<SymRegTree>,
66+
EvolutionModel<ArithmeticGrammarPrior, FactorFitness<SymRegFit>>,
67+
);
68+
69+
fn run_inference(rng: &mut StdRng, fitness: &SymRegFit, n_functions: usize) -> SymRegResult {
7170
let prior = ArithmeticGrammarPrior {
7271
terminal_prob: 0.35,
7372
max_depth: 5,

0 commit comments

Comments
 (0)