Skip to content

Commit

Permalink
Merge pull request #57 from Bears-R-Us/four-cycles
Browse files Browse the repository at this point in the history
Four cycles (squares)
  • Loading branch information
zhihuidu authored Oct 11, 2023
2 parents d3e8704 + d1d10db commit 1e8396f
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,6 @@ dmypy.json

# Old Arkouda-NJIT stuff
GraphServerModules.cfg.*

# Random server things
(nil)
26 changes: 12 additions & 14 deletions arachne/benchmarks/breadth_first_search.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
"""Breadth-First Search Benchmark
This script provides a benchmark on a graph built in Arachne from two Arkouda arrays. The graphs are
randomly generated by using the ak.randint function with the range of the vertex names being picked
from [0,n-1] and the number of edges m. No special distribution is used to generate the graph.
Select seed value to ensure repeatability of experiments.
The values of n and m are accepted from command line input. As well as the number of trials for
executing breadth-first search.
This script provides a benchmark on a graph that is randomly generated or specified by a matrix
market file. The random graphs are generated by using the ak.randint function with the range of the
vertex names being picked from [0,n-1] and the number of edges m. No special distribution is used to
generate the graph. Select seed value to ensure repeatability of experiments.
Assumes Arkouda server is running. It will shutdown the Arkouda server upon completion.
"""
Expand All @@ -26,27 +23,28 @@ def create_parser():
parser.add_argument("port", type = int, default = 5555,
help = "Port used by the arkouda server")

parser.add_argument("--rand", type = bool, default = True, help = "Run random benchmark?")
parser.add_argument("--rand", action="store_true", help = "Run random benchmark?")
parser.add_argument("-n", type = int, default = 1000, help = "Number of vertices for graph")
parser.add_argument("-m", type = int, default = 2000, help = "Number of edges for graph")

parser.add_argument("--mtx", type = bool, default = True, help = "Run mtx benchmark?")
parser.add_argument("--mtx", action="store_true", help = "Run mtx benchmark?")
parser.add_argument("--filepath", type = str, default = "../data/karate.mtx",
help = ".mtx file to benchmark")
parser.add_argument("--directed", type = bool, default = False, help = "Is graph directed?")
parser.add_argument("--is_directed", action="store_true", help = "Is graph directed?")

parser.add_argument("-t", "--trials", type = int, default = 5,
help = " Number of trials for BFS"
help = "Number of trials for BFS"
)
parser.add_argument("-s", "--seed", type = int, default = 2, help = "Set seed for randomness.")

return parser

def run_rand_benchmark(args):
"""Runs rand benchmark and prints out results to terminal."""
### Build graph from randomly generated source and destination arrays.
# 1. Use Arkouda's randint to generate the random edge arrays.
src = ak.randint(0, args.n, args.m)
dst = ak.randint(0, args.n, args.m)
src = ak.randint(0, args.n, args.m, seed=args.seed*2)
dst = ak.randint(0, args.n, args.m, seed=args.seed*4)

# 2. Build undirected graph.
print("### Arachne Graph Building")
Expand All @@ -67,7 +65,7 @@ def run_rand_benchmark(args):
print()

