fugue_evo/inference/
mh.rs1use std::collections::HashMap;
13
14use fugue::inference::mcmc_utils::DiminishingAdaptation;
15use fugue::{
16 adaptive_mcmc_chain_with_overrides, adaptive_single_site_mh, Address, SiteProposal, Trace,
17};
18use rand::Rng;
19
20use super::likelihood::GenomeLikelihood;
21use super::model::EvolutionModel;
22use super::prior::GenomePrior;
23
24pub struct EvolutionChain<P, L>
26where
27 P: GenomePrior,
28 L: GenomeLikelihood<P::Genome>,
29{
30 model: EvolutionModel<P, L>,
31 adaptation: DiminishingAdaptation,
32 overrides: HashMap<Address, SiteProposal>,
33}
34
35impl<P, L> EvolutionChain<P, L>
36where
37 P: GenomePrior,
38 L: GenomeLikelihood<P::Genome>,
39{
40 pub fn new(model: EvolutionModel<P, L>) -> Self {
42 Self {
43 model,
44 adaptation: DiminishingAdaptation::new(0.44, 0.7),
45 overrides: HashMap::new(),
46 }
47 }
48
49 pub fn target_rate(mut self, rate: f64) -> Self {
51 self.adaptation = DiminishingAdaptation::new(rate, 0.7);
52 self
53 }
54
55 pub fn override_site(mut self, addr: Address, proposal: SiteProposal) -> Self {
58 self.overrides.insert(addr, proposal);
59 self
60 }
61
62 pub fn model(&self) -> &EvolutionModel<P, L> {
64 &self.model
65 }
66
67 pub fn init<R: Rng>(&self, rng: &mut R) -> Trace {
69 use fugue::runtime::handler::run;
70 use fugue::runtime::interpreters::PriorHandler;
71 let (_g, trace) = run(
72 PriorHandler {
73 rng,
74 trace: Trace::default(),
75 },
76 (self.model.target_model())(),
77 );
78 trace
79 }
80
81 pub fn init_from(&self, genome: &P::Genome) -> Option<Trace> {
88 let (_g, trace) = self.model.score(genome);
89 if trace.total_log_weight().is_finite() {
90 Some(trace)
91 } else {
92 None
93 }
94 }
95
96 pub fn step<R: Rng>(&mut self, rng: &mut R, current: &Trace) -> (P::Genome, Trace) {
98 adaptive_single_site_mh(
99 rng,
100 self.model.target_model(),
101 current,
102 &mut self.adaptation,
103 )
104 }
105
106 pub fn run_chain<R: Rng>(&self, rng: &mut R, n: usize, warmup: usize) -> Vec<(P::Genome, Trace)>
110 where
111 P::Genome: Clone,
112 {
113 adaptive_mcmc_chain_with_overrides(
114 rng,
115 self.model.target_model(),
116 n,
117 warmup,
118 &self.overrides,
119 )
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::fitness::traits::Fitness;
127 use crate::genome::bounds::{Bounds, MultiBounds};
128 use crate::genome::real_vector::RealVector;
129 use crate::genome::traits::{BinaryGenome, PermutationGenome, RealValuedGenome};
130 use crate::inference::model::tests::PtrFitness;
131 use crate::inference::prior::{BitStringPrior, PermutationPrior, UniformBoxPrior};
132 use rand::rngs::StdRng;
133 use rand::SeedableRng;
134
135 fn linear_x0(g: &RealVector) -> f64 {
136 g.genes()[0]
137 }
138
139 #[test]
145 fn test_mh_respects_bounds() {
146 let prior = UniformBoxPrior::new(MultiBounds::new(vec![Bounds::new(-2.0, 2.0)]));
147 let model = EvolutionModel::new(prior, PtrFitness(linear_x0)).with_beta(1.0);
148 let mut chain = EvolutionChain::new(model);
149
150 let mut rng = StdRng::seed_from_u64(20260710);
151 let mut current = chain.init(&mut rng);
152 let mut samples = Vec::new();
153 for i in 0..40_000 {
154 let (g, t) = chain.step(&mut rng, ¤t);
155 current = t;
156 let x = g.genes()[0];
157 assert!((-2.0..=2.0).contains(&x), "MH sample escaped bounds: {}", x);
158 if i >= 5_000 {
159 samples.push(x);
160 }
161 }
162 let mean = samples.iter().sum::<f64>() / samples.len() as f64;
163 let analytic = {
164 let e2 = 2.0_f64.exp();
165 let em2 = (-2.0_f64).exp();
166 (e2 + 3.0 * em2) / (e2 - em2)
167 };
168 assert!(
169 (mean - analytic).abs() < 0.1,
170 "posterior mean {} deviates from truncated-exponential analytic {}",
171 mean,
172 analytic
173 );
174 }
175
176 #[test]
180 fn test_bitstring_chain_moves() {
181 #[derive(Clone, Copy)]
182 struct OnesCount;
183 impl Fitness for OnesCount {
184 type Genome = crate::genome::bit_string::BitString;
185 type Value = f64;
186 fn evaluate(&self, g: &Self::Genome) -> f64 {
187 g.bits().iter().filter(|&&b| b).count() as f64
188 }
189 }
190
191 let model = EvolutionModel::new(BitStringPrior::uniform(8), OnesCount).with_beta(1.0);
192 let mut chain = EvolutionChain::new(model);
193 let mut rng = StdRng::seed_from_u64(11);
194 let init = chain.init(&mut rng);
195 let init_bits: Vec<Option<bool>> = (0..8)
196 .map(|i| init.get_bool(&fugue::addr!("bit", i)))
197 .collect();
198
199 let mut current = init.clone();
200 let mut moved = false;
201 for _ in 0..200 {
202 let (_g, t) = chain.step(&mut rng, ¤t);
203 current = t;
204 let bits: Vec<Option<bool>> = (0..8)
205 .map(|i| current.get_bool(&fugue::addr!("bit", i)))
206 .collect();
207 if bits != init_bits {
208 moved = true;
209 break;
210 }
211 }
212 assert!(moved, "BitString chain never moved (dead-chain regression)");
213 }
214
215 #[test]
219 fn test_permutation_chain_moves() {
220 #[derive(Clone, Copy)]
221 struct SortedNess;
222 impl Fitness for SortedNess {
223 type Genome = crate::genome::permutation::Permutation;
224 type Value = f64;
225 fn evaluate(&self, g: &Self::Genome) -> f64 {
226 g.permutation().windows(2).filter(|w| w[0] < w[1]).count() as f64
228 }
229 }
230
231 let model = EvolutionModel::new(PermutationPrior::new(5), SortedNess).with_beta(1.0);
232 let mut chain = EvolutionChain::new(model);
233 let mut rng = StdRng::seed_from_u64(17);
234 let init = chain.init(&mut rng);
235 let read_perm = |t: &Trace| -> Vec<usize> {
236 (0..5)
237 .map(|i| t.get_usize(&fugue::addr!("perm", i)).unwrap())
238 .collect()
239 };
240 let init_perm = read_perm(&init);
241
242 let mut current = init;
243 let mut moved = false;
244 for _ in 0..500 {
245 let (g, t) = chain.step(&mut rng, ¤t);
246 current = t;
247 assert!(
248 g.is_valid_permutation(),
249 "chain left the permutation support: {:?}",
250 g.permutation()
251 );
252 if read_perm(¤t) != init_perm {
253 moved = true;
254 }
255 }
256 assert!(
257 moved,
258 "Permutation chain never moved (dead-chain regression)"
259 );
260 }
261}