1use std::collections::HashMap;
6use std::fmt;
7use std::sync::Arc;
8
9use antecedent_core::{Value, VariableId};
10
11use crate::{DomainRef, InterventionAssignment};
12
13pub type QuadratureNodes = Arc<[(Arc<[Value]>, f64)]>;
15
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
18pub struct EvalContext {
19 pub draw: Option<usize>,
21}
22
23#[derive(Clone, Debug, Default)]
25pub struct Assignment {
26 entries: Vec<(VariableId, Value)>,
28}
29
30impl Assignment {
31 #[must_use]
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 #[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 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 #[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 #[must_use]
65 pub fn entries(&self) -> &[(VariableId, Value)] {
66 &self.entries
67 }
68
69 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 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#[derive(Clone, Debug)]
91pub struct FactorSpec<'a> {
92 pub variables: &'a [VariableId],
94 pub conditioned_on: &'a [VariableId],
96 pub intervention: &'a [InterventionAssignment],
98 pub domain: DomainRef,
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
104#[non_exhaustive]
105pub enum EvalError {
106 UnsupportedIntegralOut,
108 MissingTableEntry,
110 MissingBinding(VariableId),
112 EmptySupport(VariableId),
114 DivisionByZero,
116 DrawOutOfRange {
118 draw: usize,
120 n_draws: usize,
122 },
123 SupportShape {
125 expected: usize,
127 actual: usize,
129 },
130 ProviderKind(&'static str),
132 UnsupportedConditioning(&'static str),
135}
136
137impl fmt::Display for EvalError {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 match self {
140 Self::UnsupportedIntegralOut => {
141 write!(f, "IntegralOut requires provider quadrature nodes or discrete support")
142 }
143 Self::MissingTableEntry => write!(f, "missing probability table entry"),
144 Self::MissingBinding(v) => write!(f, "missing binding for V{}", v.raw()),
145 Self::EmptySupport(v) => write!(f, "empty support for V{}", v.raw()),
146 Self::DivisionByZero => write!(f, "division by zero in ratio"),
147 Self::DrawOutOfRange { draw, n_draws } => {
148 write!(f, "draw {draw} out of range (n_draws={n_draws})")
149 }
150 Self::SupportShape { expected, actual } => {
151 write!(f, "support row arity {actual} != expected {expected}")
152 }
153 Self::ProviderKind(msg) | Self::UnsupportedConditioning(msg) => write!(f, "{msg}"),
154 }
155 }
156}
157
158impl std::error::Error for EvalError {}
159
160pub trait DistributionProvider {
162 fn probability(
168 &self,
169 spec: &FactorSpec<'_>,
170 assignment: &Assignment,
171 ctx: &EvalContext,
172 ) -> Result<f64, EvalError>;
173
174 fn support(
180 &self,
181 vars: &[VariableId],
182 ctx: &EvalContext,
183 ) -> Result<Arc<[Arc<[Value]>]>, EvalError>;
184
185 fn quadrature(
194 &self,
195 _vars: &[VariableId],
196 _ctx: &EvalContext,
197 ) -> Result<Option<QuadratureNodes>, EvalError> {
198 Ok(None)
199 }
200
201 fn outcome(
207 &self,
208 var: VariableId,
209 assignment: &Assignment,
210 ctx: &EvalContext,
211 ) -> Result<f64, EvalError>;
212
213 fn n_draws(&self) -> Option<usize>;
215}
216
217#[derive(Clone, Debug, Eq, PartialEq, Hash)]
219struct FactorKey {
220 variables: Arc<[VariableId]>,
221 conditioned_on: Arc<[VariableId]>,
222 intervention: Arc<[InterventionAssignment]>,
223 domain: DomainRef,
224 values: Arc<[Value]>,
226}
227
228fn factor_key(spec: &FactorSpec<'_>, assignment: &Assignment) -> Result<FactorKey, EvalError> {
229 let mut values = assignment.values_for(spec.variables)?;
230 values.extend(assignment.values_for(spec.conditioned_on)?);
231 Ok(FactorKey {
232 variables: Arc::from(spec.variables),
233 conditioned_on: Arc::from(spec.conditioned_on),
234 intervention: Arc::from(spec.intervention.to_vec()),
235 domain: spec.domain,
236 values: Arc::from(values),
237 })
238}
239
240#[derive(Clone, Debug, Default)]
242pub struct EmpiricalTableProvider {
243 domains: HashMap<VariableId, Arc<[Value]>>,
244 tables: HashMap<FactorKey, f64>,
245}
246
247impl EmpiricalTableProvider {
248 #[must_use]
250 pub fn new() -> Self {
251 Self::default()
252 }
253
254 pub fn set_domain(&mut self, var: VariableId, values: impl IntoIterator<Item = Value>) {
256 let mut v: Vec<Value> = values.into_iter().collect();
257 let mut seen = std::collections::HashSet::new();
259 v.retain(|x| seen.insert(x.clone()));
260 self.domains.insert(var, Arc::from(v));
261 }
262
263 pub fn insert_probability(
269 &mut self,
270 spec: &FactorSpec<'_>,
271 assignment: &Assignment,
272 probability: f64,
273 ) -> Result<(), EvalError> {
274 let key = factor_key(spec, assignment)?;
275 self.tables.insert(key, probability);
276 Ok(())
277 }
278}
279
280impl DistributionProvider for EmpiricalTableProvider {
281 fn probability(
282 &self,
283 spec: &FactorSpec<'_>,
284 assignment: &Assignment,
285 _ctx: &EvalContext,
286 ) -> Result<f64, EvalError> {
287 let key = factor_key(spec, assignment)?;
288 self.tables.get(&key).copied().ok_or(EvalError::MissingTableEntry)
289 }
290
291 fn support(
292 &self,
293 vars: &[VariableId],
294 _ctx: &EvalContext,
295 ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
296 if vars.is_empty() {
297 return Ok(Arc::from(vec![Arc::from(Vec::<Value>::new())]));
298 }
299 let mut rows: Vec<Vec<Value>> = vec![Vec::new()];
300 for &v in vars {
301 let domain = self.domains.get(&v).ok_or(EvalError::EmptySupport(v))?;
302 if domain.is_empty() {
303 return Err(EvalError::EmptySupport(v));
304 }
305 let mut next = Vec::with_capacity(rows.len() * domain.len());
306 for prefix in &rows {
307 for val in domain.iter() {
308 let mut row = prefix.clone();
309 row.push(val.clone());
310 next.push(row);
311 }
312 }
313 rows = next;
314 }
315 let out: Vec<Arc<[Value]>> = rows.into_iter().map(Arc::from).collect();
316 Ok(Arc::from(out))
317 }
318
319 fn outcome(
320 &self,
321 var: VariableId,
322 assignment: &Assignment,
323 _ctx: &EvalContext,
324 ) -> Result<f64, EvalError> {
325 let value = assignment.get(var).ok_or(EvalError::MissingBinding(var))?;
326 value.as_f64().ok_or(EvalError::MissingBinding(var))
327 }
328
329 fn n_draws(&self) -> Option<usize> {
330 None
331 }
332}
333
334#[derive(Clone, Debug, Default)]
336pub struct PosteriorDrawProvider {
337 draws: Vec<EmpiricalTableProvider>,
338}
339
340impl PosteriorDrawProvider {
341 #[must_use]
343 pub fn new() -> Self {
344 Self::default()
345 }
346
347 #[must_use]
349 pub fn from_draws(draws: Vec<EmpiricalTableProvider>) -> Self {
350 Self { draws }
351 }
352
353 #[must_use]
355 pub fn len(&self) -> usize {
356 self.draws.len()
357 }
358
359 #[must_use]
361 pub fn is_empty(&self) -> bool {
362 self.draws.is_empty()
363 }
364
365 fn table(&self, ctx: &EvalContext) -> Result<&EmpiricalTableProvider, EvalError> {
366 let draw = ctx
367 .draw
368 .ok_or(EvalError::ProviderKind("PosteriorDrawProvider requires EvalContext.draw"))?;
369 self.draws.get(draw).ok_or(EvalError::DrawOutOfRange { draw, n_draws: self.draws.len() })
370 }
371}
372
373impl DistributionProvider for PosteriorDrawProvider {
374 fn probability(
375 &self,
376 spec: &FactorSpec<'_>,
377 assignment: &Assignment,
378 ctx: &EvalContext,
379 ) -> Result<f64, EvalError> {
380 self.table(ctx)?.probability(spec, assignment, ctx)
381 }
382
383 fn support(
384 &self,
385 vars: &[VariableId],
386 ctx: &EvalContext,
387 ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
388 self.table(ctx)?.support(vars, ctx)
389 }
390
391 fn outcome(
392 &self,
393 var: VariableId,
394 assignment: &Assignment,
395 ctx: &EvalContext,
396 ) -> Result<f64, EvalError> {
397 self.table(ctx)?.outcome(var, assignment, ctx)
398 }
399
400 fn n_draws(&self) -> Option<usize> {
401 Some(self.draws.len())
402 }
403}
404
405#[derive(Clone, Debug, Default)]
411pub struct GaussianDensityProvider {
412 params: HashMap<VariableId, (f64, f64)>,
414}
415
416impl GaussianDensityProvider {
417 #[must_use]
419 pub fn new() -> Self {
420 Self::default()
421 }
422
423 pub fn set_gaussian(&mut self, var: VariableId, mean: f64, variance: f64) {
430 if variance > 0.0 && variance.is_finite() && mean.is_finite() {
431 self.params.insert(var, (mean, variance));
432 }
433 }
434}
435
436const GH5_NODES: [f64; 5] = [
438 -2.020_182_870_456_085_6,
439 -0.958_572_464_613_818_5,
440 0.0,
441 0.958_572_464_613_818_5,
442 2.020_182_870_456_085_6,
443];
444const GH5_WEIGHTS: [f64; 5] = [
445 0.019_953_242_059_045_913,
446 0.393_619_323_152_241_35,
447 0.945_308_720_482_941_9,
448 0.393_619_323_152_241_35,
449 0.019_953_242_059_045_913,
450];
451
452impl DistributionProvider for GaussianDensityProvider {
453 fn probability(
454 &self,
455 spec: &FactorSpec<'_>,
456 assignment: &Assignment,
457 _ctx: &EvalContext,
458 ) -> Result<f64, EvalError> {
459 if !spec.conditioned_on.is_empty() {
463 return Err(EvalError::UnsupportedConditioning(
464 "GaussianDensityProvider models independent Gaussians and cannot answer \
465 conditional queries; conditioned_on must be empty",
466 ));
467 }
468 let mut dens = 1.0;
469 for &v in spec.variables {
470 let (mean, var) = self.params.get(&v).copied().ok_or(EvalError::EmptySupport(v))?;
471 let x =
472 assignment.get(v).and_then(Value::as_f64).ok_or(EvalError::MissingBinding(v))?;
473 let inv_sqrt = (2.0 * std::f64::consts::PI * var).sqrt().recip();
474 let z = (x - mean) / var.sqrt();
475 dens *= inv_sqrt * (-0.5 * z * z).exp();
476 }
477 Ok(dens)
478 }
479
480 fn support(
481 &self,
482 vars: &[VariableId],
483 _ctx: &EvalContext,
484 ) -> Result<Arc<[Arc<[Value]>]>, EvalError> {
485 if vars.is_empty() {
486 return Ok(Arc::from(vec![Arc::from(Vec::<Value>::new())]));
487 }
488 Err(EvalError::EmptySupport(vars[0]))
489 }
490
491 fn quadrature(
492 &self,
493 vars: &[VariableId],
494 _ctx: &EvalContext,
495 ) -> Result<Option<QuadratureNodes>, EvalError> {
496 if vars.is_empty() {
497 return Ok(Some(Arc::from([(Arc::from(Vec::<Value>::new()), 1.0)])));
498 }
499 let mut nodes: Vec<(Vec<Value>, f64)> = vec![(Vec::new(), 1.0)];
501 for &v in vars {
502 let (mean, variance) =
503 self.params.get(&v).copied().ok_or(EvalError::EmptySupport(v))?;
504 let sigma = variance.sqrt();
505 let scale = sigma * std::f64::consts::SQRT_2;
506 let mut next = Vec::with_capacity(nodes.len() * GH5_NODES.len());
507 for (prefix, w0) in &nodes {
508 for (i, &t) in GH5_NODES.iter().enumerate() {
509 let x = mean + scale * t;
510 let w = w0 * GH5_WEIGHTS[i] * scale * (t * t).exp();
514 let mut row = prefix.clone();
515 row.push(Value::f64(x));
516 next.push((row, w));
517 }
518 }
519 nodes = next;
520 }
521 let out: Vec<(Arc<[Value]>, f64)> =
522 nodes.into_iter().map(|(row, w)| (Arc::from(row), w)).collect();
523 Ok(Some(Arc::from(out)))
524 }
525
526 fn outcome(
527 &self,
528 var: VariableId,
529 assignment: &Assignment,
530 _ctx: &EvalContext,
531 ) -> Result<f64, EvalError> {
532 let value = assignment.get(var).ok_or(EvalError::MissingBinding(var))?;
533 value.as_f64().ok_or(EvalError::MissingBinding(var))
534 }
535
536 fn n_draws(&self) -> Option<usize> {
537 None
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 fn v(id: u32) -> VariableId {
546 VariableId::from_raw(id)
547 }
548
549 fn f(x: f64) -> Value {
550 Value::f64(x)
551 }
552
553 #[test]
554 fn empirical_table_missing_entry_errors() {
555 let mut p = EmpiricalTableProvider::new();
558 let y = v(0);
559 p.set_domain(y, [f(0.0), f(1.0)]);
560 let spec = FactorSpec {
561 variables: &[y],
562 conditioned_on: &[],
563 intervention: &[],
564 domain: DomainRef::Observational,
565 };
566 let assignment = Assignment::from_pairs([(y, f(0.0))]);
567 let err = p.probability(&spec, &assignment, &EvalContext::default()).unwrap_err();
568 assert_eq!(err, EvalError::MissingTableEntry);
569 }
570
571 #[test]
572 fn gaussian_provider_rejects_conditional_query() {
573 let mut p = GaussianDensityProvider::new();
577 let y = v(0);
578 let z = v(1);
579 p.set_gaussian(y, 0.0, 1.0);
580 p.set_gaussian(z, 0.0, 1.0);
581 let spec = FactorSpec {
582 variables: &[y],
583 conditioned_on: &[z],
584 intervention: &[],
585 domain: DomainRef::Observational,
586 };
587 let assignment = Assignment::from_pairs([(y, f(0.5)), (z, f(0.2))]);
588 let err = p.probability(&spec, &assignment, &EvalContext::default()).unwrap_err();
589 assert!(matches!(err, EvalError::UnsupportedConditioning(_)));
590 }
591}