1use crate::types::CsrGraph;
9use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
10use std::collections::HashSet;
11
12#[derive(Debug, Clone)]
18pub struct CycleParticipationResult {
19 pub node_index: usize,
21 pub cycle_count_2hop: u32,
23 pub cycle_count_3hop: u32,
25 pub cycle_count_4hop: u32,
27 pub total_cycle_weight: f64,
29 pub cycle_ratio: f64,
31 pub risk_level: CycleRiskLevel,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum CycleRiskLevel {
38 Baseline,
40 Low,
42 Medium,
44 High,
46 Critical,
48}
49
50#[derive(Debug, Clone)]
60pub struct ShortCycleParticipation {
61 metadata: KernelMetadata,
62}
63
64impl Default for ShortCycleParticipation {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl ShortCycleParticipation {
71 #[must_use]
73 pub fn new() -> Self {
74 Self {
75 metadata: KernelMetadata::batch("graph/cycle-participation", Domain::GraphAnalytics)
76 .with_description("Short cycle (2-4 hop) participation detection")
77 .with_throughput(25_000)
78 .with_latency_us(200.0),
79 }
80 }
81
82 pub fn detect_2_cycles(graph: &CsrGraph) -> Vec<u32> {
84 let n = graph.num_nodes;
85 let mut cycle_counts = vec![0u32; n];
86
87 for u in 0..n {
89 for &v in graph.neighbors(u as u64) {
90 if graph.neighbors(v).contains(&(u as u64)) {
92 cycle_counts[u] += 1;
93 }
94 }
95 }
96
97 cycle_counts
101 }
102
103 pub fn detect_3_cycles(graph: &CsrGraph) -> Vec<u32> {
107 let n = graph.num_nodes;
108 let mut cycle_counts = vec![0u32; n];
109
110 for u in 0..n {
111 let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
112
113 for &v in graph.neighbors(u as u64) {
114 if v as usize <= u {
115 continue; }
117
118 for &w in graph.neighbors(v) {
120 if w as usize <= v as usize {
121 continue;
122 }
123
124 if neighbors_u.contains(&w) {
125 cycle_counts[u] += 1;
127 cycle_counts[v as usize] += 1;
128 cycle_counts[w as usize] += 1;
129 }
130 }
131 }
132 }
133
134 cycle_counts
135 }
136
137 pub fn detect_4_cycles(graph: &CsrGraph) -> Vec<u32> {
141 let n = graph.num_nodes;
142 let mut cycle_counts = vec![0u32; n];
143
144 if n > 1000 {
146 return Self::detect_4_cycles_sampled(graph, 0.1);
148 }
149
150 for u in 0..n {
151 let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
152
153 for &v in graph.neighbors(u as u64) {
155 for &w in graph.neighbors(v) {
156 if w as usize == u {
157 continue; }
159
160 for &x in graph.neighbors(w) {
162 if x as usize != u && x != v && neighbors_u.contains(&x) {
163 cycle_counts[u] += 1;
165 }
166 }
167 }
168 }
169 }
170
171 for count in &mut cycle_counts {
173 *count /= 4;
174 }
175
176 cycle_counts
177 }
178
179 fn detect_4_cycles_sampled(graph: &CsrGraph, sample_rate: f64) -> Vec<u32> {
181 let n = graph.num_nodes;
182 let mut cycle_counts = vec![0u32; n];
183
184 let sample_count = (n as f64 * sample_rate).max(1.0) as usize;
185 let step = (n / sample_count).max(1);
186
187 for u in (0..n).step_by(step) {
188 let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
189
190 for &v in graph.neighbors(u as u64) {
191 for &w in graph.neighbors(v) {
192 if w as usize == u {
193 continue;
194 }
195
196 for &x in graph.neighbors(w) {
197 if x as usize != u && x != v && neighbors_u.contains(&x) {
198 cycle_counts[u] += 1;
199 }
200 }
201 }
202 }
203 }
204
205 let scale = 1.0 / sample_rate;
207 for count in &mut cycle_counts {
208 *count = (*count as f64 * scale) as u32 / 4;
209 }
210
211 cycle_counts
212 }
213
214 pub fn compute_all(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
216 let cycles_2 = Self::detect_2_cycles(graph);
217 let cycles_3 = Self::detect_3_cycles(graph);
218 let cycles_4 = Self::detect_4_cycles(graph);
219
220 let n = graph.num_nodes;
221
222 (0..n)
223 .map(|i| {
224 let c2 = cycles_2[i];
225 let c3 = cycles_3[i];
226 let c4 = cycles_4[i];
227
228 let degree = graph.out_degree(i as u64);
229 let cycle_ratio = if degree > 0 {
230 (c2 + c3 + c4) as f64 / degree as f64
231 } else {
232 0.0
233 };
234
235 let risk_level = Self::calculate_risk_level(c2, c3, c4);
236
237 CycleParticipationResult {
238 node_index: i,
239 cycle_count_2hop: c2,
240 cycle_count_3hop: c3,
241 cycle_count_4hop: c4,
242 total_cycle_weight: 0.0, cycle_ratio,
244 risk_level,
245 }
246 })
247 .collect()
248 }
249
250 fn calculate_risk_level(cycles_2: u32, cycles_3: u32, cycles_4: u32) -> CycleRiskLevel {
252 if cycles_4 > 3 {
253 CycleRiskLevel::Critical
254 } else if cycles_4 > 0 {
255 CycleRiskLevel::High
256 } else if cycles_3 >= 3 {
257 CycleRiskLevel::Medium
258 } else if cycles_3 >= 1 || cycles_2 >= 3 {
259 CycleRiskLevel::Low
260 } else {
261 CycleRiskLevel::Baseline
262 }
263 }
264
265 pub fn find_high_risk_nodes(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
267 Self::compute_all(graph)
268 .into_iter()
269 .filter(|r| {
270 matches!(
271 r.risk_level,
272 CycleRiskLevel::High | CycleRiskLevel::Critical
273 )
274 })
275 .collect()
276 }
277
278 pub fn find_4_cycle_nodes(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
280 Self::compute_all(graph)
281 .into_iter()
282 .filter(|r| r.cycle_count_4hop > 0)
283 .collect()
284 }
285
286 pub fn count_triangles(graph: &CsrGraph) -> u64 {
288 let cycles_3 = Self::detect_3_cycles(graph);
289 cycles_3.iter().map(|&c| c as u64).sum::<u64>() / 3
291 }
292}
293
294impl GpuKernel for ShortCycleParticipation {
295 fn metadata(&self) -> &KernelMetadata {
296 &self.metadata
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn create_triangle_graph() -> CsrGraph {
305 CsrGraph::from_edges(3, &[(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)])
307 }
308
309 fn create_square_graph() -> CsrGraph {
310 CsrGraph::from_edges(4, &[(0, 1), (1, 2), (2, 3), (3, 0)])
312 }
313
314 fn create_reciprocal_graph() -> CsrGraph {
315 CsrGraph::from_edges(3, &[(0, 1), (1, 0), (1, 2), (2, 1)])
317 }
318
319 fn create_complex_graph() -> CsrGraph {
320 CsrGraph::from_edges(
322 5,
323 &[
324 (0, 1),
326 (1, 0),
327 (1, 2),
328 (2, 1),
329 (0, 2),
330 (2, 0),
331 (2, 3),
333 (3, 2),
334 (3, 4),
335 (4, 3),
336 (2, 4),
337 (4, 2),
338 ],
339 )
340 }
341
342 #[test]
343 fn test_cycle_participation_metadata() {
344 let kernel = ShortCycleParticipation::new();
345 assert_eq!(kernel.metadata().id, "graph/cycle-participation");
346 assert_eq!(kernel.metadata().domain, Domain::GraphAnalytics);
347 }
348
349 #[test]
350 fn test_detect_2_cycles() {
351 let graph = create_reciprocal_graph();
352 let counts = ShortCycleParticipation::detect_2_cycles(&graph);
353
354 assert!(counts[1] >= 2);
356 }
357
358 #[test]
359 fn test_detect_3_cycles() {
360 let graph = create_triangle_graph();
361 let counts = ShortCycleParticipation::detect_3_cycles(&graph);
362
363 assert!(
365 counts[0] >= 1,
366 "Node 0 should participate in triangle: got {}",
367 counts[0]
368 );
369 assert!(
370 counts[1] >= 1,
371 "Node 1 should participate in triangle: got {}",
372 counts[1]
373 );
374 assert!(
375 counts[2] >= 1,
376 "Node 2 should participate in triangle: got {}",
377 counts[2]
378 );
379 }
380
381 #[test]
382 fn test_count_triangles() {
383 let graph = create_triangle_graph();
384 let count = ShortCycleParticipation::count_triangles(&graph);
385
386 assert_eq!(
387 count, 1,
388 "Should find 1 triangle in undirected triangle graph"
389 );
390 }
391
392 #[test]
393 fn test_complex_graph_triangles() {
394 let graph = create_complex_graph();
395 let count = ShortCycleParticipation::count_triangles(&graph);
396
397 assert_eq!(count, 2, "Should find 2 triangles"); }
399
400 #[test]
401 fn test_risk_level_baseline() {
402 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 0);
403 assert_eq!(level, CycleRiskLevel::Baseline);
404 }
405
406 #[test]
407 fn test_risk_level_low() {
408 let level = ShortCycleParticipation::calculate_risk_level(0, 1, 0);
409 assert_eq!(level, CycleRiskLevel::Low);
410 }
411
412 #[test]
413 fn test_risk_level_medium() {
414 let level = ShortCycleParticipation::calculate_risk_level(0, 3, 0);
415 assert_eq!(level, CycleRiskLevel::Medium);
416 }
417
418 #[test]
419 fn test_risk_level_high() {
420 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 1);
421 assert_eq!(level, CycleRiskLevel::High);
422 }
423
424 #[test]
425 fn test_risk_level_critical() {
426 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 5);
427 assert_eq!(level, CycleRiskLevel::Critical);
428 }
429
430 #[test]
431 fn test_find_high_risk_nodes() {
432 let graph = create_triangle_graph();
433 let high_risk = ShortCycleParticipation::find_high_risk_nodes(&graph);
434
435 assert!(high_risk.is_empty());
437 }
438
439 #[test]
440 fn test_compute_all() {
441 let graph = create_triangle_graph();
442 let results = ShortCycleParticipation::compute_all(&graph);
443
444 assert_eq!(results.len(), 3);
445 for result in &results {
446 assert!(
447 result.cycle_count_3hop >= 1,
448 "Each node should have at least 1 triangle participation"
449 );
450 assert_eq!(result.cycle_count_4hop, 0);
451 }
452 }
453}