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 let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
90
91 for &v in graph.neighbors(u as u64) {
92 if graph.neighbors(v).contains(&(u as u64)) {
94 cycle_counts[u] += 1;
95 }
96 }
97 }
98
99 cycle_counts
103 }
104
105 pub fn detect_3_cycles(graph: &CsrGraph) -> Vec<u32> {
109 let n = graph.num_nodes;
110 let mut cycle_counts = vec![0u32; n];
111
112 for u in 0..n {
113 let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
114
115 for &v in graph.neighbors(u as u64) {
116 if v as usize <= u {
117 continue; }
119
120 for &w in graph.neighbors(v) {
122 if w as usize <= v as usize {
123 continue;
124 }
125
126 if neighbors_u.contains(&w) {
127 cycle_counts[u] += 1;
129 cycle_counts[v as usize] += 1;
130 cycle_counts[w as usize] += 1;
131 }
132 }
133 }
134 }
135
136 cycle_counts
137 }
138
139 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 fn create_square_graph() -> CsrGraph {
312 CsrGraph::from_edges(4, &[(0, 1), (1, 2), (2, 3), (3, 0)])
314 }
315
316 fn create_reciprocal_graph() -> CsrGraph {
317 CsrGraph::from_edges(3, &[(0, 1), (1, 0), (1, 2), (2, 1)])
319 }
320
321 fn create_complex_graph() -> CsrGraph {
322 CsrGraph::from_edges(
324 5,
325 &[
326 (0, 1),
328 (1, 0),
329 (1, 2),
330 (2, 1),
331 (0, 2),
332 (2, 0),
333 (2, 3),
335 (3, 2),
336 (3, 4),
337 (4, 3),
338 (2, 4),
339 (4, 2),
340 ],
341 )
342 }
343
344 #[test]
345 fn test_cycle_participation_metadata() {
346 let kernel = ShortCycleParticipation::new();
347 assert_eq!(kernel.metadata().id, "graph/cycle-participation");
348 assert_eq!(kernel.metadata().domain, Domain::GraphAnalytics);
349 }
350
351 #[test]
352 fn test_detect_2_cycles() {
353 let graph = create_reciprocal_graph();
354 let counts = ShortCycleParticipation::detect_2_cycles(&graph);
355
356 assert!(counts[1] >= 2);
358 }
359
360 #[test]
361 fn test_detect_3_cycles() {
362 let graph = create_triangle_graph();
363 let counts = ShortCycleParticipation::detect_3_cycles(&graph);
364
365 assert!(
367 counts[0] >= 1,
368 "Node 0 should participate in triangle: got {}",
369 counts[0]
370 );
371 assert!(
372 counts[1] >= 1,
373 "Node 1 should participate in triangle: got {}",
374 counts[1]
375 );
376 assert!(
377 counts[2] >= 1,
378 "Node 2 should participate in triangle: got {}",
379 counts[2]
380 );
381 }
382
383 #[test]
384 fn test_count_triangles() {
385 let graph = create_triangle_graph();
386 let count = ShortCycleParticipation::count_triangles(&graph);
387
388 assert_eq!(
389 count, 1,
390 "Should find 1 triangle in undirected triangle graph"
391 );
392 }
393
394 #[test]
395 fn test_complex_graph_triangles() {
396 let graph = create_complex_graph();
397 let count = ShortCycleParticipation::count_triangles(&graph);
398
399 assert_eq!(count, 2, "Should find 2 triangles"); }
401
402 #[test]
403 fn test_risk_level_baseline() {
404 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 0);
405 assert_eq!(level, CycleRiskLevel::Baseline);
406 }
407
408 #[test]
409 fn test_risk_level_low() {
410 let level = ShortCycleParticipation::calculate_risk_level(0, 1, 0);
411 assert_eq!(level, CycleRiskLevel::Low);
412 }
413
414 #[test]
415 fn test_risk_level_medium() {
416 let level = ShortCycleParticipation::calculate_risk_level(0, 3, 0);
417 assert_eq!(level, CycleRiskLevel::Medium);
418 }
419
420 #[test]
421 fn test_risk_level_high() {
422 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 1);
423 assert_eq!(level, CycleRiskLevel::High);
424 }
425
426 #[test]
427 fn test_risk_level_critical() {
428 let level = ShortCycleParticipation::calculate_risk_level(0, 0, 5);
429 assert_eq!(level, CycleRiskLevel::Critical);
430 }
431
432 #[test]
433 fn test_find_high_risk_nodes() {
434 let graph = create_triangle_graph();
435 let high_risk = ShortCycleParticipation::find_high_risk_nodes(&graph);
436
437 assert!(high_risk.is_empty());
439 }
440
441 #[test]
442 fn test_compute_all() {
443 let graph = create_triangle_graph();
444 let results = ShortCycleParticipation::compute_all(&graph);
445
446 assert_eq!(results.len(), 3);
447 for result in &results {
448 assert!(
449 result.cycle_count_3hop >= 1,
450 "Each node should have at least 1 triangle participation"
451 );
452 assert_eq!(result.cycle_count_4hop, 0);
453 }
454 }
455}