1use std::collections::HashMap;
23use thiserror::Error;
24
25#[derive(Debug, Error, Clone, PartialEq)]
31pub enum NsError {
32 #[error("symbol not found: id {0}")]
34 SymbolNotFound(usize),
35
36 #[error("dimension mismatch: expected {expected}, got {got}")]
39 DimensionMismatch { expected: usize, got: usize },
40
41 #[error("maximum symbol count reached")]
43 MaxSymbolsReached,
44
45 #[error("invalid rule: {0}")]
48 InvalidRule(String),
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
58pub struct SymbolId(pub usize);
59
60impl std::fmt::Display for SymbolId {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 write!(f, "sym:{}", self.0)
63 }
64}
65
66#[derive(Debug, Clone)]
72pub struct Symbol {
73 pub id: SymbolId,
75 pub name: String,
77 pub embedding: Vec<f64>,
80 pub confidence: f64,
83}
84
85#[derive(Debug, Clone, PartialEq)]
88pub enum RuleType {
89 Definite,
92 Probabilistic,
95 Soft {
98 temperature: f64,
100 },
101}
102
103#[derive(Debug, Clone)]
105pub struct LogicalRule {
106 pub head: SymbolId,
108 pub body: Vec<SymbolId>,
110 pub weight: f64,
112 pub rule_type: RuleType,
115}
116
117#[derive(Debug, Clone, PartialEq)]
124pub enum InferenceMode {
125 PureSymbolic,
127 PureNeural,
129 Hybrid {
132 neural_weight: f64,
134 },
135}
136
137#[derive(Debug, Clone)]
140pub struct NsQuery {
141 pub target: SymbolId,
143 pub evidence: Vec<(SymbolId, f64)>,
145 pub mode: InferenceMode,
147}
148
149#[derive(Debug, Clone)]
151pub struct NsResult {
152 pub symbol: SymbolId,
154 pub confidence: f64,
156 pub explanation: Vec<String>,
159 pub neural_contribution: f64,
161 pub symbolic_contribution: f64,
163}
164
165#[derive(Debug, Clone)]
171pub struct IntegratorConfig {
172 pub embedding_dim: usize,
174 pub max_symbols: usize,
176 pub inference_depth: usize,
178 pub similarity_threshold: f64,
181}
182
183impl Default for IntegratorConfig {
184 fn default() -> Self {
185 Self {
186 embedding_dim: 128,
187 max_symbols: 10_000,
188 inference_depth: 5,
189 similarity_threshold: 0.7,
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq)]
200pub struct NsStats {
201 pub symbol_count: usize,
203 pub rule_count: usize,
205 pub total_inferences: u64,
207 pub avg_embedding_norm: f64,
210}
211
212pub struct NeuralSymbolicIntegrator {
228 pub config: IntegratorConfig,
230 pub symbols: Vec<Symbol>,
232 pub rules: Vec<LogicalRule>,
234 pub total_inferences: u64,
236}
237
238impl NeuralSymbolicIntegrator {
239 pub fn new(config: IntegratorConfig) -> Self {
245 Self {
246 config,
247 symbols: Vec::new(),
248 rules: Vec::new(),
249 total_inferences: 0,
250 }
251 }
252
253 pub fn add_symbol(
267 &mut self,
268 name: String,
269 embedding: Vec<f64>,
270 confidence: f64,
271 ) -> Result<SymbolId, NsError> {
272 if self.symbols.len() >= self.config.max_symbols {
273 return Err(NsError::MaxSymbolsReached);
274 }
275 if embedding.len() != self.config.embedding_dim {
276 return Err(NsError::DimensionMismatch {
277 expected: self.config.embedding_dim,
278 got: embedding.len(),
279 });
280 }
281 let id = SymbolId(self.symbols.len());
282 self.symbols.push(Symbol {
283 id,
284 name,
285 embedding,
286 confidence: confidence.clamp(0.0, 1.0),
287 });
288 Ok(id)
289 }
290
291 pub fn add_rule(&mut self, rule: LogicalRule) -> Result<(), NsError> {
298 if rule.head.0 >= self.symbols.len() {
299 return Err(NsError::InvalidRule(format!(
300 "head symbol {} does not exist",
301 rule.head.0
302 )));
303 }
304 for &body_sym in &rule.body {
305 if body_sym.0 >= self.symbols.len() {
306 return Err(NsError::InvalidRule(format!(
307 "body symbol {} does not exist",
308 body_sym.0
309 )));
310 }
311 }
312 self.rules.push(rule);
313 Ok(())
314 }
315
316 pub fn neural_similarity(a: &[f64], b: &[f64]) -> f64 {
325 debug_assert_eq!(a.len(), b.len(), "embedding dimension mismatch");
326 let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
327 let norm_a: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
328 let norm_b: f64 = b.iter().map(|x| x * x).sum::<f64>().sqrt();
329 if norm_a == 0.0 || norm_b == 0.0 {
330 return 0.0;
331 }
332 (dot / (norm_a * norm_b)).clamp(-1.0, 1.0)
333 }
334
335 pub fn symbolic_forward_chain(
352 &self,
353 target: SymbolId,
354 evidence: &[(SymbolId, f64)],
355 depth: usize,
356 ) -> f64 {
357 let ev_map: HashMap<usize, f64> = evidence.iter().map(|(s, c)| (s.0, *c)).collect();
359 self.symbolic_chain_inner(target, &ev_map, depth)
360 }
361
362 fn symbolic_chain_inner(
364 &self,
365 target: SymbolId,
366 ev_map: &HashMap<usize, f64>,
367 depth: usize,
368 ) -> f64 {
369 if let Some(&conf) = ev_map.get(&target.0) {
371 return conf;
372 }
373
374 if depth == 0 {
375 return self
377 .symbols
378 .get(target.0)
379 .map(|s| s.confidence)
380 .unwrap_or(0.0);
381 }
382
383 let mut best: f64 = 0.0;
384
385 for rule in &self.rules {
386 if rule.head != target {
387 continue;
388 }
389 if rule.body.is_empty() {
390 let adjusted = self.apply_rule_type(rule, 1.0, rule.weight);
393 if adjusted > best {
394 best = adjusted;
395 }
396 continue;
397 }
398
399 let body_satisfaction: f64 = rule.body.iter().fold(1.0, |acc, &body_sym| {
402 let body_conf = self.symbolic_chain_inner(body_sym, ev_map, depth - 1);
403 acc * body_conf
404 });
405
406 let rule_confidence = self.apply_rule_type(rule, body_satisfaction, rule.weight);
407
408 if rule_confidence > best {
409 best = rule_confidence;
410 }
411 }
412
413 best
414 }
415
416 fn apply_rule_type(
419 &self,
420 rule: &LogicalRule,
421 body_satisfaction: f64,
422 _base_weight: f64,
423 ) -> f64 {
424 match &rule.rule_type {
425 RuleType::Definite => rule.weight * body_satisfaction,
426 RuleType::Probabilistic => rule.weight * body_satisfaction * body_satisfaction,
427 RuleType::Soft { temperature } => {
428 let temp = temperature.max(f64::EPSILON);
429 let sigmoid_input = body_satisfaction / temp;
430 let sigmoid = 1.0 / (1.0 + (-sigmoid_input).exp());
431 rule.weight * sigmoid
432 }
433 }
434 }
435
436 pub fn neural_forward(&self, target: SymbolId, evidence: &[(SymbolId, f64)]) -> f64 {
447 let target_sym = match self.symbols.get(target.0) {
448 Some(s) => s,
449 None => return 0.0,
450 };
451
452 let mut best: f64 = 0.0;
453 for &(ev_sym_id, conf) in evidence {
454 if let Some(ev_sym) = self.symbols.get(ev_sym_id.0) {
455 let sim = Self::neural_similarity(&target_sym.embedding, &ev_sym.embedding);
456 let score = sim * conf;
457 if score > best {
458 best = score;
459 }
460 }
461 }
462 best
463 }
464
465 pub fn infer(&mut self, query: &NsQuery) -> Result<NsResult, NsError> {
475 if query.target.0 >= self.symbols.len() {
477 return Err(NsError::SymbolNotFound(query.target.0));
478 }
479
480 let symbolic =
481 self.symbolic_forward_chain(query.target, &query.evidence, self.config.inference_depth);
482 let neural = self.neural_forward(query.target, &query.evidence);
483
484 let combined = match &query.mode {
485 InferenceMode::PureSymbolic => symbolic,
486 InferenceMode::PureNeural => neural,
487 InferenceMode::Hybrid { neural_weight } => {
488 let nw = neural_weight.clamp(0.0, 1.0);
489 nw * neural + (1.0 - nw) * symbolic
490 }
491 };
492
493 let explanation = self.build_explanation(query.target, &query.evidence);
495
496 self.total_inferences += 1;
497
498 Ok(NsResult {
499 symbol: query.target,
500 confidence: combined.clamp(0.0, 1.0),
501 explanation,
502 neural_contribution: neural,
503 symbolic_contribution: symbolic,
504 })
505 }
506
507 fn build_explanation(&self, target: SymbolId, evidence: &[(SymbolId, f64)]) -> Vec<String> {
510 let mut explanations: Vec<String> = Vec::new();
511
512 for rule in &self.rules {
514 if rule.head != target {
515 continue;
516 }
517 let body_names: Vec<String> = rule
518 .body
519 .iter()
520 .map(|b| {
521 self.symbols
522 .get(b.0)
523 .map(|s| s.name.clone())
524 .unwrap_or_else(|| format!("sym:{}", b.0))
525 })
526 .collect();
527 let head_name = self
528 .symbols
529 .get(target.0)
530 .map(|s| s.name.clone())
531 .unwrap_or_else(|| format!("sym:{}", target.0));
532 let body_str = if body_names.is_empty() {
533 "∅".to_string()
534 } else {
535 body_names.join(", ")
536 };
537 explanations.push(format!(
538 "rule_{}_from_[{}] (weight={:.3})",
539 head_name, body_str, rule.weight
540 ));
541 }
542
543 if let Some(target_sym) = self.symbols.get(target.0) {
545 for &(ev_id, conf) in evidence {
546 if let Some(ev_sym) = self.symbols.get(ev_id.0) {
547 let sim = Self::neural_similarity(&target_sym.embedding, &ev_sym.embedding);
548 if sim > 0.0 {
549 explanations.push(format!(
550 "neural_similarity({}, {})={:.3} × conf={:.3}",
551 target_sym.name, ev_sym.name, sim, conf
552 ));
553 }
554 }
555 }
556 }
557
558 explanations
559 }
560
561 pub fn explain_symbol(&self, id: SymbolId) -> Result<Vec<String>, NsError> {
572 if id.0 >= self.symbols.len() {
573 return Err(NsError::SymbolNotFound(id.0));
574 }
575 let sym_name = &self.symbols[id.0].name;
576 let mut lines: Vec<String> = Vec::new();
577 for rule in &self.rules {
578 if rule.head == id || rule.body.contains(&id) {
579 let head_name = self
580 .symbols
581 .get(rule.head.0)
582 .map(|s| s.name.as_str())
583 .unwrap_or("?");
584 let body_names: Vec<&str> = rule
585 .body
586 .iter()
587 .map(|b| {
588 self.symbols
589 .get(b.0)
590 .map(|s| s.name.as_str())
591 .unwrap_or("?")
592 })
593 .collect();
594 lines.push(format!(
595 "{} ← [{}] (w={:.3}, {:?})",
596 head_name,
597 body_names.join(", "),
598 rule.weight,
599 rule.rule_type
600 ));
601 }
602 }
603 if lines.is_empty() {
604 lines.push(format!("'{}' has no associated rules", sym_name));
605 }
606 Ok(lines)
607 }
608
609 pub fn similar_symbols(&self, id: SymbolId, k: usize) -> Result<Vec<(SymbolId, f64)>, NsError> {
619 if id.0 >= self.symbols.len() {
620 return Err(NsError::SymbolNotFound(id.0));
621 }
622 let query_emb = &self.symbols[id.0].embedding;
623 let threshold = self.config.similarity_threshold;
624
625 let mut scored: Vec<(SymbolId, f64)> = self
626 .symbols
627 .iter()
628 .filter(|s| s.id != id)
629 .map(|s| {
630 let sim = Self::neural_similarity(query_emb, &s.embedding);
631 (s.id, sim)
632 })
633 .filter(|(_, sim)| *sim >= threshold)
634 .collect();
635
636 scored.sort_by(|a, b| {
638 b.1.partial_cmp(&a.1)
639 .unwrap_or(std::cmp::Ordering::Equal)
640 .then_with(|| a.0.cmp(&b.0))
641 });
642 scored.truncate(k);
643 Ok(scored)
644 }
645
646 pub fn stats(&self) -> NsStats {
648 let avg_embedding_norm = if self.symbols.is_empty() {
649 0.0
650 } else {
651 let total_norm: f64 = self
652 .symbols
653 .iter()
654 .map(|s| s.embedding.iter().map(|x| x * x).sum::<f64>().sqrt())
655 .sum();
656 total_norm / self.symbols.len() as f64
657 };
658 NsStats {
659 symbol_count: self.symbols.len(),
660 rule_count: self.rules.len(),
661 total_inferences: self.total_inferences,
662 avg_embedding_norm,
663 }
664 }
665}
666
667#[cfg(test)]
672mod tests {
673 use super::{
674 InferenceMode, IntegratorConfig, LogicalRule, NeuralSymbolicIntegrator, NsError, NsQuery,
675 RuleType, SymbolId,
676 };
677
678 fn uniform_emb(dim: usize, val: f64) -> Vec<f64> {
684 let norm = (dim as f64 * val * val).sqrt();
685 if norm == 0.0 {
686 return vec![0.0; dim];
687 }
688 vec![val / norm; dim]
689 }
690
691 fn basis_emb(dim: usize, idx: usize) -> Vec<f64> {
693 let mut v = vec![0.0; dim];
694 if idx < dim {
695 v[idx] = 1.0;
696 }
697 v
698 }
699
700 fn default_integrator() -> NeuralSymbolicIntegrator {
701 NeuralSymbolicIntegrator::new(IntegratorConfig {
702 embedding_dim: 4,
703 max_symbols: 100,
704 inference_depth: 5,
705 similarity_threshold: 0.5,
706 })
707 }
708
709 #[test]
714 fn symbol_id_display() {
715 let id = SymbolId(42);
716 assert_eq!(id.to_string(), "sym:42");
717 }
718
719 #[test]
720 fn symbol_id_ordering() {
721 assert!(SymbolId(0) < SymbolId(1));
722 assert!(SymbolId(5) > SymbolId(3));
723 assert_eq!(SymbolId(7), SymbolId(7));
724 }
725
726 #[test]
731 fn add_symbol_returns_sequential_ids() {
732 let mut ig = default_integrator();
733 let id0 = ig
734 .add_symbol("a".into(), basis_emb(4, 0), 0.9)
735 .expect("test setup: add symbol 'a'");
736 let id1 = ig
737 .add_symbol("b".into(), basis_emb(4, 1), 0.8)
738 .expect("test setup: add symbol 'b'");
739 assert_eq!(id0, SymbolId(0));
740 assert_eq!(id1, SymbolId(1));
741 }
742
743 #[test]
744 fn add_symbol_dimension_mismatch() {
745 let mut ig = default_integrator();
746 let err = ig.add_symbol("x".into(), vec![0.1, 0.2], 0.5).unwrap_err();
747 assert_eq!(
748 err,
749 NsError::DimensionMismatch {
750 expected: 4,
751 got: 2
752 }
753 );
754 }
755
756 #[test]
757 fn add_symbol_max_reached() {
758 let mut ig = NeuralSymbolicIntegrator::new(IntegratorConfig {
759 embedding_dim: 2,
760 max_symbols: 2,
761 inference_depth: 3,
762 similarity_threshold: 0.5,
763 });
764 ig.add_symbol("a".into(), vec![1.0, 0.0], 1.0)
765 .expect("test setup: add symbol 'a'");
766 ig.add_symbol("b".into(), vec![0.0, 1.0], 1.0)
767 .expect("test setup: add symbol 'b'");
768 let err = ig.add_symbol("c".into(), vec![0.5, 0.5], 1.0).unwrap_err();
769 assert_eq!(err, NsError::MaxSymbolsReached);
770 }
771
772 #[test]
773 fn add_symbol_clamps_confidence() {
774 let mut ig = default_integrator();
775 let id = ig
776 .add_symbol("over".into(), basis_emb(4, 0), 1.5)
777 .expect("test setup: add symbol 'over'");
778 assert!((ig.symbols[id.0].confidence - 1.0).abs() < 1e-9);
779 let id2 = ig
780 .add_symbol("neg".into(), basis_emb(4, 1), -0.3)
781 .expect("test setup: add symbol 'neg'");
782 assert!((ig.symbols[id2.0].confidence).abs() < 1e-9);
783 }
784
785 #[test]
790 fn add_rule_valid() {
791 let mut ig = default_integrator();
792 let a = ig
793 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
794 .expect("test setup: add symbol 'a'");
795 let b = ig
796 .add_symbol("b".into(), basis_emb(4, 1), 1.0)
797 .expect("test setup: add symbol 'b'");
798 let result = ig.add_rule(LogicalRule {
799 head: a,
800 body: vec![b],
801 weight: 0.8,
802 rule_type: RuleType::Definite,
803 });
804 assert!(result.is_ok());
805 }
806
807 #[test]
808 fn add_rule_invalid_head() {
809 let mut ig = default_integrator();
810 let err = ig
811 .add_rule(LogicalRule {
812 head: SymbolId(99),
813 body: vec![],
814 weight: 1.0,
815 rule_type: RuleType::Definite,
816 })
817 .unwrap_err();
818 assert!(matches!(err, NsError::InvalidRule(_)));
819 }
820
821 #[test]
822 fn add_rule_invalid_body_sym() {
823 let mut ig = default_integrator();
824 let a = ig
825 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
826 .expect("test setup: add symbol 'a'");
827 let err = ig
828 .add_rule(LogicalRule {
829 head: a,
830 body: vec![SymbolId(50)],
831 weight: 0.9,
832 rule_type: RuleType::Probabilistic,
833 })
834 .unwrap_err();
835 assert!(matches!(err, NsError::InvalidRule(_)));
836 }
837
838 #[test]
843 fn neural_similarity_identical() {
844 let v = vec![1.0, 0.0, 0.0, 0.0];
845 let sim = NeuralSymbolicIntegrator::neural_similarity(&v, &v);
846 assert!((sim - 1.0).abs() < 1e-10);
847 }
848
849 #[test]
850 fn neural_similarity_orthogonal() {
851 let a = vec![1.0, 0.0];
852 let b = vec![0.0, 1.0];
853 let sim = NeuralSymbolicIntegrator::neural_similarity(&a, &b);
854 assert!(sim.abs() < 1e-10);
855 }
856
857 #[test]
858 fn neural_similarity_opposite() {
859 let a = vec![1.0, 0.0];
860 let b = vec![-1.0, 0.0];
861 let sim = NeuralSymbolicIntegrator::neural_similarity(&a, &b);
862 assert!((sim + 1.0).abs() < 1e-10);
863 }
864
865 #[test]
866 fn neural_similarity_zero_vector() {
867 let a = vec![0.0, 0.0];
868 let b = vec![1.0, 0.0];
869 let sim = NeuralSymbolicIntegrator::neural_similarity(&a, &b);
870 assert_eq!(sim, 0.0);
871 }
872
873 #[test]
874 fn neural_similarity_partial() {
875 let a = vec![1.0, 1.0];
876 let b = vec![1.0, 0.0];
877 let sim = NeuralSymbolicIntegrator::neural_similarity(&a, &b);
878 let expected = 1.0 / 2.0_f64.sqrt();
879 assert!((sim - expected).abs() < 1e-10);
880 }
881
882 #[test]
883 fn neural_similarity_clamp() {
884 let a = vec![3.0, 4.0];
887 let b = vec![6.0, 8.0]; let sim = NeuralSymbolicIntegrator::neural_similarity(&a, &b);
889 assert!(sim <= 1.0);
890 assert!(sim >= -1.0);
891 assert!((sim - 1.0).abs() < 1e-10);
892 }
893
894 #[test]
899 fn symbolic_chain_direct_evidence() {
900 let mut ig = default_integrator();
901 let a = ig
902 .add_symbol("a".into(), basis_emb(4, 0), 0.5)
903 .expect("test setup: add symbol 'a'");
904 let conf = ig.symbolic_forward_chain(a, &[(a, 0.9)], 3);
906 assert!((conf - 0.9).abs() < 1e-10);
907 }
908
909 #[test]
910 fn symbolic_chain_simple_rule_definite() {
911 let mut ig = default_integrator();
912 let a = ig
913 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
914 .expect("test setup: add symbol 'a'");
915 let b = ig
916 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
917 .expect("test setup: add symbol 'b'");
918 ig.add_rule(LogicalRule {
919 head: a,
920 body: vec![b],
921 weight: 0.9,
922 rule_type: RuleType::Definite,
923 })
924 .expect("test setup: add rule a <- b");
925 let conf = ig.symbolic_forward_chain(a, &[(b, 1.0)], 3);
926 assert!((conf - 0.9).abs() < 1e-9);
928 }
929
930 #[test]
931 fn symbolic_chain_probabilistic_rule() {
932 let mut ig = default_integrator();
933 let a = ig
934 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
935 .expect("test setup: add symbol 'a'");
936 let b = ig
937 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
938 .expect("test setup: add symbol 'b'");
939 ig.add_rule(LogicalRule {
940 head: a,
941 body: vec![b],
942 weight: 1.0,
943 rule_type: RuleType::Probabilistic,
944 })
945 .expect("test setup: add probabilistic rule a <- b");
946 let conf = ig.symbolic_forward_chain(a, &[(b, 0.8)], 3);
947 assert!((conf - 0.64).abs() < 1e-9);
949 }
950
951 #[test]
952 fn symbolic_chain_soft_rule() {
953 let mut ig = default_integrator();
954 let a = ig
955 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
956 .expect("test setup: add symbol 'a'");
957 let b = ig
958 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
959 .expect("test setup: add symbol 'b'");
960 ig.add_rule(LogicalRule {
961 head: a,
962 body: vec![b],
963 weight: 1.0,
964 rule_type: RuleType::Soft { temperature: 1.0 },
965 })
966 .expect("test setup: add soft rule a <- b");
967 let conf = ig.symbolic_forward_chain(a, &[(b, 1.0)], 3);
968 let expected = 1.0 / (1.0 + (-1.0_f64).exp());
970 assert!((conf - expected).abs() < 1e-9);
971 }
972
973 #[test]
974 fn symbolic_chain_empty_body_fact() {
975 let mut ig = default_integrator();
976 let a = ig
977 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
978 .expect("test setup: add symbol 'a'");
979 ig.add_rule(LogicalRule {
980 head: a,
981 body: vec![],
982 weight: 0.7,
983 rule_type: RuleType::Definite,
984 })
985 .expect("test setup: add fact rule for 'a'");
986 let conf = ig.symbolic_forward_chain(a, &[], 3);
988 assert!((conf - 0.7).abs() < 1e-9);
989 }
990
991 #[test]
992 fn symbolic_chain_depth_limit() {
993 let mut ig = default_integrator();
994 let a = ig
995 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
996 .expect("test setup: add symbol 'a'");
997 let b = ig
998 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
999 .expect("test setup: add symbol 'b'");
1000 ig.add_rule(LogicalRule {
1001 head: a,
1002 body: vec![b],
1003 weight: 1.0,
1004 rule_type: RuleType::Definite,
1005 })
1006 .expect("test setup: add rule a <- b");
1007 let conf = ig.symbolic_forward_chain(a, &[], 0);
1009 assert_eq!(conf, 0.0);
1010 }
1011
1012 #[test]
1013 fn symbolic_chain_multi_body() {
1014 let mut ig = default_integrator();
1015 let a = ig
1016 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1017 .expect("test setup: add symbol 'a'");
1018 let b = ig
1019 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
1020 .expect("test setup: add symbol 'b'");
1021 let c = ig
1022 .add_symbol("c".into(), basis_emb(4, 2), 0.0)
1023 .expect("test setup: add symbol 'c'");
1024 ig.add_rule(LogicalRule {
1025 head: a,
1026 body: vec![b, c],
1027 weight: 1.0,
1028 rule_type: RuleType::Definite,
1029 })
1030 .expect("test setup: add rule a <- b, c");
1031 let conf = ig.symbolic_forward_chain(a, &[(b, 0.8), (c, 0.6)], 3);
1033 assert!((conf - 0.48).abs() < 1e-9);
1034 }
1035
1036 #[test]
1041 fn neural_forward_no_evidence() {
1042 let mut ig = default_integrator();
1043 let a = ig
1044 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1045 .expect("test setup: add symbol 'a'");
1046 assert_eq!(ig.neural_forward(a, &[]), 0.0);
1047 }
1048
1049 #[test]
1050 fn neural_forward_identical_embedding() {
1051 let mut ig = default_integrator();
1052 let a = ig
1053 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1054 .expect("test setup: add symbol 'a'");
1055 let b = ig
1056 .add_symbol("b".into(), basis_emb(4, 0), 1.0)
1057 .expect("test setup: add symbol 'b'");
1058 let score = ig.neural_forward(a, &[(b, 0.9)]);
1059 assert!((score - 0.9).abs() < 1e-9);
1061 }
1062
1063 #[test]
1064 fn neural_forward_orthogonal_embedding() {
1065 let mut ig = default_integrator();
1066 let a = ig
1067 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1068 .expect("test setup: add symbol 'a'");
1069 let b = ig
1070 .add_symbol("b".into(), basis_emb(4, 1), 1.0)
1071 .expect("test setup: add symbol 'b'");
1072 let score = ig.neural_forward(a, &[(b, 1.0)]);
1073 assert!(score.abs() < 1e-10);
1074 }
1075
1076 #[test]
1077 fn neural_forward_invalid_target() {
1078 let ig = default_integrator();
1079 assert_eq!(ig.neural_forward(SymbolId(99), &[]), 0.0);
1081 }
1082
1083 #[test]
1088 fn infer_pure_symbolic() {
1089 let mut ig = default_integrator();
1090 let a = ig
1091 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1092 .expect("test setup: add symbol 'a'");
1093 let b = ig
1094 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
1095 .expect("test setup: add symbol 'b'");
1096 ig.add_rule(LogicalRule {
1097 head: a,
1098 body: vec![b],
1099 weight: 1.0,
1100 rule_type: RuleType::Definite,
1101 })
1102 .expect("test setup: add rule a <- b");
1103 let query = NsQuery {
1104 target: a,
1105 evidence: vec![(b, 1.0)],
1106 mode: InferenceMode::PureSymbolic,
1107 };
1108 let result = ig.infer(&query).expect("test setup: infer pure symbolic");
1109 assert!((result.confidence - 1.0).abs() < 1e-9);
1110 assert!((result.symbolic_contribution - 1.0).abs() < 1e-9);
1111 }
1112
1113 #[test]
1114 fn infer_pure_neural() {
1115 let mut ig = default_integrator();
1116 let a = ig
1117 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1118 .expect("test setup: add symbol 'a'");
1119 let b = ig
1120 .add_symbol("b".into(), basis_emb(4, 0), 0.0)
1121 .expect("test setup: add symbol 'b'");
1122 let query = NsQuery {
1123 target: a,
1124 evidence: vec![(b, 0.7)],
1125 mode: InferenceMode::PureNeural,
1126 };
1127 let result = ig.infer(&query).expect("test setup: infer pure neural");
1128 assert!((result.confidence - 0.7).abs() < 1e-9);
1130 assert!((result.neural_contribution - 0.7).abs() < 1e-9);
1131 }
1132
1133 #[test]
1134 fn infer_hybrid() {
1135 let mut ig = default_integrator();
1136 let a = ig
1137 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1138 .expect("test setup: add symbol 'a'");
1139 let b = ig
1140 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
1141 .expect("test setup: add symbol 'b'");
1142 ig.add_rule(LogicalRule {
1144 head: a,
1145 body: vec![b],
1146 weight: 1.0,
1147 rule_type: RuleType::Definite,
1148 })
1149 .expect("test setup: add rule a <- b");
1150 let query = NsQuery {
1152 target: a,
1153 evidence: vec![(b, 1.0)],
1154 mode: InferenceMode::Hybrid { neural_weight: 0.5 },
1155 };
1156 let result = ig.infer(&query).expect("test setup: infer hybrid");
1157 assert!((result.confidence - 0.5).abs() < 1e-9);
1159 }
1160
1161 #[test]
1162 fn infer_target_not_found() {
1163 let mut ig = default_integrator();
1164 let query = NsQuery {
1165 target: SymbolId(99),
1166 evidence: vec![],
1167 mode: InferenceMode::PureSymbolic,
1168 };
1169 assert_eq!(ig.infer(&query).unwrap_err(), NsError::SymbolNotFound(99));
1170 }
1171
1172 #[test]
1173 fn infer_increments_counter() {
1174 let mut ig = default_integrator();
1175 let a = ig
1176 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1177 .expect("test setup: add symbol 'a'");
1178 let query = NsQuery {
1179 target: a,
1180 evidence: vec![],
1181 mode: InferenceMode::PureSymbolic,
1182 };
1183 ig.infer(&query).expect("test setup: first infer call");
1184 ig.infer(&query).expect("test setup: second infer call");
1185 assert_eq!(ig.total_inferences, 2);
1186 }
1187
1188 #[test]
1189 fn infer_result_confidence_clamped() {
1190 let mut ig = default_integrator();
1192 let a = ig
1193 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1194 .expect("test setup: add symbol 'a'");
1195 ig.add_rule(LogicalRule {
1196 head: a,
1197 body: vec![],
1198 weight: 2.0, rule_type: RuleType::Definite,
1200 })
1201 .expect("test setup: add rule with weight > 1");
1202 let query = NsQuery {
1203 target: a,
1204 evidence: vec![],
1205 mode: InferenceMode::PureSymbolic,
1206 };
1207 let result = ig
1208 .infer(&query)
1209 .expect("test setup: infer with oversized weight");
1210 assert!(result.confidence <= 1.0);
1211 }
1212
1213 #[test]
1214 fn infer_explanation_nonempty_for_matching_rule() {
1215 let mut ig = default_integrator();
1216 let a = ig
1217 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1218 .expect("test setup: add symbol 'a'");
1219 let b = ig
1220 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
1221 .expect("test setup: add symbol 'b'");
1222 ig.add_rule(LogicalRule {
1223 head: a,
1224 body: vec![b],
1225 weight: 0.9,
1226 rule_type: RuleType::Definite,
1227 })
1228 .expect("test setup: add rule a <- b");
1229 let query = NsQuery {
1230 target: a,
1231 evidence: vec![(b, 1.0)],
1232 mode: InferenceMode::PureSymbolic,
1233 };
1234 let result = ig.infer(&query).expect("test setup: infer for explanation");
1235 assert!(!result.explanation.is_empty());
1236 let rule_exp = result
1237 .explanation
1238 .iter()
1239 .any(|e| e.contains("rule_a_from_"));
1240 assert!(
1241 rule_exp,
1242 "expected rule explanation, got: {:?}",
1243 result.explanation
1244 );
1245 }
1246
1247 #[test]
1252 fn explain_symbol_not_found() {
1253 let ig = default_integrator();
1254 assert_eq!(
1255 ig.explain_symbol(SymbolId(0)),
1256 Err(NsError::SymbolNotFound(0))
1257 );
1258 }
1259
1260 #[test]
1261 fn explain_symbol_no_rules() {
1262 let mut ig = default_integrator();
1263 let a = ig
1264 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1265 .expect("test setup: add symbol 'a'");
1266 let lines = ig
1267 .explain_symbol(a)
1268 .expect("test setup: explain symbol 'a'");
1269 assert!(!lines.is_empty());
1270 assert!(lines[0].contains("no associated rules"));
1271 }
1272
1273 #[test]
1274 fn explain_symbol_as_head() {
1275 let mut ig = default_integrator();
1276 let a = ig
1277 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1278 .expect("test setup: add symbol 'a'");
1279 let b = ig
1280 .add_symbol("b".into(), basis_emb(4, 1), 1.0)
1281 .expect("test setup: add symbol 'b'");
1282 ig.add_rule(LogicalRule {
1283 head: a,
1284 body: vec![b],
1285 weight: 0.8,
1286 rule_type: RuleType::Definite,
1287 })
1288 .expect("test setup: add rule a <- b");
1289 let lines = ig
1290 .explain_symbol(a)
1291 .expect("test setup: explain symbol 'a'");
1292 assert!(lines.iter().any(|l| l.contains("a ←")));
1293 }
1294
1295 #[test]
1296 fn explain_symbol_as_body() {
1297 let mut ig = default_integrator();
1298 let a = ig
1299 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1300 .expect("test setup: add symbol 'a'");
1301 let b = ig
1302 .add_symbol("b".into(), basis_emb(4, 1), 1.0)
1303 .expect("test setup: add symbol 'b'");
1304 ig.add_rule(LogicalRule {
1305 head: a,
1306 body: vec![b],
1307 weight: 0.8,
1308 rule_type: RuleType::Definite,
1309 })
1310 .expect("test setup: add rule a <- b");
1311 let lines = ig
1313 .explain_symbol(b)
1314 .expect("test setup: explain symbol 'b'");
1315 assert!(lines.iter().any(|l| l.contains('b')));
1316 }
1317
1318 #[test]
1323 fn similar_symbols_not_found() {
1324 let ig = default_integrator();
1325 assert_eq!(
1326 ig.similar_symbols(SymbolId(99), 5),
1327 Err(NsError::SymbolNotFound(99))
1328 );
1329 }
1330
1331 #[test]
1332 fn similar_symbols_returns_top_k() {
1333 let mut ig = NeuralSymbolicIntegrator::new(IntegratorConfig {
1334 embedding_dim: 4,
1335 max_symbols: 100,
1336 inference_depth: 3,
1337 similarity_threshold: 0.0, });
1339 let a = ig
1340 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1341 .expect("test setup: add symbol 'a'");
1342 let _b = ig
1343 .add_symbol("b".into(), basis_emb(4, 0), 1.0)
1344 .expect("test setup: add symbol 'b'"); let _c = ig
1346 .add_symbol("c".into(), basis_emb(4, 0), 1.0)
1347 .expect("test setup: add symbol 'c'"); let _d = ig
1349 .add_symbol("d".into(), basis_emb(4, 1), 1.0)
1350 .expect("test setup: add symbol 'd'"); let results = ig
1353 .similar_symbols(a, 2)
1354 .expect("test setup: similar symbols for 'a'");
1355 assert!(results.len() <= 2);
1357 }
1358
1359 #[test]
1360 fn similar_symbols_excludes_self() {
1361 let mut ig = NeuralSymbolicIntegrator::new(IntegratorConfig {
1362 embedding_dim: 4,
1363 max_symbols: 100,
1364 inference_depth: 3,
1365 similarity_threshold: 0.0,
1366 });
1367 let a = ig
1368 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1369 .expect("test setup: add symbol 'a'");
1370 let results = ig
1371 .similar_symbols(a, 10)
1372 .expect("test setup: similar symbols for 'a'");
1373 assert!(results.is_empty());
1375 }
1376
1377 #[test]
1378 fn similar_symbols_threshold_filters() {
1379 let mut ig = NeuralSymbolicIntegrator::new(IntegratorConfig {
1380 embedding_dim: 4,
1381 max_symbols: 100,
1382 inference_depth: 3,
1383 similarity_threshold: 0.9, });
1385 let a = ig
1386 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1387 .expect("test setup: add symbol 'a'");
1388 let _b = ig
1391 .add_symbol("b".into(), basis_emb(4, 1), 1.0)
1392 .expect("test setup: add symbol 'b'");
1393 let results = ig
1394 .similar_symbols(a, 10)
1395 .expect("test setup: similar symbols for 'a'");
1396 assert!(
1397 results.is_empty(),
1398 "orthogonal vector should be below threshold"
1399 );
1400 }
1401
1402 #[test]
1403 fn similar_symbols_sorted_desc() {
1404 let mut ig = NeuralSymbolicIntegrator::new(IntegratorConfig {
1405 embedding_dim: 4,
1406 max_symbols: 100,
1407 inference_depth: 3,
1408 similarity_threshold: 0.0,
1409 });
1410 let a = ig
1411 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1412 .expect("test setup: add symbol 'a'");
1413 ig.add_symbol("b".into(), basis_emb(4, 0), 1.0)
1415 .expect("test setup: add symbol 'b'");
1416 let partial: Vec<f64> = {
1417 let mut v = vec![0.0; 4];
1418 v[0] = 0.7;
1419 v[1] = 0.3;
1420 let norm = (0.7_f64 * 0.7 + 0.3_f64 * 0.3).sqrt();
1422 v.iter_mut().for_each(|x| *x /= norm);
1423 v
1424 };
1425 ig.add_symbol("c".into(), partial, 1.0)
1426 .expect("test setup: add symbol 'c'");
1427 let results = ig
1428 .similar_symbols(a, 10)
1429 .expect("test setup: similar symbols for 'a'");
1430 if results.len() >= 2 {
1432 assert!(results[0].1 >= results[1].1);
1433 }
1434 }
1435
1436 #[test]
1441 fn stats_empty_integrator() {
1442 let ig = default_integrator();
1443 let s = ig.stats();
1444 assert_eq!(s.symbol_count, 0);
1445 assert_eq!(s.rule_count, 0);
1446 assert_eq!(s.total_inferences, 0);
1447 assert_eq!(s.avg_embedding_norm, 0.0);
1448 }
1449
1450 #[test]
1451 fn stats_counts_symbols_and_rules() {
1452 let mut ig = default_integrator();
1453 let a = ig
1454 .add_symbol("a".into(), basis_emb(4, 0), 1.0)
1455 .expect("test setup: add symbol 'a'");
1456 let b = ig
1457 .add_symbol("b".into(), basis_emb(4, 1), 1.0)
1458 .expect("test setup: add symbol 'b'");
1459 ig.add_rule(LogicalRule {
1460 head: a,
1461 body: vec![b],
1462 weight: 0.9,
1463 rule_type: RuleType::Definite,
1464 })
1465 .expect("test setup: add rule a <- b");
1466 let s = ig.stats();
1467 assert_eq!(s.symbol_count, 2);
1468 assert_eq!(s.rule_count, 1);
1469 }
1470
1471 #[test]
1472 fn stats_avg_norm_unit_vectors() {
1473 let mut ig = default_integrator();
1474 ig.add_symbol("a".into(), basis_emb(4, 0), 1.0)
1475 .expect("test setup: add symbol 'a'");
1476 ig.add_symbol("b".into(), basis_emb(4, 1), 1.0)
1477 .expect("test setup: add symbol 'b'");
1478 let s = ig.stats();
1479 assert!((s.avg_embedding_norm - 1.0).abs() < 1e-10);
1481 }
1482
1483 #[test]
1484 fn stats_tracks_inferences() {
1485 let mut ig = default_integrator();
1486 let a = ig
1487 .add_symbol("a".into(), basis_emb(4, 0), 0.5)
1488 .expect("test setup: add symbol 'a'");
1489 let q = NsQuery {
1490 target: a,
1491 evidence: vec![],
1492 mode: InferenceMode::PureSymbolic,
1493 };
1494 ig.infer(&q).expect("test setup: first infer call");
1495 ig.infer(&q).expect("test setup: second infer call");
1496 ig.infer(&q).expect("test setup: third infer call");
1497 assert_eq!(ig.stats().total_inferences, 3);
1498 }
1499
1500 #[test]
1505 fn scenario_chain_of_rules() {
1506 let mut ig = default_integrator();
1508 let a = ig
1509 .add_symbol("a".into(), uniform_emb(4, 1.0), 0.0)
1510 .expect("test setup: add symbol 'a'");
1511 let b = ig
1512 .add_symbol("b".into(), uniform_emb(4, 1.0), 0.0)
1513 .expect("test setup: add symbol 'b'");
1514 let c = ig
1515 .add_symbol("c".into(), uniform_emb(4, 1.0), 0.0)
1516 .expect("test setup: add symbol 'c'");
1517 ig.add_rule(LogicalRule {
1518 head: a,
1519 body: vec![b],
1520 weight: 0.9,
1521 rule_type: RuleType::Definite,
1522 })
1523 .expect("test setup: add rule a <- b");
1524 ig.add_rule(LogicalRule {
1525 head: b,
1526 body: vec![c],
1527 weight: 0.8,
1528 rule_type: RuleType::Definite,
1529 })
1530 .expect("test setup: add rule b <- c");
1531 let q = NsQuery {
1532 target: a,
1533 evidence: vec![(c, 1.0)],
1534 mode: InferenceMode::PureSymbolic,
1535 };
1536 let result = ig.infer(&q).expect("test setup: infer chain a <- b <- c");
1537 assert!((result.confidence - 0.72).abs() < 1e-9);
1539 }
1540
1541 #[test]
1542 fn scenario_multiple_rules_max_selected() {
1543 let mut ig = default_integrator();
1544 let a = ig
1545 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1546 .expect("test setup: add symbol 'a'");
1547 let b = ig
1548 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
1549 .expect("test setup: add symbol 'b'");
1550 let c = ig
1551 .add_symbol("c".into(), basis_emb(4, 2), 0.0)
1552 .expect("test setup: add symbol 'c'");
1553 ig.add_rule(LogicalRule {
1555 head: a,
1556 body: vec![b],
1557 weight: 0.3,
1558 rule_type: RuleType::Definite,
1559 })
1560 .expect("test setup: add low-weight rule a <- b");
1561 ig.add_rule(LogicalRule {
1562 head: a,
1563 body: vec![c],
1564 weight: 0.9,
1565 rule_type: RuleType::Definite,
1566 })
1567 .expect("test setup: add high-weight rule a <- c");
1568 let q = NsQuery {
1569 target: a,
1570 evidence: vec![(b, 1.0), (c, 1.0)],
1571 mode: InferenceMode::PureSymbolic,
1572 };
1573 let result = ig
1574 .infer(&q)
1575 .expect("test setup: infer with competing rules");
1576 assert!((result.confidence - 0.9).abs() < 1e-9);
1578 }
1579
1580 #[test]
1581 fn scenario_hybrid_blends_both() {
1582 let mut ig = default_integrator();
1583 let a = ig
1585 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1586 .expect("test setup: add symbol 'a'");
1587 let b = ig
1588 .add_symbol("b".into(), basis_emb(4, 0), 0.0)
1589 .expect("test setup: add symbol 'b'"); ig.add_rule(LogicalRule {
1592 head: a,
1593 body: vec![b],
1594 weight: 0.8,
1595 rule_type: RuleType::Definite,
1596 })
1597 .expect("test setup: add rule a <- b");
1598 let q = NsQuery {
1599 target: a,
1600 evidence: vec![(b, 0.6)],
1601 mode: InferenceMode::Hybrid { neural_weight: 0.4 },
1602 };
1603 let result = ig.infer(&q).expect("test setup: infer hybrid blend");
1604 assert!((result.confidence - 0.528).abs() < 1e-9);
1607 }
1608
1609 #[test]
1610 fn scenario_soft_rule_low_temperature() {
1611 let mut ig = default_integrator();
1613 let a = ig
1614 .add_symbol("a".into(), basis_emb(4, 0), 0.0)
1615 .expect("test setup: add symbol 'a'");
1616 let b = ig
1617 .add_symbol("b".into(), basis_emb(4, 1), 0.0)
1618 .expect("test setup: add symbol 'b'");
1619 ig.add_rule(LogicalRule {
1620 head: a,
1621 body: vec![b],
1622 weight: 1.0,
1623 rule_type: RuleType::Soft { temperature: 0.01 },
1624 })
1625 .expect("test setup: add soft rule a <- b");
1626 let conf = ig.symbolic_forward_chain(a, &[(b, 1.0)], 3);
1628 assert!(conf > 0.99, "expected ≈1.0, got {}", conf);
1629 }
1630}