Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
693
examples/prime-radiant/src/spectral/analyzer.rs
Normal file
693
examples/prime-radiant/src/spectral/analyzer.rs
Normal file
@@ -0,0 +1,693 @@
|
||||
//! Core Spectral Analyzer
|
||||
//!
|
||||
//! Provides the main `SpectralAnalyzer` struct for computing spectral properties
|
||||
//! of graphs, including eigenvalues, eigenvectors, and derived invariants.
|
||||
|
||||
use super::lanczos::{LanczosAlgorithm, PowerIteration};
|
||||
use super::types::{Graph, SparseMatrix, SpectralGap, Vector, Bottleneck, MinCutPrediction, EPS, NodeId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Core spectral analyzer for graph analysis
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpectralAnalyzer {
|
||||
/// The graph being analyzed
|
||||
pub graph: Graph,
|
||||
/// Graph Laplacian matrix
|
||||
pub laplacian: SparseMatrix,
|
||||
/// Normalized Laplacian matrix
|
||||
pub normalized_laplacian: SparseMatrix,
|
||||
/// Computed eigenvalues (sorted ascending)
|
||||
pub eigenvalues: Vec<f64>,
|
||||
/// Corresponding eigenvectors
|
||||
pub eigenvectors: Vec<Vector>,
|
||||
/// Configuration
|
||||
config: SpectralConfig,
|
||||
}
|
||||
|
||||
/// Configuration for spectral analysis
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpectralConfig {
|
||||
/// Number of eigenvalues to compute
|
||||
pub num_eigenvalues: usize,
|
||||
/// Use normalized Laplacian
|
||||
pub use_normalized: bool,
|
||||
/// Maximum iterations for eigenvalue computation
|
||||
pub max_iter: usize,
|
||||
/// Convergence tolerance
|
||||
pub tol: f64,
|
||||
}
|
||||
|
||||
impl Default for SpectralConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
num_eigenvalues: 10,
|
||||
use_normalized: true,
|
||||
max_iter: 1000,
|
||||
tol: 1e-10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpectralConfig {
|
||||
/// Create a builder for configuration
|
||||
pub fn builder() -> SpectralConfigBuilder {
|
||||
SpectralConfigBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for SpectralConfig
|
||||
#[derive(Default)]
|
||||
pub struct SpectralConfigBuilder {
|
||||
config: SpectralConfig,
|
||||
}
|
||||
|
||||
impl SpectralConfigBuilder {
|
||||
/// Set number of eigenvalues to compute
|
||||
pub fn num_eigenvalues(mut self, n: usize) -> Self {
|
||||
self.config.num_eigenvalues = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use normalized Laplacian
|
||||
pub fn normalized(mut self, use_norm: bool) -> Self {
|
||||
self.config.use_normalized = use_norm;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set maximum iterations
|
||||
pub fn max_iter(mut self, n: usize) -> Self {
|
||||
self.config.max_iter = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set convergence tolerance
|
||||
pub fn tolerance(mut self, tol: f64) -> Self {
|
||||
self.config.tol = tol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the configuration
|
||||
pub fn build(self) -> SpectralConfig {
|
||||
self.config
|
||||
}
|
||||
}
|
||||
|
||||
impl SpectralAnalyzer {
|
||||
/// Create a new spectral analyzer for a graph
|
||||
pub fn new(graph: Graph) -> Self {
|
||||
Self::with_config(graph, SpectralConfig::default())
|
||||
}
|
||||
|
||||
/// Create with custom configuration
|
||||
pub fn with_config(graph: Graph, config: SpectralConfig) -> Self {
|
||||
let laplacian = graph.laplacian();
|
||||
let normalized_laplacian = graph.normalized_laplacian();
|
||||
|
||||
Self {
|
||||
graph,
|
||||
laplacian,
|
||||
normalized_laplacian,
|
||||
eigenvalues: Vec::new(),
|
||||
eigenvectors: Vec::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the Laplacian spectrum
|
||||
pub fn compute_laplacian_spectrum(&mut self) -> &[f64] {
|
||||
let matrix = if self.config.use_normalized {
|
||||
&self.normalized_laplacian
|
||||
} else {
|
||||
&self.laplacian
|
||||
};
|
||||
|
||||
let lanczos = LanczosAlgorithm::new(self.config.num_eigenvalues);
|
||||
let (eigenvalues, eigenvectors) = lanczos.compute_smallest(matrix);
|
||||
|
||||
self.eigenvalues = eigenvalues;
|
||||
self.eigenvectors = eigenvectors;
|
||||
|
||||
&self.eigenvalues
|
||||
}
|
||||
|
||||
/// Get the algebraic connectivity (second smallest eigenvalue)
|
||||
/// Also known as the Fiedler value
|
||||
pub fn algebraic_connectivity(&self) -> f64 {
|
||||
if self.eigenvalues.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Skip the first eigenvalue (should be 0 for connected graphs)
|
||||
// Find the first non-trivial eigenvalue
|
||||
for &ev in &self.eigenvalues {
|
||||
if ev > EPS {
|
||||
return ev;
|
||||
}
|
||||
}
|
||||
|
||||
0.0
|
||||
}
|
||||
|
||||
/// Get the Fiedler vector (eigenvector for second smallest eigenvalue)
|
||||
pub fn fiedler_vector(&self) -> Option<&Vector> {
|
||||
if self.eigenvectors.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find index of first non-trivial eigenvalue
|
||||
for (i, &ev) in self.eigenvalues.iter().enumerate() {
|
||||
if ev > EPS {
|
||||
return self.eigenvectors.get(i);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Compute the spectral gap
|
||||
pub fn spectral_gap(&self) -> SpectralGap {
|
||||
let lambda_1 = self.algebraic_connectivity();
|
||||
let lambda_2 = if self.eigenvalues.len() >= 3 {
|
||||
// Find third non-trivial eigenvalue
|
||||
let mut count = 0;
|
||||
for &ev in &self.eigenvalues {
|
||||
if ev > EPS {
|
||||
count += 1;
|
||||
if count == 2 {
|
||||
return SpectralGap::new(lambda_1, ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
lambda_1 * 2.0 // Default if not enough eigenvalues
|
||||
} else {
|
||||
lambda_1 * 2.0
|
||||
};
|
||||
|
||||
SpectralGap::new(lambda_1, lambda_2)
|
||||
}
|
||||
|
||||
/// Predict minimum cut difficulty using spectral gap
|
||||
pub fn predict_min_cut(&self) -> MinCutPrediction {
|
||||
let fiedler_value = self.algebraic_connectivity();
|
||||
let n = self.graph.n;
|
||||
let total_weight = self.graph.total_weight();
|
||||
|
||||
// Cheeger inequality bounds on isoperimetric number
|
||||
// h(G) >= lambda_2 / 2 (lower bound)
|
||||
// h(G) <= sqrt(2 * lambda_2) (upper bound)
|
||||
|
||||
let lower_bound = fiedler_value / 2.0;
|
||||
let upper_bound = (2.0 * fiedler_value).sqrt();
|
||||
|
||||
// Predicted cut based on isoperimetric number and graph volume
|
||||
let predicted_cut = if total_weight > EPS {
|
||||
// Cut value ~ h(G) * min_volume
|
||||
// For balanced cut, min_volume ~ total_weight / 2
|
||||
let avg_bound = (lower_bound + upper_bound) / 2.0;
|
||||
avg_bound * total_weight / 2.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Compute confidence based on spectral gap clarity
|
||||
let gap = self.spectral_gap();
|
||||
let confidence = if gap.ratio > 2.0 {
|
||||
0.9 // Clear separation
|
||||
} else if gap.ratio > 1.5 {
|
||||
0.7
|
||||
} else if gap.ratio > 1.2 {
|
||||
0.5
|
||||
} else {
|
||||
0.3 // Gap unclear, low confidence
|
||||
};
|
||||
|
||||
// Suggest cut nodes from Fiedler vector
|
||||
let cut_nodes = self.find_spectral_cut();
|
||||
|
||||
MinCutPrediction {
|
||||
predicted_cut,
|
||||
lower_bound: lower_bound * total_weight / 2.0,
|
||||
upper_bound: upper_bound * total_weight / 2.0,
|
||||
confidence,
|
||||
cut_nodes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the optimal cut using the Fiedler vector
|
||||
fn find_spectral_cut(&self) -> Vec<NodeId> {
|
||||
let fiedler = match self.fiedler_vector() {
|
||||
Some(v) => v,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
// Simple threshold at zero
|
||||
let positive_nodes: Vec<NodeId> = fiedler
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &v)| v > 0.0)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
let negative_nodes: Vec<NodeId> = fiedler
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &v)| v <= 0.0)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
// Return the smaller set (typically defines the cut boundary)
|
||||
if positive_nodes.len() <= negative_nodes.len() {
|
||||
positive_nodes
|
||||
} else {
|
||||
negative_nodes
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect structural bottlenecks via Fiedler vector analysis
|
||||
pub fn detect_bottlenecks(&self) -> Vec<Bottleneck> {
|
||||
let fiedler = match self.fiedler_vector() {
|
||||
Some(v) => v.clone(),
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
let n = self.graph.n;
|
||||
let mut bottlenecks = Vec::new();
|
||||
|
||||
// Sort nodes by Fiedler value
|
||||
let mut sorted_indices: Vec<usize> = (0..n).collect();
|
||||
sorted_indices.sort_by(|&a, &b| {
|
||||
fiedler[a].partial_cmp(&fiedler[b]).unwrap()
|
||||
});
|
||||
|
||||
// Find bottleneck at median split
|
||||
let mid = n / 2;
|
||||
let left_set: Vec<NodeId> = sorted_indices[..mid].to_vec();
|
||||
let right_set: Vec<NodeId> = sorted_indices[mid..].to_vec();
|
||||
|
||||
// Find crossing edges
|
||||
let left_set_hashset: std::collections::HashSet<NodeId> =
|
||||
left_set.iter().cloned().collect();
|
||||
|
||||
let mut crossing_edges = Vec::new();
|
||||
for &u in &left_set {
|
||||
for &(v, _) in &self.graph.adj[u] {
|
||||
if !left_set_hashset.contains(&v) {
|
||||
crossing_edges.push((u.min(v), u.max(v)));
|
||||
}
|
||||
}
|
||||
}
|
||||
crossing_edges.sort();
|
||||
crossing_edges.dedup();
|
||||
|
||||
// Compute bottleneck score (conductance)
|
||||
let left_volume: f64 = left_set.iter().map(|&i| self.graph.degree(i)).sum();
|
||||
let right_volume: f64 = right_set.iter().map(|&i| self.graph.degree(i)).sum();
|
||||
let cut_weight: f64 = crossing_edges
|
||||
.iter()
|
||||
.map(|&(u, v)| {
|
||||
self.graph.adj[u]
|
||||
.iter()
|
||||
.find(|(n, _)| *n == v)
|
||||
.map(|(_, w)| *w)
|
||||
.unwrap_or(0.0)
|
||||
})
|
||||
.sum();
|
||||
|
||||
let min_volume = left_volume.min(right_volume);
|
||||
let score = if min_volume > EPS {
|
||||
cut_weight / min_volume
|
||||
} else {
|
||||
f64::INFINITY
|
||||
};
|
||||
|
||||
let volume_ratio = if (left_volume + right_volume) > EPS {
|
||||
left_volume.min(right_volume) / (left_volume + right_volume)
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
|
||||
// Find nodes at the bottleneck (near zero in Fiedler vector)
|
||||
let threshold = self.compute_fiedler_threshold(&fiedler);
|
||||
let bottleneck_nodes: Vec<NodeId> = fiedler
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &v)| v.abs() < threshold)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
bottlenecks.push(Bottleneck {
|
||||
nodes: bottleneck_nodes,
|
||||
crossing_edges,
|
||||
score,
|
||||
volume_ratio,
|
||||
});
|
||||
|
||||
// Look for additional bottlenecks at different thresholds
|
||||
self.find_additional_bottlenecks(&sorted_indices, &fiedler, &mut bottlenecks);
|
||||
|
||||
bottlenecks
|
||||
}
|
||||
|
||||
/// Compute adaptive threshold for Fiedler vector
|
||||
fn compute_fiedler_threshold(&self, fiedler: &[f64]) -> f64 {
|
||||
let max_val = fiedler.iter().cloned().fold(0.0f64, f64::max);
|
||||
let min_val = fiedler.iter().cloned().fold(0.0f64, f64::min);
|
||||
let range = max_val - min_val;
|
||||
|
||||
if range > EPS {
|
||||
range * 0.1 // 10% of range
|
||||
} else {
|
||||
0.01
|
||||
}
|
||||
}
|
||||
|
||||
/// Find additional bottlenecks at quartile splits
|
||||
fn find_additional_bottlenecks(
|
||||
&self,
|
||||
sorted_indices: &[usize],
|
||||
fiedler: &[f64],
|
||||
bottlenecks: &mut Vec<Bottleneck>,
|
||||
) {
|
||||
let n = self.graph.n;
|
||||
|
||||
// Check at quartiles
|
||||
for &split_point in &[n / 4, 3 * n / 4] {
|
||||
if split_point == 0 || split_point >= n {
|
||||
continue;
|
||||
}
|
||||
|
||||
let left_set: Vec<NodeId> = sorted_indices[..split_point].to_vec();
|
||||
let left_set_hashset: std::collections::HashSet<NodeId> =
|
||||
left_set.iter().cloned().collect();
|
||||
|
||||
let mut crossing_edges = Vec::new();
|
||||
for &u in &left_set {
|
||||
for &(v, _) in &self.graph.adj[u] {
|
||||
if !left_set_hashset.contains(&v) {
|
||||
crossing_edges.push((u.min(v), u.max(v)));
|
||||
}
|
||||
}
|
||||
}
|
||||
crossing_edges.sort();
|
||||
crossing_edges.dedup();
|
||||
|
||||
let left_volume: f64 = left_set.iter().map(|&i| self.graph.degree(i)).sum();
|
||||
let right_volume: f64 = sorted_indices[split_point..]
|
||||
.iter()
|
||||
.map(|&i| self.graph.degree(i))
|
||||
.sum();
|
||||
|
||||
let cut_weight: f64 = crossing_edges
|
||||
.iter()
|
||||
.map(|&(u, v)| {
|
||||
self.graph.adj[u]
|
||||
.iter()
|
||||
.find(|(n, _)| *n == v)
|
||||
.map(|(_, w)| *w)
|
||||
.unwrap_or(0.0)
|
||||
})
|
||||
.sum();
|
||||
|
||||
let min_volume = left_volume.min(right_volume);
|
||||
let score = if min_volume > EPS {
|
||||
cut_weight / min_volume
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let volume_ratio = if (left_volume + right_volume) > EPS {
|
||||
left_volume.min(right_volume) / (left_volume + right_volume)
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
|
||||
// Only add if it's a significantly different bottleneck
|
||||
if score < 0.9 * bottlenecks[0].score {
|
||||
let threshold_val = fiedler[sorted_indices[split_point]].abs() * 0.5;
|
||||
let bottleneck_nodes: Vec<NodeId> = fiedler
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &v)| (v - fiedler[sorted_indices[split_point]]).abs() < threshold_val)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
bottlenecks.push(Bottleneck {
|
||||
nodes: bottleneck_nodes,
|
||||
crossing_edges,
|
||||
score,
|
||||
volume_ratio,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort bottlenecks by score (ascending - lower is tighter)
|
||||
bottlenecks.sort_by(|a, b| a.score.partial_cmp(&b.score).unwrap());
|
||||
}
|
||||
|
||||
/// Get spectral embedding of nodes (coordinates from eigenvectors)
|
||||
pub fn spectral_embedding(&self, dimensions: usize) -> Vec<Vector> {
|
||||
let n = self.graph.n;
|
||||
let dim = dimensions.min(self.eigenvectors.len());
|
||||
|
||||
let mut embedding = vec![vec![0.0; dim]; n];
|
||||
|
||||
// Skip the trivial eigenvector (constant)
|
||||
let start_idx = if self.eigenvalues.first().map(|&v| v < EPS).unwrap_or(false) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
for d in 0..dim {
|
||||
let ev_idx = start_idx + d;
|
||||
if ev_idx < self.eigenvectors.len() {
|
||||
for i in 0..n {
|
||||
embedding[i][d] = self.eigenvectors[ev_idx][i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
embedding
|
||||
}
|
||||
|
||||
/// Compute the effective resistance between two nodes
|
||||
pub fn effective_resistance(&self, u: NodeId, v: NodeId) -> f64 {
|
||||
if u == v || self.eigenvalues.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut resistance = 0.0;
|
||||
|
||||
// R_uv = sum_i (1/lambda_i) * (phi_i(u) - phi_i(v))^2
|
||||
// Skip the zero eigenvalue
|
||||
for (i, (&lambda, eigvec)) in self.eigenvalues.iter()
|
||||
.zip(self.eigenvectors.iter())
|
||||
.enumerate()
|
||||
{
|
||||
if lambda > EPS {
|
||||
let diff = eigvec[u] - eigvec[v];
|
||||
resistance += diff * diff / lambda;
|
||||
}
|
||||
}
|
||||
|
||||
resistance
|
||||
}
|
||||
|
||||
/// Compute total effective resistance (Kirchhoff index)
|
||||
pub fn kirchhoff_index(&self) -> f64 {
|
||||
let n = self.graph.n;
|
||||
|
||||
if self.eigenvalues.is_empty() {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
|
||||
// K(G) = n * sum_i (1/lambda_i) for lambda_i > 0
|
||||
let sum_reciprocal: f64 = self.eigenvalues
|
||||
.iter()
|
||||
.filter(|&&lambda| lambda > EPS)
|
||||
.map(|&lambda| 1.0 / lambda)
|
||||
.sum();
|
||||
|
||||
n as f64 * sum_reciprocal
|
||||
}
|
||||
|
||||
/// Estimate the spectral radius (largest eigenvalue)
|
||||
pub fn spectral_radius(&self) -> f64 {
|
||||
let power = PowerIteration::default();
|
||||
let (lambda, _) = power.largest_eigenvalue(&self.laplacian);
|
||||
lambda
|
||||
}
|
||||
|
||||
/// Check if graph is bipartite using spectral properties
|
||||
pub fn is_bipartite(&self) -> bool {
|
||||
// A graph is bipartite iff lambda_max = -lambda_min for the adjacency matrix
|
||||
let adj = self.graph.adjacency_matrix();
|
||||
let power = PowerIteration::default();
|
||||
|
||||
let (lambda_max, _) = power.largest_eigenvalue(&adj);
|
||||
let (lambda_min, _) = power.smallest_eigenvalue(&adj, 0.0);
|
||||
|
||||
(lambda_max + lambda_min).abs() < 0.01
|
||||
}
|
||||
|
||||
/// Get the number of connected components from eigenvalue spectrum
|
||||
pub fn spectral_components(&self) -> usize {
|
||||
// Count eigenvalues very close to zero
|
||||
self.eigenvalues
|
||||
.iter()
|
||||
.filter(|&&ev| ev.abs() < 1e-6)
|
||||
.count()
|
||||
.max(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_path_graph(n: usize) -> Graph {
|
||||
let edges: Vec<(usize, usize, f64)> = (0..n - 1)
|
||||
.map(|i| (i, i + 1, 1.0))
|
||||
.collect();
|
||||
Graph::from_edges(n, &edges)
|
||||
}
|
||||
|
||||
fn create_cycle_graph(n: usize) -> Graph {
|
||||
let mut edges: Vec<(usize, usize, f64)> = (0..n - 1)
|
||||
.map(|i| (i, i + 1, 1.0))
|
||||
.collect();
|
||||
edges.push((n - 1, 0, 1.0)); // Close the cycle
|
||||
Graph::from_edges(n, &edges)
|
||||
}
|
||||
|
||||
fn create_complete_graph(n: usize) -> Graph {
|
||||
let mut edges = Vec::new();
|
||||
for i in 0..n {
|
||||
for j in i + 1..n {
|
||||
edges.push((i, j, 1.0));
|
||||
}
|
||||
}
|
||||
Graph::from_edges(n, &edges)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyzer_path_graph() {
|
||||
let g = create_path_graph(5);
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
// Path graph should have small algebraic connectivity
|
||||
let lambda_2 = analyzer.algebraic_connectivity();
|
||||
assert!(lambda_2 > 0.0);
|
||||
assert!(lambda_2 < 1.0);
|
||||
|
||||
// Should have one component
|
||||
assert_eq!(analyzer.spectral_components(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyzer_complete_graph() {
|
||||
let g = create_complete_graph(5);
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
// Complete graph has high algebraic connectivity
|
||||
let lambda_2 = analyzer.algebraic_connectivity();
|
||||
assert!(lambda_2 > 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fiedler_vector() {
|
||||
let g = create_path_graph(6);
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
let fiedler = analyzer.fiedler_vector();
|
||||
assert!(fiedler.is_some());
|
||||
|
||||
let v = fiedler.unwrap();
|
||||
assert_eq!(v.len(), 6);
|
||||
|
||||
// Fiedler vector should be approximately monotonic for path graph
|
||||
// (either increasing or decreasing)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bottleneck_detection() {
|
||||
// Create a barbell graph (two cliques connected by a single edge)
|
||||
let mut g = Graph::new(8);
|
||||
|
||||
// First clique (0, 1, 2, 3)
|
||||
for i in 0..4 {
|
||||
for j in i + 1..4 {
|
||||
g.add_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Second clique (4, 5, 6, 7)
|
||||
for i in 4..8 {
|
||||
for j in i + 1..8 {
|
||||
g.add_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Bridge
|
||||
g.add_edge(3, 4, 1.0);
|
||||
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
let bottlenecks = analyzer.detect_bottlenecks();
|
||||
assert!(!bottlenecks.is_empty());
|
||||
|
||||
// The bottleneck should include the bridge edge
|
||||
let bridge_found = bottlenecks.iter().any(|b| {
|
||||
b.crossing_edges.contains(&(3, 4))
|
||||
});
|
||||
assert!(bridge_found);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_cut_prediction() {
|
||||
let g = create_path_graph(10);
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
let prediction = analyzer.predict_min_cut();
|
||||
assert!(prediction.predicted_cut > 0.0);
|
||||
assert!(prediction.lower_bound <= prediction.upper_bound);
|
||||
assert!(prediction.confidence > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_effective_resistance() {
|
||||
let g = create_path_graph(5);
|
||||
let mut analyzer = SpectralAnalyzer::new(g);
|
||||
analyzer.compute_laplacian_spectrum();
|
||||
|
||||
// Effective resistance should increase with distance
|
||||
let r_01 = analyzer.effective_resistance(0, 1);
|
||||
let r_04 = analyzer.effective_resistance(0, 4);
|
||||
|
||||
assert!(r_01 < r_04);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_bipartite() {
|
||||
// Even cycle is bipartite
|
||||
let even_cycle = create_cycle_graph(6);
|
||||
let mut analyzer_even = SpectralAnalyzer::new(even_cycle);
|
||||
analyzer_even.compute_laplacian_spectrum();
|
||||
|
||||
// Odd cycle is not bipartite
|
||||
let odd_cycle = create_cycle_graph(5);
|
||||
let mut analyzer_odd = SpectralAnalyzer::new(odd_cycle);
|
||||
analyzer_odd.compute_laplacian_spectrum();
|
||||
|
||||
// Even cycle should be bipartite
|
||||
assert!(analyzer_even.is_bipartite());
|
||||
|
||||
// Odd cycle should not be bipartite
|
||||
assert!(!analyzer_odd.is_bipartite());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user