#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/docs/core/conjugate.md"))]
use crate::addr;
use crate::core::distribution::{Distribution, Gamma, LogF64};
use crate::core::model::{factor, pure, sample, sequence_vec, Model, ModelExt};
use crate::core::numerical::log_sum_exp;
use crate::error::{ErrorCode, FugueError, FugueResult};
use rand::distributions::Open01;
use rand::{Rng, RngCore};
use rand_distr::{Binomial as RDBinomial, Distribution as RandDistr, Gamma as RDGamma};
pub const SIMPLEX_TOLERANCE: f64 = 1e-6;
pub(crate) fn validate_concentrations(who: &str, alpha: &[f64]) -> FugueResult<f64> {
if alpha.is_empty() {
return Err(FugueError::invalid_parameters(
who,
"Concentration vector cannot be empty",
ErrorCode::InvalidCount,
)
.with_context("length", "0"));
}
for (i, &a) in alpha.iter().enumerate() {
check_positive_finite(who, "concentration", a)
.map_err(|e| e.with_context("index", i.to_string()))?;
}
let total: f64 = alpha.iter().sum();
check_total_concentration(who, total)?;
Ok(total)
}
fn validate_beta_pair(who: &str, a: f64, b: f64) -> FugueResult<()> {
check_positive_finite(who, "a", a)?;
check_positive_finite(who, "b", b)?;
check_total_concentration(who, a + b)
}
fn check_positive_finite(who: &str, name: &str, value: f64) -> FugueResult<()> {
if value > 0.0 && value.is_finite() {
return Ok(());
}
Err(FugueError::invalid_parameters(
who,
format!("Parameter {} must be positive and finite", name),
ErrorCode::InvalidShape,
)
.with_context(name, value.to_string())
.with_context("expected", "> 0.0 and finite"))
}
fn check_total_concentration(who: &str, total: f64) -> FugueResult<()> {
if libm::lgamma(total).is_finite() {
return Ok(());
}
Err(FugueError::invalid_parameters(
who,
"Sum of concentrations is too large: its lnΓ overflows f64",
ErrorCode::InvalidShape,
)
.with_context("sum", total.to_string())
.with_context("expected", "<= 2.5e305"))
}
pub(crate) fn validate_probabilities(who: &str, p: &[f64]) -> FugueResult<()> {
if p.is_empty() {
return Err(FugueError::invalid_parameters(
who,
"Probability vector cannot be empty",
ErrorCode::InvalidProbability,
)
.with_context("length", "0"));
}
for (i, &pi) in p.iter().enumerate() {
if !pi.is_finite() || pi < 0.0 {
return Err(FugueError::invalid_parameters(
who,
"All probabilities must be non-negative and finite",
ErrorCode::InvalidProbability,
)
.with_context("index", i.to_string())
.with_context("value", pi.to_string())
.with_context("expected", ">= 0.0 and finite"));
}
}
let sum: f64 = p.iter().sum();
if (sum - 1.0).abs() > SIMPLEX_TOLERANCE {
return Err(FugueError::invalid_parameters(
who,
"Probabilities must sum to 1.0",
ErrorCode::InvalidProbability,
)
.with_context("sum", sum.to_string())
.with_context("expected", "1.0")
.with_context("tolerance", SIMPLEX_TOLERANCE.to_string()));
}
Ok(())
}
fn check_same_len(who: &str, what: &str, categories: usize, len: usize) -> FugueResult<()> {
if len == categories {
return Ok(());
}
Err(FugueError::invalid_parameters(
who,
format!("{} must have one entry per category", what),
ErrorCode::InvalidCount,
)
.with_context("categories", categories.to_string())
.with_context(what, len.to_string()))
}
fn total_count(who: &str, counts: &[u64]) -> FugueResult<u64> {
counts
.iter()
.try_fold(0u64, |acc, &n| acc.checked_add(n))
.ok_or_else(|| {
FugueError::invalid_parameters(
who,
"Total count overflows u64",
ErrorCode::InvalidCount,
)
.with_context("max", u64::MAX.to_string())
})
}
fn validate_alpha_counts(who: &str, alpha: &[f64], counts: &[u64]) -> FugueResult<(f64, u64)> {
let a_total = validate_concentrations(who, alpha)?;
check_same_len(who, "counts", alpha.len(), counts.len())?;
let n_total = total_count(who, counts)?;
Ok((a_total, n_total))
}
const STIRLING_MIN: f64 = 10.0;
fn stirling_tail(z: f64) -> f64 {
let y = 1.0 / (z * z);
(1.0 / 12.0
- y * (1.0 / 360.0
- y * (1.0 / 1260.0
- y * (1.0 / 1680.0 - y * (1.0 / 1188.0 - y * (691.0 / 360_360.0 - y / 156.0))))))
/ z
}
fn ln_gamma_ratio(z: f64, a: f64, b: f64) -> f64 {
let x = z + a;
let y = z + b;
if x < STIRLING_MIN || y < STIRLING_MIN {
return libm::lgamma(x) - libm::lgamma(y);
}
let d = a - b;
let t = d / y;
let ln_x_over_y = if t > -0.5 { t.ln_1p() } else { x.ln() - y.ln() };
(x - 0.5) * ln_x_over_y + d * (y.ln() - 1.0) + (stirling_tail(x) - stirling_tail(y))
}
fn ln_polya_coefficient(x: f64, m: f64) -> f64 {
if m == 0.0 {
return 0.0;
}
if x >= m + 1.0 {
ln_gamma_ratio(x, m, 0.0) - libm::lgamma(m + 1.0)
} else {
ln_gamma_ratio(m, x, 1.0) - libm::lgamma(x)
}
}
fn ln_mbeta(alpha: &[f64]) -> f64 {
let j = (0..alpha.len()).fold(0, |best, i| if alpha[i] > alpha[best] { i } else { best });
let rest: f64 = alpha
.iter()
.enumerate()
.filter(|&(i, _)| i != j)
.map(|(_, &a)| a)
.sum();
let others: f64 = alpha
.iter()
.enumerate()
.filter(|&(i, _)| i != j)
.map(|(_, &a)| libm::lgamma(a))
.sum();
others - ln_gamma_ratio(alpha[j], rest, 0.0)
}
fn ln_mcoef(counts: &[u64]) -> f64 {
let mut before = 0u64;
let mut out = 0.0;
for &n in counts {
out += ln_polya_coefficient(before as f64 + 1.0, n as f64);
before += n;
}
out
}
fn dm_log_marginal(alpha: &[f64], a_total: f64, counts: &[u64], n_total: u64) -> f64 {
let per_category: f64 = alpha
.iter()
.zip(counts)
.map(|(&a, &n)| ln_polya_coefficient(a, n as f64))
.sum();
per_category - ln_polya_coefficient(a_total, n_total as f64)
}
fn dirichlet_log_density(alpha: &[f64], ln_beta: f64, x: &[f64]) -> LogF64 {
if x.len() != alpha.len() {
return f64::NEG_INFINITY;
}
let mut sum = 0.0;
for &xi in x {
if !xi.is_finite() || xi < 0.0 {
return f64::NEG_INFINITY;
}
sum += xi;
}
if (sum - 1.0).abs() > SIMPLEX_TOLERANCE {
return f64::NEG_INFINITY;
}
let mut log_density = -ln_beta;
let mut diverges = false;
for (&a, &xi) in alpha.iter().zip(x) {
if xi == 0.0 {
if a > 1.0 {
return f64::NEG_INFINITY;
}
diverges |= a < 1.0;
} else {
log_density += (a - 1.0) * xi.ln();
}
}
if diverges {
f64::INFINITY
} else {
log_density
}
}
fn multinomial_log_mass(p: &[f64], counts: &[u64]) -> LogF64 {
let mut log_mass = ln_mcoef(counts);
for (&pi, &n) in p.iter().zip(counts) {
if n == 0 {
continue;
}
if pi <= 0.0 {
return f64::NEG_INFINITY;
}
log_mass += n as f64 * pi.ln();
}
log_mass
}
pub fn ln_multivariate_beta(alpha: &[f64]) -> FugueResult<f64> {
validate_concentrations("Dirichlet", alpha)?;
Ok(ln_mbeta(alpha))
}
pub fn ln_multinomial_coefficient(counts: &[u64]) -> FugueResult<f64> {
if counts.is_empty() {
return Err(FugueError::invalid_parameters(
"Multinomial",
"Count vector cannot be empty",
ErrorCode::InvalidCount,
)
.with_context("length", "0"));
}
total_count("Multinomial", counts)?;
Ok(ln_mcoef(counts))
}
pub fn dirichlet_log_pdf(alpha: &[f64], x: &[f64]) -> FugueResult<f64> {
validate_concentrations("Dirichlet", alpha)?;
check_same_len("Dirichlet", "x", alpha.len(), x.len())?;
Ok(dirichlet_log_density(alpha, ln_mbeta(alpha), x))
}
pub fn multinomial_log_pmf(p: &[f64], counts: &[u64]) -> FugueResult<f64> {
validate_probabilities("Multinomial", p)?;
check_same_len("Multinomial", "counts", p.len(), counts.len())?;
total_count("Multinomial", counts)?;
Ok(multinomial_log_mass(p, counts))
}
pub fn dirichlet_multinomial_log_marginal(alpha: &[f64], counts: &[u64]) -> FugueResult<f64> {
let (a_total, n_total) = validate_alpha_counts("DirichletMultinomial", alpha, counts)?;
Ok(dm_log_marginal(alpha, a_total, counts, n_total))
}
pub fn dirichlet_categorical_log_marginal(alpha: &[f64], counts: &[u64]) -> FugueResult<f64> {
let (a_total, n_total) = validate_alpha_counts("DirichletCategorical", alpha, counts)?;
Ok(dm_log_marginal(alpha, a_total, counts, n_total) - ln_mcoef(counts))
}
pub fn beta_binomial_log_marginal(a: f64, b: f64, k: u64, n: u64) -> FugueResult<f64> {
validate_beta_pair("BetaBinomial", a, b)?;
if k > n {
return Ok(f64::NEG_INFINITY);
}
Ok(dm_log_marginal(&[a, b], a + b, &[k, n - k], n))
}
pub fn beta_bernoulli_log_marginal(
a: f64,
b: f64,
successes: u64,
failures: u64,
) -> FugueResult<f64> {
validate_beta_pair("BetaBernoulli", a, b)?;
let counts = [successes, failures];
let n = total_count("BetaBernoulli", &counts)?;
Ok(dm_log_marginal(&[a, b], a + b, &counts, n) - ln_mcoef(&counts))
}
pub fn dirichlet_posterior(alpha: &[f64], counts: &[u64]) -> FugueResult<Vec<f64>> {
validate_alpha_counts("Dirichlet", alpha, counts)?;
Ok(alpha
.iter()
.zip(counts)
.map(|(&a, &n)| a + n as f64)
.collect())
}
pub fn beta_posterior(a: f64, b: f64, successes: u64, failures: u64) -> FugueResult<(f64, f64)> {
validate_beta_pair("Beta", a, b)?;
Ok((a + successes as f64, b + failures as f64))
}
pub fn dirichlet_predictive(alpha: &[f64], counts: &[u64]) -> FugueResult<Vec<f64>> {
let post = dirichlet_posterior(alpha, counts)?;
let total: f64 = post.iter().sum();
Ok(post.into_iter().map(|w| w / total).collect())
}
pub fn dirichlet_log_predictive(alpha: &[f64], counts: &[u64]) -> FugueResult<Vec<f64>> {
let post = dirichlet_posterior(alpha, counts)?;
let ln_total = post.iter().sum::<f64>().ln();
Ok(post.into_iter().map(|w| w.ln() - ln_total).collect())
}
fn ln_gamma_variate(shape: f64, rng: &mut dyn RngCore) -> f64 {
if shape >= 1.0 {
RDGamma::new(shape, 1.0)
.expect("shape is positive and finite")
.sample(rng)
.ln()
} else {
let boosted = RDGamma::new(shape + 1.0, 1.0)
.expect("shape + 1 is positive and finite")
.sample(rng);
let u: f64 = rng.sample(Open01);
boosted.ln() + u.ln() / shape
}
}
fn vertex_index(alpha: &[f64], rng: &mut dyn RngCore) -> usize {
let total: f64 = alpha.iter().sum();
let target = rng.gen::<f64>() * total;
let mut acc = 0.0;
for (i, &a) in alpha.iter().enumerate() {
acc += a;
if target < acc {
return i;
}
}
alpha.len() - 1
}
#[derive(Clone, Debug)]
pub struct Dirichlet {
alpha: Vec<f64>,
ln_beta: f64,
}
impl Dirichlet {
pub fn new(alpha: Vec<f64>) -> FugueResult<Self> {
validate_concentrations("Dirichlet", &alpha)?;
let ln_beta = ln_mbeta(&alpha);
Ok(Dirichlet { alpha, ln_beta })
}
pub fn alpha(&self) -> &[f64] {
&self.alpha
}
pub fn len(&self) -> usize {
self.alpha.len()
}
pub fn is_empty(&self) -> bool {
self.alpha.is_empty()
}
pub fn sample_log(&self, rng: &mut dyn RngCore) -> Vec<f64> {
match self.shifted_log_gammas(rng) {
Ok(shifted) => {
let ln_total = log_sum_exp(&shifted);
shifted.into_iter().map(|l| l - ln_total).collect()
}
Err(vertex) => (0..self.alpha.len())
.map(|i| if i == vertex { 0.0 } else { f64::NEG_INFINITY })
.collect(),
}
}
fn shifted_log_gammas(&self, rng: &mut dyn RngCore) -> Result<Vec<f64>, usize> {
let ln_g: Vec<f64> = self
.alpha
.iter()
.map(|&a| ln_gamma_variate(a, rng))
.collect();
let max = ln_g.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if max == f64::NEG_INFINITY {
return Err(vertex_index(&self.alpha, rng));
}
Ok(ln_g.into_iter().map(|l| l - max).collect())
}
}
impl Distribution<Vec<f64>> for Dirichlet {
fn sample(&self, rng: &mut dyn RngCore) -> Vec<f64> {
match self.shifted_log_gammas(rng) {
Ok(shifted) => {
let weights: Vec<f64> = shifted.into_iter().map(f64::exp).collect();
let total: f64 = weights.iter().sum();
weights.into_iter().map(|w| w / total).collect()
}
Err(vertex) => (0..self.alpha.len())
.map(|i| if i == vertex { 1.0 } else { 0.0 })
.collect(),
}
}
fn log_prob(&self, x: &Vec<f64>) -> LogF64 {
dirichlet_log_density(&self.alpha, self.ln_beta, x)
}
fn clone_box(&self) -> Box<dyn Distribution<Vec<f64>>> {
Box::new(self.clone())
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
}
#[derive(Clone, Debug)]
pub struct Multinomial {
n: u64,
probs: Vec<f64>,
}
impl Multinomial {
pub fn new(n: u64, probs: Vec<f64>) -> FugueResult<Self> {
validate_probabilities("Multinomial", &probs)?;
Ok(Multinomial { n, probs })
}
pub fn n(&self) -> u64 {
self.n
}
pub fn probs(&self) -> &[f64] {
&self.probs
}
pub fn len(&self) -> usize {
self.probs.len()
}
pub fn is_empty(&self) -> bool {
self.probs.is_empty()
}
}
impl Distribution<Vec<u64>> for Multinomial {
fn sample(&self, rng: &mut dyn RngCore) -> Vec<u64> {
let k = self.probs.len();
let mut tail = vec![0.0; k + 1];
for i in (0..k).rev() {
tail[i] = tail[i + 1] + self.probs[i];
}
let mut counts = vec![0u64; k];
let mut remaining = self.n;
for i in 0..k {
if remaining == 0 || tail[i] <= 0.0 {
break;
}
let q = (self.probs[i] / tail[i]).min(1.0);
let c = if q >= 1.0 {
remaining
} else {
RDBinomial::new(remaining, q)
.expect("conditional probability is in [0, 1]")
.sample(rng)
};
counts[i] = c;
remaining -= c;
}
if remaining > 0 {
if let Some(j) = self.probs.iter().rposition(|&p| p > 0.0) {
counts[j] += remaining;
}
}
counts
}
fn log_prob(&self, x: &Vec<u64>) -> LogF64 {
if x.len() != self.probs.len() {
return f64::NEG_INFINITY;
}
match x.iter().try_fold(0u64, |acc, &c| acc.checked_add(c)) {
Some(total) if total == self.n => multinomial_log_mass(&self.probs, x),
_ => f64::NEG_INFINITY,
}
}
fn clone_box(&self) -> Box<dyn Distribution<Vec<u64>>> {
Box::new(self.clone())
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
}
fn normalize_weights(w: &[f64]) -> Option<Vec<f64>> {
let mut max = 0.0_f64;
for &v in w {
if !(v >= 0.0 && v.is_finite()) {
return None;
}
max = max.max(v);
}
if max <= 0.0 {
return None;
}
let scaled: Vec<f64> = w.iter().map(|&v| v / max).collect();
let total: f64 = scaled.iter().sum();
Some(scaled.into_iter().map(|v| v / total).collect())
}
fn normalize_gamma_sites(g: Vec<f64>) -> Model<Vec<f64>> {
match normalize_weights(&g) {
Some(x) => pure(x),
None => {
let k = g.len();
factor(f64::NEG_INFINITY).map(move |()| vec![1.0 / k as f64; k])
}
}
}
pub fn sample_dirichlet(name: &str, alpha: &[f64]) -> FugueResult<Model<Vec<f64>>> {
validate_concentrations("Dirichlet", alpha)?;
let sites = alpha
.iter()
.enumerate()
.map(|(i, &a)| Ok(sample(addr!(name, i), Gamma::new(a, 1.0)?)))
.collect::<FugueResult<Vec<Model<f64>>>>()?;
Ok(sequence_vec(sites).bind(normalize_gamma_sites))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::address::Address;
use crate::core::distribution::{Beta, Binomial, Categorical, Normal};
use crate::core::model::{observe, traverse_vec};
use crate::error::Validate;
use crate::inference::mh::adaptive_mcmc_chain;
use crate::runtime::handler::run;
use crate::runtime::interpreters::{PriorHandler, ScoreGivenTrace};
use crate::runtime::trace::{ChoiceValue, Trace};
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::f64::consts::{FRAC_PI_2, LN_2};
#[test]
fn the_vector_distributions_downcast_to_themselves() {
let d: Box<dyn Distribution<Vec<f64>>> = Box::new(Dirichlet::new(vec![1.0, 2.0]).unwrap());
assert_eq!(
d.downcast_ref::<Dirichlet>().map(|d| d.alpha().to_vec()),
Some(vec![1.0, 2.0])
);
let m: Box<dyn Distribution<Vec<u64>>> =
Box::new(Multinomial::new(3, vec![0.5, 0.5]).unwrap());
assert_eq!(m.downcast_ref::<Multinomial>().map(|m| m.n()), Some(3));
assert!(m.clone_box().downcast_ref::<Multinomial>().is_some());
}
const NEG_INF: f64 = f64::NEG_INFINITY;
fn close(got: f64, want: f64, tol: f64) {
assert!(
(got - want).abs() <= tol,
"expected {want}, got {got} (|diff| = {:e}, tol {tol:e})",
(got - want).abs()
);
}
fn close_rel(got: f64, want: f64, tol: f64) {
close(got, want, tol * want.abs().max(1.0));
}
fn lgamma(x: f64) -> f64 {
libm::lgamma(x)
}
fn err_code<T>(result: FugueResult<T>) -> ErrorCode {
match result {
Ok(_) => panic!("expected an error"),
Err(e) => e.code(),
}
}
fn prior_run<A>(seed: u64, model: Model<A>) -> (A, Trace) {
let mut rng = StdRng::seed_from_u64(seed);
run(
PriorHandler {
rng: &mut rng,
trace: Trace::default(),
},
model,
)
}
#[test]
fn ln_gamma_ratio_matches_high_precision_references() {
close_rel(ln_gamma_ratio(1e9, 1e-8, 1.0), -20.723265629713755, 4e-15);
close_rel(ln_gamma_ratio(12.5, 0.5, 1.0), -1.272861660582815, 4e-15);
close_rel(ln_gamma_ratio(1e15, 2.5, 0.0), 86.34694098727671, 4e-15);
close_rel(ln_gamma_ratio(1e6, 1e3, 1.0), 13802.194381072677, 4e-15);
close_rel(ln_gamma_ratio(10.0, 1e12, 0.0), 26631021116179.16, 4e-15);
close_rel(ln_gamma_ratio(3.0, 1e-3, 2.5), -3.2637438052823198, 4e-15);
for &z in &[10.5, 1e3, 1e9, 1e15] {
close_rel(ln_gamma_ratio(z, 1.0, 0.0), z.ln(), 4e-15);
assert_eq!(ln_gamma_ratio(z, 0.25, 0.25), 0.0);
}
close_rel(ln_gamma_ratio(0.0, 10.0, 1e12), -26631021115902.85, 4e-15);
close_rel(
ln_gamma_ratio(10.0, 10.0, 1e6 - 10.0),
-12815465.229263425,
4e-15,
);
}
#[test]
fn ln_polya_coefficient_matches_exact_binomial_coefficients() {
for n in 0u64..=60 {
let mut c: u128 = 1;
for k in 0..=n {
if k > 0 {
c = c * u128::from(n - k + 1) / u128::from(k);
}
close_rel(
ln_polya_coefficient(k as f64 + 1.0, (n - k) as f64),
(c as f64).ln(),
1e-14,
);
}
}
}
#[test]
fn ln_multivariate_beta_known_values() {
close(
ln_multivariate_beta(&[1.0, 1.0, 1.0]).unwrap(),
-LN_2,
1e-15,
);
close(
ln_multivariate_beta(&[2.0, 3.0]).unwrap(),
-(12f64.ln()),
1e-14,
);
close(ln_multivariate_beta(&[3.7]).unwrap(), 0.0, 1e-15);
for &(a, b) in &[(0.5, 0.5), (2.0, 5.0), (0.3, 7.5), (12.0, 40.0)] {
close(
ln_multivariate_beta(&[a, b]).unwrap(),
lgamma(a) + lgamma(b) - lgamma(a + b),
1e-12,
);
}
close_rel(
ln_multivariate_beta(&[1e9, 1.0]).unwrap(),
-(1e9f64.ln()),
4e-15,
);
close_rel(
ln_multivariate_beta(&[0.3, 7.0, 1e5]).unwrap(),
-76.36953663224247,
1e-14,
);
close_rel(
ln_multivariate_beta(&[1e-8, 1e-8]).unwrap(),
19.11382792451231,
1e-14,
);
close_rel(
ln_multivariate_beta(&[1e9, 1e9]).unwrap(),
-1386294370.2160115,
1e-14,
);
close_rel(
ln_multivariate_beta(&[2.5, 0.7, 11.0, 1e-4]).unwrap(),
1.7857625624285316,
1e-13,
);
}
#[test]
fn ln_multinomial_coefficient_known_values() {
close(
ln_multinomial_coefficient(&[2, 1, 1]).unwrap(),
12f64.ln(),
1e-14,
);
close(ln_multinomial_coefficient(&[0, 0, 0]).unwrap(), 0.0, 0.0);
close(ln_multinomial_coefficient(&[7]).unwrap(), 0.0, 0.0);
close_rel(
ln_multinomial_coefficient(&[1_000_000_000_000; 3]).unwrap(),
3295836865975.4097,
1e-14,
);
close_rel(
ln_multinomial_coefficient(&[999_999_999, 1]).unwrap(),
1e9f64.ln(),
4e-15,
);
}
#[test]
fn uniform_dirichlet_density_is_ln_2_on_the_triangle() {
let alpha = [1.0, 1.0, 1.0];
let third = 1.0 / 3.0;
for x in [
[0.2, 0.3, 0.5],
[third, third, third],
[0.98, 0.01, 0.01],
[1e-12, 0.5, 0.5 - 1e-12],
] {
close(dirichlet_log_pdf(&alpha, &x).unwrap(), LN_2, 1e-14);
}
close(
dirichlet_log_pdf(&alpha, &[0.0, 0.4, 0.6]).unwrap(),
LN_2,
1e-14,
);
close(
dirichlet_log_pdf(&alpha, &[1.0, 0.0, 0.0]).unwrap(),
LN_2,
1e-14,
);
}
#[test]
fn two_category_dirichlet_is_the_beta_distribution() {
for &(a, b) in &[
(1.0, 1.0),
(2.0, 5.0),
(0.5, 0.5),
(0.3, 2.7),
(40.0, 3.5),
(1e-3, 1e-3),
] {
let beta = Beta::new(a, b).unwrap();
for &x in &[1e-9, 0.01, 0.25, 0.5, 0.8, 0.999] {
let want = beta.log_prob(&x);
close_rel(
dirichlet_log_pdf(&[a, b], &[x, 1.0 - x]).unwrap(),
want,
1e-12,
);
}
assert_eq!(
dirichlet_log_pdf(&[a, b], &[0.0, 1.0]).unwrap(),
beta.log_prob(&0.0)
);
assert_eq!(
dirichlet_log_pdf(&[a, b], &[1.0, 0.0]).unwrap(),
beta.log_prob(&1.0)
);
}
}
#[test]
fn dirichlet_log_pdf_known_values() {
close_rel(
dirichlet_log_pdf(&[2.0, 3.0, 4.0], &[0.2, 0.3, 0.5]).unwrap(),
2.0228711901914416,
1e-14,
);
close_rel(
dirichlet_log_pdf(&[0.5; 4], &[0.1, 0.2, 0.3, 0.4]).unwrap(),
0.7266834991153182,
1e-14,
);
close_rel(
dirichlet_log_pdf(&[1e-3, 1e-3], &[1e-300, 1.0]).unwrap(),
682.4838515533071,
1e-14,
);
close(dirichlet_log_pdf(&[4.2], &[1.0]).unwrap(), 0.0, 1e-15);
}
#[test]
fn dirichlet_log_pdf_support_and_boundaries() {
let alpha = [2.0, 0.5, 1.0];
assert_eq!(
dirichlet_log_pdf(&alpha, &[0.5, 0.5, 0.5]).unwrap(),
NEG_INF
);
assert_eq!(
dirichlet_log_pdf(&alpha, &[1.2, -0.1, -0.1]).unwrap(),
NEG_INF
);
assert_eq!(
dirichlet_log_pdf(&alpha, &[f64::NAN, 0.5, 0.5]).unwrap(),
NEG_INF
);
assert_eq!(
dirichlet_log_pdf(&alpha, &[f64::INFINITY, 0.0, 0.0]).unwrap(),
NEG_INF
);
let inside = [0.5 + 0.5 * SIMPLEX_TOLERANCE, 0.25, 0.25];
assert!(dirichlet_log_pdf(&alpha, &inside).unwrap().is_finite());
let outside = [0.5 + 2.0 * SIMPLEX_TOLERANCE, 0.25, 0.25];
assert_eq!(dirichlet_log_pdf(&alpha, &outside).unwrap(), NEG_INF);
assert_eq!(
dirichlet_log_pdf(&alpha, &[0.0, 0.5, 0.5]).unwrap(),
NEG_INF
);
assert_eq!(
dirichlet_log_pdf(&alpha, &[0.5, 0.0, 0.5]).unwrap(),
f64::INFINITY
);
assert!(dirichlet_log_pdf(&alpha, &[0.5, 0.5, 0.0])
.unwrap()
.is_finite());
assert_eq!(
dirichlet_log_pdf(&alpha, &[0.0, 0.0, 1.0]).unwrap(),
NEG_INF
);
assert_eq!(
err_code(dirichlet_log_pdf(&alpha, &[0.5, 0.5])),
ErrorCode::InvalidCount
);
let d = Dirichlet::new(alpha.to_vec()).unwrap();
assert_eq!(d.log_prob(&vec![0.5, 0.5]), NEG_INF);
}
#[test]
fn multinomial_matches_the_factorial_formula() {
fn factorial(n: u64) -> f64 {
(1..=n).map(|k| k as f64).product()
}
let p: [f64; 4] = [0.1, 0.25, 0.4, 0.25];
for counts in [
[0u64, 0, 0, 0],
[1, 0, 0, 0],
[2, 1, 3, 0],
[5, 5, 5, 5],
[0, 7, 1, 2],
[3, 0, 0, 9],
] {
let n: u64 = counts.iter().sum();
let coef = factorial(n) / counts.iter().map(|&c| factorial(c)).product::<f64>();
let prob = coef
* p.iter()
.zip(&counts)
.map(|(&pi, &c)| pi.powi(c as i32))
.product::<f64>();
close_rel(multinomial_log_pmf(&p, &counts).unwrap(), prob.ln(), 1e-13);
}
let cat = Categorical::new(p.to_vec()).unwrap();
for i in 0..4 {
let mut one_hot = [0u64; 4];
one_hot[i] = 1;
close(
multinomial_log_pmf(&p, &one_hot).unwrap(),
cat.log_prob(&i),
1e-15,
);
}
let binomial = Binomial::new(17, 0.3).unwrap();
for k in 0..=17u64 {
close_rel(
multinomial_log_pmf(&[0.3, 0.7], &[k, 17 - k]).unwrap(),
binomial.log_prob(&k),
1e-12,
);
}
}
#[test]
fn multinomial_zero_probability_categories() {
close(
multinomial_log_pmf(&[0.5, 0.0, 0.5], &[2, 0, 1]).unwrap(),
multinomial_log_pmf(&[0.5, 0.5], &[2, 1]).unwrap(),
1e-15,
);
assert_eq!(
multinomial_log_pmf(&[0.5, 0.0, 0.5], &[2, 1, 1]).unwrap(),
NEG_INF
);
close(multinomial_log_pmf(&[1.0, 0.0], &[9, 0]).unwrap(), 0.0, 0.0);
assert_eq!(multinomial_log_pmf(&[1.0, 0.0], &[8, 1]).unwrap(), NEG_INF);
}
#[test]
fn beta_binomial_matches_the_lgamma_formula_and_sums_to_one() {
let n = 20u64;
for &(a, b) in &[(1.0, 1.0), (0.5, 2.0), (2.5, 0.7), (0.5, 0.5), (7.0, 3.0)] {
let mut terms = Vec::new();
for k in 0..=n {
let (kf, nf) = (k as f64, n as f64);
let formula = lgamma(nf + 1.0) - lgamma(kf + 1.0) - lgamma(nf - kf + 1.0)
+ lgamma(a + kf)
+ lgamma(b + nf - kf)
- lgamma(a + b + nf)
- (lgamma(a) + lgamma(b) - lgamma(a + b));
let got = beta_binomial_log_marginal(a, b, k, n).unwrap();
close(got, formula, 1e-12);
terms.push(got);
}
close(log_sum_exp(&terms), 0.0, 1e-13);
}
}
#[test]
fn beta_binomial_matches_numerical_integration() {
fn simpson(f: impl Fn(f64) -> f64, lo: f64, hi: f64, panels: usize) -> f64 {
let h = (hi - lo) / panels as f64;
let inner: f64 = (1..panels)
.map(|i| {
let w = if i % 2 == 1 { 4.0 } else { 2.0 };
w * f(lo + i as f64 * h)
})
.sum();
h / 3.0 * (f(lo) + inner + f(hi))
}
for &(a, b, k, n) in &[
(0.5, 2.0, 7u64, 20u64),
(2.5, 0.7, 13, 20),
(1.0, 1.0, 5, 10),
(0.5, 0.5, 0, 10),
(3.0, 4.5, 20, 20),
] {
let e1 = 2.0 * (k as f64 + a) - 1.0;
let e2 = 2.0 * ((n - k) as f64 + b) - 1.0;
let integral = simpson(
|t: f64| 2.0 * t.sin().powf(e1) * t.cos().powf(e2),
0.0,
FRAC_PI_2,
20_000,
);
let want = ln_multinomial_coefficient(&[k, n - k]).unwrap()
- ln_multivariate_beta(&[a, b]).unwrap()
+ integral.ln();
close_rel(beta_binomial_log_marginal(a, b, k, n).unwrap(), want, 1e-9);
}
}
#[test]
fn beta_binomial_uniform_prior_is_uniform_at_any_scale() {
for &n in &[0u64, 1, 10, 1_000_000, 1_000_000_000, 1_000_000_000_000] {
for &k in &[0, n / 3, n / 2, n] {
close_rel(
beta_binomial_log_marginal(1.0, 1.0, k, n).unwrap(),
-(n as f64 + 1.0).ln(),
4e-15,
);
}
}
}
#[test]
fn categorical_marginal_is_the_polya_urn_product() {
let sequence = [0usize, 2, 2, 1, 0, 2, 2, 2, 0, 1, 3, 2, 2, 0];
for alpha in [
vec![1.0, 1.0, 1.0, 1.0],
vec![0.5, 1.5, 0.25, 3.0],
vec![1e-3, 1e-3, 1e-3, 1e-3],
vec![50.0, 20.0, 5.0, 0.1],
] {
let a_total: f64 = alpha.iter().sum();
let mut counts = [0u64; 4];
let mut urn = 0.0;
for (t, &c) in sequence.iter().enumerate() {
let by_hand = (alpha[c] + counts[c] as f64) / (a_total + t as f64);
let p = dirichlet_predictive(&alpha, &counts).unwrap();
close_rel(p[c], by_hand, 1e-14);
urn += by_hand.ln();
counts[c] += 1;
}
close_rel(
dirichlet_categorical_log_marginal(&alpha, &counts).unwrap(),
urn,
1e-13,
);
}
}
#[test]
fn marginals_sum_to_one_by_enumeration() {
for alpha in [
vec![1.0, 1.0, 1.0],
vec![0.5, 2.0, 1e-3],
vec![30.0, 0.2, 4.0],
] {
for n in [0u64, 1, 6, 11] {
let mut terms = Vec::new();
for n0 in 0..=n {
for n1 in 0..=(n - n0) {
let counts = [n0, n1, n - n0 - n1];
terms.push(dirichlet_multinomial_log_marginal(&alpha, &counts).unwrap());
}
}
close(log_sum_exp(&terms), 0.0, 1e-13);
}
}
let alpha = [1e-3, 2.0, 0.5, 30.0];
let n = 5u64;
let mut terms = Vec::new();
for a in 0..=n {
for b in 0..=(n - a) {
for c in 0..=(n - a - b) {
let counts = [a, b, c, n - a - b - c];
terms.push(dirichlet_multinomial_log_marginal(&alpha, &counts).unwrap());
}
}
}
close(log_sum_exp(&terms), 0.0, 1e-13);
let alpha = [0.5, 1.5, 0.25];
let len = 6u32;
let mut terms = Vec::new();
for code in 0..3usize.pow(len) {
let mut counts = [0u64; 3];
let mut rest = code;
for _ in 0..len {
counts[rest % 3] += 1;
rest /= 3;
}
terms.push(dirichlet_categorical_log_marginal(&alpha, &counts).unwrap());
}
close(log_sum_exp(&terms), 0.0, 1e-13);
}
#[test]
fn two_category_marginals_are_the_beta_ones() {
for &(a, b) in &[(0.5, 2.0), (1e-3, 7.0), (30.0, 30.0)] {
for &(k, n) in &[(0u64, 0u64), (3, 10), (10, 10), (400, 1000)] {
let bb = beta_binomial_log_marginal(a, b, k, n).unwrap();
let bbern = beta_bernoulli_log_marginal(a, b, k, n - k).unwrap();
close(
dirichlet_multinomial_log_marginal(&[a, b], &[k, n - k]).unwrap(),
bb,
1e-12,
);
close(
dirichlet_categorical_log_marginal(&[a, b], &[k, n - k]).unwrap(),
bbern,
1e-12,
);
close_rel(
bb - bbern,
ln_multinomial_coefficient(&[k, n - k]).unwrap(),
1e-12,
);
}
}
}
#[test]
fn marginals_match_references_for_tiny_alpha_and_huge_counts() {
type Case = (&'static [f64], &'static [u64], f64, f64);
let cases: [Case; 10] = [
(
&[1e-8, 1e-8],
&[1_000_000_000, 0],
-0.6931473935647601,
-0.6931473935647601,
),
(
&[1e-8, 1e-8, 1e-8],
&[1_000_000_000, 3, 0],
-20.617905729298215,
-80.9959437769094,
),
(
&[1e-3, 1e-3, 1e-3],
&[1_000_000, 1_000_000, 1],
-28.052952146279214,
-1386329.4426099285,
),
(
&[0.5, 0.5, 0.5],
&[300_000, 300_000, 400_000],
-27.80678153290809,
-1088913.790856622,
),
(
&[30.0, 30.0, 30.0],
&[1_000_000_000_000; 3],
-53.15682287497383,
-3295836866028.5664,
),
(
&[1e9, 1e9],
&[1, 2],
-0.9808292535117262,
-2.079441542179836,
),
(
&[2.5, 0.7, 11.0, 1e-4],
&[3, 0, 17, 1],
-12.21625409320739,
-22.299560072319355,
),
(
&[1e-8; 4],
&[1_000_000, 0, 0, 0],
-1.3862947929016611,
-1.3862947929016611,
),
(
&[1e-8; 4],
&[1_000_000, 1_000_000, 0, 7],
-53.295926166570936,
-1386433.212382519,
),
(
&[0.5, 2.0],
&[1_000_000_000, 2_000_000_000],
-21.965719162090412,
-1909542515.7723186,
),
];
for (alpha, counts, dm_ref, dc_ref) in cases {
close_rel(
dirichlet_multinomial_log_marginal(alpha, counts).unwrap(),
dm_ref,
1e-13,
);
close_rel(
dirichlet_categorical_log_marginal(alpha, counts).unwrap(),
dc_ref,
1e-13,
);
}
close_rel(
beta_binomial_log_marginal(1.0, 1.0, 5, 1_000_000_000_000).unwrap(),
-27.63102111592955,
1e-14,
);
close_rel(
beta_binomial_log_marginal(0.5, 0.5, 1_000_000_000, 2_000_000_000).unwrap(),
-21.867995723045812,
1e-13,
);
close_rel(
beta_binomial_log_marginal(1e-8, 1e-8, 0, 1_000_000_000).unwrap(),
-0.6931473935647601,
1e-13,
);
}
#[test]
fn marginals_match_the_lgamma_formula_where_it_is_accurate() {
let naive = |alpha: &[f64], counts: &[u64]| -> f64 {
let a_total: f64 = alpha.iter().sum();
let n_total = counts.iter().sum::<u64>() as f64;
let mut out = lgamma(n_total + 1.0) + lgamma(a_total) - lgamma(a_total + n_total);
for (&a, &n) in alpha.iter().zip(counts) {
let n = n as f64;
out += lgamma(a + n) - lgamma(a) - lgamma(n + 1.0);
}
out
};
for &a in &[1e-8, 1e-3, 0.5, 3.0] {
for counts in [
[0u64, 0, 0],
[1, 0, 0],
[5, 2, 9],
[1_000, 0, 7],
[1_000_000, 3, 0],
[400_000, 350_000, 250_000],
] {
let alpha = [a, 2.0 * a, 0.5 * a];
close(
dirichlet_multinomial_log_marginal(&alpha, &counts).unwrap(),
naive(&alpha, &counts),
1e-6,
);
}
}
}
#[test]
fn nothing_is_nan_across_an_extreme_parameter_grid() {
let alphas = [5e-324, 1e-300, 1e-8, 1e-3, 0.5, 1.0, 7.5, 1e3, 1e8, 1e15];
let counts = [
0u64,
1,
17,
1_000,
1_000_000,
1_000_000_000,
1_000_000_000_000,
u64::MAX / 4,
];
for &a in &alphas {
for &b in &alphas {
for &n0 in &counts {
for &n1 in &counts {
let (alpha, c) = ([a, b], [n0, n1]);
let dm = dirichlet_multinomial_log_marginal(&alpha, &c).unwrap();
let dc = dirichlet_categorical_log_marginal(&alpha, &c).unwrap();
let bb = beta_binomial_log_marginal(a, b, n0, n0 + n1).unwrap();
assert!(
dm.is_finite() && dc.is_finite() && bb.is_finite(),
"alpha {alpha:?} counts {c:?}: {dm} {dc} {bb}"
);
let lp = dirichlet_log_predictive(&alpha, &c).unwrap();
assert!(lp.iter().all(|l| l.is_finite() && *l <= 1e-12));
}
}
}
}
}
#[test]
fn posterior_updates_and_predictives() {
let (alpha, counts) = ([0.5, 1.0, 2.0], [3u64, 0, 7]);
assert_eq!(
dirichlet_posterior(&alpha, &counts).unwrap(),
vec![3.5, 1.0, 9.0]
);
assert_eq!(beta_posterior(2.0, 3.0, 5, 1).unwrap(), (7.0, 4.0));
let want = [3.5 / 13.5, 1.0 / 13.5, 9.0 / 13.5];
let p = dirichlet_predictive(&alpha, &counts).unwrap();
let lp = dirichlet_log_predictive(&alpha, &counts).unwrap();
for i in 0..3 {
close(p[i], want[i], 1e-15);
close(lp[i], want[i].ln(), 1e-14);
}
close(p.iter().sum::<f64>(), 1.0, 1e-15);
let (a_post, b_post) = beta_posterior(2.0, 3.0, 5, 1).unwrap();
let p2 = dirichlet_predictive(&[2.0, 3.0], &[5, 1]).unwrap();
close(p2[0], a_post / (a_post + b_post), 1e-15);
let lp = dirichlet_log_predictive(&[1e-300; 3], &[0, 1_000_000_000_000, 5]).unwrap();
assert!(lp.iter().all(|l| l.is_finite()));
close(log_sum_exp(&lp), 0.0, 1e-14);
}
#[test]
fn invalid_parameters_are_errors_with_codes() {
use ErrorCode::{InvalidCount, InvalidProbability, InvalidShape};
assert_eq!(err_code(ln_multivariate_beta(&[])), InvalidCount);
for bad in [
vec![1.0, 0.0],
vec![1.0, -2.0],
vec![f64::NAN, 1.0],
vec![f64::INFINITY],
vec![1e306],
vec![1e308, 1e308],
] {
assert_eq!(
err_code(ln_multivariate_beta(&bad)),
InvalidShape,
"{bad:?}"
);
assert_eq!(err_code(Dirichlet::new(bad.clone())), InvalidShape);
assert_eq!(err_code(sample_dirichlet("w", &bad)), InvalidShape);
let x = vec![1.0 / bad.len() as f64; bad.len()];
assert_eq!(err_code(dirichlet_log_pdf(&bad, &x)), InvalidShape);
let zeros = vec![0u64; bad.len()];
assert_eq!(
err_code(dirichlet_multinomial_log_marginal(&bad, &zeros)),
InvalidShape
);
assert_eq!(
err_code(dirichlet_categorical_log_marginal(&bad, &zeros)),
InvalidShape
);
assert_eq!(err_code(dirichlet_posterior(&bad, &zeros)), InvalidShape);
assert_eq!(err_code(dirichlet_predictive(&bad, &zeros)), InvalidShape);
assert_eq!(
err_code(dirichlet_log_predictive(&bad, &zeros)),
InvalidShape
);
}
assert_eq!(err_code(Dirichlet::new(vec![])), InvalidCount);
assert_eq!(err_code(sample_dirichlet("w", &[])), InvalidCount);
assert_eq!(
err_code(dirichlet_log_pdf(&[1.0, 1.0], &[1.0])),
InvalidCount
);
assert_eq!(
err_code(dirichlet_multinomial_log_marginal(&[1.0, 1.0], &[1])),
InvalidCount
);
assert_eq!(
err_code(dirichlet_categorical_log_marginal(&[1.0], &[1, 2])),
InvalidCount
);
assert_eq!(
err_code(dirichlet_multinomial_log_marginal(
&[1.0, 1.0],
&[u64::MAX, 1]
)),
InvalidCount
);
assert_eq!(
err_code(beta_bernoulli_log_marginal(1.0, 1.0, u64::MAX, 1)),
InvalidCount
);
assert_eq!(err_code(ln_multinomial_coefficient(&[])), InvalidCount);
assert_eq!(
err_code(ln_multinomial_coefficient(&[u64::MAX, 1])),
InvalidCount
);
assert_eq!(err_code(multinomial_log_pmf(&[], &[])), InvalidProbability);
assert_eq!(
err_code(multinomial_log_pmf(&[0.5, 0.6], &[1, 1])),
InvalidProbability
);
assert_eq!(
err_code(multinomial_log_pmf(&[1.5, -0.5], &[1, 1])),
InvalidProbability
);
assert_eq!(
err_code(multinomial_log_pmf(&[f64::NAN, 1.0], &[1, 1])),
InvalidProbability
);
assert_eq!(
err_code(multinomial_log_pmf(&[0.5, 0.5], &[1])),
InvalidCount
);
assert_eq!(
err_code(Multinomial::new(3, vec![0.5, 0.6])),
InvalidProbability
);
assert_eq!(err_code(Multinomial::new(3, vec![])), InvalidProbability);
assert_eq!(
err_code(beta_binomial_log_marginal(0.0, 1.0, 1, 2)),
InvalidShape
);
assert_eq!(
err_code(beta_binomial_log_marginal(1.0, f64::NAN, 1, 2)),
InvalidShape
);
assert_eq!(
err_code(beta_bernoulli_log_marginal(-1.0, 1.0, 1, 2)),
InvalidShape
);
assert_eq!(
err_code(beta_posterior(1.0, f64::INFINITY, 0, 0)),
InvalidShape
);
assert_eq!(err_code(beta_posterior(1e308, 1e308, 0, 0)), InvalidShape);
let err = dirichlet_log_pdf(&[1.0, -1.0], &[0.5, 0.5]).unwrap_err();
assert!(err.is_validation_error());
let msg = err.to_string();
assert!(
msg.contains("Dirichlet") && msg.contains("index=1"),
"{msg}"
);
assert_eq!(beta_binomial_log_marginal(1.0, 1.0, 3, 2).unwrap(), NEG_INF);
}
#[test]
fn validate_impls_mirror_the_constructors() {
let bad = Dirichlet {
alpha: vec![1.0, 0.0],
ln_beta: 0.0,
};
assert_eq!(
bad.validate().unwrap_err().code(),
Dirichlet::new(vec![1.0, 0.0]).unwrap_err().code()
);
let empty = Dirichlet {
alpha: vec![],
ln_beta: 0.0,
};
assert_eq!(
empty.validate().unwrap_err().code(),
ErrorCode::InvalidCount
);
let bad = Multinomial {
n: 3,
probs: vec![0.5, 0.6],
};
assert_eq!(
bad.validate().unwrap_err().code(),
Multinomial::new(3, vec![0.5, 0.6]).unwrap_err().code()
);
assert!(Dirichlet::new(vec![0.1]).unwrap().validate().is_ok());
assert!(Multinomial::new(0, vec![1.0]).unwrap().validate().is_ok());
}
#[test]
fn dirichlet_sampling_is_stable_for_tiny_concentrations() {
let mut rng = StdRng::seed_from_u64(66);
for alpha in [vec![1e-3; 5], vec![1e-8; 3], vec![1e-3, 2.0, 1e-5]] {
let d = Dirichlet::new(alpha).unwrap();
for _ in 0..4_000 {
let x = d.sample(&mut rng);
assert!(x.iter().all(|v| v.is_finite() && *v >= 0.0), "{x:?}");
assert!(x.iter().any(|&v| v > 0.0));
close(x.iter().sum::<f64>(), 1.0, 1e-12);
let ln_x = d.sample_log(&mut rng);
assert!(ln_x.iter().all(|l| l.is_finite() && *l <= 0.0), "{ln_x:?}");
close(log_sum_exp(&ln_x), 0.0, 1e-12);
}
}
let d = Dirichlet::new(vec![1e-320, 3e-320]).unwrap();
let mut first = 0usize;
for _ in 0..4_000 {
let x = d.sample(&mut rng);
assert!(x == vec![1.0, 0.0] || x == vec![0.0, 1.0], "{x:?}");
first += usize::from(x[0] == 1.0);
let ln_x = d.sample_log(&mut rng);
assert!(ln_x == vec![0.0, NEG_INF] || ln_x == vec![NEG_INF, 0.0]);
}
close(first as f64 / 4_000.0, 0.25, 0.03);
}
#[test]
fn dirichlet_sample_moments_match() {
let alpha = [0.5, 1.0, 2.5];
let a_total = 4.0;
let d = Dirichlet::new(alpha.to_vec()).unwrap();
let mut rng = StdRng::seed_from_u64(2026);
let n = 40_000;
let mut sum = [0.0; 3];
let mut sum_sq = [0.0; 3];
for _ in 0..n {
let x = d.sample(&mut rng);
for i in 0..3 {
sum[i] += x[i];
sum_sq[i] += x[i] * x[i];
}
}
for i in 0..3 {
let mean = sum[i] / n as f64;
let var = sum_sq[i] / n as f64 - mean * mean;
let m = alpha[i] / a_total;
let v = m * (1.0 - m) / (a_total + 1.0);
close(mean, m, 5.0 * (v / n as f64).sqrt());
close(var, v, 0.05 * v);
}
}
#[test]
fn distribution_types_agree_with_the_free_functions() {
let alpha = vec![0.7, 2.0, 5.0];
let d = Dirichlet::new(alpha.clone()).unwrap();
let x = vec![0.1, 0.3, 0.6];
assert_eq!(d.log_prob(&x), dirichlet_log_pdf(&alpha, &x).unwrap());
let boxed: Box<dyn Distribution<Vec<f64>>> = d.clone_box();
assert_eq!(boxed.log_prob(&x), d.log_prob(&x));
assert_eq!((d.alpha(), d.len(), d.is_empty()), (&alpha[..], 3, false));
let (mut r1, mut r2) = (StdRng::seed_from_u64(4), StdRng::seed_from_u64(4));
let (x1, l2) = (d.sample(&mut r1), d.sample_log(&mut r2));
for i in 0..3 {
close(x1[i], l2[i].exp(), 1e-14);
}
let p = vec![0.2, 0.3, 0.5];
let m = Multinomial::new(6, p.clone()).unwrap();
let counts = vec![1, 2, 3];
assert_eq!(
m.log_prob(&counts),
multinomial_log_pmf(&p, &counts).unwrap()
);
let boxed: Box<dyn Distribution<Vec<u64>>> = m.clone_box();
assert_eq!(boxed.log_prob(&counts), m.log_prob(&counts));
assert_eq!(
(m.n(), m.probs(), m.len(), m.is_empty()),
(6, &p[..], 3, false)
);
}
#[test]
fn multinomial_sampling_and_support() {
let p = vec![0.1, 0.0, 0.6, 0.3];
let m = Multinomial::new(50, p.clone()).unwrap();
let mut rng = StdRng::seed_from_u64(9);
let draws = 20_000;
let mut mean = [0.0; 4];
for _ in 0..draws {
let c = m.sample(&mut rng);
assert_eq!(c.iter().sum::<u64>(), 50);
assert_eq!(c[1], 0);
assert!(m.log_prob(&c).is_finite());
for i in 0..4 {
mean[i] += c[i] as f64 / draws as f64;
}
}
for i in 0..4 {
let se = (50.0 * p[i] * (1.0 - p[i]) / draws as f64).sqrt();
close(mean[i], 50.0 * p[i], 5.0 * se + 1e-12);
}
let big = Multinomial::new(1_000_000_000_000, vec![0.25, 0.25, 0.5]).unwrap();
let c = big.sample(&mut rng);
assert_eq!(c.iter().sum::<u64>(), 1_000_000_000_000);
assert!(big.log_prob(&c).is_finite());
assert_eq!(m.log_prob(&vec![10, 0, 40]), NEG_INF);
assert_eq!(m.log_prob(&vec![10, 0, 30, 9]), NEG_INF);
assert_eq!(m.log_prob(&vec![u64::MAX, 0, 1, 0]), NEG_INF);
assert_eq!(m.log_prob(&vec![10, 1, 30, 9]), NEG_INF);
let zero = Multinomial::new(0, vec![0.5, 0.5]).unwrap();
assert_eq!(zero.sample(&mut rng), vec![0, 0]);
assert_eq!(zero.log_prob(&vec![0, 0]), 0.0);
}
#[test]
fn sample_dirichlet_records_gamma_sites_at_indexed_addresses() {
let alpha = [0.7, 2.0, 5.0, 1.3];
for name in ["mix", "a#b\\c"] {
let (x, trace) = prior_run(11, sample_dirichlet(name, &alpha).unwrap());
let mut expected: Vec<Address> = (0..4).map(|i| addr!(name, i)).collect();
expected.sort();
let recorded: Vec<Address> = trace.choices.keys().cloned().collect();
assert_eq!(recorded, expected);
let g: Vec<f64> = (0..4)
.map(|i| trace.get_f64(&addr!(name, i)).unwrap())
.collect();
let total: f64 = g.iter().sum();
for i in 0..4 {
assert!(x[i] >= 0.0);
close(x[i], g[i] / total, 1e-15);
}
close(x.iter().sum::<f64>(), 1.0, 1e-12);
assert_eq!((trace.log_likelihood, trace.log_factors), (0.0, 0.0));
}
assert_eq!(addr!("mix", 3).as_str(), "mix#3");
}
#[test]
fn sample_dirichlet_log_prior_is_the_joint_gamma_density() {
let alpha = [0.7, 2.0, 5.0, 1.3];
let a_total: f64 = alpha.iter().sum();
for seed in 0..20 {
let (x, trace) = prior_run(seed, sample_dirichlet("w", &alpha).unwrap());
let g: Vec<f64> = (0..4)
.map(|i| trace.get_f64(&addr!("w", i)).unwrap())
.collect();
let per_site: Vec<f64> = alpha
.iter()
.zip(&g)
.map(|(&a, gi)| Gamma::new(a, 1.0).unwrap().log_prob(gi))
.collect();
for (i, &lp) in per_site.iter().enumerate() {
assert_eq!(trace.choices[&addr!("w", i)].logp, lp);
}
close_rel(trace.log_prior, per_site.iter().sum(), 1e-14);
let s: f64 = g.iter().sum();
let decomposed = dirichlet_log_pdf(&alpha, &x).unwrap()
+ Gamma::new(a_total, 1.0).unwrap().log_prob(&s)
- 3.0 * s.ln();
close_rel(trace.log_prior, decomposed, 1e-11);
}
}
#[test]
fn sample_dirichlet_rescoring_reproduces_the_trace() {
let alpha = [0.7, 2.0, 5.0];
let model = || sample_dirichlet("w", &alpha).unwrap();
let (x, trace) = prior_run(5, model());
let (x2, rescored) = run(
ScoreGivenTrace {
base: trace.clone(),
trace: Trace::default(),
},
model(),
);
assert_eq!(x, x2);
assert_eq!(rescored.log_prior, trace.log_prior);
assert_eq!(rescored.total_log_weight(), trace.total_log_weight());
}
#[test]
fn sample_dirichlet_is_dirichlet_distributed() {
let alpha = [0.5, 1.5, 3.0];
let a_total: f64 = alpha.iter().sum();
let mut rng = StdRng::seed_from_u64(77);
let n = 20_000;
let mut mean = [0.0; 3];
for _ in 0..n {
let (x, trace) = run(
PriorHandler {
rng: &mut rng,
trace: Trace::default(),
},
sample_dirichlet("w", &alpha).unwrap(),
);
assert!(trace.log_prior.is_finite());
for i in 0..3 {
mean[i] += x[i] / n as f64;
}
}
for i in 0..3 {
let m = alpha[i] / a_total;
let v = m * (1.0 - m) / (a_total + 1.0);
close(mean[i], m, 5.0 * (v / n as f64).sqrt());
}
}
#[test]
fn sample_dirichlet_underflow_is_a_zero_weight_uniform_vector() {
let (x, trace) = prior_run(3, sample_dirichlet("w", &[1e-300; 3]).unwrap());
assert_eq!(x, vec![1.0 / 3.0; 3]);
assert_eq!(trace.log_factors, NEG_INF);
assert_eq!(trace.total_log_weight(), NEG_INF);
let alpha = [2.0, 3.0];
for bad in [
[0.0, 0.0],
[1.0, -0.5],
[f64::NAN, 1.0],
[f64::INFINITY, 1.0],
] {
let mut base = Trace::default();
for (i, &g) in bad.iter().enumerate() {
base.insert_choice(addr!("w", i), ChoiceValue::F64(g), 0.0);
}
let (x, t) = run(
ScoreGivenTrace {
base,
trace: Trace::default(),
},
sample_dirichlet("w", &alpha).unwrap(),
);
assert_eq!(x, vec![0.5, 0.5], "{bad:?}");
assert_eq!(t.log_factors, NEG_INF, "{bad:?}");
}
let mut base = Trace::default();
base.insert_choice(addr!("w", 0), ChoiceValue::F64(0.0), 0.0);
base.insert_choice(addr!("w", 1), ChoiceValue::F64(2.0), 0.0);
let (x, t) = run(
ScoreGivenTrace {
base,
trace: Trace::default(),
},
sample_dirichlet("w", &[0.5, 3.0]).unwrap(),
);
assert_eq!(x, vec![0.0, 1.0]);
assert_eq!(t.log_prior, NEG_INF);
assert_eq!(t.log_factors, 0.0);
}
#[test]
fn mh_over_gamma_sites_recovers_the_conjugate_posterior() {
let alpha = [2.0, 1.0, 1.0];
let data = [0usize, 0, 1, 0, 2, 0, 1, 0]; let model = move || {
sample_dirichlet("w", &alpha).unwrap().bind(move |w| {
let weights = w.clone();
let obs: Vec<(usize, usize)> = data.iter().copied().enumerate().collect();
traverse_vec(obs, move |(t, y)| {
observe(addr!("y", t), Categorical::new(weights.clone()).unwrap(), y)
})
.map(move |_| w)
})
};
let mut rng = StdRng::seed_from_u64(61);
let draws = adaptive_mcmc_chain(&mut rng, model, 6_000, 2_000);
let posterior_mean = dirichlet_predictive(&alpha, &[5, 2, 1]).unwrap();
for (i, &want) in posterior_mean.iter().enumerate() {
let mean = draws.iter().map(|(w, _)| w[i]).sum::<f64>() / draws.len() as f64;
close(mean, want, 0.04);
}
}
#[test]
fn concentration_posterior_via_factor_matches_quadrature() {
let counts = vec![40u64, 3, 1, 0];
let prior = Normal::new(0.0, 2.0).unwrap();
let log_post = |la: f64| {
prior.log_prob(&la)
+ dirichlet_categorical_log_marginal(&[la.exp(); 4], &counts).unwrap()
};
let grid: Vec<f64> = (0..=8_000)
.map(|i| -16.0 + 24.0 * i as f64 / 8_000.0)
.collect();
let lps: Vec<f64> = grid.iter().map(|&la| log_post(la)).collect();
let z = log_sum_exp(&lps);
let grid_mean: f64 = grid
.iter()
.zip(&lps)
.map(|(&la, &lp)| la * (lp - z).exp())
.sum();
let model = move || {
let counts = counts.clone();
sample(addr!("log_alpha"), Normal::new(0.0, 2.0).unwrap()).bind(move |log_alpha| {
let alpha = vec![log_alpha.exp(); counts.len()];
let loglik = dirichlet_categorical_log_marginal(&alpha, &counts).unwrap_or(NEG_INF);
factor(loglik).map(move |_| log_alpha)
})
};
let mut rng = StdRng::seed_from_u64(68);
let draws = adaptive_mcmc_chain(&mut rng, model, 8_000, 2_000);
let mh_mean = draws.iter().map(|(la, _)| la).sum::<f64>() / draws.len() as f64;
close(mh_mean, grid_mean, 0.15);
}
}