use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
use crate::error::{MemoryError, Result};
use crate::singularity::Singularity;
#[derive(Debug, Clone)]
pub struct TraversalConfig {
pub max_depth: usize,
pub min_strength: f32,
pub max_results: usize,
}
impl Default for TraversalConfig {
fn default() -> Self {
Self {
max_depth: 3,
min_strength: 0.0,
max_results: 100,
}
}
}
impl Singularity {
pub fn neighbors(&self, id: &str, min_strength: f32) -> Vec<(String, f32)> {
self.get_associations(id)
.into_iter()
.filter(|(_, strength)| *strength >= min_strength)
.collect()
}
pub fn incoming_associations(&self, id: &str) -> Vec<(String, f32)> {
let mut incoming = Vec::new();
for (from_id, links) in &self.associations {
if let Some(&strength) = links.get(id) {
incoming.push((from_id.clone(), strength));
}
}
incoming.sort_by(|a, b| b.1.total_cmp(&a.1));
incoming
}
pub fn bfs(&self, start: &str, config: &TraversalConfig) -> Result<Vec<(String, u32)>> {
if !self.concepts.contains_key(start) {
return Err(MemoryError::NotFound {
entity: "Concept".to_string(),
id: start.to_string(),
});
}
let mut visited: HashSet<String> = HashSet::new();
let mut results: Vec<(String, u32)> = Vec::new();
let mut queue: VecDeque<(String, u32)> = VecDeque::new();
visited.insert(start.to_string());
queue.push_back((start.to_string(), 0));
while let Some((current, depth)) = queue.pop_front() {
if results.len() >= config.max_results {
break;
}
results.push((current.clone(), depth));
if depth as usize >= config.max_depth {
continue;
}
let neighbors = self.neighbors(¤t, config.min_strength);
for (neighbor, _) in neighbors {
if visited.insert(neighbor.clone()) {
queue.push_back((neighbor, depth + 1));
}
}
}
Ok(results)
}
pub fn shortest_path(
&self,
from: &str,
to: &str,
config: &TraversalConfig,
) -> Result<Option<Vec<String>>> {
if !self.concepts.contains_key(from) {
return Err(MemoryError::NotFound {
entity: "Concept".to_string(),
id: from.to_string(),
});
}
if !self.concepts.contains_key(to) {
return Err(MemoryError::NotFound {
entity: "Concept".to_string(),
id: to.to_string(),
});
}
if from == to {
return Ok(Some(vec![from.to_string()]));
}
let mut dist: HashMap<String, f32> = HashMap::new();
let mut parent: HashMap<String, String> = HashMap::new();
let mut heap: BinaryHeap<Reverse<(u32, u32, String)>> = BinaryHeap::new();
dist.insert(from.to_string(), 0.0);
heap.push(Reverse((0u32, 0u32, from.to_string())));
while let Some(Reverse((cost_bits, depth, current))) = heap.pop() {
if current == to {
let mut path = vec![to.to_string()];
let mut node = to;
while let Some(p) = parent.get(node) {
path.push(p.clone());
node = p;
if node == from {
break;
}
}
path.reverse();
return Ok(Some(path));
}
let current_cost = f32::from_bits(cost_bits);
if let Some(&best) = dist.get(¤t) {
if current_cost > best {
continue; }
}
if depth as usize >= config.max_depth {
continue;
}
let neighbors = self.neighbors(¤t, config.min_strength);
for (neighbor, strength) in neighbors {
let edge_cost = if strength > 0.0 {
-strength.ln()
} else {
f32::MAX / 2.0
};
let new_cost = current_cost + edge_cost;
let best = dist.get(&neighbor).copied().unwrap_or(f32::MAX);
if new_cost < best {
dist.insert(neighbor.clone(), new_cost);
parent.insert(neighbor.clone(), current.clone());
heap.push(Reverse((new_cost.to_bits(), depth + 1, neighbor)));
}
}
}
Ok(None)
}
pub fn shortest_path_hops(
&self,
from: &str,
to: &str,
config: &TraversalConfig,
) -> Result<Option<Vec<String>>> {
if !self.concepts.contains_key(from) {
return Err(MemoryError::NotFound {
entity: "Concept".to_string(),
id: from.to_string(),
});
}
if !self.concepts.contains_key(to) {
return Err(MemoryError::NotFound {
entity: "Concept".to_string(),
id: to.to_string(),
});
}
if from == to {
return Ok(Some(vec![from.to_string()]));
}
let mut visited: HashSet<String> = HashSet::new();
let mut parent: HashMap<String, String> = HashMap::new();
let mut queue: VecDeque<(String, u32)> = VecDeque::new();
visited.insert(from.to_string());
queue.push_back((from.to_string(), 0));
while let Some((current, depth)) = queue.pop_front() {
if depth as usize >= config.max_depth {
continue;
}
let neighbors = self.neighbors(¤t, config.min_strength);
for (neighbor, _) in neighbors {
if visited.insert(neighbor.clone()) {
parent.insert(neighbor.clone(), current.clone());
if neighbor == to {
let mut path = vec![to.to_string()];
let mut node = to;
while let Some(p) = parent.get(node) {
path.push(p.clone());
node = p;
if node == from {
break;
}
}
path.reverse();
return Ok(Some(path));
}
queue.push_back((neighbor, depth + 1));
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hyperdim::HVec10240;
use crate::singularity::{Concept, ConceptBuilder, Singularity};
fn make_concept(id: &str) -> Concept {
ConceptBuilder::new(id)
.with_vector(HVec10240::random())
.build()
.unwrap()
}
#[test]
fn test_neighbors() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
sing.inject(make_concept("c")).unwrap();
sing.associate("a", "b", 0.8).unwrap();
sing.associate("a", "c", 0.3).unwrap();
let neighbors = sing.neighbors("a", 0.5);
assert_eq!(neighbors.len(), 1);
assert_eq!(neighbors[0].0, "b");
}
#[test]
fn test_incoming_associations() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
sing.inject(make_concept("c")).unwrap();
sing.associate("a", "c", 0.8).unwrap();
sing.associate("b", "c", 0.5).unwrap();
let incoming = sing.incoming_associations("c");
assert_eq!(incoming.len(), 2);
assert_eq!(incoming[0].0, "a");
assert_eq!(incoming[1].0, "b");
}
#[test]
fn test_bfs_simple() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
sing.inject(make_concept("c")).unwrap();
sing.associate("a", "b", 0.5).unwrap();
sing.associate("b", "c", 0.5).unwrap();
let config = TraversalConfig::default();
let results = sing.bfs("a", &config).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0], ("a".to_string(), 0));
assert_eq!(results[1], ("b".to_string(), 1));
assert_eq!(results[2], ("c".to_string(), 2));
}
#[test]
fn test_bfs_max_depth() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
sing.inject(make_concept("c")).unwrap();
sing.associate("a", "b", 0.5).unwrap();
sing.associate("b", "c", 0.5).unwrap();
let config = TraversalConfig {
max_depth: 1,
..Default::default()
};
let results = sing.bfs("a", &config).unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_bfs_missing_concept() {
let sing = Singularity::new();
let config = TraversalConfig::default();
let result = sing.bfs("nonexistent", &config);
assert!(result.is_err());
}
#[test]
fn test_shortest_path_direct() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
sing.associate("a", "b", 0.9).unwrap();
let config = TraversalConfig::default();
let path = sing.shortest_path("a", "b", &config).unwrap();
assert_eq!(path, Some(vec!["a".to_string(), "b".to_string()]));
}
#[test]
fn test_shortest_path_indirect() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
sing.inject(make_concept("c")).unwrap();
sing.associate("a", "b", 0.9).unwrap();
sing.associate("b", "c", 0.9).unwrap();
let config = TraversalConfig::default();
let path = sing.shortest_path("a", "c", &config).unwrap();
assert_eq!(
path,
Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
);
}
#[test]
fn test_shortest_path_no_path() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
let config = TraversalConfig::default();
let path = sing.shortest_path("a", "b", &config).unwrap();
assert!(path.is_none());
}
#[test]
fn test_shortest_path_same_node() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
let config = TraversalConfig::default();
let path = sing.shortest_path("a", "a", &config).unwrap();
assert_eq!(path, Some(vec!["a".to_string()]));
}
#[test]
fn test_shortest_path_dijkstra_prefers_strong_edge() {
let mut sing = Singularity::new();
for id in ["a", "b", "c", "d"] {
sing.inject(make_concept(id)).unwrap();
}
sing.associate("a", "b", 0.9).unwrap();
sing.associate("b", "d", 0.9).unwrap();
sing.associate("a", "c", 0.1).unwrap();
sing.associate("c", "d", 0.1).unwrap();
let config = TraversalConfig::default();
let path = sing.shortest_path("a", "d", &config).unwrap().unwrap();
assert_eq!(path, vec!["a", "b", "d"]);
}
#[test]
fn test_shortest_path_hops_direct() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
sing.associate("a", "b", 0.5).unwrap();
let config = TraversalConfig::default();
let path = sing.shortest_path_hops("a", "b", &config).unwrap();
assert_eq!(path, Some(vec!["a".to_string(), "b".to_string()]));
}
#[test]
fn test_shortest_path_hops_indirect() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
sing.inject(make_concept("c")).unwrap();
sing.associate("a", "b", 0.5).unwrap();
sing.associate("b", "c", 0.5).unwrap();
let config = TraversalConfig::default();
let path = sing.shortest_path_hops("a", "c", &config).unwrap();
assert_eq!(
path,
Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
);
}
#[test]
fn test_shortest_path_hops_no_path() {
let mut sing = Singularity::new();
sing.inject(make_concept("a")).unwrap();
sing.inject(make_concept("b")).unwrap();
let config = TraversalConfig::default();
let path = sing.shortest_path_hops("a", "b", &config).unwrap();
assert!(path.is_none());
}
}