oxios_kernel/memory/
graph.rs1use std::collections::HashMap;
8
9#[derive(Debug, Clone, Default)]
14pub struct MemoryGraph {
15 edges: HashMap<u64, Vec<u64>>,
17 incoming: HashMap<u64, Vec<u64>>,
19 node_count: usize,
21}
22
23impl MemoryGraph {
24 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn add_edge(&mut self, from: u64, to: u64) {
33 if from == to {
34 return; }
36 self.edges.entry(from).or_default();
37 self.edges.entry(to).or_default();
38 self.incoming.entry(from).or_default();
39 self.incoming.entry(to).or_default();
40
41 let neighbors = self
42 .edges
43 .get_mut(&from)
44 .expect("entry(or_default) guarantees existence");
45 if !neighbors.contains(&to) {
46 neighbors.push(to);
47 self.incoming
48 .get_mut(&to)
49 .expect("entry(or_default) guarantees existence")
50 .push(from);
51 }
52
53 self.node_count = self.edges.len();
54 }
55
56 pub fn link(&mut self, a: u64, b: u64) {
58 self.add_edge(a, b);
59 self.add_edge(b, a);
60 }
61
62 pub fn node_count(&self) -> usize {
64 self.node_count
65 }
66
67 pub fn neighbors(&self, node: u64) -> &[u64] {
69 self.edges.get(&node).map(|v| v.as_slice()).unwrap_or(&[])
70 }
71
72 pub fn pagerank(
85 &self,
86 damping: f64,
87 iterations: usize,
88 initial_scores: Option<&HashMap<u64, f64>>,
89 ) -> HashMap<u64, f64> {
90 if self.node_count == 0 {
91 return HashMap::new();
92 }
93
94 let n = self.node_count as f64;
95 let base = 1.0 / n;
96
97 let mut scores: HashMap<u64, f64> = self
99 .edges
100 .keys()
101 .map(|&k| {
102 let init = initial_scores
103 .and_then(|m| m.get(&k))
104 .copied()
105 .unwrap_or(base);
106 (k, init)
107 })
108 .collect();
109
110 let out_degree: HashMap<u64, usize> =
112 self.edges.iter().map(|(&k, v)| (k, v.len())).collect();
113
114 for _ in 0..iterations {
116 let mut new_scores = HashMap::with_capacity(self.node_count);
117
118 let sink_sum: f64 = scores
119 .iter()
120 .filter(|(&k, _)| out_degree.get(&k).copied().unwrap_or(0) == 0)
121 .map(|(_, &s)| s)
122 .sum();
123
124 for &node in self.edges.keys() {
125 let incoming_sum: f64 = self
127 .incoming
128 .get(&node)
129 .map(|neighbors| {
130 neighbors
131 .iter()
132 .map(|&src| {
133 let src_out = out_degree.get(&src).copied().unwrap_or(1) as f64;
134 scores.get(&src).copied().unwrap_or(0.0) / src_out
135 })
136 .sum()
137 })
138 .unwrap_or(0.0);
139
140 let rank = (1.0 - damping) / n + damping * (incoming_sum + sink_sum / n);
141 new_scores.insert(node, rank);
142 }
143
144 scores = new_scores;
145 }
146
147 scores
148 }
149
150 pub fn from_co_access(sessions: &[Vec<u64>]) -> Self {
156 let mut graph = Self::new();
157 for session in sessions {
158 for i in 0..session.len() {
160 for j in (i + 1)..session.len() {
161 graph.link(session[i], session[j]);
162 }
163 }
164 }
165 graph
166 }
167}
168
169#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_empty_graph() {
179 let graph = MemoryGraph::new();
180 let scores = graph.pagerank(0.85, 20, None);
181 assert!(scores.is_empty());
182 }
183
184 #[test]
185 fn test_single_node() {
186 let mut graph = MemoryGraph::new();
187 graph.add_edge(1, 1); let scores = graph.pagerank(0.85, 20, None);
189 assert!(scores.is_empty() || scores.values().all(|&v| v > 0.0));
190 }
191
192 #[test]
193 fn test_two_nodes() {
194 let mut graph = MemoryGraph::new();
195 graph.link(1, 2);
196
197 let scores = graph.pagerank(0.85, 50, None);
198 assert_eq!(scores.len(), 2);
199
200 let s1 = scores.get(&1).unwrap();
202 let s2 = scores.get(&2).unwrap();
203 assert!(
204 (s1 - s2).abs() < 0.01,
205 "Symmetric graph should have equal scores"
206 );
207 }
208
209 #[test]
210 fn test_hub_authority() {
211 let mut graph = MemoryGraph::new();
215 graph.add_edge(1, 2);
216 graph.add_edge(1, 3);
217 graph.add_edge(1, 4);
218 graph.add_edge(2, 1);
219 graph.add_edge(3, 1);
220 graph.add_edge(4, 1);
221
222 let scores = graph.pagerank(0.85, 50, None);
223 let s1 = scores.get(&1).unwrap();
224
225 for &node in &[2u64, 3, 4] {
227 let sn = scores.get(&node).unwrap();
228 assert!(*s1 >= *sn, "Hub node should have >= score than leaf");
229 }
230 }
231
232 #[test]
233 fn test_from_co_access() {
234 let sessions = vec![
235 vec![1, 2, 3], vec![2, 4], ];
238
239 let graph = MemoryGraph::from_co_access(&sessions);
240 assert_eq!(graph.node_count(), 4);
241
242 let scores = graph.pagerank(0.85, 50, None);
244 let s2 = scores.get(&2).unwrap();
245 for &node in &[1u64, 3, 4] {
246 let sn = scores.get(&node).unwrap();
247 assert!(*s2 >= *sn, "Node 2 should have highest score");
248 }
249 }
250
251 #[test]
252 fn test_initial_scores_influence() {
253 let mut graph = MemoryGraph::new();
255 graph.add_edge(1, 2);
256
257 let initial = HashMap::from([(1u64, 10.0), (2u64, 0.1)]);
259 let scores = graph.pagerank(0.85, 5, Some(&initial));
260
261 let s1 = scores.get(&1).unwrap();
263 let s2 = scores.get(&2).unwrap();
264 assert!(*s1 > 0.0, "Node 1 should have positive score");
266 assert!(*s2 > 0.0, "Node 2 should have positive score");
267 }
268}