1use std::borrow::Borrow;
6use std::collections::HashMap;
7use std::fmt;
8use std::hash::{Hash, Hasher};
9use std::sync::{Arc, PoisonError, RwLock};
10
11use antecedent_core::{Value, VariableId};
12
13use crate::{DomainRef, InterventionAssignment};
14
15pub type QuadratureNodes = Arc<[(Arc<[Value]>, f64)]>;
17
18type SupportRows = Arc<[Arc<[Value]>]>;
20
21#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
23pub struct EvalContext {
24 pub draw: Option<usize>,
26}
27
28#[derive(Clone, Debug, Default)]
30pub struct Assignment {
31 entries: Vec<(VariableId, Value)>,
33}
34
35impl Assignment {
36 #[must_use]
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 #[must_use]
44 pub fn from_pairs(pairs: impl IntoIterator<Item = (VariableId, Value)>) -> Self {
45 let mut entries: Vec<(VariableId, Value)> = pairs.into_iter().collect();
46 entries.sort_by_key(|(v, _)| v.raw());
47 entries.dedup_by_key(|(v, _)| *v);
48 Self { entries }
49 }
50
51 pub fn set(&mut self, var: VariableId, value: Value) {
53 match self.entries.binary_search_by_key(&var.raw(), |(v, _)| v.raw()) {
54 Ok(i) => self.entries[i].1 = value,
55 Err(i) => self.entries.insert(i, (var, value)),
56 }
57 }
58
59 #[must_use]
61 pub fn get(&self, var: VariableId) -> Option<&Value> {
62 self.entries
63 .binary_search_by_key(&var.raw(), |(v, _)| v.raw())
64 .ok()
65 .map(|i| &self.entries[i].1)
66 }
67
68 #[must_use]
70 pub fn entries(&self) -> &[(VariableId, Value)] {
71 &self.entries
72 }
73
74 pub fn extend_from(&mut self, other: &Assignment) {
76 for (v, val) in &other.entries {
77 self.set(*v, val.clone());
78 }
79 }
80
81 pub fn remove(&mut self, var: VariableId) -> Option<Value> {
87 match self.entries.binary_search_by_key(&var.raw(), |(v, _)| v.raw()) {
88 Ok(i) => Some(self.entries.remove(i).1),
89 Err(_) => None,
90 }
91 }
92
93 pub fn values_for(&self, vars: &[VariableId]) -> Result<Vec<Value>, EvalError> {
95 let mut out = Vec::with_capacity(vars.len());
96 for &v in vars {
97 let Some(val) = self.get(v) else {
98 return Err(EvalError::MissingBinding(v));
99 };
100 out.push(val.clone());
101 }
102 Ok(out)
103 }
104}
105
106#[derive(Clone, Debug)]
108pub struct FactorSpec<'a> {
109 pub variables: &'a [VariableId],
111 pub conditioned_on: &'a [VariableId],
113 pub intervention: &'a [InterventionAssignment],
115 pub domain: DomainRef,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
121#[non_exhaustive]
122pub enum EvalError {
123 UnsupportedIntegralOut,
125 MissingTableEntry,
127 MissingBinding(VariableId),
129 EmptySupport(VariableId),
131 DivisionByZero,
133 DrawOutOfRange {
135 draw: usize,
137 n_draws: usize,
139 },
140 SupportShape {
142 expected: usize,
144 actual: usize,
146 },
147 ProviderKind(&'static str),
149 UnsupportedConditioning(&'static str),
152}
153
154impl fmt::Display for EvalError {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 match self {
157 Self::UnsupportedIntegralOut => {
158 write!(f, "IntegralOut requires provider quadrature nodes or discrete support")
159 }
160 Self::MissingTableEntry => write!(f, "missing probability table entry"),
161 Self::MissingBinding(v) => write!(f, "missing binding for V{}", v.raw()),
162 Self::EmptySupport(v) => write!(f, "empty support for V{}", v.raw()),
163 Self::DivisionByZero => write!(f, "division by zero in ratio"),
164 Self::DrawOutOfRange { draw, n_draws } => {
165 write!(f, "draw {draw} out of range (n_draws={n_draws})")
166 }
167 Self::SupportShape { expected, actual } => {
168 write!(f, "support row arity {actual} != expected {expected}")
169 }
170 Self::ProviderKind(msg) | Self::UnsupportedConditioning(msg) => write!(f, "{msg}"),
171 }
172 }
173}
174
175impl std::error::Error for EvalError {}
176
177pub trait DistributionProvider {
179 fn probability(
185 &self,
186 spec: &FactorSpec<'_>,
187 assignment: &Assignment,
188 ctx: &EvalContext,
189 ) -> Result<f64, EvalError>;
190
191 fn support(
197 &self,
198 vars: &[VariableId],
199 ctx: &EvalContext,
200 ) -> Result<Arc<[Arc<[Value]>]>, EvalError>;
201
202 fn quadrature(
211 &self,
212 _vars: &[VariableId],
213 _ctx: &EvalContext,
214 ) -> Result<Option<QuadratureNodes>, EvalError> {
215 Ok(None)
216 }
217
218 fn outcome(
224 &self,
225 var: VariableId,
226 assignment: &Assignment,
227 ctx: &EvalContext,
228 ) -> Result<f64, EvalError>;
229
230 fn n_draws(&self) -> Option<usize>;
232}
233
234#[derive(Clone, Debug, Eq)]
240struct FactorKey {
241 variables: Arc<[VariableId]>,
242 conditioned_on: Arc<[VariableId]>,
243 intervention: Arc<[InterventionAssignment]>,
244 domain: DomainRef,
245 values: Arc<[Value]>,
247}
248
249#[derive(Clone, Copy)]
256struct FactorKeyView<'a> {
257 variables: &'a [VariableId],
258 conditioned_on: &'a [VariableId],
259 intervention: &'a [InterventionAssignment],
260 domain: DomainRef,
261 values: &'a [Value],
263}
264
265trait FactorKeyLookup {
269 fn view(&self) -> FactorKeyView<'_>;
270}
271
272impl FactorKeyLookup for FactorKey {
273 fn view(&self) -> FactorKeyView<'_> {
274 FactorKeyView {
275 variables: &self.variables,
276 conditioned_on: &self.conditioned_on,
277 intervention: &self.intervention,
278 domain: self.domain,
279 values: &self.values,
280 }
281 }
282}
283
284impl FactorKeyLookup for FactorKeyView<'_> {
285 fn view(&self) -> FactorKeyView<'_> {
286 *self
287 }
288}
289
290impl<'a> Borrow<dyn FactorKeyLookup + 'a> for FactorKey {
291 fn borrow(&self) -> &(dyn FactorKeyLookup + 'a) {
292 self
293 }
294}
295
296fn hash_factor_view<H: Hasher>(v: &FactorKeyView<'_>, state: &mut H) {
299 v.variables.hash(state);
300 v.conditioned_on.hash(state);
301 v.intervention.hash(state);
302 v.domain.hash(state);
303 v.values.hash(state);
304}
305
306impl Hash for FactorKey {
307 fn hash<H: Hasher>(&self, state: &mut H) {
308 hash_factor_view(&self.view(), state);
309 }
310}
311
312impl PartialEq for FactorKey {
313 fn eq(&self, other: &Self) -> bool {
314 factor_views_eq(&self.view(), &other.view())
315 }
316}
317
318impl Hash for dyn FactorKeyLookup + '_ {
319 fn hash<H: Hasher>(&self, state: &mut H) {
320 hash_factor_view(&self.view(), state);
321 }
322}
323
324impl PartialEq for dyn FactorKeyLookup + '_ {
325 fn eq(&self, other: &Self) -> bool {
326 factor_views_eq(&self.view(), &other.view())
327 }
328}
329
330impl Eq for dyn FactorKeyLookup + '_ {}
331
332fn factor_views_eq(a: &FactorKeyView<'_>, b: &FactorKeyView<'_>) -> bool {
333 a.variables == b.variables
334 && a.conditioned_on == b.conditioned_on
335 && a.intervention == b.intervention
336 && a.domain == b.domain
337 && a.values == b.values
338}
339
340fn factor_values(spec: &FactorSpec<'_>, assignment: &Assignment) -> Result<Vec<Value>, EvalError> {
343 let mut values = Vec::with_capacity(spec.variables.len() + spec.conditioned_on.len());
344 for &v in spec.variables.iter().chain(spec.conditioned_on.iter()) {
345 let Some(val) = assignment.get(v) else {
346 return Err(EvalError::MissingBinding(v));
347 };
348 values.push(val.clone());
349 }
350 Ok(values)
351}
352
353fn factor_key(spec: &FactorSpec<'_>, assignment: &Assignment) -> Result<FactorKey, EvalError> {
355 let values = factor_values(spec, assignment)?;
356 Ok(FactorKey {
357 variables: Arc::from(spec.variables),
358 conditioned_on: Arc::from(spec.conditioned_on),
359 intervention: Arc::from(spec.intervention.to_vec()),
360 domain: spec.domain,
361 values: Arc::from(values),
362 })
363}
364
365#[derive(Debug, Default)]
367pub struct EmpiricalTableProvider {
368 domains: HashMap<VariableId, Arc<[Value]>>,
369 tables: HashMap<FactorKey, f64>,
370 support_cache: RwLock<HashMap<Vec<VariableId>, SupportRows>>,
378}
379
380impl Clone for EmpiricalTableProvider {
381 fn clone(&self) -> Self {
382 Self {
383 domains: self.domains.clone(),
384 tables: self.tables.clone(),
385 support_cache: RwLock::new(
388 self.support_cache.read().unwrap_or_else(PoisonError::into_inner).clone(),
389 ),
390 }
391 }
392}
393
394impl EmpiricalTableProvider {
395 #[must_use]
397 pub fn new() -> Self {
398 Self::default()
399 }
400
401 pub fn set_domain(&mut self, var: VariableId, values: impl IntoIterator<Item = Value>) {
403 let mut v: Vec<Value> = values.into_iter().collect();
404 let mut seen = std::collections::HashSet::new();
406 v.retain(|x| seen.insert(x.clone()));
407 self.domains.insert(var, Arc::from(v));
408 self.support_cache.write().unwrap_or_else(PoisonError::into_inner).clear();
411 }
412
413 pub fn insert_probability(
419 &mut self,
420 spec: &FactorSpec<'_>,
421 assignment: &Assignment,
422 probability: f64,
423 ) -> Result<(), EvalError> {
424 let key = factor_key(spec, assignment)?;
425 self.tables.insert(key, probability);
426 Ok(())
427 }
428}
429
430impl DistributionProvider for EmpiricalTableProvider {
431 fn probability(
432 &self,
433 spec: &FactorSpec<'_>,
434 assignment: &Assignment,
435 _ctx: &EvalContext,
436 ) -> Result<f64, EvalError> {
437 let values = factor_values(spec, assignment)?;
440 let key = FactorKeyView {
441 variables: spec.variables,
442 conditioned_on: spec.conditioned_on,
443 intervention: spec.intervention,
444 domain: spec.domain,
445 values: &values,
446 };
447 self.tables.get(&key as &dyn FactorKeyLookup).copied().ok_or(EvalError::MissingTableEntry)
448 }
449
450 fn support(
451 &self,
452 vars: &[VariableId],
453 _ctx: &EvalContext,
454 ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
455 if let Some(hit) =
461 self.support_cache.read().unwrap_or_else(PoisonError::into_inner).get(vars)
462 {
463 return Ok(Arc::clone(hit));
464 }
465 let mut rows: Vec<Vec<Value>> = vec![Vec::new()];
468 for &v in vars {
469 let domain = self.domains.get(&v).ok_or(EvalError::EmptySupport(v))?;
470 if domain.is_empty() {
471 return Err(EvalError::EmptySupport(v));
472 }
473 let mut next = Vec::with_capacity(rows.len() * domain.len());
474 for prefix in &rows {
475 for val in domain.iter() {
476 let mut row = prefix.clone();
477 row.push(val.clone());
478 next.push(row);
479 }
480 }
481 rows = next;
482 }
483 let out: Arc<[Arc<[Value]>]> = rows.into_iter().map(Arc::from).collect();
484 self.support_cache
485 .write()
486 .unwrap_or_else(PoisonError::into_inner)
487 .insert(vars.to_vec(), Arc::clone(&out));
488 Ok(out)
489 }
490
491 fn outcome(
492 &self,
493 var: VariableId,
494 assignment: &Assignment,
495 _ctx: &EvalContext,
496 ) -> Result<f64, EvalError> {
497 let value = assignment.get(var).ok_or(EvalError::MissingBinding(var))?;
498 value.as_f64().ok_or(EvalError::MissingBinding(var))
499 }
500
501 fn n_draws(&self) -> Option<usize> {
502 None
503 }
504}
505
506#[derive(Clone, Debug, Default)]
508pub struct PosteriorDrawProvider {
509 draws: Vec<EmpiricalTableProvider>,
510}
511
512impl PosteriorDrawProvider {
513 #[must_use]
515 pub fn new() -> Self {
516 Self::default()
517 }
518
519 #[must_use]
521 pub fn from_draws(draws: Vec<EmpiricalTableProvider>) -> Self {
522 Self { draws }
523 }
524
525 #[must_use]
527 pub fn len(&self) -> usize {
528 self.draws.len()
529 }
530
531 #[must_use]
533 pub fn is_empty(&self) -> bool {
534 self.draws.is_empty()
535 }
536
537 fn table(&self, ctx: &EvalContext) -> Result<&EmpiricalTableProvider, EvalError> {
538 let draw = ctx
539 .draw
540 .ok_or(EvalError::ProviderKind("PosteriorDrawProvider requires EvalContext.draw"))?;
541 self.draws.get(draw).ok_or(EvalError::DrawOutOfRange { draw, n_draws: self.draws.len() })
542 }
543}
544
545impl DistributionProvider for PosteriorDrawProvider {
546 fn probability(
547 &self,
548 spec: &FactorSpec<'_>,
549 assignment: &Assignment,
550 ctx: &EvalContext,
551 ) -> Result<f64, EvalError> {
552 self.table(ctx)?.probability(spec, assignment, ctx)
553 }
554
555 fn support(
556 &self,
557 vars: &[VariableId],
558 ctx: &EvalContext,
559 ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
560 self.table(ctx)?.support(vars, ctx)
561 }
562
563 fn outcome(
564 &self,
565 var: VariableId,
566 assignment: &Assignment,
567 ctx: &EvalContext,
568 ) -> Result<f64, EvalError> {
569 self.table(ctx)?.outcome(var, assignment, ctx)
570 }
571
572 fn n_draws(&self) -> Option<usize> {
573 Some(self.draws.len())
574 }
575}
576
577#[derive(Clone, Debug, Default)]
583pub struct GaussianDensityProvider {
584 params: HashMap<VariableId, (f64, f64)>,
586}
587
588impl GaussianDensityProvider {
589 #[must_use]
591 pub fn new() -> Self {
592 Self::default()
593 }
594
595 pub fn set_gaussian(&mut self, var: VariableId, mean: f64, variance: f64) {
602 if variance > 0.0 && variance.is_finite() && mean.is_finite() {
603 self.params.insert(var, (mean, variance));
604 }
605 }
606}
607
608const GH5_NODES: [f64; 5] = [
610 -2.020_182_870_456_085_6,
611 -0.958_572_464_613_818_5,
612 0.0,
613 0.958_572_464_613_818_5,
614 2.020_182_870_456_085_6,
615];
616const GH5_WEIGHTS: [f64; 5] = [
617 0.019_953_242_059_045_913,
618 0.393_619_323_152_241_35,
619 0.945_308_720_482_941_9,
620 0.393_619_323_152_241_35,
621 0.019_953_242_059_045_913,
622];
623
624impl DistributionProvider for GaussianDensityProvider {
625 fn probability(
626 &self,
627 spec: &FactorSpec<'_>,
628 assignment: &Assignment,
629 _ctx: &EvalContext,
630 ) -> Result<f64, EvalError> {
631 if !spec.conditioned_on.is_empty() {
635 return Err(EvalError::UnsupportedConditioning(
636 "GaussianDensityProvider models independent Gaussians and cannot answer \
637 conditional queries; conditioned_on must be empty",
638 ));
639 }
640 let mut dens = 1.0;
641 for &v in spec.variables {
642 let (mean, var) = self.params.get(&v).copied().ok_or(EvalError::EmptySupport(v))?;
643 let x =
644 assignment.get(v).and_then(Value::as_f64).ok_or(EvalError::MissingBinding(v))?;
645 let inv_sqrt = (2.0 * std::f64::consts::PI * var).sqrt().recip();
646 let z = (x - mean) / var.sqrt();
647 dens *= inv_sqrt * (-0.5 * z * z).exp();
648 }
649 Ok(dens)
650 }
651
652 fn support(
653 &self,
654 vars: &[VariableId],
655 _ctx: &EvalContext,
656 ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
657 if vars.is_empty() {
658 return Ok(Arc::from(vec![Arc::from(Vec::<Value>::new())]));
659 }
660 Err(EvalError::EmptySupport(vars[0]))
661 }
662
663 fn quadrature(
664 &self,
665 vars: &[VariableId],
666 _ctx: &EvalContext,
667 ) -> Result<Option<QuadratureNodes>, EvalError> {
668 if vars.is_empty() {
669 return Ok(Some(Arc::from([(Arc::from(Vec::<Value>::new()), 1.0)])));
670 }
671 let mut nodes: Vec<(Vec<Value>, f64)> = vec![(Vec::new(), 1.0)];
673 for &v in vars {
674 let (mean, variance) =
675 self.params.get(&v).copied().ok_or(EvalError::EmptySupport(v))?;
676 let sigma = variance.sqrt();
677 let scale = sigma * std::f64::consts::SQRT_2;
678 let mut next = Vec::with_capacity(nodes.len() * GH5_NODES.len());
679 for (prefix, w0) in &nodes {
680 for (i, &t) in GH5_NODES.iter().enumerate() {
681 let x = mean + scale * t;
682 let w = w0 * GH5_WEIGHTS[i] * scale * (t * t).exp();
686 let mut row = prefix.clone();
687 row.push(Value::f64(x));
688 next.push((row, w));
689 }
690 }
691 nodes = next;
692 }
693 let out: Vec<(Arc<[Value]>, f64)> =
694 nodes.into_iter().map(|(row, w)| (Arc::from(row), w)).collect();
695 Ok(Some(Arc::from(out)))
696 }
697
698 fn outcome(
699 &self,
700 var: VariableId,
701 assignment: &Assignment,
702 _ctx: &EvalContext,
703 ) -> Result<f64, EvalError> {
704 let value = assignment.get(var).ok_or(EvalError::MissingBinding(var))?;
705 value.as_f64().ok_or(EvalError::MissingBinding(var))
706 }
707
708 fn n_draws(&self) -> Option<usize> {
709 None
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 fn v(id: u32) -> VariableId {
718 VariableId::from_raw(id)
719 }
720
721 fn f(x: f64) -> Value {
722 Value::f64(x)
723 }
724
725 #[test]
726 fn empirical_table_missing_entry_errors() {
727 let mut p = EmpiricalTableProvider::new();
730 let y = v(0);
731 p.set_domain(y, [f(0.0), f(1.0)]);
732 let spec = FactorSpec {
733 variables: &[y],
734 conditioned_on: &[],
735 intervention: &[],
736 domain: DomainRef::Observational,
737 };
738 let assignment = Assignment::from_pairs([(y, f(0.0))]);
739 let err = p.probability(&spec, &assignment, &EvalContext::default()).unwrap_err();
740 assert_eq!(err, EvalError::MissingTableEntry);
741 }
742
743 #[test]
744 fn support_memoizes_and_invalidates_on_set_domain() {
745 let mut p = EmpiricalTableProvider::new();
749 let a = v(0);
750 let b = v(1);
751 p.set_domain(a, [f(0.0), f(1.0)]);
752 p.set_domain(b, [f(0.0), f(1.0), f(2.0)]);
753 let ctx = EvalContext::default();
754 let first = p.support(&[a, b], &ctx).unwrap();
755 assert_eq!(first.len(), 6);
756 let second = p.support(&[a, b], &ctx).unwrap();
757 assert!(Arc::ptr_eq(&first, &second), "cache hit must return the shared rows");
758 p.set_domain(b, [f(0.0), f(1.0)]);
759 let third = p.support(&[a, b], &ctx).unwrap();
760 assert!(!Arc::ptr_eq(&first, &third), "set_domain must invalidate the cache");
761 let rows: Vec<Vec<f64>> =
763 third.iter().map(|r| r.iter().map(|x| x.as_f64().unwrap()).collect()).collect();
764 assert_eq!(rows, vec![vec![0.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0], vec![1.0, 1.0]]);
765 }
766
767 #[test]
768 fn empty_support_query_yields_single_empty_row() {
769 let p = EmpiricalTableProvider::new();
772 let rows = p.support(&[], &EvalContext::default()).unwrap();
773 assert_eq!(rows.len(), 1);
774 assert!(rows[0].is_empty());
775 }
776
777 #[test]
778 fn borrowed_key_lookup_matches_owned_insert() {
779 let mut p = EmpiricalTableProvider::new();
784 let y = v(0);
785 let z = v(1);
786 let t = v(2);
787 let interv = [InterventionAssignment { variable: t, value: f(1.0) }];
788 let spec = FactorSpec {
789 variables: &[y],
790 conditioned_on: &[z],
791 intervention: &interv,
792 domain: DomainRef::Interventional,
793 };
794 let assign = Assignment::from_pairs([(y, f(1.0)), (z, f(0.0))]);
795 p.insert_probability(&spec, &assign, 0.25).unwrap();
796 let ctx = EvalContext::default();
797 assert!((p.probability(&spec, &assign, &ctx).unwrap() - 0.25).abs() < 1e-15);
798 let other = Assignment::from_pairs([(y, f(1.0)), (z, f(1.0))]);
799 assert_eq!(p.probability(&spec, &other, &ctx).unwrap_err(), EvalError::MissingTableEntry);
800 let obs = FactorSpec { domain: DomainRef::Observational, ..spec.clone() };
801 assert_eq!(p.probability(&obs, &assign, &ctx).unwrap_err(), EvalError::MissingTableEntry);
802 }
803
804 #[test]
805 fn gaussian_provider_rejects_conditional_query() {
806 let mut p = GaussianDensityProvider::new();
810 let y = v(0);
811 let z = v(1);
812 p.set_gaussian(y, 0.0, 1.0);
813 p.set_gaussian(z, 0.0, 1.0);
814 let spec = FactorSpec {
815 variables: &[y],
816 conditioned_on: &[z],
817 intervention: &[],
818 domain: DomainRef::Observational,
819 };
820 let assignment = Assignment::from_pairs([(y, f(0.5)), (z, f(0.2))]);
821 let err = p.probability(&spec, &assignment, &EvalContext::default()).unwrap_err();
822 assert!(matches!(err, EvalError::UnsupportedConditioning(_)));
823 }
824}