fugue/core/model.rs
1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/docs/core/model.md"))]
2use crate::core::address::Address;
3use crate::core::distribution::{Distribution, LogF64};
4
5/// `Model<A>` represents a probabilistic program that yields a value of type `A` when executed by a handler.
6/// Models are built from four variants: `Pure`, `Sample*`, `Observe*`, and `Factor`.
7///
8/// Example:
9/// ```rust
10/// # use fugue::*;
11/// // Deterministic value
12/// let m = pure(42.0);
13///
14/// // Sample from distribution
15/// let s = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap());
16///
17/// // Dependent sampling
18/// let chain = s.bind(|x| sample(addr!("y"), Normal::new(x, 0.5).unwrap()));
19/// ```
20pub enum Model<A> {
21 /// A deterministic computation yielding a pure value.
22 Pure(A),
23 /// Sample from an f64 distribution (continuous distributions).
24 SampleF64 {
25 /// Unique identifier for this sampling site.
26 addr: Address,
27 /// Distribution to sample from.
28 dist: Box<dyn Distribution<f64>>,
29 /// Continuation function to apply to the sampled value.
30 k: Box<dyn FnOnce(f64) -> Model<A> + Send + 'static>,
31 },
32 /// Sample from a bool distribution (Bernoulli).
33 SampleBool {
34 /// Unique identifier for this sampling site.
35 addr: Address,
36 /// Distribution to sample from.
37 dist: Box<dyn Distribution<bool>>,
38 /// Continuation function to apply to the sampled value.
39 k: Box<dyn FnOnce(bool) -> Model<A> + Send + 'static>,
40 },
41 /// Sample from a u64 distribution (Poisson, Binomial).
42 SampleU64 {
43 /// Unique identifier for this sampling site.
44 addr: Address,
45 /// Distribution to sample from.
46 dist: Box<dyn Distribution<u64>>,
47 /// Continuation function to apply to the sampled value.
48 k: Box<dyn FnOnce(u64) -> Model<A> + Send + 'static>,
49 },
50 /// Sample from a usize distribution (Categorical).
51 SampleUsize {
52 /// Unique identifier for this sampling site.
53 addr: Address,
54 /// Distribution to sample from.
55 dist: Box<dyn Distribution<usize>>,
56 /// Continuation function to apply to the sampled value.
57 k: Box<dyn FnOnce(usize) -> Model<A> + Send + 'static>,
58 },
59 /// Sample from an i64 distribution (signed discrete distributions, e.g. a
60 /// future `DiscreteUniform` over a signed range).
61 SampleI64 {
62 /// Unique identifier for this sampling site.
63 addr: Address,
64 /// Distribution to sample from.
65 dist: Box<dyn Distribution<i64>>,
66 /// Continuation function to apply to the sampled value.
67 k: Box<dyn FnOnce(i64) -> Model<A> + Send + 'static>,
68 },
69 /// Observe/condition on an f64 value.
70 ObserveF64 {
71 /// Unique identifier for this observation site.
72 addr: Address,
73 /// Distribution that generates the observed value.
74 dist: Box<dyn Distribution<f64>>,
75 /// The observed value to condition on.
76 value: f64,
77 /// Continuation function (always receives unit).
78 k: Box<dyn FnOnce(()) -> Model<A> + Send + 'static>,
79 },
80 /// Observe/condition on a bool value.
81 ObserveBool {
82 /// Unique identifier for this observation site.
83 addr: Address,
84 /// Distribution that generates the observed value.
85 dist: Box<dyn Distribution<bool>>,
86 /// The observed value to condition on.
87 value: bool,
88 /// Continuation function (always receives unit).
89 k: Box<dyn FnOnce(()) -> Model<A> + Send + 'static>,
90 },
91 /// Observe/condition on a u64 value.
92 ObserveU64 {
93 /// Unique identifier for this observation site.
94 addr: Address,
95 /// Distribution that generates the observed value.
96 dist: Box<dyn Distribution<u64>>,
97 /// The observed value to condition on.
98 value: u64,
99 /// Continuation function (always receives unit).
100 k: Box<dyn FnOnce(()) -> Model<A> + Send + 'static>,
101 },
102 /// Observe/condition on a usize value.
103 ObserveUsize {
104 /// Unique identifier for this observation site.
105 addr: Address,
106 /// Distribution that generates the observed value.
107 dist: Box<dyn Distribution<usize>>,
108 /// The observed value to condition on.
109 value: usize,
110 /// Continuation function (always receives unit).
111 k: Box<dyn FnOnce(()) -> Model<A> + Send + 'static>,
112 },
113 /// Observe/condition on an i64 value.
114 ObserveI64 {
115 /// Unique identifier for this observation site.
116 addr: Address,
117 /// Distribution that generates the observed value.
118 dist: Box<dyn Distribution<i64>>,
119 /// The observed value to condition on.
120 value: i64,
121 /// Continuation function (always receives unit).
122 k: Box<dyn FnOnce(()) -> Model<A> + Send + 'static>,
123 },
124 /// Add a log-weight factor to the model.
125 Factor {
126 /// Log-weight to add to the model's total weight.
127 logw: LogF64,
128 /// Continuation function (always receives unit).
129 k: Box<dyn FnOnce(()) -> Model<A> + Send + 'static>,
130 },
131}
132
133/// Lift a deterministic value, `a`, into the model monad.
134/// Creates a `Model` that always returns the given value, `a`, without any probabilistic behavior.
135/// This is the unit operation for the model monad.
136///
137/// Example:
138/// ```rust
139/// # use fugue::*;
140///
141/// let model = pure(42.0);
142/// // When executed, this model will always return 42.0
143/// ```
144pub fn pure<A>(a: A) -> Model<A> {
145 Model::Pure(a)
146}
147/// Sample from an f64 distribution (continuous distributions).
148///
149/// Example:
150/// ```rust
151/// # use fugue::*;
152///
153/// let model = sample_f64(addr!("x"), Normal::new(0.0, 1.0).unwrap());
154/// ```
155pub fn sample_f64(addr: Address, dist: impl Distribution<f64> + 'static) -> Model<f64> {
156 Model::SampleF64 {
157 addr,
158 dist: Box::new(dist),
159 k: Box::new(pure),
160 }
161}
162/// Sample from a bool distribution (Bernoulli).
163///
164/// Example:
165/// ```rust
166/// # use fugue::*;
167///
168/// let model = sample_bool(addr!("coin"), Bernoulli::new(0.5).unwrap());
169/// ```
170pub fn sample_bool(addr: Address, dist: impl Distribution<bool> + 'static) -> Model<bool> {
171 Model::SampleBool {
172 addr,
173 dist: Box::new(dist),
174 k: Box::new(pure),
175 }
176}
177/// Sample from a u64 distribution (Poisson, Binomial).
178///
179/// Example:
180/// ```rust
181/// # use fugue::*;
182///
183/// let model = sample_u64(addr!("count"), Poisson::new(3.0).unwrap());
184/// ```
185pub fn sample_u64(addr: Address, dist: impl Distribution<u64> + 'static) -> Model<u64> {
186 Model::SampleU64 {
187 addr,
188 dist: Box::new(dist),
189 k: Box::new(pure),
190 }
191}
192/// Sample from a usize distribution (Categorical).
193///
194/// Example:
195/// ```rust
196/// # use fugue::*;
197///
198/// let model = sample_usize(addr!("choice"), Categorical::new(vec![0.3, 0.5, 0.2]).unwrap());
199/// ```
200pub fn sample_usize(addr: Address, dist: impl Distribution<usize> + 'static) -> Model<usize> {
201 Model::SampleUsize {
202 addr,
203 dist: Box::new(dist),
204 k: Box::new(pure),
205 }
206}
207
208/// Sample from an i64 distribution (signed discrete distributions).
209///
210/// Example:
211/// ```rust
212/// # use fugue::*;
213/// # use fugue::core::model::sample_i64;
214/// # use fugue::core::distribution::Distribution;
215/// # use rand::RngCore;
216/// // A tiny signed-discrete distribution (a real `DiscreteUniform` lands in a
217/// // later work package); shown here only to illustrate the i64 sample path.
218/// #[derive(Clone)]
219/// struct AlwaysZero;
220/// impl Distribution<i64> for AlwaysZero {
221/// fn sample(&self, _rng: &mut dyn RngCore) -> i64 { 0 }
222/// fn log_prob(&self, x: &i64) -> f64 { if *x == 0 { 0.0 } else { f64::NEG_INFINITY } }
223/// fn clone_box(&self) -> Box<dyn Distribution<i64>> { Box::new(self.clone()) }
224/// }
225/// let model = sample_i64(addr!("k"), AlwaysZero);
226/// ```
227pub fn sample_i64(addr: Address, dist: impl Distribution<i64> + 'static) -> Model<i64> {
228 Model::SampleI64 {
229 addr,
230 dist: Box::new(dist),
231 k: Box::new(pure),
232 }
233}
234
235/// Sample from a distribution (generic version - chooses the right variant automatically).
236// This is the main sampling function that works with any distribution type.
237// The return type is inferred from the distribution type.
238///
239/// Type-specific variants:
240/// - `sample_f64` - Sample from f64 distributions (continuous distributions)
241/// - `sample_bool` - Sample from bool distributions (Bernoulli)
242/// - `sample_u64` - Sample from u64 distributions (Poisson, Binomial)
243/// - `sample_usize` - Sample from usize distributions (Categorical)
244///
245/// Example:
246/// ```rust
247/// # use fugue::*;
248/// // Automatically returns f64 for continuous distributions
249/// let normal_sample: Model<f64> = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap());
250/// // Automatically returns bool for Bernoulli
251/// let coin_flip: Model<bool> = sample(addr!("coin"), Bernoulli::new(0.5).unwrap());
252/// // Automatically returns u64 for Poisson
253/// let count: Model<u64> = sample(addr!("count"), Poisson::new(3.0).unwrap());
254/// // Automatically returns usize for Categorical
255/// let choice: Model<usize> = sample(addr!("choice"),
256/// Categorical::new(vec![0.3, 0.5, 0.2]).unwrap());
257/// ```
258pub fn sample<T>(addr: Address, dist: impl Distribution<T> + 'static) -> Model<T>
259where
260 T: SampleType,
261{
262 T::make_sample_model(addr, Box::new(dist))
263}
264
265/// Trait for types that can be sampled in Models.
266/// This enables automatic dispatch to the right Model variant.
267pub trait SampleType: 'static + Send + Sync + Sized {
268 fn make_sample_model(addr: Address, dist: Box<dyn Distribution<Self>>) -> Model<Self>;
269 fn make_observe_model(
270 addr: Address,
271 dist: Box<dyn Distribution<Self>>,
272 value: Self,
273 ) -> Model<()>;
274}
275impl SampleType for f64 {
276 fn make_sample_model(addr: Address, dist: Box<dyn Distribution<f64>>) -> Model<f64> {
277 Model::SampleF64 {
278 addr,
279 dist,
280 k: Box::new(pure),
281 }
282 }
283 fn make_observe_model(
284 addr: Address,
285 dist: Box<dyn Distribution<f64>>,
286 value: f64,
287 ) -> Model<()> {
288 Model::ObserveF64 {
289 addr,
290 dist,
291 value,
292 k: Box::new(pure),
293 }
294 }
295}
296impl SampleType for bool {
297 fn make_sample_model(addr: Address, dist: Box<dyn Distribution<bool>>) -> Model<bool> {
298 Model::SampleBool {
299 addr,
300 dist,
301 k: Box::new(pure),
302 }
303 }
304 fn make_observe_model(
305 addr: Address,
306 dist: Box<dyn Distribution<bool>>,
307 value: bool,
308 ) -> Model<()> {
309 Model::ObserveBool {
310 addr,
311 dist,
312 value,
313 k: Box::new(pure),
314 }
315 }
316}
317impl SampleType for u64 {
318 fn make_sample_model(addr: Address, dist: Box<dyn Distribution<u64>>) -> Model<u64> {
319 Model::SampleU64 {
320 addr,
321 dist,
322 k: Box::new(pure),
323 }
324 }
325 fn make_observe_model(
326 addr: Address,
327 dist: Box<dyn Distribution<u64>>,
328 value: u64,
329 ) -> Model<()> {
330 Model::ObserveU64 {
331 addr,
332 dist,
333 value,
334 k: Box::new(pure),
335 }
336 }
337}
338impl SampleType for usize {
339 fn make_sample_model(addr: Address, dist: Box<dyn Distribution<usize>>) -> Model<usize> {
340 Model::SampleUsize {
341 addr,
342 dist,
343 k: Box::new(pure),
344 }
345 }
346 fn make_observe_model(
347 addr: Address,
348 dist: Box<dyn Distribution<usize>>,
349 value: usize,
350 ) -> Model<()> {
351 Model::ObserveUsize {
352 addr,
353 dist,
354 value,
355 k: Box::new(pure),
356 }
357 }
358}
359impl SampleType for i64 {
360 fn make_sample_model(addr: Address, dist: Box<dyn Distribution<i64>>) -> Model<i64> {
361 Model::SampleI64 {
362 addr,
363 dist,
364 k: Box::new(pure),
365 }
366 }
367 fn make_observe_model(
368 addr: Address,
369 dist: Box<dyn Distribution<i64>>,
370 value: i64,
371 ) -> Model<()> {
372 Model::ObserveI64 {
373 addr,
374 dist,
375 value,
376 k: Box::new(pure),
377 }
378 }
379}
380
381/// Observe a value from a distribution (generic version).
382/// This function automatically chooses the right observation variant based on the distribution type and observed value type.
383///
384/// Example:
385/// ```rust
386/// use fugue::*;
387/// // Observe f64 value from continuous distribution
388/// let model = observe(addr!("y"), Normal::new(1.0, 0.5).unwrap(), 2.5);
389/// // Observe bool value from Bernoulli
390/// let model = observe(addr!("coin"), Bernoulli::new(0.6).unwrap(), true);
391/// // Observe u64 count from Poisson
392/// let model = observe(addr!("count"), Poisson::new(3.0).unwrap(), 5u64);
393/// // Observe usize choice from Categorical
394/// let model = observe(addr!("choice"),
395/// Categorical::new(vec![0.3, 0.5, 0.2]).unwrap(), 1usize);
396/// ```
397pub fn observe<T>(addr: Address, dist: impl Distribution<T> + 'static, value: T) -> Model<()>
398where
399 T: SampleType,
400{
401 T::make_observe_model(addr, Box::new(dist), value)
402}
403
404/// Add an unnormalized log-weight `logw` to the model, returning a `Model<()>`.
405///
406/// Factors allow encoding soft constraints or arbitrary log-probability contributions to the model.
407/// They are particularly useful for:
408///
409/// - Encoding constraints that should be "mostly satisfied"
410/// - Adding custom log-likelihood terms
411/// - Implementing rejection sampling (using negative infinity)
412///
413/// Example:
414/// ```rust
415/// # use fugue::*;
416/// // Add positive log-weight (increases probability)
417/// let model = factor(1.0); // Adds log(e) = 1.0 to weight
418/// // Add negative log-weight (decreases probability)
419/// let model = factor(-2.0); // Subtracts 2.0 from log-weight
420/// // Reject/fail (zero probability)
421/// let model = factor(f64::NEG_INFINITY);
422/// // Soft constraint: prefer values near zero
423/// let x = 5.0;
424/// let soft_constraint = factor(-0.5 * x * x); // Gaussian-like penalty
425/// ```
426pub fn factor(logw: LogF64) -> Model<()> {
427 Model::Factor {
428 logw,
429 k: Box::new(pure),
430 }
431}
432
433/// `ModelExt<A>` provides monadic operations for composing `Model<A>` values.
434/// Provides `bind`, `map`, and `and_then` for chaining and transforming probabilistic computations.
435///
436/// Example:
437/// ```rust
438/// # use fugue::*;
439/// // Transform result with map
440/// let transformed = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
441/// .map(|x| x * 2.0);
442///
443/// // Chain dependent computations with bind
444/// let dependent = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
445/// .bind(|x| sample(addr!("y"), Normal::new(x, 0.5).unwrap()));
446/// ```
447pub trait ModelExt<A>: Sized {
448 /// Monadic bind operation (>>=).
449 ///
450 /// Chains two probabilistic computations where the second depends on the result of the first.
451 /// This is the fundamental operation for building complex probabilistic models from simpler parts.
452 /// The function `k` takes the result of this model and returns a new model.
453 ///
454 /// Example:
455 /// ```rust
456 /// # use fugue::*;
457 /// // Dependent sampling: y depends on x
458 /// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
459 /// .bind(|x| sample(addr!("y"), Normal::new(x, 0.1).unwrap()));
460 /// ```
461 fn bind<B>(self, k: impl FnOnce(A) -> Model<B> + Send + 'static) -> Model<B>;
462
463 /// Apply a function, `f`, to transform the result of this model.
464 /// This is the functor map operation - it transforms the output of a model without adding any additional probabilistic behavior.
465 ///
466 /// Example:
467 /// ```rust
468 /// # use fugue::*;
469 /// // Transform the sampled value
470 /// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
471 /// .map(|x| x.exp()); // Apply exponential function
472 /// ```
473 fn map<B>(self, f: impl FnOnce(A) -> B + Send + 'static) -> Model<B> {
474 self.bind(|a| pure(f(a)))
475 }
476
477 /// Alias for `bind` - chains dependent probabilistic computations.
478 /// This method provides a more familiar interface for Rust developers used to `Option::and_then` and `Result::and_then`.
479 ///
480 /// Example:
481 /// ```rust
482 /// # use fugue::*;
483 /// // Dependent sampling: y depends on x
484 /// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
485 /// .and_then(|x| sample(addr!("y"), Normal::new(x, 0.1).unwrap()));
486 /// ```
487 fn and_then<B>(self, k: impl FnOnce(A) -> Model<B> + Send + 'static) -> Model<B> {
488 self.bind(k)
489 }
490}
491impl<A: 'static> ModelExt<A> for Model<A> {
492 fn bind<B>(self, k: impl FnOnce(A) -> Model<B> + Send + 'static) -> Model<B> {
493 match self {
494 Model::Pure(a) => k(a),
495 Model::SampleF64 { addr, dist, k: k1 } => Model::SampleF64 {
496 addr,
497 dist,
498 k: Box::new(move |x| k1(x).bind(k)),
499 },
500 Model::SampleBool { addr, dist, k: k1 } => Model::SampleBool {
501 addr,
502 dist,
503 k: Box::new(move |x| k1(x).bind(k)),
504 },
505 Model::SampleU64 { addr, dist, k: k1 } => Model::SampleU64 {
506 addr,
507 dist,
508 k: Box::new(move |x| k1(x).bind(k)),
509 },
510 Model::SampleUsize { addr, dist, k: k1 } => Model::SampleUsize {
511 addr,
512 dist,
513 k: Box::new(move |x| k1(x).bind(k)),
514 },
515 Model::SampleI64 { addr, dist, k: k1 } => Model::SampleI64 {
516 addr,
517 dist,
518 k: Box::new(move |x| k1(x).bind(k)),
519 },
520 Model::ObserveF64 {
521 addr,
522 dist,
523 value,
524 k: k1,
525 } => Model::ObserveF64 {
526 addr,
527 dist,
528 value,
529 k: Box::new(move |()| k1(()).bind(k)),
530 },
531 Model::ObserveBool {
532 addr,
533 dist,
534 value,
535 k: k1,
536 } => Model::ObserveBool {
537 addr,
538 dist,
539 value,
540 k: Box::new(move |()| k1(()).bind(k)),
541 },
542 Model::ObserveU64 {
543 addr,
544 dist,
545 value,
546 k: k1,
547 } => Model::ObserveU64 {
548 addr,
549 dist,
550 value,
551 k: Box::new(move |()| k1(()).bind(k)),
552 },
553 Model::ObserveUsize {
554 addr,
555 dist,
556 value,
557 k: k1,
558 } => Model::ObserveUsize {
559 addr,
560 dist,
561 value,
562 k: Box::new(move |()| k1(()).bind(k)),
563 },
564 Model::ObserveI64 {
565 addr,
566 dist,
567 value,
568 k: k1,
569 } => Model::ObserveI64 {
570 addr,
571 dist,
572 value,
573 k: Box::new(move |()| k1(()).bind(k)),
574 },
575 Model::Factor { logw, k: k1 } => Model::Factor {
576 logw,
577 k: Box::new(move |()| k1(()).bind(k)),
578 },
579 }
580 }
581}
582
583/// Combine two independent models, `ma` and `mb`, into a model of their paired results.
584/// This operation runs both models and combines their results into a tuple.
585/// The models are executed independently (neither depends on the other's result).
586///
587/// Example:
588/// ```rust
589/// # use fugue::*;
590/// // Sample two independent random variables
591/// let x_model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap());
592/// let y_model = sample(addr!("y"), Uniform::new(0.0, 1.0).unwrap());
593/// let paired = zip(x_model, y_model); // Model<(f64, f64)>
594/// // Can be used with any model types
595/// let mixed = zip(pure(42.0), sample(addr!("z"), Exponential::new(1.0).unwrap()));
596/// ```
597pub fn zip<A: Send + 'static, B: Send + 'static>(ma: Model<A>, mb: Model<B>) -> Model<(A, B)> {
598 ma.bind(|a| mb.map(move |b| (a, b)))
599}
600
601/// Execute a vector of models, `models`, and collect their results into a single model of a vector.
602/// This function takes a collection of independent models and runs them all, collecting their results into a vector.
603/// This is useful for running multiple similar probabilistic computations.
604///
605/// Example:
606/// ```rust
607/// use fugue::*;
608/// // Create multiple independent samples
609/// let models = vec![
610/// sample(addr!("x", 0), Normal::new(0.0, 1.0).unwrap()),
611/// sample(addr!("x", 1), Normal::new(1.0, 1.0).unwrap()),
612/// sample(addr!("x", 2), Normal::new(2.0, 1.0).unwrap()),
613/// ];
614/// let all_samples = sequence_vec(models); // Model<Vec<f64>>
615/// // Mix deterministic and probabilistic models
616/// let mixed_models = vec![
617/// pure(1.0),
618/// sample(addr!("random"), Uniform::new(0.0, 1.0).unwrap()),
619/// pure(3.0),
620/// ];
621/// let results = sequence_vec(mixed_models);
622/// ```
623pub fn sequence_vec<A: Send + 'static>(models: Vec<Model<A>>) -> Model<Vec<A>> {
624 // FG-19: assemble a model that THREADS the growing result `Vec` forward
625 // through a right-nested bind chain — `m0.bind(|a0| { acc.push(a0);
626 // m1.bind(|a1| { acc.push(a1); … pure(acc) }) })`. The interpreter's
627 // trampoline then advances exactly one site per O(1) step, so a large
628 // `plate!` / `sequence_vec` no longer overflows the stack.
629 //
630 // Two shapes are specifically AVOIDED here because both recurse once per
631 // element at *interpretation* time even though the trampoline itself is
632 // iterative:
633 // * the old left fold `zip(zip(zip(pure, m0), m1), …)`, whose first
634 // continuation is a left-associated tower; and
635 // * a right fold that accumulates with `acc.map(push)`, which instead
636 // defers a left-nested chain of `push` maps into the continuation.
637 // Threading the `Vec` through the bind (pushing eagerly inside each
638 // continuation, then tail-calling the next model) keeps every continuation
639 // O(1): it yields the next `Sample` node directly with no wrapper build-up.
640 //
641 // The chain is assembled iteratively (a plain `for` loop, O(1) build stack)
642 // by folding the models in reverse into a continuation `cont_k: Vec<A> ->
643 // Model<Vec<A>>` = "given the results of `m0..m_{k-1}`, finish the vector".
644 // `cont_0(vec![])` executes `m0` first, preserving input/address order with no
645 // terminal reverse.
646 let n = models.len();
647 let mut cont: Box<dyn FnOnce(Vec<A>) -> Model<Vec<A>> + Send> = Box::new(pure);
648 for m in models.into_iter().rev() {
649 let next = cont;
650 cont = Box::new(move |mut acc: Vec<A>| {
651 m.bind(move |a| {
652 acc.push(a);
653 next(acc)
654 })
655 });
656 }
657 cont(Vec::with_capacity(n))
658}
659
660/// Apply a function, `f`, that produces models to each item in a vector, `items`, collecting the results.
661/// This is a higher-order function that maps each item in the input vector through a function that produces a model,
662/// then sequences all the resulting models into a single model of a vector.
663/// This is equivalent to `sequence_vec(items.map(f))` but more convenient.
664///
665/// Example:
666/// ```rust
667/// use fugue::*;
668/// // Add noise to each data point
669/// let data = vec![1.0, 2.0, 3.0];
670/// let noisy_data = traverse_vec(data, |x| {
671/// sample(addr!("noise", x as usize), Normal::new(0.0, 0.1).unwrap())
672/// .map(move |noise| x + noise)
673/// });
674/// // Create observations for each data point
675/// let observations = vec![1.2, 2.1, 2.9];
676/// let model = traverse_vec(observations, |obs| {
677/// observe(addr!("y", obs as usize), Normal::new(2.0, 0.5).unwrap(), obs)
678/// });
679/// ```
680pub fn traverse_vec<T, A: Send + 'static>(
681 items: Vec<T>,
682 f: impl Fn(T) -> Model<A> + Send + Sync + 'static,
683) -> Model<Vec<A>> {
684 sequence_vec(items.into_iter().map(f).collect())
685}
686
687/// Conditional execution: fail with zero probability when predicate is false.
688///
689/// Guards provide a way to enforce hard constraints in probabilistic models.
690/// When the predicate `pred` is true, the model continues normally.
691/// When false, the model receives negative infinite log-weight, effectively ruling out that execution path,
692/// returning a `Model<()>` that fails with zero probability.
693///
694/// Example:
695/// ```rust
696/// # use fugue::*;
697/// // Ensure a sampled value is positive
698/// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
699/// .bind(|x| {
700/// guard(x > 0.0).bind(move |_| pure(x))
701/// });
702/// // Multiple constraints
703/// let model = sample(addr!("x"), Uniform::new(-2.0, 2.0).unwrap())
704/// .bind(|x| {
705/// guard(x > -1.0).bind(move |_|
706/// guard(x < 1.0).bind(move |_| pure(x * x))
707/// )
708/// });
709/// ```
710pub fn guard(pred: bool) -> Model<()> {
711 if pred {
712 pure(())
713 } else {
714 factor(f64::NEG_INFINITY)
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721 use crate::addr;
722 use crate::core::distribution::*;
723 use crate::runtime::handler::run;
724 use crate::runtime::interpreters::PriorHandler;
725 use crate::runtime::trace::Trace;
726 use rand::rngs::StdRng;
727 use rand::SeedableRng;
728
729 #[test]
730 fn pure_and_map_work() {
731 let m = pure(2).map(|x| x + 3);
732 let (val, t) = run(
733 PriorHandler {
734 rng: &mut StdRng::seed_from_u64(1),
735 trace: Trace::default(),
736 },
737 m,
738 );
739 assert_eq!(val, 5);
740 assert_eq!(t.choices.len(), 0);
741 }
742
743 #[test]
744 fn sample_and_observe_sites() {
745 let m = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
746 .and_then(|x| observe(addr!("y"), Normal::new(x, 1.0).unwrap(), 0.5).map(move |_| x));
747
748 let mut rng = StdRng::seed_from_u64(42);
749 let (_val, trace) = run(
750 PriorHandler {
751 rng: &mut rng,
752 trace: Trace::default(),
753 },
754 m,
755 );
756 assert!(trace.choices.contains_key(&addr!("x")));
757 // Observation contributes to likelihood but not to choices
758 assert!((trace.log_likelihood.is_finite()));
759 }
760
761 #[test]
762 fn factor_and_guard_affect_weight() {
763 // factor adds a finite weight
764 let m_ok = factor(-1.23);
765 let ((), t_ok) = run(
766 PriorHandler {
767 rng: &mut StdRng::seed_from_u64(2),
768 trace: Trace::default(),
769 },
770 m_ok,
771 );
772 assert!((t_ok.total_log_weight() + 1.23).abs() < 1e-12);
773
774 // guard(false) adds -inf weight via factor
775 let m_bad = guard(false);
776 let ((), t_bad) = run(
777 PriorHandler {
778 rng: &mut StdRng::seed_from_u64(3),
779 trace: Trace::default(),
780 },
781 m_bad,
782 );
783 assert!(
784 t_bad.total_log_weight().is_infinite() && t_bad.total_log_weight().is_sign_negative()
785 );
786 }
787
788 #[test]
789 fn sequence_and_traverse_vec() {
790 let models: Vec<Model<i32>> = (0..5).map(pure).collect();
791 let seq = sequence_vec(models);
792 let (vals, t) = run(
793 PriorHandler {
794 rng: &mut StdRng::seed_from_u64(4),
795 trace: Trace::default(),
796 },
797 seq,
798 );
799 assert_eq!(vals, vec![0, 1, 2, 3, 4]);
800 assert_eq!(t.choices.len(), 0);
801
802 let trav = traverse_vec(vec![1, 2, 3], |i| pure(i * 2));
803 let (v2, _t2) = run(
804 PriorHandler {
805 rng: &mut StdRng::seed_from_u64(5),
806 trace: Trace::default(),
807 },
808 trav,
809 );
810 assert_eq!(v2, vec![2, 4, 6]);
811 }
812
813 #[test]
814 fn zip_and_sequence_empty_and_bind_chaining() {
815 // zip
816 let m1 = pure(1);
817 let m2 = pure(2);
818 let (pair, _t) = run(
819 PriorHandler {
820 rng: &mut StdRng::seed_from_u64(6),
821 trace: Trace::default(),
822 },
823 zip(m1, m2),
824 );
825 assert_eq!(pair, (1, 2));
826
827 // sequence empty
828 let empty: Vec<Model<i32>> = vec![];
829 let (vals, _t2) = run(
830 PriorHandler {
831 rng: &mut StdRng::seed_from_u64(7),
832 trace: Trace::default(),
833 },
834 sequence_vec(empty),
835 );
836 assert!(vals.is_empty());
837
838 // bind chaining across types
839 let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
840 .bind(|x| pure(x > 0.0))
841 .bind(|b| if b { pure(1u64) } else { pure(0u64) });
842 let (_val, _t3) = run(
843 PriorHandler {
844 rng: &mut StdRng::seed_from_u64(8),
845 trace: Trace::default(),
846 },
847 model,
848 );
849 }
850}