use super::{num_vertices, validate_graph};
use crate::csr_array::CsrArray;
use crate::error::{SparseError, SparseResult};
use crate::sparray::SparseArray;
use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::{Float, SparseElement};
use std::fmt::Debug;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LaplacianType {
Standard,
Normalized,
RandomWalk,
}
impl LaplacianType {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> SparseResult<Self> {
match s.to_lowercase().as_str() {
"standard" | "unnormalized" => Ok(Self::Standard),
"normalized" | "symmetric" => Ok(Self::Normalized),
"random_walk" | "random-walk" | "randomwalk" => Ok(Self::RandomWalk),
_ => Err(SparseError::ValueError(format!(
"Unknown Laplacian type: {s}. Use 'standard', 'normalized', or 'random_walk'"
))),
}
}
}
#[allow(dead_code)]
pub fn laplacian<T, S>(
graph: &S,
normed: bool,
return_diag: bool,
use_outdegree: bool,
) -> SparseResult<(CsrArray<T>, Option<Array1<T>>)>
where
T: Float + SparseElement + Debug + Copy + 'static,
S: SparseArray<T>,
{
validate_graph(graph, true)?;
let laplaciantype = if normed {
LaplacianType::Normalized
} else {
LaplacianType::Standard
};
compute_laplacianmatrix(graph, laplaciantype, return_diag, use_outdegree)
}
#[allow(dead_code)]
pub fn compute_laplacianmatrix<T, S>(
graph: &S,
laplaciantype: LaplacianType,
return_diag: bool,
use_outdegree: bool,
) -> SparseResult<(CsrArray<T>, Option<Array1<T>>)>
where
T: Float + SparseElement + Debug + Copy + 'static,
S: SparseArray<T>,
{
let n = num_vertices(graph);
let degrees = if use_outdegree {
compute_out_degrees(graph)?
} else {
compute_in_degrees(graph)?
};
let (row_indices, col_indices, values) = graph.find();
match laplaciantype {
LaplacianType::Standard => compute_standard_laplacian(
row_indices.as_slice().expect("Operation failed"),
col_indices.as_slice().expect("Operation failed"),
values.as_slice().expect("Operation failed"),
°rees,
n,
),
LaplacianType::Normalized => compute_normalized_laplacian(
row_indices.as_slice().expect("Operation failed"),
col_indices.as_slice().expect("Operation failed"),
values.as_slice().expect("Operation failed"),
°rees,
n,
),
LaplacianType::RandomWalk => compute_random_walk_laplacian(
row_indices.as_slice().expect("Operation failed"),
col_indices.as_slice().expect("Operation failed"),
values.as_slice().expect("Operation failed"),
°rees,
n,
),
}
.map(|laplacian| {
let diag = if return_diag { Some(degrees) } else { None };
(laplacian, diag)
})
}
#[allow(dead_code)]
fn compute_out_degrees<T, S>(graph: &S) -> SparseResult<Array1<T>>
where
T: Float + SparseElement + Debug + Copy + 'static,
S: SparseArray<T>,
{
let n = num_vertices(graph);
let mut degrees = Array1::zeros(n);
let (row_indices, _, values) = graph.find();
for (i, &row) in row_indices.iter().enumerate() {
degrees[row] = degrees[row] + values[i];
}
Ok(degrees)
}
#[allow(dead_code)]
fn compute_in_degrees<T, S>(graph: &S) -> SparseResult<Array1<T>>
where
T: Float + SparseElement + Debug + Copy + 'static,
S: SparseArray<T>,
{
let n = num_vertices(graph);
let mut degrees = Array1::zeros(n);
let (_, col_indices, values) = graph.find();
for (i, &col) in col_indices.iter().enumerate() {
degrees[col] = degrees[col] + values[i];
}
Ok(degrees)
}
#[allow(dead_code)]
fn compute_standard_laplacian<T>(
row_indices: &[usize],
col_indices: &[usize],
values: &[T],
degrees: &Array1<T>,
n: usize,
) -> SparseResult<CsrArray<T>>
where
T: Float + SparseElement + Debug + Copy + 'static,
{
let mut laplacianrows = Vec::new();
let mut laplaciancols = Vec::new();
let mut laplacianvalues = Vec::new();
for (i, °ree) in degrees.iter().enumerate() {
if !SparseElement::is_zero(°ree) {
laplacianrows.push(i);
laplaciancols.push(i);
laplacianvalues.push(degree);
}
}
for (i, (&row, &col)) in row_indices.iter().zip(col_indices.iter()).enumerate() {
if row != col && !SparseElement::is_zero(&values[i]) {
laplacianrows.push(row);
laplaciancols.push(col);
laplacianvalues.push(-values[i]);
}
}
CsrArray::from_triplets(
&laplacianrows,
&laplaciancols,
&laplacianvalues,
(n, n),
false, )
}
#[allow(dead_code)]
fn compute_normalized_laplacian<T>(
row_indices: &[usize],
col_indices: &[usize],
values: &[T],
degrees: &Array1<T>,
n: usize,
) -> SparseResult<CsrArray<T>>
where
T: Float + SparseElement + Debug + Copy + 'static,
{
let mut sqrt_inv_degrees = Array1::zeros(n);
for (i, °ree) in degrees.iter().enumerate() {
if degree > T::sparse_zero() {
sqrt_inv_degrees[i] = T::sparse_one() / degree.sqrt();
}
}
let mut laplacianrows = Vec::new();
let mut laplaciancols = Vec::new();
let mut laplacianvalues = Vec::new();
for (i, °ree) in degrees.iter().enumerate() {
if degree > T::sparse_zero() {
laplacianrows.push(i);
laplaciancols.push(i);
laplacianvalues.push(T::sparse_one());
}
}
for (k, (&i, &j)) in row_indices.iter().zip(col_indices.iter()).enumerate() {
if i != j && !SparseElement::is_zero(&values[k]) {
let normalization = sqrt_inv_degrees[i] * sqrt_inv_degrees[j];
if !SparseElement::is_zero(&normalization) {
laplacianrows.push(i);
laplaciancols.push(j);
laplacianvalues.push(-values[k] * normalization);
}
}
}
CsrArray::from_triplets(
&laplacianrows,
&laplaciancols,
&laplacianvalues,
(n, n),
false, )
}
#[allow(dead_code)]
fn compute_random_walk_laplacian<T>(
row_indices: &[usize],
col_indices: &[usize],
values: &[T],
degrees: &Array1<T>,
n: usize,
) -> SparseResult<CsrArray<T>>
where
T: Float + SparseElement + Debug + Copy + 'static,
{
let mut inv_degrees = Array1::zeros(n);
for (i, °ree) in degrees.iter().enumerate() {
if degree > T::sparse_zero() {
inv_degrees[i] = T::sparse_one() / degree;
}
}
let mut laplacianrows = Vec::new();
let mut laplaciancols = Vec::new();
let mut laplacianvalues = Vec::new();
for (i, °ree) in degrees.iter().enumerate() {
if degree > T::sparse_zero() {
laplacianrows.push(i);
laplaciancols.push(i);
laplacianvalues.push(T::sparse_one());
}
}
for (k, (&i, &j)) in row_indices.iter().zip(col_indices.iter()).enumerate() {
if i != j && !SparseElement::is_zero(&values[k]) && inv_degrees[i] > T::sparse_zero() {
laplacianrows.push(i);
laplaciancols.push(j);
laplacianvalues.push(-values[k] * inv_degrees[i]);
}
}
CsrArray::from_triplets(
&laplacianrows,
&laplaciancols,
&laplacianvalues,
(n, n),
false, )
}
#[allow(dead_code)]
pub fn degree_matrix<T, S>(graph: &S, use_outdegree: bool) -> SparseResult<Array1<T>>
where
T: Float + SparseElement + Debug + Copy + 'static,
S: SparseArray<T>,
{
if use_outdegree {
compute_out_degrees(graph)
} else {
compute_in_degrees(graph)
}
}
#[allow(dead_code)]
pub fn algebraic_connectivity<T, S>(graph: &S, normalized: bool) -> SparseResult<T>
where
T: Float
+ SparseElement
+ Debug
+ Copy
+ 'static
+ std::iter::Sum
+ scirs2_core::simd_ops::SimdUnifiedOps
+ Send
+ Sync,
S: SparseArray<T>,
{
use crate::linalg::{lanczos, LanczosOptions};
use crate::sym_csr::SymCsrMatrix;
validate_graph(graph, false)?;
let (laplacian, _) = compute_laplacianmatrix(
graph,
if normalized {
LaplacianType::Normalized
} else {
LaplacianType::Standard
},
false,
true,
)?;
let (rows, cols) = laplacian.shape();
let (row_indices, col_indices, values) = laplacian.find();
let mut sym_indices = Vec::new();
let mut sym_data = Vec::new();
let mut sym_indptr = vec![0; rows + 1];
let mut nnz_count = 0;
#[allow(clippy::needless_range_loop)]
for i in 0..rows {
sym_indptr[i] = nnz_count;
for k in 0..row_indices.len() {
let row = row_indices[k];
let col = col_indices[k];
if row == i && col <= i {
sym_indices.push(col);
sym_data.push(values[k]);
nnz_count += 1;
}
}
}
sym_indptr[rows] = nnz_count;
let sym_laplacian = SymCsrMatrix::new(sym_data, sym_indices, sym_indptr, (rows, cols))?;
let options = LanczosOptions {
max_iter: 1000,
max_subspace_size: (rows / 4).clamp(10, 50), tol: 1e-12,
numeigenvalues: 3.min(rows), compute_eigenvectors: false, };
let eigen_result = lanczos(&sym_laplacian, &options, None)?;
if !eigen_result.converged {
return Err(SparseError::ValueError(
"Eigenvalue computation did not converge".to_string(),
));
}
let eigenvalues = eigen_result.eigenvalues;
if eigenvalues.len() < 2 {
return Err(SparseError::ValueError(
"Not enough eigenvalues computed to determine algebraic connectivity".to_string(),
));
}
let mut sorted_eigenvals: Vec<T> = eigenvalues.iter().copied().collect();
sorted_eigenvals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let algebraic_conn = sorted_eigenvals[1];
let smallest_eigenval = sorted_eigenvals[0];
let zero_threshold = T::from(1e-10).expect("Operation failed");
if smallest_eigenval.abs() > zero_threshold {
return Err(SparseError::ValueError(format!(
"Graph may not be connected: smallest eigenvalue is {:.2e}, expected ~0",
smallest_eigenval.to_f64().unwrap_or(0.0)
)));
}
Ok(algebraic_conn)
}
#[allow(dead_code)]
pub fn is_laplacian<T, S>(matrix: &S, tol: T) -> SparseResult<bool>
where
T: Float + SparseElement + Debug + Copy + 'static,
S: SparseArray<T>,
{
let (n, m) = matrix.shape();
if n != m {
return Ok(false);
}
let (row_indices, col_indices, values) = matrix.find();
let mut row_sums = vec![T::sparse_zero(); n];
for (i, (&row, &_col)) in row_indices.iter().zip(col_indices.iter()).enumerate() {
row_sums[row] = row_sums[row] + values[i];
}
for &sum in &row_sums {
if sum.abs() > tol {
return Ok(false);
}
}
for (i, (&row, &col)) in row_indices.iter().zip(col_indices.iter()).enumerate() {
if row != col && values[i] > tol {
return Ok(false);
}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::csr_array::CsrArray;
use approx::assert_relative_eq;
fn create_simple_graph() -> CsrArray<f64> {
let rows = vec![0, 0, 1, 1, 2, 2];
let cols = vec![1, 2, 0, 2, 0, 1];
let data = vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
CsrArray::from_triplets(&rows, &cols, &data, (3, 3), false).expect("Operation failed")
}
#[test]
fn test_compute_degrees() {
let graph = create_simple_graph();
let out_degrees = compute_out_degrees(&graph).expect("Operation failed");
let in_degrees = compute_in_degrees(&graph).expect("Operation failed");
assert_relative_eq!(out_degrees[0], 2.0); assert_relative_eq!(out_degrees[1], 2.0); assert_relative_eq!(out_degrees[2], 2.0);
for i in 0..3 {
assert_relative_eq!(out_degrees[i], in_degrees[i]);
}
}
#[test]
fn test_standard_laplacian() {
let graph = create_simple_graph();
let (laplacian, degrees) =
compute_laplacianmatrix(&graph, LaplacianType::Standard, true, true)
.expect("Operation failed");
let degrees = degrees.expect("Operation failed");
assert_relative_eq!(degrees[0], 2.0);
assert_relative_eq!(degrees[1], 2.0);
assert_relative_eq!(degrees[2], 2.0);
assert_relative_eq!(laplacian.get(0, 0), 2.0);
assert_relative_eq!(laplacian.get(1, 1), 2.0);
assert_relative_eq!(laplacian.get(2, 2), 2.0);
assert_relative_eq!(laplacian.get(0, 1), -1.0);
assert_relative_eq!(laplacian.get(0, 2), -1.0);
assert_relative_eq!(laplacian.get(1, 0), -1.0);
assert_relative_eq!(laplacian.get(1, 2), -1.0);
assert_relative_eq!(laplacian.get(2, 0), -1.0);
assert_relative_eq!(laplacian.get(2, 1), -1.0);
}
#[test]
fn test_normalized_laplacian() {
let graph = create_simple_graph();
let (laplacian, _) =
compute_laplacianmatrix(&graph, LaplacianType::Normalized, false, true)
.expect("Operation failed");
assert_relative_eq!(laplacian.get(0, 0), 1.0);
assert_relative_eq!(laplacian.get(1, 1), 1.0);
assert_relative_eq!(laplacian.get(2, 2), 1.0);
assert_relative_eq!(laplacian.get(0, 1), -0.5);
assert_relative_eq!(laplacian.get(1, 2), -0.5);
assert_relative_eq!(laplacian.get(2, 0), -0.5);
}
#[test]
fn test_random_walk_laplacian() {
let graph = create_simple_graph();
let (laplacian, _) =
compute_laplacianmatrix(&graph, LaplacianType::RandomWalk, false, true)
.expect("Operation failed");
assert_relative_eq!(laplacian.get(0, 0), 1.0);
assert_relative_eq!(laplacian.get(1, 1), 1.0);
assert_relative_eq!(laplacian.get(2, 2), 1.0);
assert_relative_eq!(laplacian.get(0, 1), -0.5);
assert_relative_eq!(laplacian.get(1, 2), -0.5);
assert_relative_eq!(laplacian.get(2, 0), -0.5);
}
#[test]
fn test_laplacianapi() {
let graph = create_simple_graph();
let (lap_std_, _) = laplacian(&graph, false, false, true).expect("Operation failed");
assert_relative_eq!(lap_std_.get(0, 0), 2.0);
let (lap_norm, degrees) = laplacian(&graph, true, true, true).expect("Operation failed");
assert_relative_eq!(lap_norm.get(0, 0), 1.0);
let degrees = degrees.expect("Operation failed");
assert_relative_eq!(degrees[0], 2.0);
}
#[test]
fn test_degree_matrix() {
let graph = create_simple_graph();
let degrees = degree_matrix(&graph, true).expect("Operation failed");
assert_relative_eq!(degrees[0], 2.0);
assert_relative_eq!(degrees[1], 2.0);
assert_relative_eq!(degrees[2], 2.0);
}
#[test]
fn test_laplacianrow_sums() {
let graph = create_simple_graph();
let (laplacian, _) = compute_laplacianmatrix(&graph, LaplacianType::Standard, false, true)
.expect("Operation failed");
for i in 0..3 {
let mut row_sum = 0.0;
for j in 0..3 {
row_sum += laplacian.get(i, j);
}
assert_relative_eq!(row_sum, 0.0, epsilon = 1e-10);
}
}
#[test]
fn test_is_laplacian() {
let graph = create_simple_graph();
let (laplacian, _) = compute_laplacianmatrix(&graph, LaplacianType::Standard, false, true)
.expect("Operation failed");
assert!(is_laplacian(&laplacian, 1e-10).expect("Operation failed"));
}
#[test]
fn test_isolated_vertex() {
let rows = vec![0, 1];
let cols = vec![1, 0];
let data = vec![1.0, 1.0];
let graph =
CsrArray::from_triplets(&rows, &cols, &data, (3, 3), false).expect("Operation failed");
let (laplacian, degrees) =
compute_laplacianmatrix(&graph, LaplacianType::Standard, true, true)
.expect("Operation failed");
let degrees = degrees.expect("Operation failed");
assert_relative_eq!(degrees[2], 0.0);
assert_relative_eq!(laplacian.get(2, 2), 0.0);
assert_relative_eq!(laplacian.get(2, 0), 0.0);
assert_relative_eq!(laplacian.get(2, 1), 0.0);
}
#[test]
fn test_directed_graph() {
let rows = vec![0, 1];
let cols = vec![1, 2];
let data = vec![1.0, 1.0];
let graph =
CsrArray::from_triplets(&rows, &cols, &data, (3, 3), false).expect("Operation failed");
let out_degrees = compute_out_degrees(&graph).expect("Operation failed");
assert_relative_eq!(out_degrees[0], 1.0);
assert_relative_eq!(out_degrees[1], 1.0);
assert_relative_eq!(out_degrees[2], 0.0);
let in_degrees = compute_in_degrees(&graph).expect("Operation failed");
assert_relative_eq!(in_degrees[0], 0.0);
assert_relative_eq!(in_degrees[1], 1.0);
assert_relative_eq!(in_degrees[2], 1.0);
}
}