Skip to main content

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}