print("### Arachne Breadth-First Search")
### Run Arachne breadth-first search on the input graphs.
### Run Arachne breadth-first search on the input graph.
# 1. BFS on undirected graph.
undirected_bfs_trials = []
highest_degree = ak.argmax(graph.degree())
Expand Down
133 changes: 133 additions & 0 deletions arachne/benchmarks/square_counts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Square Counts Benchmark
This script provides a benchmark on a graph that is randomly generated or specified by a matrix
market file. The random graphs are generated by using the ak.randint function with the range of the
vertex names being picked from [0,n-1] and the number of edges m. No special distribution is used to
generate the graph. Select seed value to ensure repeatability of experiments.
Assumes Arkouda server is running. It will shutdown the Arkouda server upon completion.
"""
import argparse
import os
import time
import statistics as st
import arachne as ar
import arkouda as ak

def create_parser():
"""Creates the command line parser for this script"""
parser = argparse.ArgumentParser(
description="Simple benchmark for breadth-first search on randomly generated sample graphs."
)
parser.add_argument("hostname", help = "Hostname of the arkouda server")
parser.add_argument("port", type = int, default = 5555,
help = "Port used by the arkouda server")

parser.add_argument("--rand", action="store_true", help = "Run random benchmark?")
parser.add_argument("-n", type = int, default = 1000, help = "Number of vertices for graph")
parser.add_argument("-m", type = int, default = 2000, help = "Number of edges for graph")

parser.add_argument("--mtx", action="store_true", help = "Run mtx benchmark?")
parser.add_argument("--filepath", type = str, default = "../data/karate.mtx",
help = ".mtx file to benchmark")
parser.add_argument("--is_directed", action="store_true", help = "Is graph directed?")

parser.add_argument("-t", "--trials", type = int, default = 5,
help = "Number of trials for BFS"
)
parser.add_argument("-s", "--seed", type = int, default = 2, help = "Set seed for randomness.")

return parser

def run_rand_benchmark(args):
"""Runs rand benchmark and prints out results to terminal."""
### Build graph from randomly generated source and destination arrays.
# 1. Use Arkouda's randint to generate the random edge arrays.
src = ak.randint(0, args.n, args.m, seed=args.seed*2)
dst = ak.randint(0, args.n, args.m, seed=args.seed*4)

# 2. Build undirected graph.
print("### Arachne Graph Building")
start = time.time()
graph = ar.Graph()
graph.add_edges_from(src, dst)
end = time.time()
print(f"Building undirected graph with {len(graph)} vertices and {graph.size()} edges "
f"took {round(end-start,2)} seconds")

edges = graph.edges()
print(edges[0])
print(edges[1])

print()
print("### Arachne Square Count")
### Run Arachne square count on the input graph.
# 1. Square count on undirected graph.
square_count_trials = []
for _ in range(args.trials):
start = time.time()
square_count = ar.squares(graph)
end = time.time()
square_count_trials.append(end-start)
avg_runtime = round(st.mean(square_count_trials),2)
print(f"Running square count on undirected graph took on average {avg_runtime} seconds and "
f"counted {square_count} squares with max degree {ak.max(graph.degree())}")

# 3. Print out times in comma-delimited manner.
print("### Full Timings")
square_count_trials = [round(x,2) for x in square_count_trials]
print(f"square_count_trials = {square_count_trials}")

def run_mtx_benchmark(args):
"""Runs mtx benchmark and prints out results to terminal."""
### Build graph from matrix market file.
# 1. Use Arachne's matrix_market reading method.
print("### Arachne Graph Building")
start = time.time()
graph = ar.read_matrix_market_file(os.path.abspath(args.filepath), args.is_directed)
end = time.time()
graph_type = "directed" if args.is_directed else "undirected"
print(f"Building {graph_type} graph with {len(graph)} vertices and {graph.size()} edges took "
f"{round(end-start,2)} seconds")

print()
print("### Arachne Square Count")
### Run Arachne square count on the input graph.
# 1a. Sequential square count on undirected graph.
square_count_trials = []
for _ in range(args.trials):
start = time.time()
square_count = ar.squares(graph)
end = time.time()
square_count_trials.append(end-start)
avg_runtime = round(st.mean(square_count_trials),2)
print(f"Running sequential square count on undirected graph took on average {avg_runtime} "
f"seconds and counted {square_count} squares with max degree {ak.max(graph.degree())}")

# 1b. Print out times in comma-delimited manner.
print("### Full Timings")
square_count_trials = [round(x,2) for x in square_count_trials]
print(f"square_count_trials = {square_count_trials}")

if __name__ == "__main__":
# Command line parser and extraction.
benchmark_parser = create_parser()
in_args = benchmark_parser.parse_args()

# Connect to the Arkouda server.
ak.verbose = False
ak.connect(in_args.hostname, in_args.port)

if in_args.rand:
print("\n##### RAND BENCHMARK")
run_rand_benchmark(in_args)

if in_args.mtx:
filename = in_args.filepath.split("/")[-1]
print(f"\n##### MTX BENCHMARK FOR GRAPH {filename}")
run_mtx_benchmark(in_args)

if not in_args.rand and not in_args.mtx:
print("Unrecognized benchmark type. Terminating.")

ak.shutdown()
39 changes: 35 additions & 4 deletions arachne/client/arachne/arachne.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"bfs_layers",
"subgraph_isomorphism",
"triangles",
"squares",
"k_truss",
"triangle_centrality",
"connected_components",
Expand Down Expand Up @@ -558,7 +559,7 @@ def __init__(self, *args) -> None:

self.dtype = akint
self.logger = getArkoudaLogger(name=__class__.__name__)

def add_node_labels(self, labels:ak.DataFrame) -> None:
"""Populates the graph object with labels from a dataframe. Passed dataframe should follow
the same format specified in the Parameters section below.
Expand Down Expand Up @@ -724,7 +725,7 @@ def query_labels( self,
Returns
-------
final_vertices : pdarray
pdarray
Vertex names that contain the specified nodes.
"""
cmd = "queryLabels"
Expand Down Expand Up @@ -765,7 +766,7 @@ def query_relationships( self,
Returns
-------
(src,dst) : (pdarray,pdarray)
(pdarray,pdarray)
Source and destination vertex pairs that contain the specified edges.
"""
cmd = "queryRelationships"
Expand Down Expand Up @@ -815,7 +816,7 @@ def one_path( self,
Returns
-------
(src, dst) : (pdarray,pdarray)
(pdarray,pdarray)
Source and destination vertex pairs that contain the length one paths.
"""
# 1. Get the nodes and edges that contain the specified labels and relationships.
Expand Down Expand Up @@ -953,6 +954,36 @@ def triangles(graph: Graph, vertexArray: pdarray = None) -> pdarray:
repMsg = generic_msg(cmd=cmd,args=args)
return create_pdarray(repMsg)

@typechecked
def squares(graph: Graph) -> int:
"""
This function will return the number of squares in an undirected graph.
Parameters
----------
graph : Graph
An undirected graph whose number of squares are to be returned
Returns
-------
int
The total number of squares
See Also
--------
triangles
Raises
------
RuntimeError
"""
degree = graph.degree()
cmd = "segmentedGraphSquares"
args = { "GraphName" : graph.name,
"DegreeName" : degree.name }
rep_msg = generic_msg(cmd=cmd,args=args)
return int(rep_msg)

@typechecked
def subgraph_isomorphism(G: PropGraph, H:PropGraph, type: str = "ullmann") -> pdarray:
"""
Expand Down
1 change: 1 addition & 0 deletions arachne/server/ServerModules.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ BreadthFirstSearchMsg
PropertyGraphMsg
TriCtrMsg
TriangleCountMsg
SquareCountMsg
TrussMsg
CCMsg
76 changes: 76 additions & 0 deletions arachne/server/SquareCount.chpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module SquareCount {
// Arachne modules.
use GraphArray;

// Arkouda modules.
use MultiTypeSymbolTable;
use MultiTypeSymEntry;
use ServerConfig;
use AryUtil;

/**
* Total degree order operator u << v compares the degrees of the two nodes and returns
* true if the degree of u is less than the degree of v, or if equal, if the integer specifier
* of u is less than that of v.
*
* :arg u: vertex u
* :type u: int
* :arg v: vertex v
* :type v: int
* :arg degree: array containing degrees
* :type degree: [?D] int
*
* :returns: bool */
inline proc nodeCompare(u: int, v: int, ref degree): bool {
if degree[u] < degree[v] then return true;
else if degree[u] == degree[v] && u < v then return true;
else return false;
}

/**
* Sequential square counting for an undirected graph.
*
* :arg graph: SegGraph to run square counting on.
* :type graph: SegGraph
*
* :returns: int */
proc squareCountSequential(graph:SegGraph, degree:[?D1] int):int throws {
var src = toSymEntry(graph.getComp("SRC"),int).a;
var dst = toSymEntry(graph.getComp("DST"),int).a;
var seg = toSymEntry(graph.getComp("SEGMENTS"),int).a;

var square_count:int = 0;
var L : [0..<graph.n_vertices] int;
L = 0;

for v in L.domain {
var v_adj_list_start = seg[v];
var v_adj_list_end = seg[v+1] - 1;
ref v_neighborhood = dst.localSlice(v_adj_list_start..v_adj_list_end);
for u in v_neighborhood {
if nodeCompare(u,v,degree) {
var u_adj_list_start = seg[u];
var u_adj_list_end = seg[u+1] - 1;
ref u_neighborhood = dst.localSlice(u_adj_list_start..u_adj_list_end);
for y in u_neighborhood {
if nodeCompare(y,v,degree) {
square_count += L[y];
L[y] += 1;
}
}
}
}
for u in v_neighborhood {
if (nodeCompare(u,v,degree)) {
var u_adj_list_start = seg[u];
var u_adj_list_end = seg[u+1] - 1;
ref u_neighborhood = dst.localSlice(u_adj_list_start..u_adj_list_end);
for y in u_neighborhood {
L[y] = 0;
}
}
}
}
return square_count;
}
}
Loading

0 comments on commit 1e8396f

Please sign in to comment.