use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use antecedent_core::{Value, VariableId};
use crate::{DomainRef, InterventionAssignment};
pub type QuadratureNodes = Arc<[(Arc<[Value]>, f64)]>;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct EvalContext {
pub draw: Option<usize>,
}
#[derive(Clone, Debug, Default)]
pub struct Assignment {
entries: Vec<(VariableId, Value)>,
}
impl Assignment {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_pairs(pairs: impl IntoIterator<Item = (VariableId, Value)>) -> Self {
let mut entries: Vec<(VariableId, Value)> = pairs.into_iter().collect();
entries.sort_by_key(|(v, _)| v.raw());
entries.dedup_by_key(|(v, _)| *v);
Self { entries }
}
pub fn set(&mut self, var: VariableId, value: Value) {
match self.entries.binary_search_by_key(&var.raw(), |(v, _)| v.raw()) {
Ok(i) => self.entries[i].1 = value,
Err(i) => self.entries.insert(i, (var, value)),
}
}
#[must_use]
pub fn get(&self, var: VariableId) -> Option<&Value> {
self.entries
.binary_search_by_key(&var.raw(), |(v, _)| v.raw())
.ok()
.map(|i| &self.entries[i].1)
}
#[must_use]
pub fn entries(&self) -> &[(VariableId, Value)] {
&self.entries
}
pub fn extend_from(&mut self, other: &Assignment) {
for (v, val) in &other.entries {
self.set(*v, val.clone());
}
}
pub fn values_for(&self, vars: &[VariableId]) -> Result<Vec<Value>, EvalError> {
let mut out = Vec::with_capacity(vars.len());
for &v in vars {
let Some(val) = self.get(v) else {
return Err(EvalError::MissingBinding(v));
};
out.push(val.clone());
}
Ok(out)
}
}
#[derive(Clone, Debug)]
pub struct FactorSpec<'a> {
pub variables: &'a [VariableId],
pub conditioned_on: &'a [VariableId],
pub intervention: &'a [InterventionAssignment],
pub domain: DomainRef,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EvalError {
UnsupportedIntegralOut,
MissingTableEntry,
MissingBinding(VariableId),
EmptySupport(VariableId),
DivisionByZero,
DrawOutOfRange {
draw: usize,
n_draws: usize,
},
SupportShape {
expected: usize,
actual: usize,
},
ProviderKind(&'static str),
UnsupportedConditioning(&'static str),
}
impl fmt::Display for EvalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedIntegralOut => {
write!(f, "IntegralOut requires provider quadrature nodes or discrete support")
}
Self::MissingTableEntry => write!(f, "missing probability table entry"),
Self::MissingBinding(v) => write!(f, "missing binding for V{}", v.raw()),
Self::EmptySupport(v) => write!(f, "empty support for V{}", v.raw()),
Self::DivisionByZero => write!(f, "division by zero in ratio"),
Self::DrawOutOfRange { draw, n_draws } => {
write!(f, "draw {draw} out of range (n_draws={n_draws})")
}
Self::SupportShape { expected, actual } => {
write!(f, "support row arity {actual} != expected {expected}")
}
Self::ProviderKind(msg) | Self::UnsupportedConditioning(msg) => write!(f, "{msg}"),
}
}
}
impl std::error::Error for EvalError {}
pub trait DistributionProvider {
fn probability(
&self,
spec: &FactorSpec<'_>,
assignment: &Assignment,
ctx: &EvalContext,
) -> Result<f64, EvalError>;
fn support(
&self,
vars: &[VariableId],
ctx: &EvalContext,
) -> Result<Arc<[Arc<[Value]>]>, EvalError>;
fn quadrature(
&self,
_vars: &[VariableId],
_ctx: &EvalContext,
) -> Result<Option<QuadratureNodes>, EvalError> {
Ok(None)
}
fn outcome(
&self,
var: VariableId,
assignment: &Assignment,
ctx: &EvalContext,
) -> Result<f64, EvalError>;
fn n_draws(&self) -> Option<usize>;
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
struct FactorKey {
variables: Arc<[VariableId]>,
conditioned_on: Arc<[VariableId]>,
intervention: Arc<[InterventionAssignment]>,
domain: DomainRef,
values: Arc<[Value]>,
}
fn factor_key(spec: &FactorSpec<'_>, assignment: &Assignment) -> Result<FactorKey, EvalError> {
let mut values = assignment.values_for(spec.variables)?;
values.extend(assignment.values_for(spec.conditioned_on)?);
Ok(FactorKey {
variables: Arc::from(spec.variables),
conditioned_on: Arc::from(spec.conditioned_on),
intervention: Arc::from(spec.intervention.to_vec()),
domain: spec.domain,
values: Arc::from(values),
})
}
#[derive(Clone, Debug, Default)]
pub struct EmpiricalTableProvider {
domains: HashMap<VariableId, Arc<[Value]>>,
tables: HashMap<FactorKey, f64>,
}
impl EmpiricalTableProvider {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_domain(&mut self, var: VariableId, values: impl IntoIterator<Item = Value>) {
let mut v: Vec<Value> = values.into_iter().collect();
let mut seen = std::collections::HashSet::new();
v.retain(|x| seen.insert(x.clone()));
self.domains.insert(var, Arc::from(v));
}
pub fn insert_probability(
&mut self,
spec: &FactorSpec<'_>,
assignment: &Assignment,
probability: f64,
) -> Result<(), EvalError> {
let key = factor_key(spec, assignment)?;
self.tables.insert(key, probability);
Ok(())
}
}
impl DistributionProvider for EmpiricalTableProvider {
fn probability(
&self,
spec: &FactorSpec<'_>,
assignment: &Assignment,
_ctx: &EvalContext,
) -> Result<f64, EvalError> {
let key = factor_key(spec, assignment)?;
self.tables.get(&key).copied().ok_or(EvalError::MissingTableEntry)
}
fn support(
&self,
vars: &[VariableId],
_ctx: &EvalContext,
) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
if vars.is_empty() {
return Ok(Arc::from(vec![Arc::from(Vec::<Value>::new())]));
}
let mut rows: Vec<Vec<Value>> = vec![Vec::new()];
for &v in vars {
let domain = self.domains.get(&v).ok_or(EvalError::EmptySupport(v))?;
if domain.is_empty() {
return Err(EvalError::EmptySupport(v));
}
let mut next = Vec::with_capacity(rows.len() * domain.len());
for prefix in &rows {
for val in domain.iter() {
let mut row = prefix.clone();
row.push(val.clone());
next.push(row);
}
}
rows = next;
}
let out: Vec<Arc<[Value]>> = rows.into_iter().map(Arc::from).collect();
Ok(Arc::from(out))
}
fn outcome(
&self,
var: VariableId,
assignment: &Assignment,
_ctx: &EvalContext,
) -> Result<f64, EvalError> {
let value = assignment.get(var).ok_or(EvalError::MissingBinding(var))?;
value.as_f64().ok_or(EvalError::MissingBinding(var))
}
fn n_draws(&self) -> Option<usize> {
None
}
}
#[derive(Clone, Debug, Default)]
pub struct PosteriorDrawProvider {
draws: Vec<EmpiricalTableProvider>,
}
impl PosteriorDrawProvider {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_draws(draws: Vec<EmpiricalTableProvider>) -> Self {
Self { draws }
}
#[must_use]
pub fn len(&self) -> usize {
self.draws.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.draws.is_empty()
}
fn table(&self, ctx: &EvalContext) -> Result<&EmpiricalTableProvider, EvalError> {
let draw = ctx
.draw
.ok_or(EvalError::ProviderKind("PosteriorDrawProvider requires EvalContext.draw"))?;
self.draws.get(draw).ok_or(EvalError::DrawOutOfRange { draw, n_draws: self.draws.len() })
}
}
impl DistributionProvider for PosteriorDrawProvider {
fn probability(
&self,
spec: &FactorSpec<'_>,
assignment: &Assignment,
ctx: &EvalContext,
) -> Result<f64, EvalError> {
self.table(ctx)?.probability(spec, assignment, ctx)
}
fn support(
&self,
vars: &[VariableId],
ctx: &EvalContext,
) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
self.table(ctx)?.support(vars, ctx)
}
fn outcome(
&self,
var: VariableId,
assignment: &Assignment,
ctx: &EvalContext,
) -> Result<f64, EvalError> {
self.table(ctx)?.outcome(var, assignment, ctx)
}
fn n_draws(&self) -> Option<usize> {
Some(self.draws.len())
}
}
#[derive(Clone, Debug, Default)]
pub struct GaussianDensityProvider {
params: HashMap<VariableId, (f64, f64)>,
}
impl GaussianDensityProvider {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_gaussian(&mut self, var: VariableId, mean: f64, variance: f64) {
if variance > 0.0 && variance.is_finite() && mean.is_finite() {
self.params.insert(var, (mean, variance));
}
}
}
const GH5_NODES: [f64; 5] = [
-2.020_182_870_456_085_6,
-0.958_572_464_613_818_5,
0.0,
0.958_572_464_613_818_5,
2.020_182_870_456_085_6,
];
const GH5_WEIGHTS: [f64; 5] = [
0.019_953_242_059_045_913,
0.393_619_323_152_241_35,
0.945_308_720_482_941_9,
0.393_619_323_152_241_35,
0.019_953_242_059_045_913,
];
impl DistributionProvider for GaussianDensityProvider {
fn probability(
&self,
spec: &FactorSpec<'_>,
assignment: &Assignment,
_ctx: &EvalContext,
) -> Result<f64, EvalError> {
if !spec.conditioned_on.is_empty() {
return Err(EvalError::UnsupportedConditioning(
"GaussianDensityProvider models independent Gaussians and cannot answer \
conditional queries; conditioned_on must be empty",
));
}
let mut dens = 1.0;
for &v in spec.variables {
let (mean, var) = self.params.get(&v).copied().ok_or(EvalError::EmptySupport(v))?;
let x =
assignment.get(v).and_then(Value::as_f64).ok_or(EvalError::MissingBinding(v))?;
let inv_sqrt = (2.0 * std::f64::consts::PI * var).sqrt().recip();
let z = (x - mean) / var.sqrt();
dens *= inv_sqrt * (-0.5 * z * z).exp();
}
Ok(dens)
}
fn support(
&self,
vars: &[VariableId],
_ctx: &EvalContext,
) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
if vars.is_empty() {
return Ok(Arc::from(vec![Arc::from(Vec::<Value>::new())]));
}
Err(EvalError::EmptySupport(vars[0]))
}
fn quadrature(
&self,
vars: &[VariableId],
_ctx: &EvalContext,
) -> Result<Option<QuadratureNodes>, EvalError> {
if vars.is_empty() {
return Ok(Some(Arc::from([(Arc::from(Vec::<Value>::new()), 1.0)])));
}
let mut nodes: Vec<(Vec<Value>, f64)> = vec![(Vec::new(), 1.0)];
for &v in vars {
let (mean, variance) =
self.params.get(&v).copied().ok_or(EvalError::EmptySupport(v))?;
let sigma = variance.sqrt();
let scale = sigma * std::f64::consts::SQRT_2;
let mut next = Vec::with_capacity(nodes.len() * GH5_NODES.len());
for (prefix, w0) in &nodes {
for (i, &t) in GH5_NODES.iter().enumerate() {
let x = mean + scale * t;
let w = w0 * GH5_WEIGHTS[i] * scale * (t * t).exp();
let mut row = prefix.clone();
row.push(Value::f64(x));
next.push((row, w));
}
}
nodes = next;
}
let out: Vec<(Arc<[Value]>, f64)> =
nodes.into_iter().map(|(row, w)| (Arc::from(row), w)).collect();
Ok(Some(Arc::from(out)))
}
fn outcome(
&self,
var: VariableId,
assignment: &Assignment,
_ctx: &EvalContext,
) -> Result<f64, EvalError> {
let value = assignment.get(var).ok_or(EvalError::MissingBinding(var))?;
value.as_f64().ok_or(EvalError::MissingBinding(var))
}
fn n_draws(&self) -> Option<usize> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(id: u32) -> VariableId {
VariableId::from_raw(id)
}
fn f(x: f64) -> Value {
Value::f64(x)
}
#[test]
fn empirical_table_missing_entry_errors() {
let mut p = EmpiricalTableProvider::new();
let y = v(0);
p.set_domain(y, [f(0.0), f(1.0)]);
let spec = FactorSpec {
variables: &[y],
conditioned_on: &[],
intervention: &[],
domain: DomainRef::Observational,
};
let assignment = Assignment::from_pairs([(y, f(0.0))]);
let err = p.probability(&spec, &assignment, &EvalContext::default()).unwrap_err();
assert_eq!(err, EvalError::MissingTableEntry);
}
#[test]
fn gaussian_provider_rejects_conditional_query() {
let mut p = GaussianDensityProvider::new();
let y = v(0);
let z = v(1);
p.set_gaussian(y, 0.0, 1.0);
p.set_gaussian(z, 0.0, 1.0);
let spec = FactorSpec {
variables: &[y],
conditioned_on: &[z],
intervention: &[],
domain: DomainRef::Observational,
};
let assignment = Assignment::from_pairs([(y, f(0.5)), (z, f(0.2))]);
let err = p.probability(&spec, &assignment, &EvalContext::default()).unwrap_err();
assert!(matches!(err, EvalError::UnsupportedConditioning(_)));
}
}