mirror of
https://github.com/ruvnet/RuView
synced 2026-08-03 19:21:42 +00:00
feat: vendor midstream and sublinear-time-solver libraries (#109)
Add ruvnet/midstream (AIMDS real-time inference) and ruvnet/sublinear-time-solver (sublinear optimization algorithms) as vendored dependencies under vendor/.
This commit is contained in:
+325
@@ -0,0 +1,325 @@
|
||||
//! Adjacency list representation for graphs
|
||||
//!
|
||||
//! Optimized for push algorithms with efficient neighbor iteration
|
||||
//! and degree lookups.
|
||||
|
||||
use super::{Graph, NodeId, Weight, CompressedSparseRow};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Adjacency list representation of a graph
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AdjacencyList {
|
||||
/// Number of nodes in the graph
|
||||
num_nodes: usize,
|
||||
/// Adjacency lists for each node
|
||||
adjacency: Vec<Vec<(NodeId, Weight)>>,
|
||||
/// Reverse adjacency lists (for backward operations)
|
||||
reverse_adjacency: Vec<Vec<(NodeId, Weight)>>,
|
||||
/// Cached degrees for each node
|
||||
degrees: Vec<Weight>,
|
||||
/// Cached reverse degrees for each node
|
||||
reverse_degrees: Vec<Weight>,
|
||||
}
|
||||
|
||||
impl AdjacencyList {
|
||||
/// Create a new adjacency list with given number of nodes
|
||||
pub fn new(num_nodes: usize) -> Self {
|
||||
Self {
|
||||
num_nodes,
|
||||
adjacency: vec![Vec::new(); num_nodes],
|
||||
reverse_adjacency: vec![Vec::new(); num_nodes],
|
||||
degrees: vec![0.0; num_nodes],
|
||||
reverse_degrees: vec![0.0; num_nodes],
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an edge to the graph
|
||||
pub fn add_edge(&mut self, from: NodeId, to: NodeId, weight: Weight) {
|
||||
if from < self.num_nodes && to < self.num_nodes {
|
||||
self.adjacency[from].push((to, weight));
|
||||
self.reverse_adjacency[to].push((from, weight));
|
||||
self.degrees[from] += weight;
|
||||
self.reverse_degrees[to] += weight;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a sparse matrix representation
|
||||
pub fn from_csr(csr: &CompressedSparseRow) -> Self {
|
||||
let mut graph = Self::new(csr.nrows);
|
||||
|
||||
for row in 0..csr.nrows {
|
||||
for (col, value) in csr.row(row) {
|
||||
if col < csr.ncols {
|
||||
graph.adjacency[row].push((col, value));
|
||||
graph.reverse_adjacency[col].push((row, value));
|
||||
graph.degrees[row] += value;
|
||||
graph.reverse_degrees[col] += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
}
|
||||
|
||||
/// Convert to compressed sparse row format
|
||||
pub fn to_csr(&self) -> CompressedSparseRow {
|
||||
let mut row_ptr = vec![0; self.num_nodes + 1];
|
||||
let mut col_indices = Vec::new();
|
||||
let mut values = Vec::new();
|
||||
|
||||
for (row, neighbors) in self.adjacency.iter().enumerate() {
|
||||
row_ptr[row + 1] = row_ptr[row] + neighbors.len();
|
||||
|
||||
for &(col, weight) in neighbors {
|
||||
col_indices.push(col);
|
||||
values.push(weight);
|
||||
}
|
||||
}
|
||||
|
||||
CompressedSparseRow {
|
||||
nrows: self.num_nodes,
|
||||
ncols: self.num_nodes,
|
||||
row_ptr,
|
||||
col_indices,
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get reverse neighbors (for backward push)
|
||||
pub fn reverse_neighbors(&self, node: NodeId) -> &[(NodeId, Weight)] {
|
||||
if node < self.num_nodes {
|
||||
&self.reverse_adjacency[node]
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the reverse degree of a node
|
||||
pub fn reverse_degree(&self, node: NodeId) -> Weight {
|
||||
if node < self.num_nodes {
|
||||
self.reverse_degrees[node]
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize the graph to make it a proper transition matrix
|
||||
pub fn normalize(&mut self) {
|
||||
for node in 0..self.num_nodes {
|
||||
let degree = self.degrees[node];
|
||||
if degree > 0.0 {
|
||||
for (_, weight) in &mut self.adjacency[node] {
|
||||
*weight /= degree;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recompute degrees after normalization
|
||||
self.degrees.fill(1.0);
|
||||
|
||||
// Also normalize reverse adjacency
|
||||
for node in 0..self.num_nodes {
|
||||
let reverse_degree = self.reverse_degrees[node];
|
||||
if reverse_degree > 0.0 {
|
||||
for (_, weight) in &mut self.reverse_adjacency[node] {
|
||||
*weight /= reverse_degree;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.reverse_degrees.fill(1.0);
|
||||
}
|
||||
|
||||
/// Get the sparsity of the graph (fraction of possible edges that exist)
|
||||
pub fn sparsity(&self) -> f64 {
|
||||
let total_edges: usize = self.adjacency.iter().map(|adj| adj.len()).sum();
|
||||
let max_edges = self.num_nodes * self.num_nodes;
|
||||
1.0 - (total_edges as f64 / max_edges as f64)
|
||||
}
|
||||
|
||||
/// Check if the graph is strongly connected (simplified check)
|
||||
pub fn is_strongly_connected(&self) -> bool {
|
||||
// Simple check: every node has at least one outgoing and incoming edge
|
||||
for node in 0..self.num_nodes {
|
||||
if self.adjacency[node].is_empty() || self.reverse_adjacency[node].is_empty() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Graph for AdjacencyList {
|
||||
fn num_nodes(&self) -> usize {
|
||||
self.num_nodes
|
||||
}
|
||||
|
||||
fn num_edges(&self) -> usize {
|
||||
self.adjacency.iter().map(|adj| adj.len()).sum()
|
||||
}
|
||||
|
||||
fn neighbors(&self, node: NodeId) -> Vec<(NodeId, Weight)> {
|
||||
if node < self.num_nodes {
|
||||
self.adjacency[node].clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn degree(&self, node: NodeId) -> Weight {
|
||||
if node < self.num_nodes {
|
||||
self.degrees[node]
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn has_edge(&self, from: NodeId, to: NodeId) -> bool {
|
||||
if from < self.num_nodes {
|
||||
self.adjacency[from].iter().any(|&(neighbor, _)| neighbor == to)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn edge_weight(&self, from: NodeId, to: NodeId) -> Option<Weight> {
|
||||
if from < self.num_nodes {
|
||||
self.adjacency[from]
|
||||
.iter()
|
||||
.find(|&&(neighbor, _)| neighbor == to)
|
||||
.map(|&(_, weight)| weight)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Specialized graph structure optimized for push algorithms
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PushGraph {
|
||||
/// Forward adjacency representation
|
||||
pub adjacency: CompressedSparseRow,
|
||||
/// Reverse adjacency for backward push
|
||||
pub reverse_adjacency: CompressedSparseRow,
|
||||
/// Node degrees (row sums)
|
||||
pub degrees: Vec<f64>,
|
||||
/// Reverse degrees (column sums)
|
||||
pub reverse_degrees: Vec<f64>,
|
||||
}
|
||||
|
||||
impl PushGraph {
|
||||
/// Create a push graph from a sparse matrix
|
||||
pub fn from_matrix(matrix: &CompressedSparseRow) -> Self {
|
||||
let adjacency = matrix.clone();
|
||||
let reverse_adjacency = matrix.transpose();
|
||||
let degrees = adjacency.row_sums();
|
||||
let reverse_degrees = reverse_adjacency.row_sums();
|
||||
|
||||
Self {
|
||||
adjacency,
|
||||
reverse_adjacency,
|
||||
degrees,
|
||||
reverse_degrees,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a push graph from edge list
|
||||
pub fn from_edges(num_nodes: usize, edges: &[(usize, usize, f64)]) -> Self {
|
||||
let mut adjacency_list = AdjacencyList::new(num_nodes);
|
||||
|
||||
for &(from, to, weight) in edges {
|
||||
if from < num_nodes && to < num_nodes {
|
||||
adjacency_list.add_edge(from, to, weight);
|
||||
}
|
||||
}
|
||||
|
||||
let matrix = adjacency_list.to_csr();
|
||||
Self::from_matrix(&matrix)
|
||||
}
|
||||
|
||||
/// Get the number of nodes
|
||||
pub fn num_nodes(&self) -> usize {
|
||||
self.adjacency.nrows
|
||||
}
|
||||
|
||||
/// Get the number of edges
|
||||
pub fn num_edges(&self) -> usize {
|
||||
self.adjacency.nnz()
|
||||
}
|
||||
|
||||
/// Get forward neighbors with weights
|
||||
pub fn forward_neighbors(&self, node: usize) -> impl Iterator<Item = (usize, f64)> + '_ {
|
||||
self.adjacency.row(node)
|
||||
}
|
||||
|
||||
/// Get backward neighbors with weights
|
||||
pub fn backward_neighbors(&self, node: usize) -> impl Iterator<Item = (usize, f64)> + '_ {
|
||||
self.reverse_adjacency.row(node)
|
||||
}
|
||||
|
||||
/// Get the out-degree of a node
|
||||
pub fn out_degree(&self, node: usize) -> f64 {
|
||||
if node < self.degrees.len() {
|
||||
self.degrees[node]
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the in-degree of a node
|
||||
pub fn in_degree(&self, node: usize) -> f64 {
|
||||
if node < self.reverse_degrees.len() {
|
||||
self.reverse_degrees[node]
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_adjacency_list_creation() {
|
||||
let mut graph = AdjacencyList::new(3);
|
||||
graph.add_edge(0, 1, 0.5);
|
||||
graph.add_edge(1, 2, 1.0);
|
||||
graph.add_edge(2, 0, 0.3);
|
||||
|
||||
assert_eq!(graph.num_nodes(), 3);
|
||||
assert_eq!(graph.num_edges(), 3);
|
||||
assert_eq!(graph.degree(0), 0.5);
|
||||
assert_eq!(graph.degree(1), 1.0);
|
||||
assert_eq!(graph.degree(2), 0.3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_csr_conversion() {
|
||||
let mut graph = AdjacencyList::new(2);
|
||||
graph.add_edge(0, 1, 1.0);
|
||||
graph.add_edge(1, 0, 0.5);
|
||||
|
||||
let csr = graph.to_csr();
|
||||
assert_eq!(csr.nrows, 2);
|
||||
assert_eq!(csr.ncols, 2);
|
||||
assert_eq!(csr.nnz(), 2);
|
||||
|
||||
let reconstructed = AdjacencyList::from_csr(&csr);
|
||||
assert_eq!(reconstructed.degree(0), 1.0);
|
||||
assert_eq!(reconstructed.degree(1), 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_graph() {
|
||||
let mut csr = CompressedSparseRow::new(3, 3);
|
||||
csr.row_ptr = vec![0, 1, 2, 3];
|
||||
csr.col_indices = vec![1, 2, 0];
|
||||
csr.values = vec![1.0, 0.5, 0.8];
|
||||
|
||||
let push_graph = PushGraph::from_matrix(&csr);
|
||||
assert_eq!(push_graph.num_nodes(), 3);
|
||||
assert_eq!(push_graph.out_degree(0), 1.0);
|
||||
assert_eq!(push_graph.in_degree(0), 0.8);
|
||||
}
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
//! Graph representations and algorithms for the sublinear time solver
|
||||
//!
|
||||
//! This module provides efficient graph data structures optimized for
|
||||
//! push-based algorithms and random walk computations.
|
||||
|
||||
pub mod adjacency;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use bit_set::BitSet;
|
||||
|
||||
/// A node identifier in the graph
|
||||
pub type NodeId = usize;
|
||||
|
||||
/// Weight type for edges
|
||||
pub type Weight = f64;
|
||||
|
||||
/// Graph trait for different implementations
|
||||
pub trait Graph {
|
||||
/// Number of nodes in the graph
|
||||
fn num_nodes(&self) -> usize;
|
||||
|
||||
/// Number of edges in the graph
|
||||
fn num_edges(&self) -> usize;
|
||||
|
||||
/// Get neighbors of a node with their weights
|
||||
fn neighbors(&self, node: NodeId) -> Vec<(NodeId, Weight)>;
|
||||
|
||||
/// Get the degree of a node (sum of edge weights)
|
||||
fn degree(&self, node: NodeId) -> Weight;
|
||||
|
||||
/// Check if an edge exists between two nodes
|
||||
fn has_edge(&self, from: NodeId, to: NodeId) -> bool;
|
||||
|
||||
/// Get the weight of an edge if it exists
|
||||
fn edge_weight(&self, from: NodeId, to: NodeId) -> Option<Weight>;
|
||||
}
|
||||
|
||||
/// Sparse matrix representation using Compressed Sparse Row format
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompressedSparseRow {
|
||||
/// Number of rows in the matrix
|
||||
pub nrows: usize,
|
||||
/// Number of columns in the matrix
|
||||
pub ncols: usize,
|
||||
/// Row pointers (length = nrows + 1)
|
||||
pub row_ptr: Vec<usize>,
|
||||
/// Column indices of non-zero elements
|
||||
pub col_indices: Vec<usize>,
|
||||
/// Values of non-zero elements
|
||||
pub values: Vec<f64>,
|
||||
}
|
||||
|
||||
impl CompressedSparseRow {
|
||||
/// Create a new empty CSR matrix
|
||||
pub fn new(nrows: usize, ncols: usize) -> Self {
|
||||
Self {
|
||||
nrows,
|
||||
ncols,
|
||||
row_ptr: vec![0; nrows + 1],
|
||||
col_indices: Vec::new(),
|
||||
values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of non-zero elements
|
||||
pub fn nnz(&self) -> usize {
|
||||
self.values.len()
|
||||
}
|
||||
|
||||
/// Get neighbors of a row with their values
|
||||
pub fn row(&self, row: usize) -> impl Iterator<Item = (usize, f64)> + '_ {
|
||||
let start = self.row_ptr[row];
|
||||
let end = self.row_ptr[row + 1];
|
||||
|
||||
(start..end).map(move |idx| {
|
||||
(self.col_indices[idx], self.values[idx])
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute row sums (degrees for graphs)
|
||||
pub fn row_sums(&self) -> Vec<f64> {
|
||||
let mut sums = vec![0.0; self.nrows];
|
||||
for row in 0..self.nrows {
|
||||
for (_, value) in self.row(row) {
|
||||
sums[row] += value;
|
||||
}
|
||||
}
|
||||
sums
|
||||
}
|
||||
|
||||
/// Transpose the matrix
|
||||
pub fn transpose(&self) -> Self {
|
||||
let mut col_counts = vec![0; self.ncols];
|
||||
|
||||
// Count elements per column
|
||||
for &col in &self.col_indices {
|
||||
col_counts[col] += 1;
|
||||
}
|
||||
|
||||
// Build row pointers for transposed matrix
|
||||
let mut new_row_ptr = vec![0; self.ncols + 1];
|
||||
for i in 0..self.ncols {
|
||||
new_row_ptr[i + 1] = new_row_ptr[i] + col_counts[i];
|
||||
}
|
||||
|
||||
let mut new_col_indices = vec![0; self.nnz()];
|
||||
let mut new_values = vec![0.0; self.nnz()];
|
||||
let mut col_positions = new_row_ptr.clone();
|
||||
|
||||
// Fill transposed matrix
|
||||
for row in 0..self.nrows {
|
||||
for (col, value) in self.row(row) {
|
||||
let pos = col_positions[col];
|
||||
new_col_indices[pos] = row;
|
||||
new_values[pos] = value;
|
||||
col_positions[col] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
nrows: self.ncols,
|
||||
ncols: self.nrows,
|
||||
row_ptr: new_row_ptr,
|
||||
col_indices: new_col_indices,
|
||||
values: new_values,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Work queue for push algorithms with priority-based processing
|
||||
#[derive(Debug)]
|
||||
pub struct WorkQueue {
|
||||
/// Binary heap for priority queue
|
||||
heap: std::collections::BinaryHeap<WorkItem>,
|
||||
/// Bit set to track which nodes are in the queue
|
||||
in_queue: BitSet,
|
||||
/// Threshold for adding items to queue
|
||||
threshold: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd)]
|
||||
struct WorkItem {
|
||||
/// Priority value (higher = more important)
|
||||
priority: OrderedFloat,
|
||||
/// Node identifier
|
||||
node_id: usize,
|
||||
}
|
||||
|
||||
/// Wrapper for f64 to enable ordering
|
||||
#[derive(Debug, PartialEq, PartialOrd)]
|
||||
struct OrderedFloat(f64);
|
||||
|
||||
impl Eq for OrderedFloat {}
|
||||
impl Ord for OrderedFloat {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkQueue {
|
||||
/// Create a new work queue with given threshold
|
||||
pub fn new(threshold: f64) -> Self {
|
||||
Self {
|
||||
heap: std::collections::BinaryHeap::new(),
|
||||
in_queue: BitSet::new(),
|
||||
threshold,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a node if it meets the threshold
|
||||
pub fn push_if_threshold(&mut self, node: usize, residual: f64, degree: f64) {
|
||||
let priority = if degree > 0.0 { residual / degree } else { residual };
|
||||
|
||||
if priority >= self.threshold && !self.in_queue.contains(node) {
|
||||
self.heap.push(WorkItem {
|
||||
priority: OrderedFloat(priority),
|
||||
node_id: node,
|
||||
});
|
||||
self.in_queue.insert(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pop the highest priority item
|
||||
pub fn pop(&mut self) -> Option<(usize, f64)> {
|
||||
if let Some(item) = self.heap.pop() {
|
||||
self.in_queue.remove(item.node_id);
|
||||
Some((item.node_id, item.priority.0))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if queue is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.heap.is_empty()
|
||||
}
|
||||
|
||||
/// Get current queue size
|
||||
pub fn len(&self) -> usize {
|
||||
self.heap.len()
|
||||
}
|
||||
|
||||
/// Adaptively adjust threshold based on queue size
|
||||
pub fn adaptive_threshold(&mut self, max_queue_size: usize, min_queue_size: usize) {
|
||||
let current_size = self.len();
|
||||
|
||||
if current_size > max_queue_size {
|
||||
self.threshold *= 1.1; // Increase threshold to reduce queue size
|
||||
} else if current_size < min_queue_size && self.threshold > 1e-12 {
|
||||
self.threshold *= 0.9; // Decrease threshold to increase queue size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracker for visited nodes during push operations
|
||||
#[derive(Debug)]
|
||||
pub struct VisitedTracker {
|
||||
/// Bit set for fast membership testing
|
||||
visited: BitSet,
|
||||
/// Order in which nodes were visited
|
||||
visit_order: Vec<usize>,
|
||||
/// Timestamps for each node
|
||||
timestamps: Vec<u32>,
|
||||
/// Current timestamp
|
||||
current_time: u32,
|
||||
}
|
||||
|
||||
impl VisitedTracker {
|
||||
/// Create a new visited tracker for n nodes
|
||||
pub fn new(n: usize) -> Self {
|
||||
Self {
|
||||
visited: BitSet::new(),
|
||||
visit_order: Vec::new(),
|
||||
timestamps: vec![0; n],
|
||||
current_time: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a node as visited, returns true if newly visited
|
||||
pub fn mark_visited(&mut self, node: usize) -> bool {
|
||||
if !self.visited.contains(node) {
|
||||
self.visited.insert(node);
|
||||
self.visit_order.push(node);
|
||||
if node < self.timestamps.len() {
|
||||
self.timestamps[node] = self.current_time;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a node has been visited
|
||||
pub fn is_visited(&self, node: usize) -> bool {
|
||||
self.visited.contains(node)
|
||||
}
|
||||
|
||||
/// Get all visited nodes in order
|
||||
pub fn visited_nodes(&self) -> &[usize] {
|
||||
&self.visit_order
|
||||
}
|
||||
|
||||
/// Reset for a new query (incremental timestamp)
|
||||
pub fn reset_for_new_query(&mut self) {
|
||||
self.current_time += 1;
|
||||
self.visit_order.clear();
|
||||
|
||||
// Full reset if timestamp overflow
|
||||
if self.current_time == u32::MAX {
|
||||
self.full_reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete reset of all tracking data
|
||||
fn full_reset(&mut self) {
|
||||
self.visited.clear();
|
||||
self.visit_order.clear();
|
||||
self.timestamps.fill(0);
|
||||
self.current_time = 1;
|
||||
}
|
||||
|
||||
/// Get number of visited nodes
|
||||
pub fn num_visited(&self) -> usize {
|
||||
self.visit_order.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub use adjacency::AdjacencyList;
|
||||
Reference in New Issue
Block a user