1use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
36
37#[inline]
43pub fn abr_xorshift64(state: &mut u64) -> u64 {
44 let mut x = *state;
45 x ^= x << 13;
46 x ^= x >> 7;
47 x ^= x << 17;
48 *state = x;
49 x
50}
51
52#[inline]
54pub fn fnv1a_64(data: &[u8]) -> u64 {
55 let mut h: u64 = 14_695_981_039_346_656_037;
56 for &b in data {
57 h ^= b as u64;
58 h = h.wrapping_mul(1_099_511_628_211);
59 }
60 h
61}
62
63pub type HypothesisId = u64;
69
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
76pub struct AbrTerm {
77 pub predicate: String,
79 pub args: Vec<String>,
81}
82
83impl AbrTerm {
84 pub fn new(predicate: impl Into<String>, args: Vec<impl Into<String>>) -> Self {
86 Self {
87 predicate: predicate.into(),
88 args: args.into_iter().map(|a| a.into()).collect(),
89 }
90 }
91
92 pub fn prop(predicate: impl Into<String>) -> Self {
94 Self {
95 predicate: predicate.into(),
96 args: Vec::new(),
97 }
98 }
99
100 fn canonical(&self) -> String {
102 if self.args.is_empty() {
103 self.predicate.clone()
104 } else {
105 format!("{}({})", self.predicate, self.args.join(","))
106 }
107 }
108
109 pub fn fingerprint(&self) -> u64 {
111 fnv1a_64(self.canonical().as_bytes())
112 }
113
114 pub fn matches(&self, other: &AbrTerm) -> bool {
117 if self.predicate != other.predicate {
118 return false;
119 }
120 if self.args.len() != other.args.len() {
121 return false;
122 }
123 self.args
124 .iter()
125 .zip(other.args.iter())
126 .all(|(a, b)| a == "_" || b == "_" || a == b)
127 }
128}
129
130#[derive(Debug, Clone)]
136pub struct AbrHypothesis {
137 pub id: HypothesisId,
139 pub term: AbrTerm,
141 pub cost: f64,
143 pub is_abducible: bool,
146}
147
148impl AbrHypothesis {
149 fn new(id: HypothesisId, term: AbrTerm, cost: f64, is_abducible: bool) -> Self {
150 Self {
151 id,
152 term,
153 cost: cost.max(0.0),
154 is_abducible,
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
165pub struct AbrRule {
166 pub head: AbrTerm,
168 pub body: Vec<AbrTerm>,
170 pub confidence: f64,
172}
173
174impl AbrRule {
175 fn new(head: AbrTerm, body: Vec<AbrTerm>, confidence: f64) -> Self {
176 Self {
177 head,
178 body,
179 confidence: confidence.clamp(0.0, 1.0),
180 }
181 }
182}
183
184#[derive(Debug, Clone)]
190pub enum AbrCostFunction {
191 SumCost,
193 MaxCost,
195 CountCost,
197 WeightedCost(HashMap<HypothesisId, f64>),
199}
200
201impl AbrCostFunction {
202 pub fn compute(
204 &self,
205 ids: &[HypothesisId],
206 hypotheses: &HashMap<HypothesisId, AbrHypothesis>,
207 ) -> f64 {
208 match self {
209 AbrCostFunction::SumCost => ids
210 .iter()
211 .filter_map(|id| hypotheses.get(id).map(|h| h.cost))
212 .sum(),
213 AbrCostFunction::MaxCost => ids
214 .iter()
215 .filter_map(|id| hypotheses.get(id).map(|h| h.cost))
216 .fold(0.0_f64, f64::max),
217 AbrCostFunction::CountCost => ids.len() as f64,
218 AbrCostFunction::WeightedCost(weights) => ids
219 .iter()
220 .filter_map(|id| {
221 hypotheses.get(id).map(|h| {
222 let w = weights.get(id).copied().unwrap_or(1.0);
223 h.cost * w
224 })
225 })
226 .sum(),
227 }
228 }
229}
230
231#[derive(Debug, Clone)]
237pub struct AbrEngineConfig {
238 pub max_explanations: usize,
240 pub max_hypothesis_set_size: usize,
242 pub cost_function: AbrCostFunction,
244 pub prefer_minimal: bool,
246 pub max_search_nodes: usize,
248}
249
250impl Default for AbrEngineConfig {
251 fn default() -> Self {
252 Self {
253 max_explanations: 10,
254 max_hypothesis_set_size: 8,
255 cost_function: AbrCostFunction::SumCost,
256 prefer_minimal: true,
257 max_search_nodes: 100_000,
258 }
259 }
260}
261
262#[derive(Debug, Clone)]
268pub struct AbrExplanation {
269 pub hypotheses: Vec<HypothesisId>,
271 pub covered: Vec<AbrTerm>,
273 pub total_cost: f64,
275 pub completeness: f64,
277}
278
279impl AbrExplanation {
280 pub fn is_complete(&self, n_observations: usize) -> bool {
282 self.covered.len() >= n_observations
283 }
284}
285
286#[derive(Debug, Clone)]
292pub struct AbrExplanationRecord {
293 pub timestamp_ms: u64,
295 pub n_observations: usize,
297 pub n_hypotheses_tried: u64,
299 pub best_cost: f64,
301}
302
303#[derive(Debug, Clone)]
309pub struct AbrReasoningStats {
310 pub abduce_calls: u64,
312 pub total_nodes_explored: u64,
314 pub total_explanations_found: u64,
316 pub n_hypotheses: usize,
318 pub n_rules: usize,
320 pub n_observations: usize,
322 pub history_len: usize,
324 pub best_cost_ever: f64,
326}
327
328#[derive(Debug, Clone)]
334struct SearchNode {
335 chosen: Vec<HypothesisId>,
337 next_idx: usize,
339 cost_so_far: f64,
341}
342
343#[derive(Debug, Clone)]
345struct MinHeapNode {
346 neg_cost: i64, node: SearchNode,
348}
349
350impl PartialEq for MinHeapNode {
351 fn eq(&self, other: &Self) -> bool {
352 self.neg_cost == other.neg_cost
353 }
354}
355impl Eq for MinHeapNode {}
356impl PartialOrd for MinHeapNode {
357 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
358 Some(self.cmp(other))
359 }
360}
361impl Ord for MinHeapNode {
362 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
363 self.neg_cost.cmp(&other.neg_cost)
364 }
365}
366
367impl MinHeapNode {
368 fn new(node: SearchNode) -> Self {
369 let neg_cost = -(node.cost_so_far * 1_000_000.0) as i64;
370 Self { neg_cost, node }
371 }
372}
373
374pub struct AbductiveReasoningEngine {
393 hypotheses: HashMap<HypothesisId, AbrHypothesis>,
394 rules: Vec<AbrRule>,
395 observations: Vec<AbrTerm>,
396 history: VecDeque<AbrExplanationRecord>,
397 config: AbrEngineConfig,
398 next_id: u64,
400 abduce_calls: u64,
402 total_nodes_explored: u64,
403 total_explanations_found: u64,
404 best_cost_ever: f64,
405 rng_state: u64,
407}
408
409impl AbductiveReasoningEngine {
410 pub fn new(config: AbrEngineConfig) -> Self {
414 Self {
415 hypotheses: HashMap::new(),
416 rules: Vec::new(),
417 observations: Vec::new(),
418 history: VecDeque::with_capacity(200),
419 config,
420 next_id: 1,
421 abduce_calls: 0,
422 total_nodes_explored: 0,
423 total_explanations_found: 0,
424 best_cost_ever: f64::INFINITY,
425 rng_state: 0xDEAD_BEEF_CAFE_BABEu64,
426 }
427 }
428
429 pub fn default_engine() -> Self {
431 Self::new(AbrEngineConfig::default())
432 }
433
434 pub fn add_hypothesis(&mut self, term: AbrTerm, cost: f64, is_abducible: bool) -> HypothesisId {
438 let id = self.fresh_id();
439 self.hypotheses
440 .insert(id, AbrHypothesis::new(id, term, cost, is_abducible));
441 id
442 }
443
444 pub fn add_rule(&mut self, head: AbrTerm, body: Vec<AbrTerm>, confidence: f64) {
446 self.rules.push(AbrRule::new(head, body, confidence));
447 }
448
449 pub fn add_observation(&mut self, term: AbrTerm) {
451 self.observations.push(term);
452 }
453
454 pub fn clear_observations(&mut self) {
456 self.observations.clear();
457 }
458
459 pub fn set_config(&mut self, config: AbrEngineConfig) {
461 self.config = config;
462 }
463
464 pub fn remove_hypothesis(&mut self, id: HypothesisId) -> bool {
466 self.hypotheses.remove(&id).is_some()
467 }
468
469 pub fn abduce(&mut self) -> Vec<AbrExplanation> {
477 self.abduce_calls += 1;
478
479 if self.observations.is_empty() {
480 let expl = AbrExplanation {
482 hypotheses: Vec::new(),
483 covered: Vec::new(),
484 total_cost: 0.0,
485 completeness: 1.0,
486 };
487 self.record_history(0, 0, 0.0);
488 return vec![expl];
489 }
490
491 let mut abducibles: Vec<HypothesisId> = self
493 .hypotheses
494 .values()
495 .filter(|h| h.is_abducible)
496 .map(|h| h.id)
497 .collect();
498 abducibles.sort_by(|a, b| {
499 let ca = self.hypotheses[a].cost;
500 let cb = self.hypotheses[b].cost;
501 ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
502 });
503
504 let n_obs = self.observations.len();
505 let max_set_size = self.config.max_hypothesis_set_size;
506 let max_nodes = self.config.max_search_nodes;
507 let max_expls = self.config.max_explanations;
508 let prefer_min = self.config.prefer_minimal;
509
510 let mut best_cost: f64 = f64::INFINITY;
511 let mut explanations: Vec<AbrExplanation> = Vec::new();
512 let mut seen_fingerprints: HashSet<u64> = HashSet::new();
513 let mut nodes_explored: u64 = 0;
514
515 let mut queue: BinaryHeap<MinHeapNode> = BinaryHeap::new();
517 queue.push(MinHeapNode::new(SearchNode {
518 chosen: Vec::new(),
519 next_idx: 0,
520 cost_so_far: 0.0,
521 }));
522
523 while let Some(wrapper) = queue.pop() {
524 if nodes_explored >= max_nodes as u64 {
525 break;
526 }
527 nodes_explored += 1;
528
529 let node = wrapper.node;
530
531 let derived = self.apply_rules_for_set(&node.chosen);
533 let covered = self.covered_observations(&node.chosen, &derived);
534
535 if covered.len() == n_obs {
536 let cost = self
538 .config
539 .cost_function
540 .compute(&node.chosen, &self.hypotheses);
541 let fp = set_fingerprint(&node.chosen);
542
543 if !seen_fingerprints.contains(&fp) {
544 seen_fingerprints.insert(fp);
545
546 let accept = if prefer_min {
548 cost <= best_cost + 1e-9
549 } else {
550 true
551 };
552
553 if accept {
554 if cost < best_cost {
555 best_cost = cost;
556 if prefer_min {
558 explanations
559 .retain(|e: &AbrExplanation| e.total_cost <= best_cost + 1e-9);
560 }
561 }
562 let expl = AbrExplanation {
563 hypotheses: node.chosen.clone(),
564 covered: covered.clone(),
565 total_cost: cost,
566 completeness: covered.len() as f64 / n_obs as f64,
567 };
568 explanations.push(expl);
569 if explanations.len() >= max_expls {
570 break;
571 }
572 }
573 }
574 continue;
576 }
577
578 for (branch_idx, &hid) in abducibles.iter().enumerate().skip(node.next_idx) {
580 if node.chosen.len() + 1 > max_set_size {
582 break;
583 }
584
585 if node.chosen.contains(&hid) {
587 continue;
588 }
589
590 let hcost = self.hypotheses.get(&hid).map_or(0.0, |h| h.cost);
591 let new_cost = match &self.config.cost_function {
592 AbrCostFunction::SumCost | AbrCostFunction::WeightedCost(_) => {
593 node.cost_so_far + hcost
594 }
595 AbrCostFunction::MaxCost => node.cost_so_far.max(hcost),
596 AbrCostFunction::CountCost => node.cost_so_far + 1.0,
597 };
598
599 if new_cost > best_cost + 1e-9 {
601 continue;
602 }
603
604 let mut new_chosen = node.chosen.clone();
605 new_chosen.push(hid);
606
607 queue.push(MinHeapNode::new(SearchNode {
608 chosen: new_chosen,
609 next_idx: branch_idx + 1,
610 cost_so_far: new_cost,
611 }));
612 }
613 }
614
615 self.total_nodes_explored += nodes_explored;
616 self.total_explanations_found += explanations.len() as u64;
617
618 if let Some(best) = explanations.iter().map(|e| e.total_cost).reduce(f64::min) {
620 if best < self.best_cost_ever {
621 self.best_cost_ever = best;
622 }
623 }
624
625 let best_found = explanations
627 .iter()
628 .map(|e| e.total_cost)
629 .fold(f64::INFINITY, f64::min);
630 self.record_history(n_obs, nodes_explored, best_found);
631
632 explanations.sort_by(|a, b| {
634 a.total_cost
635 .partial_cmp(&b.total_cost)
636 .unwrap_or(std::cmp::Ordering::Equal)
637 .then_with(|| a.hypotheses.len().cmp(&b.hypotheses.len()))
638 });
639
640 explanations
641 }
642
643 pub fn best_explanation(&mut self) -> Option<AbrExplanation> {
645 let mut all = self.abduce();
646 if all.is_empty() {
647 None
648 } else {
649 Some(all.remove(0))
650 }
651 }
652
653 pub fn covers(&self, hypothesis_set: &[HypothesisId]) -> Vec<AbrTerm> {
655 let derived = self.apply_rules_for_set(hypothesis_set);
656 self.covered_observations(hypothesis_set, &derived)
657 }
658
659 pub fn is_consistent(&self, hypothesis_set: &[HypothesisId]) -> bool {
665 let terms: Vec<&AbrTerm> = hypothesis_set
666 .iter()
667 .filter_map(|id| self.hypotheses.get(id).map(|h| &h.term))
668 .collect();
669
670 for (i, t) in terms.iter().enumerate() {
671 for t2 in &terms[i + 1..] {
672 if self.contradicts(t, t2) {
673 return false;
674 }
675 }
676 }
677 true
678 }
679
680 pub fn apply_rules(&self) -> Vec<AbrTerm> {
684 let all_ids: Vec<HypothesisId> = self.hypotheses.keys().copied().collect();
685 self.apply_rules_for_set(&all_ids)
686 }
687
688 pub fn reasoning_stats(&self) -> AbrReasoningStats {
690 AbrReasoningStats {
691 abduce_calls: self.abduce_calls,
692 total_nodes_explored: self.total_nodes_explored,
693 total_explanations_found: self.total_explanations_found,
694 n_hypotheses: self.hypotheses.len(),
695 n_rules: self.rules.len(),
696 n_observations: self.observations.len(),
697 history_len: self.history.len(),
698 best_cost_ever: self.best_cost_ever,
699 }
700 }
701
702 pub fn hypothesis(&self, id: HypothesisId) -> Option<&AbrHypothesis> {
706 self.hypotheses.get(&id)
707 }
708
709 pub fn hypothesis_ids(&self) -> Vec<HypothesisId> {
711 self.hypotheses.keys().copied().collect()
712 }
713
714 pub fn observations(&self) -> &[AbrTerm] {
716 &self.observations
717 }
718
719 pub fn rules(&self) -> &[AbrRule] {
721 &self.rules
722 }
723
724 pub fn history(&self) -> &VecDeque<AbrExplanationRecord> {
726 &self.history
727 }
728
729 fn fresh_id(&mut self) -> HypothesisId {
733 let x = abr_xorshift64(&mut self.rng_state);
735 let id = fnv1a_64(&(self.next_id ^ x).to_le_bytes());
736 self.next_id += 1;
737 id
738 }
739
740 fn apply_rules_for_set(&self, hypothesis_set: &[HypothesisId]) -> Vec<AbrTerm> {
744 let mut known: HashSet<u64> = hypothesis_set
746 .iter()
747 .filter_map(|id| self.hypotheses.get(id))
748 .map(|h| h.term.fingerprint())
749 .collect();
750
751 let mut known_terms: Vec<AbrTerm> = hypothesis_set
752 .iter()
753 .filter_map(|id| self.hypotheses.get(id))
754 .map(|h| h.term.clone())
755 .collect();
756
757 loop {
759 let before = known.len();
760 for rule in &self.rules {
761 let body_satisfied = rule
763 .body
764 .iter()
765 .all(|bt| known_terms.iter().any(|kt| kt.matches(bt)));
766 if body_satisfied {
767 let fp = rule.head.fingerprint();
768 if !known.contains(&fp) {
769 known.insert(fp);
770 known_terms.push(rule.head.clone());
771 }
772 }
773 }
774 if known.len() == before {
775 break;
776 }
777 }
778
779 let seed_fps: HashSet<u64> = hypothesis_set
781 .iter()
782 .filter_map(|id| self.hypotheses.get(id))
783 .map(|h| h.term.fingerprint())
784 .collect();
785
786 known_terms
787 .into_iter()
788 .filter(|t| !seed_fps.contains(&t.fingerprint()))
789 .collect()
790 }
791
792 fn covered_observations(
794 &self,
795 hypothesis_set: &[HypothesisId],
796 derived: &[AbrTerm],
797 ) -> Vec<AbrTerm> {
798 let mut covered = Vec::new();
799 for obs in &self.observations {
800 let direct = hypothesis_set
802 .iter()
803 .filter_map(|id| self.hypotheses.get(id))
804 .any(|h| h.term.matches(obs));
805
806 let indirect = derived.iter().any(|d| d.matches(obs));
808
809 if direct || indirect {
810 covered.push(obs.clone());
811 }
812 }
813 covered
814 }
815
816 fn contradicts(&self, a: &AbrTerm, b: &AbrTerm) -> bool {
818 let negation_of = |pos: &str, neg: &str| -> bool {
820 neg == format!("not_{}", pos) || neg == format!("NOT:{}", pos)
821 };
822 if a.args == b.args
823 && (negation_of(&a.predicate, &b.predicate) || negation_of(&b.predicate, &a.predicate))
824 {
825 return true;
826 }
827 if a.predicate.starts_with("NOT:") {
829 let pos_pred = &a.predicate["NOT:".len()..];
830 if b.predicate == pos_pred && a.args == b.args {
831 return true;
832 }
833 }
834 if b.predicate.starts_with("NOT:") {
835 let pos_pred = &b.predicate["NOT:".len()..];
836 if a.predicate == pos_pred && a.args == b.args {
837 return true;
838 }
839 }
840 false
841 }
842
843 fn record_history(&mut self, n_obs: usize, n_tried: u64, best_cost: f64) {
845 let ts = abr_xorshift64(&mut self.rng_state) % 1_700_000_000_000;
847 if self.history.len() >= 200 {
848 self.history.pop_front();
849 }
850 self.history.push_back(AbrExplanationRecord {
851 timestamp_ms: ts,
852 n_observations: n_obs,
853 n_hypotheses_tried: n_tried,
854 best_cost,
855 });
856 }
857}
858
859pub fn set_fingerprint(ids: &[HypothesisId]) -> u64 {
866 let mut sorted: Vec<HypothesisId> = ids.to_vec();
867 sorted.sort_unstable();
868 let mut buf: Vec<u8> = Vec::with_capacity(sorted.len() * 8);
869 for id in &sorted {
870 buf.extend_from_slice(&id.to_le_bytes());
871 }
872 fnv1a_64(&buf)
873}
874
875pub type AbrAbductiveReasoningEngine = AbductiveReasoningEngine;
881
882#[cfg(test)]
887mod tests {
888 use super::*;
889
890 fn prop(p: &str) -> AbrTerm {
893 AbrTerm::prop(p)
894 }
895 fn term(p: &str, args: &[&str]) -> AbrTerm {
896 AbrTerm::new(p, args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
897 }
898 fn engine() -> AbductiveReasoningEngine {
899 AbductiveReasoningEngine::new(AbrEngineConfig {
900 max_explanations: 20,
901 max_hypothesis_set_size: 6,
902 cost_function: AbrCostFunction::SumCost,
903 prefer_minimal: true,
904 max_search_nodes: 50_000,
905 })
906 }
907
908 #[test]
910 fn t01_new_engine_empty() {
911 let eng = engine();
912 assert_eq!(eng.hypotheses.len(), 0);
913 assert_eq!(eng.rules.len(), 0);
914 assert_eq!(eng.observations.len(), 0);
915 }
916
917 #[test]
919 fn t02_hypothesis_ids_distinct() {
920 let mut eng = engine();
921 let id1 = eng.add_hypothesis(prop("a"), 1.0, true);
922 let id2 = eng.add_hypothesis(prop("b"), 1.0, true);
923 assert_ne!(id1, id2);
924 }
925
926 #[test]
928 fn t03_hypothesis_retrieval() {
929 let mut eng = engine();
930 let id = eng.add_hypothesis(prop("rain"), 2.5, true);
931 let h = eng.hypothesis(id).expect("should find hypothesis");
932 assert_eq!(h.term.predicate, "rain");
933 assert!((h.cost - 2.5).abs() < 1e-9);
934 assert!(h.is_abducible);
935 }
936
937 #[test]
939 fn t04_non_abducible_ignored() {
940 let mut eng = engine();
941 eng.add_hypothesis(prop("rain"), 1.0, false); eng.add_observation(prop("rain"));
943 let expls = eng.abduce();
944 assert!(expls.is_empty() || expls[0].completeness < 1.0);
946 }
947
948 #[test]
950 fn t05_single_hyp_covers_obs() {
951 let mut eng = engine();
952 let _id = eng.add_hypothesis(prop("rain"), 1.0, true);
953 eng.add_observation(prop("rain"));
954 let expls = eng.abduce();
955 assert!(!expls.is_empty());
956 assert_eq!(expls[0].completeness, 1.0);
957 }
958
959 #[test]
961 fn t06_minimal_set_preferred() {
962 let mut eng = engine();
963 let id1 = eng.add_hypothesis(prop("rain"), 1.0, true);
964 let _id2 = eng.add_hypothesis(prop("sun"), 5.0, true);
965 eng.add_observation(prop("rain"));
966 let expls = eng.abduce();
967 assert!(!expls.is_empty());
968 assert!(expls[0].hypotheses.contains(&id1));
970 assert_eq!(expls[0].hypotheses.len(), 1);
971 }
972
973 #[test]
975 fn t07_sum_cost() {
976 let mut eng = engine();
977 let id1 = eng.add_hypothesis(prop("a"), 2.0, true);
978 let id2 = eng.add_hypothesis(prop("b"), 3.0, true);
979 eng.add_observation(prop("a"));
980 eng.add_observation(prop("b"));
981 let expls = eng.abduce();
982 assert!(!expls.is_empty());
983 let best = &expls[0];
984 assert!((best.total_cost - 5.0).abs() < 1e-6);
985 assert!(best.hypotheses.contains(&id1));
986 assert!(best.hypotheses.contains(&id2));
987 }
988
989 #[test]
991 fn t08_max_cost() {
992 let mut eng = AbductiveReasoningEngine::new(AbrEngineConfig {
993 cost_function: AbrCostFunction::MaxCost,
994 ..AbrEngineConfig::default()
995 });
996 eng.add_hypothesis(prop("a"), 2.0, true);
997 eng.add_hypothesis(prop("b"), 3.0, true);
998 eng.add_observation(prop("a"));
999 eng.add_observation(prop("b"));
1000 let expls = eng.abduce();
1001 assert!(!expls.is_empty());
1002 assert!((expls[0].total_cost - 3.0).abs() < 1e-6);
1004 }
1005
1006 #[test]
1008 fn t09_count_cost() {
1009 let mut eng = AbductiveReasoningEngine::new(AbrEngineConfig {
1010 cost_function: AbrCostFunction::CountCost,
1011 ..AbrEngineConfig::default()
1012 });
1013 eng.add_hypothesis(prop("a"), 99.0, true);
1014 eng.add_hypothesis(prop("b"), 0.1, true);
1015 eng.add_observation(prop("a"));
1016 eng.add_observation(prop("b"));
1017 let expls = eng.abduce();
1018 assert!(!expls.is_empty());
1019 assert!((expls[0].total_cost - 2.0).abs() < 1e-6);
1021 }
1022
1023 #[test]
1025 fn t10_weighted_cost() {
1026 let mut eng = engine();
1027 let id1 = eng.add_hypothesis(prop("x"), 1.0, true);
1028 let id2 = eng.add_hypothesis(prop("y"), 1.0, true);
1029 let mut weights = HashMap::new();
1030 weights.insert(id1, 3.0);
1031 weights.insert(id2, 0.5);
1032 eng.set_config(AbrEngineConfig {
1033 cost_function: AbrCostFunction::WeightedCost(weights),
1034 max_explanations: 10,
1035 max_hypothesis_set_size: 6,
1036 prefer_minimal: true,
1037 max_search_nodes: 50_000,
1038 });
1039 eng.add_observation(prop("x"));
1040 eng.add_observation(prop("y"));
1041 let expls = eng.abduce();
1042 assert!(!expls.is_empty());
1043 assert!((expls[0].total_cost - 3.5).abs() < 1e-6);
1045 }
1046
1047 #[test]
1049 fn t11_rule_derived_coverage() {
1050 let mut eng = engine();
1051 let id_rain = eng.add_hypothesis(prop("rain"), 1.0, true);
1052 eng.add_rule(prop("wet_grass"), vec![prop("rain")], 1.0);
1054 eng.add_observation(prop("wet_grass"));
1055 let expls = eng.abduce();
1056 assert!(!expls.is_empty());
1057 assert_eq!(expls[0].completeness, 1.0);
1058 assert!(expls[0].hypotheses.contains(&id_rain));
1059 }
1060
1061 #[test]
1063 fn t12_rule_chain() {
1064 let mut eng = engine();
1065 let id = eng.add_hypothesis(prop("cloudy"), 1.0, true);
1066 eng.add_rule(prop("rain"), vec![prop("cloudy")], 1.0);
1067 eng.add_rule(prop("wet_grass"), vec![prop("rain")], 1.0);
1068 eng.add_observation(prop("wet_grass"));
1069 let expls = eng.abduce();
1070 assert!(!expls.is_empty());
1071 assert!(expls[0].hypotheses.contains(&id));
1072 }
1073
1074 #[test]
1076 fn t13_covers() {
1077 let mut eng = engine();
1078 let id_r = eng.add_hypothesis(prop("rain"), 1.0, true);
1079 let _id_s = eng.add_hypothesis(prop("sun"), 1.0, true);
1080 eng.add_rule(prop("wet"), vec![prop("rain")], 1.0);
1081 eng.add_observation(prop("wet"));
1082 eng.add_observation(prop("sun"));
1083 let covered = eng.covers(&[id_r]);
1084 assert!(covered.iter().any(|t| t.predicate == "wet"));
1086 assert!(!covered.iter().any(|t| t.predicate == "sun"));
1087 }
1088
1089 #[test]
1091 fn t14_consistent_set() {
1092 let mut eng = engine();
1093 let id1 = eng.add_hypothesis(prop("a"), 1.0, true);
1094 let id2 = eng.add_hypothesis(prop("b"), 1.0, true);
1095 assert!(eng.is_consistent(&[id1, id2]));
1096 }
1097
1098 #[test]
1100 fn t15_inconsistent_set_not_prefix() {
1101 let mut eng = engine();
1102 let id1 = eng.add_hypothesis(prop("rain"), 1.0, true);
1103 let id2 = eng.add_hypothesis(prop("not_rain"), 1.0, true);
1104 assert!(!eng.is_consistent(&[id1, id2]));
1105 }
1106
1107 #[test]
1109 fn t16_inconsistent_not_colon_prefix() {
1110 let mut eng = engine();
1111 let id1 = eng.add_hypothesis(prop("rain"), 1.0, true);
1112 let id2 = eng.add_hypothesis(prop("NOT:rain"), 1.0, true);
1113 assert!(!eng.is_consistent(&[id1, id2]));
1114 }
1115
1116 #[test]
1118 fn t17_apply_rules_global() {
1119 let mut eng = engine();
1120 eng.add_hypothesis(prop("raining"), 1.0, true);
1121 eng.add_rule(prop("puddles"), vec![prop("raining")], 1.0);
1122 let derived = eng.apply_rules();
1123 assert!(derived.iter().any(|t| t.predicate == "puddles"));
1124 }
1125
1126 #[test]
1128 fn t18_reasoning_stats() {
1129 let mut eng = engine();
1130 eng.add_hypothesis(prop("x"), 1.0, true);
1131 eng.add_observation(prop("x"));
1132 eng.abduce();
1133 let stats = eng.reasoning_stats();
1134 assert_eq!(stats.abduce_calls, 1);
1135 assert!(stats.total_explanations_found > 0);
1136 }
1137
1138 #[test]
1140 fn t19_empty_observations_vacuous() {
1141 let mut eng = engine();
1142 eng.add_hypothesis(prop("a"), 1.0, true);
1143 let expls = eng.abduce();
1144 assert!(!expls.is_empty());
1145 assert_eq!(expls[0].completeness, 1.0);
1146 assert_eq!(expls[0].total_cost, 0.0);
1147 }
1148
1149 #[test]
1151 fn t20_best_explanation() {
1152 let mut eng = engine();
1153 eng.add_hypothesis(prop("cheap"), 0.5, true);
1154 eng.add_hypothesis(prop("expensive"), 10.0, true);
1155 eng.add_observation(prop("cheap"));
1156 let best = eng.best_explanation().expect("should find one");
1157 assert!((best.total_cost - 0.5).abs() < 1e-6);
1158 }
1159
1160 #[test]
1162 fn t21_unexplainable_observation() {
1163 let mut eng = engine();
1164 eng.add_hypothesis(prop("irrelevant"), 1.0, true);
1165 eng.add_observation(prop("some_fact"));
1166 let expls = eng.abduce();
1167 let complete: Vec<_> = expls.iter().filter(|e| e.completeness >= 1.0).collect();
1169 assert!(complete.is_empty());
1170 }
1171
1172 #[test]
1174 fn t22_history_bounded() {
1175 let mut eng = engine();
1176 eng.add_hypothesis(prop("x"), 1.0, true);
1177 eng.add_observation(prop("x"));
1178 for _ in 0..250 {
1179 eng.abduce();
1180 }
1181 assert!(eng.history().len() <= 200);
1182 }
1183
1184 #[test]
1186 fn t23_remove_hypothesis() {
1187 let mut eng = engine();
1188 let id = eng.add_hypothesis(prop("x"), 1.0, true);
1189 assert!(eng.remove_hypothesis(id));
1190 assert!(!eng.remove_hypothesis(id)); assert!(eng.hypothesis(id).is_none());
1192 }
1193
1194 #[test]
1196 fn t24_clear_observations() {
1197 let mut eng = engine();
1198 eng.add_observation(prop("x"));
1199 eng.add_observation(prop("y"));
1200 eng.clear_observations();
1201 assert!(eng.observations().is_empty());
1202 }
1203
1204 #[test]
1206 fn t25_fnv1a_deterministic() {
1207 let a = fnv1a_64(b"hello");
1208 let b = fnv1a_64(b"hello");
1209 assert_eq!(a, b);
1210 assert_ne!(fnv1a_64(b"hello"), fnv1a_64(b"world"));
1211 }
1212
1213 #[test]
1215 fn t26_set_fingerprint_order_independent() {
1216 let fp1 = set_fingerprint(&[1, 2, 3]);
1217 let fp2 = set_fingerprint(&[3, 1, 2]);
1218 assert_eq!(fp1, fp2);
1219 }
1220
1221 #[test]
1223 fn t27_term_fingerprint_stable() {
1224 let t = AbrTerm::new("parent", vec!["alice", "bob"]);
1225 assert_eq!(t.fingerprint(), t.fingerprint());
1226 }
1227
1228 #[test]
1230 fn t28_term_wildcard_match() {
1231 let pattern = term("parent", &["alice", "_"]);
1232 let ground = term("parent", &["alice", "bob"]);
1233 assert!(pattern.matches(&ground));
1234 }
1235
1236 #[test]
1238 fn t29_term_no_match_predicate() {
1239 let a = prop("rain");
1240 let b = prop("sun");
1241 assert!(!a.matches(&b));
1242 }
1243
1244 #[test]
1246 fn t30_rule_construction() {
1247 let r = AbrRule::new(prop("wet"), vec![prop("rain")], 0.9);
1248 assert_eq!(r.body.len(), 1);
1249 assert!((r.confidence - 0.9).abs() < 1e-9);
1250 }
1251
1252 #[test]
1254 fn t31_rule_confidence_clamped() {
1255 let r = AbrRule::new(prop("x"), vec![], 2.5);
1256 assert!((r.confidence - 1.0).abs() < 1e-9);
1257 let r2 = AbrRule::new(prop("x"), vec![], -1.0);
1258 assert!((r2.confidence - 0.0).abs() < 1e-9);
1259 }
1260
1261 #[test]
1263 fn t32_hypothesis_cost_floored() {
1264 let h = AbrHypothesis::new(1, prop("x"), -5.0, true);
1265 assert_eq!(h.cost, 0.0);
1266 }
1267
1268 #[test]
1270 fn t33_single_hyp_multiple_observations_via_rules() {
1271 let mut eng = engine();
1272 let id = eng.add_hypothesis(prop("storm"), 1.0, true);
1273 eng.add_rule(prop("rain"), vec![prop("storm")], 1.0);
1274 eng.add_rule(prop("wind"), vec![prop("storm")], 1.0);
1275 eng.add_observation(prop("rain"));
1276 eng.add_observation(prop("wind"));
1277 let expls = eng.abduce();
1278 assert!(!expls.is_empty());
1279 assert_eq!(expls[0].completeness, 1.0);
1280 assert_eq!(expls[0].hypotheses.len(), 1);
1281 assert!(expls[0].hypotheses.contains(&id));
1282 }
1283
1284 #[test]
1286 fn t34_explanation_is_complete() {
1287 let expl = AbrExplanation {
1288 hypotheses: vec![1],
1289 covered: vec![prop("a"), prop("b")],
1290 total_cost: 2.0,
1291 completeness: 1.0,
1292 };
1293 assert!(expl.is_complete(2));
1294 assert!(!expl.is_complete(3));
1295 }
1296
1297 #[test]
1299 fn t35_xorshift64_nonzero() {
1300 let mut state = 12345u64;
1301 let v = abr_xorshift64(&mut state);
1302 assert_ne!(v, 0);
1303 }
1304
1305 #[test]
1307 fn t36_multiple_rules_same_head() {
1308 let mut eng = engine();
1309 let id1 = eng.add_hypothesis(prop("heat"), 1.0, true);
1310 let id2 = eng.add_hypothesis(prop("cold"), 1.0, true);
1311 eng.add_rule(prop("fog"), vec![prop("heat")], 1.0);
1313 eng.add_rule(prop("fog"), vec![prop("cold")], 1.0);
1314 eng.add_observation(prop("fog"));
1315 let expls = eng.abduce();
1316 assert!(!expls.is_empty());
1317 let hyp_sets: Vec<_> = expls.iter().map(|e| e.hypotheses.clone()).collect();
1319 let has_heat = hyp_sets.iter().any(|s| s == &[id1]);
1320 let has_cold = hyp_sets.iter().any(|s| s == &[id2]);
1321 assert!(has_heat || has_cold);
1322 }
1323
1324 #[test]
1326 fn t37_max_set_size() {
1327 let mut eng = AbductiveReasoningEngine::new(AbrEngineConfig {
1328 max_hypothesis_set_size: 1,
1329 max_explanations: 10,
1330 cost_function: AbrCostFunction::SumCost,
1331 prefer_minimal: true,
1332 max_search_nodes: 10_000,
1333 });
1334 eng.add_hypothesis(prop("a"), 1.0, true);
1336 eng.add_hypothesis(prop("b"), 1.0, true);
1337 eng.add_observation(prop("a"));
1338 eng.add_observation(prop("b"));
1339 let expls = eng.abduce();
1340 let complete: Vec<_> = expls.iter().filter(|e| e.completeness >= 1.0).collect();
1342 assert!(complete.is_empty());
1343 }
1344
1345 #[test]
1347 fn t38_hypothesis_ids() {
1348 let mut eng = engine();
1349 let id1 = eng.add_hypothesis(prop("a"), 1.0, true);
1350 let id2 = eng.add_hypothesis(prop("b"), 1.0, true);
1351 let ids = eng.hypothesis_ids();
1352 assert!(ids.contains(&id1));
1353 assert!(ids.contains(&id2));
1354 }
1355
1356 #[test]
1358 fn t39_rules_accessor() {
1359 let mut eng = engine();
1360 eng.add_rule(prop("x"), vec![], 1.0);
1361 assert_eq!(eng.rules().len(), 1);
1362 }
1363
1364 #[test]
1366 fn t40_observations_accessor() {
1367 let mut eng = engine();
1368 eng.add_observation(prop("a"));
1369 eng.add_observation(prop("b"));
1370 assert_eq!(eng.observations().len(), 2);
1371 }
1372
1373 #[test]
1375 fn t41_default_engine() {
1376 let eng = AbductiveReasoningEngine::default_engine();
1377 assert_eq!(eng.config.max_explanations, 10);
1378 assert_eq!(eng.config.max_hypothesis_set_size, 8);
1379 }
1380
1381 #[test]
1383 fn t42_term_canonical() {
1384 let t = AbrTerm::new("p", vec!["a", "b"]);
1385 assert_eq!(t.canonical(), "p(a,b)");
1386 let t2 = prop("q");
1387 assert_eq!(t2.canonical(), "q");
1388 }
1389
1390 #[test]
1392 fn t43_set_config() {
1393 let mut eng = engine();
1394 eng.set_config(AbrEngineConfig {
1395 max_explanations: 3,
1396 ..AbrEngineConfig::default()
1397 });
1398 assert_eq!(eng.config.max_explanations, 3);
1399 }
1400
1401 #[test]
1403 fn t44_history_grows_on_abduce() {
1404 let mut eng = engine();
1405 eng.add_hypothesis(prop("x"), 1.0, true);
1406 eng.add_observation(prop("x"));
1407 assert_eq!(eng.history().len(), 0);
1408 eng.abduce();
1409 assert_eq!(eng.history().len(), 1);
1410 }
1411
1412 #[test]
1414 fn t45_contradicts_same_args() {
1415 let eng = engine();
1416 let a = term("wet", &["lawn"]);
1417 let b = term("not_wet", &["lawn"]);
1418 let c = term("not_wet", &["floor"]); assert!(eng.contradicts(&a, &b));
1420 assert!(!eng.contradicts(&a, &c));
1421 }
1422
1423 #[test]
1425 fn t46_best_explanation_no_obs() {
1426 let mut eng = engine();
1427 eng.add_hypothesis(prop("x"), 1.0, true);
1428 let best = eng.best_explanation().expect("vacuous explanation");
1429 assert_eq!(best.completeness, 1.0);
1430 }
1431
1432 #[test]
1434 fn t47_deduplication() {
1435 let mut eng = engine();
1436 let id = eng.add_hypothesis(prop("r"), 1.0, true);
1437 eng.add_observation(prop("r"));
1438 let expls = eng.abduce();
1439 let count = expls.iter().filter(|e| e.hypotheses == vec![id]).count();
1441 assert_eq!(count, 1);
1442 }
1443
1444 #[test]
1446 fn t48_best_cost_infinity_if_none() {
1447 let eng = engine();
1448 assert_eq!(eng.best_cost_ever, f64::INFINITY);
1449 }
1450
1451 #[test]
1453 fn t49_prefer_minimal_false() {
1454 let mut eng = AbductiveReasoningEngine::new(AbrEngineConfig {
1455 prefer_minimal: false,
1456 max_explanations: 5,
1457 max_hypothesis_set_size: 4,
1458 cost_function: AbrCostFunction::SumCost,
1459 max_search_nodes: 50_000,
1460 });
1461 eng.add_hypothesis(prop("a"), 1.0, true);
1462 eng.add_observation(prop("a"));
1463 let expls = eng.abduce();
1464 assert!(!expls.is_empty());
1465 }
1466
1467 #[test]
1469 fn t50_apply_rules_no_cycle() {
1470 let mut eng = engine();
1471 eng.add_hypothesis(prop("x"), 1.0, true);
1472 eng.add_rule(prop("x"), vec![prop("x")], 1.0);
1474 let derived = eng.apply_rules();
1475 assert!(!derived.iter().any(|t| t.predicate == "x"));
1477 }
1478
1479 #[test]
1481 fn t51_conjunctive_rule_body() {
1482 let mut eng = engine();
1483 let id1 = eng.add_hypothesis(prop("a"), 1.0, true);
1484 let id2 = eng.add_hypothesis(prop("b"), 1.0, true);
1485 eng.add_rule(prop("c"), vec![prop("a"), prop("b")], 1.0);
1487 eng.add_observation(prop("c"));
1488 let expls = eng.abduce();
1489 assert!(!expls.is_empty());
1490 let best = &expls[0];
1491 assert!(best.hypotheses.contains(&id1));
1492 assert!(best.hypotheses.contains(&id2));
1493 }
1494
1495 #[test]
1497 fn t52_partial_completeness_value() {
1498 let expl = AbrExplanation {
1499 hypotheses: vec![1],
1500 covered: vec![prop("a")],
1501 total_cost: 1.0,
1502 completeness: 0.5,
1503 };
1504 assert!((expl.completeness - 0.5).abs() < 1e-9);
1505 assert!(!expl.is_complete(2));
1506 }
1507
1508 #[test]
1510 fn t53_multiple_complete_explanations() {
1511 let mut eng = AbductiveReasoningEngine::new(AbrEngineConfig {
1512 prefer_minimal: false,
1513 max_explanations: 20,
1514 max_hypothesis_set_size: 6,
1515 cost_function: AbrCostFunction::SumCost,
1516 max_search_nodes: 50_000,
1517 });
1518 let id1 = eng.add_hypothesis(prop("cause_a"), 1.0, true);
1519 let id2 = eng.add_hypothesis(prop("cause_b"), 1.0, true);
1520 eng.add_rule(prop("effect"), vec![prop("cause_a")], 1.0);
1521 eng.add_rule(prop("effect"), vec![prop("cause_b")], 1.0);
1522 eng.add_observation(prop("effect"));
1523 let expls = eng.abduce();
1524 let hyp_sets: Vec<_> = expls.iter().map(|e| e.hypotheses.clone()).collect();
1525 let has_a = hyp_sets.iter().any(|s| s == &[id1]);
1526 let has_b = hyp_sets.iter().any(|s| s == &[id2]);
1527 assert!(has_a && has_b);
1528 }
1529
1530 #[test]
1532 fn t54_reasoning_stats_fields() {
1533 let mut eng = engine();
1534 let _id = eng.add_hypothesis(prop("x"), 1.0, true);
1535 eng.add_rule(prop("y"), vec![prop("x")], 1.0);
1536 eng.add_observation(prop("x"));
1537 eng.abduce();
1538 let s = eng.reasoning_stats();
1539 assert_eq!(s.n_hypotheses, 1);
1540 assert_eq!(s.n_rules, 1);
1541 assert_eq!(s.n_observations, 1);
1542 assert_eq!(s.abduce_calls, 1);
1543 assert!(s.total_nodes_explored > 0);
1544 }
1545
1546 #[test]
1548 fn t55_explanation_record_stored() {
1549 let mut eng = engine();
1550 eng.add_hypothesis(prop("x"), 1.0, true);
1551 eng.add_observation(prop("x"));
1552 eng.abduce();
1553 let rec = eng.history().back().expect("should have record");
1554 assert_eq!(rec.n_observations, 1);
1555 assert!(rec.best_cost < f64::INFINITY);
1556 }
1557
1558 #[test]
1560 fn t56_prefer_minimal_prunes_costlier() {
1561 let mut eng = engine(); let _id_cheap = eng.add_hypothesis(prop("obs"), 1.0, true);
1563 let _id_expensive = eng.add_hypothesis(prop("obs_alt"), 100.0, true);
1564 eng.add_rule(prop("obs"), vec![prop("obs_alt")], 1.0);
1566 eng.add_observation(prop("obs"));
1567 let expls = eng.abduce();
1568 if expls.len() > 1 {
1569 let best = expls[0].total_cost;
1571 for e in &expls {
1572 assert!(e.total_cost <= best + 1e-6);
1573 }
1574 }
1575 }
1576
1577 #[test]
1579 fn t57_fingerprint_empty() {
1580 let fp = set_fingerprint(&[]);
1581 assert_eq!(fp, fnv1a_64(b""));
1583 }
1584
1585 #[test]
1587 fn t58_term_args_vs_prop_fingerprint() {
1588 let a = prop("p");
1589 let b = AbrTerm::new("p", vec!["x"]);
1590 assert_ne!(a.fingerprint(), b.fingerprint());
1591 }
1592
1593 #[test]
1595 fn t59_history_nodes_tried() {
1596 let mut eng = engine();
1597 eng.add_hypothesis(prop("a"), 1.0, true);
1598 eng.add_hypothesis(prop("b"), 1.0, true);
1599 eng.add_observation(prop("a"));
1600 eng.add_observation(prop("b"));
1601 eng.abduce();
1602 let rec = eng.history().back().expect("record");
1603 assert!(rec.n_hypotheses_tried > 0);
1605 }
1606
1607 #[test]
1609 fn t60_zero_cost_hypothesis() {
1610 let mut eng = engine();
1611 let id = eng.add_hypothesis(prop("free_fact"), 0.0, true);
1612 eng.add_observation(prop("free_fact"));
1613 let expls = eng.abduce();
1614 assert!(!expls.is_empty());
1615 assert_eq!(expls[0].total_cost, 0.0);
1616 assert!(expls[0].hypotheses.contains(&id));
1617 }
1618
1619 #[test]
1621 fn t61_best_cost_ever_updated() {
1622 let mut eng = engine();
1623 eng.add_hypothesis(prop("x"), 3.0, true);
1624 eng.add_observation(prop("x"));
1625 eng.abduce();
1626 assert!((eng.best_cost_ever - 3.0).abs() < 1e-6);
1627
1628 eng.clear_observations();
1630 let id2 = eng.add_hypothesis(prop("y"), 1.0, true);
1631 eng.add_observation(prop("y"));
1632 eng.abduce();
1633 assert!((eng.best_cost_ever - 1.0).abs() < 1e-6);
1635 let _ = id2;
1636 }
1637
1638 #[test]
1640 fn t62_empty_body_rule() {
1641 let mut eng = engine();
1642 eng.add_rule(prop("always_true"), vec![], 1.0);
1644 eng.add_observation(prop("always_true"));
1645 let derived = eng.apply_rules_for_set(&[]);
1647 assert!(derived.iter().any(|t| t.predicate == "always_true"));
1648 }
1649
1650 #[test]
1652 fn t63_total_nodes_explored_cumulative() {
1653 let mut eng = engine();
1654 eng.add_hypothesis(prop("x"), 1.0, true);
1655 eng.add_observation(prop("x"));
1656 eng.abduce();
1657 let n1 = eng.total_nodes_explored;
1658 eng.abduce();
1659 let n2 = eng.total_nodes_explored;
1660 assert!(n2 >= n1);
1661 }
1662
1663 #[test]
1665 fn t64_max_explanations_limit() {
1666 let mut eng = AbductiveReasoningEngine::new(AbrEngineConfig {
1667 max_explanations: 2,
1668 max_hypothesis_set_size: 6,
1669 cost_function: AbrCostFunction::SumCost,
1670 prefer_minimal: false,
1671 max_search_nodes: 100_000,
1672 });
1673 for i in 0..10 {
1675 let p = format!("cause{}", i);
1676 eng.add_hypothesis(AbrTerm::prop(p.clone()), 1.0, true);
1677 eng.add_rule(prop("effect"), vec![AbrTerm::prop(p)], 1.0);
1678 }
1679 eng.add_observation(prop("effect"));
1680 let expls = eng.abduce();
1681 assert!(expls.len() <= 2);
1682 }
1683}