1use std::collections::{BTreeMap, VecDeque};
12
13use uqa_core::{DocId, Payload, PostingEntry, PostingList, Value, VertexId};
14
15use crate::posting_list::{GraphPayload, GraphPostingList};
16use crate::store::{GraphStore, GraphStoreError, GraphStoreResult};
17
18const MAX_EXACT_F64_INTEGER: u64 = 9_007_199_254_740_992;
19
20fn usize_as_f64(value: usize, context: &str) -> GraphStoreResult<f64> {
21 let value = u64::try_from(value)
22 .map_err(|_| GraphStoreError::CorruptGraph(format!("{context} exceeds the u64 range")))?;
23 u64_as_f64(value, context)
24}
25
26fn u64_as_f64(value: u64, context: &str) -> GraphStoreResult<f64> {
27 if value <= MAX_EXACT_F64_INTEGER {
28 Ok(value as f64)
29 } else {
30 Err(GraphStoreError::CorruptGraph(format!(
31 "{context} {value} exceeds the exact f64 integer range"
32 )))
33 }
34}
35
36pub struct PageRank<'a> {
43 pub graph: &'a str,
44 pub damping: f64,
45 pub max_iterations: u32,
46 pub tolerance: f64,
47}
48
49impl<'a> PageRank<'a> {
50 pub fn new(graph: &'a str) -> Self {
51 Self {
52 graph,
53 damping: 0.85,
54 max_iterations: 100,
55 tolerance: 1e-6,
56 }
57 }
58
59 pub fn damping(mut self, d: f64) -> Self {
60 self.damping = d;
61 self
62 }
63
64 pub fn max_iterations(mut self, k: u32) -> Self {
65 self.max_iterations = k;
66 self
67 }
68
69 pub fn tolerance(mut self, t: f64) -> Self {
70 self.tolerance = t;
71 self
72 }
73
74 pub fn execute<G: GraphStore>(&self, store: &G) -> GraphStoreResult<GraphPostingList> {
75 if !self.damping.is_finite() || !(0.0..=1.0).contains(&self.damping) {
76 return Err(GraphStoreError::InvalidMutation(format!(
77 "PageRank damping must be finite and in [0, 1], got {}",
78 self.damping
79 )));
80 }
81 if !self.tolerance.is_finite() || self.tolerance < 0.0 {
82 return Err(GraphStoreError::InvalidMutation(format!(
83 "PageRank tolerance must be finite and non-negative, got {}",
84 self.tolerance
85 )));
86 }
87 let vertices: Vec<VertexId> = store.vertex_ids_in_graph(self.graph)?.into_iter().collect();
88 let n = vertices.len();
89 if n == 0 {
90 return Ok(GraphPostingList::new());
91 }
92 if n == 1 {
93 return single_vertex_result(vertices[0], 1.0, &vertices, self.graph);
94 }
95
96 let n_f64 = usize_as_f64(n, "PageRank vertex count")?;
97 let mut rank: BTreeMap<VertexId, f64> =
98 vertices.iter().map(|v| (*v, 1.0 / n_f64)).collect();
99 let mut out_degree: BTreeMap<VertexId, usize> = BTreeMap::new();
100 let mut in_neighbors: BTreeMap<VertexId, Vec<VertexId>> = BTreeMap::new();
101 for v in &vertices {
102 out_degree.insert(*v, store.out_edge_ids(*v, self.graph)?.len());
103 let mut ins: Vec<VertexId> = Vec::new();
104 for eid in store.in_edge_ids(*v, self.graph)? {
105 let edge = store.get_edge(eid).ok_or_else(|| {
106 GraphStoreError::CorruptGraph(format!("missing PageRank edge {eid}"))
107 })?;
108 ins.push(edge.source_id);
109 }
110 in_neighbors.insert(*v, ins);
111 }
112
113 let d = self.damping;
114 for _ in 0..self.max_iterations {
115 let mut new_rank: BTreeMap<VertexId, f64> = BTreeMap::new();
116 for v in &vertices {
117 let mut incoming = 0.0;
118 if let Some(ins) = in_neighbors.get(v) {
119 for u in ins {
120 let deg = *out_degree.get(u).unwrap_or(&0);
121 if deg > 0 {
122 incoming += rank[u] / usize_as_f64(deg, "PageRank out-degree")?;
123 }
124 }
125 }
126 new_rank.insert(*v, (1.0 - d) / n_f64 + d * incoming);
127 }
128 let delta: f64 = vertices.iter().map(|v| (new_rank[v] - rank[v]).abs()).sum();
129 rank = new_rank;
130 if delta < self.tolerance {
131 break;
132 }
133 }
134
135 let normalized = min_max_normalize(&rank, &vertices)?;
136 build_score_result(&vertices, &normalized, self.graph, &BTreeMap::new())
137 }
138}
139
140pub struct HITS<'a> {
148 pub graph: &'a str,
149 pub max_iterations: u32,
150 pub tolerance: f64,
151}
152
153impl<'a> HITS<'a> {
154 pub fn new(graph: &'a str) -> Self {
155 Self {
156 graph,
157 max_iterations: 100,
158 tolerance: 1e-6,
159 }
160 }
161
162 pub fn max_iterations(mut self, k: u32) -> Self {
163 self.max_iterations = k;
164 self
165 }
166
167 pub fn tolerance(mut self, t: f64) -> Self {
168 self.tolerance = t;
169 self
170 }
171
172 pub fn execute<G: GraphStore>(&self, store: &G) -> GraphStoreResult<GraphPostingList> {
173 if !self.tolerance.is_finite() || self.tolerance < 0.0 {
174 return Err(GraphStoreError::InvalidMutation(format!(
175 "HITS tolerance must be finite and non-negative, got {}",
176 self.tolerance
177 )));
178 }
179 let vertices: Vec<VertexId> = store.vertex_ids_in_graph(self.graph)?.into_iter().collect();
180 if vertices.is_empty() {
181 return Ok(GraphPostingList::new());
182 }
183
184 let mut hub: BTreeMap<VertexId, f64> = vertices.iter().map(|v| (*v, 1.0)).collect();
185 let mut auth: BTreeMap<VertexId, f64> = vertices.iter().map(|v| (*v, 1.0)).collect();
186 let mut in_neighbors: BTreeMap<VertexId, Vec<VertexId>> = BTreeMap::new();
187 let mut out_neighbors: BTreeMap<VertexId, Vec<VertexId>> = BTreeMap::new();
188 for v in &vertices {
189 let mut ins = Vec::new();
190 for eid in store.in_edge_ids(*v, self.graph)? {
191 let edge = store.get_edge(eid).ok_or_else(|| {
192 GraphStoreError::CorruptGraph(format!("missing HITS edge {eid}"))
193 })?;
194 ins.push(edge.source_id);
195 }
196 in_neighbors.insert(*v, ins);
197 let mut outs = Vec::new();
198 for eid in store.out_edge_ids(*v, self.graph)? {
199 let edge = store.get_edge(eid).ok_or_else(|| {
200 GraphStoreError::CorruptGraph(format!("missing HITS edge {eid}"))
201 })?;
202 outs.push(edge.target_id);
203 }
204 out_neighbors.insert(*v, outs);
205 }
206
207 for _ in 0..self.max_iterations {
208 let mut new_auth: BTreeMap<VertexId, f64> = BTreeMap::new();
209 for v in &vertices {
210 let s = in_neighbors[v].iter().map(|u| hub[u]).sum::<f64>();
211 new_auth.insert(*v, s);
212 }
213 let mut new_hub: BTreeMap<VertexId, f64> = BTreeMap::new();
214 for v in &vertices {
215 let s = out_neighbors[v].iter().map(|w| new_auth[w]).sum::<f64>();
216 new_hub.insert(*v, s);
217 }
218 let auth_norm = new_auth.values().map(|x| x * x).sum::<f64>().sqrt();
219 let hub_norm = new_hub.values().map(|x| x * x).sum::<f64>().sqrt();
220 if auth_norm > 0.0 {
221 for v in &vertices {
222 let value = new_auth.get_mut(v).ok_or_else(|| {
223 GraphStoreError::CorruptGraph(format!(
224 "missing HITS authority state for vertex {v}"
225 ))
226 })?;
227 *value /= auth_norm;
228 }
229 }
230 if hub_norm > 0.0 {
231 for v in &vertices {
232 let value = new_hub.get_mut(v).ok_or_else(|| {
233 GraphStoreError::CorruptGraph(format!(
234 "missing HITS hub state for vertex {v}"
235 ))
236 })?;
237 *value /= hub_norm;
238 }
239 }
240 let delta: f64 = vertices
241 .iter()
242 .map(|v| (new_auth[v] - auth[v]).abs() + (new_hub[v] - hub[v]).abs())
243 .sum();
244 auth = new_auth;
245 hub = new_hub;
246 if delta < self.tolerance {
247 break;
248 }
249 }
250
251 let auth_n = min_max_normalize(&auth, &vertices)?;
252 let hub_n = min_max_normalize(&hub, &vertices)?;
253 let mut extra_fields: BTreeMap<VertexId, BTreeMap<String, Value>> = BTreeMap::new();
254 for v in &vertices {
255 let mut m: BTreeMap<String, Value> = BTreeMap::new();
256 m.insert("hub_score".into(), Value::Float(hub_n[v]));
257 m.insert("authority_score".into(), Value::Float(auth_n[v]));
258 extra_fields.insert(*v, m);
259 }
260 build_score_result(&vertices, &auth_n, self.graph, &extra_fields)
261 }
262}
263
264pub struct BetweennessCentrality<'a> {
270 pub graph: &'a str,
271}
272
273impl<'a> BetweennessCentrality<'a> {
274 pub fn new(graph: &'a str) -> Self {
275 Self { graph }
276 }
277
278 pub fn execute<G: GraphStore>(&self, store: &G) -> GraphStoreResult<GraphPostingList> {
279 let vertices: Vec<VertexId> = store.vertex_ids_in_graph(self.graph)?.into_iter().collect();
280 let n = vertices.len();
281 if n == 0 {
282 return Ok(GraphPostingList::new());
283 }
284 if n == 1 {
285 return single_vertex_result(vertices[0], 0.0, &vertices, self.graph);
286 }
287
288 let vertex_index: BTreeMap<VertexId, usize> = vertices
289 .iter()
290 .enumerate()
291 .map(|(idx, vertex_id)| (*vertex_id, idx))
292 .collect();
293 let mut out_neighbors: Vec<Vec<usize>> = vec![Vec::new(); n];
294 for (idx, vertex_id) in vertices.iter().enumerate() {
295 for eid in store.out_edge_ids(*vertex_id, self.graph)? {
296 let edge = store.get_edge(eid).ok_or_else(|| {
297 GraphStoreError::CorruptGraph(format!("missing betweenness edge {eid}"))
298 })?;
299 if let Some(target_idx) = vertex_index.get(&edge.target_id) {
300 out_neighbors[idx].push(*target_idx);
301 }
302 }
303 }
304
305 let mut cb = vec![0.0; n];
306 for s in 0..n {
307 let mut stack: Vec<usize> = Vec::with_capacity(n);
308 let mut predecessors: Vec<Vec<usize>> = vec![Vec::new(); n];
309 let mut sigma = vec![0u64; n];
310 sigma[s] = 1;
311 let mut dist = vec![-1i64; n];
312 dist[s] = 0;
313 let mut queue: VecDeque<usize> = VecDeque::new();
314 queue.push_back(s);
315 while let Some(v) = queue.pop_front() {
316 stack.push(v);
317 for &w in &out_neighbors[v] {
318 if dist[w] < 0 {
319 dist[w] = dist[v].checked_add(1).ok_or_else(|| {
320 GraphStoreError::CorruptGraph(
321 "betweenness path distance exceeds bigint range".into(),
322 )
323 })?;
324 queue.push_back(w);
325 }
326 if dist[w] == dist[v] + 1 {
327 sigma[w] = sigma[w].checked_add(sigma[v]).ok_or_else(|| {
328 GraphStoreError::CorruptGraph(
329 "betweenness shortest-path count exceeds u64".into(),
330 )
331 })?;
332 predecessors[w].push(v);
333 }
334 }
335 }
336 let mut delta = vec![0.0; n];
337 while let Some(w) = stack.pop() {
338 for &v in &predecessors[w] {
339 if sigma[w] > 0 {
340 let contrib = (u64_as_f64(sigma[v], "betweenness path count")?
341 / u64_as_f64(sigma[w], "betweenness path count")?)
342 * (1.0 + delta[w]);
343 delta[v] += contrib;
344 }
345 }
346 if w != s {
347 cb[w] += delta[w];
348 }
349 }
350 }
351
352 let normalization_count = (n - 1).checked_mul(n - 2).ok_or_else(|| {
353 GraphStoreError::CorruptGraph("betweenness normalization count overflow".into())
354 })?;
355 let normalization = usize_as_f64(normalization_count, "betweenness normalization")?;
356 if normalization > 0.0 {
357 for value in &mut cb {
358 *value /= normalization;
359 }
360 }
361 let cb: BTreeMap<VertexId, f64> = vertices
362 .iter()
363 .zip(cb)
364 .map(|(vertex_id, score)| (*vertex_id, score.clamp(0.0, 1.0)))
365 .collect();
366 build_score_result(&vertices, &cb, self.graph, &BTreeMap::new())
367 }
368}
369
370fn min_max_normalize(
371 scores: &BTreeMap<VertexId, f64>,
372 vertices: &[VertexId],
373) -> GraphStoreResult<BTreeMap<VertexId, f64>> {
374 let min_s = scores.values().copied().fold(f64::INFINITY, f64::min);
375 let max_s = scores.values().copied().fold(f64::NEG_INFINITY, f64::max);
376 if max_s - min_s > 0.0 {
377 vertices
378 .iter()
379 .map(|v| {
380 scores
381 .get(v)
382 .copied()
383 .map(|score| (*v, (score - min_s) / (max_s - min_s)))
384 .ok_or_else(|| {
385 GraphStoreError::CorruptGraph(format!(
386 "missing centrality score for vertex {v}"
387 ))
388 })
389 })
390 .collect()
391 } else {
392 Ok(vertices.iter().map(|v| (*v, 1.0)).collect())
393 }
394}
395
396fn single_vertex_result(
397 vid: VertexId,
398 score: f64,
399 vertices: &[VertexId],
400 graph: &str,
401) -> GraphStoreResult<GraphPostingList> {
402 let entry = PostingEntry::new(vid, Payload::with_score(score));
403 let mut graph_payloads: BTreeMap<DocId, GraphPayload> = BTreeMap::new();
404 graph_payloads.insert(
405 vid,
406 GraphPayload {
407 subgraph_vertices: vertices.to_vec(),
408 subgraph_edges: Vec::new(),
409 graph_name: graph.to_string(),
410 score_override: Some(score),
411 },
412 );
413 GraphPostingList::try_from_parts(
414 PostingList::from_sorted_unchecked(vec![entry]),
415 graph_payloads,
416 )
417 .map_err(Into::into)
418}
419
420fn build_score_result(
421 vertices: &[VertexId],
422 scores: &BTreeMap<VertexId, f64>,
423 graph: &str,
424 extra_fields: &BTreeMap<VertexId, BTreeMap<String, Value>>,
425) -> GraphStoreResult<GraphPostingList> {
426 let mut entries: Vec<PostingEntry> = Vec::with_capacity(vertices.len());
427 let mut graph_payloads: BTreeMap<DocId, GraphPayload> = BTreeMap::new();
428 let mut sorted = vertices.to_vec();
429 sorted.sort_unstable();
430 for vid in &sorted {
431 let score = *scores.get(vid).ok_or_else(|| {
432 GraphStoreError::CorruptGraph(format!("missing centrality score for vertex {vid}"))
433 })?;
434 let mut payload = Payload::with_score(score);
435 if let Some(fields) = extra_fields.get(vid) {
436 payload.fields = fields.clone();
437 }
438 entries.push(PostingEntry::new(*vid, payload));
439 graph_payloads.insert(
440 *vid,
441 GraphPayload {
442 subgraph_vertices: sorted.clone(),
443 subgraph_edges: Vec::new(),
444 graph_name: graph.to_string(),
445 score_override: Some(score),
446 },
447 );
448 }
449 GraphPostingList::try_from_parts(PostingList::from_sorted_unchecked(entries), graph_payloads)
450 .map_err(Into::into)
451}