Skip to main content

fugue_evo/inference/
prior.rs

1//! Priors over genomes as probabilistic programs
2//!
3//! A [`GenomePrior`] is *the* load-bearing abstraction of the inference layer:
4//! instead of a closed enum of built-in priors, the prior over genomes is an
5//! arbitrary fugue [`Model`] written by the user (or one of the constructors
6//! below). Running it under a `PriorHandler` both draws `p(x)` and accumulates
7//! `log_prior`; scoring an existing genome's trace against it recovers the
8//! genuine prior density — there is no hand-written density code anywhere in
9//! this layer.
10//!
11//! The model returns the **decoded genome** `G`, not a bare vector: the model's
12//! return value *is* the decode, which is what lets the SMC layer recover a
13//! genome from a bare particle trace by replay (see
14//! [`crate::inference::smc::EvolutionPosterior`]).
15
16use fugue::{addr, plate, sample, Bernoulli, Categorical, Model, ModelExt, Normal, Uniform};
17
18use crate::genome::bit_string::BitString;
19use crate::genome::bounds::MultiBounds;
20use crate::genome::permutation::Permutation;
21use crate::genome::real_vector::RealVector;
22use crate::genome::trace_genome::TraceGenome;
23use crate::genome::traits::{BinaryGenome, PermutationGenome, RealValuedGenome};
24
25/// A prior distribution over genomes, expressed as a probabilistic program.
26///
27/// The program must sample the genome's canonical trace sites (the same
28/// addresses [`TraceGenome::to_trace`] writes — `gene#i`, `bit#i`, `perm#i`,
29/// …) and return the assembled genome. Anything expressible as a fugue
30/// `Model` is a valid prior: correlated coordinates, hierarchical scales,
31/// variable-length genomes (a length site followed by that many coordinate
32/// sites — fugue's MH treats the resulting births/deaths as reversible-jump
33/// moves with no extra code here).
34pub trait GenomePrior: Clone + Send + Sync + 'static {
35    /// The genome type this prior generates.
36    type Genome: TraceGenome;
37
38    /// The generative program `p(x)`.
39    fn model(&self) -> Model<Self::Genome>;
40
41    /// Encode a genome as a trace **under this prior's address scheme** — the
42    /// inverse direction of running [`Self::model`]. The default delegates to
43    /// the genome's canonical [`TraceGenome::to_trace`] encoding, which is
44    /// correct whenever the prior samples exactly the canonical sites (all the
45    /// vector priors here). Priors with their own generative scheme — e.g.
46    /// [`ArithmeticGrammarPrior`](super::grammar::ArithmeticGrammarPrior)'s
47    /// tree-path grammar — override this so that scoring, weighted traces, and
48    /// chain warm-starts work for any genome the prior can express.
49    fn trace_of(&self, genome: &Self::Genome) -> fugue::Trace {
50        genome.to_trace()
51    }
52}
53
54/// Independent uniform prior over a bounded box (per-dimension `[min, max]`).
55///
56/// The support behavior formerly hand-coded in the old `Prior::UniformBounds`
57/// match (`−∞` outside the box) now falls out of scoring `Uniform` sites under
58/// replay: an out-of-bounds value has `log_prob = −∞` and any MH move onto it
59/// is rejected.
60#[derive(Clone, Debug)]
61pub struct UniformBoxPrior {
62    bounds: MultiBounds,
63}
64
65impl UniformBoxPrior {
66    /// Uniform prior over the given per-dimension bounds.
67    pub fn new(bounds: MultiBounds) -> Self {
68        Self { bounds }
69    }
70
71    /// The bounds of the box.
72    pub fn bounds(&self) -> &MultiBounds {
73        &self.bounds
74    }
75}
76
77impl GenomePrior for UniformBoxPrior {
78    type Genome = RealVector;
79
80    fn model(&self) -> Model<RealVector> {
81        let bounds = self.bounds.clone();
82        plate!(i in 0..bounds.dimension().max(1) => {
83            let (lo, hi) = match bounds.get(i) {
84                Some(b) if b.max > b.min => (b.min, b.max),
85                Some(b) => (b.min - 1e-9, b.min + 1e-9),
86                None => (-1.0, 1.0),
87            };
88            sample(addr!("gene", i), Uniform::new(lo, hi).expect("valid uniform prior bounds"))
89        })
90        .map(|genes| RealVector::from_genes(genes).expect("plate produced genes"))
91    }
92}
93
94/// Independent Gaussian `N(mean, std²)` prior on every real coordinate.
95#[derive(Clone, Debug)]
96pub struct GaussianPrior {
97    mean: f64,
98    std: f64,
99    dim: usize,
100}
101
102impl GaussianPrior {
103    /// I.i.d. Gaussian prior with the given per-coordinate mean/std over `dim`
104    /// coordinates. `std` must be positive.
105    pub fn new(mean: f64, std: f64, dim: usize) -> Self {
106        assert!(std > 0.0, "Gaussian prior std must be > 0");
107        Self { mean, std, dim }
108    }
109}
110
111impl GenomePrior for GaussianPrior {
112    type Genome = RealVector;
113
114    fn model(&self) -> Model<RealVector> {
115        let (mean, std, dim) = (self.mean, self.std, self.dim.max(1));
116        plate!(i in 0..dim => {
117            sample(addr!("gene", i), Normal::new(mean, std).expect("valid Gaussian prior"))
118        })
119        .map(|genes| RealVector::from_genes(genes).expect("plate produced genes"))
120    }
121}
122
123/// Independent `Bernoulli(p)` prior on every bit of a [`BitString`].
124#[derive(Clone, Debug)]
125pub struct BitStringPrior {
126    p_one: f64,
127    len: usize,
128}
129
130impl BitStringPrior {
131    /// Prior over `len`-bit strings with per-bit probability `p_one` of a set
132    /// bit. `p_one` must lie in `(0, 1)` so every string has support.
133    pub fn new(p_one: f64, len: usize) -> Self {
134        assert!(
135            p_one > 0.0 && p_one < 1.0,
136            "BitStringPrior p_one must be in (0, 1)"
137        );
138        Self { p_one, len }
139    }
140
141    /// Uniform prior over `len`-bit strings (`p = 1/2` per bit).
142    pub fn uniform(len: usize) -> Self {
143        Self::new(0.5, len)
144    }
145}
146
147impl GenomePrior for BitStringPrior {
148    type Genome = BitString;
149
150    fn model(&self) -> Model<BitString> {
151        let (p, len) = (self.p_one, self.len.max(1));
152        plate!(i in 0..len => {
153            sample(addr!("bit", i), Bernoulli::new(p).expect("valid Bernoulli prior"))
154        })
155        .map(|bits| BitString::from_bits(bits).expect("plate produced bits"))
156    }
157}
158
159/// Fisher–Yates / Lehmer-code uniform prior over permutations of `0..n`.
160///
161/// Position `i` samples a **rank** at `perm#i`, uniform over the `n−i` values
162/// not yet used (a `Usize` in `0..n−i`); the rank sequence decodes to a
163/// permutation against the shrinking available-value list. This coincides
164/// site-for-site with [`Permutation::to_trace`]'s Lehmer encoding, so scoring
165/// an existing genome's trace under replay finds every site with matching
166/// semantics, and:
167///
168/// - every model execution decodes to a valid permutation (density exactly
169///   `1/n!`), and
170/// - under single-site MH, resampling one rank always decodes to a *different
171///   valid* permutation — the rank encoding is what makes single-site moves
172///   live (a raw value encoding would turn every single-site change into a
173///   duplicate and freeze the chain).
174#[derive(Clone, Debug)]
175pub struct PermutationPrior {
176    n: usize,
177}
178
179impl PermutationPrior {
180    /// Uniform prior over permutations of `0..n`.
181    pub fn new(n: usize) -> Self {
182        assert!(n > 0, "PermutationPrior needs n > 0");
183        Self { n }
184    }
185}
186
187impl GenomePrior for PermutationPrior {
188    type Genome = Permutation;
189
190    fn model(&self) -> Model<Permutation> {
191        let n = self.n;
192        fn rank_model(n: usize, i: usize, ranks: Vec<usize>) -> Model<Vec<usize>> {
193            if i == n {
194                return fugue::pure(ranks);
195            }
196            let k = n - i;
197            let probs = vec![1.0 / k as f64; k];
198            sample(
199                addr!("perm", i),
200                Categorical::new(probs).expect("valid categorical prior"),
201            )
202            .bind(move |r| {
203                let mut ranks = ranks;
204                ranks.push(r);
205                rank_model(n, i + 1, ranks)
206            })
207        }
208        rank_model(n, 0, Vec::with_capacity(n)).map(move |ranks| {
209            let mut available: Vec<usize> = (0..n).collect();
210            let perm: Vec<usize> = ranks.into_iter().map(|r| available.remove(r)).collect();
211            Permutation::from_permutation(perm).expect("Lehmer decode produced a permutation")
212        })
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use fugue::runtime::handler::run;
220    use fugue::runtime::interpreters::{PriorHandler, ScoreGivenTrace};
221    use fugue::Trace;
222    use rand::rngs::StdRng;
223    use rand::SeedableRng;
224
225    #[test]
226    fn test_gaussian_prior_draws_match_moments() {
227        let prior = GaussianPrior::new(0.0, 2.0, 1);
228        let mut rng = StdRng::seed_from_u64(99);
229        let xs: Vec<f64> = (0..5000)
230            .map(|_| {
231                let (g, _) = run(
232                    PriorHandler {
233                        rng: &mut rng,
234                        trace: Trace::default(),
235                    },
236                    prior.model(),
237                );
238                g.genes()[0]
239            })
240            .collect();
241        let mean = xs.iter().sum::<f64>() / xs.len() as f64;
242        let var = xs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / xs.len() as f64;
243        assert!(mean.abs() < 0.2, "prior mean {}", mean);
244        assert!((var.sqrt() - 2.0).abs() < 0.2, "prior std {}", var.sqrt());
245    }
246
247    /// Replacement anchor for the deleted hand-written `log_prior_density`:
248    /// scoring a genome's trace under the prior model reproduces the analytic
249    /// Gaussian log-density.
250    #[test]
251    fn test_prior_model_log_prior_matches_analytic() {
252        let prior = GaussianPrior::new(1.0, 2.0, 3);
253        let g = RealVector::new(vec![0.5, 1.5, -2.0]);
254        let (_, scored) = run(
255            ScoreGivenTrace {
256                base: g.to_trace(),
257                trace: Trace::default(),
258            },
259            prior.model(),
260        );
261        let normal = Normal::new(1.0, 2.0).unwrap();
262        let analytic: f64 = g
263            .genes()
264            .iter()
265            .map(|x| fugue::Distribution::log_prob(&normal, x))
266            .sum();
267        assert!((scored.log_prior - analytic).abs() < 1e-12);
268    }
269
270    #[test]
271    fn test_uniform_prior_out_of_box_scores_neg_inf() {
272        let prior = UniformBoxPrior::new(MultiBounds::symmetric(1.0, 2));
273        let g = RealVector::new(vec![0.5, 5.0]); // second coordinate outside
274        let (_, scored) = run(
275            ScoreGivenTrace {
276                base: g.to_trace(),
277                trace: Trace::default(),
278            },
279            prior.model(),
280        );
281        assert_eq!(scored.log_prior, f64::NEG_INFINITY);
282    }
283
284    #[test]
285    fn test_permutation_prior_generates_valid_permutations() {
286        let prior = PermutationPrior::new(6);
287        let mut rng = StdRng::seed_from_u64(7);
288        for _ in 0..50 {
289            let (p, trace) = run(
290                PriorHandler {
291                    rng: &mut rng,
292                    trace: Trace::default(),
293                },
294                prior.model(),
295            );
296            assert!(p.is_valid_permutation());
297            // Density of any permutation is 1/n!.
298            let expected = -(720.0f64).ln(); // ln(1/6!)
299            assert!((trace.log_prior - expected).abs() < 1e-9);
300            // The trace encoding coincides with Permutation::to_trace.
301            let canonical = p.to_trace();
302            for (addr, choice) in &canonical.choices {
303                assert_eq!(trace.choices[addr].value, choice.value);
304            }
305        }
306    }
307
308    #[test]
309    fn test_bitstring_prior_scores_canonical_trace() {
310        let prior = BitStringPrior::uniform(4);
311        let g = BitString::from_bits(vec![true, false, true, true]).unwrap();
312        let (decoded, scored) = run(
313            ScoreGivenTrace {
314                base: g.to_trace(),
315                trace: Trace::default(),
316            },
317            prior.model(),
318        );
319        assert_eq!(decoded.bits(), g.bits());
320        assert!((scored.log_prior - 4.0 * (0.5f64).ln()).abs() < 1e-12);
321    }
322}