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 #[allow(clippy::needless_range_loop)]
84 pub fn detect_2_cycles(graph: &CsrGraph) -> Vec<u32> {
85 let n = graph.num_nodes;
86 let mut cycle_counts = vec![0u32; n];
87
88 for u in 0..n {
90 for &v in graph.neighbors(u as u64) {
91 if graph.neighbors(v).contains(&(u as u64)) {
93 cycle_counts[u] += 1;
94 }
95 }
96 }
97
98 cycle_counts
102 }
103
104 pub fn detect_3_cycles(graph: &CsrGraph) -> Vec<u32> {
108 let n = graph.num_nodes;
109 let mut cycle_counts = vec![0u32; n];
110
111 for u in 0..n {
112 let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
113
114 for &v in graph.neighbors(u as u64) {
115 if v as usize <= u {
116 continue; }
118
119 for &w in graph.neighbors(v) {
121 if w as usize <= v as usize {
122 continue;
123 }
124
125 if neighbors_u.contains(&w) {
126 cycle_counts[u] += 1;
128 cycle_counts[v as usize] += 1;
129 cycle_counts[w as usize] += 1;
130 }
131 }
132 }
133 }
134
135 cycle_counts
136 }
137
138 #[allow(clippy::needless_range_loop)]
142 pub fn detect_4_cycles(graph: &CsrGraph) -> Vec<u32> {
143 let n = graph.num_nodes;
144 let mut cycle_counts = vec![0u32; n];
145
146 if n > 1000 {
148 return Self::detect_4_cycles_sampled(graph, 0.1);
150 }
151
152 for u in 0..n {
153 let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
154
155 for &v in graph.neighbors(u as u64) {
157 for &w in graph.neighbors(v) {
158 if w as usize == u {
159 continue; }
161
162 for &x in graph.neighbors(w) {
164 if x as usize != u && x != v && neighbors_u.contains(&x) {
165 cycle_counts[u] += 1;
167 }
168 }
169 }
170 }
171 }
172
173 for count in &mut cycle_counts {
175 *count /= 4;
176 }
177
178 cycle_counts
179 }
180
181 fn detect_4_cycles_sampled(graph: &CsrGraph, sample_rate: f64) -> Vec<u32> {
183 let n = graph.num_nodes;
184 let mut cycle_counts = vec![0u32; n];
185
186 let sample_count = (n as f64 * sample_rate).max(1.0) as usize;
187 let step = (n / sample_count).max(1);
188
189 for u in (0..n).step_by(step) {
190 let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
191
192 for &v in graph.neighbors(u as u64) {
193 for &w in graph.neighbors(v) {
194 if w as usize == u {
195 continue;
196 }
197
198 for &x in graph.neighbors(w) {
199 if x as usize != u && x != v && neighbors_u.contains(&x) {
200 cycle_counts[u] += 1;
201 }
202 }
203 }
204 }
205 }
206
207 let scale = 1.0 / sample_rate;
209 for count in &mut cycle_counts {
210 *count = (*count as f64 * scale) as u32 / 4;
211 }
212
213 cycle_counts
214 }
215
216 pub fn compute_all(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
218 let cycles_2 = Self::detect_2_cycles(graph);
219 let cycles_3 = Self::detect_3_cycles(graph);
220 let cycles_4 = Self::detect_4_cycles(graph);
221
222 let n = graph.num_nodes;
223
224 (0..n)
225 .map(|i| {
226 let c2 = cycles_2[i];
227 let c3 = cycles_3[i];
228 let c4 = cycles_4[i];
229
230 let degree = graph.out_degree(i as u64);
231 let cycle_ratio = if degree > 0 {
232 (c2 + c3 + c4) as f64 / degree as f64
233 } else {
234 0.0
235 };
236
237 let risk_level = Self::calculate_risk_level(c2, c3, c4);
238
239 CycleParticipationResult {
240 node_index: i,
241 cycle_count_2hop: c2,
242 cycle_count_3hop: c3,
243 cycle_count_4hop: c4,
244 total_cycle_weight: 0.0, cycle_ratio,
246 risk_level,
247 }
248 })
249 .collect()
250 }
251
252 fn calculate_risk_level(cycles_2: u32, cycles_3: u32, cycles_4: u32) -> CycleRiskLevel {
254 if cycles_4 > 3 {
255 CycleRiskLevel::Critical
256 } else if cycles_4 > 0 {
257 CycleRiskLevel::High
258 } else if cycles_3 >= 3 {
259 CycleRiskLevel::Medium
260 } else if cycles_3 >= 1 || cycles_2 >= 3 {
261 CycleRiskLevel::Low
262 } else {
263 CycleRiskLevel::Baseline
264 }
265 }
266
267 pub fn find_high_risk_nodes(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
269 Self::compute_all(graph)
270 .into_iter()
271 .filter(|r| {
272 matches!(
273 r.risk_level,
274 CycleRiskLevel::High | CycleRiskLevel::Critical
275 )
276 })
277 .collect()
278 }
279
280 pub fn find_4_cycle_nodes(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
282 Self::compute_all(graph)
283 .into_iter()
284 .filter(|r| r.cycle_count_4hop > 0)
285 .collect()
286 }
287
288 pub fn count_triangles(graph: &CsrGraph) -> u64 {
290 let cycles_3 = Self::detect_3_cycles(graph);
291 cycles_3.iter().map(|&c| c as u64).sum::<u64>() / 3
293 }
294}
295
296impl GpuKernel for ShortCycleParticipation {
297 fn metadata(&self) -> &KernelMetadata {
298 &self.metadata
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 fn create_triangle_graph() -> CsrGraph {
307 CsrGraph::from_edges(3, &[(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)])
309 }
310
311 #[allow(dead_code)]
312 fn create_square_graph() -> CsrGraph {
313 CsrGraph::from_edges(4, &[(0, 1), (1, 2), (2, 3), (3, 0)])
315 }
316
317 fn create_reciprocal_graph() -> CsrGraph {
318 CsrGraph::from_edges(3, &[(0, 1), (1, 0), (1, 2), (2, 1)])
320 }
321
322 fn create_complex_graph() -> CsrGraph {
323 CsrGraph::from_edges(
325 5,
326 &[
327 (0, 1),
329 (1, 0),
330 (1, 2),
331 (2, 1),
332 (0, 2),
333 (2, 0),
334 (2, 3),
336 (3, 2),
337 (3, 4),
338 (4, 3),
339 (2, 4),
340 (4, 2),
341 ],
342 )
343 }
344
345 #[test]
346 fn test_cycle_participation_metadata() {
347 let kernel = ShortCycleParticipation::new();
348 assert_eq!(kernel.metadata().id, "graph/cycle-participation");
349 assert_eq!(kernel.metadata().domain, Domain::GraphAnalytics);
350 }
351
352 #[test]
353 fn test_detect_2_cycles() {
354 let graph = create_reciprocal_graph();
355 let counts = ShortCycleParticipation::detect_2_cycles(&graph);
356
357 assert!(counts[1] >= 2);
359 }
360
361 #[test]
362 fn test_detect_3_cycles() {
363 let graph = create_triangle_graph();
364 let counts = ShortCycleParticipation::detect_3_cycles(&graph);
365
366 assert!(
368 counts[0] >= 1,
369 "Node 0 should participate in triangle: got {}",
370 counts[0]
371 );
372 assert!(
373 counts[1] >= 1,
374 "Node 1 should participate in triangle: got {}",
375 counts[1]
376 );
377 assert!(
378 counts[2] >= 1,
379 "Node 2 should participate in triangle: got {}",
380 counts[2]
381 );
382 }
383
384 #[test]
385 fn test_count_triangles() {
386 let graph = create_triangle_graph();
387 let count = ShortCycleParticipation::count_triangles(&graph);
388
389 assert_eq!(
390 count, 1,
391 "Should find 1 triangle in undirected triangle graph"
392 );
393 }
394
395 #[test]
396 fn test_complex_graph_triangles() {
397 let graph = create_complex_graph();
398 let count = ShortCycleParticipation::count_triangles(&graph);
399
400 assert_eq!(count, 2, "Should find 2 triangles"); }
402
403 #[test]
404 fn test_risk_level_baseline() {
405 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 0);
406 assert_eq!(level, CycleRiskLevel::Baseline);
407 }
408
409 #[test]
410 fn test_risk_level_low() {
411 let level = ShortCycleParticipation::calculate_risk_level(0, 1, 0);
412 assert_eq!(level, CycleRiskLevel::Low);
413 }
414
415 #[test]
416 fn test_risk_level_medium() {
417 let level = ShortCycleParticipation::calculate_risk_level(0, 3, 0);
418 assert_eq!(level, CycleRiskLevel::Medium);
419 }
420
421 #[test]
422 fn test_risk_level_high() {
423 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 1);
424 assert_eq!(level, CycleRiskLevel::High);
425 }
426
427 #[test]
428 fn test_risk_level_critical() {
429 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 5);
430 assert_eq!(level, CycleRiskLevel::Critical);
431 }
432
433 #[test]
434 fn test_find_high_risk_nodes() {
435 let graph = create_triangle_graph();
436 let high_risk = ShortCycleParticipation::find_high_risk_nodes(&graph);
437
438 assert!(high_risk.is_empty());
440 }
441
442 #[test]
443 fn test_compute_all() {
444 let graph = create_triangle_graph();
445 let results = ShortCycleParticipation::compute_all(&graph);
446
447 assert_eq!(results.len(), 3);
448 for result in &results {
449 assert!(
450 result.cycle_count_3hop >= 1,
451 "Each node should have at least 1 triangle participation"
452 );
453 assert_eq!(result.cycle_count_4hop, 0);
454 }
455 }
456}