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        })),
172        v => v.clone(),
173    }
174}
175
176/// An outcome drawn into a world: its draws with fresh numbers after
177/// `next`.
178pub fn fresh(v: &Value, next: &mut u64) -> Value {
179    let mut ids = BTreeSet::new();
180    analytic::collect_ids(v, &mut ids);
181    if ids.is_empty() {
182        return v.clone();
183    }
184    let map: BTreeMap<u64, u64> = ids
185        .into_iter()
186        .map(|id| {
187            *next += 1;
188            (id, *next)
189        })
190        .collect();
191    analytic::renamed(v, &map)
192}
193
194/// A draw when sampling: an outcome chosen with its probability, with a
195/// value for each of its draws, from its distribution where it can be.
196#[cold]
197#[inline(never)]
198pub fn sample(d: &Dist, rng: &mut Rng) -> Value {
199    let i = rng.choose(d.outcomes.iter().map(|(_, p)| *p)).unwrap_or(0);
200    let v = &d.outcomes[i].0;
201    let mut latents: BTreeMap<u64, Drawn> = BTreeMap::new();
202    gather(v, &mut latents);
203    let at: BTreeMap<u64, (f64, f64)> = latents
204        .into_iter()
205        .map(|(id, (family, domain, weight))| {
206            let c = within(&domain, weight.as_deref(), rng);
207            (id, (c, family.quantile(c)))
208        })
209        .collect();
210    concrete(v, &at)
211}
212
213/// A latent's distribution, where it can be, and its likelihood.
214type Drawn = (Family, Domain, Option<Arc<crate::likelihood::Likelihood>>);
215
216/// Each latent of a value, with its distribution, where it can be and its
217/// likelihood.
218fn gather(v: &Value, latents: &mut BTreeMap<u64, Drawn>) {
219    let mut add = |a: &analytic::Analytic| match &a.form {
220        Some(f) => {
221            for t in &f.terms {
222                latents.entry(t.id).or_insert((t.family, Domain::full(), None));
223            }
224        }
225        None => {
226            let entry = latents
227                .entry(a.id)
228                .or_insert((a.family, a.domain.clone(), a.weight.clone()));
229            entry.1 = entry.1.intersect(&a.domain);
230        }
231    };
232    let mut pending = vec![v];
233    while let Some(v) = pending.pop() {
234        match v {
235            Value::Analytic(a) => add(a),
236            Value::Event(e) => e.draws.iter().for_each(&mut add),
237            Value::List(xs) => pending.extend(xs.iter()),
238            Value::Map(xs) => pending.extend(xs.values()),
239            Value::Record(r) => pending.extend(r.fields.iter().map(|(_, x)| x)),
240            Value::Dist(d) => pending.extend(d.outcomes.iter().map(|(x, _)| x)),
241            Value::Closure(c) => pending.extend(c.captured.iter()),
242            _ => {}
243        }
244    }
245}
246
247/// A CDF coordinate drawn from where a latent can be: uniformly, or by its
248/// likelihood.
249fn within(domain: &Domain, weight: Option<&crate::likelihood::Likelihood>, rng: &mut Rng) -> f64 {
250    if let Some(l) = weight {
251        return l.inverse(domain, rng.open());
252    }
253    let mut left = rng.open() * domain.mass();
254    for &(a, b) in &domain.0 {
255        if left <= b - a {
256            return (a + left).clamp(a, b);
257        }
258        left -= b - a;
259    }
260    domain.0.last().map_or(0.5, |&(_, b)| b)
261}
262
263/// An axis's coordinate and value, given its latents' as `at` has them.
264fn axis(a: &analytic::Analytic, at: &BTreeMap<u64, (f64, f64)>) -> (f64, f64) {
265    match &a.form {
266        Some(f) => {
267            let t = f.constant + f.terms.iter().map(|t| t.coef * at[&t.id].1).sum::<f64>();
268            (a.family.cdf(t), t)
269        }
270        None => at[&a.id],
271    }
272}
273
274/// `v` with each outcome of a draw at the values its latents have in `at`.
275fn concrete(v: &Value, at: &BTreeMap<u64, (f64, f64)>) -> Value {
276    if !analytic::contains(v) {
277        return v.clone();
278    }
279    let child = |x: &Value| concrete(x, at);
280    match v {
281        Value::Analytic(a) => {
282            let (c, t) = axis(a, at);
283            a.at(c, t)
284        }
285        Value::Event(e) => {
286            let coordinates: Vec<f64> = e.draws.iter().map(|d| axis(d, at).0).collect();
287            let inside = |d: &Domain, c: f64| d.0.iter().any(|&(a, b)| a <= c && c <= b);
288            Value::Bool(
289                e.boxes
290                    .iter()
291                    .any(|part| part.iter().zip(&coordinates).all(|(d, &c)| inside(d, c))),
292            )
293        }
294        Value::List(xs) => Value::list(xs.iter().map(child).collect()),
295        Value::Map(xs) => Value::map(xs.iter().map(|(k, x)| (k.clone(), child(x))).collect()),
296        Value::Record(r) => crate::ops::make_record(
297            r.ty.clone(),
298            r.fields.iter().map(|(k, x)| (k.clone(), child(x))).collect(),
299        ),
300        Value::Dist(d) => {
301            Dist::from_pairs(d.outcomes.iter().map(|(x, p)| (child(x), *p)).collect(), d.missing).into_value()
302        }
303        Value::Closure(c) => Value::Closure(Arc::new(crate::value::Closure {
304            func: c.func,
305            captured: c.captured.iter().map(child).collect(),
306        })),
307        v => v.clone(),
308    }
309}
310
311/// The marginal of a joint distribution of numbers: the mixture of its
312/// outcomes'. `None` if an outcome isn't a number.
313pub fn mixture(d: &Dist) -> Option<Mixture> {
314    let parts = d
315        .outcomes
316        .iter()
317        .map(|(v, p)| {
318            let part = match v {
319                Value::Analytic(a) => Part::Analytic((**a).clone()),
320                Value::Continuous(f) => Part::Continuous(**f),
321                v => Part::Point(v.as_f64()?),
322            };
323            Some((part, *p))
324        })
325        .collect::<Option<_>>()?;
326    Some(Mixture { parts })
327}
328
329/// `op` between a joint distribution and a value that isn't one, outcome
330/// by outcome: joint again, or an ordinary distribution of what's left,
331/// like the truth values of comparisons.
332#[cold]
333#[inline(never)]
334pub fn binary(op: BinOp, a: &Value, b: &Value, budget: &mut Budget) -> OpResult<Value> {
335    let (d, other, flipped) = match (a, b) {
336        (Value::Joint(d), other) => (d, other, false),
337        (other, Value::Joint(d)) => (d, other, true),
338        _ => unreachable!("an operand is joint"),
339    };
340    if matches!(other, Value::Joint(_)) || analytic::contains(other) {
341        return Err(OpError::unsupported(format!(
342            "`{}` between a `simulate` distribution of continuous outcomes and another distribution of them, or an outcome of a draw, isn't supported yet",
343            op.symbol()
344        ))
345        .help("draw from it first, with `~`"));
346    }
347    let mut pairs = Vec::with_capacity(d.outcomes.len());
348    for (v, p) in &d.outcomes {
349        let r = if flipped {
350            crate::ops::binary(op, other, v, budget)?
351        } else {
352            crate::ops::binary(op, v, other, budget)?
353        };
354        pairs.push((r, *p));
355    }
356    of(pairs, d.missing, budget)
357}