Skip to main content

probl_engine/
joint.rs

1//! Distributions whose outcomes own continuous draws: what `simulate`
2//! returns when its value holds outcomes of draws made inside it
3//! (docs/semantics.md, section 8).
4//!
5//! Each outcome's draws are numbered apart from any world's, from `OWNED`,
6//! in the order of the latents they were. Drawing from the distribution
7//! gives them fresh numbers in the world, so that two draws are independent
8//! and the parts of one outcome, like the fields of a record, stay
9//! correlated. When sampling, an outcome is chosen, then a value for each of
10//! its draws.
11
12use crate::analytic::{self, Domain, Event};
13use crate::continuous::{Family, Mixture, Part, Rng};
14use crate::dist::{Budget, Dist};
15use crate::error::{OpError, OpResult};
16use crate::value::Value;
17use probl_syntax::ast::BinOp;
18use std::collections::{BTreeMap, BTreeSet};
19use std::sync::Arc;
20
21/// Where an outcome's own draws are numbered from: above any world's.
22pub const OWNED: u64 = 1 << 63;
23
24/// The most events in one outcome whose truth values `simulate` lists.
25const MAX_EVENTS: usize = 16;
26
27/// The distribution of values that may hold outcomes of draws made where
28/// they were computed, as `simulate` gives it: each outcome's draws its own,
29/// and outcomes of only events as their truth values. An ordinary
30/// distribution when no outcome holds an outcome of a draw.
31pub fn of(pairs: Vec<(Value, f64)>, missing: f64, budget: &mut Budget) -> OpResult<Value> {
32    if pairs.is_empty() {
33        return Err(OpError::new("the result would be an empty distribution"));
34    }
35    let mut missing = missing;
36    let mut flat = Vec::with_capacity(pairs.len());
37    for (v, w) in pairs {
38        mix_in(v, w, &mut flat, &mut missing, budget)?;
39    }
40    let mut out = Vec::with_capacity(flat.len());
41    let mut joint = false;
42    for (v, w) in flat {
43        if !analytic::contains(&v) {
44            out.push((v, w));
45        } else if analytic::has_outcomes(&v) {
46            joint = true;
47            out.push((owned(&v), w));
48        } else {
49            for (x, p) in truths(&v, budget)? {
50                out.push((x, w * p));
51            }
52        }
53    }
54    budget.outcomes(out.len() as u128)?;
55    budget.work(out.len() as u64)?;
56    let d = Dist::from_pairs(out, missing.min(1.0));
57    Ok(if joint {
58        Value::Joint(Arc::new(d))
59    } else {
60        d.into_value()
61    })
62}
63
64/// A value's outcomes, mixed into `flat` with weight `w`: a distribution's,
65/// joint or not, and the value itself otherwise.
66fn mix_in(v: Value, w: f64, flat: &mut Vec<(Value, f64)>, missing: &mut f64, budget: &mut Budget) -> OpResult<()> {
67    let v = match v {
68        Value::Counts(_) => crate::ops::listed(&v, budget)?.into_owned(),
69        v => v,
70    };
71    match v {
72        Value::Dist(d) | Value::Joint(d) => {
73            *missing += w * d.missing;
74            for (x, p) in &d.outcomes {
75                match x {
76                    Value::Joint(inner) => {
77                        *missing += w * p * inner.missing;
78                        flat.extend(inner.outcomes.iter().map(|(y, q)| (y.clone(), w * p * q)));
79                    }
80                    x => flat.push((x.clone(), w * p)),
81                }
82            }
83        }
84        other => flat.push((other, w)),
85    }
86    Ok(())
87}
88
89/// A value with its latents numbered from `OWNED`, in their order.
90fn owned(v: &Value) -> Value {
91    let mut ids = BTreeSet::new();
92    analytic::collect_ids(v, &mut ids);
93    let map: BTreeMap<u64, u64> = ids.into_iter().zip(OWNED + 1..).collect();
94    analytic::renamed(v, &map)
95}
96
97/// The values a value of events becomes, each event as true or false, with
98/// their probabilities.
99fn truths(v: &Value, budget: &mut Budget) -> OpResult<Vec<(Value, f64)>> {
100    let mut events: Vec<Arc<Event>> = Vec::new();
101    collect_events(v, &mut events);
102    if events.len() > MAX_EVENTS {
103        return Err(OpError::limit(format!(
104            "a `simulate` value with more than {MAX_EVENTS} conditions on continuous draws is over the limit"
105        )));
106    }
107    budget.work(1u64 << events.len())?;
108    let mut out = Vec::new();
109    for mask in 0..1u32 << events.len() {
110        let mut all: Option<Event> = None;
111        for (i, e) in events.iter().enumerate() {
112            let side = if mask & (1 << i) != 0 {
113                (**e).clone()
114            } else {
115                e.complement()?
116            };
117            all = Some(match all {
118                None => side,
119                Some(all) => all.combine(&side, BinOp::And)?,
120            });
121        }
122        let p = all.map_or(1.0, |e| e.probability());
123        if p > 0.0 {
124            out.push((with_truths(v, &events, mask), p));
125        }
126    }
127    Ok(out)
128}
129
130fn collect_events(v: &Value, events: &mut Vec<Arc<Event>>) {
131    match v {
132        Value::Event(e) => {
133            let key = e.key();
134            if !events.iter().any(|x| x.key() == key) {
135                events.push(e.clone());
136            }
137        }
138        Value::List(xs) => xs.iter().for_each(|x| collect_events(x, events)),
139        Value::Map(xs) => xs.values().for_each(|x| collect_events(x, events)),
140        Value::Record(r) => r.fields.iter().for_each(|(_, x)| collect_events(x, events)),
141        Value::Dist(d) => d.outcomes.iter().for_each(|(x, _)| collect_events(x, events)),
142        Value::Closure(c) => c.captured.iter().for_each(|x| collect_events(x, events)),
143        _ => {}
144    }
145}
146
147/// `v` with each event of `events` true where `mask` has its bit.
148fn with_truths(v: &Value, events: &[Arc<Event>], mask: u32) -> Value {
149    if !analytic::contains(v) {
150        return v.clone();
151    }
152    let child = |x: &Value| with_truths(x, events, mask);
153    match v {
154        Value::Event(e) => {
155            let key = e.key();
156            let i = events.iter().position(|x| x.key() == key).expect("a collected event");
157            Value::Bool(mask & (1 << i) != 0)
158        }
159        Value::List(xs) => Value::list(xs.iter().map(child).collect()),
160        Value::Map(xs) => Value::map(xs.iter().map(|(k, x)| (k.clone(), child(x))).collect()),
161        Value::Record(r) => crate::ops::make_record(
162            r.ty.clone(),
163            r.fields.iter().map(|(k, x)| (k.clone(), child(x))).collect(),
164        ),
165        Value::Dist(d) => {
166            Dist::from_pairs(d.outcomes.iter().map(|(x, p)| (child(x), *p)).collect(), d.missing).into_value()
167        }
168        Value::Closure(c) => Value::Closure(Arc::new(crate::value::Closure {
169            func: c.func,
170            captured: c.captured.iter().map(child).collect(),
171            named: c.named,
172        })),
173        v => v.clone(),
174    }
175}
176
177/// An outcome drawn into a world: its draws with fresh numbers after
178/// `next`.
179pub fn fresh(v: &Value, next: &mut u64) -> Value {
180    let mut ids = BTreeSet::new();
181    analytic::collect_ids(v, &mut ids);
182    if ids.is_empty() {
183        return v.clone();
184    }
185    let map: BTreeMap<u64, u64> = ids
186        .into_iter()
187        .map(|id| {
188            *next += 1;
189            (id, *next)
190        })
191        .collect();
192    analytic::renamed(v, &map)
193}
194
195/// A draw when sampling: an outcome chosen with its probability, with a
196/// value for each of its draws, from its distribution where it can be.
197#[cold]
198#[inline(never)]
199pub fn sample(d: &Dist, rng: &mut Rng) -> Value {
200    let i = rng.choose(d.outcomes.iter().map(|(_, p)| *p)).unwrap_or(0);
201    let v = &d.outcomes[i].0;
202    let mut latents: BTreeMap<u64, Drawn> = BTreeMap::new();
203    gather(v, &mut latents);
204    let at: BTreeMap<u64, (f64, f64)> = latents
205        .into_iter()
206        .map(|(id, (family, domain, weight))| {
207            let c = within(&domain, weight.as_deref(), rng);
208            (id, (c, family.quantile(c)))
209        })
210        .collect();
211    concrete(v, &at)
212}
213
214/// A latent's distribution, where it can be, and its likelihood.
215type Drawn = (Family, Domain, Option<Arc<crate::likelihood::Likelihood>>);
216
217/// Each latent of a value, with its distribution, where it can be and its
218/// likelihood.
219fn gather(v: &Value, latents: &mut BTreeMap<u64, Drawn>) {
220    let mut add = |a: &analytic::Analytic| match &a.form {
221        Some(f) => {
222            for t in &f.terms {
223                latents.entry(t.id).or_insert((t.family, Domain::full(), None));
224            }
225        }
226        None => {
227            let entry = latents
228                .entry(a.id)
229                .or_insert((a.family, a.domain.clone(), a.weight.clone()));
230            entry.1 = entry.1.intersect(&a.domain);
231        }
232    };
233    let mut pending = vec![v];
234    while let Some(v) = pending.pop() {
235        match v {
236            Value::Analytic(a) => add(a),
237            Value::Event(e) => e.draws.iter().for_each(&mut add),
238            Value::List(xs) => pending.extend(xs.iter()),
239            Value::Map(xs) => pending.extend(xs.values()),
240            Value::Record(r) => pending.extend(r.fields.iter().map(|(_, x)| x)),
241            Value::Dist(d) => pending.extend(d.outcomes.iter().map(|(x, _)| x)),
242            Value::Closure(c) => pending.extend(c.captured.iter()),
243            _ => {}
244        }
245    }
246}
247
248/// A CDF coordinate drawn from where a latent can be: uniformly, or by its
249/// likelihood.
250fn within(domain: &Domain, weight: Option<&crate::likelihood::Likelihood>, rng: &mut Rng) -> f64 {
251    if let Some(l) = weight {
252        return l.inverse(domain, rng.open());
253    }
254    let mut left = rng.open() * domain.mass();
255    for &(a, b) in &domain.0 {
256        if left <= b - a {
257            return (a + left).clamp(a, b);
258        }
259        left -= b - a;
260    }
261    domain.0.last().map_or(0.5, |&(_, b)| b)
262}
263
264/// An axis's coordinate and value, given its latents' as `at` has them.
265fn axis(a: &analytic::Analytic, at: &BTreeMap<u64, (f64, f64)>) -> (f64, f64) {
266    match &a.form {
267        Some(f) => {
268            let t = f.constant + f.terms.iter().map(|t| t.coef * at[&t.id].1).sum::<f64>();
269            (a.family.cdf(t), t)
270        }
271        None => at[&a.id],
272    }
273}
274
275/// `v` with each outcome of a draw at the values its latents have in `at`.
276fn concrete(v: &Value, at: &BTreeMap<u64, (f64, f64)>) -> Value {
277    if !analytic::contains(v) {
278        return v.clone();
279    }
280    let child = |x: &Value| concrete(x, at);
281    match v {
282        Value::Analytic(a) => {
283            let (c, t) = axis(a, at);
284            a.at(c, t)
285        }
286        Value::Event(e) => {
287            let coordinates: Vec<f64> = e.draws.iter().map(|d| axis(d, at).0).collect();
288            let inside = |d: &Domain, c: f64| d.0.iter().any(|&(a, b)| a <= c && c <= b);
289            Value::Bool(
290                e.boxes
291                    .iter()
292                    .any(|part| part.iter().zip(&coordinates).all(|(d, &c)| inside(d, c))),
293            )
294        }
295        Value::List(xs) => Value::list(xs.iter().map(child).collect()),
296        Value::Map(xs) => Value::map(xs.iter().map(|(k, x)| (k.clone(), child(x))).collect()),
297        Value::Record(r) => crate::ops::make_record(
298            r.ty.clone(),
299            r.fields.iter().map(|(k, x)| (k.clone(), child(x))).collect(),
300        ),
301        Value::Dist(d) => {
302            Dist::from_pairs(d.outcomes.iter().map(|(x, p)| (child(x), *p)).collect(), d.missing).into_value()
303        }
304        Value::Closure(c) => Value::Closure(Arc::new(crate::value::Closure {
305            func: c.func,
306            captured: c.captured.iter().map(child).collect(),
307            named: c.named,
308        })),
309        v => v.clone(),
310    }
311}
312
313/// The marginal of a joint distribution of numbers: the mixture of its
314/// outcomes'. `None` if an outcome isn't a number.
315pub fn mixture(d: &Dist) -> Option<Mixture> {
316    let parts = d
317        .outcomes
318        .iter()
319        .map(|(v, p)| {
320            let part = match v {
321                Value::Analytic(a) => Part::Analytic((**a).clone()),
322                Value::Continuous(f) => Part::Continuous(**f),
323                v => Part::Point(v.as_f64()?),
324            };
325            Some((part, *p))
326        })
327        .collect::<Option<_>>()?;
328    Some(Mixture { parts })
329}
330
331/// `op` between a joint distribution and a value that isn't one, outcome
332/// by outcome: joint again, or an ordinary distribution of what's left,
333/// like the truth values of comparisons.
334#[cold]
335#[inline(never)]
336pub fn binary(op: BinOp, a: &Value, b: &Value, budget: &mut Budget) -> OpResult<Value> {
337    let (d, other, flipped) = match (a, b) {
338        (Value::Joint(d), other) => (d, other, false),
339        (other, Value::Joint(d)) => (d, other, true),
340        _ => unreachable!("an operand is joint"),
341    };
342    if matches!(other, Value::Joint(_)) || analytic::contains(other) {
343        return Err(OpError::unsupported(format!(
344            "`{}` between a `simulate` distribution of continuous outcomes and another distribution of them, or an outcome of a draw, isn't supported yet",
345            op.symbol()
346        ))
347        .help("draw from it first, with `~`"));
348    }
349    let mut pairs = Vec::with_capacity(d.outcomes.len());
350    for (v, p) in &d.outcomes {
351        let r = if flipped {
352            crate::ops::binary(op, other, v, budget)?
353        } else {
354            crate::ops::binary(op, v, other, budget)?
355        };
356        pairs.push((r, *p));
357    }
358    of(pairs, d.missing, budget)
359}