diff --git a/examples/centrality.rs b/examples/centrality.rs index 381fe77..35b1054 100644 --- a/examples/centrality.rs +++ b/examples/centrality.rs @@ -166,6 +166,52 @@ fn katz_centrality_impl_example() { println!("{:.5?}", centrality); // [0.23167, 0.71650, 0.65800] } +fn closeness_centrality_example() { + use graphina::core::types::Graph; + + use graphina::centrality::algorithms::closeness_centrality; + + let mut graph = Graph::new(); + let ids = (0..5).map(|i| graph.add_node(i)).collect::>(); + let edges = [(0, 1, 1.0), (0, 2, 1.0), (1, 3, 1.0)]; + for (s, d, w) in edges { + graph.add_edge(ids[s], ids[d], w); + } + + let centrality = closeness_centrality(&graph, false).unwrap(); + println!("{:.5?}", centrality); // [0.75000, 0.75000, 0.50000, 0.50000, 0.00000] +} + +fn closeness_centrality_impl_example() { + use graphina::core::types::Graph; + + use graphina::centrality::algorithms::closeness_centrality_impl; + + let mut graph: Graph = Graph::new(); + + let ids = (0..5).map(|i| graph.add_node(i)).collect::>(); + + let edges = [ + (0, 1, ("friend".to_string(), 0.9)), + (0, 2, ("family".to_string(), 0.8)), + (1, 3, ("friend".to_string(), 0.7)), + (2, 4, ("enemy".to_string(), 0.1)), + ]; + for (s, d, w) in edges { + graph.add_edge(ids[s], ids[d], w); + } + + let eval_cost = |(s, f): &(String, f64)| match s.as_str() { + "friend" => Some(1.0 / *f / 2.0), + "family" => Some(1.0 / *f / 4.0), + "enemy" => None, + _ => Some(1.0 / *f), + }; + + let centrality = closeness_centrality_impl(&graph, eval_cost, true).unwrap(); + println!("{:.5?}", centrality); // [1.05244, 1.05244, 0.81436, 0.63088, 0.00000] +} + macro_rules! run_examples { ($($func:ident),* $(,)?) => { $( @@ -189,6 +235,9 @@ fn main() { // katz centrality katz_centrality_example, katz_centrality_numpy_example, - katz_centrality_impl_example + katz_centrality_impl_example, + // closeness centrality + closeness_centrality_example, + closeness_centrality_impl_example, ); } diff --git a/examples/path_dijkstra.rs b/examples/path_dijkstra.rs new file mode 100644 index 0000000..0ffe83b --- /dev/null +++ b/examples/path_dijkstra.rs @@ -0,0 +1,91 @@ +fn line() { + use graphina::core::types::Graph; + + use graphina::core::paths::dijkstra_path_f64; + + let mut graph = Graph::new(); + let ids = (0..5).map(|i| graph.add_node(i)).collect::>(); + let edges = [(0, 1, 1.0), (1, 2, 1.0), (2, 3, 2.0), (3, 4, 1.0)]; + for (s, d, w) in edges { + graph.add_edge(ids[s], ids[d], w); + } + + let (cost, trace) = dijkstra_path_f64(&graph, ids[0], None).unwrap(); + + println!("cost : {:?}", cost); + println!("trace: {:?}", trace); + // cost : [Some(0.0), Some(1.0), Some(2.0), Some(4.0), Some(5.0)] + // trace: [None, Some(NodeId(NodeIndex(0))), Some(NodeId(NodeIndex(1))), Some(NodeId(NodeIndex(2))), Some(NodeId(NodeIndex(3)))] +} + +fn flight() { + use graphina::core::types::Digraph; + + use graphina::core::paths::dijkstra_path_impl; + + let mut graph: Digraph = Digraph::new(); + // ^^^^^^^^^^^^^ + // L arbitrary type as edge + + let cities = ["ATL", "PEK", "LHR", "HND", "CDG", "FRA", "HKG"]; + + let ids = cities + .iter() + .map(|s| graph.add_node(s.to_string())) + .collect::>(); + + let edges = [ + // + ("ATL", "PEK", (900.0, "boeing")), + ("ATL", "LHR", (500.0, "airbus")), + ("ATL", "HND", (700.0, "airbus")), + // + ("PEK", "LHR", (800.0, "boeing")), + ("PEK", "HND", (100.0, "airbus")), + ("PEK", "HKG", (100.0, "airbus")), + // + ("LHR", "CDG", (100.0, "airbus")), + ("LHR", "FRA", (200.0, "boeing")), + ("LHR", "HND", (600.0, "airbus")), + // + ("HND", "ATL", (700.0, "airbus")), + ("HND", "FRA", (600.0, "airbus")), + ("HND", "HKG", (100.0, "airbus")), + // + ]; + + for (s, d, w) in edges { + let depart = cities.iter().position(|city| s == *city).unwrap(); + let destin = cities.iter().position(|city| d == *city).unwrap(); + graph.add_edge(ids[depart], ids[destin], (w.0, w.1.to_string())); + } + + // function for evaluating possible cost for the edge + // Some(f64) for cost + // None for impassable + let eval_cost = |(price, manufactuer): &(f64, String)| match manufactuer.as_str() { + "boeing" => None, // avoid boeing plane + _ => Some(*price), // return price as the cost + }; + + let (cost, trace) = dijkstra_path_impl(&graph, ids[0], Some(1000.0), eval_cost).unwrap(); + + println!("cost : {:?}", cost); + println!("trace: {:?}", trace); + // cost : [Some(0.0), None, Some(500.0), Some(700.0), Some(600.0), None, Some(800.0)] + // trace: [None, None, Some(NodeId(NodeIndex(0))), Some(NodeId(NodeIndex(0))), Some(NodeId(NodeIndex(2))), None, Some(NodeId(NodeIndex(3)))] +} + +macro_rules! run_examples { + ($($func:ident),* $(,)?) => { + $( + println!("<{}>", stringify!($func)); + $func(); + println!(); + )* + }; +} + +fn main() { + run_examples!(line, flight); +} diff --git a/src/centrality/algorithms.rs b/src/centrality/algorithms.rs index d3051ba..41dd55a 100644 --- a/src/centrality/algorithms.rs +++ b/src/centrality/algorithms.rs @@ -16,9 +16,10 @@ use petgraph::graph::NodeIndex; use crate::core::exceptions::GraphinaException; -use crate::core::paths::dijkstra; -use crate::core::types::{BaseGraph, GraphConstructor, NodeId}; +use crate::core::paths::{dijkstra, dijkstra_path_impl}; +use crate::core::types::{BaseGraph, GraphConstructor, GraphinaGraph, NodeId}; use std::collections::{HashMap, VecDeque}; +use std::fmt::Debug; // // ----------------------------- @@ -54,19 +55,18 @@ pub fn degree_centrality(graph: &BaseGraph) -> Vec where W: Copy, Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { - if !>::is_directed() { + if !graph.is_directed() { return out_degree_centrality(graph); } let n = graph.node_count(); - let mut degree = vec![0; n]; - for (node, _) in graph.nodes() { - degree[node.index()] += graph.neighbors(node).count(); - } - for (_u, v, _w) in graph.edges() { - degree[v.index()] += 1; + let mut cent = vec![0.0; n]; + for (src, dst, _) in graph.edges() { + cent[src.index()] += 1.0; + cent[dst.index()] += 1.0; } - degree.into_iter().map(|d| d as f64).collect() + cent } /// In–degree centrality. @@ -97,14 +97,12 @@ pub fn in_degree_centrality(graph: &BaseGraph) -> Vec where W: Copy, Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { - if !>::is_directed() { - return out_degree_centrality(graph); - } let n = graph.node_count(); let mut cent = vec![0.0; n]; - for (_u, v, _w) in graph.edges() { - cent[v.index()] += 1.0; + for (_, dst, _) in graph.edges() { + cent[dst.index()] += 1.0; } cent } @@ -137,11 +135,12 @@ pub fn out_degree_centrality(graph: &BaseGraph) -> Vec where W: Copy, Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { let n = graph.node_count(); let mut cent = vec![0.0; n]; - for (u, _) in graph.nodes() { - cent[u.index()] = graph.neighbors(u).count() as f64; + for (src, _, _) in graph.edges() { + cent[src.index()] += 1.0; } cent } @@ -197,17 +196,15 @@ pub fn eigenvector_centrality_impl( ) -> Vec where Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { let n = graph.node_count(); let mut centrality = vec![1.0; n]; let mut next = centrality.clone(); for _ in 0..max_iter { - for (src, dst, w) in graph.edges() { + for (src, dst, w) in graph.flow_edges() { let w = eval_weight(w); next[dst.index()] += w * centrality[src.index()]; - if !>::is_directed() { - next[src.index()] += w * centrality[dst.index()]; - } } let norm = next.iter().map(|x| x * x).sum::().sqrt(); if norm > 0.0 { @@ -267,6 +264,7 @@ pub fn eigenvector_centrality( ) -> Vec where Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { let eval_weight = if weighted { |f: &f64| *f @@ -312,6 +310,7 @@ pub fn eigenvector_centrality_numpy( ) -> Vec where Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { let eval_weight = if weighted { |f: &f64| *f @@ -395,6 +394,7 @@ pub fn katz_centrality_impl( ) -> Vec where Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { let n = graph.node_count(); let betas = (0..n) @@ -408,12 +408,9 @@ where let mut next = vec![0.0; n]; for _ in 0..max_iter { - for (src, dst, w) in graph.edges() { + for (src, dst, w) in graph.flow_edges() { let w = eval_weight(w); next[dst.index()] += w * centrality[src.index()]; - if !>::is_directed() { - next[src.index()] += w * centrality[dst.index()]; - } } for (i, n) in next.iter_mut().enumerate() { @@ -494,7 +491,8 @@ pub fn katz_centrality( normalized: bool, ) -> Vec where - Ty: crate::core::types::GraphConstructor, + Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { let alpha = |_a: &A| alpha; let beta = |_a: &A| beta; @@ -560,7 +558,8 @@ pub fn katz_centrality_numpy( normalized: bool, ) -> Vec where - Ty: crate::core::types::GraphConstructor, + Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { let alpha = |_a: &A| alpha; let beta = |_a: &A| beta; @@ -579,25 +578,124 @@ where // /// Compute closeness centrality using Dijkstra’s algorithm. +/// /// Closeness = (n - 1) / (sum of shortest-path distances). -pub fn closeness_centrality( - graph: &BaseGraph, Ty>, +/// +/// where n is number of reachable nodes. +/// +/// # Arguments +/// +/// * `graph`: the targeted graph. +/// * `eval_cost`: callback to evaluate the cost of edges in the graph, returning +/// - `Some(f64)` for cost +/// - `None` for impassable +/// * `wf_improved`: whether or not to scale the result by reachability ratio. +/// +/// # Returns +/// +/// a vector of `f64` representing closeness centralities of each node in the graph. +/// +/// # Example +/// ```rust +/// use graphina::core::types::Graph; +/// +/// use graphina::centrality::algorithms::closeness_centrality_impl; +/// +/// let mut graph: Graph = Graph::new(); +/// +/// let ids = (0..5).map(|i| graph.add_node(i)).collect::>(); +/// +/// let edges = [ +/// (0, 1, ("friend".to_string(), 0.9)), +/// (0, 2, ("family".to_string(), 0.8)), +/// (1, 3, ("friend".to_string(), 0.7)), +/// (2, 4, ("enemy".to_string(), 0.1)), +/// ]; +/// for (s, d, w) in edges { +/// graph.add_edge(ids[s], ids[d], w); +/// } +/// +/// let eval_cost = |(s, f): &(String, f64)| match s.as_str() { +/// "friend" => Some(1.0 / *f / 2.0), +/// "family" => Some(1.0 / *f / 4.0), +/// "enemy" => None, +/// _ => Some(1.0 / *f), +/// }; +/// +/// let centrality = closeness_centrality_impl(&graph, eval_cost, true).unwrap(); +/// println!("{:.5?}", centrality); // [1.05244, 1.05244, 0.81436, 0.63088, 0.00000] +/// ``` +pub fn closeness_centrality_impl( + graph: &BaseGraph, + eval_cost: impl Fn(&W) -> Option, + wf_improved: bool, ) -> Result, GraphinaException> where - Ty: GraphConstructor>, + A: Debug, + W: Debug, + Ty: GraphConstructor, + BaseGraph: GraphinaGraph, { let n = graph.node_count(); let mut closeness = vec![0.0; n]; for (node, _) in graph.nodes() { - let distances = dijkstra(graph, node)?; - let sum: f64 = distances.iter().filter_map(|d| d.map(|od| od.0)).sum(); + let (distances, _) = dijkstra_path_impl(graph, node, None, &eval_cost)?; + let reachable = distances.iter().filter(|d| d.is_some()).count() as f64; + let sum: f64 = distances.iter().filter_map(|d| d.to_owned()).sum(); if sum > 0.0 { - closeness[node.index()] = (n as f64 - 1.0) / sum; + closeness[node.index()] = (reachable - 1.0) / sum; + } + if wf_improved { + closeness[node.index()] *= (reachable - 1.0) / (n as f64 - 1.0); } } Ok(closeness) } +/// Compute closeness centrality using Dijkstra’s algorithm. +/// +/// Closeness = (n - 1) / (sum of shortest-path distances). +/// +/// where n is number of reachable nodes. +/// +/// # Arguments +/// +/// * `graph`: the targeted graph. +/// * `wf_improved`: whether or not to scale the result by reachability ratio. +/// +/// # Returns +/// +/// a vector of `f64` representing closeness centralities of each node in the graph. +/// +/// # Example +/// ```rust +/// use graphina::core::types::Graph; +/// +/// use graphina::centrality::algorithms::closeness_centrality; +/// +/// let mut graph = Graph::new(); +/// let ids = (0..5).map(|i| graph.add_node(i)).collect::>(); +/// let edges = [(0, 1, 1.0), (0, 2, 1.0), (1, 3, 1.0)]; +/// for (s, d, w) in edges { +/// graph.add_edge(ids[s], ids[d], w); +/// } +/// +/// let centrality = closeness_centrality(&graph, false).unwrap(); +/// println!("{:.5?}", centrality); // [0.75000, 0.75000, 0.50000, 0.50000, 0.00000] +/// ``` +pub fn closeness_centrality( + graph: &BaseGraph, + wf_improved: bool, +) -> Result, GraphinaException> +where + A: Debug, + Ty: GraphConstructor, + BaseGraph: GraphinaGraph, +{ + let eval_cost = |f: &f64| Some(*f); + closeness_centrality_impl(graph, eval_cost, wf_improved) +} + // // ----------------------------- // PageRank diff --git a/src/core/paths.rs b/src/core/paths.rs index baad388..9b360df 100644 --- a/src/core/paths.rs +++ b/src/core/paths.rs @@ -32,12 +32,16 @@ if a negative weight is encountered. Users should handle these `Result` types ac */ use crate::core::exceptions::GraphinaException; -use crate::core::types::{BaseGraph, GraphConstructor, NodeId}; +use crate::core::types::{BaseGraph, GraphConstructor, GraphinaGraph, NodeId}; use std::cmp::Reverse; use std::collections::{BinaryHeap, HashSet}; use std::fmt::Debug; use std::ops::{Add, Sub}; +use ordered_float::NotNan; + +pub type PathFindResult = (Vec>, Vec>); + /// Returns an iterator over outgoing edges from a given node as `(target, weight)`. fn outgoing_edges( graph: &BaseGraph, @@ -53,10 +57,203 @@ where .map(|(_src, tgt, w)| (tgt, *w)) } -/// ============================ -/// Dijkstra’s Algorithm -/// ============================ +// ============================ +// Dijkstra’s Algorithm +// ============================ +// + +/// Generic, Full implementationof Dijkstra's algorithm for finding shortest paths in a graph +/// with non-negative weights. +/// +/// # Arguments +/// +/// * `graph`: the target graph. +/// * `source`: the source of path finding. +/// * `cutoff`: the maximum total cost before stopping search. +/// * `eval_cost`: callback to evaluate the cost of possible edges in the graph, returning +/// - `Some(f64)` for cost, +/// - `None` for not passable edge. +/// +/// # Returns +/// +/// - `Vec>` in which `None` for unreachable, and `Some(cost)` for the total path cost. +/// - `Vec>` in which `None` for no traceback (i.e. is source or unreachable), +/// and `Some(NodeId)` for the previous node visited in the path. +/// +/// # Error +/// +/// return error, if encounter negative cost, or encounter `NaN` weight. +/// +/// # Example +/// ```rust +/// use graphina::core::types::Digraph; +/// +/// use graphina::core::paths::dijkstra_path_impl; +/// +/// let mut graph: Digraph = Digraph::new(); +/// // ^^^^^^^^^^^^^ +/// // L arbitrary type as edge +/// +/// let cities = ["ATL", "PEK", "LHR", "HND", "CDG", "FRA", "HKG"]; +/// +/// let ids = cities +/// .iter() +/// .map(|s| graph.add_node(s.to_string())) +/// .collect::>(); +/// +/// let edges = [ +/// // +/// ("ATL", "PEK", (900.0, "boeing")), +/// ("ATL", "LHR", (500.0, "airbus")), +/// ("ATL", "HND", (700.0, "airbus")), +/// // +/// ("PEK", "LHR", (800.0, "boeing")), +/// ("PEK", "HND", (100.0, "airbus")), +/// ("PEK", "HKG", (100.0, "airbus")), +/// // +/// ("LHR", "CDG", (100.0, "airbus")), +/// ("LHR", "FRA", (200.0, "boeing")), +/// ("LHR", "HND", (600.0, "airbus")), +/// // +/// ("HND", "ATL", (700.0, "airbus")), +/// ("HND", "FRA", (600.0, "airbus")), +/// ("HND", "HKG", (100.0, "airbus")), +/// // +/// ]; +/// +/// for (s, d, w) in edges { +/// let depart = cities.iter().position(|city| s == *city).unwrap(); +/// let destin = cities.iter().position(|city| d == *city).unwrap(); +/// graph.add_edge(ids[depart], ids[destin], (w.0, w.1.to_string())); +/// } +/// +/// // function for evaluating possible cost for the edge +/// // Some(f64) for cost +/// // None for impassable +/// let eval_cost = |(price, manufactuer): &(f64, String)| match manufactuer.as_str() { +/// "boeing" => None, // avoid boeing plane +/// _ => Some(*price), // return price as the cost +/// }; +/// +/// let (cost, trace) = dijkstra_path_impl(&graph, ids[0], Some(1000.0), eval_cost).unwrap(); +/// +/// println!("cost : {:?}", cost); +/// println!("trace: {:?}", trace); +/// // cost : [Some(0.0), None, Some(500.0), Some(700.0), Some(600.0), None, Some(800.0)] +/// // trace: [None, None, Some(NodeId(NodeIndex(0))), Some(NodeId(NodeIndex(0))), Some(NodeId(NodeIndex(2))), None, Some(NodeId(NodeIndex(3)))] +/// ``` +pub fn dijkstra_path_impl( + graph: &BaseGraph, + source: NodeId, + cutoff: Option, + eval_cost: impl Fn(&W) -> Option, +) -> Result +where + W: Debug, + A: Debug, + Ty: GraphConstructor, + NodeId: Ord, + BaseGraph: GraphinaGraph, +{ + let n = graph.node_count(); + let mut dist = vec![None; n]; + let mut trace = vec![None; n]; + let mut heap = BinaryHeap::new(); + + dist[source.index()] = Some(0.0); + heap.push(Reverse((NotNan::new(0.0).unwrap(), source))); + + while let Some(Reverse((d, u))) = heap.pop() { + if let Some(current) = dist[u.index()] { + if *d > current { + continue; + } + } + for (v, edge) in graph.outgoing_edges(u) { + let Some(w) = eval_cost(edge) else { + continue; + }; + if w.is_sign_negative() { + return Err(GraphinaException::new(&format!( + "Dijkstra requires nonnegative costs, but found cost: {:?}, src: {:?}, dst: {:?}, edge: {:?}", + w, u, v, edge + ))); + } + let Ok(w) = NotNan::new(w) else { + return Err(GraphinaException::new(&format!( + "Dijkstra requires not NaN costs, but found cost: {:?}, src: {:?}, dst: {:?}, edge: {:?}", + w, u, v, edge + ))); + }; + let next = d + w; + if let Some(cutoff) = cutoff { + if *next > cutoff { + continue; + } + } + if dist[v.index()].is_none() || Some(*next) < dist[v.index()] { + dist[v.index()] = Some(*next); + trace[v.index()] = Some(u); + heap.push(Reverse((next, v))); + } + } + } + Ok((dist, trace)) +} + +/// Full implementationof Dijkstra's algorithm for finding shortest paths in a graph +/// for graph with edge type `f64` +/// with non-negative weights. /// +/// # Arguments +/// +/// * `graph`: the target graph. +/// * `source`: the source of path finding. +/// * `cutoff`: the maximum total cost before stopping search. +/// +/// # Returns +/// +/// - `Vec>` in which `None` for unreachable, and `Some(cost)` for the total path cost. +/// - `Vec>` in which `None` for no traceback (i.e. is source or unreachable), +/// and `Some(NodeId)` for the previous node visited in the path. +/// +/// # Error +/// +/// return error, if encounter negative cost, or encounter `NaN` weight. +/// +/// # Example +/// ```rust +/// use graphina::core::types::Graph; +/// +/// use graphina::core::paths::dijkstra_path_f64; +/// +/// let mut graph = Graph::new(); +/// let ids = (0..5).map(|i| graph.add_node(i)).collect::>(); +/// let edges = [(0, 1, 1.0), (1, 2, 1.0), (2, 3, 2.0), (3, 4, 1.0)]; +/// for (s, d, w) in edges { +/// graph.add_edge(ids[s], ids[d], w); +/// } +/// +/// let (cost, trace) = dijkstra_path_f64(&graph, ids[0], None).unwrap(); +/// +/// println!("cost : {:?}", cost); +/// println!("trace: {:?}", trace); +/// // cost : [Some(0.0), Some(1.0), Some(2.0), Some(4.0), Some(5.0)] +/// // trace: [None, Some(NodeId(NodeIndex(0))), Some(NodeId(NodeIndex(1))), Some(NodeId(NodeIndex(2))), Some(NodeId(NodeIndex(3)))] +/// ``` +pub fn dijkstra_path_f64( + graph: &BaseGraph, + source: NodeId, + cutoff: Option, +) -> Result +where + A: Debug, + Ty: GraphConstructor, + BaseGraph: GraphinaGraph, +{ + dijkstra_path_impl(graph, source, cutoff, |f| Some(*f)) +} + /// Computes single‑source shortest paths for graphs with nonnegative weights. /// /// # Returns diff --git a/src/core/types.rs b/src/core/types.rs index f4caa79..657777c 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -155,6 +155,10 @@ impl + EdgeType> BaseGraph { } } + pub fn is_directed(&self) -> bool { + self.inner.is_directed() + } + /// Adds a node with the specified attribute to the graph. /// /// # Example @@ -284,6 +288,12 @@ impl + EdgeType> BaseGraph { }) } + pub fn outgoing_edges(&self, source: NodeId) -> impl Iterator + '_ { + self.inner + .edges(source.0) + .map(|edge| (NodeId(edge.target()), edge.weight())) + } + /// Returns a reference to the inner petgraph instance. fn inner(&self) -> &PetGraph { &self.inner @@ -313,6 +323,33 @@ impl + EdgeType> BaseGraph { } } +/// Extra util +pub trait GraphinaGraph { + /// return edges in form or `(src: NodeId, dst: NodeId, attr: &W)`, + /// where the backward edge is included for undirected graph. + fn flow_edges<'a>(&'a self) -> impl Iterator + 'a + where + W: 'a; +} + +impl GraphinaGraph for BaseGraph { + fn flow_edges<'a>(&'a self) -> impl Iterator + 'a + where + W: 'a, + { + self.edges() + .flat_map(|(src, dst, w)| [(src, dst, w), (dst, src, w)].into_iter()) + } +} +impl GraphinaGraph for BaseGraph { + fn flow_edges<'a>(&'a self) -> impl Iterator + 'a + where + W: 'a, + { + self.edges() + } +} + /// Dense matrix API using owned values. /// /// The adjacency matrix is built using a contiguous mapping of the current nodes. diff --git a/tests/test_centrality_algorithms.rs b/tests/test_centrality_algorithms.rs index 5155b18..f98c24c 100644 --- a/tests/test_centrality_algorithms.rs +++ b/tests/test_centrality_algorithms.rs @@ -59,8 +59,8 @@ fn test_degree_centrality() { #[test] fn test_closeness_centrality() { - let graph = build_test_graph_ordered(); - let closeness = closeness_centrality(&graph).unwrap(); + let graph = build_test_graph_f64(); + let closeness = closeness_centrality(&graph, false).unwrap(); // In our strongly connected graph with all edges = 1.0, // each node's distances: two neighbors at 1 and one at 2 -> sum = 4. // Closeness = (n-1)/sum = 3/4 = 0.75.