diff --git a/src/lib.rs b/src/lib.rs index f4b3909..52be22a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod embedding; pub mod migrations; pub mod tools; pub mod transport; +pub mod truth; pub mod ttl; use anyhow::Result; diff --git a/src/truth/mod.rs b/src/truth/mod.rs new file mode 100644 index 0000000..76ce9fe --- /dev/null +++ b/src/truth/mod.rs @@ -0,0 +1,3 @@ +//! Truth scoring engine — PLN deduction, ECAN attention, and memory scoring. + +pub mod pln; diff --git a/src/truth/pln.rs b/src/truth/pln.rs new file mode 100644 index 0000000..dd0812c --- /dev/null +++ b/src/truth/pln.rs @@ -0,0 +1,319 @@ +//! 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); + } +}