diff --git a/python/python/raphtory/algorithms/__init__.pyi b/python/python/raphtory/algorithms/__init__.pyi index 84a1c21c71..809ef2b2df 100644 --- a/python/python/raphtory/algorithms/__init__.pyi +++ b/python/python/raphtory/algorithms/__init__.pyi @@ -89,21 +89,21 @@ class Matching(object): """ -def all_local_reciprocity(g: GraphView): +def all_local_reciprocity(graph: GraphView): """ Local reciprocity - measure of the symmetry of relationships associated with a node This measures the proportion of a node's outgoing edges which are reciprocated with an incoming edge. Arguments: - g (GraphView) : a directed Raphtory graph + graph (GraphView) : a directed Raphtory graph Returns: AlgorithmResult : AlgorithmResult with string keys and float values mapping each node name to its reciprocity value. """ -def average_degree(g: GraphView): +def average_degree(graph: GraphView): """ The average (undirected) degree of all nodes in the graph. @@ -111,14 +111,14 @@ def average_degree(g: GraphView): the number of undirected edges divided by the number of nodes. Arguments: - g (GraphView) : a Raphtory graph + graph (GraphView) : a Raphtory graph Returns: float : the average degree of the nodes in the graph """ def balance( - g: GraphView, + graph: GraphView, name: str = "weight", direction: Direction = "both", threads: Optional[int] = None, @@ -129,7 +129,7 @@ def balance( This function computes the sum of edge weights based on the direction provided, and can be executed in parallel using a given number of threads. Arguments: - g (GraphView): The graph view on which the operation is to be performed. + graph (GraphView): The graph view on which the operation is to be performed. name (str): The name of the edge property used as the weight. Defaults to "weight". direction (Direction): Specifies the direction of the edges to be considered for summation. Defaults to "both". * "out": Only consider outgoing edges. @@ -143,35 +143,55 @@ def balance( """ def betweenness_centrality( - g: GraphView, k: Optional[int] = None, normalized: bool = True + graph: GraphView, k: Optional[int] = None, normalized: bool = True ) -> AlgorithmResult: """ Computes the betweenness centrality for nodes in a given graph. Arguments: - g (GraphView): A reference to the graph. + graph (GraphView): A reference to the graph. k (int, optional): Specifies the number of nodes to consider for the centrality computation. All nodes are considered by default. normalized (bool): Indicates whether to normalize the centrality values. Returns: - AlgorithmResult: Returns an `AlgorithmResult` containing the betweenness centrality of each node. + AlgorithmResult: Returns an AlgorithmResult containing the betweenness centrality of each node. """ def cohesive_fruchterman_reingold( - graph, iterations=100, scale=1.0, node_start_size=1.0, cooloff_factor=0.95, dt=0.1 -): - """Cohesive version of `fruchterman_reingold` that adds virtual edges between isolated nodes""" + graph: GraphView, + iter_count: int = 100, + scale: float = 1.0, + node_start_size: float = 1.0, + cooloff_factor: float = 0.95, + dt: float = 0.1, +) -> AlgorithmResult: + """ + Cohesive version of `fruchterman_reingold` that adds virtual edges between isolated nodes + Arguments: + graph (GraphView): A reference to the graph + iter_count (int): The number of iterations to run + scale (float): Global scaling factor to control the overall spread of the graph + node_start_size (float): Initial size or movement range for nodes + cooloff_factor (float): Factor to reduce node movement in later iterations, helping stabilize the layout + dt (float): Time step or movement factor in each iteration + + Returns: + AlgorithmResult: Returns an AlgorithmResult containing a mapping between vertices and a pair of coordinates. + + """ -def connected_components(g): ... -def degree_centrality(g: GraphView, threads: Optional[int] = None) -> AlgorithmResult: +def connected_components(graph): ... +def degree_centrality( + graph: GraphView, threads: Optional[int] = None +) -> AlgorithmResult: """ Computes the degree centrality of all nodes in the graph. The values are normalized by dividing each result with the maximum possible degree. Graphs with self-loops can have values of centrality greater than 1. Arguments: - g (GraphView): The graph view on which the operation is to be performed. + graph (GraphView): The graph view on which the operation is to be performed. threads (int, optional): The number of threads to be used for parallel execution. Returns: @@ -179,28 +199,28 @@ def degree_centrality(g: GraphView, threads: Optional[int] = None) -> AlgorithmR """ def dijkstra_single_source_shortest_paths( - g: GraphView, + graph: GraphView, source: InputNode, targets: list[InputNode], direction: Direction = "both", weight: str = "weight", -) -> dict: +) -> AlgorithmResult: """ Finds the shortest paths from a single source to multiple targets in a graph. Arguments: - g (GraphView): The graph to search in. + graph (GraphView): The graph to search in. source (InputNode): The source node. targets (list[InputNode]): A list of target nodes. direction (Direction): The direction of the edges to be considered for the shortest path. Defaults to "both". weight (str): The name of the weight property for the edges. Defaults to "weight". Returns: - dict: Returns a `Dict` where the key is the target node and the value is a tuple containing the total cost and a vector of nodes representing the shortest path. + AlgorithmResult: Returns an AlgorithmResult where the key is the target node and the value is a tuple containing the total cost and a vector of nodes representing the shortest path. """ -def directed_graph_density(g: GraphView): +def directed_graph_density(graph: GraphView): """ Graph density - measures how dense or sparse a graph is. @@ -208,14 +228,14 @@ def directed_graph_density(g: GraphView): edges (given by N * (N-1) where N is the number of nodes). Arguments: - g (GraphView) : a directed Raphtory graph + graph (GraphView) : a directed Raphtory graph Returns: - float : Directed graph density of G. + float : Directed graph density of graph. """ def fast_rp( - g: GraphView, + graph: GraphView, embedding_dim: int, normalization_strength: float, iter_weights: list[float], @@ -226,7 +246,7 @@ def fast_rp( Computes embedding vectors for each vertex of an undirected/bidirectional graph according to the Fast RP algorithm. Original Paper: https://doi.org/10.48550/arXiv.1908.11512 Arguments: - g (GraphView): The graph view on which embeddings are generated. + graph (GraphView): The graph view on which embeddings are generated. embedding_dim (int): The size (dimension) of the generated embeddings. normalization_strength (float): The extent to which high-degree vertices should be discounted (range: 1-0) iter_weights (list[float]): The scalar weights to apply to the results of each iteration @@ -234,7 +254,7 @@ def fast_rp( threads (int, optional): The number of threads to be used for parallel execution. Returns: - AlgorithmResult: Vertices mapped to their corresponding embedding vectors + AlgorithmResult: Returns an AlgorithmResult containing the embedding vector of each node. """ def fruchterman_reingold( @@ -244,7 +264,7 @@ def fruchterman_reingold( node_start_size: float | None = 1.0, cooloff_factor: float | None = 0.95, dt: float | None = 0.1, -): +) -> AlgorithmResult: """ Fruchterman Reingold layout algorithm @@ -257,10 +277,10 @@ def fruchterman_reingold( dt (float | None): the time increment between iterations (default: 0.1) Returns: - a dict with the position for each node as a list with two numbers [x, y] + AlgorithmResult: an AlgorithmResult with the position for each node as a list with two numbers [x, y] """ -def global_clustering_coefficient(g: GraphView): +def global_clustering_coefficient(graph: GraphView): """ Computes the global clustering coefficient of a graph. The global clustering coefficient is defined as the number of triangles in the graph divided by the number of triplets in the graph. @@ -268,16 +288,16 @@ def global_clustering_coefficient(g: GraphView): Note that this is also known as transitivity and is different to the average clustering coefficient. Arguments: - g (GraphView) : a Raphtory graph, treated as undirected + graph (GraphView) : a Raphtory graph, treated as undirected Returns: float : the global clustering coefficient of the graph See also: - [`Triplet Count`](triplet_count) + [Triplet Count](triplet_count) """ -def global_reciprocity(g: GraphView): +def global_reciprocity(graph: GraphView): """ Reciprocity - measure of the symmetry of relationships in a graph, the global reciprocity of the entire graph. @@ -285,13 +305,15 @@ def global_reciprocity(g: GraphView): graph and normalizes it by the total number of directed edges. Arguments: - g (GraphView) : a directed Raphtory graph + graph (GraphView) : a directed Raphtory graph Returns: float : reciprocity of the graph between 0 and 1. """ -def global_temporal_three_node_motif(g: GraphView, delta: int): +def global_temporal_three_node_motif( + graph: GraphView, delta: int, threads: Optional[int] = None +): """ Computes the number of three edge, up-to-three node delta-temporal motifs in the graph, using the algorithm of Paranjape et al, Motifs in Temporal Networks (2017). We point the reader to this reference for more information on the algorithm and background, but provide a short summary below. @@ -328,8 +350,9 @@ def global_temporal_three_node_motif(g: GraphView, delta: int): 8. i --> j, i --> k, k --> j Arguments: - g (GraphView) : A directed raphtory graph + graph (GraphView) : A directed raphtory graph delta (int): Maximum time difference between the first and last edge of the motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise milliseconds should be used (if edge times were given as string) + threads (int, optional): Number of threads to use Returns: list : A 40 dimensional array with the counts of each motif, given in the same order as described above. Note that the two-node motif counts are symmetrical so it may be more useful just to consider the first four elements. @@ -339,19 +362,22 @@ def global_temporal_three_node_motif(g: GraphView, delta: int): """ -def global_temporal_three_node_motif_multi(g: GraphView, deltas: list[int]): +def global_temporal_three_node_motif_multi( + graph: GraphView, deltas: list[int], threads: Optional[int] = None +): """ - Computes the global counts of three-edge up-to-three node temporal motifs for a range of timescales. See `global_temporal_three_node_motif` for an interpretation of each row returned. + Computes the global counts of three-edge up-to-three node temporal motifs for a range of timescales. See global_temporal_three_node_motif for an interpretation of each row returned. Arguments: - g (GraphView) : A directed raphtory graph - deltas(list[int]): A list of delta values to use. + graph (GraphView) : A directed raphtory graph + deltas (list[int]): A list of delta values to use. + threads (int, optional): Number of threads to use Returns: list[list[int]] : A list of 40d arrays, each array is the motif count for a particular value of delta, returned in the order that the deltas were given as input. """ -def hits(g: GraphView, iter_count: int = 20, threads: Optional[int] = None): +def hits(graph: GraphView, iter_count: int = 20, threads: Optional[int] = None): """ HITS (Hubs and Authority) Algorithm: AuthScore of a node (A) = Sum of HubScore of all nodes pointing at node (A) from previous iteration / @@ -361,7 +387,7 @@ def hits(g: GraphView, iter_count: int = 20, threads: Optional[int] = None): Sum of AuthScore of all nodes in the current iteration Arguments: - g (GraphView): Graph to run the algorithm on + graph (GraphView): Graph to run the algorithm on iter_count (int): How many iterations to run the algorithm threads (int, optional): Number of threads to use @@ -380,23 +406,25 @@ def in_component(node: Node): An array containing the Nodes within the given nodes in-component """ -def in_components(g: GraphView): +def in_components(graph: GraphView): """ In components -- Finding the "in-component" of a node in a directed graph involves identifying all nodes that can be reached following only incoming edges. Arguments: - g (GraphView) : Raphtory graph + graph (GraphView) : Raphtory graph Returns: AlgorithmResult : AlgorithmResult object mapping each node to an array containing the ids of all nodes within their 'in-component' """ -def label_propagation(g: GraphView, seed: Optional[bytes] = None) -> list[set[Node]]: +def label_propagation( + graph: GraphView, seed: Optional[bytes] = None +) -> list[set[Node]]: """ Computes components using a label propagation algorithm Arguments: - g (GraphView): A reference to the graph + graph (GraphView): A reference to the graph seed (bytes, optional): Array of 32 bytes of u8 which is set as the rng seed Returns: @@ -404,26 +432,26 @@ def label_propagation(g: GraphView, seed: Optional[bytes] = None) -> list[set[No """ -def local_clustering_coefficient(g: GraphView, v: InputNode): +def local_clustering_coefficient(graph: GraphView, v: InputNode): """ Local clustering coefficient - measures the degree to which nodes in a graph tend to cluster together. The proportion of pairs of neighbours of a node who are themselves connected. Arguments: - g (GraphView) : Raphtory graph, can be directed or undirected but will be treated as undirected. + graph (GraphView) : Raphtory graph, can be directed or undirected but will be treated as undirected. v (InputNode): node id or name Returns: - float : the local clustering coefficient of node v in g. + float : the local clustering coefficient of node v in graph. """ -def local_temporal_three_node_motifs(g: GraphView, delta: int): +def local_temporal_three_node_motifs(graph: GraphView, delta: int, threads=None): """ Computes the number of each type of motif that each node participates in. See global_temporal_three_node_motifs for a summary of the motifs involved. Arguments: - g (GraphView) : A directed raphtory graph + graph (GraphView) : A directed raphtory graph delta (int): Maximum time difference between the first and last edge of the motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise milliseconds should be used (if edge times were given as string) Returns: @@ -434,7 +462,7 @@ def local_temporal_three_node_motifs(g: GraphView, delta: int): the motif. For two node motifs, both constituent nodes count the motif. For triangles, all three constituent nodes count the motif. """ -def local_triangle_count(g: GraphView, v: InputNode): +def local_triangle_count(graph: GraphView, v: InputNode): """ Implementations of various graph algorithms that can be run on a graph. @@ -445,7 +473,7 @@ def local_triangle_count(g: GraphView, v: InputNode): This function returns the number of pairs of neighbours of a given node which are themselves connected. Arguments: - g (GraphView) : Raphtory graph, this can be directed or undirected but will be treated as undirected + graph (GraphView) : Raphtory graph, this can be directed or undirected but will be treated as undirected v (InputNode) : node id or name Returns: @@ -458,45 +486,48 @@ def louvain( resolution: float = 1.0, weight_prop: str | None = None, tol: None | float = None, -): +) -> AlgorithmResult: """ Louvain algorithm for community detection Arguments: graph (GraphView): the graph view - resolution (float): the resolution paramter for modularity + resolution (float): the resolution parameter for modularity 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) + + Returns: + AlgorithmResult: Returns an AlgorithmResult containing the community id of each node. """ -def max_degree(g: GraphView) -> int: +def max_degree(graph: GraphView) -> int: """ Returns the largest degree found in the graph Arguments: - g (GraphView): The graph view on which the operation is to be performed. + graph (GraphView): The graph view on which the operation is to be performed. Returns: int: The largest degree """ -def max_in_degree(g: GraphView): +def max_in_degree(graph: GraphView): """ The maximum in degree of any node in the graph. Arguments: - g (GraphView) : a directed Raphtory graph + graph (GraphView) : a directed Raphtory graph Returns: int : value of the largest indegree """ -def max_out_degree(g: GraphView): +def max_out_degree(graph: GraphView): """ The maximum out degree of any node in the graph. Arguments: - g (GraphView) : a directed Raphtory graph + graph (GraphView) : a directed Raphtory graph Returns: int : value of the largest outdegree @@ -510,7 +541,7 @@ def max_weight_matching( ) -> Matching: """ Compute a maximum-weighted matching in the general undirected weighted - graph given by "edges". If `max_cardinality` is true, only + graph given by "edges". If max_cardinality is true, only maximum-cardinality matchings are considered as solutions. The algorithm is based on "Efficient Algorithms for Finding Maximum @@ -542,34 +573,34 @@ def max_weight_matching( Matching: The matching """ -def min_degree(g: GraphView) -> int: +def min_degree(graph: GraphView) -> int: """ Returns the smallest degree found in the graph Arguments: - g (GraphView): The graph view on which the operation is to be performed. + graph (GraphView): The graph view on which the operation is to be performed. Returns: int: The smallest degree found """ -def min_in_degree(g: GraphView): +def min_in_degree(graph: GraphView): """ The minimum in degree of any node in the graph. Arguments: - g (GraphView) : a directed Raphtory graph + graph (GraphView) : a directed Raphtory graph Returns: int : value of the smallest indegree """ -def min_out_degree(g: GraphView): +def min_out_degree(graph: GraphView): """ The minimum out degree of any node in the graph. Arguments: - g (GraphView) : a directed Raphtory graph + graph (GraphView) : a directed Raphtory graph Returns: int : value of the smallest outdegree @@ -586,19 +617,19 @@ def out_component(node: Node) -> NodeStateUsize: NodeStateUsize: A NodeState mapping the nodes in the out-component to their distance from the starting node. """ -def out_components(g: GraphView): +def out_components(graph: GraphView): """ Out components -- Finding the "out-component" of a node in a directed graph involves identifying all nodes that can be reached following only outgoing edges. Arguments: - g (GraphView) : Raphtory graph + graph (GraphView) : Raphtory graph Returns: AlgorithmResult : AlgorithmResult object mapping each node to an array containing the ids of all nodes within their 'out-component' """ def pagerank( - g: GraphView, + graph: GraphView, iter_count: int = 20, max_diff: Optional[float] = None, use_l2_norm=True, @@ -612,7 +643,7 @@ def pagerank( is less than the max diff value given. Arguments: - g (GraphView) : Raphtory graph + graph (GraphView) : Raphtory graph iter_count (int) : Maximum number of iterations to run. Note that this will terminate early if convergence is reached. max_diff (Optional[float]) : Optional parameter providing an alternative stopping condition. The algorithm will terminate if the sum of the absolute difference in pagerank values between iterations @@ -623,29 +654,29 @@ def pagerank( """ def single_source_shortest_path( - g: GraphView, source: InputNode, cutoff: Optional[int] = None + graph: GraphView, source: InputNode, cutoff: Optional[int] = None ) -> AlgorithmResult: """ Calculates the single source shortest paths from a given source node. Arguments: - g (GraphView): A reference to the graph. Must implement `GraphViewOps`. - source (InputNode): The source node. Must implement `InputNode`. + graph (GraphView): A reference to the graph. Must implement GraphViewOps. + source (InputNode): The source node. Must implement InputNode. cutoff (int, optional): An optional cutoff level. The algorithm will stop if this level is reached. Returns: - AlgorithmResult: Returns an `AlgorithmResult[str, list[str]]` containing the shortest paths from the source to all reachable nodes. + AlgorithmResult: Returns an AlgorithmResult[str, list[str]] containing the shortest paths from the source to all reachable nodes. """ -def strongly_connected_components(g: GraphView): +def strongly_connected_components(graph: GraphView): """ Strongly connected components Partitions the graph into node sets which are mutually reachable by an directed path Arguments: - g (GraphView) : Raphtory graph + graph (GraphView) : Raphtory graph Returns: list[list[int]] : List of strongly connected nodes identified by ids @@ -667,8 +698,8 @@ def temporal_SEIR( Arguments: graph (GraphView): the graph view - seeds (int | float | list[InputNode]): the seeding strategy to use for the initial infection (if `int`, choose fixed number - of nodes at random, if `float` infect each node with this probability, if `list` + seeds (int | float | list[InputNode]): the seeding strategy to use for the initial infection (if int, choose fixed number + of nodes at random, if float infect each node with this probability, if list initially infect the specified nodes infection_prob (float): the probability for a contact between infected and susceptible nodes to lead to a transmission @@ -682,34 +713,34 @@ def temporal_SEIR( rng_seed (int | None): optional seed for the random number generator Returns: - AlgorithmResult: Returns an `Infected` object for each infected node with attributes + AlgorithmResult: Returns an AlgorithmResult mapping vertices to Infected objects with the following attributes: - `infected`: the time stamp of the infection event + infected: the time stamp of the infection event - `active`: the time stamp at which the node actively starts spreading the infection (i.e., the end of the incubation period) + active: the time stamp at which the node actively starts spreading the infection (i.e., the end of the incubation period) - `recovered`: the time stamp at which the node recovered (i.e., stopped spreading the infection) + recovered: the time stamp at which the node recovered (i.e., stopped spreading the infection) """ def temporal_bipartite_graph_projection( - g: GraphView, delta: int, pivot_type + graph: GraphView, delta: int, pivot_type: str ) -> GraphView: """ - Projects a temporal bipartite graph into an undirected temporal graph over the pivot node type. Let G be a bipartite graph with node types A and B. Given delta > 0, the projection graph G' pivoting over type B nodes, - will make a connection between nodes n1 and n2 (of type A) at time (t1 + t2)/2 if they respectively have an edge at time t1, t2 with the same node of type B in G, and |t2-t1| < delta. + Projects a temporal bipartite graph into an undirected temporal graph over the pivot node type. Let graph be a bipartite graph with node types A and B. Given delta > 0, the projection graph graph' pivoting over type B nodes, + will make a connection between nodes n1 and n2 (of type A) at time (t1 + t2)/2 if they respectively have an edge at time t1, t2 with the same node of type B in graph, and |t2-t1| < delta. Arguments: - g (GraphView) : A directed raphtory graph + graph (GraphView) : A directed raphtory graph delta (int): Time period - pivot (str) : node type to pivot over. If a bipartite graph has types A and B, and B is the pivot type, the new graph will consist of type A nodes. + pivot_type (str) : node type to pivot over. If a bipartite graph has types A and B, and B is the pivot type, the new graph will consist of type A nodes. Returns: GraphView: Projected (unipartite) temporal graph. """ def temporally_reachable_nodes( - g: GraphView, + graph: GraphView, max_hops: int, start_time: int, seed_nodes: list[InputNode], @@ -723,7 +754,7 @@ def temporally_reachable_nodes( a sequence of edges (v_i, v_i+1, t_i) with t_i < t_i+1 for i = 1, ... , k - 1. Arguments: - g (GraphView) : directed Raphtory graph + graph (GraphView) : directed Raphtory graph max_hops (int) : maximum number of hops to propagate out start_time (int) : time at which to start the path (such that t_1 > start_time for any path starting from these seed nodes) seed_nodes (list[InputNode]) : list of node names or ids which should be the starting nodes @@ -733,7 +764,7 @@ def temporally_reachable_nodes( AlgorithmResult : AlgorithmResult with string keys and float values mapping node names to their pagerank value. """ -def triplet_count(g: GraphView): +def triplet_count(graph: GraphView): """ Computes the number of connected triplets within a graph @@ -741,13 +772,15 @@ def triplet_count(g: GraphView): A-B, B-C, C-A is formed of three connected triplets. Arguments: - g (GraphView) : a Raphtory graph, treated as undirected + graph (GraphView) : a Raphtory graph, treated as undirected Returns: int : the number of triplets in the graph """ -def weakly_connected_components(g: GraphView, iter_count: int = 9223372036854775807): +def weakly_connected_components( + graph: GraphView, iter_count: int = 9223372036854775807 +): """ Weakly connected components -- partitions the graph into node sets which are mutually reachable by an undirected path @@ -755,7 +788,7 @@ def weakly_connected_components(g: GraphView, iter_count: int = 9223372036854775 by an undirected path. Arguments: - g (GraphView) : Raphtory graph + graph (GraphView) : Raphtory graph iter_count (int) : Maximum number of iterations to run. Note that this will terminate early if the labels converge prior to the number of iterations being reached. Returns: diff --git a/python/tests/test_graphdb/test_graphdb.py b/python/tests/test_graphdb/test_graphdb.py index b087633cf1..17538a8075 100644 --- a/python/tests/test_graphdb/test_graphdb.py +++ b/python/tests/test_graphdb/test_graphdb.py @@ -1085,7 +1085,7 @@ def check(g): lotr_graph, "Frodo" ) lotr_local_triangle_count = algorithms.local_triangle_count(lotr_graph, "Frodo") - assert lotr_clustering_coefficient == 0.1984313726425171 + assert lotr_clustering_coefficient == 0.1984313725490196 assert lotr_local_triangle_count == 253 check(g) diff --git a/raphtory-graphql/src/model/algorithms/algorithms.rs b/raphtory-graphql/src/model/algorithms/algorithms.rs index 552ff2dc4e..0b8ffcbcc9 100644 --- a/raphtory-graphql/src/model/algorithms/algorithms.rs +++ b/raphtory-graphql/src/model/algorithms/algorithms.rs @@ -7,11 +7,15 @@ use dynamic_graphql::{internal::TypeName, SimpleObject}; use futures_util::future::BoxFuture; use itertools::Itertools; use ordered_float::OrderedFloat; -use raphtory::algorithms::{ - centrality::pagerank::unweighted_page_rank, - pathing::dijkstra::dijkstra_single_source_shortest_paths, +use raphtory::{ + algorithms::{ + centrality::pagerank::unweighted_page_rank, + pathing::dijkstra::dijkstra_single_source_shortest_paths, + }, + core::Prop, }; use raphtory_api::core::Direction; +use std::collections::HashMap; #[derive(SimpleObject)] pub(crate) struct PagerankOutput { @@ -160,8 +164,15 @@ fn apply_shortest_path<'b>( .iter() .map(|v| v.string()) .collect::, _>>()?; - let binding = - dijkstra_single_source_shortest_paths(&entry_point.graph, source, targets, None, direction); + let binding: Result)>, &str> = + Ok(dijkstra_single_source_shortest_paths( + &entry_point.graph, + source, + targets, + None, + direction, + )? + .get_all_with_names()); let result: Vec = binding .into_iter() .flat_map(|pair| { diff --git a/raphtory/src/algorithms/algorithm_result.rs b/raphtory/src/algorithms/algorithm_result.rs index 92fe1fe2f1..64cfd43d83 100644 --- a/raphtory/src/algorithms/algorithm_result.rs +++ b/raphtory/src/algorithms/algorithm_result.rs @@ -27,6 +27,18 @@ impl AsOrd> for T { } } +impl AsOrd<[OrderedFloat; 2]> for [T; 2] { + fn as_ord(&self) -> &[OrderedFloat; 2] { + unsafe { &*(self as *const [T; 2] as *const [OrderedFloat; 2]) } + } +} + +impl AsOrd<(OrderedFloat, Vec)> for (T, Vec) { + fn as_ord(&self) -> &(OrderedFloat, Vec) { + unsafe { &*(self as *const (T, Vec) as *const (OrderedFloat, Vec)) } + } +} + impl AsOrd<(OrderedFloat, OrderedFloat)> for (T, T) { fn as_ord(&self) -> &(OrderedFloat, OrderedFloat) { // Safety: OrderedFloat is #[repr(transparent)] and has no invalid values, i.e. there is no physical difference between OrderedFloat and Float. diff --git a/raphtory/src/algorithms/bipartite/max_weight_matching.rs b/raphtory/src/algorithms/bipartite/max_weight_matching.rs index 044e8e024c..aa52f1b6b5 100644 --- a/raphtory/src/algorithms/bipartite/max_weight_matching.rs +++ b/raphtory/src/algorithms/bipartite/max_weight_matching.rs @@ -826,17 +826,21 @@ fn verify_optimum( /// /// The function takes time O(n**3) /// -/// Arguments: +/// # Arguments /// -/// * `graph` - The graph to compute the maximum weight matching for -/// * `max_cardinality` - If set to true compute the maximum-cardinality matching +/// - `graph` - The graph to compute the maximum weight matching for +/// - `max_cardinality` - If set to true compute the maximum-cardinality matching /// with maximum weight among all maximum-cardinality matchings -/// * `verify_optimum_flag`: If true prior to returning an additional routine +/// - `verify_optimum_flag`: If true prior to returning an additional routine /// to verify the optimal solution was found will be run after computing /// the maximum weight matching. If it's true and the found matching is not /// an optimal solution this function will panic. This option should /// normally be only set true during testing. /// +/// # Returns +/// +/// A [Matching] object that contains a mapping of vertices to outwardly and inwardly assigned target vertices. +/// /// # Example /// ```rust /// diff --git a/raphtory/src/algorithms/centrality/betweenness.rs b/raphtory/src/algorithms/centrality/betweenness.rs index d95deb6d24..d01e0a21ba 100644 --- a/raphtory/src/algorithms/centrality/betweenness.rs +++ b/raphtory/src/algorithms/centrality/betweenness.rs @@ -9,7 +9,7 @@ use std::collections::{HashMap, VecDeque}; /// Computes the betweenness centrality for nodes in a given graph. /// -/// # Parameters +/// # Arguments /// /// - `g`: A reference to the graph. /// - `k`: An `Option` specifying the number of nodes to consider for the centrality computation. Defaults to all nodes if `None`. @@ -17,7 +17,7 @@ use std::collections::{HashMap, VecDeque}; /// /// # Returns /// -/// Returns an `AlgorithmResult` containing the betweenness centrality of each node. +/// An [AlgorithmResult] containing the betweenness centrality of each node. pub fn betweenness_centrality<'graph, G: GraphViewOps<'graph>>( g: &'graph G, k: Option, diff --git a/raphtory/src/algorithms/centrality/degree_centrality.rs b/raphtory/src/algorithms/centrality/degree_centrality.rs index c9892d8579..a72bfdd42d 100644 --- a/raphtory/src/algorithms/centrality/degree_centrality.rs +++ b/raphtory/src/algorithms/centrality/degree_centrality.rs @@ -17,6 +17,15 @@ use ordered_float::OrderedFloat; /// Computes the degree centrality of all nodes in the graph. The values are normalized /// by dividing each result with the maximum possible degree. Graphs with self-loops can have /// values of centrality greater than 1. +/// +/// # Arguments +/// +/// - `g`: A reference to the graph. +/// - `threads` - Number of threads to use +/// +/// # Returns +/// +/// An [AlgorithmResult] containing the degree centrality of each node. pub fn degree_centrality( g: &G, threads: Option, diff --git a/raphtory/src/algorithms/centrality/hits.rs b/raphtory/src/algorithms/centrality/hits.rs index 15842786f9..abd677226c 100644 --- a/raphtory/src/algorithms/centrality/hits.rs +++ b/raphtory/src/algorithms/centrality/hits.rs @@ -45,9 +45,15 @@ impl Default for Hits { /// HubScore of a node (A) = Sum of AuthScore of all nodes pointing away from node (A) from previous iteration / /// Sum of AuthScore of all nodes in the current iteration /// -/// Returns +/// # Arguments /// -/// * An AlgorithmResult object containing the mapping from node ID to the hub and authority score of the node +/// - `g`: A reference to the graph. +/// - `iter_count` - The number of iterations to run +/// - `threads` - Number of threads to use +/// +/// # Returns +/// +/// An [AlgorithmResult] object containing the mapping from node ID to the hub and authority score of the node pub fn hits( g: &G, iter_count: usize, diff --git a/raphtory/src/algorithms/centrality/pagerank.rs b/raphtory/src/algorithms/centrality/pagerank.rs index f27fe3fa9b..cd40051530 100644 --- a/raphtory/src/algorithms/centrality/pagerank.rs +++ b/raphtory/src/algorithms/centrality/pagerank.rs @@ -39,18 +39,18 @@ impl PageRankState { /// PageRank Algorithm: /// PageRank shows how important a node is in a graph. /// -/// Arguments: +/// # Arguments /// -/// * `g`: A GraphView object -/// * `iter_count`: Number of iterations to run the algorithm for -/// * `threads`: Number of threads to use for parallel execution -/// * `tol`: The tolerance value for convergence -/// * `use_l2_norm`: Whether to use L2 norm for convergence -/// * `damping_factor`: Probability of likelihood the spread will continue +/// - `g`: A GraphView object +/// - `iter_count`: Number of iterations to run the algorithm for +/// - `threads`: Number of threads to use for parallel execution +/// - `tol`: The tolerance value for convergence +/// - `use_l2_norm`: Whether to use L2 norm for convergence +/// - `damping_factor`: Probability of likelihood the spread will continue /// -/// Result: +/// # Returns /// -/// * An AlgorithmResult object containing the mapping from node ID to the PageRank score of the node +/// An [AlgorithmResult] object containing the mapping from node ID to the PageRank score of the node /// pub fn unweighted_page_rank( g: &G, diff --git a/raphtory/src/algorithms/community_detection/label_propagation.rs b/raphtory/src/algorithms/community_detection/label_propagation.rs index 9631a9abaf..c656ae6aa3 100644 --- a/raphtory/src/algorithms/community_detection/label_propagation.rs +++ b/raphtory/src/algorithms/community_detection/label_propagation.rs @@ -11,22 +11,22 @@ use crate::{ /// /// # Arguments /// -/// * `g` - A reference to the graph -/// * `seed` - (Optional) Array of 32 bytes of u8 which is set as the rng seed +/// - `g` - A reference to the graph +/// - `seed` - (Optional) Array of 32 bytes of u8 which is set as the rng seed /// -/// Returns: +/// # Returns /// /// A vector of hashsets each containing nodes /// pub fn label_propagation( - graph: &G, + g: &G, seed: Option<[u8; 32]>, ) -> Result>>, &'static str> where G: StaticGraphViewOps, { let mut labels: HashMap, GID> = HashMap::new(); - let nodes = &graph.nodes(); + let nodes = &g.nodes(); for node in nodes.iter() { labels.insert(node, node.id()); } diff --git a/raphtory/src/algorithms/community_detection/louvain.rs b/raphtory/src/algorithms/community_detection/louvain.rs index 18f1dd21e8..cabe170abb 100644 --- a/raphtory/src/algorithms/community_detection/louvain.rs +++ b/raphtory/src/algorithms/community_detection/louvain.rs @@ -9,8 +9,20 @@ use crate::{ use rand::prelude::SliceRandom; use std::collections::HashMap; +/// Louvain algorithm for community detection +/// +/// # Arguments +/// +/// - `g` (GraphView): the graph view +/// - `resolution` (float): the resolution parameter for modularity +/// - `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) +/// +/// # Returns +/// +/// An [AlgorithmResult] containing a mapping of vertices to cluster ID. pub fn louvain<'graph, M: ModularityFunction, G: GraphViewOps<'graph>>( - graph: &G, + g: &G, resolution: f64, weight_prop: Option<&str>, tol: Option, @@ -18,13 +30,13 @@ pub fn louvain<'graph, M: ModularityFunction, G: GraphViewOps<'graph>>( let tol = tol.unwrap_or(1e-8); let mut rng = rand::thread_rng(); let mut modularity_state = M::new( - graph, + g, weight_prop, resolution, - Partition::new_singletons(graph.count_nodes()), + Partition::new_singletons(g.count_nodes()), tol, ); - let mut global_partition: HashMap<_, _> = graph + let mut global_partition: HashMap<_, _> = g .nodes() .iter() .enumerate() @@ -59,7 +71,7 @@ pub fn louvain<'graph, M: ModularityFunction, G: GraphViewOps<'graph>>( *c = partition.com(&VID(*c)).index(); } } - AlgorithmResult::new(graph.clone(), "louvain", "usize", global_partition) + AlgorithmResult::new(g.clone(), "louvain", "usize", global_partition) } #[cfg(test)] diff --git a/raphtory/src/algorithms/components/connected_components.rs b/raphtory/src/algorithms/components/connected_components.rs index 86cd41182c..fcfc701e0f 100644 --- a/raphtory/src/algorithms/components/connected_components.rs +++ b/raphtory/src/algorithms/components/connected_components.rs @@ -27,19 +27,19 @@ struct WccState { /// * `iter_count` - The number of iterations to run /// * `threads` - Number of threads to use /// -/// Returns: +/// # Returns /// -/// An AlgorithmResult containing the mapping from the node to its component ID +/// An [AlgorithmResult] containing the mapping from each node to its component ID /// pub fn weakly_connected_components( - graph: &G, + g: &G, iter_count: usize, threads: Option, ) -> AlgorithmResult where G: StaticGraphViewOps, { - let ctx: Context = graph.into(); + let ctx: Context = g.into(); let step1 = ATask::new(move |vv| { let min_neighbour_id = vv.neighbours().iter().map(|n| n.node.0).min(); let id = vv.node.0; @@ -73,12 +73,11 @@ where vec![Job::read_only(step2)], None, |_, _, _, local: Vec| { - graph - .nodes() + g.nodes() .par_iter() .map(|node| { let VID(id) = node.node; - let comp = graph.node_id(VID(local[id].component)); + let comp = g.node_id(VID(local[id].component)); (id, comp) }) .collect() @@ -89,7 +88,7 @@ where None, ); - AlgorithmResult::new(graph.clone(), "Connected Components", results_type, res) + AlgorithmResult::new(g.clone(), "Connected Components", results_type, res) } #[cfg(test)] diff --git a/raphtory/src/algorithms/components/in_components.rs b/raphtory/src/algorithms/components/in_components.rs index 74833fcc3a..ad153aad6d 100644 --- a/raphtory/src/algorithms/components/in_components.rs +++ b/raphtory/src/algorithms/components/in_components.rs @@ -31,18 +31,18 @@ struct InState { /// /// # Arguments /// -/// * `g` - A reference to the graph -/// * `threads` - Number of threads to use +/// - `g` - A reference to the graph +/// - `threads` - Number of threads to use /// -/// Returns: +/// # Returns /// -/// An AlgorithmResult containing the mapping from node to a vector of node ids (the nodes in component) +/// An [AlgorithmResult] containing the mapping from each node to a vector of node ids (the nodes in component) /// -pub fn in_components(graph: &G, threads: Option) -> AlgorithmResult, Vec> +pub fn in_components(g: &G, threads: Option) -> AlgorithmResult, Vec> where G: StaticGraphViewOps, { - let ctx: Context = graph.into(); + let ctx: Context = g.into(); let step1 = ATask::new(move |vv: &mut EvalNodeView| { let mut in_components = HashSet::new(); let mut to_check_stack = Vec::new(); @@ -76,15 +76,14 @@ where vec![], None, |_, _, _, local: Vec| { - graph - .nodes() + g.nodes() .par_iter() .map(|node| { let VID(id) = node.node; let components = local[id] .in_components .iter() - .map(|vid| graph.node_id(*vid)) + .map(|vid| g.node_id(*vid)) .collect(); (id, components) }) @@ -95,15 +94,15 @@ where None, None, ); - AlgorithmResult::new(graph.clone(), "In Components", results_type, res) + AlgorithmResult::new(g.clone(), "In Components", results_type, res) } /// Computes the in-component of a given node in the graph /// -/// # Arguments +/// # Arguments: /// -/// * `node` - The node whose in-component we wish to calculate +/// - `node` - The node whose in-component we wish to calculate /// -/// Returns: +/// # Returns: /// /// The nodes within the given nodes in-component and their distances from the starting node. /// diff --git a/raphtory/src/algorithms/components/out_components.rs b/raphtory/src/algorithms/components/out_components.rs index ed7d119829..fb3f089b9c 100644 --- a/raphtory/src/algorithms/components/out_components.rs +++ b/raphtory/src/algorithms/components/out_components.rs @@ -30,21 +30,18 @@ struct OutState { /// /// # Arguments /// -/// * `g` - A reference to the graph -/// * `threads` - Number of threads to use +/// - `g` - A reference to the graph +/// - `threads` - Number of threads to use /// -/// Returns: +/// # Returns /// -/// An AlgorithmResult containing the mapping from node to a vector of node ids (the nodes out component) +/// An [AlgorithmResult] containing the mapping from each node to a vector of node ids (the nodes out component) /// -pub fn out_components( - graph: &G, - threads: Option, -) -> AlgorithmResult, Vec> +pub fn out_components(g: &G, threads: Option) -> AlgorithmResult, Vec> where G: StaticGraphViewOps, { - let ctx: Context = graph.into(); + let ctx: Context = g.into(); let step1 = ATask::new(move |vv: &mut EvalNodeView| { let mut out_components = HashSet::new(); let mut to_check_stack = Vec::new(); @@ -78,15 +75,14 @@ where vec![], None, |_, _, _, local: Vec| { - graph - .nodes() + g.nodes() .par_iter() .map(|node| { let VID(id) = node.node; let comps = local[id] .out_components .iter() - .map(|vid| graph.node_id(*vid)) + .map(|vid| g.node_id(*vid)) .collect(); (id, comps) }) @@ -97,16 +93,16 @@ where None, None, ); - AlgorithmResult::new(graph.clone(), "Out Components", results_type, res) + AlgorithmResult::new(g.clone(), "Out Components", results_type, res) } /// Computes the out-component of a given node in the graph /// -/// # Arguments +/// # Arguments: /// -/// * `node` - The node whose out-component we wish to calculate +/// - `node` - The node whose out-component we wish to calculate /// -/// Returns: +/// # Returns: /// /// Nodes in the out-component with their distances from the starting node. /// diff --git a/raphtory/src/algorithms/components/scc.rs b/raphtory/src/algorithms/components/scc.rs index 08449ea055..e5a46f7248 100644 --- a/raphtory/src/algorithms/components/scc.rs +++ b/raphtory/src/algorithms/components/scc.rs @@ -88,10 +88,21 @@ where result } -pub fn strongly_connected_components( - graph: &G, - threads: Option, -) -> AlgorithmResult +/// Computes the strongly connected components of a graph using Tarjan's Strongly Connected Components algorithm +/// +/// Original Paper: +/// https://web.archive.org/web/20170829214726id_/http://www.cs.ucsb.edu/~gilbert/cs240a/old/cs240aSpr2011/slides/TarjanDFS.pdf +/// +/// # Arguments +/// +/// - `g` - A reference to the graph +/// - `threads` - Number of threads to use +/// +/// # Returns +/// +/// An [AlgorithmResult] containing the mapping from each node to its component ID +/// +pub fn strongly_connected_components(g: &G, threads: Option) -> AlgorithmResult where G: StaticGraphViewOps, { @@ -100,19 +111,21 @@ where is_scc_node: bool, } - let ctx: Context = graph.into(); + let ctx: Context = g.into(); let step1 = ATask::new(move |vv: &mut EvalNodeView| { let id = vv.node; let mut out_components = HashSet::new(); let mut to_check_stack = Vec::new(); + // get all neighbours vv.out_neighbours().iter().for_each(|node| { let id = node.node; out_components.insert(id); to_check_stack.push(id); }); - + // iterate over neighbors while let Some(neighbour_id) = to_check_stack.pop() { if let Some(neighbour) = vv.graph().node(neighbour_id) { + // for each neighbour's neighbour, if the node is visitable from itself, it's part of an SCC neighbour.out_neighbours().iter().for_each(|node| { let id = node.node; if !out_components.contains(&id) { @@ -140,7 +153,7 @@ where None, None, ); - let sub_graph = graph.subgraph( + let sub_graph = g.subgraph( local .iter() .enumerate() @@ -165,7 +178,7 @@ where } AlgorithmResult::new( - graph.clone(), + g.clone(), "Strongly-connected Components", results_type, res, diff --git a/raphtory/src/algorithms/cores/k_core.rs b/raphtory/src/algorithms/cores/k_core.rs index e19e30d96c..6e67df2088 100644 --- a/raphtory/src/algorithms/cores/k_core.rs +++ b/raphtory/src/algorithms/cores/k_core.rs @@ -28,20 +28,20 @@ impl Default for KCoreState { /// /// # Arguments /// -/// * `g` - A reference to the graph -/// * `k` - Value of k such that the returned nodes have degree > k (recursively) -/// * `iter_count` - The number of iterations to run -/// * `threads` - number of threads to run on +/// - `g` - A reference to the graph +/// - `k` - Value of k such that the returned nodes have degree > k (recursively) +/// - `iter_count` - The number of iterations to run +/// - `threads` - number of threads to run on /// -/// Returns: +/// # Returns /// /// A hash set of nodes in the k core /// -pub fn k_core_set(graph: &G, k: usize, iter_count: usize, threads: Option) -> HashSet +pub fn k_core_set(g: &G, k: usize, iter_count: usize, threads: Option) -> HashSet where G: StaticGraphViewOps, { - let ctx: Context = graph.into(); + let ctx: Context = g.into(); let step1 = ATask::new(move |vv| { let deg = vv.degree(); @@ -78,8 +78,7 @@ where vec![Job::read_only(step2)], None, |_, _, _, local| { - graph - .nodes() + g.nodes() .iter() .filter(|node| local[node.node.0].alive) .map(|node| node.node) @@ -92,12 +91,12 @@ where ) } -pub fn k_core(graph: &G, k: usize, iter_count: usize, threads: Option) -> NodeSubgraph +pub fn k_core(g: &G, k: usize, iter_count: usize, threads: Option) -> NodeSubgraph where G: StaticGraphViewOps, { - let v_set = k_core_set(graph, k, iter_count, threads); - graph.subgraph(v_set) + let v_set = k_core_set(g, k, iter_count, threads); + g.subgraph(v_set) } #[cfg(test)] diff --git a/raphtory/src/algorithms/dynamics/temporal/epidemics.rs b/raphtory/src/algorithms/dynamics/temporal/epidemics.rs index 8bb9a17ebf..0e91f90428 100644 --- a/raphtory/src/algorithms/dynamics/temporal/epidemics.rs +++ b/raphtory/src/algorithms/dynamics/temporal/epidemics.rs @@ -153,17 +153,25 @@ struct Infection { /// /// # Arguments /// -/// * `graph` - the graph -/// * `recovery_rate` - Optional recovery rate (actual recovery times are sampled from an exponential +/// - `graph` - the graph +/// - `recovery_rate` - Optional recovery rate (actual recovery times are sampled from an exponential /// distribution with this rate). If `None`, nodes never recover (i.e. SI model) -/// * `incubation_rate` - Optional incubation rate (the time nodes take to transition from exposed to infected +/// - `incubation_rate` - Optional incubation rate (the time nodes take to transition from exposed to infected /// is sampled from an exponential distribution with this rate). If `None`, /// the incubation time is `1`, i.e., an infected nodes becomes infectious at /// the next time step -/// * `initial_infection` - time stamp for the initial infection events -/// * `seeds` - Specify how to choose seeds, can be either a list of nodes, `Number(n: usize)` for +/// - `initial_infection` - time stamp for the initial infection events +/// - `seeds` - Specify how to choose seeds, can be either a list of nodes, `Number(n: usize)` for /// sampling a fixed number `n` of seed nodes, or `Probability(p: f64)` in which case a node is initially infected with probability `p`. -/// * `rng` - The random number generator to use +/// - `rng` - The random number generator to use +/// +/// # Returns +/// +/// A [Result] wrapping an [AlgorithmResult] which contains a mapping of each vertex to an [Infected] object, which contains the following structure: +/// - `infected`: the time stamp of the infection event +/// - `active`: the time stamp at which the node actively starts spreading the infection (i.e., the end of the incubation period) +/// - `recovered`: the time stamp at which the node recovered (i.e., stopped spreading the infection) +/// #[allow(non_snake_case)] pub fn temporal_SEIR< G: StaticGraphViewOps, @@ -172,7 +180,7 @@ pub fn temporal_SEIR< R: Rng + ?Sized, T: TryIntoTime, >( - graph: &G, + g: &G, recovery_rate: Option, incubation_rate: Option, infection_prob: P, @@ -184,7 +192,7 @@ where SeedError: From, { let infection_prob = infection_prob.try_into()?; - let seeds = seeds.into_initial_list(graph, rng)?; + let seeds = seeds.into_initial_list(g, rng)?; let recovery_dist = recovery_rate.map(Exp::new).transpose()?; let incubation_dist = incubation_rate.map(Exp::new).transpose()?; let infection_dist = Bernoulli::new(infection_prob.0).unwrap(); @@ -203,7 +211,7 @@ where let Reverse(next_event) = event_queue.pop().unwrap(); if let Entry::Vacant(e) = states.entry(next_event.node) { // node not yet infected - let node = graph.node(next_event.node).unwrap(); + let node = g.node(next_event.node).unwrap(); let incubation_time = incubation_dist .map(|dist| dist.sample(rng) as i64) .unwrap_or(1); @@ -234,7 +242,7 @@ where } } let result = AlgorithmResult::new( - graph.clone(), + g.clone(), "temporal_SEIR", "State", states.into_iter().map(|(k, v)| (k.0, v)).collect(), diff --git a/raphtory/src/algorithms/embeddings/fast_rp.rs b/raphtory/src/algorithms/embeddings/fast_rp.rs index bf50cf422e..53b602b98f 100644 --- a/raphtory/src/algorithms/embeddings/fast_rp.rs +++ b/raphtory/src/algorithms/embeddings/fast_rp.rs @@ -25,19 +25,19 @@ struct FastRPState { /// /// # Arguments /// -/// * `graph` - A reference to the graph -/// * `embedding_dim` - The size of the generated embeddings -/// * `normalization_strength` - The extent to which high-degree vertices should be discounted (range: 1-0) -/// * `iter_weights` - The scalar weights to apply to the results of each iteration -/// * `seed` - The seed for initialisation of random vectors -/// * `threads` - Number of threads to use +/// - `g` - A reference to the graph +/// - `embedding_dim` - The size of the generated embeddings +/// - `normalization_strength` - The extent to which high-degree vertices should be discounted (range: 1-0) +/// - `iter_weights` - The scalar weights to apply to the results of each iteration +/// - `seed` - The seed for initialisation of random vectors +/// - `threads` - Number of threads to use /// -/// # Returns: +/// # Returns /// -/// An AlgorithmResult containing the mapping from the node to its embedding +/// An [AlgorithmResult] containing the mapping from the node to its embedding /// pub fn fast_rp( - graph: &G, + g: &G, embedding_dim: usize, normalization_strength: f64, iter_weights: Vec, @@ -47,8 +47,8 @@ pub fn fast_rp( where G: StaticGraphViewOps, { - let ctx: Context = graph.into(); - let m = graph.count_nodes() as f64; + let ctx: Context = g.into(); + let m = g.count_nodes() as f64; let s = m.sqrt(); let beta = normalization_strength - 1.0; let num_iters = iter_weights.len() - 1; @@ -94,15 +94,13 @@ where }); let mut runner: TaskRunner = TaskRunner::new(ctx); - let results_type = std::any::type_name::(); let res = runner.run( vec![Job::new(step1)], vec![Job::read_only(step2)], None, |_, _, _, local: Vec| { - graph - .nodes() + g.nodes() .par_iter() .map(|node| { let VID(id) = node.node; @@ -118,8 +116,8 @@ where ); // TODO: add flag to optionally normalize results - - AlgorithmResult::new(graph.clone(), "Fast RP", results_type, res) + let results_type = std::any::type_name::>(); + AlgorithmResult::new(g.clone(), "Fast RP", results_type, res) } #[cfg(test)] diff --git a/raphtory/src/algorithms/embeddings/mod.rs b/raphtory/src/algorithms/embeddings/mod.rs index 98e4064851..dc59ca1e59 100644 --- a/raphtory/src/algorithms/embeddings/mod.rs +++ b/raphtory/src/algorithms/embeddings/mod.rs @@ -1,3 +1 @@ -mod fast_rp; - -pub use fast_rp::fast_rp; +pub mod fast_rp; diff --git a/raphtory/src/algorithms/layout/cohesive_fruchterman_reingold.rs b/raphtory/src/algorithms/layout/cohesive_fruchterman_reingold.rs index eb64aec7f1..dc73023343 100644 --- a/raphtory/src/algorithms/layout/cohesive_fruchterman_reingold.rs +++ b/raphtory/src/algorithms/layout/cohesive_fruchterman_reingold.rs @@ -2,24 +2,39 @@ use raphtory_api::core::entities::GID; use crate::{ algorithms::{ - components::weakly_connected_components, - layout::{fruchterman_reingold::fruchterman_reingold_unbounded, NodeVectors}, + algorithm_result::AlgorithmResult, components::weakly_connected_components, + layout::fruchterman_reingold::fruchterman_reingold_unbounded, }, db::{api::view::MaterializedGraph, graph::node::NodeView}, prelude::{AdditionOps, GraphViewOps, NodeViewOps, NO_PROPS}, }; +use ordered_float::OrderedFloat; use std::collections::HashMap; /// Cohesive version of `fruchterman_reingold` that adds virtual edges between isolated nodes +/// +/// # Arguments +/// +/// - `g` - A reference to the graph +/// - `iter_count` - The number of iterations to run +/// - `scale`: Global scaling factor to control the overall spread of the graph +/// - `node_start_size`: Initial size or movement range for nodes +/// - `cooloff_factor`: Factor to reduce node movement in later iterations, helping stabilize the layout +/// - `dt`: Time step or movement factor in each iteration +/// +/// # Returns +/// +/// An [AlgorithmResult] containing a mapping between vertices and a [Vec2] of coordinates. +/// pub fn cohesive_fruchterman_reingold<'graph, G: GraphViewOps<'graph>>( - graph: &'graph G, - iterations: u64, + g: &'graph G, + iter_count: u64, scale: f32, node_start_size: f32, cooloff_factor: f32, dt: f32, -) -> NodeVectors { - let virtual_graph = graph.materialize().unwrap(); +) -> AlgorithmResult; 2]> { + let virtual_graph = g.materialize().unwrap(); let transform_map = |input_map: HashMap| -> HashMap>> { let mut output_map: HashMap>> = HashMap::new(); @@ -61,12 +76,5 @@ pub fn cohesive_fruchterman_reingold<'graph, G: GraphViewOps<'graph>>( } } - fruchterman_reingold_unbounded( - &virtual_graph, - iterations, - scale, - node_start_size, - cooloff_factor, - dt, - ) + fruchterman_reingold_unbounded(g, iter_count, scale, node_start_size, cooloff_factor, dt) } diff --git a/raphtory/src/algorithms/layout/fruchterman_reingold.rs b/raphtory/src/algorithms/layout/fruchterman_reingold.rs index ce0f4757d0..1eb36a4979 100644 --- a/raphtory/src/algorithms/layout/fruchterman_reingold.rs +++ b/raphtory/src/algorithms/layout/fruchterman_reingold.rs @@ -1,35 +1,38 @@ use crate::{ - algorithms::layout::NodeVectors, + algorithms::{algorithm_result::AlgorithmResult, layout::NodeVectors}, prelude::{GraphViewOps, NodeViewOps}, }; use glam::Vec2; +use ordered_float::OrderedFloat; use quad_rand::RandomRange; -use raphtory_api::core::entities::GID; +use raphtory_api::core::entities::{GID, VID}; /// Return the position of the nodes after running Fruchterman Reingold algorithm on the `graph` pub fn fruchterman_reingold_unbounded<'graph, G: GraphViewOps<'graph>>( - graph: &'graph G, - iterations: u64, + g: &'graph G, + iter_count: u64, scale: f32, node_start_size: f32, cooloff_factor: f32, dt: f32, -) -> NodeVectors { - let mut positions = init_positions(graph, node_start_size); - let mut velocities = init_velocities(graph); - - for _index in 0..iterations { - positions = update_positions( - &positions, - &mut velocities, - graph, - scale, - cooloff_factor, - dt, - ); +) -> AlgorithmResult; 2]> { + let mut positions = init_positions(g, node_start_size); + let mut velocities = init_velocities(g); + + for _index in 0..iter_count { + positions = update_positions(&positions, &mut velocities, g, scale, cooloff_factor, dt); } - positions + let res = positions + .into_iter() + .map(move |(id, vec)| { + let VID(id) = g.node(id).unwrap().node; + (id, [vec.x, vec.y]) + }) + .collect(); + + let results_type = std::any::type_name::(); + AlgorithmResult::new(g.clone(), "Fruchterman-Reingold", results_type, res) } fn update_positions<'graph, G: GraphViewOps<'graph>>( diff --git a/raphtory/src/algorithms/metrics/balance.rs b/raphtory/src/algorithms/metrics/balance.rs index 1afd0a395b..436cefef26 100644 --- a/raphtory/src/algorithms/metrics/balance.rs +++ b/raphtory/src/algorithms/metrics/balance.rs @@ -36,13 +36,13 @@ use ordered_float::OrderedFloat; /// the weight is treated as positive. /// - In all other cases, the weight contribution is zero. /// -/// Arguments: +/// # Arguments /// - `v`: The node for which we want to compute the weight sum. /// - `name`: The name of the property which holds the edge weight. /// - `direction`: Specifies the direction of edges to consider (`IN`, `OUT`, or `BOTH`). /// -/// Returns: -/// Returns a `f64` which is the net sum of weights for the node considering the specified direction. +/// # Returns +/// An `f64` which is the net sum of weights for the node considering the specified direction. fn balance_per_node<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>, CS: ComputeState>( v: &EvalNodeView<'graph, '_, G, (), GH, CS>, name: &str, @@ -89,14 +89,14 @@ fn balance_per_node<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>, C /// Incoming edges have a positive sum and outgoing edges have a negative sum /// It uses a compute context and tasks to achieve this. /// -/// Arguments: +/// # Arguments /// - `graph`: The graph on which the operation is to be performed. /// - `name`: The name of the property which holds the edge weight. /// - `threads`: An optional parameter to specify the number of threads to use. /// If `None`, it defaults to a suitable number. /// -/// Returns: -/// Returns an `AlgorithmResult` which maps each node to its corresponding net weight sum. +/// # Returns +/// An [AlgorithmResult] which maps each node to its corresponding net weight sum. pub fn balance( graph: &G, name: String, diff --git a/raphtory/src/algorithms/metrics/clustering_coefficient.rs b/raphtory/src/algorithms/metrics/clustering_coefficient.rs index bc8ec458d6..e3a4adf3b6 100644 --- a/raphtory/src/algorithms/metrics/clustering_coefficient.rs +++ b/raphtory/src/algorithms/metrics/clustering_coefficient.rs @@ -8,9 +8,9 @@ use crate::{ /// /// # Arguments /// -/// * `g` - A reference to the graph +/// - `g` - A reference to the graph /// -/// Returns: +/// # Returns /// /// The global clustering coefficient of the graph /// diff --git a/raphtory/src/algorithms/metrics/degree.rs b/raphtory/src/algorithms/metrics/degree.rs index 7fa913716b..422bd7edf6 100644 --- a/raphtory/src/algorithms/metrics/degree.rs +++ b/raphtory/src/algorithms/metrics/degree.rs @@ -46,7 +46,7 @@ use crate::{db::api::view::*, prelude::*}; use rayon::prelude::*; -/// The maximum degree of any node in the graph +/// The largest degree pub fn max_degree<'graph, G: GraphViewOps<'graph>>(graph: &G) -> usize { graph.nodes().degree().max().unwrap_or(0) } diff --git a/raphtory/src/algorithms/metrics/directed_graph_density.rs b/raphtory/src/algorithms/metrics/directed_graph_density.rs index 65dde18118..2da3508e35 100644 --- a/raphtory/src/algorithms/metrics/directed_graph_density.rs +++ b/raphtory/src/algorithms/metrics/directed_graph_density.rs @@ -33,9 +33,18 @@ //! use crate::db::api::view::*; -/// Measures how dense or sparse a graph is -pub fn directed_graph_density<'graph, G: GraphViewOps<'graph>>(graph: &G) -> f32 { - graph.count_edges() as f32 / (graph.count_nodes() as f32 * (graph.count_nodes() as f32 - 1.0)) +/// Graph density - measures how dense or sparse a graph is. +/// +/// The ratio of the number of directed edges in the graph to the total number of possible directed +/// edges (given by N * (N-1) where N is the number of nodes). +/// +/// # Arguments +/// - `g`: a directed Raphtory graph +/// +/// # Returns +/// Directed graph density of G. +pub fn directed_graph_density<'graph, G: GraphViewOps<'graph>>(graph: &G) -> f64 { + graph.count_edges() as f64 / (graph.count_nodes() as f64 * (graph.count_nodes() as f64 - 1.0)) } #[cfg(test)] diff --git a/raphtory/src/algorithms/metrics/local_clustering_coefficient.rs b/raphtory/src/algorithms/metrics/local_clustering_coefficient.rs index a010c774e3..8e00a0db9d 100644 --- a/raphtory/src/algorithms/metrics/local_clustering_coefficient.rs +++ b/raphtory/src/algorithms/metrics/local_clustering_coefficient.rs @@ -53,16 +53,25 @@ use crate::{ core::entities::nodes::node_ref::AsNodeRef, db::api::view::*, }; -/// measures the degree to which nodes in a graph tend to cluster together +/// Local clustering coefficient - measures the degree to which nodes in a graph tend to cluster together. +/// +/// The proportion of pairs of neighbours of a node who are themselves connected. +/// +/// # Arguments +/// - `graph`: Raphtory graph, can be directed or undirected but will be treated as undirected. +/// - `v`: node id or name +/// +/// # Returns +/// the local clustering coefficient of node v in g. pub fn local_clustering_coefficient( graph: &G, v: V, -) -> Option { +) -> Option { let v = v.as_node_ref(); if let Some(node) = graph.node(v) { if let Some(triangle_count) = local_triangle_count(graph, v) { - let triangle_count = triangle_count as f32; - let degree = node.degree() as f32; + let triangle_count = triangle_count as f64; + let degree = node.degree() as f64; if degree > 1.0 { Some((2.0 * triangle_count) / (degree * (degree - 1.0))) } else { @@ -105,7 +114,7 @@ mod clustering_coefficient_tests { } test_storage!(&graph, |graph| { - let expected = vec![0.33333334, 1.0, 1.0, 0.0, 0.0]; + let expected = vec![0.3333333333333333, 1.0, 1.0, 0.0, 0.0]; let windowed_graph = graph.window(0, 7); let actual = (1..=5) .map(|v| local_clustering_coefficient(&windowed_graph, v).unwrap()) diff --git a/raphtory/src/algorithms/metrics/reciprocity.rs b/raphtory/src/algorithms/metrics/reciprocity.rs index b6aa71e3d0..287576a8ed 100644 --- a/raphtory/src/algorithms/metrics/reciprocity.rs +++ b/raphtory/src/algorithms/metrics/reciprocity.rs @@ -90,7 +90,16 @@ fn get_reciprocal_edge_count< (out_neighbours.len(), in_neighbours.len(), out_inter_in) } -/// returns the global reciprocity of the entire graph +/// Reciprocity - measure of the symmetry of relationships in a graph, the global reciprocity of +/// the entire graph. +/// This calculates the number of reciprocal connections (edges that go in both directions) in a +/// graph and normalizes it by the total number of directed edges. +/// +/// # Arguments +/// - `g`: a directed Raphtory graph +/// +/// # Returns +/// reciprocity of the graph between 0 and 1. pub fn global_reciprocity(g: &G, threads: Option) -> f64 { let mut ctx: Context = g.into(); @@ -123,8 +132,16 @@ pub fn global_reciprocity(g: &G, threads: Option) ) } -/// returns the reciprocity of every node in the graph as a tuple of -/// vector id and the reciprocity +/// Local reciprocity - measure of the symmetry of relationships associated with a node +/// +/// This measures the proportion of a node's outgoing edges which are reciprocated with an incoming edge. +/// +/// # Arguments +/// - `g` : a directed Raphtory graph +/// +/// # Returns +/// [AlgorithmResult] with string keys and float values mapping each node name to its reciprocity value. +/// pub fn all_local_reciprocity( g: &G, threads: Option, diff --git a/raphtory/src/algorithms/motifs/global_temporal_three_node_motifs.rs b/raphtory/src/algorithms/motifs/global_temporal_three_node_motifs.rs index 4cda37e7e4..5c492bcf48 100644 --- a/raphtory/src/algorithms/motifs/global_temporal_three_node_motifs.rs +++ b/raphtory/src/algorithms/motifs/global_temporal_three_node_motifs.rs @@ -251,7 +251,15 @@ where } /////////////////////////////////////////////////////// - +/// Computes the global counts of three-edge up-to-three node temporal motifs for a range of timescales. See `global_temporal_three_node_motif` for an interpretation of each row returned. +/// +/// # Arguments +/// - `g`: A directed raphtory graph +/// - `deltas`: A list of delta values to use. +/// - `threads`: Number of threads to use +/// +/// # Returns +/// A list of 40d arrays, each array is the motif count for a particular value of delta, returned in the order that the deltas were given as input. pub fn temporal_three_node_motif_multi( g: &G, deltas: Vec, @@ -303,6 +311,19 @@ where ) } +/// Computes the number of three edge, up-to-three node delta-temporal motifs in the graph, using the algorithm of Paranjape et al, Motifs in Temporal Networks (2017). +/// +/// # Arguments +/// - `g`: A directed raphtory graph +/// - `delta`: Maximum time difference between the first and last edge of the motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise milliseconds should be used (if edge times were given as string) +/// - `threads`: Number of threads to use +/// +/// # Returns +/// A 40 dimensional array with the counts of each motif, given in the same order as described above. Note that the two-node motif counts are symmetrical so it may be more useful just to consider the first four elements. +/// +/// # Notes +/// This is achieved by calling the local motif counting algorithm, summing the resulting arrays and dealing with overcounted motifs: the triangles (by dividing each motif count by three) and two-node motifs (dividing by two). +/// pub fn global_temporal_three_node_motif( graph: &G, delta: i64, diff --git a/raphtory/src/algorithms/motifs/local_temporal_three_node_motifs.rs b/raphtory/src/algorithms/motifs/local_temporal_three_node_motifs.rs index 499ffc358d..b9b310b86e 100644 --- a/raphtory/src/algorithms/motifs/local_temporal_three_node_motifs.rs +++ b/raphtory/src/algorithms/motifs/local_temporal_three_node_motifs.rs @@ -14,6 +14,7 @@ use crate::{ }, }; +use crate::algorithms::algorithm_result::AlgorithmResult; use itertools::{enumerate, Itertools}; use num_traits::Zero; use raphtory_api::core::entities::VID; @@ -319,12 +320,23 @@ where } /////////////////////////////////////////////////////// - +/// Computes the number of each type of motif that each node participates in. See global_temporal_three_node_motifs for a summary of the motifs involved. +/// +/// # Arguments +/// - `g`: A directed raphtory graph +/// - `delta`: Maximum time difference between the first and last edge of the motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise milliseconds should be used (if edge times were given as string) +/// +/// # Returns +/// An [AlgorithmResult] mapping each node to a 40d array of motif counts as values (in the same order as the global motif counts) with the number of each motif that node participates in. +/// +/// # Notes +/// For this local count, a node is counted as participating in a motif in the following way. For star motifs, only the centre node counts +/// the motif. For two node motifs, both constituent nodes count the motif. For triangles, all three constituent nodes count the motif. pub fn temporal_three_node_motif( g: &G, deltas: Vec, threads: Option, -) -> HashMap>> +) -> AlgorithmResult> where G: StaticGraphViewOps, { @@ -351,45 +363,56 @@ where let mut runner: TaskRunner = TaskRunner::new(ctx); - runner.run( - vec![Job::new(star_motif_step)], - vec![], - None, - |_, _, _els, local| { - let mut motifs = HashMap::new(); - for (vref, mc) in enumerate(local) { - if mc.two_nodes.is_empty() || mc.star_nodes.is_empty() { - continue; + let result = runner + .run( + vec![Job::new(star_motif_step)], + vec![], + None, + |_, _, _els, local| { + let mut motifs = HashMap::new(); + for (vref, mc) in enumerate(local) { + if mc.two_nodes.is_empty() || mc.star_nodes.is_empty() { + continue; + } + let v_gid = g.node_name(vref.into()); + let triangles = triadic_motifs + .get(&v_gid) + .cloned() + .unwrap_or_else(|| vec![[0usize; 8]; delta_len]); + let run_counts = (0..delta_len) + .map(|i| { + let two_nodes = mc.two_nodes[i].to_vec(); + let tmp_stars = mc.star_nodes[i].to_vec(); + let stars: Vec = tmp_stars + .iter() + .zip(two_nodes.iter().cycle().take(24)) + .map(|(&x1, &x2)| x1 - x2) + .collect(); + let mut final_cts = Vec::new(); + final_cts.extend(stars.into_iter()); + final_cts.extend(two_nodes.into_iter()); + final_cts.extend(triangles[i].into_iter()); + final_cts + }) + .collect::>>(); + motifs.insert(vref, run_counts); } - let v_gid = g.node_name(vref.into()); - let triangles = triadic_motifs - .get(&v_gid) - .cloned() - .unwrap_or_else(|| vec![[0usize; 8]; delta_len]); - let run_counts = (0..delta_len) - .map(|i| { - let two_nodes = mc.two_nodes[i].to_vec(); - let tmp_stars = mc.star_nodes[i].to_vec(); - let stars: Vec = tmp_stars - .iter() - .zip(two_nodes.iter().cycle().take(24)) - .map(|(&x1, &x2)| x1 - x2) - .collect(); - let mut final_cts = Vec::new(); - final_cts.extend(stars.into_iter()); - final_cts.extend(two_nodes.into_iter()); - final_cts.extend(triangles[i].into_iter()); - final_cts - }) - .collect::>>(); - motifs.insert(v_gid.clone(), run_counts); - } - motifs - }, - threads, - 1, - None, - None, + motifs + }, + threads, + 1, + None, + None, + ) + .into_iter() + .map(|(k, v)| (k, v[0].clone())) + .collect::>>(); + let results_type = std::any::type_name::>(); + AlgorithmResult::new( + g.clone(), + "Temporal Three Node Motifs", + results_type, + result, ) } @@ -451,75 +474,91 @@ mod motifs_test { global_debug_logger(); let ij_kj_ik = vec![(1, 1, 2), (2, 3, 2), (3, 1, 3)]; let g = load_graph(ij_kj_ik); - let mc = temporal_three_node_motif(&g, vec![3], None) - .iter() - .map(|(k, v)| (k.clone(), v[0].clone())) - .into_iter() - .collect::>>(); - info!("{:?}", mc.get("3").unwrap()); + let mc = temporal_three_node_motif(&g, vec![3], None); + assert_eq!( + *mc.get(&3).unwrap(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 + ] + ); let ij_ki_jk = vec![(1, 1, 2), (2, 3, 1), (3, 2, 3)]; let g = load_graph(ij_ki_jk); - let mc = temporal_three_node_motif(&g, vec![3], None) - .iter() - .map(|(k, v)| (k.clone(), v[0].clone())) - .into_iter() - .collect::>>(); - info!("{:?}", mc.get("3").unwrap()); + let mc = temporal_three_node_motif(&g, vec![3], None); + assert_eq!( + *mc.get(&3).unwrap(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 + ] + ); let ij_jk_ik = vec![(1, 1, 2), (2, 2, 3), (3, 1, 3)]; let g = load_graph(ij_jk_ik); - let mc = temporal_three_node_motif(&g, vec![3], None) - .iter() - .map(|(k, v)| (k.clone(), v[0].clone())) - .into_iter() - .collect::>>(); - info!("{:?}", mc.get("3").unwrap()); + let mc = temporal_three_node_motif(&g, vec![3], None); + assert_eq!( + *mc.get(&3).unwrap(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 + ] + ); let ij_ik_jk = vec![(1, 1, 2), (2, 1, 3), (3, 2, 3)]; let g = load_graph(ij_ik_jk); - let mc = temporal_three_node_motif(&g, vec![3], None) - .iter() - .map(|(k, v)| (k.clone(), v[0].clone())) - .into_iter() - .collect::>>(); - info!("{:?}", mc.get("3").unwrap()); + let mc = temporal_three_node_motif(&g, vec![3], None); + assert_eq!( + *mc.get(&3).unwrap(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 + ] + ); let ij_kj_ki = vec![(1, 1, 2), (2, 3, 2), (3, 3, 1)]; let g = load_graph(ij_kj_ki); - let mc = temporal_three_node_motif(&g, vec![3], None) - .iter() - .map(|(k, v)| (k.clone(), v[0].clone())) - .into_iter() - .collect::>>(); - info!("{:?}", mc.get("3").unwrap()); + let mc = temporal_three_node_motif(&g, vec![3], None); + assert_eq!( + *mc.get(&3).unwrap(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 + ] + ); let ij_ki_kj = vec![(1, 1, 2), (2, 3, 1), (3, 3, 2)]; let g = load_graph(ij_ki_kj); - let mc = temporal_three_node_motif(&g, vec![3], None) - .iter() - .map(|(k, v)| (k.clone(), v[0].clone())) - .into_iter() - .collect::>>(); - info!("{:?}", mc.get("3").unwrap()); + let mc = temporal_three_node_motif(&g, vec![3], None); + assert_eq!( + *mc.get(&3).unwrap(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 + ] + ); let ij_jk_ki = vec![(1, 1, 2), (2, 2, 3), (3, 3, 1)]; let g = load_graph(ij_jk_ki); - let mc = temporal_three_node_motif(&g, vec![3], None) - .iter() - .map(|(k, v)| (k.clone(), v[0].clone())) - .into_iter() - .collect::>>(); - info!("{:?}", mc.get("3").unwrap()); + let mc = temporal_three_node_motif(&g, vec![3], None); + assert_eq!( + *mc.get(&3).unwrap(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 + ] + ); let ij_ik_kj = vec![(1, 1, 2), (2, 1, 3), (3, 3, 2)]; let g = load_graph(ij_ik_kj); - let mc = temporal_three_node_motif(&g, vec![3], None) - .iter() - .map(|(k, v)| (k.clone(), v[0].clone())) - .into_iter() - .collect::>>(); - info!("{:?}", mc.get("3").unwrap()); + let mc = temporal_three_node_motif(&g, vec![3], None); + assert_eq!( + *mc.get(&3).unwrap(), + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + ] + ); } #[test] @@ -527,12 +566,8 @@ mod motifs_test { let graph = load_sample_graph(); test_storage!(&graph, |graph| { - let binding = temporal_three_node_motif(graph, Vec::from([10]), None); - let actual = binding - .iter() - .map(|(k, v)| (k, v[0].clone())) - .into_iter() - .collect::>>(); + let actual = + temporal_three_node_motif(graph, Vec::from([10]), None).get_all_with_names(); let expected: HashMap> = HashMap::from([ ( @@ -630,12 +665,8 @@ mod motifs_test { let g_windowed = g.before(11).after(0); info! {"windowed graph has {:?} vertices",g_windowed.count_nodes()} - let binding = temporal_three_node_motif(&g_windowed, Vec::from([10]), None); - let actual = binding - .iter() - .map(|(k, v)| (k, v[0].clone())) - .into_iter() - .collect::>>(); + let actual = + temporal_three_node_motif(&g_windowed, Vec::from([10]), None).get_all_with_names(); let expected: HashMap> = HashMap::from([ ( diff --git a/raphtory/src/algorithms/motifs/local_triangle_count.rs b/raphtory/src/algorithms/motifs/local_triangle_count.rs index bb38ff09d5..645a6aecfc 100644 --- a/raphtory/src/algorithms/motifs/local_triangle_count.rs +++ b/raphtory/src/algorithms/motifs/local_triangle_count.rs @@ -40,7 +40,17 @@ use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::*}; use itertools::Itertools; -/// calculates the number of triangles (a cycle of length 3) for a node. +/// Local triangle count - calculates the number of triangles (a cycle of length 3) a node participates in. +/// +/// This function returns the number of pairs of neighbours of a given node which are themselves connected. +/// +/// # Arguments +/// - `g`: Raphtory graph, this can be directed or undirected but will be treated as undirected +/// - `v`: node id or name +/// +/// # Returns +/// Number of triangles associated with node v +/// pub fn local_triangle_count(graph: &G, v: V) -> Option { if let Some(node) = (&graph).node(v) { if node.degree() >= 2 { diff --git a/raphtory/src/algorithms/motifs/temporal_rich_club_coefficient.rs b/raphtory/src/algorithms/motifs/temporal_rich_club_coefficient.rs index 4c5103c322..19d43527b8 100644 --- a/raphtory/src/algorithms/motifs/temporal_rich_club_coefficient.rs +++ b/raphtory/src/algorithms/motifs/temporal_rich_club_coefficient.rs @@ -36,8 +36,28 @@ where } } +/// Temporal rich club coefficient +/// +/// The traditional rich-club coefficient in a static undirected graph measures the density of connections between the highest +/// degree nodes. It takes a single parameter k, creates a subgraph of the nodes of degree greater than or equal to k, and +/// returns the density of this subgraph. +/// +/// In a temporal graph taking the form of a sequence of static snapshots, the temporal rich club coefficient takes a parameter k +/// and a window size delta (both positive integers). It measures the maximal density of the highest connected nodes (of degree +/// greater than or equal to k in the aggregate graph) that persists at least a delta number of consecutive snapshots. For an in-depth +/// definition and usage example, please read to the following paper: Pedreschi, N., Battaglia, D., & Barrat, A. (2022). The temporal +/// rich club phenomenon. Nature Physics, 18(8), 931-938. +/// +/// # Arguments +/// - `graph`: the aggregate graph +/// - `views`: sequence of graphs (can be obtained by calling g.rolling(..) on an aggregate graph g) +/// - `k`: min degree of nodes to include in rich-club +/// - `window_size`: the number of consecutive snapshots over which the edges should persist +/// +/// # Returns +/// the rich-club coefficient as a float. pub fn temporal_rich_club_coefficient<'a, I, G1, G2>( - agg_graph: G2, + graph: &G2, views: I, k: usize, window_size: usize, @@ -48,7 +68,7 @@ where G2: GraphViewOps<'a>, { // Extract the set of nodes with degree greater than or equal to k - let s_k: HashSet = agg_graph + let s_k: HashSet = graph .nodes() .into_iter() .filter(|v| v.degree() >= k) @@ -170,9 +190,9 @@ mod rich_club_test { let g = load_sample_graph(); let g_rolling = g.rolling(1, Some(1)).unwrap(); - let rc_coef_1 = temporal_rich_club_coefficient(g.clone(), g_rolling.clone(), 3, 1); - let rc_coef_3 = temporal_rich_club_coefficient(g.clone(), g_rolling.clone(), 3, 3); - let rc_coef_5 = temporal_rich_club_coefficient(g.clone(), g_rolling.clone(), 3, 5); + let rc_coef_1 = temporal_rich_club_coefficient(&g, g_rolling.clone(), 3, 1); + let rc_coef_3 = temporal_rich_club_coefficient(&g, g_rolling.clone(), 3, 3); + let rc_coef_5 = temporal_rich_club_coefficient(&g, g_rolling.clone(), 3, 5); assert_eq_f64(Some(rc_coef_1), Some(1.0), 3); assert_eq_f64(Some(rc_coef_3), Some(0.66666), 3); assert_eq_f64(Some(rc_coef_5), Some(0.5), 3); diff --git a/raphtory/src/algorithms/pathing/dijkstra.rs b/raphtory/src/algorithms/pathing/dijkstra.rs index e61c3bc56d..bc4334ded8 100644 --- a/raphtory/src/algorithms/pathing/dijkstra.rs +++ b/raphtory/src/algorithms/pathing/dijkstra.rs @@ -1,14 +1,15 @@ -use std::{ - cmp::Ordering, - collections::{BinaryHeap, HashMap, HashSet}, -}; - -/// Dijkstra's algorithm -use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; use crate::{ + algorithms::algorithm_result::AlgorithmResult, core::{Direction, PropType}, prelude::{EdgeViewOps, NodeViewOps, Prop}, }; +/// Dijkstra's algorithm +use crate::{core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps}; +use ordered_float::OrderedFloat; +use std::{ + cmp::Ordering, + collections::{BinaryHeap, HashMap, HashSet}, +}; /// A state in the Dijkstra algorithm with a cost and a node name. #[derive(PartialEq)] @@ -47,31 +48,31 @@ impl PartialOrd for State { /// the total cost and a vector of nodes representing the shortest path. /// pub fn dijkstra_single_source_shortest_paths( - graph: &G, + g: &G, source: T, targets: Vec, weight: Option, direction: Direction, -) -> Result)>, &'static str> { - let source_node = match graph.node(source) { +) -> Result), (OrderedFloat, Vec)>, &'static str> +{ + let source_node = match g.node(source) { Some(src) => src, None => return Err("Source node not found"), }; let mut weight_type = Some(PropType::U8); if weight.is_some() { - weight_type = match graph + weight_type = match g .edge_meta() .temporal_prop_meta() .get_id(&weight.clone().unwrap()) { - Some(weight_id) => graph.edge_meta().temporal_prop_meta().get_dtype(weight_id), - None => graph + Some(weight_id) => g.edge_meta().temporal_prop_meta().get_dtype(weight_id), + None => g .edge_meta() .const_prop_meta() .get_id(&weight.clone().unwrap()) .map(|weight_id| { - graph - .edge_meta() + g.edge_meta() .const_prop_meta() .get_dtype(weight_id) .unwrap() @@ -84,8 +85,8 @@ pub fn dijkstra_single_source_shortest_paths = targets .iter() - .filter_map(|p| match graph.has_node(p) { - true => Some(graph.node(p)?.name()), + .filter_map(|p| match g.has_node(p) { + true => Some(g.node(p)?.name()), false => None, }) .collect(); @@ -141,7 +142,7 @@ pub fn dijkstra_single_source_shortest_paths = HashMap::new(); let mut predecessor: HashMap = HashMap::new(); let mut visited: HashSet = HashSet::new(); - let mut paths: HashMap)> = HashMap::new(); + let mut paths: HashMap)> = HashMap::new(); dist.insert(source_node.name(), cost_val.clone()); @@ -150,7 +151,8 @@ pub fn dijkstra_single_source_shortest_paths graph.node(node_name.clone()).unwrap().out_edges(), - Direction::IN => graph.node(node_name.clone()).unwrap().in_edges(), - Direction::BOTH => graph.node(node_name.clone()).unwrap().edges(), + Direction::OUT => g.node(node_name.clone()).unwrap().out_edges(), + Direction::IN => g.node(node_name.clone()).unwrap().in_edges(), + Direction::BOTH => g.node(node_name.clone()).unwrap().edges(), }; // Replace this loop with your actual logic to iterate over the outgoing edges @@ -207,7 +209,13 @@ pub fn dijkstra_single_source_shortest_paths)>(); + Ok(AlgorithmResult::new( + g.clone(), + "Dijkstra Single Source Shortest Path", + results_type, + paths, + )) } #[cfg(test)] @@ -257,10 +265,10 @@ mod dijkstra_tests { let results = results.unwrap(); - assert_eq!(results.get("D").unwrap().0, Prop::F32(7.0f32)); + assert_eq!(results.get("D").unwrap().0, 7.0f64); assert_eq!(results.get("D").unwrap().1, vec!["A", "C", "D"]); - assert_eq!(results.get("F").unwrap().0, Prop::F32(8.0f32)); + assert_eq!(results.get("F").unwrap().0, 8.0f64); assert_eq!(results.get("F").unwrap().1, vec!["A", "C", "E", "F"]); let targets: Vec<&str> = vec!["D", "E", "F"]; @@ -272,9 +280,9 @@ mod dijkstra_tests { Direction::OUT, ); let results = results.unwrap(); - assert_eq!(results.get("D").unwrap().0, Prop::F32(5.0f32)); - assert_eq!(results.get("E").unwrap().0, Prop::F32(3.0f32)); - assert_eq!(results.get("F").unwrap().0, Prop::F32(6.0f32)); + assert_eq!(results.get("D").unwrap().0, 5.0f64); + assert_eq!(results.get("E").unwrap().0, 3.0f64); + assert_eq!(results.get("F").unwrap().0, 6.0f64); assert_eq!(results.get("D").unwrap().1, vec!["B", "C", "D"]); assert_eq!(results.get("E").unwrap().1, vec!["B", "C", "E"]); assert_eq!(results.get("F").unwrap().1, vec!["B", "C", "E", "F"]); @@ -324,10 +332,10 @@ mod dijkstra_tests { Direction::OUT, ); let results = results.unwrap(); - assert_eq!(results.get("4").unwrap().0, Prop::U64(7u64)); + assert_eq!(results.get("4").unwrap().0, 7f64); assert_eq!(results.get("4").unwrap().1, vec!["1", "3", "4"]); - assert_eq!(results.get("6").unwrap().0, Prop::U64(8u64)); + assert_eq!(results.get("6").unwrap().0, 8f64); assert_eq!(results.get("6").unwrap().1, vec!["1", "3", "5", "6"]); let targets = vec![4, 5, 6]; @@ -339,9 +347,9 @@ mod dijkstra_tests { Direction::OUT, ); let results = results.unwrap(); - assert_eq!(results.get("4").unwrap().0, Prop::U64(5u64)); - assert_eq!(results.get("5").unwrap().0, Prop::U64(3u64)); - assert_eq!(results.get("6").unwrap().0, Prop::U64(6u64)); + assert_eq!(results.get("4").unwrap().0, 5f64); + assert_eq!(results.get("5").unwrap().0, 3f64); + assert_eq!(results.get("6").unwrap().0, 6f64); assert_eq!(results.get("4").unwrap().1, vec!["2", "3", "4"]); assert_eq!(results.get("5").unwrap().1, vec!["2", "3", "5"]); assert_eq!(results.get("6").unwrap().1, vec!["2", "3", "5", "6"]); @@ -377,10 +385,10 @@ mod dijkstra_tests { Direction::OUT, ); let results = results.unwrap(); - assert_eq!(results.get("D").unwrap().0, Prop::U64(7u64)); + assert_eq!(results.get("D").unwrap().0, 7f64); assert_eq!(results.get("D").unwrap().1, vec!["A", "C", "D"]); - assert_eq!(results.get("F").unwrap().0, Prop::U64(8u64)); + assert_eq!(results.get("F").unwrap().0, 8f64); assert_eq!(results.get("F").unwrap().1, vec!["A", "C", "E", "F"]); let targets: Vec<&str> = vec!["D", "E", "F"]; @@ -392,9 +400,9 @@ mod dijkstra_tests { Direction::OUT, ); let results = results.unwrap(); - assert_eq!(results.get("D").unwrap().0, Prop::U64(5u64)); - assert_eq!(results.get("E").unwrap().0, Prop::U64(3u64)); - assert_eq!(results.get("F").unwrap().0, Prop::U64(6u64)); + assert_eq!(results.get("D").unwrap().0, 5f64); + assert_eq!(results.get("E").unwrap().0, 3f64); + assert_eq!(results.get("F").unwrap().0, 6f64); assert_eq!(results.get("D").unwrap().1, vec!["B", "C", "D"]); assert_eq!(results.get("E").unwrap().1, vec!["B", "C", "E"]); assert_eq!(results.get("F").unwrap().1, vec!["B", "C", "E", "F"]); @@ -426,7 +434,7 @@ mod dijkstra_tests { ); let results = results.unwrap(); - assert_eq!(results.get("D").unwrap().0, Prop::U64(7u64)); + assert_eq!(results.get("D").unwrap().0, 7f64); assert_eq!(results.get("D").unwrap().1, vec!["A", "C", "D"]); }); } diff --git a/raphtory/src/algorithms/projections/temporal_bipartite_projection.rs b/raphtory/src/algorithms/projections/temporal_bipartite_projection.rs index c4f436832d..ea73be21f2 100644 --- a/raphtory/src/algorithms/projections/temporal_bipartite_projection.rs +++ b/raphtory/src/algorithms/projections/temporal_bipartite_projection.rs @@ -16,6 +16,16 @@ struct Visitor { time: i64, } +/// Projects a temporal bipartite graph into an undirected temporal graph over the pivot node type. Let G be a bipartite graph with node types A and B. Given delta > 0, the projection graph G' pivoting over type B nodes, +/// will make a connection between nodes n1 and n2 (of type A) at time (t1 + t2)/2 if they respectively have an edge at time t1, t2 with the same node of type B in G, and |t2-t1| < delta. +/// +/// # Arguments +/// - `graph`: A directed raphtory graph +/// - `delta`: Time period +/// - `pivot_type`: node type to pivot over. If a bipartite graph has types A and B, and B is the pivot type, the new graph will consist of type A nodes. +/// +/// # Returns +/// Projected (unipartite) temporal graph. pub fn temporal_bipartite_projection( graph: &G, delta: i64, diff --git a/raphtory/src/python/algorithm/epidemics.rs b/raphtory/src/python/algorithm/epidemics.rs index 55bc20bbb9..647fa7c2e6 100644 --- a/raphtory/src/python/algorithm/epidemics.rs +++ b/raphtory/src/python/algorithm/epidemics.rs @@ -97,7 +97,7 @@ impl IntoSeeds for PySeed { } py_algorithm_result!(AlgorithmResultSEIR, DynamicGraph, Infected, Infected); -py_algorithm_result_new_ord_hash_eq!(AlgorithmResultSEIR, DynamicGraph, Infected, Infected); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultSEIR, DynamicGraph, Infected); impl From for PyErr { fn from(value: SeedError) -> Self { diff --git a/raphtory/src/python/graph/algorithm_result.rs b/raphtory/src/python/graph/algorithm_result.rs index 9de380102b..5497581053 100644 --- a/raphtory/src/python/graph/algorithm_result.rs +++ b/raphtory/src/python/graph/algorithm_result.rs @@ -4,6 +4,7 @@ use crate::{ db::api::view::{internal::DynamicGraph, StaticGraphViewOps}, python::types::repr::{Repr, StructReprBuilder}, }; +use num_traits::float::FloatCore; use ordered_float::OrderedFloat; use pyo3::prelude::*; use raphtory_api::core::entities::GID; @@ -52,7 +53,7 @@ macro_rules! py_algorithm_result { #[macro_export] macro_rules! py_algorithm_result_base { - ($objectName:ident, $rustGraph:ty, $rustValue:ty, $rustOrderedValue:ty) => { + ($objectName:ident, $rustGraph:ty, $rustValue:ty) => { #[pymethods] impl $objectName { /// Returns a Dict containing all the nodes (as keys) and their corresponding values (values) or none. @@ -151,7 +152,7 @@ macro_rules! py_algorithm_result_base { #[macro_export] macro_rules! py_algorithm_result_partial_ord { - ($objectName:ident, $rustGraph:ty, $rustValue:ty, $rustOrderedValue:ty) => { + ($objectName:ident, $rustGraph:ty, $rustValue:ty) => { #[pymethods] impl $objectName { /// Sorts the `AlgorithmResult` by its values in ascending or descending order. @@ -248,13 +249,13 @@ macro_rules! py_algorithm_result_partial_ord { self.0.median() } } - $crate::py_algorithm_result_base!($objectName, $rustGraph, $rustValue, $rustOrderedValue); + $crate::py_algorithm_result_base!($objectName, $rustGraph, $rustValue); }; } #[macro_export] macro_rules! py_algorithm_result_new_ord_hash_eq { - ($objectName:ident, $rustGraph:ty, $rustValue:ty, $rustOrderedValue:ty) => { + ($objectName:ident, $rustGraph:ty, $rustValue:ty) => { #[pymethods] impl $objectName { /// Groups the `AlgorithmResult` by its values. @@ -266,26 +267,29 @@ macro_rules! py_algorithm_result_new_ord_hash_eq { self.0.group_by() } } - $crate::py_algorithm_result_partial_ord!( - $objectName, - $rustGraph, - $rustValue, - $rustOrderedValue - ); + $crate::py_algorithm_result_partial_ord!($objectName, $rustGraph, $rustValue); }; } py_algorithm_result!(AlgorithmResult, DynamicGraph, String, String); -py_algorithm_result_new_ord_hash_eq!(AlgorithmResult, DynamicGraph, String, String); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResult, DynamicGraph, String); py_algorithm_result!(AlgorithmResultF64, DynamicGraph, f64, OrderedFloat); -py_algorithm_result_partial_ord!(AlgorithmResultF64, DynamicGraph, f64, OrderedFloat); +py_algorithm_result_partial_ord!(AlgorithmResultF64, DynamicGraph, f64); + +py_algorithm_result!( + AlgorithmResultPairF32, + DynamicGraph, + [f32; 2], + [OrderedFloat; 2] +); +py_algorithm_result_partial_ord!(AlgorithmResultPairF32, DynamicGraph, [f32; 2]); py_algorithm_result!(AlgorithmResultU64, DynamicGraph, u64, u64); -py_algorithm_result_new_ord_hash_eq!(AlgorithmResultU64, DynamicGraph, u64, u64); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultU64, DynamicGraph, u64); py_algorithm_result!(AlgorithmResultGID, DynamicGraph, GID, GID); -py_algorithm_result_new_ord_hash_eq!(AlgorithmResultGID, DynamicGraph, GID, GID); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultGID, DynamicGraph, GID); py_algorithm_result!( AlgorithmResultTupleF32F32, @@ -293,12 +297,7 @@ py_algorithm_result!( (f32, f32), (OrderedFloat, OrderedFloat) ); -py_algorithm_result_partial_ord!( - AlgorithmResultTupleF32F32, - DynamicGraph, - (f32, f32), - (f32, f32) -); +py_algorithm_result_partial_ord!(AlgorithmResultTupleF32F32, DynamicGraph, (f32, f32)); py_algorithm_result!( AlgorithmResultVecI64Str, @@ -306,12 +305,7 @@ py_algorithm_result!( Vec<(i64, String)>, Vec<(i64, String)> ); -py_algorithm_result_new_ord_hash_eq!( - AlgorithmResultVecI64Str, - DynamicGraph, - Vec<(i64, String)>, - Vec<(i64, String)> -); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultVecI64Str, DynamicGraph, Vec<(i64, String)>); py_algorithm_result!( AlgorithmResultVecF64, @@ -319,15 +313,10 @@ py_algorithm_result!( Vec, Vec> ); -py_algorithm_result_partial_ord!( - AlgorithmResultVecF64, - DynamicGraph, - Vec, - Vec> -); +py_algorithm_result_partial_ord!(AlgorithmResultVecF64, DynamicGraph, Vec); py_algorithm_result!(AlgorithmResultUsize, DynamicGraph, usize, usize); -py_algorithm_result_new_ord_hash_eq!(AlgorithmResultUsize, DynamicGraph, usize, usize); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultUsize, DynamicGraph, usize); py_algorithm_result!( AlgorithmResultVecUsize, @@ -335,18 +324,13 @@ py_algorithm_result!( Vec, Vec ); -py_algorithm_result_new_ord_hash_eq!( - AlgorithmResultVecUsize, - DynamicGraph, - Vec, - Vec -); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultVecUsize, DynamicGraph, Vec); py_algorithm_result!(AlgorithmResultU64VecU64, DynamicGraph, Vec, Vec); -py_algorithm_result_new_ord_hash_eq!(AlgorithmResultU64VecU64, DynamicGraph, Vec, Vec); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultU64VecU64, DynamicGraph, Vec); py_algorithm_result!(AlgorithmResultGIDVecGID, DynamicGraph, Vec, Vec); -py_algorithm_result_new_ord_hash_eq!(AlgorithmResultGIDVecGID, DynamicGraph, Vec, Vec); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultGIDVecGID, DynamicGraph, Vec); py_algorithm_result!( AlgorithmResultVecStr, @@ -354,9 +338,12 @@ py_algorithm_result!( Vec, Vec ); -py_algorithm_result_new_ord_hash_eq!( - AlgorithmResultVecStr, +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultVecStr, DynamicGraph, Vec); + +py_algorithm_result!( + AlgorithmResultPropVecStr, DynamicGraph, - Vec, - Vec + (f64, Vec), + (OrderedFloat, Vec) ); +py_algorithm_result_partial_ord!(AlgorithmResultPropVecStr, DynamicGraph, (f64, Vec)); diff --git a/raphtory/src/python/packages/algorithms.rs b/raphtory/src/python/packages/algorithms.rs index aec487d5c6..057531a72d 100644 --- a/raphtory/src/python/packages/algorithms.rs +++ b/raphtory/src/python/packages/algorithms.rs @@ -1,9 +1,11 @@ #![allow(non_snake_case)] +#[cfg(feature = "storage")] +use crate::python::graph::disk_graph::PyDiskGraph; use crate::{ algorithms::{ algorithm_result::AlgorithmResult, - bipartite::max_weight_matching::max_weight_matching as mwm, + bipartite::max_weight_matching::{max_weight_matching as mwm, Matching}, centrality::{ betweenness::betweenness_centrality as betweenness_rs, degree_centrality::degree_centrality as degree_centrality_rs, hits::hits as hits_rs, @@ -15,7 +17,7 @@ use crate::{ }, components, dynamics::temporal::epidemics::{temporal_SEIR as temporal_SEIR_rs, Infected, SeedError}, - embeddings::fast_rp as fast_rp_rs, + embeddings::fast_rp::fast_rp as fast_rp_rs, layout::{ cohesive_fruchterman_reingold::cohesive_fruchterman_reingold as cohesive_fruchterman_reingold_rs, fruchterman_reingold::fruchterman_reingold_unbounded as fruchterman_reingold_rs, @@ -51,24 +53,22 @@ use crate::{ }, projections::temporal_bipartite_projection::temporal_bipartite_projection as temporal_bipartite_rs, }, - core::Prop, - db::{api::view::internal::DynamicGraph, graph::node::NodeView}, + db::{ + api::{state::NodeState, view::internal::DynamicGraph}, + graph::node::NodeView, + }, python::{ graph::{node::PyNode, views::graph_view::PyGraphView}, utils::{PyNodeRef, PyTime}, }, }; use ordered_float::OrderedFloat; +#[cfg(feature = "storage")] +use pometry_storage::algorithms::connected_components::connected_components as connected_components_rs; use pyo3::prelude::*; use rand::{prelude::StdRng, SeedableRng}; use raphtory_api::core::{entities::GID, Direction}; -use std::collections::{HashMap, HashSet}; - -#[cfg(feature = "storage")] -use crate::python::graph::disk_graph::PyDiskGraph; -use crate::{algorithms::bipartite::max_weight_matching::Matching, db::api::state::NodeState}; -#[cfg(feature = "storage")] -use pometry_storage::algorithms::connected_components::connected_components as connected_components_rs; +use std::collections::HashSet; /// Implementations of various graph algorithms that can be run on a graph. /// @@ -80,15 +80,16 @@ use pometry_storage::algorithms::connected_components::connected_components as c /// This function returns the number of pairs of neighbours of a given node which are themselves connected. /// /// Arguments: -/// g (GraphView) : Raphtory graph, this can be directed or undirected but will be treated as undirected +/// graph (GraphView) : Raphtory graph, this can be directed or undirected but will be treated as undirected /// v (InputNode) : node id or name /// /// Returns: /// int : number of triangles associated with node v /// #[pyfunction] -pub fn local_triangle_count(g: &PyGraphView, v: PyNodeRef) -> Option { - local_triangle_count_rs(&g.graph, v) +#[pyo3(signature = (graph, v))] +pub fn local_triangle_count(graph: &PyGraphView, v: PyNodeRef) -> Option { + local_triangle_count_rs(&graph.graph, v) } /// Weakly connected components -- partitions the graph into node sets which are mutually reachable by an undirected path @@ -97,18 +98,18 @@ pub fn local_triangle_count(g: &PyGraphView, v: PyNodeRef) -> Option { /// by an undirected path. /// /// Arguments: -/// g (GraphView) : Raphtory graph +/// graph (GraphView) : Raphtory graph /// iter_count (int) : Maximum number of iterations to run. Note that this will terminate early if the labels converge prior to the number of iterations being reached. /// /// Returns: /// AlgorithmResult : AlgorithmResult object mapping nodes to their component ids. #[pyfunction] -#[pyo3(signature = (g, iter_count=9223372036854775807))] +#[pyo3(signature = (graph, iter_count=9223372036854775807))] pub fn weakly_connected_components( - g: &PyGraphView, + graph: &PyGraphView, iter_count: usize, ) -> AlgorithmResult { - components::weakly_connected_components(&g.graph, iter_count, None) + components::weakly_connected_components(&graph.graph, iter_count, None) } /// Strongly connected components @@ -116,36 +117,36 @@ pub fn weakly_connected_components( /// Partitions the graph into node sets which are mutually reachable by an directed path /// /// Arguments: -/// g (GraphView) : Raphtory graph +/// graph (GraphView) : Raphtory graph /// /// Returns: /// list[list[int]] : List of strongly connected nodes identified by ids #[pyfunction] -#[pyo3(signature = (g))] +#[pyo3(signature = (graph))] pub fn strongly_connected_components( - g: &PyGraphView, + graph: &PyGraphView, ) -> AlgorithmResult { - components::strongly_connected_components(&g.graph, None) + components::strongly_connected_components(&graph.graph, None) } #[cfg(feature = "storage")] #[pyfunction] -#[pyo3(signature = (g))] -pub fn connected_components(g: &PyDiskGraph) -> Vec { - connected_components_rs(g.graph.as_ref()) +#[pyo3(signature = (graph))] +pub fn connected_components(graph: &PyDiskGraph) -> Vec { + connected_components_rs(graph.graph.as_ref()) } /// In components -- Finding the "in-component" of a node in a directed graph involves identifying all nodes that can be reached following only incoming edges. /// /// Arguments: -/// g (GraphView) : Raphtory graph +/// graph (GraphView) : Raphtory graph /// /// Returns: /// AlgorithmResult : AlgorithmResult object mapping each node to an array containing the ids of all nodes within their 'in-component' #[pyfunction] -#[pyo3(signature = (g))] -pub fn in_components(g: &PyGraphView) -> AlgorithmResult, Vec> { - components::in_components(&g.graph, None) +#[pyo3(signature = (graph))] +pub fn in_components(graph: &PyGraphView) -> AlgorithmResult, Vec> { + components::in_components(&graph.graph, None) } /// In component -- Finding the "in-component" of a node in a directed graph involves identifying all nodes that can be reached following only incoming edges. @@ -164,14 +165,14 @@ pub fn in_component(node: &PyNode) -> NodeState<'static, usize, DynamicGraph> { /// Out components -- Finding the "out-component" of a node in a directed graph involves identifying all nodes that can be reached following only outgoing edges. /// /// Arguments: -/// g (GraphView) : Raphtory graph +/// graph (GraphView) : Raphtory graph /// /// Returns: /// AlgorithmResult : AlgorithmResult object mapping each node to an array containing the ids of all nodes within their 'out-component' #[pyfunction] -#[pyo3(signature = (g))] -pub fn out_components(g: &PyGraphView) -> AlgorithmResult, Vec> { - components::out_components(&g.graph, None) +#[pyo3(signature = (graph))] +pub fn out_components(graph: &PyGraphView) -> AlgorithmResult, Vec> { + components::out_components(&graph.graph, None) } /// Out component -- Finding the "out-component" of a node in a directed graph involves identifying all nodes that can be reached following only outgoing edges. @@ -194,7 +195,7 @@ pub fn out_component(node: &PyNode) -> NodeState<'static, usize, DynamicGraph> { /// is less than the max diff value given. /// /// Arguments: -/// g (GraphView) : Raphtory graph +/// graph (GraphView) : Raphtory graph /// iter_count (int) : Maximum number of iterations to run. Note that this will terminate early if convergence is reached. /// max_diff (Optional[float]) : Optional parameter providing an alternative stopping condition. /// The algorithm will terminate if the sum of the absolute difference in pagerank values between iterations @@ -203,16 +204,16 @@ pub fn out_component(node: &PyNode) -> NodeState<'static, usize, DynamicGraph> { /// Returns: /// AlgorithmResult : AlgorithmResult with string keys and float values mapping node names to their pagerank value. #[pyfunction] -#[pyo3(signature = (g, iter_count=20, max_diff=None, use_l2_norm=true, damping_factor=0.85))] +#[pyo3(signature = (graph, iter_count=20, max_diff=None, use_l2_norm=true, damping_factor=0.85))] pub fn pagerank( - g: &PyGraphView, + graph: &PyGraphView, iter_count: usize, max_diff: Option, use_l2_norm: bool, damping_factor: Option, ) -> AlgorithmResult> { unweighted_page_rank( - &g.graph, + &graph.graph, Some(iter_count), None, max_diff, @@ -228,7 +229,7 @@ pub fn pagerank( /// a sequence of edges (v_i, v_i+1, t_i) with t_i < t_i+1 for i = 1, ... , k - 1. /// /// Arguments: -/// g (GraphView) : directed Raphtory graph +/// graph (GraphView) : directed Raphtory graph /// max_hops (int) : maximum number of hops to propagate out /// start_time (int) : time at which to start the path (such that t_1 > start_time for any path starting from these seed nodes) /// seed_nodes (list[InputNode]) : list of node names or ids which should be the starting nodes @@ -237,15 +238,22 @@ pub fn pagerank( /// Returns: /// AlgorithmResult : AlgorithmResult with string keys and float values mapping node names to their pagerank value. #[pyfunction] -#[pyo3(signature = (g, max_hops, start_time, seed_nodes, stop_nodes=None))] +#[pyo3(signature = (graph, max_hops, start_time, seed_nodes, stop_nodes=None))] pub fn temporally_reachable_nodes( - g: &PyGraphView, + graph: &PyGraphView, max_hops: usize, start_time: i64, seed_nodes: Vec, stop_nodes: Option>, ) -> AlgorithmResult, Vec<(i64, String)>> { - temporal_reachability_rs(&g.graph, None, max_hops, start_time, seed_nodes, stop_nodes) + temporal_reachability_rs( + &graph.graph, + None, + max_hops, + start_time, + seed_nodes, + stop_nodes, + ) } /// Local clustering coefficient - measures the degree to which nodes in a graph tend to cluster together. @@ -253,14 +261,14 @@ pub fn temporally_reachable_nodes( /// The proportion of pairs of neighbours of a node who are themselves connected. /// /// Arguments: -/// g (GraphView) : Raphtory graph, can be directed or undirected but will be treated as undirected. +/// graph (GraphView) : Raphtory graph, can be directed or undirected but will be treated as undirected. /// v (InputNode): node id or name /// /// Returns: -/// float : the local clustering coefficient of node v in g. +/// float : the local clustering coefficient of node v in graph. #[pyfunction] -pub fn local_clustering_coefficient(g: &PyGraphView, v: PyNodeRef) -> Option { - local_clustering_coefficient_rs(&g.graph, v) +pub fn local_clustering_coefficient(graph: &PyGraphView, v: PyNodeRef) -> Option { + local_clustering_coefficient_rs(&graph.graph, v) } /// Graph density - measures how dense or sparse a graph is. @@ -269,13 +277,13 @@ pub fn local_clustering_coefficient(g: &PyGraphView, v: PyNodeRef) -> Option f32 { - directed_graph_density_rs(&g.graph) +pub fn directed_graph_density(graph: &PyGraphView) -> f64 { + directed_graph_density_rs(&graph.graph) } /// The average (undirected) degree of all nodes in the graph. @@ -284,61 +292,61 @@ pub fn directed_graph_density(g: &PyGraphView) -> f32 { /// the number of undirected edges divided by the number of nodes. /// /// Arguments: -/// g (GraphView) : a Raphtory graph +/// graph (GraphView) : a Raphtory graph /// /// Returns: /// float : the average degree of the nodes in the graph #[pyfunction] -pub fn average_degree(g: &PyGraphView) -> f64 { - average_degree_rs(&g.graph) +pub fn average_degree(graph: &PyGraphView) -> f64 { + average_degree_rs(&graph.graph) } /// The maximum out degree of any node in the graph. /// /// Arguments: -/// g (GraphView) : a directed Raphtory graph +/// graph (GraphView) : a directed Raphtory graph /// /// Returns: /// int : value of the largest outdegree #[pyfunction] -pub fn max_out_degree(g: &PyGraphView) -> usize { - max_out_degree_rs(&g.graph) +pub fn max_out_degree(graph: &PyGraphView) -> usize { + max_out_degree_rs(&graph.graph) } /// The maximum in degree of any node in the graph. /// /// Arguments: -/// g (GraphView) : a directed Raphtory graph +/// graph (GraphView) : a directed Raphtory graph /// /// Returns: /// int : value of the largest indegree #[pyfunction] -pub fn max_in_degree(g: &PyGraphView) -> usize { - max_in_degree_rs(&g.graph) +pub fn max_in_degree(graph: &PyGraphView) -> usize { + max_in_degree_rs(&graph.graph) } /// The minimum out degree of any node in the graph. /// /// Arguments: -/// g (GraphView) : a directed Raphtory graph +/// graph (GraphView) : a directed Raphtory graph /// /// Returns: /// int : value of the smallest outdegree #[pyfunction] -pub fn min_out_degree(g: &PyGraphView) -> usize { - min_out_degree_rs(&g.graph) +pub fn min_out_degree(graph: &PyGraphView) -> usize { + min_out_degree_rs(&graph.graph) } /// The minimum in degree of any node in the graph. /// /// Arguments: -/// g (GraphView) : a directed Raphtory graph +/// graph (GraphView) : a directed Raphtory graph /// /// Returns: /// int : value of the smallest indegree #[pyfunction] -pub fn min_in_degree(g: &PyGraphView) -> usize { - min_in_degree_rs(&g.graph) +pub fn min_in_degree(graph: &PyGraphView) -> usize { + min_in_degree_rs(&graph.graph) } /// Reciprocity - measure of the symmetry of relationships in a graph, the global reciprocity of @@ -347,14 +355,14 @@ pub fn min_in_degree(g: &PyGraphView) -> usize { /// graph and normalizes it by the total number of directed edges. /// /// Arguments: -/// g (GraphView) : a directed Raphtory graph +/// graph (GraphView) : a directed Raphtory graph /// /// Returns: /// float : reciprocity of the graph between 0 and 1. #[pyfunction] -pub fn global_reciprocity(g: &PyGraphView) -> f64 { - global_reciprocity_rs(&g.graph, None) +pub fn global_reciprocity(graph: &PyGraphView) -> f64 { + global_reciprocity_rs(&graph.graph, None) } /// Local reciprocity - measure of the symmetry of relationships associated with a node @@ -362,16 +370,16 @@ pub fn global_reciprocity(g: &PyGraphView) -> f64 { /// This measures the proportion of a node's outgoing edges which are reciprocated with an incoming edge. /// /// Arguments: -/// g (GraphView) : a directed Raphtory graph +/// graph (GraphView) : a directed Raphtory graph /// /// Returns: /// AlgorithmResult : AlgorithmResult with string keys and float values mapping each node name to its reciprocity value. /// #[pyfunction] pub fn all_local_reciprocity( - g: &PyGraphView, + graph: &PyGraphView, ) -> AlgorithmResult> { - all_local_reciprocity_rs(&g.graph, None) + all_local_reciprocity_rs(&graph.graph, None) } /// Computes the number of connected triplets within a graph @@ -380,13 +388,13 @@ pub fn all_local_reciprocity( /// A-B, B-C, C-A is formed of three connected triplets. /// /// Arguments: -/// g (GraphView) : a Raphtory graph, treated as undirected +/// graph (GraphView) : a Raphtory graph, treated as undirected /// /// Returns: /// int : the number of triplets in the graph #[pyfunction] -pub fn triplet_count(g: &PyGraphView) -> usize { - crate::algorithms::motifs::triplet_count::triplet_count(&g.graph, None) +pub fn triplet_count(graph: &PyGraphView) -> usize { + crate::algorithms::motifs::triplet_count::triplet_count(&graph.graph, None) } /// Computes the global clustering coefficient of a graph. The global clustering coefficient is @@ -395,16 +403,16 @@ pub fn triplet_count(g: &PyGraphView) -> usize { /// Note that this is also known as transitivity and is different to the average clustering coefficient. /// /// Arguments: -/// g (GraphView) : a Raphtory graph, treated as undirected +/// graph (GraphView) : a Raphtory graph, treated as undirected /// /// Returns: /// float : the global clustering coefficient of the graph /// /// See also: -/// [`Triplet Count`](triplet_count) +/// [Triplet Count](triplet_count) #[pyfunction] -pub fn global_clustering_coefficient(g: &PyGraphView) -> f64 { - crate::algorithms::metrics::clustering_coefficient::clustering_coefficient(&g.graph) +pub fn global_clustering_coefficient(graph: &PyGraphView) -> f64 { + crate::algorithms::metrics::clustering_coefficient::clustering_coefficient(&graph.graph) } /// Computes the number of three edge, up-to-three node delta-temporal motifs in the graph, using the algorithm of Paranjape et al, Motifs in Temporal Networks (2017). @@ -442,8 +450,9 @@ pub fn global_clustering_coefficient(g: &PyGraphView) -> f64 { /// 8. i --> j, i --> k, k --> j /// /// Arguments: -/// g (GraphView) : A directed raphtory graph +/// graph (GraphView) : A directed raphtory graph /// delta (int): Maximum time difference between the first and last edge of the motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise milliseconds should be used (if edge times were given as string) +/// threads (int, optional): Number of threads to use /// /// Returns: /// list : A 40 dimensional array with the counts of each motif, given in the same order as described above. Note that the two-node motif counts are symmetrical so it may be more useful just to consider the first four elements. @@ -452,50 +461,58 @@ pub fn global_clustering_coefficient(g: &PyGraphView) -> f64 { /// This is achieved by calling the local motif counting algorithm, summing the resulting arrays and dealing with overcounted motifs: the triangles (by dividing each motif count by three) and two-node motifs (dividing by two). /// #[pyfunction] -pub fn global_temporal_three_node_motif(g: &PyGraphView, delta: i64) -> [usize; 40] { - global_temporal_three_node_motif_rs(&g.graph, delta, None) +#[pyo3(signature = (graph, delta, threads=None))] +pub fn global_temporal_three_node_motif( + graph: &PyGraphView, + delta: i64, + threads: Option, +) -> [usize; 40] { + global_temporal_three_node_motif_rs(&graph.graph, delta, threads) } -/// Projects a temporal bipartite graph into an undirected temporal graph over the pivot node type. Let G be a bipartite graph with node types A and B. Given delta > 0, the projection graph G' pivoting over type B nodes, -/// will make a connection between nodes n1 and n2 (of type A) at time (t1 + t2)/2 if they respectively have an edge at time t1, t2 with the same node of type B in G, and |t2-t1| < delta. +/// Projects a temporal bipartite graph into an undirected temporal graph over the pivot node type. Let graph be a bipartite graph with node types A and B. Given delta > 0, the projection graph graph' pivoting over type B nodes, +/// will make a connection between nodes n1 and n2 (of type A) at time (t1 + t2)/2 if they respectively have an edge at time t1, t2 with the same node of type B in graph, and |t2-t1| < delta. /// /// Arguments: -/// g (GraphView) : A directed raphtory graph +/// graph (GraphView) : A directed raphtory graph /// delta (int): Time period -/// pivot (str) : node type to pivot over. If a bipartite graph has types A and B, and B is the pivot type, the new graph will consist of type A nodes. +/// pivot_type (str) : node type to pivot over. If a bipartite graph has types A and B, and B is the pivot type, the new graph will consist of type A nodes. /// /// Returns: /// GraphView: Projected (unipartite) temporal graph. #[pyfunction] -#[pyo3(signature = (g, delta, pivot_type))] +#[pyo3(signature = (graph, delta, pivot_type))] pub fn temporal_bipartite_graph_projection( - g: &PyGraphView, + graph: &PyGraphView, delta: i64, pivot_type: String, ) -> PyGraphView { - temporal_bipartite_rs(&g.graph, delta, pivot_type).into() + temporal_bipartite_rs(&graph.graph, delta, pivot_type).into() } -/// Computes the global counts of three-edge up-to-three node temporal motifs for a range of timescales. See `global_temporal_three_node_motif` for an interpretation of each row returned. +/// Computes the global counts of three-edge up-to-three node temporal motifs for a range of timescales. See global_temporal_three_node_motif for an interpretation of each row returned. /// /// Arguments: -/// g (GraphView) : A directed raphtory graph -/// deltas(list[int]): A list of delta values to use. +/// graph (GraphView) : A directed raphtory graph +/// deltas (list[int]): A list of delta values to use. +/// threads (int, optional): Number of threads to use /// /// Returns: /// list[list[int]] : A list of 40d arrays, each array is the motif count for a particular value of delta, returned in the order that the deltas were given as input. #[pyfunction] +#[pyo3(signature = (graph, deltas, threads=None))] pub fn global_temporal_three_node_motif_multi( - g: &PyGraphView, + graph: &PyGraphView, deltas: Vec, + threads: Option, ) -> Vec<[usize; 40]> { - global_temporal_three_node_motif_general_rs(&g.graph, deltas, None) + global_temporal_three_node_motif_general_rs(&graph.graph, deltas, threads) } /// Computes the number of each type of motif that each node participates in. See global_temporal_three_node_motifs for a summary of the motifs involved. /// /// Arguments: -/// g (GraphView) : A directed raphtory graph +/// graph (GraphView) : A directed raphtory graph /// delta (int): Maximum time difference between the first and last edge of the motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise milliseconds should be used (if edge times were given as string) /// /// Returns: @@ -505,14 +522,13 @@ pub fn global_temporal_three_node_motif_multi( /// For this local count, a node is counted as participating in a motif in the following way. For star motifs, only the centre node counts /// the motif. For two node motifs, both constituent nodes count the motif. For triangles, all three constituent nodes count the motif. #[pyfunction] +#[pyo3(signature = (graph, delta, threads=None))] pub fn local_temporal_three_node_motifs( - g: &PyGraphView, + graph: &PyGraphView, delta: i64, -) -> HashMap> { - local_three_node_rs(&g.graph, vec![delta], None) - .into_iter() - .map(|(k, v)| (String::from(k), v[0].clone())) - .collect::>>() + threads: Option, +) -> AlgorithmResult> { + local_three_node_rs(&graph.graph, vec![delta], threads) } /// HITS (Hubs and Authority) Algorithm: @@ -523,20 +539,20 @@ pub fn local_temporal_three_node_motifs( /// Sum of AuthScore of all nodes in the current iteration /// /// Arguments: -/// g (GraphView): Graph to run the algorithm on +/// graph (GraphView): Graph to run the algorithm on /// iter_count (int): How many iterations to run the algorithm /// threads (int, optional): Number of threads to use /// /// Returns /// An AlgorithmResult object containing the mapping from node ID to the hub and authority score of the node #[pyfunction] -#[pyo3(signature = (g, iter_count=20, threads=None))] +#[pyo3(signature = (graph, iter_count=20, threads=None))] pub fn hits( - g: &PyGraphView, + graph: &PyGraphView, iter_count: usize, threads: Option, ) -> AlgorithmResult, OrderedFloat)> { - hits_rs(&g.graph, iter_count, threads) + hits_rs(&graph.graph, iter_count, threads) } /// Sums the weights of edges in the graph based on the specified direction. @@ -544,7 +560,7 @@ pub fn hits( /// This function computes the sum of edge weights based on the direction provided, and can be executed in parallel using a given number of threads. /// /// Arguments: -/// g (GraphView): The graph view on which the operation is to be performed. +/// graph (GraphView): The graph view on which the operation is to be performed. /// name (str): The name of the edge property used as the weight. Defaults to "weight". /// direction (Direction): Specifies the direction of the edges to be considered for summation. Defaults to "both". /// * "out": Only consider outgoing edges. @@ -556,14 +572,14 @@ pub fn hits( /// AlgorithmResult: A result containing a mapping of node names to the computed sum of their associated edge weights. /// #[pyfunction] -#[pyo3[signature = (g, name="weight".to_string(), direction=Direction::BOTH, threads=None)]] +#[pyo3[signature = (graph, name="weight".to_string(), direction=Direction::BOTH, threads=None)]] pub fn balance( - g: &PyGraphView, + graph: &PyGraphView, name: String, direction: Direction, threads: Option, ) -> AlgorithmResult> { - balance_rs(&g.graph, name.clone(), direction, threads) + balance_rs(&graph.graph, name.clone(), direction, threads) } /// Computes the degree centrality of all nodes in the graph. The values are normalized @@ -571,88 +587,89 @@ pub fn balance( /// values of centrality greater than 1. /// /// Arguments: -/// g (GraphView): The graph view on which the operation is to be performed. +/// graph (GraphView): The graph view on which the operation is to be performed. /// threads (int, optional): The number of threads to be used for parallel execution. /// /// Returns: /// AlgorithmResult: A result containing a mapping of node names to the computed sum of their associated degree centrality. #[pyfunction] -#[pyo3[signature = (g, threads=None)]] +#[pyo3[signature = (graph, threads=None)]] pub fn degree_centrality( - g: &PyGraphView, + graph: &PyGraphView, threads: Option, ) -> AlgorithmResult> { - degree_centrality_rs(&g.graph, threads) + degree_centrality_rs(&graph.graph, threads) } /// Returns the largest degree found in the graph /// /// Arguments: -/// g (GraphView): The graph view on which the operation is to be performed. +/// graph (GraphView): The graph view on which the operation is to be performed. /// /// Returns: /// int: The largest degree #[pyfunction] -#[pyo3[signature = (g)]] -pub fn max_degree(g: &PyGraphView) -> usize { - max_degree_rs(&g.graph) +#[pyo3[signature = (graph)]] +pub fn max_degree(graph: &PyGraphView) -> usize { + max_degree_rs(&graph.graph) } /// Returns the smallest degree found in the graph /// /// Arguments: -/// g (GraphView): The graph view on which the operation is to be performed. +/// graph (GraphView): The graph view on which the operation is to be performed. /// /// Returns: /// int: The smallest degree found #[pyfunction] -#[pyo3[signature = (g)]] -pub fn min_degree(g: &PyGraphView) -> usize { - min_degree_rs(&g.graph) +#[pyo3[signature = (graph)]] +pub fn min_degree(graph: &PyGraphView) -> usize { + min_degree_rs(&graph.graph) } /// Calculates the single source shortest paths from a given source node. /// /// Arguments: -/// g (GraphView): A reference to the graph. Must implement `GraphViewOps`. -/// source (InputNode): The source node. Must implement `InputNode`. +/// graph (GraphView): A reference to the graph. Must implement GraphViewOps. +/// source (InputNode): The source node. Must implement InputNode. /// cutoff (int, optional): An optional cutoff level. The algorithm will stop if this level is reached. /// /// Returns: -/// AlgorithmResult: Returns an `AlgorithmResult[str, list[str]]` containing the shortest paths from the source to all reachable nodes. +/// AlgorithmResult: Returns an AlgorithmResult[str, list[str]] containing the shortest paths from the source to all reachable nodes. /// #[pyfunction] -#[pyo3[signature = (g, source, cutoff=None)]] +#[pyo3[signature = (graph, source, cutoff=None)]] pub fn single_source_shortest_path( - g: &PyGraphView, + graph: &PyGraphView, source: PyNodeRef, cutoff: Option, ) -> AlgorithmResult, Vec> { - single_source_shortest_path_rs(&g.graph, source, cutoff) + single_source_shortest_path_rs(&graph.graph, source, cutoff) } /// Finds the shortest paths from a single source to multiple targets in a graph. /// /// Arguments: -/// g (GraphView): The graph to search in. +/// graph (GraphView): The graph to search in. /// source (InputNode): The source node. /// targets (list[InputNode]): A list of target nodes. /// direction (Direction): The direction of the edges to be considered for the shortest path. Defaults to "both". /// weight (str): The name of the weight property for the edges. Defaults to "weight". /// /// Returns: -/// dict: Returns a `Dict` where the key is the target node and the value is a tuple containing the total cost and a vector of nodes representing the shortest path. +/// AlgorithmResult: Returns an AlgorithmResult where the key is the target node and the value is a tuple containing the total cost and a vector of nodes representing the shortest path. /// #[pyfunction] -#[pyo3[signature = (g, source, targets, direction=Direction::BOTH, weight="weight".to_string())]] +#[pyo3[signature = (graph, source, targets, direction=Direction::BOTH, weight="weight".to_string())]] pub fn dijkstra_single_source_shortest_paths( - g: &PyGraphView, + graph: &PyGraphView, source: PyNodeRef, targets: Vec, direction: Direction, weight: Option, -) -> PyResult)>> { - match dijkstra_single_source_shortest_paths_rs(&g.graph, source, targets, weight, direction) { +) -> PyResult), (OrderedFloat, Vec)>> { + match dijkstra_single_source_shortest_paths_rs(&graph.graph, source, targets, weight, direction) + { Ok(result) => Ok(result), Err(err_msg) => Err(PyErr::new::(err_msg)), } @@ -661,39 +678,39 @@ pub fn dijkstra_single_source_shortest_paths( /// Computes the betweenness centrality for nodes in a given graph. /// /// Arguments: -/// g (GraphView): A reference to the graph. +/// graph (GraphView): A reference to the graph. /// k (int, optional): Specifies the number of nodes to consider for the centrality computation. /// All nodes are considered by default. /// normalized (bool): Indicates whether to normalize the centrality values. /// /// Returns: -/// AlgorithmResult: Returns an `AlgorithmResult` containing the betweenness centrality of each node. +/// AlgorithmResult: Returns an AlgorithmResult containing the betweenness centrality of each node. #[pyfunction] -#[pyo3[signature = (g, k=None, normalized=true)]] +#[pyo3[signature = (graph, k=None, normalized=true)]] pub fn betweenness_centrality( - g: &PyGraphView, + graph: &PyGraphView, k: Option, normalized: bool, ) -> AlgorithmResult> { - betweenness_rs(&g.graph, k, normalized) + betweenness_rs(&graph.graph, k, normalized) } /// Computes components using a label propagation algorithm /// /// Arguments: -/// g (GraphView): A reference to the graph +/// graph (GraphView): A reference to the graph /// seed (bytes, optional): Array of 32 bytes of u8 which is set as the rng seed /// /// Returns: /// list[set[Node]]: A list of sets each containing nodes that have been grouped /// #[pyfunction] -#[pyo3[signature = (g, seed=None)]] +#[pyo3[signature = (graph, seed=None)]] pub fn label_propagation( - g: &PyGraphView, + graph: &PyGraphView, seed: Option<[u8; 32]>, ) -> PyResult>>> { - match label_propagation_rs(&g.graph, seed) { + match label_propagation_rs(&graph.graph, seed) { Ok(result) => Ok(result), Err(err_msg) => Err(PyErr::new::(err_msg)), } @@ -705,8 +722,8 @@ pub fn label_propagation( /// /// Arguments: /// graph (GraphView): the graph view -/// seeds (int | float | list[InputNode]): the seeding strategy to use for the initial infection (if `int`, choose fixed number -/// of nodes at random, if `float` infect each node with this probability, if `list` +/// seeds (int | float | list[InputNode]): the seeding strategy to use for the initial infection (if int, choose fixed number +/// of nodes at random, if float infect each node with this probability, if list /// initially infect the specified nodes /// infection_prob (float): the probability for a contact between infected and susceptible nodes to lead /// to a transmission @@ -720,13 +737,13 @@ pub fn label_propagation( /// rng_seed (int | None): optional seed for the random number generator /// /// Returns: -/// AlgorithmResult: Returns an `Infected` object for each infected node with attributes +/// AlgorithmResult: Returns an AlgorithmResult mapping vertices to Infected objects with the following attributes: /// -/// `infected`: the time stamp of the infection event +/// infected: the time stamp of the infection event /// -/// `active`: the time stamp at which the node actively starts spreading the infection (i.e., the end of the incubation period) +/// active: the time stamp at which the node actively starts spreading the infection (i.e., the end of the incubation period) /// -/// `recovered`: the time stamp at which the node recovered (i.e., stopped spreading the infection) +/// recovered: the time stamp at which the node recovered (i.e., stopped spreading the infection) /// #[pyfunction(name = "temporal_SEIR")] #[pyo3(signature = (graph, seeds, infection_prob, initial_infection, recovery_rate=None, incubation_rate=None, rng_seed=None))] @@ -758,9 +775,12 @@ pub fn temporal_SEIR( /// /// Arguments: /// graph (GraphView): the graph view -/// resolution (float): the resolution paramter for modularity +/// resolution (float): the resolution parameter for modularity /// 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) +/// +/// Returns: +/// AlgorithmResult: Returns an AlgorithmResult containing the community id of each node. #[pyfunction] #[pyo3[signature=(graph, resolution=1.0, weight_prop=None, tol=None)]] pub fn louvain( @@ -783,7 +803,7 @@ pub fn louvain( /// dt (float | None): the time increment between iterations (default: 0.1) /// /// Returns: -/// a dict with the position for each node as a list with two numbers [x, y] +/// AlgorithmResult: an AlgorithmResult with the position for each node as a list with two numbers [x, y] #[pyfunction] #[pyo3[signature=(graph, iterations=100, scale=1.0, node_start_size=1.0, cooloff_factor=0.95, dt=0.1)]] pub fn fruchterman_reingold( @@ -793,7 +813,7 @@ pub fn fruchterman_reingold( node_start_size: f32, cooloff_factor: f32, dt: f32, -) -> HashMap { +) -> AlgorithmResult; 2]> { fruchterman_reingold_rs( &graph.graph, iterations, @@ -802,33 +822,38 @@ pub fn fruchterman_reingold( cooloff_factor, dt, ) - .into_iter() - .map(|(id, vector)| (id, [vector.x, vector.y])) - .collect() } /// Cohesive version of `fruchterman_reingold` that adds virtual edges between isolated nodes +/// Arguments: +/// graph (GraphView): A reference to the graph +/// iter_count (int): The number of iterations to run +/// scale (float): Global scaling factor to control the overall spread of the graph +/// node_start_size (float): Initial size or movement range for nodes +/// cooloff_factor (float): Factor to reduce node movement in later iterations, helping stabilize the layout +/// dt (float): Time step or movement factor in each iteration +/// +/// Returns: +/// AlgorithmResult: Returns an AlgorithmResult containing a mapping between vertices and a pair of coordinates. +/// #[pyfunction] -#[pyo3[signature=(graph, iterations=100, scale=1.0, node_start_size=1.0, cooloff_factor=0.95, dt=0.1)]] +#[pyo3[signature=(graph, iter_count=100, scale=1.0, node_start_size=1.0, cooloff_factor=0.95, dt=0.1)]] pub fn cohesive_fruchterman_reingold( graph: &PyGraphView, - iterations: u64, + iter_count: u64, scale: f32, node_start_size: f32, cooloff_factor: f32, dt: f32, -) -> HashMap { +) -> AlgorithmResult; 2]> { cohesive_fruchterman_reingold_rs( &graph.graph, - iterations, + iter_count, scale, node_start_size, cooloff_factor, dt, ) - .into_iter() - .map(|(id, vector)| (id, [vector.x, vector.y])) - .collect() } /// Temporal rich club coefficient @@ -845,29 +870,29 @@ pub fn cohesive_fruchterman_reingold( /// /// Arguments: /// graph (GraphView): the aggregate graph -/// views (iterator(GraphView)): sequence of graphs (can be obtained by calling g.rolling(..) on an aggregate graph g) +/// views (iterator(GraphView)): sequence of graphs (can be obtained by calling graph.rolling(..) on an aggregate graph graph) /// k (int): min degree of nodes to include in rich-club -/// delta (int): the number of consecutive snapshots over which the edges should persist +/// window_size (int): the number of consecutive snapshots over which the edges should persist /// /// Returns: -/// the rich-club coefficient as a float. +/// float: the rich-club coefficient as a float. #[pyfunction] -#[pyo3[signature = (graph, views, k, delta)]] +#[pyo3[signature = (graph, views, k, window_size)]] pub fn temporal_rich_club_coefficient( - graph: PyGraphView, + graph: &PyGraphView, views: &Bound, k: usize, - delta: usize, + window_size: usize, ) -> PyResult { let py_iterator = views.try_iter()?; let views = py_iterator .map(|view| view.and_then(|view| Ok(view.downcast::()?.get().graph.clone()))) .collect::>>()?; - Ok(temporal_rich_club_rs(graph.graph, views, k, delta)) + Ok(temporal_rich_club_rs(&graph.graph, views, k, window_size)) } /// Compute a maximum-weighted matching in the general undirected weighted -/// graph given by "edges". If `max_cardinality` is true, only +/// graph given by "edges". If max_cardinality is true, only /// maximum-cardinality matchings are considered as solutions. /// /// The algorithm is based on "Efficient Algorithms for Finding Maximum @@ -916,7 +941,7 @@ pub fn max_weight_matching( /// Computes embedding vectors for each vertex of an undirected/bidirectional graph according to the Fast RP algorithm. /// Original Paper: https://doi.org/10.48550/arXiv.1908.11512 /// Arguments: -/// g (GraphView): The graph view on which embeddings are generated. +/// graph (GraphView): The graph view on which embeddings are generated. /// embedding_dim (int): The size (dimension) of the generated embeddings. /// normalization_strength (float): The extent to which high-degree vertices should be discounted (range: 1-0) /// iter_weights (list[float]): The scalar weights to apply to the results of each iteration @@ -924,11 +949,11 @@ pub fn max_weight_matching( /// threads (int, optional): The number of threads to be used for parallel execution. /// /// Returns: -/// AlgorithmResult: Vertices mapped to their corresponding embedding vectors +/// AlgorithmResult: Returns an AlgorithmResult containing the embedding vector of each node. #[pyfunction] -#[pyo3[signature = (g, embedding_dim, normalization_strength, iter_weights, seed=None, threads=None)]] +#[pyo3[signature = (graph, embedding_dim, normalization_strength, iter_weights, seed=None, threads=None)]] pub fn fast_rp( - g: &PyGraphView, + graph: &PyGraphView, embedding_dim: usize, normalization_strength: f64, iter_weights: Vec, @@ -936,7 +961,7 @@ pub fn fast_rp( threads: Option, ) -> AlgorithmResult, Vec>> { fast_rp_rs( - &g.graph, + &graph.graph, embedding_dim, normalization_strength, iter_weights,