use crate::algorithms::properties::degree::DegreeMode;
use crate::core::{Graph, IgraphError, IgraphResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LaplacianNormalization {
Unnormalized,
Symmetric,
Left,
Right,
}
pub fn get_laplacian(
graph: &Graph,
mode: DegreeMode,
normalization: LaplacianNormalization,
weights: Option<&[f64]>,
) -> IgraphResult<Vec<Vec<f64>>> {
let n = graph.vcount() as usize;
let ecount = graph.ecount();
if let Some(w) = weights {
if w.len() != ecount {
return Err(IgraphError::InvalidArgument(format!(
"get_laplacian: weight vector length ({}) does not match edge count ({ecount})",
w.len()
)));
}
}
let directed = graph.is_directed();
let mode_eff = if directed { mode } else { DegreeMode::All };
let treat_as_undirected = !directed || mode == DegreeMode::All;
let mut degree = vec![0.0_f64; n];
for eid in 0..ecount {
#[allow(clippy::cast_possible_truncation)]
let (from, to) = graph.edge(eid as u32)?;
let w = weights.map_or(1.0, |ws| ws[eid]);
match mode_eff {
DegreeMode::Out => {
degree[from as usize] += w;
if !directed && from != to {
degree[to as usize] += w;
}
}
DegreeMode::In => {
degree[to as usize] += w;
if !directed && from != to {
degree[from as usize] += w;
}
}
DegreeMode::All => {
degree[from as usize] += w;
if from == to {
degree[from as usize] += w;
} else {
degree[to as usize] += w;
}
}
}
}
let mut res = vec![vec![0.0_f64; n]; n];
let mut norm_factor = vec![0.0_f64; n];
for i in 0..n {
match normalization {
LaplacianNormalization::Unnormalized => {
res[i][i] = degree[i];
}
LaplacianNormalization::Symmetric => {
if degree[i] > 0.0 {
res[i][i] = 1.0;
norm_factor[i] = 1.0 / degree[i].sqrt();
}
}
LaplacianNormalization::Left | LaplacianNormalization::Right => {
if degree[i] > 0.0 {
res[i][i] = 1.0;
norm_factor[i] = 1.0 / degree[i];
}
}
}
}
for eid in 0..ecount {
#[allow(clippy::cast_possible_truncation)]
let (from, to) = graph.edge(eid as u32)?;
let w = weights.map_or(1.0, |ws| ws[eid]);
let f = from as usize;
let t = to as usize;
match normalization {
LaplacianNormalization::Unnormalized => {
res[f][t] -= w;
if treat_as_undirected {
res[t][f] -= w;
}
}
LaplacianNormalization::Symmetric => {
let nf = norm_factor[f] * norm_factor[t];
let wn = w * nf;
res[f][t] -= wn;
if treat_as_undirected {
res[t][f] -= wn;
}
}
LaplacianNormalization::Left => {
res[f][t] -= w * norm_factor[f];
if treat_as_undirected {
res[t][f] -= w * norm_factor[t];
}
}
LaplacianNormalization::Right => {
res[f][t] -= w * norm_factor[t];
if treat_as_undirected {
res[t][f] -= w * norm_factor[f];
}
}
}
}
Ok(res)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-10
}
#[test]
fn empty_graph() {
let g = Graph::with_vertices(0);
let lap = get_laplacian(
&g,
DegreeMode::All,
LaplacianNormalization::Unnormalized,
None,
)
.unwrap();
assert!(lap.is_empty());
}
#[test]
fn single_vertex() {
let g = Graph::with_vertices(1);
let lap = get_laplacian(
&g,
DegreeMode::All,
LaplacianNormalization::Unnormalized,
None,
)
.unwrap();
assert_eq!(lap.len(), 1);
assert!(approx_eq(lap[0][0], 0.0));
}
#[test]
fn path_3_unnormalized() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let lap = get_laplacian(
&g,
DegreeMode::All,
LaplacianNormalization::Unnormalized,
None,
)
.unwrap();
assert!(approx_eq(lap[0][0], 1.0));
assert!(approx_eq(lap[0][1], -1.0));
assert!(approx_eq(lap[0][2], 0.0));
assert!(approx_eq(lap[1][0], -1.0));
assert!(approx_eq(lap[1][1], 2.0));
assert!(approx_eq(lap[1][2], -1.0));
assert!(approx_eq(lap[2][0], 0.0));
assert!(approx_eq(lap[2][1], -1.0));
assert!(approx_eq(lap[2][2], 1.0));
}
#[test]
fn row_sum_zero_unnormalized() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 3).unwrap();
g.add_edge(2, 3).unwrap();
let lap = get_laplacian(
&g,
DegreeMode::All,
LaplacianNormalization::Unnormalized,
None,
)
.unwrap();
for row in &lap {
let sum: f64 = row.iter().sum();
assert!(approx_eq(sum, 0.0));
}
}
#[test]
fn symmetric_normalization() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let lap =
get_laplacian(&g, DegreeMode::All, LaplacianNormalization::Symmetric, None).unwrap();
assert!(approx_eq(lap[0][0], 1.0));
assert!(approx_eq(lap[1][1], 1.0));
assert!(approx_eq(lap[2][2], 1.0));
assert!(approx_eq(lap[0][1], -1.0 / 2.0_f64.sqrt()));
}
#[test]
fn left_normalization() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let lap = get_laplacian(&g, DegreeMode::All, LaplacianNormalization::Left, None).unwrap();
for row in &lap {
let sum: f64 = row.iter().sum();
assert!(approx_eq(sum, 0.0));
}
assert!(approx_eq(lap[0][0], 1.0));
assert!(approx_eq(lap[1][1], 1.0));
}
#[test]
fn weighted() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let weights = vec![2.0, 3.0];
let lap = get_laplacian(
&g,
DegreeMode::All,
LaplacianNormalization::Unnormalized,
Some(&weights),
)
.unwrap();
assert!(approx_eq(lap[0][0], 2.0));
assert!(approx_eq(lap[1][1], 5.0));
assert!(approx_eq(lap[2][2], 3.0));
assert!(approx_eq(lap[0][1], -2.0));
assert!(approx_eq(lap[1][2], -3.0));
}
#[test]
fn directed_out_mode() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 2).unwrap();
let lap = get_laplacian(
&g,
DegreeMode::Out,
LaplacianNormalization::Unnormalized,
None,
)
.unwrap();
assert!(approx_eq(lap[0][0], 2.0));
assert!(approx_eq(lap[1][1], 1.0));
assert!(approx_eq(lap[2][2], 0.0));
assert!(approx_eq(lap[0][1], -1.0));
assert!(approx_eq(lap[0][2], -1.0));
assert!(approx_eq(lap[1][0], 0.0)); }
#[test]
fn directed_in_mode() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 2).unwrap();
let lap = get_laplacian(
&g,
DegreeMode::In,
LaplacianNormalization::Unnormalized,
None,
)
.unwrap();
assert!(approx_eq(lap[0][0], 0.0));
assert!(approx_eq(lap[1][1], 1.0));
assert!(approx_eq(lap[2][2], 2.0));
}
#[test]
fn self_loop_undirected() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
let lap = get_laplacian(
&g,
DegreeMode::All,
LaplacianNormalization::Unnormalized,
None,
)
.unwrap();
assert!(approx_eq(lap[0][0], 1.0));
assert!(approx_eq(lap[1][1], 1.0));
assert!(approx_eq(lap[0][1], -1.0));
}
#[test]
fn weight_mismatch() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let result = get_laplacian(
&g,
DegreeMode::All,
LaplacianNormalization::Unnormalized,
Some(&[1.0, 2.0]),
);
assert!(result.is_err());
}
#[test]
fn isolated_vertex_symmetric() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
let lap =
get_laplacian(&g, DegreeMode::All, LaplacianNormalization::Symmetric, None).unwrap();
assert!(approx_eq(lap[2][2], 0.0));
}
}