use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::state::NodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VariableId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FactorId(pub u64);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Distribution {
Beta { alpha: f64, beta: f64 },
Gaussian { mean: f64, variance: f64 },
Categorical { probs: Vec<f64> },
}
impl Distribution {
pub fn uniform_beta() -> Self {
Self::Beta {
alpha: 1.0,
beta: 1.0,
}
}
pub fn from_log_odds(log_odds: f64) -> Self {
let p = 1.0 / (1.0 + (-log_odds).exp());
let n = 2.0;
Self::Beta {
alpha: p * n,
beta: (1.0 - p) * n,
}
}
pub fn beta(alpha: f64, beta: f64) -> Self {
Self::Beta {
alpha: alpha.max(0.01),
beta: beta.max(0.01),
}
}
pub fn gaussian(mean: f64, variance: f64) -> Self {
Self::Gaussian {
mean,
variance: variance.max(1e-10),
}
}
pub fn categorical(probs: Vec<f64>) -> Self {
let sum: f64 = probs.iter().sum();
if sum <= 0.0 {
let n = probs.len().max(1);
return Self::Categorical {
probs: vec![1.0 / n as f64; n],
};
}
Self::Categorical {
probs: probs.iter().map(|p| p / sum).collect(),
}
}
pub fn mean(&self) -> f64 {
match self {
Self::Beta { alpha, beta } => alpha / (alpha + beta),
Self::Gaussian { mean, .. } => *mean,
Self::Categorical { probs } => {
probs.iter().enumerate().map(|(i, p)| i as f64 * p).sum()
}
}
}
pub fn variance(&self) -> f64 {
match self {
Self::Beta { alpha, beta } => {
let sum = alpha + beta;
(alpha * beta) / (sum * sum * (sum + 1.0))
}
Self::Gaussian { variance, .. } => *variance,
Self::Categorical { probs } => {
let mu = self.mean();
probs
.iter()
.enumerate()
.map(|(i, p)| {
let diff = i as f64 - mu;
diff * diff * p
})
.sum()
}
}
}
pub fn entropy(&self) -> f64 {
match self {
Self::Beta { alpha, beta } => {
let sum = alpha + beta;
ln_beta(*alpha, *beta)
- (alpha - 1.0) * digamma(*alpha)
- (beta - 1.0) * digamma(*beta)
+ (sum - 2.0) * digamma(sum)
}
Self::Gaussian { variance, .. } => {
0.5 * (2.0 * std::f64::consts::PI * std::f64::consts::E * variance).ln()
}
Self::Categorical { probs } => {
let mut h = 0.0;
for &p in probs {
if p > 0.0 {
h -= p * p.ln();
}
}
h
}
}
}
pub fn kl_divergence(&self, other: &Distribution) -> f64 {
match (self, other) {
(
Self::Beta {
alpha: a1,
beta: b1,
},
Self::Beta {
alpha: a2,
beta: b2,
},
) => {
let s1 = a1 + b1;
let s2 = a2 + b2;
ln_beta(*a2, *b2) - ln_beta(*a1, *b1)
+ (a1 - a2) * digamma(*a1)
+ (b1 - b2) * digamma(*b1)
+ (s2 - s1) * digamma(s1)
}
(
Self::Gaussian {
mean: m1,
variance: v1,
},
Self::Gaussian {
mean: m2,
variance: v2,
},
) => 0.5 * ((v1 / v2).ln() + (v2 + (m1 - m2).powi(2)) / v1 - 1.0),
(Self::Categorical { probs: p1 }, Self::Categorical { probs: p2 }) => {
if p1.len() != p2.len() {
return f64::INFINITY;
}
let mut kl = 0.0;
for (a, b) in p1.iter().zip(p2.iter()) {
if *a > 0.0 && *b > 0.0 {
kl += a * (a / b).ln();
} else if *a > 0.0 {
return f64::INFINITY;
}
}
kl
}
_ => f64::INFINITY, }
}
pub fn confidence(&self) -> f64 {
match self {
Self::Beta { alpha, beta } => {
let total = alpha + beta;
1.0 - 2.0 / total.max(2.0)
}
Self::Gaussian { variance, .. } => {
1.0 / (1.0 + variance.sqrt())
}
Self::Categorical { probs } => {
let n = probs.len().max(1) as f64;
let max_p = probs.iter().cloned().fold(0.0f64, f64::max);
(max_p - 1.0 / n) / (1.0 - 1.0 / n)
}
}
}
}
fn ln_beta(a: f64, b: f64) -> f64 {
ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
}
fn ln_gamma(x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let g = 7.0;
let c = [
0.99999999999980993,
676.5203681218851,
-1259.1392167224028,
771.32342877765313,
-176.61502916214059,
12.507343278686905,
-0.13857109526572012,
9.9843695780195716e-6,
1.5056327351493116e-7,
];
let z = x - 1.0;
let mut sum = c[0];
for i in 1..9 {
sum += c[i] / (z + i as f64);
}
let t = z + g + 0.5;
0.5 * (2.0 * std::f64::consts::PI).ln() + (t.ln() * (z + 0.5)) - t + sum.ln()
}
fn digamma(mut x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let mut result = 0.0;
while x < 6.0 {
result -= 1.0 / x;
x += 1.0;
}
result += x.ln() - 0.5 / x;
let x2 = x * x;
result -= 1.0 / (12.0 * x2);
result += 1.0 / (120.0 * x2 * x2);
result -= 1.0 / (252.0 * x2 * x2 * x2);
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FactorType {
Supports,
Contradicts,
Causes,
Correlates,
Implies,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PotentialFunction {
Agreement { correlation: f64 },
Opposition { anti_correlation: f64 },
Conditional { table: [[f64; 2]; 2] },
NoisyOr { leak: f64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeliefVariable {
pub id: VariableId,
pub node_id: NodeId,
pub label: String,
pub prior: Distribution,
pub posterior: Distribution,
pub observed: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Factor {
pub id: FactorId,
pub variables: Vec<VariableId>,
pub factor_type: FactorType,
pub strength: f64,
pub potential: PotentialFunction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeliefNetwork {
pub variables: Vec<BeliefVariable>,
pub factors: Vec<Factor>,
#[serde(skip)]
var_to_factors: HashMap<VariableId, Vec<usize>>,
#[serde(skip)]
factor_to_vars: HashMap<FactorId, Vec<VariableId>>,
next_var_id: u64,
next_factor_id: u64,
}
impl BeliefNetwork {
pub fn new() -> Self {
Self {
variables: Vec::new(),
factors: Vec::new(),
var_to_factors: HashMap::new(),
factor_to_vars: HashMap::new(),
next_var_id: 1,
next_factor_id: 1,
}
}
pub fn add_variable(
&mut self,
node_id: NodeId,
label: &str,
prior: Distribution,
) -> VariableId {
let id = VariableId(self.next_var_id);
self.next_var_id += 1;
self.variables.push(BeliefVariable {
id,
node_id,
label: label.to_string(),
prior: prior.clone(),
posterior: prior,
observed: None,
});
id
}
pub fn add_factor(
&mut self,
variables: Vec<VariableId>,
factor_type: FactorType,
strength: f64,
potential: PotentialFunction,
) -> FactorId {
let id = FactorId(self.next_factor_id);
self.next_factor_id += 1;
let factor_idx = self.factors.len();
for &var_id in &variables {
self.var_to_factors
.entry(var_id)
.or_default()
.push(factor_idx);
}
self.factor_to_vars.insert(id, variables.clone());
self.factors.push(Factor {
id,
variables,
factor_type,
strength,
potential,
});
id
}
pub fn rebuild_indices(&mut self) {
self.var_to_factors.clear();
self.factor_to_vars.clear();
for (idx, factor) in self.factors.iter().enumerate() {
for &var_id in &factor.variables {
self.var_to_factors.entry(var_id).or_default().push(idx);
}
self.factor_to_vars
.insert(factor.id, factor.variables.clone());
}
}
pub fn variable(&self, id: VariableId) -> Option<&BeliefVariable> {
self.variables.iter().find(|v| v.id == id)
}
fn variable_mut(&mut self, id: VariableId) -> Option<&mut BeliefVariable> {
self.variables.iter_mut().find(|v| v.id == id)
}
pub fn observe(&mut self, var_id: VariableId, value: f64) {
if let Some(var) = self.variable_mut(var_id) {
var.observed = Some(value);
var.posterior = Distribution::Beta {
alpha: if value > 0.5 { 100.0 } else { 1.0 },
beta: if value > 0.5 { 1.0 } else { 100.0 },
};
}
}
pub fn clear_observations(&mut self) {
for var in &mut self.variables {
var.observed = None;
var.posterior = var.prior.clone();
}
}
pub fn variable_count(&self) -> usize {
self.variables.len()
}
pub fn factor_count(&self) -> usize {
self.factors.len()
}
fn factors_of(&self, var_id: VariableId) -> Vec<usize> {
self.var_to_factors
.get(&var_id)
.cloned()
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BPConfig {
pub max_iterations: usize,
pub tolerance: f64,
pub damping: f64,
}
impl Default for BPConfig {
fn default() -> Self {
Self {
max_iterations: 30,
tolerance: 1e-4,
damping: 0.5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BPResult {
pub iterations: usize,
pub converged: bool,
pub max_change: f64,
}
pub fn loopy_belief_propagation(network: &mut BeliefNetwork, config: &BPConfig) -> BPResult {
let var_ids: Vec<VariableId> = network.variables.iter().map(|v| v.id).collect();
let n_vars = var_ids.len();
if n_vars == 0 {
return BPResult {
iterations: 0,
converged: true,
max_change: 0.0,
};
}
let mut messages: HashMap<(usize, VariableId), f64> = HashMap::new();
for (fi, factor) in network.factors.iter().enumerate() {
for &var_id in &factor.variables {
let prior_mean = network
.variable(var_id)
.map(|v| v.prior.mean())
.unwrap_or(0.5);
messages.insert((fi, var_id), prior_mean);
}
}
let mut iterations = 0;
let mut max_change = f64::MAX;
while iterations < config.max_iterations && max_change > config.tolerance {
max_change = 0.0;
iterations += 1;
for (fi, factor) in network.factors.iter().enumerate() {
for &target_var in &factor.variables {
let is_observed = network
.variable(target_var)
.map(|v| v.observed.is_some())
.unwrap_or(false);
if is_observed {
continue;
}
let other_means: Vec<f64> = factor
.variables
.iter()
.filter(|&&v| v != target_var)
.map(|&v| {
network
.variable(v)
.map(|var| {
if var.observed.is_some() {
var.posterior.mean()
} else {
var.posterior.mean()
}
})
.unwrap_or(0.5)
})
.collect();
let new_msg =
compute_factor_message(&factor.potential, factor.strength, &other_means);
let old_msg = messages.get(&(fi, target_var)).copied().unwrap_or(0.5);
let damped = config.damping * old_msg + (1.0 - config.damping) * new_msg;
let change = (damped - old_msg).abs();
if change > max_change {
max_change = change;
}
messages.insert((fi, target_var), damped);
}
}
for &var_id in &var_ids {
let is_observed = network
.variable(var_id)
.map(|v| v.observed.is_some())
.unwrap_or(false);
if is_observed {
continue;
}
let prior_mean = network
.variable(var_id)
.map(|v| v.prior.mean())
.unwrap_or(0.5);
let factor_indices = network.factors_of(var_id);
let mut sum = prior_mean;
let mut weight = 1.0;
for &fi in &factor_indices {
if let Some(&msg) = messages.get(&(fi, var_id)) {
sum += msg;
weight += 1.0;
}
}
let combined_mean = (sum / weight).clamp(0.001, 0.999);
let concentration = 2.0 + weight;
if let Some(var) = network.variable_mut(var_id) {
var.posterior = Distribution::Beta {
alpha: combined_mean * concentration,
beta: (1.0 - combined_mean) * concentration,
};
}
}
}
BPResult {
iterations,
converged: max_change <= config.tolerance,
max_change,
}
}
fn compute_factor_message(
potential: &PotentialFunction,
strength: f64,
other_means: &[f64],
) -> f64 {
if other_means.is_empty() {
return 0.5; }
let avg_other = other_means.iter().sum::<f64>() / other_means.len() as f64;
match potential {
PotentialFunction::Agreement { correlation } => {
let effective = correlation * strength;
0.5 + effective * (avg_other - 0.5)
}
PotentialFunction::Opposition { anti_correlation } => {
let effective = anti_correlation * strength;
0.5 - effective * (avg_other - 0.5)
}
PotentialFunction::Conditional { table } => {
let p_parent_high = avg_other;
let p_parent_low = 1.0 - avg_other;
let p_child_high = table[1][1] * p_parent_high + table[0][1] * p_parent_low;
0.5 + strength * (p_child_high - 0.5)
}
PotentialFunction::NoisyOr { leak } => {
let p_no_cause = (1.0 - strength * avg_other).max(0.0);
let p_child_off = leak * p_no_cause;
1.0 - p_child_off
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum InferenceType {
Marginal,
Conditional,
MAP,
MostProbableExplanation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceQuery {
pub target: VariableId,
pub evidence: Vec<(VariableId, f64)>,
pub query_type: InferenceType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceContribution {
pub variable: VariableId,
pub impact: f64,
pub direction: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceResult {
pub target: VariableId,
pub posterior: Distribution,
pub contributions: Vec<EvidenceContribution>,
pub iterations: usize,
pub converged: bool,
}
pub fn query(
network: &mut BeliefNetwork,
query: &InferenceQuery,
bp_config: &BPConfig,
) -> InferenceResult {
let prior_mean = network
.variable(query.target)
.map(|v| v.prior.mean())
.unwrap_or(0.5);
for var in &mut network.variables {
if var.observed.is_none() {
var.posterior = var.prior.clone();
}
}
for &(var_id, value) in &query.evidence {
network.observe(var_id, value);
}
let bp_result = loopy_belief_propagation(network, bp_config);
let posterior = network
.variable(query.target)
.map(|v| v.posterior.clone())
.unwrap_or(Distribution::uniform_beta());
let contributions = compute_evidence_contributions(
network,
query.target,
&query.evidence,
prior_mean,
bp_config,
);
for &(var_id, _) in &query.evidence {
if let Some(var) = network.variable_mut(var_id) {
var.observed = None;
}
}
InferenceResult {
target: query.target,
posterior,
contributions,
iterations: bp_result.iterations,
converged: bp_result.converged,
}
}
fn compute_evidence_contributions(
network: &mut BeliefNetwork,
target: VariableId,
evidence: &[(VariableId, f64)],
prior_mean: f64,
bp_config: &BPConfig,
) -> Vec<EvidenceContribution> {
let full_posterior_mean = network
.variable(target)
.map(|v| v.posterior.mean())
.unwrap_or(0.5);
let mut contributions = Vec::new();
for &(ev_var, ev_val) in evidence {
if let Some(var) = network.variable_mut(ev_var) {
var.observed = None;
var.posterior = var.prior.clone();
}
for var in &mut network.variables {
if var.observed.is_none() {
var.posterior = var.prior.clone();
}
}
for &(other_var, other_val) in evidence {
if other_var != ev_var {
network.observe(other_var, other_val);
}
}
loopy_belief_propagation(network, bp_config);
let without_mean = network
.variable(target)
.map(|v| v.posterior.mean())
.unwrap_or(0.5);
let impact = (full_posterior_mean - without_mean).abs();
let direction = full_posterior_mean - without_mean;
contributions.push(EvidenceContribution {
variable: ev_var,
impact,
direction,
});
network.observe(ev_var, ev_val);
}
contributions.sort_by(|a, b| b.impact.total_cmp(&a.impact));
contributions
}
pub fn information_gain(
network: &mut BeliefNetwork,
candidate: VariableId,
target: VariableId,
bp_config: &BPConfig,
) -> f64 {
reset_network(network);
loopy_belief_propagation(network, bp_config);
let baseline_entropy = network
.variable(target)
.map(|v| v.posterior.entropy())
.unwrap_or(0.0);
reset_network(network);
network.observe(candidate, 0.9);
loopy_belief_propagation(network, bp_config);
let high_entropy = network
.variable(target)
.map(|v| v.posterior.entropy())
.unwrap_or(0.0);
reset_network(network);
network.observe(candidate, 0.1);
loopy_belief_propagation(network, bp_config);
let low_entropy = network
.variable(target)
.map(|v| v.posterior.entropy())
.unwrap_or(0.0);
if let Some(var) = network.variable_mut(candidate) {
var.observed = None;
}
reset_network(network);
let avg_conditional_entropy = (high_entropy + low_entropy) / 2.0;
(baseline_entropy - avg_conditional_entropy).max(0.0)
}
pub fn sensitivity_to_evidence(
network: &mut BeliefNetwork,
target: VariableId,
bp_config: &BPConfig,
) -> Vec<(VariableId, f64)> {
let var_ids: Vec<VariableId> = network
.variables
.iter()
.filter(|v| v.id != target)
.map(|v| v.id)
.collect();
let mut results = Vec::new();
for var_id in var_ids {
let ig = information_gain(network, var_id, target, bp_config);
if ig > 1e-6 {
results.push((var_id, ig));
}
}
results.sort_by(|a, b| b.1.total_cmp(&a.1));
results
}
fn reset_network(network: &mut BeliefNetwork) {
for var in &mut network.variables {
if var.observed.is_none() {
var.posterior = var.prior.clone();
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkHealth {
pub variable_count: usize,
pub factor_count: usize,
pub components: usize,
pub extreme_priors: Vec<VariableId>,
pub potential_instabilities: Vec<VariableId>,
pub avg_confidence: f64,
pub healthy: bool,
}
pub fn network_diagnostics(network: &BeliefNetwork) -> NetworkHealth {
let n = network.variables.len();
let m = network.factors.len();
let var_ids: Vec<VariableId> = network.variables.iter().map(|v| v.id).collect();
let mut parent: HashMap<VariableId, VariableId> = HashMap::new();
for &v in &var_ids {
parent.insert(v, v);
}
fn find(parent: &mut HashMap<VariableId, VariableId>, x: VariableId) -> VariableId {
let p = *parent.get(&x).unwrap_or(&x);
if p != x {
let root = find(parent, p);
parent.insert(x, root);
root
} else {
x
}
}
for factor in &network.factors {
if factor.variables.len() >= 2 {
let root0 = find(&mut parent, factor.variables[0]);
for &v in &factor.variables[1..] {
let root_v = find(&mut parent, v);
if root0 != root_v {
parent.insert(root_v, root0);
}
}
}
}
let mut roots = std::collections::HashSet::new();
for &v in &var_ids {
roots.insert(find(&mut parent, v));
}
let components = roots.len();
let mut extreme_priors = Vec::new();
for var in &network.variables {
let conf = var.prior.confidence();
if conf > 0.95 {
extreme_priors.push(var.id);
}
}
let mut potential_instabilities = Vec::new();
for var in &network.variables {
let factor_count = network
.var_to_factors
.get(&var.id)
.map(|v| v.len())
.unwrap_or(0);
if factor_count > 5 {
potential_instabilities.push(var.id);
}
}
let avg_confidence = if n > 0 {
network
.variables
.iter()
.map(|v| v.posterior.confidence())
.sum::<f64>()
/ n as f64
} else {
0.0
};
let healthy = components <= n.max(1)
&& extreme_priors.len() <= n / 2
&& potential_instabilities.is_empty();
NetworkHealth {
variable_count: n,
factor_count: m,
components,
extreme_priors,
potential_instabilities,
avg_confidence,
healthy,
}
}
pub fn build_network_from_edges(
beliefs: &[(NodeId, &str, f64)], edges: &[(NodeId, NodeId, EdgeRelation, f64)], ) -> BeliefNetwork {
let mut network = BeliefNetwork::new();
let mut node_to_var: HashMap<NodeId, VariableId> = HashMap::new();
for &(node_id, label, log_odds) in beliefs {
let prior = Distribution::from_log_odds(log_odds);
let var_id = network.add_variable(node_id, label, prior);
node_to_var.insert(node_id, var_id);
}
for &(from, to, relation, weight) in edges {
let from_var = match node_to_var.get(&from) {
Some(&v) => v,
None => continue,
};
let to_var = match node_to_var.get(&to) {
Some(&v) => v,
None => continue,
};
let (factor_type, potential) = match relation {
EdgeRelation::Supports => (
FactorType::Supports,
PotentialFunction::Agreement {
correlation: weight,
},
),
EdgeRelation::Contradicts => (
FactorType::Contradicts,
PotentialFunction::Opposition {
anti_correlation: weight,
},
),
EdgeRelation::Causes => (
FactorType::Causes,
PotentialFunction::Conditional {
table: [
[1.0 - weight * 0.2, weight * 0.2], [1.0 - weight * 0.8, weight * 0.8], ],
},
),
EdgeRelation::Predicts => (
FactorType::Implies,
PotentialFunction::Agreement {
correlation: weight * 0.8, },
),
EdgeRelation::Correlates => (
FactorType::Correlates,
PotentialFunction::Agreement {
correlation: weight * 0.5,
},
),
};
network.add_factor(vec![from_var, to_var], factor_type, weight, potential);
}
network
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EdgeRelation {
Supports,
Contradicts,
Causes,
Predicts,
Correlates,
}
pub fn most_probable_explanation(
network: &mut BeliefNetwork,
evidence: &[(VariableId, f64)],
bp_config: &BPConfig,
) -> Vec<(VariableId, f64)> {
for &(var_id, value) in evidence {
network.observe(var_id, value);
}
loopy_belief_propagation(network, bp_config);
let mut assignment = Vec::new();
for var in &network.variables {
let value = var.posterior.mean(); assignment.push((var.id, value));
}
for &(var_id, _) in evidence {
if let Some(var) = network.variable_mut(var_id) {
var.observed = None;
}
}
reset_network(network);
assignment
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::{NodeId, NodeKind};
fn belief_node(seq: u32) -> NodeId {
NodeId::new(NodeKind::Belief, seq)
}
fn make_simple_network() -> BeliefNetwork {
let mut net = BeliefNetwork::new();
let a = net.add_variable(belief_node(1), "A", Distribution::beta(3.0, 2.0));
let b = net.add_variable(belief_node(2), "B", Distribution::uniform_beta());
let c = net.add_variable(belief_node(3), "C", Distribution::uniform_beta());
net.add_factor(
vec![a, b],
FactorType::Supports,
0.8,
PotentialFunction::Agreement { correlation: 0.8 },
);
net.add_factor(
vec![b, c],
FactorType::Contradicts,
0.7,
PotentialFunction::Opposition {
anti_correlation: 0.7,
},
);
net
}
fn make_chain_network() -> BeliefNetwork {
let mut net = BeliefNetwork::new();
let a = net.add_variable(belief_node(1), "A", Distribution::beta(8.0, 2.0)); let b = net.add_variable(belief_node(2), "B", Distribution::uniform_beta());
let c = net.add_variable(belief_node(3), "C", Distribution::uniform_beta());
net.add_factor(
vec![a, b],
FactorType::Causes,
0.9,
PotentialFunction::Conditional {
table: [[0.9, 0.1], [0.2, 0.8]],
},
);
net.add_factor(
vec![b, c],
FactorType::Causes,
0.7,
PotentialFunction::Conditional {
table: [[0.8, 0.2], [0.3, 0.7]],
},
);
net
}
#[test]
fn test_beta_distribution_basics() {
let d = Distribution::Beta {
alpha: 8.0,
beta: 2.0,
};
assert!((d.mean() - 0.8).abs() < 0.01);
assert!(d.variance() > 0.0);
assert!(d.entropy().is_finite());
assert!(d.confidence() > 0.5);
}
#[test]
fn test_gaussian_distribution() {
let d = Distribution::Gaussian {
mean: 5.0,
variance: 2.0,
};
assert_eq!(d.mean(), 5.0);
assert_eq!(d.variance(), 2.0);
assert!(d.entropy() > 0.0);
}
#[test]
fn test_categorical_distribution() {
let d = Distribution::categorical(vec![1.0, 2.0, 3.0]);
assert!((d.mean() - (0.0 / 6.0 + 2.0 / 6.0 + 6.0 / 6.0)).abs() < 0.2);
assert!(d.entropy() > 0.0);
}
#[test]
fn test_from_log_odds() {
let d = Distribution::from_log_odds(0.0);
assert!((d.mean() - 0.5).abs() < 0.01);
let d = Distribution::from_log_odds(2.0);
assert!(d.mean() > 0.7);
}
#[test]
fn test_kl_divergence_same() {
let d = Distribution::beta(3.0, 2.0);
assert!(d.kl_divergence(&d).abs() < 1e-10);
}
#[test]
fn test_kl_divergence_different() {
let d1 = Distribution::beta(8.0, 2.0);
let d2 = Distribution::beta(2.0, 8.0);
assert!(d1.kl_divergence(&d2) > 0.5);
}
#[test]
fn test_network_construction() {
let net = make_simple_network();
assert_eq!(net.variable_count(), 3);
assert_eq!(net.factor_count(), 2);
}
#[test]
fn test_build_network_from_edges() {
let beliefs = vec![
(belief_node(1), "Rain", 0.5),
(belief_node(2), "Wet ground", -0.5),
(belief_node(3), "Sprinkler on", 0.0),
];
let edges = vec![
(belief_node(1), belief_node(2), EdgeRelation::Causes, 0.9),
(belief_node(3), belief_node(2), EdgeRelation::Causes, 0.7),
(
belief_node(1),
belief_node(3),
EdgeRelation::Contradicts,
0.3,
),
];
let net = build_network_from_edges(&beliefs, &edges);
assert_eq!(net.variable_count(), 3);
assert_eq!(net.factor_count(), 3);
}
#[test]
fn test_bp_converges_simple() {
let mut net = make_simple_network();
let config = BPConfig::default();
let result = loopy_belief_propagation(&mut net, &config);
assert!(result.converged);
assert!(result.iterations <= config.max_iterations);
}
#[test]
fn test_bp_support_propagation() {
let mut net = make_simple_network();
let config = BPConfig::default();
let b_prior_mean = net.variable(VariableId(2)).unwrap().prior.mean();
loopy_belief_propagation(&mut net, &config);
let b_posterior_mean = net.variable(VariableId(2)).unwrap().posterior.mean();
assert!(b_posterior_mean > b_prior_mean);
}
#[test]
fn test_bp_contradiction_propagation() {
let mut net = make_simple_network();
let config = BPConfig::default();
loopy_belief_propagation(&mut net, &config);
let c_mean = net.variable(VariableId(3)).unwrap().posterior.mean();
assert!(c_mean < 0.5);
}
#[test]
fn test_bp_with_evidence() {
let mut net = make_simple_network();
let config = BPConfig::default();
net.observe(VariableId(1), 0.9);
loopy_belief_propagation(&mut net, &config);
let b_mean = net.variable(VariableId(2)).unwrap().posterior.mean();
let c_mean = net.variable(VariableId(3)).unwrap().posterior.mean();
assert!(b_mean > 0.5);
assert!(c_mean < 0.5);
}
#[test]
fn test_bp_causal_chain() {
let mut net = make_chain_network();
let config = BPConfig::default();
loopy_belief_propagation(&mut net, &config);
let a_mean = net.variable(VariableId(1)).unwrap().posterior.mean();
let b_mean = net.variable(VariableId(2)).unwrap().posterior.mean();
let c_mean = net.variable(VariableId(3)).unwrap().posterior.mean();
assert!(a_mean > 0.6);
assert!(b_mean > 0.45); }
#[test]
fn test_conditional_query() {
let mut net = make_simple_network();
let bp_config = BPConfig::default();
let q = InferenceQuery {
target: VariableId(2), evidence: vec![(VariableId(1), 0.95)], query_type: InferenceType::Conditional,
};
let result = query(&mut net, &q, &bp_config);
assert!(result.posterior.mean() > 0.5);
assert!(result.converged);
}
#[test]
fn test_evidence_contributions() {
let mut net = BeliefNetwork::new();
let a = net.add_variable(belief_node(1), "A", Distribution::uniform_beta());
let b = net.add_variable(belief_node(2), "B", Distribution::uniform_beta());
let target = net.add_variable(belief_node(3), "Target", Distribution::uniform_beta());
net.add_factor(
vec![a, target],
FactorType::Supports,
0.9,
PotentialFunction::Agreement { correlation: 0.9 },
);
net.add_factor(
vec![b, target],
FactorType::Supports,
0.3,
PotentialFunction::Agreement { correlation: 0.3 },
);
let q = InferenceQuery {
target,
evidence: vec![(a, 0.9), (b, 0.9)],
query_type: InferenceType::Conditional,
};
let result = query(&mut net, &q, &BPConfig::default());
if result.contributions.len() >= 2 {
assert!(result.contributions[0].impact >= result.contributions[1].impact);
}
}
#[test]
fn test_information_gain() {
let mut net = make_simple_network();
let bp_config = BPConfig::default();
let ig = information_gain(
&mut net,
VariableId(1), VariableId(2), &bp_config,
);
assert!(ig >= 0.0);
}
#[test]
fn test_sensitivity_ranking() {
let mut net = make_simple_network();
let bp_config = BPConfig::default();
let sensitivities = sensitivity_to_evidence(
&mut net,
VariableId(2), &bp_config,
);
assert!(!sensitivities.is_empty());
}
#[test]
fn test_mpe() {
let mut net = make_simple_network();
let bp_config = BPConfig::default();
let assignment = most_probable_explanation(
&mut net,
&[(VariableId(1), 0.9)], &bp_config,
);
assert_eq!(assignment.len(), 3);
let a_val = assignment
.iter()
.find(|(id, _)| *id == VariableId(1))
.unwrap()
.1;
assert!(a_val > 0.7);
}
#[test]
fn test_diagnostics_healthy() {
let net = make_simple_network();
let health = network_diagnostics(&net);
assert_eq!(health.variable_count, 3);
assert_eq!(health.factor_count, 2);
assert_eq!(health.components, 1); assert!(health.healthy);
}
#[test]
fn test_diagnostics_disconnected() {
let mut net = BeliefNetwork::new();
net.add_variable(belief_node(1), "A", Distribution::uniform_beta());
net.add_variable(belief_node(2), "B", Distribution::uniform_beta());
let health = network_diagnostics(&net);
assert_eq!(health.components, 2); }
#[test]
fn test_empty_network() {
let mut net = BeliefNetwork::new();
let config = BPConfig::default();
let result = loopy_belief_propagation(&mut net, &config);
assert!(result.converged);
assert_eq!(result.iterations, 0);
}
#[test]
fn test_single_variable_no_factors() {
let mut net = BeliefNetwork::new();
net.add_variable(belief_node(1), "alone", Distribution::beta(5.0, 2.0));
let config = BPConfig::default();
loopy_belief_propagation(&mut net, &config);
let mean = net.variable(VariableId(1)).unwrap().posterior.mean();
assert!((mean - 5.0 / 7.0).abs() < 0.1);
}
#[test]
fn test_noisy_or_potential() {
let mut net = BeliefNetwork::new();
let parent1 = net.add_variable(belief_node(1), "P1", Distribution::beta(8.0, 2.0));
let parent2 = net.add_variable(belief_node(2), "P2", Distribution::beta(7.0, 3.0));
let child = net.add_variable(belief_node(3), "Child", Distribution::uniform_beta());
net.add_factor(
vec![parent1, child],
FactorType::Causes,
0.8,
PotentialFunction::NoisyOr { leak: 0.1 },
);
net.add_factor(
vec![parent2, child],
FactorType::Causes,
0.6,
PotentialFunction::NoisyOr { leak: 0.1 },
);
let config = BPConfig::default();
loopy_belief_propagation(&mut net, &config);
let child_mean = net.variable(child).unwrap().posterior.mean();
assert!(child_mean > 0.5);
}
}