diff --git a/python/python/raphtory/algorithms/__init__.pyi b/python/python/raphtory/algorithms/__init__.pyi index ae2892f399..051b17b1a2 100644 --- a/python/python/raphtory/algorithms/__init__.pyi +++ b/python/python/raphtory/algorithms/__init__.pyi @@ -703,15 +703,17 @@ def louvain( resolution: float = 1.0, weight_prop: str | None = None, tol: None | float = None, + modularity: Literal("configuration", "constant") = "configuration", ) -> NodeStateUsize: """ - Louvain algorithm for community detection + Louvain algorithm for community detection with configuration model Arguments: graph (GraphView): the graph view resolution (float): the resolution parameter for modularity. Defaults to 1.0. weight_prop (str | None): the edge property to use for weights (has to be float) tol (None | float): the floating point tolerance for deciding if improvements are significant (default: 1e-8) + modularity (Literal("configuration", "constant")): the modularity function to use. Default to "configuration". Returns: NodeStateUsize: Mapping of nodes to their community assignment diff --git a/python/python/raphtory/vectors/__init__.pyi b/python/python/raphtory/vectors/__init__.pyi index b0be7dd4a1..75e6b0e6d7 100644 --- a/python/python/raphtory/vectors/__init__.pyi +++ b/python/python/raphtory/vectors/__init__.pyi @@ -23,11 +23,11 @@ import networkx as nx # type: ignore import pyvis # type: ignore from raphtory.iterables import * -__all__ = ['VectorisedGraph', 'Document', 'Embedding', 'VectorSelection'] -class VectorisedGraph(object): - """VectorisedGraph object that contains embedded documents that correspond to graph entities.""" +__all__ = ["VectorisedGraph", "Document", "Embedding", "VectorSelection"] class VectorisedGraph(object): + """VectorisedGraph object that contains embedded documents that correspond to graph entities.""" + def edges_by_similarity( self, query: str | list, @@ -35,7 +35,7 @@ class VectorisedGraph(object): window: Optional[Tuple[int | str, int | str]] = None, ) -> VectorSelection: """ - Search the top similarity scoring edges according to matching a specified `query` with no more than `limit` edges in the result. + Perform a similarity search between each edge's associated document and a specified `query`. Returns a number of edges up to a specified `limit` ranked in descending order of similarity score. Args: query (str | list): The text or the embedding to score against. @@ -56,7 +56,7 @@ class VectorisedGraph(object): window: Optional[Tuple[int | str, int | str]] = None, ) -> VectorSelection: """ - Search the top similarity scoring entities according to matching a specified `query` with no more than `limit` entities in the result. + Perform a similarity search between each entity's associated document and a specified `query`. Returns a number of entities up to a specified `limit` ranked in descending order of similarity score. Args: query (str | list): The text or the embedding to score against. @@ -74,7 +74,7 @@ class VectorisedGraph(object): window: Optional[Tuple[int | str, int | str]] = None, ) -> VectorSelection: """ - Search the top similarity scoring nodes according to matching a specified `query` with no more than `limit` nodes in the result. + Perform a similarity search between each node's associated document and a specified `query`. Returns a number of nodes up to a specified `limit` ranked in descending order of similarity score. Args: query (str | list): The text or the embedding to score against. @@ -86,15 +86,7 @@ class VectorisedGraph(object): """ class Document(object): - """ - A document corresponding to a graph entity. Used to generate embeddings. - - Args: - content (str): The document content. - life (int | Tuple[int, int], optional): The optional lifespan of the document. A single value - corresponds to an event, a tuple corresponds to a - window. - """ + """A document corresponding to a graph entity. Used to generate embeddings.""" def __repr__(self): """Return repr(self).""" diff --git a/raphtory/src/algorithms/community_detection/modularity.rs b/raphtory/src/algorithms/community_detection/modularity.rs index eb8bec88fd..c37c99ee4d 100644 --- a/raphtory/src/algorithms/community_detection/modularity.rs +++ b/raphtory/src/algorithms/community_detection/modularity.rs @@ -48,6 +48,22 @@ impl> FromIterator for Partition { } impl Partition { + pub fn from_coms(coms: Vec>) -> Self { + let num_nodes: usize = coms.iter().map(|com| com.len()).sum(); + let mut node_to_com = vec![ComID(0); num_nodes]; + let mut com_to_nodes = Vec::with_capacity(coms.len()); + for (i, com) in coms.into_iter().enumerate() { + let com: HashSet = com.into_iter().map(|v| v.into()).collect(); + for v in com.iter() { + node_to_com[v.index()] = ComID(i); + } + com_to_nodes.push(com); + } + Self { + node_to_com, + com_to_nodes, + } + } /// Initialise all-singleton partition (i.e., each node in its own community) pub fn new_singletons(n: usize) -> Self { let node_to_com = (0..n).map(ComID).collect(); @@ -125,6 +141,37 @@ impl Partition { old_to_new, ) } + + pub fn entropy(&self) -> f64 { + let mut value = 0.0; + let total_count = self.num_nodes() as f64; + for (_, com) in self.coms() { + let count = com.len(); + if count > 0 { + let p = count as f64 / total_count; + value += p * p.log2(); + } + } + -value + } + + /// Compute normalised mutual information between this partition and other partition in bits + pub fn nmi(&self, other: &Partition) -> f64 { + let total_count = self.num_nodes() as f64; + let mut value = 0.0; + for (_, com_i) in self.coms() { + for (j, com_j) in other.coms() { + let p_ij = + (com_i.iter().filter(|&v| other.com(v) == j).count() as f64) / total_count; + if p_ij > 0.0 { + let p_i = (com_i.len() as f64) / total_count; + let p_j = (com_j.len() as f64) / total_count; + value += p_ij * (p_ij / (p_i * p_j)).log2(); + } + } + } + 2.0 * value / (self.entropy() + other.entropy()) + } } pub trait ModularityFunction { @@ -409,3 +456,254 @@ impl ModularityFunction for ModularityUnDir { Box::new((0..self.partition.num_nodes()).map(VID)) } } + +/// Constant Potts model modularity from https://arxiv.org/pdf/1104.3083 +pub struct ConstModularity { + resolution: f64, + partition: Partition, + adj: Vec>, + self_loops: Vec, + adj_com: Vec>, + n: Vec, + n_com: Vec, + n_tot: i64, + m2: f64, + tol: f64, +} + +impl ModularityFunction for ConstModularity { + fn new<'graph, G: GraphViewOps<'graph>>( + graph: G, + weight_prop: Option<&str>, + resolution: f64, + partition: Partition, + tol: f64, + ) -> Self { + let num_nodes = graph.count_nodes(); + let n = vec![1; num_nodes]; + let nodes = graph.nodes(); + let local_id_map: HashMap<_, _> = + nodes.iter().enumerate().map(|(i, n)| (n, VID(i))).collect(); + let adj: Vec<_> = nodes + .iter() + .map(|node| { + node.edges() + .iter() + .filter(|e| e.dst() != e.src()) + .map(|e| { + let w = weight_prop + .map(|w| e.properties().get(w).unwrap_f64()) + .unwrap_or(1.0); + let dst_id = local_id_map[&e.nbr().cloned()]; + (dst_id, w) + }) + .filter(|(_, w)| w >= &tol) + .collect::>() + }) + .collect(); + let self_loops: Vec<_> = graph + .nodes() + .iter() + .map(|node| { + graph + .edge(node.node, node.node) + .map(|e| { + weight_prop + .map(|w| e.properties().get(w).unwrap_f64()) + .unwrap_or(1.0) + }) + .filter(|w| w >= &tol) + .unwrap_or(0.0) + }) + .collect(); + let m2: f64 = adj + .iter() + .flat_map(|neighbours| neighbours.iter().map(|(_, w)| w)) + .sum(); + let adj_com: Vec<_> = adj + .iter() + .enumerate() + .map(|(index, neighbours)| { + let mut com_neighbours = HashMap::new(); + for (n, w) in neighbours { + com_neighbours + .entry(partition.com(n)) + .and_modify(|old_w| *old_w += *w) + .or_insert(*w); + } + if self_loops[index] != 0.0 { + *com_neighbours + .entry(partition.com(&VID(index))) + .or_insert(0.0) += self_loops[index]; + } + com_neighbours + }) + .collect(); + + let n_com = partition.coms().map(|(_, com)| com.len() as i64).collect(); + Self { + partition, + adj, + self_loops, + adj_com, + resolution, + n, + n_com, + n_tot: num_nodes as i64, + m2, + tol, + } + } + + fn move_delta(&self, node: &VID, new_com: ComID) -> f64 { + let old_com = self.partition.com(node); + if old_com == new_com { + 0.0 + } else { + let a = 2.0 + * (self.adj_com[node.index()].get(&new_com).unwrap_or(&0.0) + - self.adj_com[node.index()].get(&old_com).unwrap_or(&0.0) + + self.self_loops[node.index()]); + let p = 2 + * (self.n[node.index()] + * (self.n_com[new_com.index()] - self.n_com[old_com.index()]) + + self.n[node.index()].pow(2)); + + (a / self.m2 - self.resolution * p as f64 / self.n_tot.pow(2) as f64) + } + } + + fn move_node(&mut self, node: &VID, new_com: ComID) { + let old_com = self.partition.com(node); + if old_com != new_com { + let w_self = self.self_loops[node.index()]; + match self.adj_com[node.index()] + .entry(old_com) + .and_modify(|v| *v -= w_self) + { + Entry::Occupied(v) => { + if *v.get() < self.tol { + v.remove(); + } + } + _ => { + // should only be possible for small values due to tolerance above + debug_assert!(w_self < self.tol) + } + } + if w_self != 0.0 { + *self.adj_com[node.index()].entry(new_com).or_insert(0.0) += w_self; + } + + for (n, w) in &self.adj[node.index()] { + match self.adj_com[n.index()] + .entry(old_com) + .and_modify(|v| *v -= w) + { + Entry::Occupied(v) => { + if *v.get() < self.tol { + v.remove(); + } + } + _ => { + // should only be possible for small values due to tolerance above + debug_assert!(*w < self.tol) + } + } + match self.adj_com[node.index()] + .entry(self.partition.com(n)) + .and_modify(|v| *v -= w) + { + Entry::Occupied(v) => { + if *v.get() < self.tol { + v.remove(); + } + } + _ => { + // should only be possible for small values due to tolerance above + debug_assert!(*w < self.tol) + } + } + *self.adj_com[n.index()].entry(new_com).or_insert(0.0) += w; + *self.adj_com[node.index()] + .entry(self.partition.com(n)) + .or_insert(0.0) += w; + } + self.n_com[old_com.index()] -= self.n[node.index()]; + self.n_com[new_com.index()] += self.n[node.index()]; + } + self.partition.move_node(node, new_com); + } + + fn candidate_moves(&self, node: &VID) -> Box + '_> { + Box::new(self.adj_com[node.index()].keys().copied()) + } + + fn aggregate(&mut self) -> Partition { + let old_partition = mem::take(&mut self.partition); + let (new_partition, new_to_old, old_to_new) = old_partition.compact(); + let adj_com: Vec<_> = new_partition + .coms() + .map(|(_c_new, com)| { + let mut neighbours = HashMap::new(); + for n in com { + for (c_old, w) in &self.adj_com[n.index()] { + *neighbours.entry(old_to_new[c_old]).or_insert(0.0) += w; + } + } + neighbours + }) + .collect(); + let adj: Vec<_> = adj_com + .iter() + .enumerate() + .map(|(index, neighbours)| { + neighbours + .iter() + .filter(|(ComID(c), _)| c != &index) + .map(|(ComID(index), w)| (VID(*index), *w)) + .collect::>() + }) + .collect(); + let self_loops: Vec<_> = adj_com + .iter() + .enumerate() + .map(|(index, neighbours)| neighbours.get(&ComID(index)).copied().unwrap_or(0.0)) + .collect(); + let n: Vec<_> = new_to_old + .into_iter() + .map(|ComID(index)| self.n_com[index]) + .collect(); + let n_com = n.clone(); + let partition = Partition::new_singletons(new_partition.num_coms()); + self.adj = adj; + self.adj_com = adj_com; + self.self_loops = self_loops; + self.n = n; + self.n_com = n_com; + self.partition = partition; + new_partition + } + + fn value(&self) -> f64 { + let e: f64 = self + .partition + .coms() + .map(|(cid, com)| { + com.iter() + .flat_map(|n| self.adj_com[n.index()].get(&cid)) + .sum::() + }) + .sum(); + let k: i64 = self.n_com.iter().map(|n| n.pow(2)).sum(); + e / self.m2 - k as f64 / self.n_tot.pow(2) as f64 + } + + fn partition(&self) -> &Partition { + &self.partition + } + + fn nodes(&self) -> Box> { + Box::new((0..self.partition.num_nodes()).map(VID)) + } +} diff --git a/raphtory/src/python/packages/algorithms.rs b/raphtory/src/python/packages/algorithms.rs index 0ca735b314..041f8d8692 100644 --- a/raphtory/src/python/packages/algorithms.rs +++ b/raphtory/src/python/packages/algorithms.rs @@ -12,7 +12,8 @@ use crate::{ }, community_detection::{ label_propagation::label_propagation as label_propagation_rs, - louvain::louvain as louvain_rs, modularity::ModularityUnDir, + louvain::louvain as louvain_rs, + modularity::{ConstModularity, ModularityUnDir}, }, components, cores::k_core::k_core_set, @@ -73,7 +74,11 @@ use crate::{ }; #[cfg(feature = "storage")] use pometry_storage::algorithms::connected_components::connected_components as connected_components_rs; -use pyo3::{prelude::*, types::PyList}; +use pyo3::{ + exceptions::{PyTypeError, PyValueError}, + prelude::*, + types::PyList, +}; use rand::{prelude::StdRng, SeedableRng}; use raphtory_api::core::Direction; use raphtory_storage::core_ops::CoreGraphOps; @@ -842,25 +847,31 @@ pub fn temporal_SEIR( ) } -/// Louvain algorithm for community detection +/// Louvain algorithm for community detection with configuration model /// /// Arguments: /// graph (GraphView): the graph view /// resolution (float): the resolution parameter for modularity. Defaults to 1.0. /// weight_prop (str | None): the edge property to use for weights (has to be float) /// tol (None | float): the floating point tolerance for deciding if improvements are significant (default: 1e-8) +/// modularity (Literal("configuration", "constant")): the modularity function to use. Default to "configuration". /// /// Returns: /// NodeStateUsize: Mapping of nodes to their community assignment #[pyfunction] -#[pyo3[signature=(graph, resolution=1.0, weight_prop=None, tol=None)]] +#[pyo3[signature=(graph, resolution=1.0, weight_prop=None, tol=None, modularity="configuration")]] pub fn louvain( graph: &PyGraphView, resolution: f64, weight_prop: Option<&str>, tol: Option, -) -> NodeState<'static, usize, DynamicGraph> { - louvain_rs::(&graph.graph, resolution, weight_prop, tol) + modularity: &str, +) -> PyResult> { + match modularity { + "configuration" => Ok(louvain_rs::(&graph.graph, resolution, weight_prop, tol)), + "constant" => Ok(louvain_rs::(&graph.graph, resolution, weight_prop, tol)), + other => Err(PyValueError::new_err(format!("'{other}' not a valid value for modularity, should be one of 'configuration' or 'constant'"))) + } } /// Fruchterman Reingold layout algorithm diff --git a/raphtory/tests/algo_tests/community_detection.rs b/raphtory/tests/algo_tests/community_detection.rs index 24571eb3db..4583c535d2 100644 --- a/raphtory/tests/algo_tests/community_detection.rs +++ b/raphtory/tests/algo_tests/community_detection.rs @@ -1,8 +1,9 @@ +use proptest::prelude::*; use raphtory::{ algorithms::community_detection::{ label_propagation::label_propagation, louvain::louvain, - modularity::{ComID, ModularityFunction, ModularityUnDir, Partition}, + modularity::{ComID, ConstModularity, ModularityFunction, ModularityUnDir, Partition}, }, logging::global_info_logger, prelude::*, @@ -12,193 +13,284 @@ use raphtory_core::entities::VID; use std::collections::HashSet; use tracing::info; -#[test] -fn lpa_test() { - let graph: Graph = Graph::new(); - let edges = vec![ - (1, "R1", "R2"), - (1, "R2", "R3"), - (1, "R3", "G"), - (1, "G", "B1"), - (1, "G", "B3"), - (1, "B1", "B2"), - (1, "B2", "B3"), - (1, "B2", "B4"), - (1, "B3", "B4"), - (1, "B3", "B5"), - (1, "B4", "B5"), - ]; - for (ts, src, dst) in edges { - graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); - } - test_storage!(&graph, |graph| { - let seed = Some([5; 32]); - let result = label_propagation(graph, seed).unwrap(); - - let expected = vec![ - HashSet::from([ - graph.node("R1").unwrap(), - graph.node("R2").unwrap(), - graph.node("R3").unwrap(), - ]), - HashSet::from([ - graph.node("G").unwrap(), - graph.node("B1").unwrap(), - graph.node("B2").unwrap(), - graph.node("B3").unwrap(), - graph.node("B4").unwrap(), - graph.node("B5").unwrap(), - ]), +mod lpa { + use super::*; + #[test] + fn lpa_test() { + let graph: Graph = Graph::new(); + let edges = vec![ + (1, "R1", "R2"), + (1, "R2", "R3"), + (1, "R3", "G"), + (1, "G", "B1"), + (1, "G", "B3"), + (1, "B1", "B2"), + (1, "B2", "B3"), + (1, "B2", "B4"), + (1, "B3", "B4"), + (1, "B3", "B5"), + (1, "B4", "B5"), ]; - for hashset in expected { - assert!(result.contains(&hashset)); + for (ts, src, dst) in edges { + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } - }); + test_storage!(&graph, |graph| { + let seed = Some([5; 32]); + let result = label_propagation(graph, seed).unwrap(); + + let expected = vec![ + HashSet::from([ + graph.node("R1").unwrap(), + graph.node("R2").unwrap(), + graph.node("R3").unwrap(), + ]), + HashSet::from([ + graph.node("G").unwrap(), + graph.node("B1").unwrap(), + graph.node("B2").unwrap(), + graph.node("B3").unwrap(), + graph.node("B4").unwrap(), + graph.node("B5").unwrap(), + ]), + ]; + for hashset in expected { + assert!(result.contains(&hashset)); + } + }); + } } -use proptest::prelude::*; +mod louvain { + use super::*; + use std::fs; + #[test] + fn test_louvain() { + let edges = vec![ + (100, 200, 2.0f64), + (100, 300, 3.0f64), + (200, 300, 8.5f64), + (300, 400, 1.0f64), + (400, 500, 1.5f64), + (600, 800, 0.5f64), + (700, 900, 3.5f64), + (100, 600, 1.5f64), + ]; + test_all_nodes_assigned_inner(edges) + } -#[test] -fn test_louvain() { - let edges = vec![ - (100, 200, 2.0f64), - (100, 300, 3.0f64), - (200, 300, 8.5f64), - (300, 400, 1.0f64), - (400, 500, 1.5f64), - (600, 800, 0.5f64), - (700, 900, 3.5f64), - (100, 600, 1.5f64), - ]; - test_all_nodes_assigned_inner(edges) -} + fn test_all_nodes_assigned_inner(edges: Vec<(u64, u64, f64)>) { + let graph = Graph::new(); + for (src, dst, weight) in edges { + graph + .add_edge(1, src, dst, [("weight", weight)], None) + .unwrap(); + graph + .add_edge(1, dst, src, [("weight", weight)], None) + .unwrap(); + } -fn test_all_nodes_assigned_inner(edges: Vec<(u64, u64, f64)>) { - let graph = Graph::new(); - for (src, dst, weight) in edges { - graph - .add_edge(1, src, dst, [("weight", weight)], None) - .unwrap(); - graph - .add_edge(1, dst, src, [("weight", weight)], None) - .unwrap(); + test_storage!(&graph, |graph| { + let result = louvain::(graph, 1.0, Some("weight"), None); + assert!(graph + .nodes() + .iter() + .all(|n| result.get_by_node(n).is_some())); + + let result = louvain::(graph, 1.0, Some("weight"), None); + assert!(graph + .nodes() + .iter() + .all(|n| result.get_by_node(n).is_some())); + }); } - test_storage!(&graph, |graph| { - let result = louvain::(graph, 1.0, Some("weight"), None); - assert!(graph - .nodes() - .iter() - .all(|n| result.get_by_node(n).is_some())); - }); -} + fn test_all_nodes_assigned_inner_unweighted(edges: Vec<(u64, u64)>) { + let graph = Graph::new(); + for (src, dst) in edges { + graph.add_edge(1, src, dst, NO_PROPS, None).unwrap(); + graph.add_edge(1, dst, src, NO_PROPS, None).unwrap(); + } -fn test_all_nodes_assigned_inner_unweighted(edges: Vec<(u64, u64)>) { - let graph = Graph::new(); - for (src, dst) in edges { - graph.add_edge(1, src, dst, NO_PROPS, None).unwrap(); - graph.add_edge(1, dst, src, NO_PROPS, None).unwrap(); + test_storage!(&graph, |graph| { + let result = louvain::(graph, 1.0, None, None); + assert!(graph + .nodes() + .iter() + .all(|n| result.get_by_node(n).is_some())); + + let result = louvain::(graph, 1.0, None, None); + assert!(graph + .nodes() + .iter() + .all(|n| result.get_by_node(n).is_some())); + }); } - test_storage!(&graph, |graph| { - let result = louvain::(graph, 1.0, None, None); - assert!(graph - .nodes() - .iter() - .all(|n| result.get_by_node(n).is_some())); - }); -} + proptest! { + #[test] + fn test_all_nodes_in_communities(edges in any::>().prop_map(|mut v| {v.iter_mut().for_each(|(_, _, w)| *w = w.abs()); v})) { + test_all_nodes_assigned_inner(edges) + } + + #[test] + fn test_all_nodes_assigned_unweighted(edges in any::>().prop_map(|v| v.into_iter().map(|(s, d)| (s as u64, d as u64)).collect::>())) { + test_all_nodes_assigned_inner_unweighted(edges) + } + } -proptest! { + #[cfg(feature = "io")] #[test] - fn test_all_nodes_in_communities(edges in any::>().prop_map(|mut v| {v.iter_mut().for_each(|(_, _, w)| *w = w.abs()); v})) { - test_all_nodes_assigned_inner(edges) + fn lfr_test() { + use raphtory::io::csv_loader::CsvLoader; + use raphtory_api::core::utils::logging::global_info_logger; + use serde::{Deserialize, Serialize}; + use std::path::PathBuf; + global_info_logger(); + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/test"); + let loader = CsvLoader::new(d.join("test.csv")).set_delimiter(","); + let graph = Graph::new(); + + #[derive(Deserialize, Serialize, Debug)] + struct CsvEdge { + src: u64, + dst: u64, + } + + loader + .load_into_graph(&graph, |e: CsvEdge, g| { + g.add_edge(1, e.src, e.dst, NO_PROPS, None).unwrap(); + }) + .unwrap(); + + let expected_coms: Vec> = + serde_json::from_str(&fs::read_to_string(d.join("communities.json")).unwrap()).unwrap(); + + let expected_coms = Partition::from_coms( + expected_coms + .into_iter() + .map(|com| { + com.into_iter() + .map(|id| graph.node(id).unwrap().node) + .collect() + }) + .collect(), + ); + + test_storage!(&graph, |graph| { + let coms = louvain::(graph, 1.0, None, None); + let partition = Partition::from_iter(coms.iter_values().copied()); + let mi = partition.nmi(&expected_coms); + assert!(mi > 0.5) + }); + + test_storage!(&graph, |graph| { + let coms = louvain::(graph, 1.0, None, None); + let partition = Partition::from_iter(coms.iter_values().copied()); + let mi = partition.nmi(&expected_coms); + assert!(mi > 0.5) + }); } #[test] - fn test_all_nodes_assigned_unweighted(edges in any::>().prop_map(|v| v.into_iter().map(|(s, d)| (s as u64, d as u64)).collect::>())) { - test_all_nodes_assigned_inner_unweighted(edges) + fn test_delta() { + global_info_logger(); + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 1, NO_PROPS, None).unwrap(); + + test_storage!(&graph, |graph| { + let mut m = ModularityUnDir::new( + graph, + None, + 1.0, + Partition::new_singletons(graph.count_nodes()), + 1e-8, + ); + let old_value = m.value(); + assert_eq!(old_value, -0.5); + let delta = m.move_delta(&VID(0), ComID(1)); + info!("delta: {delta}"); + m.move_node(&VID(0), ComID(1)); + assert_eq!(m.value(), old_value + delta) + }); } -} -#[cfg(feature = "io")] -#[test] -fn lfr_test() { - use raphtory::io::csv_loader::CsvLoader; - use raphtory_api::core::utils::logging::global_info_logger; - use serde::{Deserialize, Serialize}; - use std::path::PathBuf; - global_info_logger(); - let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - d.push("resources/test"); - let loader = CsvLoader::new(d.join("test.csv")).set_delimiter(","); - let graph = Graph::new(); - - #[derive(Deserialize, Serialize, Debug)] - struct CsvEdge { - src: u64, - dst: u64, + #[test] + fn test_delta_const() { + global_info_logger(); + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 1, NO_PROPS, None).unwrap(); + + test_storage!(&graph, |graph| { + let mut m = ConstModularity::new( + graph, + None, + 1.0, + Partition::new_singletons(graph.count_nodes()), + 1e-8, + ); + let old_value = m.value(); + assert_eq!(old_value, -0.5); + let delta = m.move_delta(&VID(0), ComID(1)); + info!("delta: {delta}"); + m.move_node(&VID(0), ComID(1)); + assert_eq!(m.value(), old_value + delta) + }); } - loader - .load_into_graph(&graph, |e: CsvEdge, g| { - g.add_edge(1, e.src, e.dst, NO_PROPS, None).unwrap(); - }) - .unwrap(); + #[test] + fn test_aggregation() { + global_info_logger(); + let graph = Graph::new(); + graph.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); + graph.add_edge(0, 1, 0, NO_PROPS, None).unwrap(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 1, NO_PROPS, None).unwrap(); + graph.add_edge(0, 0, 3, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 0, NO_PROPS, None).unwrap(); - test_storage!(&graph, |graph| { - let _ = louvain::(graph, 1.0, None, None); - // TODO: Add assertions - }); -} + test_storage!(&graph, |graph| { + let partition = Partition::from_iter([0usize, 0, 1, 1]); + let mut m = ModularityUnDir::new(graph, None, 1.0, partition, 1e-8); + let value_before = m.value(); + let _ = m.aggregate(); + let value_after = m.value(); + info!("before: {value_before}, after: {value_after}"); + assert_eq!(value_after, value_before); + let delta = m.move_delta(&VID(0), ComID(1)); + m.move_node(&VID(0), ComID(1)); + let value_merged = m.value(); + assert_eq!(value_merged, 0.0); + assert!((value_merged - (value_after + delta)).abs() < 1e-8); + }); + } -#[test] -fn test_delta() { - global_info_logger(); - let graph = Graph::new(); - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - graph.add_edge(0, 2, 1, NO_PROPS, None).unwrap(); - - test_storage!(&graph, |graph| { - let mut m = ModularityUnDir::new( - graph, - None, - 1.0, - Partition::new_singletons(graph.count_nodes()), - 1e-8, - ); - let old_value = m.value(); - assert_eq!(old_value, -0.5); - let delta = m.move_delta(&VID(0), ComID(1)); - info!("delta: {delta}"); - m.move_node(&VID(0), ComID(1)); - assert_eq!(m.value(), old_value + delta) - }); -} + #[test] + fn test_aggregation_const() { + global_info_logger(); + let graph = Graph::new(); + graph.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); + graph.add_edge(0, 1, 0, NO_PROPS, None).unwrap(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 1, NO_PROPS, None).unwrap(); + graph.add_edge(0, 0, 3, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 0, NO_PROPS, None).unwrap(); -#[test] -fn test_aggregation() { - global_info_logger(); - let graph = Graph::new(); - graph.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); - graph.add_edge(0, 1, 0, NO_PROPS, None).unwrap(); - graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); - graph.add_edge(0, 2, 1, NO_PROPS, None).unwrap(); - graph.add_edge(0, 0, 3, NO_PROPS, None).unwrap(); - graph.add_edge(0, 3, 0, NO_PROPS, None).unwrap(); - - test_storage!(&graph, |graph| { - let partition = Partition::from_iter([0usize, 0, 1, 1]); - let mut m = ModularityUnDir::new(graph, None, 1.0, partition, 1e-8); - let value_before = m.value(); - let _ = m.aggregate(); - let value_after = m.value(); - info!("before: {value_before}, after: {value_after}"); - assert_eq!(value_after, value_before); - let delta = m.move_delta(&VID(0), ComID(1)); - m.move_node(&VID(0), ComID(1)); - let value_merged = m.value(); - assert_eq!(value_merged, 0.0); - assert!((value_merged - (value_after + delta)).abs() < 1e-8); - }); + test_storage!(&graph, |graph| { + let partition = Partition::from_iter([0usize, 0, 1, 1]); + let mut m = ConstModularity::new(graph, None, 1.0, partition, 1e-8); + let value_before = m.value(); + let _ = m.aggregate(); + let value_after = m.value(); + info!("before: {value_before}, after: {value_after}"); + assert_eq!(value_after, value_before); + let delta = m.move_delta(&VID(0), ComID(1)); + m.move_node(&VID(0), ComID(1)); + let value_merged = m.value(); + assert_eq!(value_merged, 0.0); + assert!((value_merged - (value_after + delta)).abs() < 1e-8); + }); + } }