use crate::types::CsrGraph;
use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct CycleParticipationResult {
pub node_index: usize,
pub cycle_count_2hop: u32,
pub cycle_count_3hop: u32,
pub cycle_count_4hop: u32,
pub total_cycle_weight: f64,
pub cycle_ratio: f64,
pub risk_level: CycleRiskLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CycleRiskLevel {
Baseline,
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone)]
pub struct ShortCycleParticipation {
metadata: KernelMetadata,
}
impl Default for ShortCycleParticipation {
fn default() -> Self {
Self::new()
}
}
impl ShortCycleParticipation {
#[must_use]
pub fn new() -> Self {
Self {
metadata: KernelMetadata::batch("graph/cycle-participation", Domain::GraphAnalytics)
.with_description("Short cycle (2-4 hop) participation detection")
.with_throughput(25_000)
.with_latency_us(200.0),
}
}
#[allow(clippy::needless_range_loop)]
pub fn detect_2_cycles(graph: &CsrGraph) -> Vec<u32> {
let n = graph.num_nodes;
let mut cycle_counts = vec![0u32; n];
for u in 0..n {
for &v in graph.neighbors(u as u64) {
if graph.neighbors(v).contains(&(u as u64)) {
cycle_counts[u] += 1;
}
}
}
cycle_counts
}
pub fn detect_3_cycles(graph: &CsrGraph) -> Vec<u32> {
let n = graph.num_nodes;
let mut cycle_counts = vec![0u32; n];
for u in 0..n {
let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
for &v in graph.neighbors(u as u64) {
if v as usize <= u {
continue; }
for &w in graph.neighbors(v) {
if w as usize <= v as usize {
continue;
}
if neighbors_u.contains(&w) {
cycle_counts[u] += 1;
cycle_counts[v as usize] += 1;
cycle_counts[w as usize] += 1;
}
}
}
}
cycle_counts
}
#[allow(clippy::needless_range_loop)]
pub fn detect_4_cycles(graph: &CsrGraph) -> Vec<u32> {
let n = graph.num_nodes;
let mut cycle_counts = vec![0u32; n];
if n > 1000 {
return Self::detect_4_cycles_sampled(graph, 0.1);
}
for u in 0..n {
let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
for &v in graph.neighbors(u as u64) {
for &w in graph.neighbors(v) {
if w as usize == u {
continue; }
for &x in graph.neighbors(w) {
if x as usize != u && x != v && neighbors_u.contains(&x) {
cycle_counts[u] += 1;
}
}
}
}
}
for count in &mut cycle_counts {
*count /= 4;
}
cycle_counts
}
fn detect_4_cycles_sampled(graph: &CsrGraph, sample_rate: f64) -> Vec<u32> {
let n = graph.num_nodes;
let mut cycle_counts = vec![0u32; n];
let sample_count = (n as f64 * sample_rate).max(1.0) as usize;
let step = (n / sample_count).max(1);
for u in (0..n).step_by(step) {
let neighbors_u: HashSet<u64> = graph.neighbors(u as u64).iter().copied().collect();
for &v in graph.neighbors(u as u64) {
for &w in graph.neighbors(v) {
if w as usize == u {
continue;
}
for &x in graph.neighbors(w) {
if x as usize != u && x != v && neighbors_u.contains(&x) {
cycle_counts[u] += 1;
}
}
}
}
}
let scale = 1.0 / sample_rate;
for count in &mut cycle_counts {
*count = (*count as f64 * scale) as u32 / 4;
}
cycle_counts
}
pub fn compute_all(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
let cycles_2 = Self::detect_2_cycles(graph);
let cycles_3 = Self::detect_3_cycles(graph);
let cycles_4 = Self::detect_4_cycles(graph);
let n = graph.num_nodes;
(0..n)
.map(|i| {
let c2 = cycles_2[i];
let c3 = cycles_3[i];
let c4 = cycles_4[i];
let degree = graph.out_degree(i as u64);
let cycle_ratio = if degree > 0 {
(c2 + c3 + c4) as f64 / degree as f64
} else {
0.0
};
let risk_level = Self::calculate_risk_level(c2, c3, c4);
CycleParticipationResult {
node_index: i,
cycle_count_2hop: c2,
cycle_count_3hop: c3,
cycle_count_4hop: c4,
total_cycle_weight: 0.0, cycle_ratio,
risk_level,
}
})
.collect()
}
fn calculate_risk_level(cycles_2: u32, cycles_3: u32, cycles_4: u32) -> CycleRiskLevel {
if cycles_4 > 3 {
CycleRiskLevel::Critical
} else if cycles_4 > 0 {
CycleRiskLevel::High
} else if cycles_3 >= 3 {
CycleRiskLevel::Medium
} else if cycles_3 >= 1 || cycles_2 >= 3 {
CycleRiskLevel::Low
} else {
CycleRiskLevel::Baseline
}
}
pub fn find_high_risk_nodes(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
Self::compute_all(graph)
.into_iter()
.filter(|r| {
matches!(
r.risk_level,
CycleRiskLevel::High | CycleRiskLevel::Critical
)
})
.collect()
}
pub fn find_4_cycle_nodes(graph: &CsrGraph) -> Vec<CycleParticipationResult> {
Self::compute_all(graph)
.into_iter()
.filter(|r| r.cycle_count_4hop > 0)
.collect()
}
pub fn count_triangles(graph: &CsrGraph) -> u64 {
let cycles_3 = Self::detect_3_cycles(graph);
cycles_3.iter().map(|&c| c as u64).sum::<u64>() / 3
}
}
impl GpuKernel for ShortCycleParticipation {
fn metadata(&self) -> &KernelMetadata {
&self.metadata
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_triangle_graph() -> CsrGraph {
CsrGraph::from_edges(3, &[(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)])
}
#[allow(dead_code)]
fn create_square_graph() -> CsrGraph {
CsrGraph::from_edges(4, &[(0, 1), (1, 2), (2, 3), (3, 0)])
}
fn create_reciprocal_graph() -> CsrGraph {
CsrGraph::from_edges(3, &[(0, 1), (1, 0), (1, 2), (2, 1)])
}
fn create_complex_graph() -> CsrGraph {
CsrGraph::from_edges(
5,
&[
(0, 1),
(1, 0),
(1, 2),
(2, 1),
(0, 2),
(2, 0),
(2, 3),
(3, 2),
(3, 4),
(4, 3),
(2, 4),
(4, 2),
],
)
}
#[test]
fn test_cycle_participation_metadata() {
let kernel = ShortCycleParticipation::new();
assert_eq!(kernel.metadata().id, "graph/cycle-participation");
assert_eq!(kernel.metadata().domain, Domain::GraphAnalytics);
}
#[test]
fn test_detect_2_cycles() {
let graph = create_reciprocal_graph();
let counts = ShortCycleParticipation::detect_2_cycles(&graph);
assert!(counts[1] >= 2);
}
#[test]
fn test_detect_3_cycles() {
let graph = create_triangle_graph();
let counts = ShortCycleParticipation::detect_3_cycles(&graph);
assert!(
counts[0] >= 1,
"Node 0 should participate in triangle: got {}",
counts[0]
);
assert!(
counts[1] >= 1,
"Node 1 should participate in triangle: got {}",
counts[1]
);
assert!(
counts[2] >= 1,
"Node 2 should participate in triangle: got {}",
counts[2]
);
}
#[test]
fn test_count_triangles() {
let graph = create_triangle_graph();
let count = ShortCycleParticipation::count_triangles(&graph);
assert_eq!(
count, 1,
"Should find 1 triangle in undirected triangle graph"
);
}
#[test]
fn test_complex_graph_triangles() {
let graph = create_complex_graph();
let count = ShortCycleParticipation::count_triangles(&graph);
assert_eq!(count, 2, "Should find 2 triangles"); }
#[test]
fn test_risk_level_baseline() {
let level = ShortCycleParticipation::calculate_risk_level(0, 0, 0);
assert_eq!(level, CycleRiskLevel::Baseline);
}
#[test]
fn test_risk_level_low() {
let level = ShortCycleParticipation::calculate_risk_level(0, 1, 0);
assert_eq!(level, CycleRiskLevel::Low);
}
#[test]
fn test_risk_level_medium() {
let level = ShortCycleParticipation::calculate_risk_level(0, 3, 0);
assert_eq!(level, CycleRiskLevel::Medium);
}
#[test]
fn test_risk_level_high() {
let level = ShortCycleParticipation::calculate_risk_level(0, 0, 1);
assert_eq!(level, CycleRiskLevel::High);
}
#[test]
fn test_risk_level_critical() {
let level = ShortCycleParticipation::calculate_risk_level(0, 0, 5);
assert_eq!(level, CycleRiskLevel::Critical);
}
#[test]
fn test_find_high_risk_nodes() {
let graph = create_triangle_graph();
let high_risk = ShortCycleParticipation::find_high_risk_nodes(&graph);
assert!(high_risk.is_empty());
}
#[test]
fn test_compute_all() {
let graph = create_triangle_graph();
let results = ShortCycleParticipation::compute_all(&graph);
assert_eq!(results.len(), 3);
for result in &results {
assert!(
result.cycle_count_3hop >= 1,
"Each node should have at least 1 triangle participation"
);
assert_eq!(result.cycle_count_4hop, 0);
}
}
}