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}