1use crate::NQuin;
11use std::collections::{HashMap, HashSet};
12
13pub const MAX_HEAP_GRAPH_ANALYSIS_QUINS: usize = 4_096;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum GraphAnalysisError {
19 InputTooLarge,
20 NodeCapacityExceeded,
21 OutputBufferFull,
22}
23
24pub const MAX_BOUNDED_GRAPH_ANALYSIS_NODES: usize = 128;
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct CommunitySpan {
29 pub start: u16,
30 pub len: u16,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct TopNodeScore {
35 pub node_id: u64,
36 pub centrality_score: f32,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct MotifRecord {
41 pub pattern: MotifPattern,
42 pub node_a: u64,
43 pub node_b: u64,
44 pub node_c: u64,
45 pub frequency: f32,
46}
47
48pub const MAX_SUBGRAPH_PATTERN_NODES: usize = 8;
51
52#[derive(Debug, Clone, Copy)]
56pub struct SubgraphPattern {
57 pub adjacency: [[u8; MAX_SUBGRAPH_PATTERN_NODES]; MAX_SUBGRAPH_PATTERN_NODES],
58 pub node_count: usize,
59}
60
61impl SubgraphPattern {
62 pub fn new(node_count: usize) -> Self {
64 Self {
65 adjacency: [[0; MAX_SUBGRAPH_PATTERN_NODES]; MAX_SUBGRAPH_PATTERN_NODES],
66 node_count: node_count.min(MAX_SUBGRAPH_PATTERN_NODES),
67 }
68 }
69
70 pub fn with_edge(mut self, from: usize, to: usize) -> Self {
72 if from < self.node_count && to < self.node_count {
73 self.adjacency[from][to] = 1;
74 }
75 self
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct SubgraphMatch {
84 pub mapping: [u64; MAX_SUBGRAPH_PATTERN_NODES],
85 pub len: u16,
86 pub missing_edges: u8,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct BoundedGraphAnalysisSummary {
91 pub density: f32,
92 pub node_count: u16,
93 pub edge_count: u16,
94 pub community_count: u16,
95 pub motif_count: u16,
96 pub top_node_count: u16,
97 pub graph_quin_count: u16,
98}
99
100#[derive(Debug, Clone)]
101struct BoundedQualiaGraph {
102 node_ids: [u64; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
103 centrality_scores: [f32; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
104 degrees: [u16; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
105 adjacency: [[u8; MAX_BOUNDED_GRAPH_ANALYSIS_NODES]; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
106 node_count: usize,
107 edge_count: usize,
108}
109
110impl BoundedQualiaGraph {
111 fn from_quins(quins: &[NQuin]) -> Result<Self, GraphAnalysisError> {
112 let mut graph = Self {
113 node_ids: [0; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
114 centrality_scores: [0.0; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
115 degrees: [0; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
116 adjacency: [[0; MAX_BOUNDED_GRAPH_ANALYSIS_NODES]; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
117 node_count: 0,
118 edge_count: 0,
119 };
120
121 for quin in quins {
122 let source = graph.get_or_insert_node(quin.subject)?;
123 let target = graph.get_or_insert_node(quin.object)?;
124 if graph.adjacency[source][target] == 0 {
125 graph.adjacency[source][target] = 1;
126 graph.degrees[source] = graph.degrees[source].saturating_add(1);
127 graph.edge_count += 1;
128 }
129 }
130
131 Ok(graph)
132 }
133
134 fn get_or_insert_node(&mut self, node_id: u64) -> Result<usize, GraphAnalysisError> {
135 for index in 0..self.node_count {
136 if self.node_ids[index] == node_id {
137 return Ok(index);
138 }
139 }
140
141 if self.node_count >= MAX_BOUNDED_GRAPH_ANALYSIS_NODES {
142 return Err(GraphAnalysisError::NodeCapacityExceeded);
143 }
144
145 let index = self.node_count;
146 self.node_ids[index] = node_id;
147 self.node_count += 1;
148 Ok(index)
149 }
150
151 fn calculate_betweenness_centrality(&mut self) {
152 let n = self.node_count;
153 if n < 2 {
154 return;
155 }
156
157 let mut scores = [0f32; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
158 let mut sigma = [0f32; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
159 let mut dist = [-1i16; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
160 let mut delta = [0f32; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
161 let mut queue = [0usize; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
162 let mut stack = [0usize; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
163
164 for source in 0..n {
165 sigma[..n].fill(0.0);
166 dist[..n].fill(-1);
167 delta[..n].fill(0.0);
168
169 let mut queue_head = 0usize;
170 let mut queue_tail = 0usize;
171 let mut stack_len = 0usize;
172
173 sigma[source] = 1.0;
174 dist[source] = 0;
175 queue[queue_tail] = source;
176 queue_tail += 1;
177
178 while queue_head < queue_tail {
179 let v = queue[queue_head];
180 queue_head += 1;
181 stack[stack_len] = v;
182 stack_len += 1;
183
184 for w in 0..n {
185 if self.adjacency[v][w] == 0 {
186 continue;
187 }
188 if dist[w] < 0 {
189 dist[w] = dist[v] + 1;
190 queue[queue_tail] = w;
191 queue_tail += 1;
192 }
193 if dist[w] == dist[v] + 1 {
194 sigma[w] += sigma[v];
195 }
196 }
197 }
198
199 while stack_len > 0 {
200 stack_len -= 1;
201 let w = stack[stack_len];
202 for v in 0..n {
203 if self.adjacency[v][w] == 0 || dist[v] != dist[w] - 1 || sigma[w] == 0.0 {
204 continue;
205 }
206 delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w]);
207 }
208 if w != source {
209 scores[w] += delta[w];
210 }
211 }
212 }
213
214 let norm = if n > 2 {
215 ((n - 1) * (n - 2)) as f32
216 } else {
217 1.0
218 };
219 for (index, score) in scores.iter().take(n).enumerate() {
220 self.centrality_scores[index] = if norm > 0.0 { *score / norm } else { *score };
221 }
222 }
223
224 fn density(&self) -> f32 {
225 if self.node_count < 2 {
226 return 0.0;
227 }
228 let possible_edges = (self.node_count * (self.node_count - 1)) as f32;
229 self.edge_count as f32 / possible_edges
230 }
231
232 fn write_communities(
233 &self,
234 community_nodes_out: &mut [u64],
235 community_spans_out: &mut [CommunitySpan],
236 ) -> Result<usize, GraphAnalysisError> {
237 let mut visited = [false; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
238 let mut stack = [0usize; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
239 let mut nodes_written = 0usize;
240 let mut communities_written = 0usize;
241
242 for start in 0..self.node_count {
243 if visited[start] {
244 continue;
245 }
246 if communities_written >= community_spans_out.len() {
247 return Err(GraphAnalysisError::OutputBufferFull);
248 }
249 let span_start = nodes_written;
250 let mut stack_len = 0usize;
251 stack[stack_len] = start;
252 stack_len += 1;
253 visited[start] = true;
254
255 while stack_len > 0 {
256 stack_len -= 1;
257 let node = stack[stack_len];
258 if nodes_written >= community_nodes_out.len() {
259 return Err(GraphAnalysisError::OutputBufferFull);
260 }
261 community_nodes_out[nodes_written] = self.node_ids[node];
262 nodes_written += 1;
263
264 for neighbor in 0..self.node_count {
265 let linked =
266 self.adjacency[node][neighbor] != 0 || self.adjacency[neighbor][node] != 0;
267 if linked && !visited[neighbor] {
268 visited[neighbor] = true;
269 stack[stack_len] = neighbor;
270 stack_len += 1;
271 }
272 }
273 }
274
275 community_spans_out[communities_written] = CommunitySpan {
276 start: span_start as u16,
277 len: (nodes_written - span_start) as u16,
278 };
279 communities_written += 1;
280 }
281
282 Ok(communities_written)
283 }
284
285 fn write_motifs(&self, motifs_out: &mut [MotifRecord]) -> Result<usize, GraphAnalysisError> {
286 let mut written = 0usize;
287
288 for a in 0..self.node_count {
289 for b in 0..self.node_count {
290 if self.adjacency[a][b] == 0 {
291 continue;
292 }
293 for c in 0..self.node_count {
294 if c == a || self.adjacency[b][c] == 0 || self.adjacency[c][a] == 0 {
295 continue;
296 }
297
298 let mut canonical = [self.node_ids[a], self.node_ids[b], self.node_ids[c]];
299 canonical.sort_unstable();
300 if motifs_out[..written].iter().any(|m| {
301 let mut existing = [m.node_a, m.node_b, m.node_c];
302 existing.sort_unstable();
303 existing == canonical
304 }) {
305 continue;
306 }
307
308 if written >= motifs_out.len() {
309 return Err(GraphAnalysisError::OutputBufferFull);
310 }
311 motifs_out[written] = MotifRecord {
312 pattern: MotifPattern::Triangle,
313 node_a: self.node_ids[a],
314 node_b: self.node_ids[b],
315 node_c: self.node_ids[c],
316 frequency: 1.0,
317 };
318 written += 1;
319 }
320 }
321 }
322
323 Ok(written)
324 }
325
326 fn write_top_nodes(&self, out: &mut [TopNodeScore]) -> usize {
327 let count = out.len().min(self.node_count);
328 if count == 0 {
329 return 0;
330 }
331
332 let mut scratch = [TopNodeScore {
333 node_id: 0,
334 centrality_score: f32::MIN,
335 }; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
336 for i in 0..self.node_count {
337 scratch[i] = TopNodeScore {
338 node_id: self.node_ids[i],
339 centrality_score: self.centrality_scores[i],
340 };
341 }
342 scratch[..self.node_count].sort_by(|a, b| {
343 b.centrality_score
344 .partial_cmp(&a.centrality_score)
345 .unwrap_or(std::cmp::Ordering::Equal)
346 });
347 out[..count].copy_from_slice(&scratch[..count]);
348 count
349 }
350
351 fn write_graph_quins(
352 &self,
353 context: u64,
354 out: &mut [NQuin],
355 ) -> Result<usize, GraphAnalysisError> {
356 if out.len() < self.node_count {
357 return Err(GraphAnalysisError::OutputBufferFull);
358 }
359
360 for i in 0..self.node_count {
361 let mut quin = NQuin {
362 subject: self.node_ids[i],
363 predicate: crate::q_hash("has_centrality_score"),
364 object: (self.centrality_scores[i] as f64 * 1000.0) as u64,
365 context,
366 metadata: self.degrees[i] as u64,
367 parity: 0,
368 };
369 quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
370 out[i] = quin;
371 }
372
373 Ok(self.node_count)
374 }
375
376 fn calculate_pagerank(
384 &self,
385 damping: f32,
386 max_iters: u32,
387 tolerance: f32,
388 out: &mut [f32],
389 ) -> usize {
390 let n = self.node_count;
391 if n == 0 {
392 return 0;
393 }
394 let count = out.len().min(n);
395
396 let mut rank = [0f32; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
397 let mut next = [0f32; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
398 let mut outdeg = [0u16; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
399
400 let init = 1.0 / n as f32;
401 rank[..n].fill(init);
402 for v in 0..n {
403 let mut d = 0u16;
404 for w in 0..n {
405 if self.adjacency[v][w] != 0 {
406 d += 1;
407 }
408 }
409 outdeg[v] = d;
410 }
411
412 let base = (1.0 - damping) / n as f32;
413 for _ in 0..max_iters {
414 let mut dangling = 0f32;
415 for v in 0..n {
416 if outdeg[v] == 0 {
417 dangling += rank[v];
418 }
419 }
420 let dangling_share = damping * dangling / n as f32;
421 next[..n].fill(base + dangling_share);
422
423 for v in 0..n {
424 if outdeg[v] == 0 {
425 continue;
426 }
427 let share = damping * rank[v] / outdeg[v] as f32;
428 for w in 0..n {
429 if self.adjacency[v][w] != 0 {
430 next[w] += share;
431 }
432 }
433 }
434
435 let mut delta = 0f32;
436 for v in 0..n {
437 delta += (next[v] - rank[v]).abs();
438 rank[v] = next[v];
439 }
440 if delta < tolerance {
441 break;
442 }
443 }
444
445 out[..count].copy_from_slice(&rank[..count]);
446 count
447 }
448
449 fn find_subgraph_isomorphisms(
458 &self,
459 pattern: &SubgraphPattern,
460 max_missing_edges: u8,
461 out: &mut [SubgraphMatch],
462 ) -> usize {
463 if pattern.node_count == 0
464 || pattern.node_count > MAX_SUBGRAPH_PATTERN_NODES
465 || pattern.node_count > self.node_count
466 || out.is_empty()
467 {
468 return 0;
469 }
470 let mut assign = [0usize; MAX_SUBGRAPH_PATTERN_NODES];
471 let mut used = [false; MAX_BOUNDED_GRAPH_ANALYSIS_NODES];
472 let mut written = 0usize;
473 self.match_subgraph(
474 pattern,
475 0,
476 0,
477 max_missing_edges,
478 &mut assign,
479 &mut used,
480 out,
481 &mut written,
482 );
483 written
484 }
485
486 #[allow(clippy::too_many_arguments)]
487 fn match_subgraph(
488 &self,
489 pattern: &SubgraphPattern,
490 depth: usize,
491 missing: u8,
492 max_missing_edges: u8,
493 assign: &mut [usize; MAX_SUBGRAPH_PATTERN_NODES],
494 used: &mut [bool; MAX_BOUNDED_GRAPH_ANALYSIS_NODES],
495 out: &mut [SubgraphMatch],
496 written: &mut usize,
497 ) {
498 if *written >= out.len() {
499 return;
500 }
501 if depth == pattern.node_count {
502 let mut m = SubgraphMatch {
503 mapping: [0; MAX_SUBGRAPH_PATTERN_NODES],
504 len: pattern.node_count as u16,
505 missing_edges: missing,
506 };
507 for (p, slot) in m.mapping.iter_mut().take(pattern.node_count).enumerate() {
508 *slot = self.node_ids[assign[p]];
509 }
510 out[*written] = m;
511 *written += 1;
512 return;
513 }
514
515 for candidate in 0..self.node_count {
516 if used[candidate] {
517 continue;
518 }
519 let mut local_missing = 0u8;
521 for prev in 0..depth {
522 let pd = assign[prev];
523 if pattern.adjacency[prev][depth] != 0 && self.adjacency[pd][candidate] == 0 {
524 local_missing += 1;
525 }
526 if pattern.adjacency[depth][prev] != 0 && self.adjacency[candidate][pd] == 0 {
527 local_missing += 1;
528 }
529 }
530 if missing + local_missing > max_missing_edges {
531 continue;
532 }
533 assign[depth] = candidate;
534 used[candidate] = true;
535 self.match_subgraph(
536 pattern,
537 depth + 1,
538 missing + local_missing,
539 max_missing_edges,
540 assign,
541 used,
542 out,
543 written,
544 );
545 used[candidate] = false;
546 if *written >= out.len() {
547 return;
548 }
549 }
550 }
551}
552
553pub fn analyze_graph_topology_bounded(
555 quins: &[NQuin],
556 context: u64,
557 graph_quins_out: &mut [NQuin],
558 community_nodes_out: &mut [u64],
559 community_spans_out: &mut [CommunitySpan],
560 motifs_out: &mut [MotifRecord],
561 top_nodes_out: &mut [TopNodeScore],
562) -> Result<BoundedGraphAnalysisSummary, GraphAnalysisError> {
563 if quins.len() > MAX_HEAP_GRAPH_ANALYSIS_QUINS {
564 return Err(GraphAnalysisError::InputTooLarge);
565 }
566
567 let mut graph = BoundedQualiaGraph::from_quins(quins)?;
568 graph.calculate_betweenness_centrality();
569
570 let graph_quin_count = graph.write_graph_quins(context, graph_quins_out)?;
571 let community_count = graph.write_communities(community_nodes_out, community_spans_out)?;
572 let motif_count = graph.write_motifs(motifs_out)?;
573 let top_node_count = graph.write_top_nodes(top_nodes_out);
574
575 Ok(BoundedGraphAnalysisSummary {
576 density: graph.density(),
577 node_count: graph.node_count as u16,
578 edge_count: graph.edge_count as u16,
579 community_count: community_count as u16,
580 motif_count: motif_count as u16,
581 top_node_count: top_node_count as u16,
582 graph_quin_count: graph_quin_count as u16,
583 })
584}
585
586pub fn pagerank_bounded(
593 quins: &[NQuin],
594 damping: f32,
595 max_iters: u32,
596 tolerance: f32,
597 node_ids_out: &mut [u64],
598 scores_out: &mut [f32],
599) -> Result<usize, GraphAnalysisError> {
600 if quins.len() > MAX_HEAP_GRAPH_ANALYSIS_QUINS {
601 return Err(GraphAnalysisError::InputTooLarge);
602 }
603 let graph = BoundedQualiaGraph::from_quins(quins)?;
604 let count = graph.calculate_pagerank(damping, max_iters, tolerance, scores_out);
605 if node_ids_out.len() < count {
606 return Err(GraphAnalysisError::OutputBufferFull);
607 }
608 node_ids_out[..count].copy_from_slice(&graph.node_ids[..count]);
609 Ok(count)
610}
611
612pub fn find_subgraph_isomorphisms_bounded(
619 quins: &[NQuin],
620 pattern: &SubgraphPattern,
621 max_missing_edges: u8,
622 matches_out: &mut [SubgraphMatch],
623) -> Result<usize, GraphAnalysisError> {
624 if quins.len() > MAX_HEAP_GRAPH_ANALYSIS_QUINS {
625 return Err(GraphAnalysisError::InputTooLarge);
626 }
627 let graph = BoundedQualiaGraph::from_quins(quins)?;
628 Ok(graph.find_subgraph_isomorphisms(pattern, max_missing_edges, matches_out))
629}
630
631#[derive(Debug, Clone)]
633pub struct QualiaGraph {
634 pub nodes: HashMap<u64, GraphNode>,
635 pub edges: HashMap<(u64, u64), GraphEdge>,
636 pub adjacency_list: HashMap<u64, Vec<u64>>,
637}
638
639#[derive(Debug, Clone)]
640pub struct GraphNode {
641 pub id: u64,
642 pub degree: usize,
643 pub centrality_score: f64,
644 pub community_id: Option<usize>,
645}
646
647#[derive(Debug, Clone)]
648pub struct GraphEdge {
649 pub source: u64,
650 pub target: u64,
651 pub weight: f64,
652}
653
654impl QualiaGraph {
655 pub fn from_quins(quins: &[NQuin]) -> Self {
657 let mut nodes = HashMap::new();
658 let mut edges = HashMap::new();
659 let mut adjacency_list = HashMap::new();
660
661 for quin in quins {
663 nodes.entry(quin.subject).or_insert_with(|| GraphNode {
665 id: quin.subject,
666 degree: 0,
667 centrality_score: 0.0,
668 community_id: None,
669 });
670
671 nodes.entry(quin.object).or_insert_with(|| GraphNode {
673 id: quin.object,
674 degree: 0,
675 centrality_score: 0.0,
676 community_id: None,
677 });
678
679 let edge = GraphEdge {
681 source: quin.subject,
682 target: quin.object,
683 weight: 1.0, };
685 edges.insert((quin.subject, quin.object), edge);
686
687 adjacency_list
689 .entry(quin.subject)
690 .or_insert_with(Vec::new)
691 .push(quin.object);
692
693 if let Some(node) = nodes.get_mut(&quin.subject) {
695 node.degree += 1;
696 }
697 }
698
699 Self {
700 nodes,
701 edges,
702 adjacency_list,
703 }
704 }
705
706 pub fn calculate_betweenness_centrality(&mut self) {
708 let node_ids: Vec<u64> = self.nodes.keys().cloned().collect();
713 let mut scores: HashMap<u64, f64> = node_ids.iter().map(|&id| (id, 0.0)).collect();
714
715 for &source in &node_ids {
716 let mut sigma: HashMap<u64, f64> = node_ids.iter().map(|&id| (id, 0.0)).collect();
717 let mut dist: HashMap<u64, i64> = node_ids.iter().map(|&id| (id, -1)).collect();
718 let mut pred: HashMap<u64, Vec<u64>> =
720 node_ids.iter().map(|&id| (id, Vec::new())).collect();
721 let mut stack: Vec<u64> = Vec::new();
722 let mut queue: std::collections::VecDeque<u64> = std::collections::VecDeque::new();
724
725 sigma.insert(source, 1.0);
726 dist.insert(source, 0);
727 queue.push_back(source);
728
729 while let Some(v) = queue.pop_front() {
731 stack.push(v);
732 let v_dist = dist[&v];
733 if let Some(neighbors) = self.adjacency_list.get(&v) {
734 for &w in neighbors {
735 if dist[&w] < 0 {
737 queue.push_back(w);
738 dist.insert(w, v_dist + 1);
739 }
740 if dist[&w] == v_dist + 1 {
742 *sigma.get_mut(&w).unwrap() += sigma[&v];
743 pred.get_mut(&w).unwrap().push(v);
744 }
745 }
746 }
747 }
748
749 let mut delta: HashMap<u64, f64> = node_ids.iter().map(|&id| (id, 0.0)).collect();
751 while let Some(w) = stack.pop() {
752 for &v in &pred[&w] {
753 let coeff = (sigma[&v] / sigma[&w]) * (1.0 + delta[&w]);
754 *delta.get_mut(&v).unwrap() += coeff;
755 }
756 if w != source {
757 *scores.get_mut(&w).unwrap() += delta[&w];
758 }
759 }
760 }
761
762 let n = self.nodes.len() as f64;
764 if n > 2.0 {
765 let norm = (n - 1.0) * (n - 2.0);
766 for score in scores.values_mut() {
767 *score /= norm;
768 }
769 }
770
771 for (node_id, score) in scores {
773 if let Some(node) = self.nodes.get_mut(&node_id) {
774 node.centrality_score = score;
775 }
776 }
777 }
778
779 pub fn detect_communities(&mut self) -> Vec<Vec<u64>> {
781 let mut communities: Vec<Vec<u64>> =
782 self.nodes.keys().cloned().map(|id| vec![id]).collect();
783 let mut improved = true;
784
785 while improved {
786 improved = false;
787
788 for i in 0..communities.len() {
789 if i >= communities.len() {
790 break;
791 }
792
793 let current_community = communities[i].clone();
794 let best_move = self.find_best_community_move(¤t_community, &communities, i);
795
796 if let Some((target_community, modularity_gain)) = best_move {
797 if modularity_gain > 0.0 {
798 communities[target_community].extend(¤t_community);
800 communities.remove(i);
801 improved = true;
802 break;
803 }
804 }
805 }
806 }
807
808 for (community_id, community) in communities.iter().enumerate() {
810 for &node_id in community {
811 if let Some(node) = self.nodes.get_mut(&node_id) {
812 node.community_id = Some(community_id);
813 }
814 }
815 }
816
817 communities
818 }
819
820 fn find_best_community_move(
822 &self,
823 community: &[u64],
824 all_communities: &[Vec<u64>],
825 current_index: usize,
826 ) -> Option<(usize, f64)> {
827 let mut best_move = None;
828 let mut best_gain = 0.0;
829
830 for (i, other_community) in all_communities.iter().enumerate() {
831 if i == current_index {
832 continue;
833 }
834
835 let gain = self.calculate_modularity_gain(community, other_community);
836 if gain > best_gain {
837 best_gain = gain;
838 best_move = Some((i, gain));
839 }
840 }
841
842 best_move
843 }
844
845 fn calculate_modularity_gain(&self, comm1: &[u64], comm2: &[u64]) -> f64 {
852 let m = self.edges.len() as f64;
853 if m == 0.0 {
854 return 0.0;
855 }
856
857 let set1: HashSet<u64> = comm1.iter().cloned().collect();
858 let set2: HashSet<u64> = comm2.iter().cloned().collect();
859
860 let mut e_ij = 0.0;
862 for &(source, target) in self.edges.keys() {
863 if (set1.contains(&source) && set2.contains(&target))
864 || (set2.contains(&source) && set1.contains(&target))
865 {
866 e_ij += 1.0;
867 }
868 }
869
870 let a1: f64 = comm1
872 .iter()
873 .filter_map(|id| self.nodes.get(id))
874 .map(|n| n.degree as f64)
875 .sum();
876 let a2: f64 = comm2
877 .iter()
878 .filter_map(|id| self.nodes.get(id))
879 .map(|n| n.degree as f64)
880 .sum();
881
882 (e_ij / m) - (a1 * a2) / (2.0 * m * m)
883 }
884
885 pub fn find_motifs(&self) -> Vec<Motif> {
887 let mut seen: HashSet<[u64; 3]> = HashSet::new();
891 let mut motifs = Vec::new();
892
893 for &node_a in self.nodes.keys() {
894 if let Some(neighbors_a) = self.adjacency_list.get(&node_a) {
895 for &node_b in neighbors_a {
896 if let Some(neighbors_b) = self.adjacency_list.get(&node_b) {
897 for &node_c in neighbors_b {
898 if node_c != node_a {
899 if let Some(neighbors_c) = self.adjacency_list.get(&node_c) {
901 if neighbors_c.contains(&node_a) {
902 let mut key = [node_a, node_b, node_c];
904 key.sort_unstable();
905 if seen.insert(key) {
906 motifs.push(Motif {
907 pattern: MotifPattern::Triangle,
908 nodes: vec![node_a, node_b, node_c],
909 frequency: 1.0,
910 });
911 }
912 }
913 }
914 }
915 }
916 }
917 }
918 }
919 }
920
921 motifs
922 }
923
924 pub fn get_top_central_nodes(&self, top_n: usize) -> Vec<(u64, f64)> {
926 let mut nodes: Vec<(u64, f64)> = self
927 .nodes
928 .iter()
929 .map(|(id, node)| (*id, node.centrality_score))
930 .collect();
931
932 nodes.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
933 nodes.truncate(top_n);
934 nodes
935 }
936
937 pub fn density(&self) -> f64 {
939 let n = self.nodes.len();
940 if n < 2 {
941 return 0.0;
942 }
943
944 let possible_edges = n * (n - 1);
945 self.edges.len() as f64 / possible_edges as f64
946 }
947
948 pub fn to_quins(&self, context: u64) -> Vec<NQuin> {
950 let mut quins = Vec::new();
951
952 for (node_id, node) in &self.nodes {
954 let mut quin = NQuin {
955 subject: *node_id,
956 predicate: crate::q_hash("has_centrality_score"),
957 object: (node.centrality_score * 1000.0) as u64, context,
959 metadata: 0,
960 parity: 0,
961 };
962
963 quin.metadata = node.degree as u64;
965 quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
966 quins.push(quin);
967 }
968
969 quins
970 }
971}
972
973#[derive(Debug, Clone, Copy, PartialEq, Eq)]
975pub enum MotifPattern {
976 Triangle,
977 Chain,
978 Star,
979 Fork,
980}
981
982#[derive(Debug, Clone)]
984pub struct Motif {
985 pub pattern: MotifPattern,
986 pub nodes: Vec<u64>,
987 pub frequency: f64,
988}
989
990pub fn analyze_graph_topology(
992 quins: &[NQuin],
993 context: u64,
994) -> Result<GraphAnalysisResult, GraphAnalysisError> {
995 if quins.len() > MAX_HEAP_GRAPH_ANALYSIS_QUINS {
996 return Err(GraphAnalysisError::InputTooLarge);
997 }
998
999 let mut graph = QualiaGraph::from_quins(quins);
1000
1001 graph.calculate_betweenness_centrality();
1003
1004 let communities = graph.detect_communities();
1006
1007 let motifs = graph.find_motifs();
1009
1010 let top_nodes = graph.get_top_central_nodes(10);
1012
1013 let density = graph.density();
1015
1016 let graph_quins = graph.to_quins(context);
1018
1019 Ok(GraphAnalysisResult {
1020 graph_quins,
1021 communities,
1022 motifs,
1023 top_nodes,
1024 density,
1025 node_count: graph.nodes.len(),
1026 edge_count: graph.edges.len(),
1027 })
1028}
1029
1030#[derive(Debug, Clone)]
1032pub struct GraphAnalysisResult {
1033 pub graph_quins: Vec<NQuin>,
1034 pub communities: Vec<Vec<u64>>,
1035 pub motifs: Vec<Motif>,
1036 pub top_nodes: Vec<(u64, f64)>,
1037 pub density: f64,
1038 pub node_count: usize,
1039 pub edge_count: usize,
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045
1046 #[test]
1047 fn test_graph_creation() {
1048 let quins = vec![
1049 NQuin {
1050 subject: 1,
1051 predicate: crate::q_hash("connects_to"),
1052 object: 2,
1053 context: 100,
1054 metadata: 0,
1055 parity: 0,
1056 },
1057 NQuin {
1058 subject: 2,
1059 predicate: crate::q_hash("connects_to"),
1060 object: 3,
1061 context: 100,
1062 metadata: 0,
1063 parity: 0,
1064 },
1065 NQuin {
1066 subject: 1,
1067 predicate: crate::q_hash("connects_to"),
1068 object: 3,
1069 context: 100,
1070 metadata: 0,
1071 parity: 0,
1072 },
1073 ];
1074
1075 let graph = QualiaGraph::from_quins(&quins);
1076
1077 assert_eq!(graph.nodes.len(), 3);
1078 assert_eq!(graph.edges.len(), 3);
1079 assert_eq!(graph.adjacency_list.get(&1).unwrap().len(), 2);
1080 }
1081
1082 #[test]
1083 fn test_centrality_calculation() {
1084 let quins = vec![
1085 NQuin {
1086 subject: 1,
1087 predicate: 1,
1088 object: 2,
1089 context: 100,
1090 metadata: 0,
1091 parity: 0,
1092 },
1093 NQuin {
1094 subject: 2,
1095 predicate: 1,
1096 object: 3,
1097 context: 100,
1098 metadata: 0,
1099 parity: 0,
1100 },
1101 ];
1102
1103 let mut graph = QualiaGraph::from_quins(&quins);
1104 graph.calculate_betweenness_centrality();
1105
1106 let node2_centrality = graph.nodes.get(&2).unwrap().centrality_score;
1108 let node1_centrality = graph.nodes.get(&1).unwrap().centrality_score;
1109
1110 assert!(node2_centrality > node1_centrality);
1111 }
1112
1113 #[test]
1114 fn test_community_detection() {
1115 let quins = vec![
1116 NQuin {
1117 subject: 1,
1118 predicate: 1,
1119 object: 2,
1120 context: 100,
1121 metadata: 0,
1122 parity: 0,
1123 },
1124 NQuin {
1125 subject: 2,
1126 predicate: 1,
1127 object: 1,
1128 context: 100,
1129 metadata: 0,
1130 parity: 0,
1131 },
1132 NQuin {
1133 subject: 3,
1134 predicate: 1,
1135 object: 4,
1136 context: 100,
1137 metadata: 0,
1138 parity: 0,
1139 },
1140 NQuin {
1141 subject: 4,
1142 predicate: 1,
1143 object: 3,
1144 context: 100,
1145 metadata: 0,
1146 parity: 0,
1147 },
1148 ];
1149
1150 let mut graph = QualiaGraph::from_quins(&quins);
1151 let communities = graph.detect_communities();
1152
1153 assert_eq!(communities.len(), 2);
1155 }
1156
1157 #[test]
1158 fn test_motif_detection() {
1159 let quins = vec![
1160 NQuin {
1161 subject: 1,
1162 predicate: 1,
1163 object: 2,
1164 context: 100,
1165 metadata: 0,
1166 parity: 0,
1167 },
1168 NQuin {
1169 subject: 2,
1170 predicate: 1,
1171 object: 3,
1172 context: 100,
1173 metadata: 0,
1174 parity: 0,
1175 },
1176 NQuin {
1177 subject: 3,
1178 predicate: 1,
1179 object: 1,
1180 context: 100,
1181 metadata: 0,
1182 parity: 0,
1183 },
1184 ];
1185
1186 let graph = QualiaGraph::from_quins(&quins);
1187 let motifs = graph.find_motifs();
1188
1189 assert_eq!(motifs.len(), 1);
1191 assert_eq!(motifs[0].pattern, MotifPattern::Triangle);
1192 }
1193
1194 #[test]
1195 fn test_graph_analysis() {
1196 let quins = vec![
1197 NQuin {
1198 subject: 1,
1199 predicate: 1,
1200 object: 2,
1201 context: 100,
1202 metadata: 0,
1203 parity: 0,
1204 },
1205 NQuin {
1206 subject: 2,
1207 predicate: 1,
1208 object: 3,
1209 context: 100,
1210 metadata: 0,
1211 parity: 0,
1212 },
1213 NQuin {
1214 subject: 3,
1215 predicate: 1,
1216 object: 1,
1217 context: 100,
1218 metadata: 0,
1219 parity: 0,
1220 },
1221 ];
1222
1223 let result = analyze_graph_topology(&quins, 100).unwrap();
1224
1225 assert_eq!(result.node_count, 3);
1226 assert_eq!(result.edge_count, 3);
1227 assert!(result.density > 0.0);
1228 assert!(!result.communities.is_empty());
1229 }
1230
1231 #[test]
1232 fn test_graph_analysis_rejects_oversized_batches() {
1233 let quins = vec![
1234 NQuin {
1235 subject: 1,
1236 predicate: 1,
1237 object: 2,
1238 context: 100,
1239 metadata: 0,
1240 parity: 0,
1241 };
1242 MAX_HEAP_GRAPH_ANALYSIS_QUINS + 1
1243 ];
1244
1245 let result = analyze_graph_topology(&quins, 100);
1246
1247 assert!(matches!(result, Err(GraphAnalysisError::InputTooLarge)));
1248 }
1249
1250 #[test]
1251 fn test_bounded_graph_analysis_zero_heap_path() {
1252 let quins = vec![
1253 NQuin {
1254 subject: 1,
1255 predicate: 1,
1256 object: 2,
1257 context: 100,
1258 metadata: 0,
1259 parity: 0,
1260 },
1261 NQuin {
1262 subject: 2,
1263 predicate: 1,
1264 object: 3,
1265 context: 100,
1266 metadata: 0,
1267 parity: 0,
1268 },
1269 NQuin {
1270 subject: 3,
1271 predicate: 1,
1272 object: 1,
1273 context: 100,
1274 metadata: 0,
1275 parity: 0,
1276 },
1277 ];
1278 let mut graph_quins = [NQuin::default(); 16];
1279 let mut community_nodes = [0u64; 16];
1280 let mut community_spans = [CommunitySpan { start: 0, len: 0 }; 8];
1281 let mut motifs = [MotifRecord {
1282 pattern: MotifPattern::Triangle,
1283 node_a: 0,
1284 node_b: 0,
1285 node_c: 0,
1286 frequency: 0.0,
1287 }; 8];
1288 let mut top_nodes = [TopNodeScore {
1289 node_id: 0,
1290 centrality_score: 0.0,
1291 }; 8];
1292
1293 let summary = analyze_graph_topology_bounded(
1294 &quins,
1295 100,
1296 &mut graph_quins,
1297 &mut community_nodes,
1298 &mut community_spans,
1299 &mut motifs,
1300 &mut top_nodes,
1301 )
1302 .unwrap();
1303
1304 assert_eq!(summary.node_count, 3);
1305 assert_eq!(summary.edge_count, 3);
1306 assert_eq!(summary.community_count, 1);
1307 assert_eq!(summary.motif_count, 1);
1308 assert!(summary.top_node_count >= 1);
1309 assert!(summary.density > 0.0);
1310 }
1311
1312 #[test]
1313 fn test_pagerank_sums_to_one_and_ranks_hub() {
1314 let quins = vec![
1316 NQuin {
1317 subject: 1,
1318 predicate: 1,
1319 object: 3,
1320 context: 0,
1321 metadata: 0,
1322 parity: 0,
1323 },
1324 NQuin {
1325 subject: 2,
1326 predicate: 1,
1327 object: 3,
1328 context: 0,
1329 metadata: 0,
1330 parity: 0,
1331 },
1332 NQuin {
1333 subject: 4,
1334 predicate: 1,
1335 object: 3,
1336 context: 0,
1337 metadata: 0,
1338 parity: 0,
1339 },
1340 NQuin {
1341 subject: 3,
1342 predicate: 1,
1343 object: 1,
1344 context: 0,
1345 metadata: 0,
1346 parity: 0,
1347 },
1348 ];
1349 let mut ids = [0u64; 16];
1350 let mut scores = [0f32; 16];
1351 let count = pagerank_bounded(&quins, 0.85, 100, 1e-6, &mut ids, &mut scores).unwrap();
1352 assert_eq!(count, 4);
1353
1354 let total: f32 = scores[..count].iter().sum();
1356 assert!(
1357 (total - 1.0).abs() < 1e-3,
1358 "pagerank should sum to 1, got {total}"
1359 );
1360
1361 let hub_pos = ids[..count].iter().position(|&id| id == 3).unwrap();
1363 let hub_score = scores[hub_pos];
1364 for i in 0..count {
1365 if i != hub_pos {
1366 assert!(
1367 hub_score > scores[i],
1368 "hub node 3 should outrank node {}",
1369 ids[i]
1370 );
1371 }
1372 }
1373 }
1374
1375 #[test]
1376 fn test_subgraph_isomorphism_exact_triangle() {
1377 let quins = vec![
1379 NQuin {
1380 subject: 1,
1381 predicate: 1,
1382 object: 2,
1383 context: 0,
1384 metadata: 0,
1385 parity: 0,
1386 },
1387 NQuin {
1388 subject: 2,
1389 predicate: 1,
1390 object: 3,
1391 context: 0,
1392 metadata: 0,
1393 parity: 0,
1394 },
1395 NQuin {
1396 subject: 3,
1397 predicate: 1,
1398 object: 1,
1399 context: 0,
1400 metadata: 0,
1401 parity: 0,
1402 },
1403 NQuin {
1404 subject: 3,
1405 predicate: 1,
1406 object: 4,
1407 context: 0,
1408 metadata: 0,
1409 parity: 0,
1410 },
1411 ];
1412 let pattern = SubgraphPattern::new(3)
1414 .with_edge(0, 1)
1415 .with_edge(1, 2)
1416 .with_edge(2, 0);
1417 let mut matches = [SubgraphMatch {
1418 mapping: [0; MAX_SUBGRAPH_PATTERN_NODES],
1419 len: 0,
1420 missing_edges: 0,
1421 }; 16];
1422 let n = find_subgraph_isomorphisms_bounded(&quins, &pattern, 0, &mut matches).unwrap();
1423
1424 assert_eq!(n, 3, "expected 3 rotations of the directed triangle");
1426 for m in &matches[..n] {
1427 assert_eq!(m.missing_edges, 0);
1428 assert_eq!(m.len, 3);
1429 for &id in &m.mapping[..3] {
1431 assert!(
1432 id == 1 || id == 2 || id == 3,
1433 "match included non-triangle node {id}"
1434 );
1435 }
1436 }
1437 }
1438
1439 #[test]
1440 fn test_subgraph_isomorphism_approximate_allows_missing_edge() {
1441 let quins = vec![
1443 NQuin {
1444 subject: 1,
1445 predicate: 1,
1446 object: 2,
1447 context: 0,
1448 metadata: 0,
1449 parity: 0,
1450 },
1451 NQuin {
1452 subject: 2,
1453 predicate: 1,
1454 object: 3,
1455 context: 0,
1456 metadata: 0,
1457 parity: 0,
1458 },
1459 ];
1460 let triangle = SubgraphPattern::new(3)
1461 .with_edge(0, 1)
1462 .with_edge(1, 2)
1463 .with_edge(2, 0);
1464 let mut matches = [SubgraphMatch {
1465 mapping: [0; MAX_SUBGRAPH_PATTERN_NODES],
1466 len: 0,
1467 missing_edges: 0,
1468 }; 16];
1469
1470 let exact = find_subgraph_isomorphisms_bounded(&quins, &triangle, 0, &mut matches).unwrap();
1472 assert_eq!(exact, 0);
1473
1474 let approx =
1477 find_subgraph_isomorphisms_bounded(&quins, &triangle, 1, &mut matches).unwrap();
1478 assert!(approx >= 1, "approximate match should find the open path");
1479 assert!(matches[..approx].iter().any(|m| {
1480 m.missing_edges == 1 && m.mapping[0] == 1 && m.mapping[1] == 2 && m.mapping[2] == 3
1481 }));
1482 }
1483}