Skip to main content

antecedent_expr/
provider.rs

1//! Distribution providers for compiled expression evaluation.
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::collections::HashMap;
6use std::fmt;
7use std::sync::Arc;
8
9use antecedent_core::{Value, VariableId};
10
11use crate::{DomainRef, InterventionAssignment};
12
13/// Weighted quadrature nodes: assignment rows paired with integration weights.
14pub type QuadratureNodes = Arc<[(Arc<[Value]>, f64)]>;
15
16/// Evaluation context (optional posterior draw index).
17#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
18pub struct EvalContext {
19    /// Posterior draw index when evaluating against a draw-indexed provider.
20    pub draw: Option<usize>,
21}
22
23/// Variable → value binding for density / outcome lookup.
24#[derive(Clone, Debug, Default)]
25pub struct Assignment {
26    /// Sorted by variable id.
27    entries: Vec<(VariableId, Value)>,
28}
29
30impl Assignment {
31    /// Empty assignment.
32    #[must_use]
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Build from unsorted pairs (sorted + last-wins on duplicate vars).
38    #[must_use]
39    pub fn from_pairs(pairs: impl IntoIterator<Item = (VariableId, Value)>) -> Self {
40        let mut entries: Vec<(VariableId, Value)> = pairs.into_iter().collect();
41        entries.sort_by_key(|(v, _)| v.raw());
42        entries.dedup_by_key(|(v, _)| *v);
43        Self { entries }
44    }
45
46    /// Insert or replace a binding.
47    pub fn set(&mut self, var: VariableId, value: Value) {
48        match self.entries.binary_search_by_key(&var.raw(), |(v, _)| v.raw()) {
49            Ok(i) => self.entries[i].1 = value,
50            Err(i) => self.entries.insert(i, (var, value)),
51        }
52    }
53
54    /// Borrow value for `var`, if present.
55    #[must_use]
56    pub fn get(&self, var: VariableId) -> Option<&Value> {
57        self.entries
58            .binary_search_by_key(&var.raw(), |(v, _)| v.raw())
59            .ok()
60            .map(|i| &self.entries[i].1)
61    }
62
63    /// All bindings, sorted.
64    #[must_use]
65    pub fn entries(&self) -> &[(VariableId, Value)] {
66        &self.entries
67    }
68
69    /// Extend with another assignment (other wins on conflict).
70    pub fn extend_from(&mut self, other: &Assignment) {
71        for (v, val) in &other.entries {
72            self.set(*v, val.clone());
73        }
74    }
75
76    /// Restrict to the given variables (order of `vars` preserved in returned values).
77    pub fn values_for(&self, vars: &[VariableId]) -> Result<Vec<Value>, EvalError> {
78        let mut out = Vec::with_capacity(vars.len());
79        for &v in vars {
80            let Some(val) = self.get(v) else {
81                return Err(EvalError::MissingBinding(v));
82            };
83            out.push(val.clone());
84        }
85        Ok(out)
86    }
87}
88
89/// Resolved distribution factor identity (no string keys).
90#[derive(Clone, Debug)]
91pub struct FactorSpec<'a> {
92    /// Factor variables.
93    pub variables: &'a [VariableId],
94    /// Conditioning variables.
95    pub conditioned_on: &'a [VariableId],
96    /// Hard intervention assignments.
97    pub intervention: &'a [InterventionAssignment],
98    /// Observational vs interventional domain.
99    pub domain: DomainRef,
100}
101
102/// Errors from compiling or evaluating causal expressions.
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub enum EvalError {
105    /// Continuous `IntegralOut` without quadrature nodes and without discrete support.
106    UnsupportedIntegralOut,
107    /// Provider has no entry for the requested factor / assignment.
108    MissingTableEntry,
109    /// Required variable binding absent from the assignment.
110    MissingBinding(VariableId),
111    /// Provider reported empty support for a summed variable.
112    EmptySupport(VariableId),
113    /// Division by zero while evaluating a ratio.
114    DivisionByZero,
115    /// Posterior draw index out of range.
116    DrawOutOfRange {
117        /// Requested draw.
118        draw: usize,
119        /// Number of draws available.
120        n_draws: usize,
121    },
122    /// Support row length does not match requested variable count.
123    SupportShape {
124        /// Expected arity.
125        expected: usize,
126        /// Actual arity.
127        actual: usize,
128    },
129    /// Empirical provider used where posterior draws are required (or vice versa).
130    ProviderKind(&'static str),
131}
132
133impl fmt::Display for EvalError {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            Self::UnsupportedIntegralOut => {
137                write!(f, "IntegralOut requires provider quadrature nodes or discrete support")
138            }
139            Self::MissingTableEntry => write!(f, "missing probability table entry"),
140            Self::MissingBinding(v) => write!(f, "missing binding for V{}", v.raw()),
141            Self::EmptySupport(v) => write!(f, "empty support for V{}", v.raw()),
142            Self::DivisionByZero => write!(f, "division by zero in ratio"),
143            Self::DrawOutOfRange { draw, n_draws } => {
144                write!(f, "draw {draw} out of range (n_draws={n_draws})")
145            }
146            Self::SupportShape { expected, actual } => {
147                write!(f, "support row arity {actual} != expected {expected}")
148            }
149            Self::ProviderKind(msg) => write!(f, "{msg}"),
150        }
151    }
152}
153
154impl std::error::Error for EvalError {}
155
156/// Provides densities, discrete supports, and outcome values for evaluation.
157pub trait DistributionProvider {
158    /// Probability / density mass for a factor under an assignment.
159    ///
160    /// # Errors
161    ///
162    /// Missing table entries, bad draw index, or shape errors.
163    fn probability(
164        &self,
165        spec: &FactorSpec<'_>,
166        assignment: &Assignment,
167        ctx: &EvalContext,
168    ) -> Result<f64, EvalError>;
169
170    /// Discrete support for variables (cartesian rows of values aligned to `vars`).
171    ///
172    /// # Errors
173    ///
174    /// Empty domains or unsupported queries.
175    fn support(
176        &self,
177        vars: &[VariableId],
178        ctx: &EvalContext,
179    ) -> Result<Arc<[Arc<[Value]>]>, EvalError>;
180
181    /// Optional continuous quadrature nodes `(assignment_row, Lebesgue weight)` for
182    /// [`crate::ExprNode::IntegralOut`].
183    ///
184    /// Returning `Ok(None)` asks the evaluator to fall back to discrete [`Self::support`].
185    ///
186    /// # Errors
187    ///
188    /// Provider-specific continuous-integration failures.
189    fn quadrature(
190        &self,
191        _vars: &[VariableId],
192        _ctx: &EvalContext,
193    ) -> Result<Option<QuadratureNodes>, EvalError> {
194        Ok(None)
195    }
196
197    /// Outcome function value (identity: the bound value of `var`).
198    ///
199    /// # Errors
200    ///
201    /// Missing binding or non-numeric value.
202    fn outcome(
203        &self,
204        var: VariableId,
205        assignment: &Assignment,
206        ctx: &EvalContext,
207    ) -> Result<f64, EvalError>;
208
209    /// Number of posterior draws, or `None` for a single empirical world.
210    fn n_draws(&self) -> Option<usize>;
211}
212
213/// Canonical key for a factor table row.
214#[derive(Clone, Debug, Eq, PartialEq, Hash)]
215struct FactorKey {
216    variables: Arc<[VariableId]>,
217    conditioned_on: Arc<[VariableId]>,
218    intervention: Arc<[InterventionAssignment]>,
219    domain: DomainRef,
220    /// Concatenation of values for `variables` then `conditioned_on`.
221    values: Arc<[Value]>,
222}
223
224fn factor_key(spec: &FactorSpec<'_>, assignment: &Assignment) -> Result<FactorKey, EvalError> {
225    let mut values = assignment.values_for(spec.variables)?;
226    values.extend(assignment.values_for(spec.conditioned_on)?);
227    Ok(FactorKey {
228        variables: Arc::from(spec.variables),
229        conditioned_on: Arc::from(spec.conditioned_on),
230        intervention: Arc::from(spec.intervention.to_vec()),
231        domain: spec.domain,
232        values: Arc::from(values),
233    })
234}
235
236/// Tabular empirical distribution provider (discrete factors + domains).
237#[derive(Clone, Debug, Default)]
238pub struct EmpiricalTableProvider {
239    domains: HashMap<VariableId, Arc<[Value]>>,
240    tables: HashMap<FactorKey, f64>,
241}
242
243impl EmpiricalTableProvider {
244    /// Empty provider.
245    #[must_use]
246    pub fn new() -> Self {
247        Self::default()
248    }
249
250    /// Declare discrete domain for a variable.
251    pub fn set_domain(&mut self, var: VariableId, values: impl IntoIterator<Item = Value>) {
252        let mut v: Vec<Value> = values.into_iter().collect();
253        // Stable unique by hash equality.
254        let mut seen = std::collections::HashSet::new();
255        v.retain(|x| seen.insert(x.clone()));
256        self.domains.insert(var, Arc::from(v));
257    }
258
259    /// Insert a factor probability for the given spec + assignment.
260    ///
261    /// # Errors
262    ///
263    /// Missing bindings for factor variables / conditions.
264    pub fn insert_probability(
265        &mut self,
266        spec: &FactorSpec<'_>,
267        assignment: &Assignment,
268        probability: f64,
269    ) -> Result<(), EvalError> {
270        let key = factor_key(spec, assignment)?;
271        self.tables.insert(key, probability);
272        Ok(())
273    }
274}
275
276impl DistributionProvider for EmpiricalTableProvider {
277    fn probability(
278        &self,
279        spec: &FactorSpec<'_>,
280        assignment: &Assignment,
281        _ctx: &EvalContext,
282    ) -> Result<f64, EvalError> {
283        let key = factor_key(spec, assignment)?;
284        self.tables.get(&key).copied().ok_or(EvalError::MissingTableEntry)
285    }
286
287    fn support(
288        &self,
289        vars: &[VariableId],
290        _ctx: &EvalContext,
291    ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
292        if vars.is_empty() {
293            return Ok(Arc::from(vec![Arc::from(Vec::<Value>::new())]));
294        }
295        let mut rows: Vec<Vec<Value>> = vec![Vec::new()];
296        for &v in vars {
297            let domain = self.domains.get(&v).ok_or(EvalError::EmptySupport(v))?;
298            if domain.is_empty() {
299                return Err(EvalError::EmptySupport(v));
300            }
301            let mut next = Vec::with_capacity(rows.len() * domain.len());
302            for prefix in &rows {
303                for val in domain.iter() {
304                    let mut row = prefix.clone();
305                    row.push(val.clone());
306                    next.push(row);
307                }
308            }
309            rows = next;
310        }
311        let out: Vec<Arc<[Value]>> = rows.into_iter().map(Arc::from).collect();
312        Ok(Arc::from(out))
313    }
314
315    fn outcome(
316        &self,
317        var: VariableId,
318        assignment: &Assignment,
319        _ctx: &EvalContext,
320    ) -> Result<f64, EvalError> {
321        let value = assignment.get(var).ok_or(EvalError::MissingBinding(var))?;
322        value.as_f64().ok_or(EvalError::MissingBinding(var))
323    }
324
325    fn n_draws(&self) -> Option<usize> {
326        None
327    }
328}
329
330/// Draw-indexed posterior provider: one [`EmpiricalTableProvider`] per draw.
331#[derive(Clone, Debug, Default)]
332pub struct PosteriorDrawProvider {
333    draws: Vec<EmpiricalTableProvider>,
334}
335
336impl PosteriorDrawProvider {
337    /// Empty posterior provider.
338    #[must_use]
339    pub fn new() -> Self {
340        Self::default()
341    }
342
343    /// Construct from per-draw empirical tables.
344    #[must_use]
345    pub fn from_draws(draws: Vec<EmpiricalTableProvider>) -> Self {
346        Self { draws }
347    }
348
349    /// Number of draws.
350    #[must_use]
351    pub fn len(&self) -> usize {
352        self.draws.len()
353    }
354
355    /// Whether there are no draws.
356    #[must_use]
357    pub fn is_empty(&self) -> bool {
358        self.draws.is_empty()
359    }
360
361    fn table(&self, ctx: &EvalContext) -> Result<&EmpiricalTableProvider, EvalError> {
362        let draw = ctx
363            .draw
364            .ok_or(EvalError::ProviderKind("PosteriorDrawProvider requires EvalContext.draw"))?;
365        self.draws.get(draw).ok_or(EvalError::DrawOutOfRange { draw, n_draws: self.draws.len() })
366    }
367}
368
369impl DistributionProvider for PosteriorDrawProvider {
370    fn probability(
371        &self,
372        spec: &FactorSpec<'_>,
373        assignment: &Assignment,
374        ctx: &EvalContext,
375    ) -> Result<f64, EvalError> {
376        self.table(ctx)?.probability(spec, assignment, ctx)
377    }
378
379    fn support(
380        &self,
381        vars: &[VariableId],
382        ctx: &EvalContext,
383    ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
384        self.table(ctx)?.support(vars, ctx)
385    }
386
387    fn outcome(
388        &self,
389        var: VariableId,
390        assignment: &Assignment,
391        ctx: &EvalContext,
392    ) -> Result<f64, EvalError> {
393        self.table(ctx)?.outcome(var, assignment, ctx)
394    }
395
396    fn n_draws(&self) -> Option<usize> {
397        Some(self.draws.len())
398    }
399}
400
401/// Independent Gaussian density provider with Gauss–Hermite quadrature.
402///
403/// `probability` returns the product of univariate N(μ, σ²) densities for factor
404/// variables. [`Self::quadrature`] returns product Gauss–Hermite nodes in
405/// Lebesgue measure (suitable for ∫ body(x) dx near each Gaussian mode).
406#[derive(Clone, Debug, Default)]
407pub struct GaussianDensityProvider {
408    /// Per-variable (mean, variance).
409    params: HashMap<VariableId, (f64, f64)>,
410}
411
412impl GaussianDensityProvider {
413    /// Empty provider.
414    #[must_use]
415    pub fn new() -> Self {
416        Self::default()
417    }
418
419    /// Declare an independent Gaussian for `var` with mean `mean` and variance `variance`.
420    ///
421    /// # Panics
422    ///
423    /// Never panics; non-positive variance is rejected by returning early no-op... actually
424    /// we clamp: variance must be > 0 or the insert is skipped. Prefer validating at call sites.
425    pub fn set_gaussian(&mut self, var: VariableId, mean: f64, variance: f64) {
426        if variance > 0.0 && variance.is_finite() && mean.is_finite() {
427            self.params.insert(var, (mean, variance));
428        }
429    }
430}
431
432/// Physicists' Gauss–Hermite nodes/weights for ∫ e^{-t²} g(t) dt (n = 5).
433const GH5_NODES: [f64; 5] = [
434    -2.020_182_870_456_085_6,
435    -0.958_572_464_613_818_5,
436    0.0,
437    0.958_572_464_613_818_5,
438    2.020_182_870_456_085_6,
439];
440const GH5_WEIGHTS: [f64; 5] = [
441    0.019_953_242_059_045_913,
442    0.393_619_323_152_241_35,
443    0.945_308_720_482_941_9,
444    0.393_619_323_152_241_35,
445    0.019_953_242_059_045_913,
446];
447
448impl DistributionProvider for GaussianDensityProvider {
449    fn probability(
450        &self,
451        spec: &FactorSpec<'_>,
452        assignment: &Assignment,
453        _ctx: &EvalContext,
454    ) -> Result<f64, EvalError> {
455        let mut dens = 1.0;
456        for &v in spec.variables {
457            let (mean, var) = self.params.get(&v).copied().ok_or(EvalError::EmptySupport(v))?;
458            let x =
459                assignment.get(v).and_then(Value::as_f64).ok_or(EvalError::MissingBinding(v))?;
460            let inv_sqrt = (2.0 * std::f64::consts::PI * var).sqrt().recip();
461            let z = (x - mean) / var.sqrt();
462            dens *= inv_sqrt * (-0.5 * z * z).exp();
463        }
464        Ok(dens)
465    }
466
467    fn support(
468        &self,
469        vars: &[VariableId],
470        _ctx: &EvalContext,
471    ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
472        if vars.is_empty() {
473            return Ok(Arc::from(vec![Arc::from(Vec::<Value>::new())]));
474        }
475        Err(EvalError::EmptySupport(vars[0]))
476    }
477
478    fn quadrature(
479        &self,
480        vars: &[VariableId],
481        _ctx: &EvalContext,
482    ) -> Result<Option<QuadratureNodes>, EvalError> {
483        if vars.is_empty() {
484            return Ok(Some(Arc::from([(Arc::from(Vec::<Value>::new()), 1.0)])));
485        }
486        // Product GH: start with empty prefix.
487        let mut nodes: Vec<(Vec<Value>, f64)> = vec![(Vec::new(), 1.0)];
488        for &v in vars {
489            let (mean, variance) =
490                self.params.get(&v).copied().ok_or(EvalError::EmptySupport(v))?;
491            let sigma = variance.sqrt();
492            let scale = sigma * std::f64::consts::SQRT_2;
493            let mut next = Vec::with_capacity(nodes.len() * GH5_NODES.len());
494            for (prefix, w0) in &nodes {
495                for (i, &t) in GH5_NODES.iter().enumerate() {
496                    let x = mean + scale * t;
497                    // GH computes ∫ e^{-t²} g(t) dt. For Lebesgue ∫ h(x) dx with
498                    // x = μ + σ√2 t we need g(t) = h(x) σ√2 e^{t²}, so the node
499                    // weight applied to h(x) is w_i · σ√2 · e^{t²}.
500                    let w = w0 * GH5_WEIGHTS[i] * scale * (t * t).exp();
501                    let mut row = prefix.clone();
502                    row.push(Value::f64(x));
503                    next.push((row, w));
504                }
505            }
506            nodes = next;
507        }
508        let out: Vec<(Arc<[Value]>, f64)> =
509            nodes.into_iter().map(|(row, w)| (Arc::from(row), w)).collect();
510        Ok(Some(Arc::from(out)))
511    }
512
513    fn outcome(
514        &self,
515        var: VariableId,
516        assignment: &Assignment,
517        _ctx: &EvalContext,
518    ) -> Result<f64, EvalError> {
519        let value = assignment.get(var).ok_or(EvalError::MissingBinding(var))?;
520        value.as_f64().ok_or(EvalError::MissingBinding(var))
521    }
522
523    fn n_draws(&self) -> Option<usize> {
524        None
525    }
526}