use crate::compressed::CsrGraph;
use crate::error::{GraphError, Result};
use scirs2_core::random::prelude::*;
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StreamEdge {
pub src: usize,
pub dst: usize,
pub weight: f64,
pub timestamp: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamOp {
Insert,
Delete,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StreamEvent {
pub edge: StreamEdge,
pub op: StreamOp,
}
#[derive(Debug)]
pub struct StreamingGraph {
adjacency: HashMap<usize, HashMap<usize, f64>>,
num_edges: usize,
events_processed: u64,
directed: bool,
max_node_id: usize,
}
impl StreamingGraph {
pub fn new(directed: bool) -> Self {
Self {
adjacency: HashMap::new(),
num_edges: 0,
events_processed: 0,
directed,
max_node_id: 0,
}
}
pub fn process_event(&mut self, event: &StreamEvent) {
self.events_processed += 1;
match event.op {
StreamOp::Insert => self.insert_edge(event.edge.src, event.edge.dst, event.edge.weight),
StreamOp::Delete => self.delete_edge(event.edge.src, event.edge.dst),
}
}
pub fn insert_edge(&mut self, src: usize, dst: usize, weight: f64) {
self.max_node_id = self.max_node_id.max(src).max(dst);
self.adjacency.entry(src).or_default().insert(dst, weight);
if !self.directed {
self.adjacency.entry(dst).or_default().insert(src, weight);
}
self.num_edges += 1;
}
pub fn delete_edge(&mut self, src: usize, dst: usize) {
if let Some(neighbors) = self.adjacency.get_mut(&src) {
if neighbors.remove(&dst).is_some() {
self.num_edges = self.num_edges.saturating_sub(1);
}
}
if !self.directed {
if let Some(neighbors) = self.adjacency.get_mut(&dst) {
neighbors.remove(&src);
}
}
}
pub fn has_edge(&self, src: usize, dst: usize) -> bool {
self.adjacency
.get(&src)
.is_some_and(|neighbors| neighbors.contains_key(&dst))
}
pub fn degree(&self, node: usize) -> usize {
self.adjacency
.get(&node)
.map_or(0, |neighbors| neighbors.len())
}
pub fn num_nodes(&self) -> usize {
self.adjacency.len()
}
pub fn num_edges(&self) -> usize {
self.num_edges
}
pub fn events_processed(&self) -> u64 {
self.events_processed
}
pub fn neighbors(&self, node: usize) -> Vec<(usize, f64)> {
self.adjacency
.get(&node)
.map_or_else(Vec::new, |neighbors| {
neighbors.iter().map(|(&n, &w)| (n, w)).collect()
})
}
pub fn degree_distribution(&self) -> DegreeDistribution {
let mut dist = HashMap::new();
let mut max_degree = 0;
let mut total_degree = 0;
for neighbors in self.adjacency.values() {
let deg = neighbors.len();
*dist.entry(deg).or_insert(0usize) += 1;
max_degree = max_degree.max(deg);
total_degree += deg;
}
let n = self.adjacency.len();
let avg_degree = if n > 0 {
total_degree as f64 / n as f64
} else {
0.0
};
DegreeDistribution {
histogram: dist,
max_degree,
avg_degree,
num_nodes: n,
}
}
pub fn to_csr(&self) -> Result<CsrGraph> {
let num_nodes = if self.adjacency.is_empty() {
0
} else {
self.max_node_id + 1
};
let mut edges = Vec::with_capacity(self.num_edges);
for (&src, neighbors) in &self.adjacency {
for (&dst, &weight) in neighbors {
if self.directed || src <= dst {
edges.push((src, dst, weight));
}
}
}
CsrGraph::from_edges(num_nodes, edges, self.directed)
}
}
#[derive(Debug, Clone)]
pub struct DegreeDistribution {
pub histogram: HashMap<usize, usize>,
pub max_degree: usize,
pub avg_degree: f64,
pub num_nodes: usize,
}
#[derive(Debug)]
pub struct DoulionTriangleCounter {
sample_prob: f64,
sampled_adj: HashMap<usize, HashSet<usize>>,
sampled_triangles: usize,
edges_processed: u64,
edges_sampled: u64,
rng: StdRng,
}
impl DoulionTriangleCounter {
pub fn new(sample_prob: f64, seed: u64) -> Result<Self> {
if !(0.0..=1.0).contains(&sample_prob) {
return Err(GraphError::InvalidGraph(
"sample_prob must be in [0, 1]".to_string(),
));
}
Ok(Self {
sample_prob,
sampled_adj: HashMap::new(),
sampled_triangles: 0,
edges_processed: 0,
edges_sampled: 0,
rng: StdRng::seed_from_u64(seed),
})
}
pub fn process_edge(&mut self, src: usize, dst: usize) {
self.edges_processed += 1;
if self.rng.random::<f64>() >= self.sample_prob {
return;
}
self.edges_sampled += 1;
let neighbors_src: HashSet<usize> = self.sampled_adj.get(&src).cloned().unwrap_or_default();
let neighbors_dst: HashSet<usize> = self.sampled_adj.get(&dst).cloned().unwrap_or_default();
let common = neighbors_src.intersection(&neighbors_dst).count();
self.sampled_triangles += common;
self.sampled_adj.entry(src).or_default().insert(dst);
self.sampled_adj.entry(dst).or_default().insert(src);
}
pub fn estimated_triangles(&self) -> f64 {
if self.sample_prob <= 0.0 {
return 0.0;
}
let p3 = self.sample_prob * self.sample_prob * self.sample_prob;
self.sampled_triangles as f64 / p3
}
pub fn sampled_triangles(&self) -> usize {
self.sampled_triangles
}
pub fn stats(&self) -> TriangleCounterStats {
TriangleCounterStats {
edges_processed: self.edges_processed,
edges_sampled: self.edges_sampled,
sampled_triangles: self.sampled_triangles,
estimated_triangles: self.estimated_triangles(),
sample_prob: self.sample_prob,
memory_nodes: self.sampled_adj.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct TriangleCounterStats {
pub edges_processed: u64,
pub edges_sampled: u64,
pub sampled_triangles: usize,
pub estimated_triangles: f64,
pub sample_prob: f64,
pub memory_nodes: usize,
}
#[derive(Debug)]
pub struct MascotTriangleCounter {
max_edges: usize,
edges: Vec<(usize, usize)>,
adj: HashMap<usize, HashSet<usize>>,
triangle_estimate: f64,
edges_processed: u64,
rng: StdRng,
}
impl MascotTriangleCounter {
pub fn new(max_edges: usize, seed: u64) -> Self {
Self {
max_edges,
edges: Vec::with_capacity(max_edges),
adj: HashMap::new(),
triangle_estimate: 0.0,
edges_processed: 0,
rng: StdRng::seed_from_u64(seed),
}
}
pub fn process_edge(&mut self, src: usize, dst: usize) {
self.edges_processed += 1;
let neighbors_src: Vec<usize> = self
.adj
.get(&src)
.map_or_else(Vec::new, |s| s.iter().copied().collect());
let neighbors_dst: HashSet<usize> = self.adj.get(&dst).cloned().unwrap_or_default();
let common_count = neighbors_src
.iter()
.filter(|w| neighbors_dst.contains(w))
.count();
let t = self.edges_processed;
let m = self.max_edges;
if t <= m as u64 {
self.triangle_estimate += common_count as f64;
} else {
let p = m as f64 / t as f64;
if p > 0.0 {
self.triangle_estimate += common_count as f64 / (p * p);
}
}
if self.edges.len() < self.max_edges {
self.edges.push((src, dst));
self.adj.entry(src).or_default().insert(dst);
self.adj.entry(dst).or_default().insert(src);
} else {
let j = self.rng.random_range(0..self.edges_processed as usize);
if j < self.max_edges {
let (old_src, old_dst) = self.edges[j];
if let Some(set) = self.adj.get_mut(&old_src) {
set.remove(&old_dst);
}
if let Some(set) = self.adj.get_mut(&old_dst) {
set.remove(&old_src);
}
self.edges[j] = (src, dst);
self.adj.entry(src).or_default().insert(dst);
self.adj.entry(dst).or_default().insert(src);
}
}
}
pub fn estimated_triangles(&self) -> f64 {
self.triangle_estimate
}
pub fn stats(&self) -> TriangleCounterStats {
TriangleCounterStats {
edges_processed: self.edges_processed,
edges_sampled: self.edges.len() as u64,
sampled_triangles: 0, estimated_triangles: self.triangle_estimate,
sample_prob: if self.edges_processed > 0 {
self.edges.len() as f64 / self.edges_processed as f64
} else {
1.0
},
memory_nodes: self.adj.len(),
}
}
}
#[derive(Debug)]
pub struct SlidingWindowGraph {
window_size: usize,
edge_queue: VecDeque<(usize, usize, f64)>,
adjacency: HashMap<usize, HashMap<usize, f64>>,
edge_counts: HashMap<(usize, usize), usize>,
directed: bool,
events_processed: u64,
}
impl SlidingWindowGraph {
pub fn new(window_size: usize, directed: bool) -> Self {
Self {
window_size,
edge_queue: VecDeque::with_capacity(window_size),
adjacency: HashMap::new(),
edge_counts: HashMap::new(),
directed,
events_processed: 0,
}
}
pub fn process_edge(&mut self, src: usize, dst: usize, weight: f64) {
self.events_processed += 1;
if self.edge_queue.len() >= self.window_size {
if let Some((old_src, old_dst, _old_weight)) = self.edge_queue.pop_front() {
self.remove_edge_internal(old_src, old_dst);
}
}
self.edge_queue.push_back((src, dst, weight));
self.add_edge_internal(src, dst, weight);
}
fn add_edge_internal(&mut self, src: usize, dst: usize, weight: f64) {
self.adjacency.entry(src).or_default().insert(dst, weight);
*self.edge_counts.entry((src, dst)).or_insert(0) += 1;
if !self.directed {
self.adjacency.entry(dst).or_default().insert(src, weight);
*self.edge_counts.entry((dst, src)).or_insert(0) += 1;
}
}
fn remove_edge_internal(&mut self, src: usize, dst: usize) {
let key = (src, dst);
if let Some(count) = self.edge_counts.get_mut(&key) {
*count = count.saturating_sub(1);
if *count == 0 {
self.edge_counts.remove(&key);
if let Some(neighbors) = self.adjacency.get_mut(&src) {
neighbors.remove(&dst);
if neighbors.is_empty() {
self.adjacency.remove(&src);
}
}
}
}
if !self.directed {
let rev_key = (dst, src);
if let Some(count) = self.edge_counts.get_mut(&rev_key) {
*count = count.saturating_sub(1);
if *count == 0 {
self.edge_counts.remove(&rev_key);
if let Some(neighbors) = self.adjacency.get_mut(&dst) {
neighbors.remove(&src);
if neighbors.is_empty() {
self.adjacency.remove(&dst);
}
}
}
}
}
}
pub fn num_edges(&self) -> usize {
self.edge_queue.len()
}
pub fn num_nodes(&self) -> usize {
self.adjacency.len()
}
pub fn window_size(&self) -> usize {
self.window_size
}
pub fn neighbors(&self, node: usize) -> Vec<(usize, f64)> {
self.adjacency
.get(&node)
.map_or_else(Vec::new, |neighbors| {
neighbors.iter().map(|(&n, &w)| (n, w)).collect()
})
}
pub fn degree(&self, node: usize) -> usize {
self.adjacency
.get(&node)
.map_or(0, |neighbors| neighbors.len())
}
pub fn has_edge(&self, src: usize, dst: usize) -> bool {
self.adjacency
.get(&src)
.is_some_and(|n| n.contains_key(&dst))
}
pub fn events_processed(&self) -> u64 {
self.events_processed
}
pub fn to_csr(&self) -> Result<CsrGraph> {
let max_node = self
.adjacency
.keys()
.chain(self.adjacency.values().flat_map(|n| n.keys()))
.copied()
.max()
.map_or(0, |m| m + 1);
let mut edges = Vec::new();
for (&src, neighbors) in &self.adjacency {
for (&dst, &weight) in neighbors {
if self.directed || src <= dst {
edges.push((src, dst, weight));
}
}
}
CsrGraph::from_edges(max_node, edges, self.directed)
}
}
#[derive(Debug, Clone)]
pub struct MemoryBoundedConfig {
pub max_memory_bytes: usize,
pub eviction_strategy: EvictionStrategy,
pub track_degrees: bool,
pub count_triangles: bool,
pub triangle_sample_prob: f64,
}
impl Default for MemoryBoundedConfig {
fn default() -> Self {
Self {
max_memory_bytes: 100 * 1024 * 1024, eviction_strategy: EvictionStrategy::LeastRecentEdge,
track_degrees: true,
count_triangles: false,
triangle_sample_prob: 0.1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvictionStrategy {
LeastRecentEdge,
LowestDegreeNode,
RandomEdge,
}
#[derive(Debug)]
pub struct MemoryBoundedProcessor {
config: MemoryBoundedConfig,
graph: StreamingGraph,
insertion_order: VecDeque<(usize, usize)>,
estimated_memory: usize,
edges_evicted: u64,
rng: StdRng,
}
impl MemoryBoundedProcessor {
pub fn new(config: MemoryBoundedConfig) -> Self {
Self {
graph: StreamingGraph::new(false),
insertion_order: VecDeque::new(),
estimated_memory: 0,
edges_evicted: 0,
rng: StdRng::seed_from_u64(42),
config,
}
}
pub fn process_edge(&mut self, src: usize, dst: usize, weight: f64) {
let edge_memory_estimate = 80;
while self.estimated_memory + edge_memory_estimate > self.config.max_memory_bytes
&& !self.insertion_order.is_empty()
{
self.evict_one();
}
self.graph.insert_edge(src, dst, weight);
self.insertion_order.push_back((src, dst));
self.estimated_memory += edge_memory_estimate;
}
fn evict_one(&mut self) {
match self.config.eviction_strategy {
EvictionStrategy::LeastRecentEdge => {
if let Some((src, dst)) = self.insertion_order.pop_front() {
self.graph.delete_edge(src, dst);
self.estimated_memory = self.estimated_memory.saturating_sub(80);
self.edges_evicted += 1;
}
}
EvictionStrategy::LowestDegreeNode => {
if let Some((&node, _)) = self
.graph
.adjacency
.iter()
.min_by_key(|(_, neighbors)| neighbors.len())
{
let neighbors: Vec<usize> = self
.graph
.adjacency
.get(&node)
.map_or_else(Vec::new, |n| n.keys().copied().collect());
for neighbor in neighbors {
self.graph.delete_edge(node, neighbor);
self.estimated_memory = self.estimated_memory.saturating_sub(80);
self.edges_evicted += 1;
}
}
}
EvictionStrategy::RandomEdge => {
if !self.insertion_order.is_empty() {
let idx = self.rng.random_range(0..self.insertion_order.len());
if let Some((src, dst)) = self.insertion_order.remove(idx) {
self.graph.delete_edge(src, dst);
self.estimated_memory = self.estimated_memory.saturating_sub(80);
self.edges_evicted += 1;
}
}
}
}
}
pub fn graph(&self) -> &StreamingGraph {
&self.graph
}
pub fn edges_evicted(&self) -> u64 {
self.edges_evicted
}
pub fn estimated_memory(&self) -> usize {
self.estimated_memory
}
pub fn degree_distribution(&self) -> DegreeDistribution {
self.graph.degree_distribution()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_streaming_graph_insert() {
let mut g = StreamingGraph::new(false);
g.insert_edge(0, 1, 1.0);
g.insert_edge(1, 2, 2.0);
assert_eq!(g.num_edges(), 2);
assert_eq!(g.num_nodes(), 3);
assert!(g.has_edge(0, 1));
assert!(g.has_edge(1, 0)); assert!(g.has_edge(1, 2));
}
#[test]
fn test_streaming_graph_delete() {
let mut g = StreamingGraph::new(false);
g.insert_edge(0, 1, 1.0);
g.insert_edge(1, 2, 2.0);
g.delete_edge(0, 1);
assert_eq!(g.num_edges(), 1);
assert!(!g.has_edge(0, 1));
assert!(!g.has_edge(1, 0)); assert!(g.has_edge(1, 2));
}
#[test]
fn test_streaming_graph_directed() {
let mut g = StreamingGraph::new(true);
g.insert_edge(0, 1, 1.0);
assert!(g.has_edge(0, 1));
assert!(!g.has_edge(1, 0)); }
#[test]
fn test_streaming_graph_process_event() {
let mut g = StreamingGraph::new(false);
let event = StreamEvent {
edge: StreamEdge {
src: 0,
dst: 1,
weight: 1.0,
timestamp: 0,
},
op: StreamOp::Insert,
};
g.process_event(&event);
assert_eq!(g.num_edges(), 1);
assert_eq!(g.events_processed(), 1);
let del_event = StreamEvent {
edge: StreamEdge {
src: 0,
dst: 1,
weight: 1.0,
timestamp: 1,
},
op: StreamOp::Delete,
};
g.process_event(&del_event);
assert_eq!(g.num_edges(), 0);
assert_eq!(g.events_processed(), 2);
}
#[test]
fn test_streaming_graph_degree() {
let mut g = StreamingGraph::new(false);
g.insert_edge(0, 1, 1.0);
g.insert_edge(0, 2, 1.0);
g.insert_edge(0, 3, 1.0);
assert_eq!(g.degree(0), 3);
assert_eq!(g.degree(1), 1);
assert_eq!(g.degree(4), 0); }
#[test]
fn test_streaming_graph_neighbors() {
let mut g = StreamingGraph::new(false);
g.insert_edge(0, 1, 1.0);
g.insert_edge(0, 2, 2.0);
let mut neighbors = g.neighbors(0);
neighbors.sort_by_key(|&(n, _)| n);
assert_eq!(neighbors.len(), 2);
assert_eq!(neighbors[0].0, 1);
assert_eq!(neighbors[1].0, 2);
}
#[test]
fn test_streaming_graph_degree_distribution() {
let mut g = StreamingGraph::new(false);
g.insert_edge(0, 1, 1.0);
g.insert_edge(0, 2, 1.0);
g.insert_edge(0, 3, 1.0);
g.insert_edge(0, 4, 1.0);
let dist = g.degree_distribution();
assert_eq!(dist.max_degree, 4);
assert_eq!(dist.num_nodes, 5);
assert_eq!(dist.histogram.get(&4), Some(&1));
assert_eq!(dist.histogram.get(&1), Some(&4));
}
#[test]
fn test_streaming_graph_to_csr() {
let mut g = StreamingGraph::new(false);
g.insert_edge(0, 1, 1.0);
g.insert_edge(1, 2, 2.0);
let csr = g.to_csr().expect("to_csr");
assert_eq!(csr.num_nodes(), 3);
assert!(csr.has_edge(0, 1));
assert!(csr.has_edge(1, 0));
assert!(csr.has_edge(1, 2));
}
#[test]
fn test_doulion_basic() {
let mut counter = DoulionTriangleCounter::new(1.0, 42).expect("new");
counter.process_edge(0, 1);
counter.process_edge(0, 2);
counter.process_edge(0, 3);
counter.process_edge(1, 2);
counter.process_edge(1, 3);
counter.process_edge(2, 3);
let est = counter.estimated_triangles();
assert!((est - 4.0).abs() < 1e-6, "expected 4 triangles, got {est}");
let stats = counter.stats();
assert_eq!(stats.edges_processed, 6);
assert_eq!(stats.edges_sampled, 6);
}
#[test]
fn test_doulion_no_triangles() {
let mut counter = DoulionTriangleCounter::new(1.0, 42).expect("new");
counter.process_edge(0, 1);
counter.process_edge(1, 2);
counter.process_edge(2, 3);
assert!(counter.estimated_triangles().abs() < 1e-6);
}
#[test]
fn test_doulion_sampling() {
let mut counter = DoulionTriangleCounter::new(0.5, 42).expect("new");
counter.process_edge(0, 1);
counter.process_edge(0, 2);
counter.process_edge(0, 3);
counter.process_edge(1, 2);
counter.process_edge(1, 3);
counter.process_edge(2, 3);
assert!(counter.estimated_triangles() >= 0.0);
}
#[test]
fn test_doulion_invalid_prob() {
assert!(DoulionTriangleCounter::new(1.5, 42).is_err());
assert!(DoulionTriangleCounter::new(-0.1, 42).is_err());
}
#[test]
fn test_mascot_basic() {
let mut counter = MascotTriangleCounter::new(100, 42);
counter.process_edge(0, 1);
counter.process_edge(0, 2);
counter.process_edge(0, 3);
counter.process_edge(1, 2);
counter.process_edge(1, 3);
counter.process_edge(2, 3);
let est = counter.estimated_triangles();
assert!((est - 4.0).abs() < 1e-6, "expected 4 triangles, got {est}");
}
#[test]
fn test_mascot_stats() {
let mut counter = MascotTriangleCounter::new(100, 42);
counter.process_edge(0, 1);
counter.process_edge(1, 2);
let stats = counter.stats();
assert_eq!(stats.edges_processed, 2);
assert_eq!(stats.edges_sampled, 2);
}
#[test]
fn test_sliding_window_basic() {
let mut sw = SlidingWindowGraph::new(3, false);
sw.process_edge(0, 1, 1.0);
sw.process_edge(1, 2, 2.0);
sw.process_edge(2, 3, 3.0);
assert_eq!(sw.num_edges(), 3);
assert!(sw.has_edge(0, 1));
assert!(sw.has_edge(1, 2));
assert!(sw.has_edge(2, 3));
sw.process_edge(3, 4, 4.0);
assert_eq!(sw.num_edges(), 3);
assert!(!sw.has_edge(0, 1)); assert!(sw.has_edge(1, 2));
assert!(sw.has_edge(2, 3));
assert!(sw.has_edge(3, 4));
}
#[test]
fn test_sliding_window_directed() {
let mut sw = SlidingWindowGraph::new(5, true);
sw.process_edge(0, 1, 1.0);
assert!(sw.has_edge(0, 1));
assert!(!sw.has_edge(1, 0)); }
#[test]
fn test_sliding_window_degree() {
let mut sw = SlidingWindowGraph::new(10, false);
sw.process_edge(0, 1, 1.0);
sw.process_edge(0, 2, 1.0);
sw.process_edge(0, 3, 1.0);
assert_eq!(sw.degree(0), 3);
assert_eq!(sw.degree(1), 1);
}
#[test]
fn test_sliding_window_events_processed() {
let mut sw = SlidingWindowGraph::new(2, false);
sw.process_edge(0, 1, 1.0);
sw.process_edge(1, 2, 1.0);
sw.process_edge(2, 3, 1.0);
assert_eq!(sw.events_processed(), 3);
assert_eq!(sw.num_edges(), 2);
}
#[test]
fn test_sliding_window_to_csr() {
let mut sw = SlidingWindowGraph::new(10, false);
sw.process_edge(0, 1, 1.0);
sw.process_edge(1, 2, 2.0);
let csr = sw.to_csr().expect("to_csr");
assert_eq!(csr.num_nodes(), 3);
assert!(csr.has_edge(0, 1));
}
#[test]
fn test_memory_bounded_basic() {
let config = MemoryBoundedConfig {
max_memory_bytes: 400, eviction_strategy: EvictionStrategy::LeastRecentEdge,
track_degrees: true,
count_triangles: false,
triangle_sample_prob: 0.1,
};
let mut proc = MemoryBoundedProcessor::new(config);
for i in 0..20 {
proc.process_edge(i, i + 1, 1.0);
}
assert!(proc.edges_evicted() > 0);
assert!(proc.estimated_memory() <= 400);
}
#[test]
fn test_memory_bounded_degree_dist() {
let config = MemoryBoundedConfig {
max_memory_bytes: 10_000,
..Default::default()
};
let mut proc = MemoryBoundedProcessor::new(config);
proc.process_edge(0, 1, 1.0);
proc.process_edge(0, 2, 1.0);
proc.process_edge(0, 3, 1.0);
let dist = proc.degree_distribution();
assert!(dist.num_nodes > 0);
}
#[test]
fn test_memory_bounded_eviction_strategies() {
for strategy in &[
EvictionStrategy::LeastRecentEdge,
EvictionStrategy::RandomEdge,
] {
let config = MemoryBoundedConfig {
max_memory_bytes: 200,
eviction_strategy: *strategy,
..Default::default()
};
let mut proc = MemoryBoundedProcessor::new(config);
for i in 0..10 {
proc.process_edge(i, i + 1, 1.0);
}
assert!(proc.edges_evicted() > 0 || proc.estimated_memory() <= 200);
}
}
#[test]
fn test_streaming_graph_empty() {
let g = StreamingGraph::new(false);
assert_eq!(g.num_nodes(), 0);
assert_eq!(g.num_edges(), 0);
assert!(g.neighbors(0).is_empty());
assert_eq!(g.degree(0), 0);
}
#[test]
fn test_streaming_graph_delete_nonexistent() {
let mut g = StreamingGraph::new(false);
g.insert_edge(0, 1, 1.0);
g.delete_edge(5, 6); assert_eq!(g.num_edges(), 1);
}
#[test]
fn test_sliding_window_single_capacity() {
let mut sw = SlidingWindowGraph::new(1, false);
sw.process_edge(0, 1, 1.0);
assert_eq!(sw.num_edges(), 1);
assert!(sw.has_edge(0, 1));
sw.process_edge(2, 3, 2.0);
assert_eq!(sw.num_edges(), 1);
assert!(!sw.has_edge(0, 1)); assert!(sw.has_edge(2, 3));
}
}