fugue/runtime/interpreters.rs
1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/docs/runtime/interpreters.md"))]
2
3use crate::core::address::Address;
4use crate::core::distribution::Distribution;
5use crate::core::model::Model;
6use crate::error::{ErrorCode, FugueError, FugueResult};
7use crate::runtime::handler::{run, Handler};
8use crate::runtime::trace::{Choice, ChoiceValue, Trace};
9
10use rand::RngCore;
11
12/// Panic when a sample site reuses an address already recorded in this
13/// execution's output trace (FG-47).
14///
15/// "Fast" handlers (`PriorHandler`, `ReplayHandler`, `ScoreGivenTrace`) treat a
16/// duplicate address as a programming error and panic with a precise message —
17/// this is the documented fast/safe split. Detection is O(log n) and needs no
18/// extra state: the output trace's `choices` map already contains exactly the
19/// addresses visited so far in this run, so a hit means the address was sampled
20/// twice. Before this check, the second visit silently double-counted its
21/// log-prior while dropping the first choice.
22#[inline]
23fn assert_no_duplicate_sample(trace: &Trace, addr: &Address, handler: &str) {
24 if trace.choices.contains_key(addr) {
25 panic!(
26 "{handler}: address {addr} was sampled twice in one execution \
27 (AddressConflict, ErrorCode::{:?}={}). Every sample site must have \
28 a unique address.",
29 ErrorCode::AddressConflict,
30 ErrorCode::AddressConflict as u32
31 );
32 }
33}
34
35/// Build the [`FugueError`] used for a duplicate sample address, so that "safe"
36/// handlers and the fallible scoring paths surface a real
37/// [`ErrorCode::AddressConflict`] (FG-47) instead of panicking.
38fn address_conflict_error(addr: &Address, handler: &str) -> FugueError {
39 FugueError::ModelError {
40 address: Some(addr.clone()),
41 reason: format!("{handler}: address sampled twice in one execution"),
42 code: ErrorCode::AddressConflict,
43 context: crate::error::ErrorContext::new(),
44 }
45}
46
47// =============================================================================
48// Internal monomorphization macros (FG-54)
49//
50// Each handler used to hand-copy four (now five, with i64) near-identical
51// `on_sample_*` / `on_observe_*` methods. These `macro_rules!` collapse that
52// copy-paste: a handler lists the `(method, type, variant)` rows once and the
53// macro expands the shared body for every supported value type. The behavior is
54// identical to the old hand-written methods, plus the FG-47 duplicate-address
55// check and the FG-48 fresh-logp fix, applied uniformly.
56// =============================================================================
57
58/// The five value types every handler supports, as `(sample_method,
59/// observe_method, rust_type, ChoiceValue variant, type-name literal,
60/// Option-getter, Result-getter)` rows. Passed to the per-handler macros so the
61/// row list lives in exactly one place.
62macro_rules! for_each_value_type {
63 ($m:ident) => {
64 $m! {
65 (on_sample_f64, on_observe_f64, f64, F64, "f64", get_f64, get_f64_result),
66 (on_sample_bool, on_observe_bool, bool, Bool, "bool", get_bool, get_bool_result),
67 (on_sample_u64, on_observe_u64, u64, U64, "u64", get_u64, get_u64_result),
68 (on_sample_usize, on_observe_usize, usize, Usize, "usize", get_usize, get_usize_result),
69 (on_sample_i64, on_observe_i64, i64, I64, "i64", get_i64, get_i64_result),
70 }
71 };
72}
73
74/// Generate the five identical `on_observe_*` methods (every handler scores an
75/// observation the same way: add its log-density to `log_likelihood`).
76macro_rules! impl_observe_methods {
77 ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
78 $(
79 fn $observe(&mut self, _addr: &Address, dist: &dyn Distribution<$ty>, value: $ty) {
80 self.trace.log_likelihood += dist.log_prob(&value);
81 }
82 )*
83 };
84}
85
86/// `PriorHandler`: draw a fresh value, score it, record it. Panics on a
87/// duplicate address (fast handler).
88macro_rules! impl_prior_sample_methods {
89 ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
90 $(
91 fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
92 assert_no_duplicate_sample(&self.trace, addr, "PriorHandler");
93 let x = dist.sample(self.rng);
94 let lp = dist.log_prob(&x);
95 self.trace.log_prior += lp;
96 self.trace.choices.insert(
97 addr.clone(),
98 Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
99 );
100 x
101 }
102 )*
103 };
104}
105
106/// `ReplayHandler`: reuse the base value if present (panic on type mismatch),
107/// otherwise sample fresh; always re-score under the current distribution.
108/// Panics on a duplicate address (fast handler).
109macro_rules! impl_replay_sample_methods {
110 ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
111 $(
112 fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
113 assert_no_duplicate_sample(&self.trace, addr, "ReplayHandler");
114 let x = if let Some(c) = self.base.choices.get(addr) {
115 match c.value {
116 ChoiceValue::$variant(v) => v,
117 _ => panic!("expected {} at {}", $tyname, addr),
118 }
119 } else {
120 dist.sample(self.rng)
121 };
122 let lp = dist.log_prob(&x);
123 self.trace.log_prior += lp;
124 self.trace.choices.insert(
125 addr.clone(),
126 Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
127 );
128 x
129 }
130 )*
131 };
132}
133
134/// `ScoreGivenTrace`: read the fixed value from the base trace (panic if
135/// missing or wrong type), score it under the current distribution, and store a
136/// FRESH choice carrying that newly computed logp (FG-48). Panics on a duplicate
137/// address (fast handler).
138macro_rules! impl_score_sample_methods {
139 ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
140 $(
141 fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
142 assert_no_duplicate_sample(&self.trace, addr, "ScoreGivenTrace");
143 let c = self
144 .base
145 .choices
146 .get(addr)
147 .unwrap_or_else(|| panic!("missing value for site {} in base trace", addr));
148 let x = match c.value {
149 ChoiceValue::$variant(v) => v,
150 _ => panic!("expected {} at {}", $tyname, addr),
151 };
152 let lp = dist.log_prob(&x);
153 self.trace.log_prior += lp;
154 // FG-48: store the freshly computed logp, not the stale base one.
155 self.trace.choices.insert(
156 addr.clone(),
157 Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
158 );
159 x
160 }
161 )*
162 };
163}
164
165/// `SafeReplayHandler`: like `ReplayHandler` but recovers from missing/mismatched
166/// base values by sampling fresh (optionally warning). A duplicate address is a
167/// programming error even in the safe handler, so it invalidates the trace with
168/// `-inf` and warns rather than silently double-counting (FG-47).
169macro_rules! impl_safe_replay_sample_methods {
170 ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
171 $(
172 fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
173 if self.trace.choices.contains_key(addr) {
174 if self.warn_on_mismatch {
175 eprintln!("Warning: {}", address_conflict_error(addr, "SafeReplayHandler"));
176 }
177 self.trace.log_prior += f64::NEG_INFINITY;
178 return dist.sample(self.rng);
179 }
180 let x = match self.base.$get(addr) {
181 Some(v) => v,
182 None => {
183 if self.warn_on_mismatch && self.base.choices.contains_key(addr) {
184 if let Some(choice) = self.base.choices.get(addr) {
185 eprintln!(
186 "Warning: Type mismatch at {}: expected {}, found {}",
187 addr, $tyname, choice.value.type_name()
188 );
189 }
190 }
191 dist.sample(self.rng)
192 }
193 };
194 let lp = dist.log_prob(&x);
195 self.trace.log_prior += lp;
196 self.trace.choices.insert(
197 addr.clone(),
198 Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
199 );
200 x
201 }
202 )*
203 };
204}
205
206/// `SafeScoreGivenTrace`: like `ScoreGivenTrace` but returns an invalid (`-inf`)
207/// trace instead of panicking on a missing/mismatched address, and stores a
208/// FRESH choice with the newly computed logp (FG-48). A duplicate address
209/// invalidates the trace (FG-47).
210macro_rules! impl_safe_score_sample_methods {
211 ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
212 $(
213 fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
214 if self.trace.choices.contains_key(addr) {
215 if self.warn_on_error {
216 eprintln!("Warning: {}", address_conflict_error(addr, "SafeScoreGivenTrace"));
217 }
218 self.trace.log_prior += f64::NEG_INFINITY;
219 return <$ty as Default>::default();
220 }
221 match self.base.$get_res(addr) {
222 Ok(x) => {
223 let lp = dist.log_prob(&x);
224 self.trace.log_prior += lp;
225 // FG-48: fresh logp consistent with what we accumulated.
226 self.trace.choices.insert(
227 addr.clone(),
228 Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
229 );
230 x
231 }
232 Err(e) => {
233 if self.warn_on_error {
234 eprintln!("Warning: Failed to get {} at {}: {}", $tyname, addr, e);
235 }
236 self.trace.log_prior += f64::NEG_INFINITY;
237 <$ty as Default>::default()
238 }
239 }
240 }
241 )*
242 };
243}
244
245/// `StrictScoreGivenTrace`: the fallible, structure-checking scoring path
246/// (FG-20/FG-21). It records the FIRST structural problem into `self.error`
247/// instead of panicking: an address absent from the base trace or a type
248/// mismatch yields `ErrorCode::UnexpectedModelStructure`; a duplicate address
249/// yields `ErrorCode::AddressConflict`. On success it stores a fresh, correctly
250/// scored choice.
251macro_rules! impl_strict_score_sample_methods {
252 ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
253 $(
254 fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
255 if self.trace.choices.contains_key(addr) {
256 if self.error.is_none() {
257 *self.error = Some(address_conflict_error(addr, "StrictScoreGivenTrace"));
258 }
259 return <$ty as Default>::default();
260 }
261 match self.base.$get_res(addr) {
262 Ok(x) => {
263 let lp = dist.log_prob(&x);
264 self.trace.log_prior += lp;
265 self.trace.choices.insert(
266 addr.clone(),
267 Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
268 );
269 x
270 }
271 Err(cause) => {
272 if self.error.is_none() {
273 *self.error = Some(FugueError::ModelError {
274 address: Some(addr.clone()),
275 reason: format!(
276 "model visited address {} not present (as {}) in the base \
277 trace; structure varies between the base trace and this model",
278 addr, $tyname
279 ),
280 code: ErrorCode::UnexpectedModelStructure,
281 context: crate::error::ErrorContext::new().with_cause(cause),
282 });
283 }
284 <$ty as Default>::default()
285 }
286 }
287 }
288 )*
289 };
290}
291
292/// `ReconcilingScoreGivenTrace`: the reconciling scoring path (FG-20/FG-21).
293/// Addresses present in the base trace are replayed and re-scored; NEW addresses
294/// (absent, or present with the wrong type) are sampled fresh from the prior and
295/// their log-prior accumulated (the RJMCMC-correct treatment of prior-proposed
296/// fresh dimensions) and recorded in `fresh`. Vanished addresses are computed
297/// after the run by the driver. A duplicate address is still an error.
298macro_rules! impl_reconciling_score_sample_methods {
299 ($(($sample:ident, $observe:ident, $ty:ty, $variant:ident, $tyname:literal, $get:ident, $get_res:ident)),* $(,)?) => {
300 $(
301 fn $sample(&mut self, addr: &Address, dist: &dyn Distribution<$ty>) -> $ty {
302 if self.trace.choices.contains_key(addr) {
303 if self.error.is_none() {
304 *self.error =
305 Some(address_conflict_error(addr, "ReconcilingScoreGivenTrace"));
306 }
307 return <$ty as Default>::default();
308 }
309 let x = match self.base.$get(addr) {
310 Some(v) => v,
311 None => {
312 // New (or type-changed) dimension: propose from the prior.
313 self.fresh.push(addr.clone());
314 dist.sample(self.rng)
315 }
316 };
317 let lp = dist.log_prob(&x);
318 self.trace.log_prior += lp;
319 self.trace.choices.insert(
320 addr.clone(),
321 Choice { addr: addr.clone(), value: ChoiceValue::$variant(x), logp: lp },
322 );
323 x
324 }
325 )*
326 };
327}
328
329/// Handler for prior sampling - generates fresh random values from distributions.
330///
331/// This is the foundational interpreter that implements standard "forward sampling"
332/// from probabilistic models. It draws fresh values from distributions and accumulates
333/// log-probabilities in the trace.
334///
335/// Example:
336/// ```rust
337/// # use fugue::*;
338/// # use fugue::runtime::interpreters::PriorHandler;
339/// # use rand::rngs::StdRng;
340/// # use rand::SeedableRng;
341///
342/// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
343/// .bind(|x| observe(addr!("y"), Normal::new(x, 0.5).unwrap(), 1.2)
344/// .map(move |_| x));
345///
346/// let mut rng = StdRng::seed_from_u64(42);
347/// let (result, trace) = runtime::handler::run(
348/// PriorHandler { rng: &mut rng, trace: Trace::default() },
349/// model
350/// );
351///
352/// assert!(result.is_finite());
353/// assert!(trace.log_likelihood.is_finite());
354/// ```
355pub struct PriorHandler<'r, R: RngCore> {
356 /// Random number generator for sampling.
357 pub rng: &'r mut R,
358 /// Trace to accumulate execution history.
359 pub trace: Trace,
360}
361impl<'r, R: RngCore> Handler for PriorHandler<'r, R> {
362 for_each_value_type!(impl_prior_sample_methods);
363 for_each_value_type!(impl_observe_methods);
364
365 fn on_factor(&mut self, logw: f64) {
366 self.trace.log_factors += logw;
367 }
368
369 fn finish(self) -> Trace {
370 self.trace
371 }
372}
373
374/// Handler for replaying models with values from an existing trace.
375///
376/// ReplayHandler replays a model execution using stored trace values. When a sampling
377/// site is encountered: if the address exists in the base trace, use that value;
378/// if missing, sample fresh. Essential for MCMC where you replay most choices
379/// but sample new values at specific sites.
380///
381/// Example:
382/// ```rust
383/// # use fugue::*;
384/// # use fugue::runtime::interpreters::*;
385/// # use rand::rngs::StdRng;
386/// # use rand::SeedableRng;
387///
388/// // Create base trace
389/// let mut rng = StdRng::seed_from_u64(42);
390/// let model_fn = || sample(addr!("x"), Normal::new(0.0, 1.0).unwrap());
391/// let (original, base_trace) = runtime::handler::run(
392/// PriorHandler { rng: &mut rng, trace: Trace::default() },
393/// model_fn()
394/// );
395///
396/// // Replay using base trace values
397/// let (replayed, _) = runtime::handler::run(
398/// ReplayHandler {
399/// rng: &mut rng,
400/// base: base_trace,
401/// trace: Trace::default()
402/// },
403/// model_fn()
404/// );
405///
406/// assert_eq!(original, replayed); // Same value replayed
407/// ```
408pub struct ReplayHandler<'r, R: RngCore> {
409 /// Random number generator for sampling at addresses not in base trace.
410 pub rng: &'r mut R,
411 /// Base trace containing values to replay.
412 pub base: Trace,
413 /// New trace to accumulate the replay execution.
414 pub trace: Trace,
415}
416impl<'r, R: RngCore> Handler for ReplayHandler<'r, R> {
417 for_each_value_type!(impl_replay_sample_methods);
418 for_each_value_type!(impl_observe_methods);
419
420 fn on_factor(&mut self, logw: f64) {
421 self.trace.log_factors += logw;
422 }
423
424 fn finish(self) -> Trace {
425 self.trace
426 }
427}
428
429/// Handler for scoring a model given a complete trace of fixed choices.
430///
431/// ScoreGivenTrace computes log-probability of a model execution where all random
432/// choices are predetermined. No sampling occurs - values are looked up from the
433/// base trace and their log-probabilities computed. Essential for MCMC acceptance
434/// ratios, importance sampling, and model comparison.
435///
436/// Example:
437/// ```rust
438/// # use fugue::*;
439/// # use fugue::runtime::interpreters::*;
440/// # use rand::rngs::StdRng;
441/// # use rand::SeedableRng;
442///
443/// // Create a complete trace
444/// let mut rng = StdRng::seed_from_u64(42);
445/// let (_, complete_trace) = runtime::handler::run(
446/// PriorHandler { rng: &mut rng, trace: Trace::default() },
447/// sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
448/// );
449///
450/// // Score under different model parameters
451/// let (value, score_trace) = runtime::handler::run(
452/// ScoreGivenTrace {
453/// base: complete_trace,
454/// trace: Trace::default()
455/// },
456/// sample(addr!("x"), Normal::new(1.0, 2.0).unwrap()) // Different parameters
457/// );
458///
459/// assert!(score_trace.total_log_weight().is_finite());
460/// ```
461pub struct ScoreGivenTrace {
462 /// Base trace containing the fixed choices to score.
463 pub base: Trace,
464 /// New trace to accumulate log-probabilities.
465 pub trace: Trace,
466}
467impl Handler for ScoreGivenTrace {
468 for_each_value_type!(impl_score_sample_methods);
469 for_each_value_type!(impl_observe_methods);
470
471 fn on_factor(&mut self, logw: f64) {
472 self.trace.log_factors += logw;
473 }
474
475 fn finish(self) -> Trace {
476 self.trace
477 }
478}
479
480/// Safe version of ReplayHandler that gracefully handles trace inconsistencies.
481///
482/// SafeReplayHandler replays model execution like ReplayHandler, but handles type
483/// mismatches and missing addresses gracefully by logging warnings and sampling
484/// fresh values instead of panicking. Essential for production systems where
485/// trace consistency cannot be guaranteed.
486///
487/// Example:
488/// ```rust
489/// # use fugue::*;
490/// # use fugue::runtime::interpreters::*;
491/// # use rand::rngs::StdRng;
492/// # use rand::SeedableRng;
493///
494/// // Create trace with potential inconsistencies
495/// let mut rng = StdRng::seed_from_u64(42);
496/// let (_, base_trace) = runtime::handler::run(
497/// PriorHandler { rng: &mut rng, trace: Trace::default() },
498/// sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()) // f64 value
499/// );
500///
501/// // Safe replay handles type mismatch gracefully
502/// let (result, trace) = runtime::handler::run(
503/// SafeReplayHandler {
504/// rng: &mut rng,
505/// base: base_trace,
506/// trace: Trace::default(),
507/// warn_on_mismatch: true, // Enable warnings
508/// },
509/// sample(addr!("x"), Bernoulli::new(0.5).unwrap()) // Expects bool
510/// );
511///
512/// assert!(trace.total_log_weight().is_finite()); // Continues execution
513/// ```
514pub struct SafeReplayHandler<'r, R: RngCore> {
515 /// Random number generator for sampling at addresses not in base trace.
516 pub rng: &'r mut R,
517 /// Base trace containing values to replay.
518 pub base: Trace,
519 /// New trace to accumulate the replay execution.
520 pub trace: Trace,
521 /// Whether to log warnings on type mismatches (useful for debugging).
522 pub warn_on_mismatch: bool,
523}
524impl<'r, R: RngCore> Handler for SafeReplayHandler<'r, R> {
525 for_each_value_type!(impl_safe_replay_sample_methods);
526 for_each_value_type!(impl_observe_methods);
527
528 fn on_factor(&mut self, logw: f64) {
529 self.trace.log_factors += logw;
530 }
531
532 fn finish(self) -> Trace {
533 self.trace
534 }
535}
536
537/// Safe version of ScoreGivenTrace that gracefully handles incomplete traces.
538///
539/// SafeScoreGivenTrace computes log-probability like ScoreGivenTrace, but handles
540/// missing addresses or type mismatches by returning negative infinity log-weight
541/// instead of panicking. Essential for production inference where trace validity
542/// cannot be guaranteed.
543///
544/// Example:
545/// ```rust
546/// # use fugue::*;
547/// # use fugue::runtime::interpreters::*;
548/// # use rand::rngs::StdRng;
549/// # use rand::SeedableRng;
550///
551/// // Create incomplete trace
552/// let mut rng = StdRng::seed_from_u64(42);
553/// let (_, incomplete_trace) = runtime::handler::run(
554/// PriorHandler { rng: &mut rng, trace: Trace::default() },
555/// sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()) // Only has "x"
556/// );
557///
558/// // Safe scoring handles missing address gracefully
559/// let (_, score_trace) = runtime::handler::run(
560/// SafeScoreGivenTrace {
561/// base: incomplete_trace,
562/// trace: Trace::default(),
563/// warn_on_error: true, // Enable warnings
564/// },
565/// sample(addr!("missing"), Normal::new(0.0, 1.0).unwrap()) // Address not in base
566/// );
567///
568/// assert_eq!(score_trace.total_log_weight(), f64::NEG_INFINITY); // Graceful failure
569/// ```
570pub struct SafeScoreGivenTrace {
571 /// Base trace containing the fixed choices to score.
572 pub base: Trace,
573 /// New trace to accumulate log-probabilities.
574 pub trace: Trace,
575 /// Whether to log warnings on missing addresses or type mismatches.
576 pub warn_on_error: bool,
577}
578impl Handler for SafeScoreGivenTrace {
579 for_each_value_type!(impl_safe_score_sample_methods);
580 for_each_value_type!(impl_observe_methods);
581
582 fn on_factor(&mut self, logw: f64) {
583 self.trace.log_factors += logw;
584 }
585
586 fn finish(self) -> Trace {
587 self.trace
588 }
589}
590
591// =============================================================================
592// Structure-varying (trans-dimensional) scoring paths (FG-20 / FG-21)
593// =============================================================================
594
595/// The strict, fallible sibling of [`ScoreGivenTrace`].
596///
597/// This handler backs [`score_given_trace_strict`]. Instead of panicking when
598/// the model's address structure differs from the base trace, it records the
599/// first structural problem so the driver can return a [`FugueError`]:
600///
601/// - visiting an address absent from the base trace (or present with the wrong
602/// value type) -> [`ErrorCode::UnexpectedModelStructure`];
603/// - visiting the same address twice -> [`ErrorCode::AddressConflict`].
604///
605/// On success every visited site stores a fresh, correctly scored choice.
606pub struct StrictScoreGivenTrace<'e> {
607 /// Base trace containing the fixed choices to score.
608 pub base: Trace,
609 /// New trace to accumulate log-probabilities.
610 pub trace: Trace,
611 /// First structural error encountered, surfaced by the driver.
612 error: &'e mut Option<FugueError>,
613}
614impl<'e> Handler for StrictScoreGivenTrace<'e> {
615 for_each_value_type!(impl_strict_score_sample_methods);
616 for_each_value_type!(impl_observe_methods);
617
618 fn on_factor(&mut self, logw: f64) {
619 self.trace.log_factors += logw;
620 }
621
622 fn finish(self) -> Trace {
623 self.trace
624 }
625}
626
627/// Score `model` against `base` strictly, returning an error rather than
628/// panicking when the model's address structure does not match the base trace
629/// (FG-20 / FG-21).
630///
631/// Returns `Ok((value, scored_trace))` when every sample site visited by the
632/// model is present in `base` with a matching value type. Returns `Err` with
633/// [`ErrorCode::UnexpectedModelStructure`] if the model visits an address absent
634/// from `base` (a branch opened by a differing latent value), or
635/// [`ErrorCode::AddressConflict`] if the model visits the same address twice.
636///
637/// This is the mechanism the MCMC layer needs to stop crashing on
638/// structure-varying proposals; wiring it into the samplers is a separate work
639/// package.
640///
641/// # Example
642///
643/// ```rust
644/// # use fugue::*;
645/// # use fugue::runtime::interpreters::{score_given_trace_strict, PriorHandler};
646/// # use rand::rngs::StdRng;
647/// # use rand::SeedableRng;
648/// let mut rng = StdRng::seed_from_u64(1);
649/// let (_, base) = runtime::handler::run(
650/// PriorHandler { rng: &mut rng, trace: Trace::default() },
651/// sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
652/// );
653///
654/// // Same structure: Ok.
655/// assert!(score_given_trace_strict(
656/// base.clone(),
657/// sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
658/// ).is_ok());
659///
660/// // Model reaches an address the base trace never recorded: Err.
661/// let err = score_given_trace_strict(
662/// base,
663/// sample(addr!("y"), Normal::new(0.0, 1.0).unwrap()),
664/// ).unwrap_err();
665/// assert_eq!(err.code(), ErrorCode::UnexpectedModelStructure);
666/// ```
667pub fn score_given_trace_strict<A>(base: Trace, model: Model<A>) -> FugueResult<(A, Trace)> {
668 let mut error: Option<FugueError> = None;
669 let handler = StrictScoreGivenTrace {
670 base,
671 trace: Trace::default(),
672 error: &mut error,
673 };
674 let (a, trace) = run(handler, model);
675 match error {
676 Some(e) => Err(e),
677 None => Ok((a, trace)),
678 }
679}
680
681/// Report of the structural differences reconciled by
682/// [`score_given_trace_reconciled`].
683#[derive(Clone, Debug, Default, PartialEq)]
684pub struct ReconcileReport {
685 /// Addresses visited by the model that were absent from the base trace (or
686 /// present with the wrong value type). Each was proposed fresh from its
687 /// prior and its `log_prob` accumulated into `log_prior`.
688 pub fresh_addresses: Vec<Address>,
689 /// Addresses present in the base trace that the model did NOT visit. Their
690 /// contribution should be dropped by the caller (e.g. removed from the
691 /// reverse-move density in an RJMCMC step).
692 pub vanished_addresses: Vec<Address>,
693}
694
695/// The reconciling sibling of [`ScoreGivenTrace`], backing
696/// [`score_given_trace_reconciled`].
697///
698/// Addresses present in the base trace are replayed and re-scored. NEW addresses
699/// (absent, or present with a different value type) are sampled fresh from the
700/// prior, their `log_prob` accumulated into `log_prior`, and recorded in
701/// `fresh`. A duplicate address is still an error.
702pub struct ReconcilingScoreGivenTrace<'r, 'f, 'e, R: RngCore> {
703 /// RNG used to propose fresh values for new addresses.
704 pub rng: &'r mut R,
705 /// Base trace containing the fixed choices to replay/score.
706 pub base: Trace,
707 /// New trace accumulating the reconciled execution.
708 pub trace: Trace,
709 /// Addresses sampled fresh from the prior (not present in `base`), in
710 /// visitation order. Borrowed so the driver can read it after `finish`.
711 fresh: &'f mut Vec<Address>,
712 /// First duplicate-address error encountered, surfaced by the driver.
713 error: &'e mut Option<FugueError>,
714}
715impl<'r, 'f, 'e, R: RngCore> Handler for ReconcilingScoreGivenTrace<'r, 'f, 'e, R> {
716 for_each_value_type!(impl_reconciling_score_sample_methods);
717 for_each_value_type!(impl_observe_methods);
718
719 fn on_factor(&mut self, logw: f64) {
720 self.trace.log_factors += logw;
721 }
722
723 fn finish(self) -> Trace {
724 self.trace
725 }
726}
727
728/// Score `model` against `base`, reconciling a differing address structure
729/// instead of panicking (FG-20 / FG-21).
730///
731/// Addresses shared with `base` are replayed and re-scored under the current
732/// model. Addresses the model introduces that are **not** in `base` (new
733/// branches) are sampled fresh from their prior and their log-prior accumulated
734/// — the RJMCMC-correct treatment of prior-proposed fresh dimensions. Addresses
735/// in `base` that the model does **not** visit are reported as
736/// [`ReconcileReport::vanished_addresses`] so the caller can drop their
737/// contribution.
738///
739/// Returns `Err` with [`ErrorCode::AddressConflict`] only if the model visits
740/// the same address twice.
741///
742/// # Example
743///
744/// ```rust
745/// # use fugue::*;
746/// # use fugue::runtime::interpreters::{score_given_trace_reconciled, PriorHandler};
747/// # use rand::rngs::StdRng;
748/// # use rand::SeedableRng;
749/// let mut rng = StdRng::seed_from_u64(7);
750/// let (_, base) = runtime::handler::run(
751/// PriorHandler { rng: &mut rng, trace: Trace::default() },
752/// sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
753/// );
754///
755/// // Model drops "x" and introduces "y": "y" is proposed fresh, "x" vanished.
756/// let (_v, trace, report) = score_given_trace_reconciled(
757/// base,
758/// &mut rng,
759/// sample(addr!("y"), Normal::new(0.0, 1.0).unwrap()),
760/// ).unwrap();
761/// assert_eq!(report.fresh_addresses, vec![addr!("y")]);
762/// assert_eq!(report.vanished_addresses, vec![addr!("x")]);
763/// assert!(trace.log_prior.is_finite());
764/// ```
765pub fn score_given_trace_reconciled<A, R: RngCore>(
766 base: Trace,
767 rng: &mut R,
768 model: Model<A>,
769) -> FugueResult<(A, Trace, ReconcileReport)> {
770 let mut error: Option<FugueError> = None;
771 let mut fresh_addresses: Vec<Address> = Vec::new();
772 // Snapshot base addresses up front so we can compute vanished ones after the
773 // run consumes the handler.
774 let base_addresses: Vec<Address> = base.choices.keys().cloned().collect();
775 let handler = ReconcilingScoreGivenTrace {
776 rng,
777 base,
778 trace: Trace::default(),
779 fresh: &mut fresh_addresses,
780 error: &mut error,
781 };
782 let (a, trace) = run(handler, model);
783 if let Some(e) = error {
784 return Err(e);
785 }
786 // Vanished = present in the base trace but not visited by this model run.
787 let vanished_addresses: Vec<Address> = base_addresses
788 .into_iter()
789 .filter(|addr| !trace.choices.contains_key(addr))
790 .collect();
791 Ok((
792 a,
793 trace,
794 ReconcileReport {
795 fresh_addresses,
796 vanished_addresses,
797 },
798 ))
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use crate::addr;
805 use crate::core::distribution::*;
806 use crate::core::model::{observe, sample, ModelExt};
807 use rand::rngs::StdRng;
808 use rand::SeedableRng;
809
810 #[test]
811 fn prior_handler_samples_and_accumulates() {
812 let mut rng = StdRng::seed_from_u64(7);
813 let (_val, trace) = crate::runtime::handler::run(
814 PriorHandler {
815 rng: &mut rng,
816 trace: Trace::default(),
817 },
818 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
819 .and_then(|x| observe(addr!("y"), Normal::new(x, 1.0).unwrap(), 0.5)),
820 );
821 assert!(trace.choices.contains_key(&addr!("x")));
822 assert!(trace.log_prior.is_finite());
823 assert!(trace.log_likelihood.is_finite());
824 }
825
826 #[test]
827 fn replay_handler_reuses_values() {
828 let mut rng = StdRng::seed_from_u64(8);
829 let ((), base) = crate::runtime::handler::run(
830 PriorHandler {
831 rng: &mut rng,
832 trace: Trace::default(),
833 },
834 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()).map(|_| ()),
835 );
836
837 let ((), replayed) = crate::runtime::handler::run(
838 ReplayHandler {
839 rng: &mut rng,
840 base: base.clone(),
841 trace: Trace::default(),
842 },
843 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()).map(|_| ()),
844 );
845
846 let x_base = base.get_f64(&addr!("x")).unwrap();
847 let x_replay = replayed.get_f64(&addr!("x")).unwrap();
848 assert_eq!(x_base, x_replay);
849 }
850
851 #[test]
852 fn score_given_trace_scores_fixed_values() {
853 let mut rng = StdRng::seed_from_u64(9);
854 let (_a, base) = crate::runtime::handler::run(
855 PriorHandler {
856 rng: &mut rng,
857 trace: Trace::default(),
858 },
859 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
860 );
861
862 let (_a2, scored) = crate::runtime::handler::run(
863 ScoreGivenTrace {
864 base: base.clone(),
865 trace: Trace::default(),
866 },
867 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
868 );
869
870 // Should have same value and finite log_prior
871 assert_eq!(scored.get_f64(&addr!("x")), base.get_f64(&addr!("x")));
872 assert!(scored.log_prior.is_finite());
873 }
874
875 #[test]
876 fn safe_variants_handle_mismatches() {
877 // Build base trace with x as f64, then attempt to replay as bool
878 let mut rng = StdRng::seed_from_u64(10);
879 let (_a, base) = crate::runtime::handler::run(
880 PriorHandler {
881 rng: &mut rng,
882 trace: Trace::default(),
883 },
884 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
885 );
886
887 // SafeReplayHandler should sample fresh value for bool and continue
888 let (_b, t1) = crate::runtime::handler::run(
889 SafeReplayHandler {
890 rng: &mut rng,
891 base: base.clone(),
892 trace: Trace::default(),
893 warn_on_mismatch: true,
894 },
895 sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
896 );
897 assert!(t1.log_prior.is_finite());
898
899 // SafeScoreGivenTrace should mark as invalid by adding -inf
900 let (_c, t2) = crate::runtime::handler::run(
901 SafeScoreGivenTrace {
902 base: base.clone(),
903 trace: Trace::default(),
904 warn_on_error: true,
905 },
906 sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
907 );
908 assert!(t2.log_prior.is_infinite());
909 }
910
911 #[test]
912 fn handlers_cover_all_types_sample_and_observe() {
913 // Model with multiple types
914 let model = sample(addr!("f"), Normal::new(0.0, 1.0).unwrap())
915 .and_then(|_| sample(addr!("b"), Bernoulli::new(0.6).unwrap()))
916 .and_then(|_| sample(addr!("u64"), Poisson::new(3.0).unwrap()))
917 .and_then(|_| sample(addr!("usz"), Categorical::new(vec![0.3, 0.7]).unwrap()))
918 .and_then(|_| observe(addr!("f_obs"), Normal::new(0.0, 1.0).unwrap(), 0.1))
919 .and_then(|_| observe(addr!("b_obs"), Bernoulli::new(0.4).unwrap(), true))
920 .and_then(|_| observe(addr!("u64_obs"), Poisson::new(2.0).unwrap(), 1))
921 .and_then(|_| {
922 observe(
923 addr!("usz_obs"),
924 Categorical::new(vec![0.5, 0.5]).unwrap(),
925 1,
926 )
927 });
928
929 let (_a, t) = crate::runtime::handler::run(
930 PriorHandler {
931 rng: &mut StdRng::seed_from_u64(100),
932 trace: Trace::default(),
933 },
934 model,
935 );
936 assert!(t.get_f64(&addr!("f")).is_some());
937 assert!(t.get_bool(&addr!("b")).is_some());
938 assert!(t.get_u64(&addr!("u64")).is_some());
939 assert!(t.get_usize(&addr!("usz")).is_some());
940 assert!(t.log_likelihood.is_finite());
941
942 // Build base and score given trace for all types
943 let base = t.clone();
944 let (_sv, scored) = crate::runtime::handler::run(
945 ScoreGivenTrace {
946 base: base.clone(),
947 trace: Trace::default(),
948 },
949 sample(addr!("f"), Normal::new(0.0, 1.0).unwrap())
950 .and_then(|_| sample(addr!("b"), Bernoulli::new(0.6).unwrap()))
951 .and_then(|_| sample(addr!("u64"), Poisson::new(3.0).unwrap()))
952 .and_then(|_| sample(addr!("usz"), Categorical::new(vec![0.3, 0.7]).unwrap())),
953 );
954 assert!(scored.log_prior.is_finite());
955
956 // Safe replay mismatches for integer/categorical types
957 let (_sv2, safe) = crate::runtime::handler::run(
958 SafeReplayHandler {
959 rng: &mut StdRng::seed_from_u64(101),
960 base: base.clone(),
961 trace: Trace::default(),
962 warn_on_mismatch: true,
963 },
964 sample(addr!("u64"), Bernoulli::new(0.5).unwrap()),
965 );
966 assert!(safe.log_prior.is_finite());
967 }
968
969 #[test]
970 fn safe_score_given_trace_warn_flag_branches() {
971 let mut rng = StdRng::seed_from_u64(102);
972 let (_a, base) = crate::runtime::handler::run(
973 PriorHandler {
974 rng: &mut rng,
975 trace: Trace::default(),
976 },
977 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
978 );
979 // warn_on_error = false
980 let (_b, t_false) = crate::runtime::handler::run(
981 SafeScoreGivenTrace {
982 base: base.clone(),
983 trace: Trace::default(),
984 warn_on_error: false,
985 },
986 sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
987 );
988 assert!(t_false.log_prior.is_infinite());
989
990 // warn_on_error = true
991 let (_c, t_true) = crate::runtime::handler::run(
992 SafeScoreGivenTrace {
993 base: base.clone(),
994 trace: Trace::default(),
995 warn_on_error: true,
996 },
997 sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
998 );
999 assert!(t_true.log_prior.is_infinite());
1000 }
1001
1002 #[test]
1003 #[should_panic]
1004 fn replay_handler_panics_on_type_mismatch() {
1005 // Base has f64, replay expects bool -> panic as designed
1006 let mut rng = StdRng::seed_from_u64(103);
1007 let (_a, base) = crate::runtime::handler::run(
1008 PriorHandler {
1009 rng: &mut rng,
1010 trace: Trace::default(),
1011 },
1012 sample(addr!("x"), Normal::new(0.0, 1.0).unwrap()),
1013 );
1014 let (_b, _t) = crate::runtime::handler::run(
1015 ReplayHandler {
1016 rng: &mut rng,
1017 base: base.clone(),
1018 trace: Trace::default(),
1019 },
1020 sample(addr!("x"), Bernoulli::new(0.5).unwrap()),
1021 );
1022 }
1023
1024 #[test]
1025 fn safe_replay_handler_samples_fresh_for_missing_address() {
1026 let mut rng = StdRng::seed_from_u64(104);
1027 // Base trace without address "z"
1028 let base = Trace::default();
1029 let (_a, t) = crate::runtime::handler::run(
1030 SafeReplayHandler {
1031 rng: &mut rng,
1032 base,
1033 trace: Trace::default(),
1034 warn_on_mismatch: true,
1035 },
1036 sample(addr!("z"), Normal::new(0.0, 1.0).unwrap()),
1037 );
1038 assert!(t.get_f64(&addr!("z")).is_some());
1039 }
1040}