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.
60    /// [`DiscreteUniform`](crate::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/// A `NaN` weight is mapped to `-∞` (probability zero) at construction (FG-N2):
414/// there is no other reading under which the trace weight, SMC's normalizer and
415/// the MH acceptance ratio all stay well-defined, and the handlers apply the
416/// same rule when they accumulate.
417///
418/// Example:
419/// ```rust
420/// # use fugue::*;
421/// // Add positive log-weight (increases probability)
422/// let model = factor(1.0); // Adds log(e) = 1.0 to weight
423/// // Add negative log-weight (decreases probability)
424/// let model = factor(-2.0); // Subtracts 2.0 from log-weight
425/// // Reject/fail (zero probability)
426/// let model = factor(f64::NEG_INFINITY);
427/// // Soft constraint: prefer values near zero
428/// let x = 5.0;
429/// let soft_constraint = factor(-0.5 * x * x); // Gaussian-like penalty
430/// ```
431pub fn factor(logw: LogF64) -> Model<()> {
432    Model::Factor {
433        logw: crate::core::numerical::nan_to_neg_inf(logw),
434        k: Box::new(pure),
435    }
436}
437
438/// `ModelExt<A>` provides monadic operations for composing `Model<A>` values.
439/// Provides `bind`, `map`, and `and_then` for chaining and transforming probabilistic computations.
440///
441/// Example:
442/// ```rust
443/// # use fugue::*;
444/// // Transform result with map
445/// let transformed = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
446/// .map(|x| x * 2.0);
447///
448/// // Chain dependent computations with bind
449/// let dependent = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
450/// .bind(|x| sample(addr!("y"), Normal::new(x, 0.5).unwrap()));
451/// ```
452pub trait ModelExt<A>: Sized {
453    /// Monadic bind operation (>>=).
454    ///
455    /// Chains two probabilistic computations where the second depends on the result of the first.
456    /// This is the fundamental operation for building complex probabilistic models from simpler parts.
457    /// The function `k` takes the result of this model and returns a new model.
458    ///
459    /// Example:
460    /// ```rust
461    /// # use fugue::*;
462    /// // Dependent sampling: y depends on x
463    /// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
464    ///     .bind(|x| sample(addr!("y"), Normal::new(x, 0.1).unwrap()));
465    /// ```
466    ///
467    /// # Loops: do not left-nest (FG-N4)
468    ///
469    /// `bind` on an effectful model wraps that model's continuation:
470    /// `k_new = |x| k_old(x).bind(k)`. The idiom
471    ///
472    /// ```rust
473    /// # use fugue::*;
474    /// # let data = vec![0.1, 0.2];
475    /// let mut m = pure(0.0);
476    /// for (i, y) in data.into_iter().enumerate() {
477    ///     m = m.bind(move |mu| observe(addr!("y", i), Normal::new(mu, 1.0).unwrap(), y).map(move |_| mu));
478    /// }
479    /// ```
480    ///
481    /// therefore builds a **left-nested** tower: the first node's continuation is
482    /// wrapped once per iteration, so interpreting it costs one stack frame per
483    /// iteration and `O(N²)` closure re-wrapping. The interpreter's trampoline
484    /// (FG-19) is iterative *across* nodes but cannot flatten a single node's
485    /// continuation; this is inherent to the CPS encoding. Measured envelope in
486    /// a debug build: a few thousand nodes on a 2 MiB thread stack (~5 000 fine,
487    /// 10 000 overflows), ~20 000 on 8 MiB, and wasm main-thread stacks are
488    /// smaller still.
489    ///
490    /// Use one of these shapes instead, both of which run in constant stack for
491    /// any `N`:
492    ///
493    /// - **Independent sites**: [`traverse_vec`](crate::traverse_vec) /
494    ///   [`sequence_vec`](crate::sequence_vec) / [`plate!`](crate::plate), e.g.
495    ///   `traverse_vec(data, move |(i, y)| observe(addr!("y", i), Normal::new(mu, 1.0).unwrap(), y))`.
496    /// - **Sequentially dependent sites**: build the chain from the **back**, so
497    ///   each continuation *returns* the rest instead of wrapping it —
498    ///   `let mut rest: Box<dyn FnOnce(f64) -> Model<f64> + Send> = Box::new(pure);
499    ///   for t in (0..n).rev() { let next = rest; rest = Box::new(move |prev| sample(..prev..).bind(move |x| next(x))); }`.
500    ///   Recursive builders (`fn build(i) -> Model { sample(..).bind(move |x| build(i + 1)) }`)
501    ///   are right-nested by construction and equally fine.
502    fn bind<B>(self, k: impl FnOnce(A) -> Model<B> + Send + 'static) -> Model<B>;
503
504    /// Apply a function, `f`, to transform the result of this model.
505    /// This is the functor map operation - it transforms the output of a model without adding any additional probabilistic behavior.
506    ///
507    /// Example:
508    /// ```rust
509    /// # use fugue::*;
510    /// // Transform the sampled value
511    /// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
512    ///     .map(|x| x.exp()); // Apply exponential function
513    /// ```
514    fn map<B>(self, f: impl FnOnce(A) -> B + Send + 'static) -> Model<B> {
515        self.bind(|a| pure(f(a)))
516    }
517
518    /// Alias for `bind` - chains dependent probabilistic computations.
519    /// This method provides a more familiar interface for Rust developers used to `Option::and_then` and `Result::and_then`.
520    ///
521    /// Example:
522    /// ```rust
523    /// # use fugue::*;
524    /// // Dependent sampling: y depends on x
525    /// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
526    ///     .and_then(|x| sample(addr!("y"), Normal::new(x, 0.1).unwrap()));
527    /// ```
528    fn and_then<B>(self, k: impl FnOnce(A) -> Model<B> + Send + 'static) -> Model<B> {
529        self.bind(k)
530    }
531}
532impl<A: 'static> ModelExt<A> for Model<A> {
533    fn bind<B>(self, k: impl FnOnce(A) -> Model<B> + Send + 'static) -> Model<B> {
534        match self {
535            Model::Pure(a) => k(a),
536            Model::SampleF64 { addr, dist, k: k1 } => Model::SampleF64 {
537                addr,
538                dist,
539                k: Box::new(move |x| k1(x).bind(k)),
540            },
541            Model::SampleBool { addr, dist, k: k1 } => Model::SampleBool {
542                addr,
543                dist,
544                k: Box::new(move |x| k1(x).bind(k)),
545            },
546            Model::SampleU64 { addr, dist, k: k1 } => Model::SampleU64 {
547                addr,
548                dist,
549                k: Box::new(move |x| k1(x).bind(k)),
550            },
551            Model::SampleUsize { addr, dist, k: k1 } => Model::SampleUsize {
552                addr,
553                dist,
554                k: Box::new(move |x| k1(x).bind(k)),
555            },
556            Model::SampleI64 { addr, dist, k: k1 } => Model::SampleI64 {
557                addr,
558                dist,
559                k: Box::new(move |x| k1(x).bind(k)),
560            },
561            Model::ObserveF64 {
562                addr,
563                dist,
564                value,
565                k: k1,
566            } => Model::ObserveF64 {
567                addr,
568                dist,
569                value,
570                k: Box::new(move |()| k1(()).bind(k)),
571            },
572            Model::ObserveBool {
573                addr,
574                dist,
575                value,
576                k: k1,
577            } => Model::ObserveBool {
578                addr,
579                dist,
580                value,
581                k: Box::new(move |()| k1(()).bind(k)),
582            },
583            Model::ObserveU64 {
584                addr,
585                dist,
586                value,
587                k: k1,
588            } => Model::ObserveU64 {
589                addr,
590                dist,
591                value,
592                k: Box::new(move |()| k1(()).bind(k)),
593            },
594            Model::ObserveUsize {
595                addr,
596                dist,
597                value,
598                k: k1,
599            } => Model::ObserveUsize {
600                addr,
601                dist,
602                value,
603                k: Box::new(move |()| k1(()).bind(k)),
604            },
605            Model::ObserveI64 {
606                addr,
607                dist,
608                value,
609                k: k1,
610            } => Model::ObserveI64 {
611                addr,
612                dist,
613                value,
614                k: Box::new(move |()| k1(()).bind(k)),
615            },
616            Model::Factor { logw, k: k1 } => Model::Factor {
617                logw,
618                k: Box::new(move |()| k1(()).bind(k)),
619            },
620        }
621    }
622}
623
624/// Combine two independent models, `ma` and `mb`, into a model of their paired results.
625/// This operation runs both models and combines their results into a tuple.
626/// The models are executed independently (neither depends on the other's result).
627///
628/// Example:
629/// ```rust
630/// # use fugue::*;
631/// // Sample two independent random variables
632/// let x_model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap());
633/// let y_model = sample(addr!("y"), Uniform::new(0.0, 1.0).unwrap());
634/// let paired = zip(x_model, y_model); // Model<(f64, f64)>
635/// // Can be used with any model types
636/// let mixed = zip(pure(42.0), sample(addr!("z"), Exponential::new(1.0).unwrap()));
637/// ```
638pub fn zip<A: Send + 'static, B: Send + 'static>(ma: Model<A>, mb: Model<B>) -> Model<(A, B)> {
639    ma.bind(|a| mb.map(move |b| (a, b)))
640}
641
642/// Execute a vector of models, `models`, and collect their results into a single model of a vector.
643/// This function takes a collection of independent models and runs them all, collecting their results into a vector.
644/// This is useful for running multiple similar probabilistic computations.
645///
646/// Example:
647/// ```rust
648/// use fugue::*;
649/// // Create multiple independent samples
650/// let models = vec![
651///     sample(addr!("x", 0), Normal::new(0.0, 1.0).unwrap()),
652///     sample(addr!("x", 1), Normal::new(1.0, 1.0).unwrap()),
653///     sample(addr!("x", 2), Normal::new(2.0, 1.0).unwrap()),
654/// ];
655/// let all_samples = sequence_vec(models); // Model<Vec<f64>>
656/// // Mix deterministic and probabilistic models
657/// let mixed_models = vec![
658///     pure(1.0),
659///     sample(addr!("random"), Uniform::new(0.0, 1.0).unwrap()),
660///     pure(3.0),
661/// ];
662/// let results = sequence_vec(mixed_models);
663/// ```
664pub fn sequence_vec<A: Send + 'static>(models: Vec<Model<A>>) -> Model<Vec<A>> {
665    // FG-19: assemble a model that THREADS the growing result `Vec` forward
666    // through a right-nested bind chain — `m0.bind(|a0| { acc.push(a0);
667    // m1.bind(|a1| { acc.push(a1); … pure(acc) }) })`. The interpreter's
668    // trampoline then advances exactly one site per O(1) step, so a large
669    // `plate!` / `sequence_vec` no longer overflows the stack.
670    //
671    // Two shapes are specifically AVOIDED here because both recurse once per
672    // element at *interpretation* time even though the trampoline itself is
673    // iterative:
674    //   * the old left fold `zip(zip(zip(pure, m0), m1), …)`, whose first
675    //     continuation is a left-associated tower; and
676    //   * a right fold that accumulates with `acc.map(push)`, which instead
677    //     defers a left-nested chain of `push` maps into the continuation.
678    // Threading the `Vec` through the bind (pushing eagerly inside each
679    // continuation, then tail-calling the next model) keeps every continuation
680    // O(1): it yields the next `Sample` node directly with no wrapper build-up.
681    //
682    // The chain is assembled iteratively (a plain `for` loop, O(1) build stack)
683    // by folding the models in reverse into a continuation `cont_k: Vec<A> ->
684    // Model<Vec<A>>` = "given the results of `m0..m_{k-1}`, finish the vector".
685    // `cont_0(vec![])` executes `m0` first, preserving input/address order with no
686    // terminal reverse.
687    //
688    // FG-N5: a `Pure` element is peeled at CONSTRUCTION, not deferred. Wrapping
689    // it as `pure(a).bind(|a| { acc.push(a); next(acc) })` is fine for one
690    // element, but `bind` on `Pure` calls its continuation immediately, and that
691    // continuation tail-calls `next` — so a RUN of k consecutive `Pure`s is k
692    // nested calls on the stack, and `plate!(i in 0..100_000 => pure(i))`
693    // overflowed before `run` was ever reached. Consecutive `Pure` values are
694    // therefore batched into ONE closure that extends the accumulator and makes
695    // a single call to the continuation that follows the run. Every remaining
696    // continuation is either such a batch or an effectful `bind` (which returns
697    // its node without calling anything), so construction and interpretation
698    // both stay O(1) in stack depth whatever the mix of `pure` and effects.
699    let n = models.len();
700    let mut cont: Box<dyn FnOnce(Vec<A>) -> Model<Vec<A>> + Send> = Box::new(pure);
701    // Values of the run of `Pure`s currently being collected, in REVERSE input
702    // order (we iterate the models back to front).
703    let mut pure_run: Vec<A> = Vec::new();
704
705    fn flush_pures<A: Send + 'static>(
706        pure_run: &mut Vec<A>,
707        cont: Box<dyn FnOnce(Vec<A>) -> Model<Vec<A>> + Send>,
708    ) -> Box<dyn FnOnce(Vec<A>) -> Model<Vec<A>> + Send> {
709        if pure_run.is_empty() {
710            return cont;
711        }
712        let mut values = std::mem::take(pure_run);
713        values.reverse(); // back to input order
714        Box::new(move |mut acc: Vec<A>| {
715            acc.extend(values);
716            cont(acc)
717        })
718    }
719
720    for m in models.into_iter().rev() {
721        match m {
722            Model::Pure(a) => pure_run.push(a),
723            m => {
724                let next = flush_pures(&mut pure_run, cont);
725                cont = Box::new(move |mut acc: Vec<A>| {
726                    m.bind(move |a| {
727                        acc.push(a);
728                        next(acc)
729                    })
730                });
731            }
732        }
733    }
734    cont = flush_pures(&mut pure_run, cont);
735    cont(Vec::with_capacity(n))
736}
737
738/// Apply a function, `f`, that produces models to each item in a vector, `items`, collecting the results.
739/// This is a higher-order function that maps each item in the input vector through a function that produces a model,
740/// then sequences all the resulting models into a single model of a vector.
741/// This is equivalent to `sequence_vec(items.map(f))` but more convenient.
742///
743/// Example:
744/// ```rust
745/// use fugue::*;
746/// // Add noise to each data point
747/// let data = vec![1.0, 2.0, 3.0];
748/// let noisy_data = traverse_vec(data, |x| {
749///     sample(addr!("noise", x as usize), Normal::new(0.0, 0.1).unwrap())
750///         .map(move |noise| x + noise)
751/// });
752/// // Create observations for each data point
753/// let observations = vec![1.2, 2.1, 2.9];
754/// let model = traverse_vec(observations, |obs| {
755///     observe(addr!("y", obs as usize), Normal::new(2.0, 0.5).unwrap(), obs)
756/// });
757/// ```
758pub fn traverse_vec<T, A: Send + 'static>(
759    items: Vec<T>,
760    f: impl Fn(T) -> Model<A> + Send + Sync + 'static,
761) -> Model<Vec<A>> {
762    sequence_vec(items.into_iter().map(f).collect())
763}
764
765/// Conditional execution: fail with zero probability when predicate is false.
766///
767/// Guards provide a way to enforce hard constraints in probabilistic models.
768/// When the predicate `pred` is true, the model continues normally.
769/// When false, the model receives negative infinite log-weight, effectively ruling out that execution path,
770/// returning a `Model<()>` that fails with zero probability.
771///
772/// Example:
773/// ```rust
774/// # use fugue::*;
775/// // Ensure a sampled value is positive
776/// let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
777///     .bind(|x| {
778///         guard(x > 0.0).bind(move |_| pure(x))
779///     });
780/// // Multiple constraints
781/// let model = sample(addr!("x"), Uniform::new(-2.0, 2.0).unwrap())
782///     .bind(|x| {
783///         guard(x > -1.0).bind(move |_|
784///             guard(x < 1.0).bind(move |_| pure(x * x))
785///         )
786///     });
787/// ```
788pub fn guard(pred: bool) -> Model<()> {
789    if pred {
790        pure(())
791    } else {
792        factor(f64::NEG_INFINITY)
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799    use crate::addr;
800    use crate::core::distribution::*;
801    use crate::runtime::handler::run;
802    use crate::runtime::interpreters::PriorHandler;
803    use crate::runtime::trace::Trace;
804    use rand::rngs::StdRng;
805    use rand::SeedableRng;
806
807    #[test]
808    fn pure_and_map_work() {
809        let m = pure(2).map(|x| x + 3);
810        let (val, t) = run(
811            PriorHandler {
812                rng: &mut StdRng::seed_from_u64(1),
813                trace: Trace::default(),
814            },
815            m,
816        );
817        assert_eq!(val, 5);
818        assert_eq!(t.choices.len(), 0);
819    }
820
821    #[test]
822    fn sample_and_observe_sites() {
823        let m = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
824            .and_then(|x| observe(addr!("y"), Normal::new(x, 1.0).unwrap(), 0.5).map(move |_| x));
825
826        let mut rng = StdRng::seed_from_u64(42);
827        let (_val, trace) = run(
828            PriorHandler {
829                rng: &mut rng,
830                trace: Trace::default(),
831            },
832            m,
833        );
834        assert!(trace.choices.contains_key(&addr!("x")));
835        // Observation contributes to likelihood but not to choices
836        assert!((trace.log_likelihood.is_finite()));
837    }
838
839    #[test]
840    fn factor_and_guard_affect_weight() {
841        // factor adds a finite weight
842        let m_ok = factor(-1.23);
843        let ((), t_ok) = run(
844            PriorHandler {
845                rng: &mut StdRng::seed_from_u64(2),
846                trace: Trace::default(),
847            },
848            m_ok,
849        );
850        assert!((t_ok.total_log_weight() + 1.23).abs() < 1e-12);
851
852        // guard(false) adds -inf weight via factor
853        let m_bad = guard(false);
854        let ((), t_bad) = run(
855            PriorHandler {
856                rng: &mut StdRng::seed_from_u64(3),
857                trace: Trace::default(),
858            },
859            m_bad,
860        );
861        assert!(
862            t_bad.total_log_weight().is_infinite() && t_bad.total_log_weight().is_sign_negative()
863        );
864    }
865
866    #[test]
867    fn sequence_and_traverse_vec() {
868        let models: Vec<Model<i32>> = (0..5).map(pure).collect();
869        let seq = sequence_vec(models);
870        let (vals, t) = run(
871            PriorHandler {
872                rng: &mut StdRng::seed_from_u64(4),
873                trace: Trace::default(),
874            },
875            seq,
876        );
877        assert_eq!(vals, vec![0, 1, 2, 3, 4]);
878        assert_eq!(t.choices.len(), 0);
879
880        let trav = traverse_vec(vec![1, 2, 3], |i| pure(i * 2));
881        let (v2, _t2) = run(
882            PriorHandler {
883                rng: &mut StdRng::seed_from_u64(5),
884                trace: Trace::default(),
885            },
886            trav,
887        );
888        assert_eq!(v2, vec![2, 4, 6]);
889    }
890
891    #[test]
892    fn zip_and_sequence_empty_and_bind_chaining() {
893        // zip
894        let m1 = pure(1);
895        let m2 = pure(2);
896        let (pair, _t) = run(
897            PriorHandler {
898                rng: &mut StdRng::seed_from_u64(6),
899                trace: Trace::default(),
900            },
901            zip(m1, m2),
902        );
903        assert_eq!(pair, (1, 2));
904
905        // sequence empty
906        let empty: Vec<Model<i32>> = vec![];
907        let (vals, _t2) = run(
908            PriorHandler {
909                rng: &mut StdRng::seed_from_u64(7),
910                trace: Trace::default(),
911            },
912            sequence_vec(empty),
913        );
914        assert!(vals.is_empty());
915
916        // bind chaining across types
917        let model = sample(addr!("x"), Normal::new(0.0, 1.0).unwrap())
918            .bind(|x| pure(x > 0.0))
919            .bind(|b| if b { pure(1u64) } else { pure(0u64) });
920        let (_val, _t3) = run(
921            PriorHandler {
922                rng: &mut StdRng::seed_from_u64(8),
923                trace: Trace::default(),
924            },
925            model,
926        );
927    }
928}