1use std::collections::{HashMap, HashSet, VecDeque};
43use std::fmt;
44
45#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct TruthValue {
54 pub strength: f64,
56 pub confidence: f64,
58}
59
60impl TruthValue {
61 #[inline]
63 pub fn new(strength: f64, confidence: f64) -> Self {
64 Self {
65 strength: strength.clamp(0.0, 1.0),
66 confidence: confidence.clamp(0.0, 1.0),
67 }
68 }
69
70 #[inline]
72 pub fn unknown() -> Self {
73 Self {
74 strength: 0.5,
75 confidence: 0.0,
76 }
77 }
78
79 #[inline]
81 pub fn certain_true() -> Self {
82 Self {
83 strength: 1.0,
84 confidence: 1.0,
85 }
86 }
87
88 #[inline]
90 pub fn certain_false() -> Self {
91 Self {
92 strength: 0.0,
93 confidence: 1.0,
94 }
95 }
96
97 #[inline]
100 pub fn count(self) -> f64 {
101 let denom = 1.0 - self.confidence;
102 if denom <= 0.0 {
103 f64::INFINITY
104 } else {
105 self.confidence / denom
106 }
107 }
108
109 #[inline]
111 pub fn negate(self) -> Self {
112 Self::new(1.0 - self.strength, self.confidence)
113 }
114
115 #[inline]
117 pub fn conjunction(self, other: Self) -> Self {
118 Self::new(
119 self.strength * other.strength,
120 self.confidence.min(other.confidence),
121 )
122 }
123
124 #[inline]
126 pub fn disjunction(self, other: Self) -> Self {
127 Self::new(
128 self.strength + other.strength - self.strength * other.strength,
129 self.confidence.min(other.confidence),
130 )
131 }
132
133 pub fn revise(self, other: Self) -> Self {
141 let n1 = self.count();
142 let n2 = other.count();
143
144 if n1.is_infinite() && n2.is_infinite() {
146 return Self::new((self.strength + other.strength) / 2.0, 1.0);
148 }
149 if n1.is_infinite() {
150 return self;
151 }
152 if n2.is_infinite() {
153 return other;
154 }
155
156 let n_total = n1 + n2;
157 if n_total <= 0.0 {
158 return Self::unknown();
159 }
160 let s_rev = (self.strength * n1 + other.strength * n2) / n_total;
161 let c_rev = n_total / (n_total + 1.0);
162 Self::new(s_rev, c_rev)
163 }
164
165 pub fn deduction(ab: Self, bc: Self, b: Self, c: Self) -> Self {
172 let s_ab = ab.strength;
173 let s_bc = bc.strength;
174 let s_b = b.strength;
175 let s_c = c.strength;
176
177 let s_ac = if (1.0 - s_b).abs() < 1e-12 {
178 s_bc
180 } else {
181 let correction = (s_c - s_b * s_bc) / (1.0 - s_b);
182 (s_ab * s_bc + (1.0 - s_ab) * correction).clamp(0.0, 1.0)
183 };
184
185 let product = ab.confidence * bc.confidence;
186 let c_ac = (product * product.min(1.0)).clamp(0.0, 1.0);
187
188 Self::new(s_ac, c_ac)
189 }
190
191 pub fn induction(ab: Self, ac: Self) -> Self {
199 let s_bc = (ac.strength / ab.strength.max(0.01)).clamp(0.0, 1.0);
200 let product = ab.confidence * ac.confidence;
201 let c_bc = (product * product.min(1.0)).clamp(0.0, 1.0);
202 Self::new(s_bc, c_bc)
203 }
204
205 pub fn abduction(ba: Self, ca: Self) -> Self {
213 let s_bc = (ba.strength * ca.strength).clamp(0.0, 1.0);
214 let product = ba.confidence * ca.confidence;
215 let c_bc = (product * product.min(1.0)).clamp(0.0, 1.0);
216 Self::new(s_bc, c_bc)
217 }
218
219 pub fn modus_ponens(a: Self, ab: Self) -> Self {
226 let s_b = (a.strength * ab.strength + (1.0 - a.strength) * (1.0 - ab.strength) * 0.5)
227 .clamp(0.0, 1.0);
228 let c_b = (a.confidence * ab.confidence).clamp(0.0, 1.0);
229 Self::new(s_b, c_b)
230 }
231}
232
233impl fmt::Display for TruthValue {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 write!(f, "TV(s={:.4}, c={:.4})", self.strength, self.confidence)
236 }
237}
238
239impl Default for TruthValue {
240 fn default() -> Self {
241 Self::unknown()
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Hash)]
249pub enum LinkType {
250 Inheritance,
252 Similarity,
254 Implication,
256 Equivalence,
258 AND,
260 OR,
262 NOT,
264 Evaluation,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
270pub enum AtomType {
271 Node(String),
273 Link(LinkType),
275}
276
277#[derive(Debug, Clone)]
281pub struct PlnAtom {
282 pub id: String,
284 pub name: String,
286 pub tv: TruthValue,
288 pub atom_type: AtomType,
290}
291
292impl PlnAtom {
293 pub fn new(
295 id: impl Into<String>,
296 name: impl Into<String>,
297 tv: TruthValue,
298 atom_type: AtomType,
299 ) -> Self {
300 Self {
301 id: id.into(),
302 name: name.into(),
303 tv,
304 atom_type,
305 }
306 }
307}
308
309#[derive(Debug, Clone)]
311pub struct PlnLink {
312 pub id: String,
314 pub link_type: LinkType,
316 pub source_ids: Vec<String>,
318 pub tv: TruthValue,
320}
321
322impl PlnLink {
323 pub fn new(
325 id: impl Into<String>,
326 link_type: LinkType,
327 source_ids: Vec<String>,
328 tv: TruthValue,
329 ) -> Self {
330 Self {
331 id: id.into(),
332 link_type,
333 source_ids,
334 tv,
335 }
336 }
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Hash)]
343pub enum PlnInferenceRule {
344 Deduction,
346 Induction,
348 Abduction,
350 Revision,
352 Conjunction,
354 Disjunction,
356 Negation,
358 ModusPonens,
360}
361
362impl fmt::Display for PlnInferenceRule {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 let s = match self {
365 Self::Deduction => "Deduction",
366 Self::Induction => "Induction",
367 Self::Abduction => "Abduction",
368 Self::Revision => "Revision",
369 Self::Conjunction => "Conjunction",
370 Self::Disjunction => "Disjunction",
371 Self::Negation => "Negation",
372 Self::ModusPonens => "ModusPonens",
373 };
374 f.write_str(s)
375 }
376}
377
378#[derive(Debug, Clone)]
380pub struct PlnInferenceResult {
381 pub conclusion_id: String,
383 pub tv: TruthValue,
385 pub rule_used: PlnInferenceRule,
387 pub premise_ids: Vec<String>,
389 pub confidence_delta: f64,
391}
392
393#[derive(Debug, Clone)]
397pub struct PlnConfig {
398 pub max_atoms: usize,
400 pub inference_depth: u8,
402 pub min_confidence_threshold: f64,
404 pub enable_revision: bool,
407}
408
409impl Default for PlnConfig {
410 fn default() -> Self {
411 Self {
412 max_atoms: 100_000,
413 inference_depth: 6,
414 min_confidence_threshold: 0.01,
415 enable_revision: true,
416 }
417 }
418}
419
420#[derive(Debug, Clone, Default)]
422pub struct PlnStats {
423 pub atom_count: usize,
425 pub link_count: usize,
427 pub inferences_performed: u64,
429 pub avg_confidence: f64,
431}
432
433#[derive(Debug, Clone, PartialEq)]
437pub enum PlnError {
438 AtomNotFound(String),
440 InsufficientConfidence(f64),
442 CyclicInference(Vec<String>),
444 InvalidRule(String),
446 MaxAtomsExceeded,
448}
449
450impl fmt::Display for PlnError {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 match self {
453 Self::AtomNotFound(id) => write!(f, "atom not found: {id}"),
454 Self::InsufficientConfidence(c) => {
455 write!(f, "confidence {c:.4} below threshold")
456 }
457 Self::CyclicInference(path) => write!(f, "cyclic inference: {path:?}"),
458 Self::InvalidRule(msg) => write!(f, "invalid rule: {msg}"),
459 Self::MaxAtomsExceeded => write!(f, "max atom limit exceeded"),
460 }
461 }
462}
463
464impl std::error::Error for PlnError {}
465
466#[derive(Debug, Clone)]
470struct OutEdge {
471 link_id: String,
473 targets: Vec<String>,
475}
476
477pub struct ProbabilisticLogicNetwork {
484 atoms: HashMap<String, PlnAtom>,
486 links: HashMap<String, PlnLink>,
488 adjacency: HashMap<String, Vec<OutEdge>>,
490 config: PlnConfig,
492 inferences_performed: u64,
494}
495
496impl ProbabilisticLogicNetwork {
497 pub fn new(config: PlnConfig) -> Self {
499 Self {
500 atoms: HashMap::new(),
501 links: HashMap::new(),
502 adjacency: HashMap::new(),
503 config,
504 inferences_performed: 0,
505 }
506 }
507
508 pub fn add_atom(&mut self, atom: PlnAtom) -> Result<(), PlnError> {
515 let total = self.atoms.len() + self.links.len();
516 if !self.atoms.contains_key(&atom.id) && total >= self.config.max_atoms {
517 return Err(PlnError::MaxAtomsExceeded);
518 }
519 self.atoms.insert(atom.id.clone(), atom);
520 Ok(())
521 }
522
523 pub fn add_link(&mut self, link: PlnLink) -> Result<(), PlnError> {
528 for src in &link.source_ids {
530 if !self.atoms.contains_key(src) && !self.links.contains_key(src) {
531 return Err(PlnError::AtomNotFound(src.clone()));
532 }
533 }
534
535 let total = self.atoms.len() + self.links.len();
536 let is_new = !self.links.contains_key(&link.id);
537 if is_new && total >= self.config.max_atoms {
538 return Err(PlnError::MaxAtomsExceeded);
539 }
540
541 if link.source_ids.len() >= 2 {
543 let src = link.source_ids[0].clone();
544 let tgts: Vec<String> = link.source_ids[1..].to_vec();
545 let edge = OutEdge {
546 link_id: link.id.clone(),
547 targets: tgts,
548 };
549 self.adjacency.entry(src).or_default().push(edge);
550 }
551
552 self.links.insert(link.id.clone(), link);
553 Ok(())
554 }
555
556 pub fn get_tv(&self, id: &str) -> Result<TruthValue, PlnError> {
560 if let Some(a) = self.atoms.get(id) {
561 return Ok(a.tv);
562 }
563 if let Some(l) = self.links.get(id) {
564 return Ok(l.tv);
565 }
566 Err(PlnError::AtomNotFound(id.to_string()))
567 }
568
569 pub fn infer(
578 &mut self,
579 rule: PlnInferenceRule,
580 premise_ids: Vec<String>,
581 ) -> Result<PlnInferenceResult, PlnError> {
582 let tv = self.apply_rule(&rule, &premise_ids)?;
583
584 if tv.confidence < self.config.min_confidence_threshold {
585 return Err(PlnError::InsufficientConfidence(tv.confidence));
586 }
587
588 let conclusion_id = conclusion_id_for(&rule, &premise_ids);
590
591 let prior_confidence = self
593 .atoms
594 .get(&conclusion_id)
595 .map(|a| a.tv.confidence)
596 .or_else(|| self.links.get(&conclusion_id).map(|l| l.tv.confidence))
597 .unwrap_or(0.0);
598
599 let confidence_delta = tv.confidence - prior_confidence;
600
601 self.inferences_performed += 1;
602
603 Ok(PlnInferenceResult {
604 conclusion_id,
605 tv,
606 rule_used: rule,
607 premise_ids,
608 confidence_delta,
609 })
610 }
611
612 pub fn apply_inference(&mut self, result: PlnInferenceResult) -> Result<(), PlnError> {
618 let id = &result.conclusion_id;
619
620 if let Some(existing) = self.atoms.get_mut(id) {
621 if self.config.enable_revision {
622 existing.tv = existing.tv.revise(result.tv);
623 } else {
624 existing.tv = result.tv;
625 }
626 } else if let Some(existing) = self.links.get_mut(id) {
627 if self.config.enable_revision {
628 existing.tv = existing.tv.revise(result.tv);
629 } else {
630 existing.tv = result.tv;
631 }
632 } else {
633 let total = self.atoms.len() + self.links.len();
635 if total >= self.config.max_atoms {
636 return Err(PlnError::MaxAtomsExceeded);
637 }
638 let atom = PlnAtom {
639 id: result.conclusion_id.clone(),
640 name: result.conclusion_id.clone(),
641 tv: result.tv,
642 atom_type: AtomType::Node(format!("InferredBy({})", result.rule_used)),
643 };
644 self.atoms.insert(result.conclusion_id, atom);
645 }
646 Ok(())
647 }
648
649 pub fn find_chains(
656 &self,
657 from: &str,
658 to: &str,
659 max_depth: u8,
660 ) -> Result<Vec<Vec<String>>, PlnError> {
661 if !self.atoms.contains_key(from) && !self.links.contains_key(from) {
663 return Err(PlnError::AtomNotFound(from.to_string()));
664 }
665 if !self.atoms.contains_key(to) && !self.links.contains_key(to) {
666 return Err(PlnError::AtomNotFound(to.to_string()));
667 }
668
669 let mut results: Vec<Vec<String>> = Vec::new();
670 let mut queue: VecDeque<(String, Vec<String>, HashSet<String>)> = VecDeque::new();
672 {
673 let mut init_visited = HashSet::new();
674 init_visited.insert(from.to_string());
675 queue.push_back((from.to_string(), vec![from.to_string()], init_visited));
676 }
677
678 while let Some((node, path, visited)) = queue.pop_front() {
679 if path.len() as u8 > max_depth + 1 {
680 continue;
681 }
682
683 if let Some(edges) = self.adjacency.get(&node) {
684 for edge in edges {
685 for tgt in &edge.targets {
686 if visited.contains(tgt) {
687 continue;
689 }
690
691 let mut new_path = path.clone();
692 new_path.push(edge.link_id.clone());
693 new_path.push(tgt.clone());
694
695 if tgt == to {
696 results.push(new_path);
697 } else if (new_path.len() as u8) <= max_depth * 2 + 1 {
698 let mut new_visited = visited.clone();
699 new_visited.insert(tgt.clone());
700 queue.push_back((tgt.clone(), new_path, new_visited));
701 }
702 }
703 }
704 }
705 }
706
707 Ok(results)
708 }
709
710 pub fn most_confident(&self, n: usize) -> Vec<PlnAtom> {
714 let mut atoms: Vec<&PlnAtom> = self.atoms.values().collect();
715 atoms.sort_by(|a, b| {
716 b.tv.confidence
717 .partial_cmp(&a.tv.confidence)
718 .unwrap_or(std::cmp::Ordering::Equal)
719 });
720 atoms.into_iter().take(n).cloned().collect()
721 }
722
723 pub fn uncertain_atoms(&self, threshold: f64) -> Vec<PlnAtom> {
725 self.atoms
726 .values()
727 .filter(|a| a.tv.confidence < threshold)
728 .cloned()
729 .collect()
730 }
731
732 pub fn stats(&self) -> PlnStats {
736 let atom_count = self.atoms.len();
737 let link_count = self.links.len();
738 let total = atom_count + link_count;
739
740 let sum_conf: f64 = self.atoms.values().map(|a| a.tv.confidence).sum::<f64>()
741 + self.links.values().map(|l| l.tv.confidence).sum::<f64>();
742
743 let avg_confidence = if total > 0 {
744 sum_conf / total as f64
745 } else {
746 0.0
747 };
748
749 PlnStats {
750 atom_count,
751 link_count,
752 inferences_performed: self.inferences_performed,
753 avg_confidence,
754 }
755 }
756
757 fn apply_rule(
761 &self,
762 rule: &PlnInferenceRule,
763 premise_ids: &[String],
764 ) -> Result<TruthValue, PlnError> {
765 match rule {
766 PlnInferenceRule::Negation => self.rule_negation(premise_ids),
767 PlnInferenceRule::Conjunction => self.rule_conjunction(premise_ids),
768 PlnInferenceRule::Disjunction => self.rule_disjunction(premise_ids),
769 PlnInferenceRule::Revision => self.rule_revision(premise_ids),
770 PlnInferenceRule::Deduction => self.rule_deduction(premise_ids),
771 PlnInferenceRule::Induction => self.rule_induction(premise_ids),
772 PlnInferenceRule::Abduction => self.rule_abduction(premise_ids),
773 PlnInferenceRule::ModusPonens => self.rule_modus_ponens(premise_ids),
774 }
775 }
776
777 fn require_tv(&self, id: &str) -> Result<TruthValue, PlnError> {
778 self.get_tv(id)
779 }
780
781 fn rule_negation(&self, ids: &[String]) -> Result<TruthValue, PlnError> {
782 if ids.len() != 1 {
783 return Err(PlnError::InvalidRule(
784 "Negation requires exactly 1 premise".into(),
785 ));
786 }
787 Ok(self.require_tv(&ids[0])?.negate())
788 }
789
790 fn rule_conjunction(&self, ids: &[String]) -> Result<TruthValue, PlnError> {
791 if ids.len() < 2 {
792 return Err(PlnError::InvalidRule(
793 "Conjunction requires at least 2 premises".into(),
794 ));
795 }
796 let mut tv = self.require_tv(&ids[0])?;
797 for id in &ids[1..] {
798 tv = tv.conjunction(self.require_tv(id)?);
799 }
800 Ok(tv)
801 }
802
803 fn rule_disjunction(&self, ids: &[String]) -> Result<TruthValue, PlnError> {
804 if ids.len() < 2 {
805 return Err(PlnError::InvalidRule(
806 "Disjunction requires at least 2 premises".into(),
807 ));
808 }
809 let mut tv = self.require_tv(&ids[0])?;
810 for id in &ids[1..] {
811 tv = tv.disjunction(self.require_tv(id)?);
812 }
813 Ok(tv)
814 }
815
816 fn rule_revision(&self, ids: &[String]) -> Result<TruthValue, PlnError> {
817 if ids.len() != 2 {
818 return Err(PlnError::InvalidRule(
819 "Revision requires exactly 2 premises".into(),
820 ));
821 }
822 let tv1 = self.require_tv(&ids[0])?;
823 let tv2 = self.require_tv(&ids[1])?;
824 Ok(tv1.revise(tv2))
825 }
826
827 fn rule_deduction(&self, ids: &[String]) -> Result<TruthValue, PlnError> {
829 if ids.len() != 4 {
830 return Err(PlnError::InvalidRule(
831 "Deduction requires 4 premises: [AB, BC, B, C]".into(),
832 ));
833 }
834 let ab = self.require_tv(&ids[0])?;
835 let bc = self.require_tv(&ids[1])?;
836 let b = self.require_tv(&ids[2])?;
837 let c = self.require_tv(&ids[3])?;
838 Ok(TruthValue::deduction(ab, bc, b, c))
839 }
840
841 fn rule_induction(&self, ids: &[String]) -> Result<TruthValue, PlnError> {
843 if ids.len() != 2 {
844 return Err(PlnError::InvalidRule(
845 "Induction requires 2 premises: [AB, AC]".into(),
846 ));
847 }
848 let ab = self.require_tv(&ids[0])?;
849 let ac = self.require_tv(&ids[1])?;
850 Ok(TruthValue::induction(ab, ac))
851 }
852
853 fn rule_abduction(&self, ids: &[String]) -> Result<TruthValue, PlnError> {
855 if ids.len() != 2 {
856 return Err(PlnError::InvalidRule(
857 "Abduction requires 2 premises: [BA, CA]".into(),
858 ));
859 }
860 let ba = self.require_tv(&ids[0])?;
861 let ca = self.require_tv(&ids[1])?;
862 Ok(TruthValue::abduction(ba, ca))
863 }
864
865 fn rule_modus_ponens(&self, ids: &[String]) -> Result<TruthValue, PlnError> {
867 if ids.len() != 2 {
868 return Err(PlnError::InvalidRule(
869 "ModusPonens requires 2 premises: [A, A→B]".into(),
870 ));
871 }
872 let a = self.require_tv(&ids[0])?;
873 let ab = self.require_tv(&ids[1])?;
874 Ok(TruthValue::modus_ponens(a, ab))
875 }
876}
877
878fn conclusion_id_for(rule: &PlnInferenceRule, premise_ids: &[String]) -> String {
882 let premises_joined = premise_ids.join("_");
883 format!("inferred__{rule}__{premises_joined}")
884}
885
886#[cfg(test)]
890fn xorshift64(state: &mut u64) -> u64 {
891 let mut x = *state;
892 x ^= x << 13;
893 x ^= x >> 7;
894 x ^= x << 17;
895 *state = x;
896 x
897}
898
899#[cfg(test)]
900fn rng_f64(state: &mut u64) -> f64 {
901 (xorshift64(state) as f64) / (u64::MAX as f64)
902}
903
904#[cfg(test)]
907mod tests {
908 use super::*;
909
910 fn concept(id: &str, s: f64, c: f64) -> PlnAtom {
913 PlnAtom::new(
914 id,
915 id,
916 TruthValue::new(s, c),
917 AtomType::Node("ConceptNode".into()),
918 )
919 }
920
921 fn inh_link(id: &str, src: &str, dst: &str, s: f64, c: f64) -> PlnLink {
922 PlnLink::new(
923 id,
924 LinkType::Inheritance,
925 vec![src.into(), dst.into()],
926 TruthValue::new(s, c),
927 )
928 }
929
930 fn basic_net() -> ProbabilisticLogicNetwork {
931 let mut pln = ProbabilisticLogicNetwork::new(PlnConfig::default());
932 pln.add_atom(concept("a", 0.8, 0.9))
933 .expect("test: should succeed");
934 pln.add_atom(concept("b", 0.7, 0.8))
935 .expect("test: should succeed");
936 pln.add_atom(concept("c", 0.6, 0.7))
937 .expect("test: should succeed");
938 pln.add_link(inh_link("ab", "a", "b", 0.9, 0.8))
939 .expect("test: should succeed");
940 pln.add_link(inh_link("bc", "b", "c", 0.85, 0.75))
941 .expect("test: should succeed");
942 pln
943 }
944
945 #[test]
948 fn tv_clamp() {
949 let tv = TruthValue::new(-0.5, 1.5);
950 assert_eq!(tv.strength, 0.0);
951 assert_eq!(tv.confidence, 1.0);
952 }
953
954 #[test]
955 fn tv_unknown() {
956 let tv = TruthValue::unknown();
957 assert_eq!(tv.strength, 0.5);
958 assert_eq!(tv.confidence, 0.0);
959 }
960
961 #[test]
962 fn tv_certain_true() {
963 let tv = TruthValue::certain_true();
964 assert_eq!(tv.strength, 1.0);
965 assert_eq!(tv.confidence, 1.0);
966 }
967
968 #[test]
969 fn tv_certain_false() {
970 let tv = TruthValue::certain_false();
971 assert_eq!(tv.strength, 0.0);
972 assert_eq!(tv.confidence, 1.0);
973 }
974
975 #[test]
978 fn tv_count_zero_confidence() {
979 let tv = TruthValue::new(0.5, 0.0);
980 assert_eq!(tv.count(), 0.0);
981 }
982
983 #[test]
984 fn tv_count_full_confidence() {
985 let tv = TruthValue::certain_true();
986 assert!(tv.count().is_infinite());
987 }
988
989 #[test]
990 fn tv_count_half_confidence() {
991 let tv = TruthValue::new(0.8, 0.5);
992 assert!((tv.count() - 1.0).abs() < 1e-10);
994 }
995
996 #[test]
999 fn tv_negate_basic() {
1000 let tv = TruthValue::new(0.7, 0.8);
1001 let neg = tv.negate();
1002 assert!((neg.strength - 0.3).abs() < 1e-10);
1003 assert_eq!(neg.confidence, 0.8);
1004 }
1005
1006 #[test]
1007 fn tv_negate_double() {
1008 let tv = TruthValue::new(0.4, 0.6);
1009 let double_neg = tv.negate().negate();
1010 assert!((double_neg.strength - tv.strength).abs() < 1e-10);
1011 assert_eq!(double_neg.confidence, tv.confidence);
1012 }
1013
1014 #[test]
1017 fn tv_conjunction_basic() {
1018 let a = TruthValue::new(0.8, 0.9);
1019 let b = TruthValue::new(0.6, 0.7);
1020 let c = a.conjunction(b);
1021 assert!((c.strength - 0.48).abs() < 1e-10);
1022 assert_eq!(c.confidence, 0.7);
1023 }
1024
1025 #[test]
1026 fn tv_conjunction_with_false() {
1027 let a = TruthValue::new(0.9, 0.9);
1028 let b = TruthValue::certain_false();
1029 let c = a.conjunction(b);
1030 assert!(c.strength < 1e-10);
1031 }
1032
1033 #[test]
1034 fn tv_conjunction_commutativity() {
1035 let a = TruthValue::new(0.7, 0.8);
1036 let b = TruthValue::new(0.5, 0.6);
1037 let ab = a.conjunction(b);
1038 let ba = b.conjunction(a);
1039 assert!((ab.strength - ba.strength).abs() < 1e-10);
1040 assert_eq!(ab.confidence, ba.confidence);
1041 }
1042
1043 #[test]
1046 fn tv_disjunction_basic() {
1047 let a = TruthValue::new(0.5, 0.8);
1048 let b = TruthValue::new(0.5, 0.7);
1049 let d = a.disjunction(b);
1050 assert!((d.strength - 0.75).abs() < 1e-10);
1052 assert_eq!(d.confidence, 0.7);
1053 }
1054
1055 #[test]
1056 fn tv_disjunction_with_true() {
1057 let a = TruthValue::new(0.3, 0.8);
1058 let b = TruthValue::certain_true();
1059 let d = a.disjunction(b);
1060 assert!((d.strength - 1.0).abs() < 1e-10);
1061 }
1062
1063 #[test]
1064 fn tv_disjunction_commutativity() {
1065 let a = TruthValue::new(0.3, 0.9);
1066 let b = TruthValue::new(0.7, 0.6);
1067 let ab = a.disjunction(b);
1068 let ba = b.disjunction(a);
1069 assert!((ab.strength - ba.strength).abs() < 1e-10);
1070 }
1071
1072 #[test]
1075 fn tv_revision_basic() {
1076 let t1 = TruthValue::new(0.8, 0.4); let t2 = TruthValue::new(0.6, 0.6); let rev = t1.revise(t2);
1079 assert!(rev.confidence > t1.confidence);
1081 assert!(rev.confidence > t2.confidence);
1082 assert!((0.0..=1.0).contains(&rev.strength));
1083 }
1084
1085 #[test]
1086 fn tv_revision_idempotent_certain() {
1087 let t1 = TruthValue::certain_true();
1088 let t2 = TruthValue::certain_true();
1089 let rev = t1.revise(t2);
1090 assert_eq!(rev.confidence, 1.0);
1091 assert_eq!(rev.strength, 1.0);
1092 }
1093
1094 #[test]
1095 fn tv_revision_unknown_improves() {
1096 let unknown = TruthValue::unknown();
1099 let strong_evidence = TruthValue::new(0.9, 0.8); let rev = unknown.revise(strong_evidence);
1101 assert!(rev.confidence >= strong_evidence.confidence);
1103 let second = TruthValue::new(0.85, 0.8);
1105 let rev2 = rev.revise(second);
1106 assert!(rev2.confidence > rev.confidence);
1107 }
1108
1109 #[test]
1110 fn tv_revision_symmetry() {
1111 let a = TruthValue::new(0.7, 0.5);
1112 let b = TruthValue::new(0.4, 0.3);
1113 let ab = a.revise(b);
1114 let ba = b.revise(a);
1115 assert!((ab.strength - ba.strength).abs() < 1e-10);
1116 assert!((ab.confidence - ba.confidence).abs() < 1e-10);
1117 }
1118
1119 #[test]
1122 fn tv_deduction_basic() {
1123 let ab = TruthValue::new(0.9, 0.8);
1124 let bc = TruthValue::new(0.85, 0.75);
1125 let b = TruthValue::new(0.7, 0.8);
1126 let c = TruthValue::new(0.6, 0.7);
1127 let ac = TruthValue::deduction(ab, bc, b, c);
1128 assert!((0.0..=1.0).contains(&ac.strength));
1129 assert!((0.0..=1.0).contains(&ac.confidence));
1130 }
1131
1132 #[test]
1133 fn tv_deduction_high_confidence_chain() {
1134 let ab = TruthValue::new(0.95, 0.9);
1135 let bc = TruthValue::new(0.95, 0.9);
1136 let b = TruthValue::new(0.8, 0.9);
1137 let c = TruthValue::new(0.8, 0.9);
1138 let ac = TruthValue::deduction(ab, bc, b, c);
1139 assert!(ac.strength > 0.8);
1141 }
1142
1143 #[test]
1144 fn tv_deduction_degenerate_sb_one() {
1145 let ab = TruthValue::new(0.9, 0.8);
1147 let bc = TruthValue::new(0.8, 0.7);
1148 let b = TruthValue::new(1.0, 0.9);
1149 let c = TruthValue::new(0.9, 0.8);
1150 let ac = TruthValue::deduction(ab, bc, b, c);
1151 assert!((0.0..=1.0).contains(&ac.strength));
1152 }
1153
1154 #[test]
1157 fn tv_induction_basic() {
1158 let ab = TruthValue::new(0.8, 0.7);
1159 let ac = TruthValue::new(0.6, 0.6);
1160 let bc = TruthValue::induction(ab, ac);
1161 assert!((0.0..=1.0).contains(&bc.strength));
1162 assert!((0.0..=1.0).contains(&bc.confidence));
1163 }
1164
1165 #[test]
1166 fn tv_induction_perfect_ab() {
1167 let ab = TruthValue::new(1.0, 0.9);
1169 let ac = TruthValue::new(0.7, 0.8);
1170 let bc = TruthValue::induction(ab, ac);
1171 assert!((bc.strength - 0.7).abs() < 1e-6);
1172 }
1173
1174 #[test]
1175 fn tv_induction_low_ab() {
1176 let ab = TruthValue::new(0.001, 0.5);
1178 let ac = TruthValue::new(0.9, 0.8);
1179 let bc = TruthValue::induction(ab, ac);
1180 assert!(bc.strength <= 1.0);
1181 }
1182
1183 #[test]
1186 fn tv_abduction_basic() {
1187 let ba = TruthValue::new(0.7, 0.8);
1188 let ca = TruthValue::new(0.8, 0.7);
1189 let bc = TruthValue::abduction(ba, ca);
1190 assert!((0.0..=1.0).contains(&bc.strength));
1191 assert!((0.0..=1.0).contains(&bc.confidence));
1192 }
1193
1194 #[test]
1195 fn tv_abduction_product_strength() {
1196 let ba = TruthValue::new(0.6, 0.8);
1197 let ca = TruthValue::new(0.5, 0.7);
1198 let bc = TruthValue::abduction(ba, ca);
1199 assert!((bc.strength - 0.3).abs() < 1e-10);
1200 }
1201
1202 #[test]
1205 fn tv_modus_ponens_basic() {
1206 let a = TruthValue::new(0.9, 0.8);
1207 let ab = TruthValue::new(0.95, 0.9);
1208 let b = TruthValue::modus_ponens(a, ab);
1209 assert!(b.strength > 0.5);
1210 assert!((0.0..=1.0).contains(&b.confidence));
1211 }
1212
1213 #[test]
1214 fn tv_modus_ponens_false_premise() {
1215 let a = TruthValue::certain_false();
1216 let ab = TruthValue::new(0.9, 0.9);
1217 let b = TruthValue::modus_ponens(a, ab);
1218 assert!(b.strength < 0.2);
1220 }
1221
1222 #[test]
1225 fn add_atom_ok() {
1226 let mut pln = ProbabilisticLogicNetwork::new(PlnConfig::default());
1227 pln.add_atom(concept("x", 0.5, 0.5))
1228 .expect("test: should succeed");
1229 assert!(pln.get_tv("x").is_ok());
1230 }
1231
1232 #[test]
1233 fn add_atom_max_exceeded() {
1234 let cfg = PlnConfig {
1235 max_atoms: 2,
1236 ..Default::default()
1237 };
1238 let mut pln = ProbabilisticLogicNetwork::new(cfg);
1239 pln.add_atom(concept("a", 0.5, 0.5))
1240 .expect("test: should succeed");
1241 pln.add_atom(concept("b", 0.5, 0.5))
1242 .expect("test: should succeed");
1243 let r = pln.add_atom(concept("c", 0.5, 0.5));
1244 assert_eq!(r, Err(PlnError::MaxAtomsExceeded));
1245 }
1246
1247 #[test]
1248 fn add_atom_replace_existing() {
1249 let mut pln = ProbabilisticLogicNetwork::new(PlnConfig::default());
1250 pln.add_atom(concept("x", 0.3, 0.4))
1251 .expect("test: should succeed");
1252 pln.add_atom(concept("x", 0.9, 0.9))
1253 .expect("test: should succeed"); let tv = pln.get_tv("x").expect("test: should succeed");
1255 assert!((tv.strength - 0.9).abs() < 1e-10);
1256 }
1257
1258 #[test]
1261 fn add_link_ok() {
1262 let pln = basic_net();
1263 let tv = pln.get_tv("ab").expect("test: should succeed");
1264 assert!((tv.strength - 0.9).abs() < 1e-10);
1265 }
1266
1267 #[test]
1268 fn add_link_missing_source() {
1269 let mut pln = ProbabilisticLogicNetwork::new(PlnConfig::default());
1270 pln.add_atom(concept("a", 0.5, 0.5))
1271 .expect("test: should succeed");
1272 let link = inh_link("ab", "a", "missing", 0.9, 0.8);
1273 let r = pln.add_link(link);
1274 assert!(matches!(r, Err(PlnError::AtomNotFound(_))));
1275 }
1276
1277 #[test]
1280 fn get_tv_missing() {
1281 let pln = ProbabilisticLogicNetwork::new(PlnConfig::default());
1282 assert!(matches!(pln.get_tv("nope"), Err(PlnError::AtomNotFound(_))));
1283 }
1284
1285 #[test]
1288 fn infer_negation() {
1289 let mut pln = basic_net();
1290 let r = pln
1291 .infer(PlnInferenceRule::Negation, vec!["a".into()])
1292 .expect("test: should succeed");
1293 assert!((r.tv.strength - (1.0 - 0.8)).abs() < 1e-10);
1294 assert!((r.tv.confidence - 0.9).abs() < 1e-10);
1295 }
1296
1297 #[test]
1298 fn infer_negation_wrong_arity() {
1299 let mut pln = basic_net();
1300 let r = pln.infer(PlnInferenceRule::Negation, vec!["a".into(), "b".into()]);
1301 assert!(matches!(r, Err(PlnError::InvalidRule(_))));
1302 }
1303
1304 #[test]
1307 fn infer_conjunction() {
1308 let mut pln = basic_net();
1309 let r = pln
1310 .infer(PlnInferenceRule::Conjunction, vec!["a".into(), "b".into()])
1311 .expect("test: should succeed");
1312 assert!((r.tv.strength - (0.8 * 0.7)).abs() < 1e-10);
1313 assert_eq!(r.tv.confidence, 0.8_f64.min(0.9));
1314 }
1315
1316 #[test]
1317 fn infer_conjunction_three_way() {
1318 let mut pln = basic_net();
1319 let r = pln
1320 .infer(
1321 PlnInferenceRule::Conjunction,
1322 vec!["a".into(), "b".into(), "c".into()],
1323 )
1324 .expect("test: should succeed");
1325 assert!((r.tv.strength - (0.8 * 0.7 * 0.6)).abs() < 1e-8);
1326 }
1327
1328 #[test]
1331 fn infer_disjunction() {
1332 let mut pln = basic_net();
1333 let r = pln
1334 .infer(PlnInferenceRule::Disjunction, vec!["a".into(), "b".into()])
1335 .expect("test: should succeed");
1336 assert!((r.tv.strength - 0.94).abs() < 1e-10);
1338 }
1339
1340 #[test]
1343 fn infer_revision() {
1344 let mut pln = basic_net();
1345 let r = pln
1346 .infer(PlnInferenceRule::Revision, vec!["a".into(), "b".into()])
1347 .expect("test: should succeed");
1348 assert!((0.0..=1.0).contains(&r.tv.strength));
1349 assert!((0.0..=1.0).contains(&r.tv.confidence));
1350 }
1351
1352 #[test]
1353 fn infer_revision_wrong_arity() {
1354 let mut pln = basic_net();
1355 let r = pln.infer(PlnInferenceRule::Revision, vec!["a".into()]);
1356 assert!(matches!(r, Err(PlnError::InvalidRule(_))));
1357 }
1358
1359 #[test]
1362 fn infer_deduction() {
1363 let mut pln = basic_net();
1364 let r = pln
1365 .infer(
1366 PlnInferenceRule::Deduction,
1367 vec!["ab".into(), "bc".into(), "b".into(), "c".into()],
1368 )
1369 .expect("test: should succeed");
1370 assert!((0.0..=1.0).contains(&r.tv.strength));
1371 assert_eq!(r.rule_used, PlnInferenceRule::Deduction);
1372 }
1373
1374 #[test]
1375 fn infer_deduction_wrong_arity() {
1376 let mut pln = basic_net();
1377 let r = pln.infer(PlnInferenceRule::Deduction, vec!["ab".into(), "bc".into()]);
1378 assert!(matches!(r, Err(PlnError::InvalidRule(_))));
1379 }
1380
1381 #[test]
1384 fn infer_induction() {
1385 let mut pln = basic_net();
1386 let r = pln
1387 .infer(PlnInferenceRule::Induction, vec!["ab".into(), "bc".into()])
1388 .expect("test: should succeed");
1389 assert!((0.0..=1.0).contains(&r.tv.strength));
1390 }
1391
1392 #[test]
1395 fn infer_abduction() {
1396 let mut pln = basic_net();
1397 let r = pln
1398 .infer(PlnInferenceRule::Abduction, vec!["ab".into(), "bc".into()])
1399 .expect("test: should succeed");
1400 assert!((0.0..=1.0).contains(&r.tv.strength));
1401 }
1402
1403 #[test]
1406 fn infer_modus_ponens() {
1407 let mut pln = basic_net();
1408 let r = pln
1409 .infer(PlnInferenceRule::ModusPonens, vec!["a".into(), "ab".into()])
1410 .expect("test: should succeed");
1411 assert!((0.0..=1.0).contains(&r.tv.strength));
1412 }
1413
1414 #[test]
1417 fn apply_inference_creates_atom() {
1418 let mut pln = basic_net();
1419 let r = pln
1420 .infer(PlnInferenceRule::Negation, vec!["a".into()])
1421 .expect("test: should succeed");
1422 let cid = r.conclusion_id.clone();
1423 pln.apply_inference(r).expect("test: should succeed");
1424 assert!(pln.get_tv(&cid).is_ok());
1425 }
1426
1427 #[test]
1428 fn apply_inference_revises_existing() {
1429 let mut pln = basic_net();
1430 let r1 = pln
1432 .infer(PlnInferenceRule::Negation, vec!["a".into()])
1433 .expect("test: should succeed");
1434 let cid = r1.conclusion_id.clone();
1435 pln.apply_inference(r1).expect("test: should succeed");
1436 let tv_before = pln.get_tv(&cid).expect("test: should succeed");
1437
1438 let r2 = pln
1440 .infer(PlnInferenceRule::Negation, vec!["a".into()])
1441 .expect("test: should succeed");
1442 pln.apply_inference(r2).expect("test: should succeed");
1443 let tv_after = pln.get_tv(&cid).expect("test: should succeed");
1444
1445 assert!(tv_after.confidence >= tv_before.confidence);
1447 }
1448
1449 #[test]
1450 fn apply_inference_no_revision_replaces() {
1451 let mut pln = ProbabilisticLogicNetwork::new(PlnConfig {
1452 enable_revision: false,
1453 ..Default::default()
1454 });
1455 pln.add_atom(concept("a", 0.8, 0.9))
1456 .expect("test: should succeed");
1457 let r = pln
1458 .infer(PlnInferenceRule::Negation, vec!["a".into()])
1459 .expect("test: should succeed");
1460 let cid = r.conclusion_id.clone();
1461 let expected_tv = r.tv;
1462 pln.apply_inference(r).expect("test: should succeed");
1463 let tv = pln.get_tv(&cid).expect("test: should succeed");
1464 assert!((tv.strength - expected_tv.strength).abs() < 1e-10);
1465 }
1466
1467 #[test]
1470 fn inference_result_confidence_delta_new_atom() {
1471 let mut pln = basic_net();
1472 let r = pln
1473 .infer(PlnInferenceRule::Negation, vec!["a".into()])
1474 .expect("test: should succeed");
1475 assert!((r.confidence_delta - r.tv.confidence).abs() < 1e-10);
1477 }
1478
1479 #[test]
1482 fn find_chains_direct() {
1483 let pln = basic_net();
1484 let chains = pln.find_chains("a", "b", 4).expect("test: should succeed");
1485 assert!(!chains.is_empty());
1487 let first = &chains[0];
1488 assert_eq!(first[0], "a");
1489 assert_eq!(*first.last().expect("test: should succeed"), "b");
1490 }
1491
1492 #[test]
1493 fn find_chains_two_hop() {
1494 let pln = basic_net();
1495 let chains = pln.find_chains("a", "c", 4).expect("test: should succeed");
1496 assert!(!chains.is_empty());
1497 let path = &chains[0];
1499 assert!(path.contains(&"b".to_string()));
1500 }
1501
1502 #[test]
1503 fn find_chains_no_path() {
1504 let pln = basic_net();
1505 let chains = pln.find_chains("c", "a", 4).expect("test: should succeed");
1507 assert!(chains.is_empty());
1508 }
1509
1510 #[test]
1511 fn find_chains_missing_from() {
1512 let pln = basic_net();
1513 let r = pln.find_chains("missing", "a", 4);
1514 assert!(matches!(r, Err(PlnError::AtomNotFound(_))));
1515 }
1516
1517 #[test]
1518 fn find_chains_missing_to() {
1519 let pln = basic_net();
1520 let r = pln.find_chains("a", "missing", 4);
1521 assert!(matches!(r, Err(PlnError::AtomNotFound(_))));
1522 }
1523
1524 #[test]
1527 fn most_confident_top1() {
1528 let pln = basic_net();
1529 let top = pln.most_confident(1);
1530 assert_eq!(top.len(), 1);
1531 assert_eq!(top[0].id, "a"); }
1533
1534 #[test]
1535 fn most_confident_all() {
1536 let pln = basic_net();
1537 let top = pln.most_confident(10);
1538 assert_eq!(top.len(), 3); }
1540
1541 #[test]
1542 fn most_confident_zero() {
1543 let pln = basic_net();
1544 let top = pln.most_confident(0);
1545 assert!(top.is_empty());
1546 }
1547
1548 #[test]
1549 fn most_confident_sorted() {
1550 let pln = basic_net();
1551 let top = pln.most_confident(3);
1552 for i in 0..top.len() - 1 {
1553 assert!(top[i].tv.confidence >= top[i + 1].tv.confidence);
1554 }
1555 }
1556
1557 #[test]
1560 fn uncertain_atoms_basic() {
1561 let pln = basic_net();
1562 let uncertain = pln.uncertain_atoms(1.0);
1564 assert_eq!(uncertain.len(), 3);
1565 }
1566
1567 #[test]
1568 fn uncertain_atoms_high_threshold() {
1569 let pln = basic_net();
1570 let uncertain = pln.uncertain_atoms(0.75);
1572 assert_eq!(uncertain.len(), 1);
1573 assert_eq!(uncertain[0].id, "c");
1574 }
1575
1576 #[test]
1577 fn uncertain_atoms_none() {
1578 let pln = basic_net();
1579 let uncertain = pln.uncertain_atoms(0.0);
1580 assert!(uncertain.is_empty());
1581 }
1582
1583 #[test]
1586 fn stats_basic() {
1587 let pln = basic_net();
1588 let s = pln.stats();
1589 assert_eq!(s.atom_count, 3);
1590 assert_eq!(s.link_count, 2);
1591 assert_eq!(s.inferences_performed, 0);
1592 assert!(s.avg_confidence > 0.0);
1593 }
1594
1595 #[test]
1596 fn stats_inferences_count() {
1597 let mut pln = basic_net();
1598 pln.infer(PlnInferenceRule::Negation, vec!["a".into()])
1599 .expect("test: should succeed");
1600 pln.infer(PlnInferenceRule::Negation, vec!["b".into()])
1601 .expect("test: should succeed");
1602 assert_eq!(pln.stats().inferences_performed, 2);
1603 }
1604
1605 #[test]
1606 fn stats_avg_confidence_empty() {
1607 let pln = ProbabilisticLogicNetwork::new(PlnConfig::default());
1608 let s = pln.stats();
1609 assert_eq!(s.avg_confidence, 0.0);
1610 }
1611
1612 #[test]
1615 fn infer_below_threshold_rejected() {
1616 let cfg = PlnConfig {
1617 min_confidence_threshold: 0.99,
1618 ..Default::default()
1619 };
1620 let mut pln = ProbabilisticLogicNetwork::new(cfg);
1621 pln.add_atom(concept("a", 0.5, 0.1))
1623 .expect("test: should succeed");
1624 let r = pln.infer(PlnInferenceRule::Negation, vec!["a".into()]);
1625 assert!(matches!(r, Err(PlnError::InsufficientConfidence(_))));
1626 }
1627
1628 #[test]
1631 fn random_atoms_and_queries() {
1632 let mut state: u64 = 0xDEAD_BEEF_CAFE_BABE;
1633 let mut pln = ProbabilisticLogicNetwork::new(PlnConfig::default());
1634
1635 for i in 0..20u32 {
1637 let s = rng_f64(&mut state);
1638 let c = rng_f64(&mut state);
1639 let id = format!("node_{i}");
1640 pln.add_atom(concept(&id, s, c))
1641 .expect("test: should succeed");
1642 }
1643
1644 let s = pln.stats();
1645 assert_eq!(s.atom_count, 20);
1646 assert!(s.avg_confidence >= 0.0);
1647 assert!(s.avg_confidence <= 1.0);
1648 }
1649
1650 #[test]
1653 fn tv_display() {
1654 let tv = TruthValue::new(0.75, 0.5);
1655 let s = format!("{tv}");
1656 assert!(s.contains("0.7500"));
1657 assert!(s.contains("0.5000"));
1658 }
1659
1660 #[test]
1661 fn rule_display() {
1662 assert_eq!(format!("{}", PlnInferenceRule::Deduction), "Deduction");
1663 assert_eq!(format!("{}", PlnInferenceRule::ModusPonens), "ModusPonens");
1664 }
1665
1666 #[test]
1669 fn deduction_sb_near_one_no_panic() {
1670 let ab = TruthValue::new(0.9, 0.8);
1671 let bc = TruthValue::new(0.8, 0.7);
1672 let b = TruthValue::new(0.9999999, 0.9);
1673 let c = TruthValue::new(0.9, 0.8);
1674 let ac = TruthValue::deduction(ab, bc, b, c);
1675 assert!((0.0..=1.0).contains(&ac.strength));
1676 }
1677
1678 #[test]
1679 fn revision_with_zero_confidence_atoms() {
1680 let a = TruthValue::new(0.8, 0.0);
1681 let b = TruthValue::new(0.3, 0.0);
1682 let rev = a.revise(b);
1683 assert_eq!(rev.strength, 0.5);
1685 }
1686
1687 #[test]
1688 fn conjunction_chain_three() {
1689 let a = TruthValue::new(0.9, 0.8);
1690 let b = TruthValue::new(0.8, 0.7);
1691 let c = TruthValue::new(0.7, 0.6);
1692 let abc = a.conjunction(b).conjunction(c);
1693 assert!((abc.strength - (0.9 * 0.8 * 0.7)).abs() < 1e-8);
1694 assert_eq!(abc.confidence, 0.6);
1695 }
1696
1697 #[test]
1698 fn find_chains_self_loop_not_followed() {
1699 let mut pln = ProbabilisticLogicNetwork::new(PlnConfig::default());
1700 pln.add_atom(concept("x", 0.5, 0.5))
1701 .expect("test: should succeed");
1702 let chains = pln.find_chains("x", "x", 0).expect("test: should succeed");
1704 assert!(chains.is_empty());
1706 }
1707}