Skip to main content

probl_engine/
likelihood.rs

1//! Likelihoods of one latent, for `@integrate` (docs/semantics.md, section
2//! 13): what observations whose parameters are outcomes of the latent say
3//! about it, as a weight on its distribution. The latent keeps its
4//! distribution and its CDF coordinates `u`, and its posterior there has
5//! the density w(u) ∝ Π L(x(u)) for x(u) its quantile. Probabilities,
6//! quantiles and moments come from integrating w numerically, to a relative
7//! tolerance, with a table of its integral shared by every question.
8
9use crate::analytic::{Domain, Pieces};
10use crate::continuous::Family;
11use crate::dist::Budget;
12use crate::error::OpResult;
13use crate::quadrature::{self, Table};
14use crate::value::{family_key, float_key};
15use std::sync::Mutex;
16
17/// The distribution an observation is from.
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub enum Law {
20    Normal,
21    Lognormal,
22    Uniform,
23    Beta,
24    Gamma,
25    Exponential,
26    Binomial,
27    Bernoulli,
28    Poisson,
29    Geometric,
30}
31
32impl Law {
33    pub fn name(self) -> &'static str {
34        match self {
35            Law::Normal => "normal",
36            Law::Lognormal => "lognormal",
37            Law::Uniform => "uniform",
38            Law::Beta => "beta",
39            Law::Gamma => "gamma",
40            Law::Exponential => "exponential",
41            Law::Binomial => "binomial",
42            Law::Bernoulli => "bernoulli",
43            Law::Poisson => "poisson",
44            Law::Geometric => "geometric",
45        }
46    }
47
48    /// Whether what it gives is a density.
49    pub fn is_density(self) -> bool {
50        !matches!(self, Law::Binomial | Law::Bernoulli | Law::Poisson | Law::Geometric)
51    }
52
53    /// The logarithm of the probability, or density, of `v` under the law
54    /// with these parameters: −∞ for what it can't give, and NaN for
55    /// parameters it doesn't take.
56    pub fn ln_likelihood(self, params: &[f64], v: f64) -> f64 {
57        let continuous = |family: OpResult<Family>| family.map_or(f64::NAN, |f| f.ln_pdf(v));
58        let count = v >= 0.0 && v.fract() == 0.0;
59        match (self, params) {
60            (Law::Normal, &[m, s]) => continuous(Family::normal(m, s)),
61            (Law::Lognormal, &[m, s]) => continuous(Family::lognormal(m, s)),
62            (Law::Uniform, &[a, b]) => continuous(Family::uniform(a, b)),
63            (Law::Beta, &[a, b]) => continuous(Family::beta(a, b)),
64            (Law::Gamma, &[k, s]) => continuous(Family::gamma(k, s)),
65            (Law::Exponential, &[r]) => continuous(Family::exponential(r)),
66            (_, &[.., p])
67                if matches!(self, Law::Binomial | Law::Bernoulli | Law::Geometric) && !(0.0..=1.0).contains(&p) =>
68            {
69                f64::NAN
70            }
71            (Law::Bernoulli, &[p]) => match v {
72                1.0 => libm::log(p),
73                0.0 => libm::log1p(-p),
74                _ => f64::NEG_INFINITY,
75            },
76            (Law::Binomial, &[n, p]) => {
77                if !count || v > n {
78                    return f64::NEG_INFINITY;
79                }
80                let ln_choose = libm::lgamma(n + 1.0) - libm::lgamma(v + 1.0) - libm::lgamma(n - v + 1.0);
81                let success = if v == 0.0 { 0.0 } else { v * libm::log(p) };
82                let failure = if v == n { 0.0 } else { (n - v) * libm::log1p(-p) };
83                ln_choose + success + failure
84            }
85            (Law::Poisson, &[rate]) if rate > 0.0 => {
86                if !count {
87                    return f64::NEG_INFINITY;
88                }
89                v * libm::log(rate) - rate - libm::lgamma(v + 1.0)
90            }
91            (Law::Poisson, &[0.0]) => {
92                if v == 0.0 {
93                    0.0
94                } else {
95                    f64::NEG_INFINITY
96                }
97            }
98            (Law::Poisson, _) => f64::NAN,
99            // Tries up to and including the first success.
100            (Law::Geometric, &[p]) => {
101                if !count || v < 1.0 {
102                    return f64::NEG_INFINITY;
103                }
104                let failures = if v == 1.0 { 0.0 } else { (v - 1.0) * libm::log1p(-p) };
105                failures + libm::log(p)
106            }
107            _ => f64::NAN,
108        }
109    }
110}
111
112/// A parameter of an observation: a number, or an outcome of the latent,
113/// by its pieces in the latent's coordinates.
114#[derive(Clone, Debug, PartialEq)]
115pub enum Param {
116    Plain(f64),
117    Outcome(Pieces),
118}
119
120impl Param {
121    fn at(&self, u: f64, x: f64) -> f64 {
122        match self {
123            Param::Plain(v) => *v,
124            Param::Outcome(pieces) => {
125                let i = pieces.partition_point(|(end, _)| *end < u).min(pieces.len() - 1);
126                pieces[i].1.at(x)
127            }
128        }
129    }
130}
131
132/// One observation of `value` from `law`, with its parameters.
133#[derive(Clone, Debug, PartialEq)]
134pub struct Factor {
135    pub law: Law,
136    pub params: Vec<Param>,
137    pub value: f64,
138}
139
140type FactorKey = (
141    &'static str,
142    Vec<Result<u64, Vec<(u64, (u8, u64, u64, u64, u64))>>>,
143    u64,
144);
145
146impl Factor {
147    fn key(&self) -> FactorKey {
148        (
149            self.law.name(),
150            self.params
151                .iter()
152                .map(|p| match p {
153                    Param::Plain(v) => Ok(float_key(*v)),
154                    Param::Outcome(pieces) => Err(pieces.iter().map(|(end, f)| (float_key(*end), f.key())).collect()),
155                })
156                .collect(),
157            float_key(self.value),
158        )
159    }
160
161    fn ln_at(&self, u: f64, x: f64) -> f64 {
162        let mut params = [0.0; 3];
163        for (i, p) in self.params.iter().enumerate() {
164            params[i] = p.at(u, x);
165        }
166        self.law.ln_likelihood(&params[..self.params.len()], self.value)
167    }
168
169    /// Where its outcome parameters change function, in the coordinates.
170    fn breaks(&self, out: &mut Vec<f64>) {
171        for p in &self.params {
172            if let Param::Outcome(pieces) = p {
173                out.extend(pieces.iter().map(|(end, _)| *end));
174            }
175        }
176    }
177}
178
179/// The product of what observations of a latent say about it, as a weight
180/// on its distribution, with the table of its integral.
181#[derive(Debug)]
182pub struct Likelihood {
183    /// The latent's distribution, in whose coordinates it's a function.
184    pub family: Family,
185    pub factors: Vec<Factor>,
186    /// The relative tolerance its integrals are computed to.
187    pub tolerance: f64,
188    /// The logarithm of the weight where it's largest, about: the weight is
189    /// computed relative to it, so that it neither overflows nor underflows.
190    pub shift: f64,
191    table: Table,
192    /// Moments already integrated, by what they're of: a value drawn again
193    /// and again, when sampling, asks for the same ones.
194    moments: Mutex<Vec<(MomentKey, Moments)>>,
195}
196
197/// A mean and variance, if integrating them converged.
198type Moments = Option<(f64, f64)>;
199
200/// An outcome's pieces and domain, which its moments are of.
201pub type MomentKey = (Vec<(u64, (u8, u64, u64, u64, u64))>, Vec<(u64, u64)>);
202
203impl Clone for Likelihood {
204    fn clone(&self) -> Likelihood {
205        Likelihood {
206            family: self.family,
207            factors: self.factors.clone(),
208            tolerance: self.tolerance,
209            shift: self.shift,
210            table: self.table.clone(),
211            moments: Mutex::new(Vec::new()),
212        }
213    }
214}
215
216/// How many points of the coordinates are tried for where the weight is
217/// largest, before refining around the best.
218const GRID: usize = 4096;
219
220impl Likelihood {
221    /// The likelihood of these factors, for a latent distributed by
222    /// `family`; `None` where every value of the latent rules out what was
223    /// observed.
224    pub fn new(
225        family: Family,
226        factors: Vec<Factor>,
227        tolerance: f64,
228        budget: &mut Budget,
229    ) -> OpResult<Option<Likelihood>> {
230        let ln_at = |u: f64| {
231            let x = family.quantile(u);
232            factors.iter().map(|f| f.ln_at(u, x)).sum::<f64>()
233        };
234        budget.work((GRID * factors.len()) as u64)?;
235        // The largest weights, on a grid, then refined around the best ones.
236        let mut grid: Vec<(f64, f64)> = (0..GRID)
237            .map(|i| {
238                let u = (i as f64 + 0.5) / GRID as f64;
239                (u, nan_to_impossible(ln_at(u)))
240            })
241            .collect();
242        let shift = grid.iter().map(|g| g.1).fold(f64::NEG_INFINITY, f64::max);
243        if shift == f64::NEG_INFINITY {
244            return Ok(None);
245        }
246        let mut peaks: Vec<(f64, f64)> = local_peaks(&grid);
247        peaks.sort_by(|a, b| b.1.total_cmp(&a.1));
248        peaks.truncate(4);
249        let mut shift = shift;
250        let mut breaks = vec![0.0, 1.0];
251        for f in &factors {
252            f.breaks(&mut breaks);
253        }
254        for &(peak, _) in &peaks {
255            let (u, ln, width) = refine(&ln_at, peak, 1.0 / GRID as f64, budget, factors.len())?;
256            shift = shift.max(ln);
257            // Breakpoints graded away from the peak, so that the rule sees
258            // its flanks whatever its width.
259            breaks.push(u);
260            let mut d = width;
261            while d < 1.0 {
262                breaks.extend([u - d, u + d]);
263                d *= 2.0;
264            }
265        }
266        grid.clear();
267        breaks.retain(|b| (0.0..=1.0).contains(b));
268        breaks.sort_by(f64::total_cmp);
269        breaks.dedup();
270        let mut f = |u: f64| relative(ln_at(u), shift);
271        let parts = quadrature::adaptive(&mut f, &breaks, tolerance, 0.0, budget)?;
272        let table = Table::new(parts);
273        if table.total() <= 0.0 {
274            return Ok(None);
275        }
276        Ok(Some(Likelihood {
277            family,
278            factors,
279            tolerance,
280            shift,
281            table,
282            moments: Mutex::new(Vec::new()),
283        }))
284    }
285
286    pub fn key(&self) -> (Vec<FactorKey>, (&'static str, Vec<u64>), u64) {
287        (
288            self.factors.iter().map(Factor::key).collect(),
289            family_key(&self.family),
290            float_key(self.tolerance),
291        )
292    }
293
294    /// The logarithm of the weight at `u`.
295    pub fn ln_at(&self, u: f64) -> f64 {
296        let x = self.family.quantile(u);
297        nan_to_impossible(self.factors.iter().map(|f| f.ln_at(u, x)).sum())
298    }
299
300    /// The weight at `u`, relative to `exp(shift)`.
301    pub fn at(&self, u: f64) -> f64 {
302        relative(self.ln_at(u), self.shift)
303    }
304
305    /// The weight's integral over the intervals of `d`, relative to
306    /// `exp(shift)`.
307    pub fn mass(&self, d: &Domain) -> f64 {
308        let mut budget = Budget::unlimited();
309        let mut f = |u: f64| self.at(u);
310        d.0.iter()
311            .map(|&(a, b)| self.table.between(&mut f, a, b, &mut budget).unwrap_or(0.0))
312            .sum()
313    }
314
315    /// The coordinate where the weight's integral over `d`, from its start,
316    /// is `share` of the whole.
317    pub fn inverse(&self, d: &Domain, share: f64) -> f64 {
318        let mut budget = Budget::unlimited();
319        let mut f = |u: f64| self.at(u);
320        let total = self.mass(d);
321        let mut left = share.clamp(0.0, 1.0) * total;
322        for &(a, b) in &d.0 {
323            let here = self.table.between(&mut f, a, b, &mut budget).unwrap_or(0.0);
324            if left <= here {
325                let start = self.table.up_to(&mut f, a, &mut budget).unwrap_or(0.0);
326                let u = self.table.inverse(&mut f, start + left, &mut budget).unwrap_or(a);
327                return u.clamp(a, b);
328            }
329            left -= here;
330        }
331        d.0.last().map_or(0.5, |&(_, b)| b)
332    }
333
334    /// The moments of an outcome, by its `key`, as `compute` gives them the
335    /// first time.
336    pub fn moments(&self, key: MomentKey, compute: impl FnOnce() -> Option<(f64, f64)>) -> Option<(f64, f64)> {
337        if let Some((_, m)) = self.moments.lock().expect("moments").iter().find(|(k, _)| *k == key) {
338            return *m;
339        }
340        let m = compute();
341        let mut cache = self.moments.lock().expect("moments");
342        if cache.len() >= 64 {
343            cache.clear();
344        }
345        cache.push((key, m));
346        m
347    }
348
349    /// ∫ g(u) w(u) du over `[a, b]`, relative to `exp(shift)`, to the
350    /// tolerance: `None` if it doesn't converge.
351    pub fn integral(&self, g: impl Fn(f64) -> f64, a: f64, b: f64) -> Option<f64> {
352        quadrature::moment(
353            |u| {
354                let w = self.at(u);
355                if w == 0.0 { 0.0 } else { g(u) * w }
356            },
357            a,
358            b,
359            self.tolerance,
360        )
361    }
362}
363
364impl PartialEq for Likelihood {
365    fn eq(&self, other: &Likelihood) -> bool {
366        self.key() == other.key()
367    }
368}
369
370fn nan_to_impossible(ln: f64) -> f64 {
371    if ln.is_nan() { f64::NEG_INFINITY } else { ln }
372}
373
374fn relative(ln: f64, shift: f64) -> f64 {
375    if ln == f64::NEG_INFINITY {
376        0.0
377    } else {
378        crate::math::exp(ln - shift)
379    }
380}
381
382/// The grid points at least as high as the one before them, and higher than
383/// the one after, with their values.
384fn local_peaks(grid: &[(f64, f64)]) -> Vec<(f64, f64)> {
385    let n = grid.len();
386    (0..n)
387        .filter(|&i| {
388            let v = grid[i].1;
389            v > f64::NEG_INFINITY && (i == 0 || grid[i - 1].1 <= v) && (i + 1 == n || grid[i + 1].1 < v)
390        })
391        .map(|i| grid[i])
392        .collect()
393}
394
395/// Around a grid peak at `u` with spacing `h`: a better peak, its log
396/// weight, and how far from it the weight falls by e⁻¹⁰, at least.
397fn refine(
398    ln_at: &impl Fn(f64) -> f64,
399    u: f64,
400    h: f64,
401    budget: &mut Budget,
402    factors: usize,
403) -> OpResult<(f64, f64, f64)> {
404    let (mut best, mut ln, mut h) = (u, nan_to_impossible(ln_at(u)), h);
405    for _ in 0..6 {
406        budget.work((65 * factors) as u64)?;
407        for i in 0..=64 {
408            let v = (best - h + 2.0 * h * i as f64 / 64.0).clamp(1e-300, 1.0 - 1e-16);
409            let l = nan_to_impossible(ln_at(v));
410            if l > ln {
411                ln = l;
412                best = v;
413            }
414        }
415        h /= 16.0;
416    }
417    // How wide the peak is: the nearest distance at which it has fallen by
418    // e⁻¹⁰ on either side, among distances that double.
419    let mut width = h;
420    while width < 0.5 {
421        let lower = nan_to_impossible(ln_at((best - width).max(0.0)));
422        let upper = nan_to_impossible(ln_at((best + width).min(1.0)));
423        if lower < ln - 10.0 || upper < ln - 10.0 {
424            break;
425        }
426        width *= 2.0;
427    }
428    Ok((best, ln, (width / 8.0).max(1e-15)))
429}