//! Probabilistic Logic Networks (PLN) deduction engine. //! //! Implements core PLN inference rules for truth-value propagation: //! deduction, revision, negation, conjunction, and evidence-based scoring. /// A PLN truth value combining strength (probability estimate) and confidence /// (weight of evidence behind that estimate). Both fields are clamped to \[0.0, 1.0\]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct TruthValue { /// Probability estimate, 0.0–1.0. pub strength: f32, /// Weight of evidence, 0.0–1.0. pub confidence: f32, } impl TruthValue { /// Create a new `TruthValue`, clamping both fields to \[0.0, 1.0\]. pub fn new(strength: f32, confidence: f32) -> Self { Self { strength: strength.clamp(0.0, 1.0), confidence: confidence.clamp(0.0, 1.0), } } } /// Clamp a `TruthValue` so both fields lie within \[0.0, 1.0\]. #[inline] fn clamped(tv: TruthValue) -> TruthValue { TruthValue::new(tv.strength, tv.confidence) } // --------------------------------------------------------------------------- // PLN Rules // --------------------------------------------------------------------------- /// **Deduction rule** — chain two implications A→B and B→C. /// /// ```text /// strength = s1 * s2 /// confidence = c1 * c2 * (s2 + (1 - s2) * (1 - c1)) /// ``` pub fn deduction(a_to_b: TruthValue, b_to_c: TruthValue) -> TruthValue { let s1 = a_to_b.strength; let c1 = a_to_b.confidence; let s2 = b_to_c.strength; let c2 = b_to_c.confidence; let strength = s1 * s2; let confidence = c1 * c2 * (s2 + (1.0 - s2) * (1.0 - c1)); clamped(TruthValue { strength, confidence }) } /// **Revision rule** — merge two independent estimates into one. /// /// ```text /// strength = (s1*c1 + s2*c2) / (c1 + c2) /// confidence = (c1 + c2) / (c1 + c2 + 1) /// ``` /// /// If both confidences are zero the input `tv1` is returned unchanged /// (no evidence to merge). pub fn revision(tv1: TruthValue, tv2: TruthValue) -> TruthValue { let s1 = tv1.strength; let c1 = tv1.confidence; let s2 = tv2.strength; let c2 = tv2.confidence; let denom = c1 + c2; if denom == 0.0 { return tv1; } let strength = (s1 * c1 + s2 * c2) / denom; let confidence = denom / (denom + 1.0); clamped(TruthValue { strength, confidence }) } /// **Negation rule** — logical NOT. /// /// ```text /// strength = 1.0 - s /// confidence = c (unchanged) /// ``` pub fn negation(tv: TruthValue) -> TruthValue { clamped(TruthValue { strength: 1.0 - tv.strength, confidence: tv.confidence, }) } /// **Conjunction rule** — logical AND (independent assumptions). /// /// ```text /// strength = s1 * s2 /// confidence = min(c1, c2) /// ``` pub fn conjunction(tv1: TruthValue, tv2: TruthValue) -> TruthValue { clamped(TruthValue { strength: tv1.strength * tv2.strength, confidence: tv1.confidence.min(tv2.confidence), }) } /// **Evidence-based scoring** — refine a base truth value with confirming and /// contradicting evidence. /// /// 1. Iteratively apply [`revision`] across all `confirmations` to boost the /// base estimate. /// 2. For each contradiction, reduce confidence proportionally to the /// contradiction's strength. /// 3. Clamp the final result to \[0.0, 1.0\]. pub fn score_with_evidence( base: TruthValue, confirmations: &[TruthValue], contradictions: &[TruthValue], ) -> TruthValue { // Phase 1: merge confirmations via revision let mut current = base; for &conf in confirmations { current = revision(current, conf); } // Phase 2: reduce confidence proportional to contradiction strength for &contra in contradictions { // Each contradiction chips away at confidence in proportion to its // own strength (strong contradictions hurt more). current.confidence *= 1.0 - contra.strength * contra.confidence; } clamped(current) } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; /// Helper: approximate f32 equality. fn approx_eq(a: f32, b: f32) -> bool { (a - b).abs() < 1e-5 } // -- deduction --------------------------------------------------------- #[test] fn pln_deduction_basic() { let ab = TruthValue::new(0.8, 0.9); let bc = TruthValue::new(0.7, 0.8); let result = deduction(ab, bc); // strength = 0.8 * 0.7 = 0.56 assert!(approx_eq(result.strength, 0.56)); // confidence = 0.9 * 0.8 * (0.7 + 0.3 * 0.1) = 0.72 * 0.73 = 0.5256 let expected_c = 0.9 * 0.8 * (0.7 + (1.0 - 0.7) * (1.0 - 0.9)); assert!(approx_eq(result.confidence, expected_c)); } #[test] fn pln_deduction_boundary() { // (0, 0) -> zero everything let r1 = deduction(TruthValue::new(0.0, 0.0), TruthValue::new(0.0, 0.0)); assert!(approx_eq(r1.strength, 0.0)); assert!(approx_eq(r1.confidence, 0.0)); // (1, 1) -> perfect chain let r2 = deduction(TruthValue::new(1.0, 1.0), TruthValue::new(1.0, 1.0)); assert!(approx_eq(r2.strength, 1.0)); // confidence = 1*1*(1 + 0*0) = 1.0 assert!(approx_eq(r2.confidence, 1.0)); // (0.5, 0.5) let half = TruthValue::new(0.5, 0.5); let r3 = deduction(half, half); assert!(approx_eq(r3.strength, 0.25)); let expected_c = 0.5 * 0.5 * (0.5 + 0.5 * 0.5); assert!(approx_eq(r3.confidence, expected_c)); } // -- revision ---------------------------------------------------------- #[test] fn pln_revision_merge() { let tv1 = TruthValue::new(0.6, 0.5); let tv2 = TruthValue::new(0.8, 0.7); let result = revision(tv1, tv2); let expected_s = (0.6 * 0.5 + 0.8 * 0.7) / (0.5 + 0.7); let expected_c = (0.5 + 0.7) / (0.5 + 0.7 + 1.0); assert!(approx_eq(result.strength, expected_s)); assert!(approx_eq(result.confidence, expected_c)); } #[test] fn pln_revision_symmetry() { let a = TruthValue::new(0.3, 0.4); let b = TruthValue::new(0.9, 0.6); let ab = revision(a, b); let ba = revision(b, a); assert!(approx_eq(ab.strength, ba.strength)); assert!(approx_eq(ab.confidence, ba.confidence)); } // -- negation ---------------------------------------------------------- #[test] fn pln_negation() { let tv = TruthValue::new(0.7, 0.8); let result = negation(tv); assert!(approx_eq(result.strength, 0.3)); assert!(approx_eq(result.confidence, 0.8)); } // -- conjunction ------------------------------------------------------- #[test] fn pln_conjunction() { let tv1 = TruthValue::new(0.8, 0.9); let tv2 = TruthValue::new(0.6, 0.7); let result = conjunction(tv1, tv2); assert!(approx_eq(result.strength, 0.48)); assert!(approx_eq(result.confidence, 0.7)); } // -- score_with_evidence ----------------------------------------------- #[test] fn pln_score_with_confirmations() { let base = TruthValue::new(0.5, 0.3); let confirmations = [ TruthValue::new(0.7, 0.5), TruthValue::new(0.8, 0.6), ]; let result = score_with_evidence(base, &confirmations, &[]); // Confirmations should boost both strength and confidence above base assert!(result.strength > base.strength); assert!(result.confidence > base.confidence); assert!(result.strength <= 1.0); assert!(result.confidence <= 1.0); } #[test] fn pln_score_with_contradictions() { let base = TruthValue::new(0.8, 0.9); let contradictions = [ TruthValue::new(0.7, 0.8), ]; let result = score_with_evidence(base, &[], &contradictions); // Contradictions should reduce confidence assert!(result.confidence < base.confidence); // Strength unchanged when no confirmations assert!(approx_eq(result.strength, base.strength)); } // -- bounds ------------------------------------------------------------ #[test] fn pln_values_bounded() { // Extreme inputs that might push results out of range let big = TruthValue::new(2.0, 5.0); assert!(big.strength <= 1.0 && big.strength >= 0.0); assert!(big.confidence <= 1.0 && big.confidence >= 0.0); let neg = TruthValue::new(-1.0, -3.0); assert!(neg.strength >= 0.0); assert!(neg.confidence >= 0.0); // Deduction of clamped extremes stays in bounds let d = deduction(big, big); assert!(d.strength >= 0.0 && d.strength <= 1.0); assert!(d.confidence >= 0.0 && d.confidence <= 1.0); // Negation of boundary stays in bounds let n = negation(TruthValue::new(0.0, 1.0)); assert!(n.strength >= 0.0 && n.strength <= 1.0); assert!(n.confidence >= 0.0 && n.confidence <= 1.0); // score_with_evidence stays bounded let scored = score_with_evidence( TruthValue::new(1.0, 1.0), &[TruthValue::new(1.0, 1.0)], &[TruthValue::new(1.0, 1.0)], ); assert!(scored.strength >= 0.0 && scored.strength <= 1.0); assert!(scored.confidence >= 0.0 && scored.confidence <= 1.0); } // -- zero-confidence edge case ----------------------------------------- #[test] fn pln_revision_zero_confidence() { let tv1 = TruthValue::new(0.5, 0.0); let tv2 = TruthValue::new(0.8, 0.0); // Should not panic; returns tv1 when both confidences are zero let result = revision(tv1, tv2); assert!(approx_eq(result.strength, tv1.strength)); assert!(approx_eq(result.confidence, tv1.confidence)); // One zero, one nonzero — should still work let tv3 = TruthValue::new(0.9, 0.6); let result2 = revision(tv1, tv3); assert!(result2.strength >= 0.0 && result2.strength <= 1.0); assert!(result2.confidence >= 0.0 && result2.confidence <= 1.0); } }