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)]
104pub enum EvalError {
105 UnsupportedIntegralOut,
107 MissingTableEntry,
109 MissingBinding(VariableId),
111 EmptySupport(VariableId),
113 DivisionByZero,
115 DrawOutOfRange {
117 draw: usize,
119 n_draws: usize,
121 },
122 SupportShape {
124 expected: usize,
126 actual: usize,
128 },
129 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
156pub trait DistributionProvider {
158 fn probability(
164 &self,
165 spec: &FactorSpec<'_>,
166 assignment: &Assignment,
167 ctx: &EvalContext,
168 ) -> Result<f64, EvalError>;
169
170 fn support(
176 &self,
177 vars: &[VariableId],
178 ctx: &EvalContext,
179 ) -> Result<Arc<[Arc<[Value]>]>, EvalError>;
180
181 fn quadrature(
190 &self,
191 _vars: &[VariableId],
192 _ctx: &EvalContext,
193 ) -> Result<Option<QuadratureNodes>, EvalError> {
194 Ok(None)
195 }
196
197 fn outcome(
203 &self,
204 var: VariableId,
205 assignment: &Assignment,
206 ctx: &EvalContext,
207 ) -> Result<f64, EvalError>;
208
209 fn n_draws(&self) -> Option<usize>;
211}
212
213#[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 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#[derive(Clone, Debug, Default)]
238pub struct EmpiricalTableProvider {
239 domains: HashMap<VariableId, Arc<[Value]>>,
240 tables: HashMap<FactorKey, f64>,
241}
242
243impl EmpiricalTableProvider {
244 #[must_use]
246 pub fn new() -> Self {
247 Self::default()
248 }
249
250 pub fn set_domain(&mut self, var: VariableId, values: impl IntoIterator<Item = Value>) {
252 let mut v: Vec<Value> = values.into_iter().collect();
253 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 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#[derive(Clone, Debug, Default)]
332pub struct PosteriorDrawProvider {
333 draws: Vec<EmpiricalTableProvider>,
334}
335
336impl PosteriorDrawProvider {
337 #[must_use]
339 pub fn new() -> Self {
340 Self::default()
341 }
342
343 #[must_use]
345 pub fn from_draws(draws: Vec<EmpiricalTableProvider>) -> Self {
346 Self { draws }
347 }
348
349 #[must_use]
351 pub fn len(&self) -> usize {
352 self.draws.len()
353 }
354
355 #[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#[derive(Clone, Debug, Default)]
407pub struct GaussianDensityProvider {
408 params: HashMap<VariableId, (f64, f64)>,
410}
411
412impl GaussianDensityProvider {
413 #[must_use]
415 pub fn new() -> Self {
416 Self::default()
417 }
418
419 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
432const 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 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 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}