use std::collections::{BTreeMap, HashMap};
use crate::error::{Error, Result};
use crate::model::domain::Domain;
use crate::model::evidence::Evidence;
use crate::rng::Rng;
use super::types::{Region, Value};
use super::{Marginals, Query};
pub const SUPPORT_EPS: f64 = 1e-3;
impl Query<'_> {
pub fn marginal(
&self,
entity: &str,
slot: &str,
condition: &BTreeMap<String, usize>,
extra: &[Evidence],
) -> Result<Vec<f64>> {
self.db.domain(slot)?;
if !self.db.adjacency.contains_key(slot) {
return self.unary(entity, slot, condition.get(slot).copied(), extra);
}
Ok(self.marginals(entity, condition, extra)?[slot].clone())
}
pub(super) fn marginals(
&self,
entity: &str,
condition: &BTreeMap<String, usize>,
extra: &[Evidence],
) -> Result<Marginals> {
let cacheable = condition.is_empty() && extra.is_empty();
if cacheable {
if let Some(m) = self.memo.borrow().get(entity) {
return Ok(m.clone());
}
}
let mut unary: Marginals = BTreeMap::new();
for slot in self.db.schema.slots.keys() {
unary.insert(
slot.clone(),
self.unary(entity, slot, condition.get(slot).copied(), extra)?,
);
}
let beliefs = if self.db.schema.couplings.is_empty() {
unary
} else {
self.belief_propagation(entity, unary)?
};
if cacheable {
self.memo
.borrow_mut()
.insert(entity.to_string(), beliefs.clone());
}
Ok(beliefs)
}
fn unary(
&self,
entity: &str,
slot: &str,
cond: Option<usize>,
extra: &[Evidence],
) -> Result<Vec<f64>> {
let domain = self.db.domain(slot)?;
let n = domain.n();
if let Some(c) = cond {
let mut v = vec![0.0; n];
v[c] = 1.0;
return Ok(v);
}
let mut logp: Vec<f64> = match self.db.prior_for(entity, slot) {
Some(w) => w
.iter()
.map(|p| if *p > 0.0 { p.ln() } else { f64::NEG_INFINITY })
.collect(),
None => vec![0.0; n],
};
let indices = self.db.evidence_for(entity, slot);
let stored = indices.iter().map(|&i| &self.db.evidence[i]);
let extras = extra
.iter()
.filter(|e| e.entity == entity && e.claim.slot() == slot);
for ev in stored.chain(extras) {
if ev.observed_at > self.as_of {
continue; }
let r = ev.reliability_at(self.as_of);
if r <= 0.0 {
continue;
}
for (lp, lik) in logp.iter_mut().zip(ev.claim.likelihood(domain, r)) {
*lp += if lik > 0.0 {
lik.ln()
} else {
f64::NEG_INFINITY
};
}
}
let m = logp.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
if m == f64::NEG_INFINITY {
return Err(Error::AxiomConflict(format!(
"{entity}.{slot}: axiomatic evidence conflicts"
)));
}
normalized(logp.iter().map(|v| (v - m).exp()).collect(), entity, slot)
}
fn belief_propagation(&self, entity: &str, unary: Marginals) -> Result<Marginals> {
let couplings = &self.db.schema.couplings;
let mut edges: Vec<(&str, &str, usize, bool)> = Vec::new();
for (ci, c) in couplings.iter().enumerate() {
edges.push((&c.slot_a, &c.slot_b, ci, true));
edges.push((&c.slot_b, &c.slot_a, ci, false));
}
let mut msgs: HashMap<(&str, &str), Vec<f64>> = HashMap::new();
for &(src, dst, _, _) in &edges {
let n = self.db.schema.slots[dst].n();
msgs.insert((src, dst), vec![1.0 / n as f64; n]);
}
let damping = if self.db.loopy { 0.5 } else { 0.0 };
let max_iters = if self.db.loopy {
100
} else {
2 * couplings.len() + 2
};
for _ in 0..max_iters {
let mut delta: f64 = 0.0;
for &(src, dst, ci, src_is_a) in &edges {
let mut pre = unary[src].clone();
if let Some(nbrs) = self.db.adjacency.get(src) {
for (nbr, _) in nbrs {
if nbr == dst {
continue;
}
for (p, q) in pre.iter_mut().zip(&msgs[&(nbr.as_str(), src)]) {
*p *= q;
}
}
}
let table = &self.db.tables[ci];
let nd = self.db.schema.slots[dst].n();
let mut new = vec![0.0; nd];
if src_is_a {
for (i, p) in pre.iter().enumerate() {
if *p > 0.0 {
for (nj, tij) in new.iter_mut().zip(&table[i]) {
*nj += p * tij;
}
}
}
} else {
for (i, nj) in new.iter_mut().enumerate() {
*nj = table[i].iter().zip(&pre).map(|(t, p)| t * p).sum();
}
}
let total: f64 = new.iter().sum();
if total <= 0.0 {
return Err(Error::AxiomConflict(format!(
"{entity}: coupling {} is incompatible with the evidence on {src}",
couplings[ci].label()
)));
}
for v in &mut new {
*v /= total;
}
let old = msgs.get_mut(&(src, dst)).unwrap();
if damping > 0.0 {
for (n_, o) in new.iter_mut().zip(old.iter()) {
*n_ = damping * o + (1.0 - damping) * *n_;
}
}
for (o, n_) in old.iter().zip(&new) {
delta = delta.max((o - n_).abs());
}
*old = new;
}
if delta < 1e-10 {
break;
}
}
let mut beliefs = Marginals::new();
for (slot, u) in &unary {
let mut b = u.clone();
if let Some(nbrs) = self.db.adjacency.get(slot) {
for (nbr, _) in nbrs {
for (p, q) in b.iter_mut().zip(&msgs[&(nbr.as_str(), slot.as_str())]) {
*p *= q;
}
}
}
beliefs.insert(slot.clone(), normalized(b, entity, slot)?);
}
Ok(beliefs)
}
}
pub fn entropy_bits(post: &[f64]) -> f64 {
let h: f64 = -post
.iter()
.filter(|p| **p > 0.0)
.map(|p| p * p.log2())
.sum::<f64>();
h.max(0.0) }
fn normalized(weights: Vec<f64>, entity: &str, slot: &str) -> Result<Vec<f64>> {
let total: f64 = weights.iter().sum();
if !total.is_finite() || total <= 0.0 {
return Err(Error::AxiomConflict(format!(
"{entity}.{slot}: all mass annihilated"
)));
}
Ok(weights.into_iter().map(|w| w / total).collect())
}
pub(super) fn argmax(post: &[f64]) -> usize {
let mut best = 0;
for (i, p) in post.iter().enumerate() {
if *p > post[best] {
best = i;
}
}
best
}
pub(super) fn cell_value(domain: &Domain, i: usize) -> Value {
match domain {
Domain::Continuous { .. } => Value::Num(domain.midpoint(i)),
Domain::Categorical { values } => Value::Cat(values[i].clone()),
}
}
pub(super) fn sample_index(post: &[f64], rng: &mut Rng) -> usize {
let x = rng.next_f64();
let mut acc = 0.0;
for (i, p) in post.iter().enumerate() {
acc += p;
if x <= acc {
return i;
}
}
post.len() - 1
}
pub fn support_indices(post: &[f64]) -> Vec<usize> {
let mut order: Vec<usize> = (0..post.len()).collect();
order.sort_by(|a, b| post[*b].total_cmp(&post[*a]));
let mut keep = Vec::new();
let mut acc = 0.0;
for i in order {
keep.push(i);
acc += post[i];
if acc >= 1.0 - SUPPORT_EPS {
break;
}
}
keep.sort_unstable();
keep
}
pub(super) fn credible_region(domain: &Domain, post: &[f64], level: f64) -> Region {
let mut order: Vec<usize> = (0..post.len()).collect();
order.sort_by(|a, b| post[*b].total_cmp(&post[*a]));
let mut keep = Vec::new();
let mut acc = 0.0;
for i in order {
keep.push(i);
acc += post[i];
if acc >= level {
break;
}
}
keep.sort_unstable();
match domain {
Domain::Categorical { values } => {
Region::Values(keep.into_iter().map(|i| values[i].clone()).collect())
}
Domain::Continuous { .. } => {
let mut intervals: Vec<(f64, f64)> = Vec::new();
let w = domain.bin_width();
for i in keep {
let (a, b) = domain.cell_bounds(i);
match intervals.last_mut() {
Some(last) if (last.1 - a).abs() < w * 1e-9 => last.1 = b,
_ => intervals.push((a, b)),
}
}
Region::Intervals(intervals)
}
}
}