diff --git a/python/algorithms/README.md b/python/algorithms/README.md new file mode 100644 index 000000000..997e351ea --- /dev/null +++ b/python/algorithms/README.md @@ -0,0 +1,123 @@ +# Graph Algorithms with SPLA + +This project contains two implementations of graph algorithms: + +- **Classic (CPU):** Python reference implementation +- **SPLA (GPU):** implementation using sparse linear algebra primitives from SPLA + +Tests (`test_compare.py`) checks that both implementations produce identical results. + +Run tests: +```bash +python -m unittest test_compare.py -v +``` +--- + +## Supported Algorithms + +| Algorithm | Flag | +|-----------|------| +| BFS (Breadth-First Search) | `--algo bfs` | +| SSSP (Single-Source Shortest Paths) | `--algo sssp` | +| PageRank | `--algo pr` | +| Triangle Counting | `--algo tc` | + +--- + +## Installation + +```bash +cd python +python -m venv venv +source venv/bin/activate +pip install -e . +cd algorithms +``` + +--- + +## Usage + +Two entry points are available: + +- `main_spla.py` — GPU version (SPLA) +- `main_classic.py` — CPU reference version + +Both scripts use the same CLI arguments. + +```text +usage: main_spla.py [-h] --algo {bfs,sssp,pr,tc} [-m MATRIX] [-v VECTORS] [-o OUTPUT] [-s START] [-a ALPHA] [-e EPS] + +options: + -h, --help show this help message and exit + --algo {bfs,sssp,pr,tc} + Algorithm to run: + bfs - Breadth-First Search + sssp - Single-Source Shortest Paths + pr - PageRank + tc - Triangle Counting + -m MATRIX, --matrix MATRIX + Path to graph in Matrix Market format (.mtx) + -v VECTORS, --vectors VECTORS + Path to graph in vectors format (.txt) + -o OUTPUT, --output OUTPUT + Output file path (default: result.txt) + -s START, --start START + Start vertex (used in bfs, sssp; default: 0) + -a ALPHA, --alpha ALPHA + Damping factor (used in pr; default: 0.85) + -e EPS, --eps EPS + Convergence tolerance (used in pr; default: 1e-4) +``` +--- + +## Examples + +```bash +# BFS +python main_spla.py --algo bfs -m graph.mtx -s 0 -o bfs_result.txt + +# SSSP +python main_spla.py --algo sssp -v graph.txt -s 0 -o sssp_result.txt + +# PageRank +python main_spla.py --algo pr -v graph.txt -a 0.85 -e 1e-6 -o pr_result.txt + +# Triangle Counting +python main_spla.py --algo tc -m graph.mtx -o tc_result.txt + +# Classic reference version +python main_classic.py --algo bfs -v graph.txt -s 0 -o bfs_classic.txt +``` + +--- + +## Input format + +Two options: + +- `-v graph.txt` — Vectors format +- `-m graph.mtx` — Matrix Market format (from https://sparse.tamu.edu/) + + +### graph.txt + +Example +```text +3 # number of vertices n +0 1 2 # I: source vertex indices +1 2 0 # J: target vertex indices +5 3 2 # V: edge weights +``` + +### graph.mtx (Matrix Market) +Example +```text +%%MatrixMarket matrix coordinate real general +% rows cols nnz +4 4 4 #rows, cols, non-zero values +1 2 1.0 #source vertex, target vertex, weight +2 3 2.0 +3 4 3.0 +4 1 4.0 +``` \ No newline at end of file diff --git a/python/algorithms/bfs_classic.py b/python/algorithms/bfs_classic.py new file mode 100644 index 000000000..582c5a51b --- /dev/null +++ b/python/algorithms/bfs_classic.py @@ -0,0 +1,20 @@ +from collections import deque + +INF = int(1e9) + +def bfs(start, graph, n): + visited = [0] * n + dist = [INF] * n + q = deque() + visited[start] = 1 + dist[start] = 0 + q.append(start) + + while q: + u = q.popleft() + for v in graph[u]: + if not visited[v]: + visited[v] = 1 + dist[v] = dist[u] + 1 + q.append(v) + return dist \ No newline at end of file diff --git a/python/algorithms/bfs_spla.py b/python/algorithms/bfs_spla.py new file mode 100644 index 000000000..952f15705 --- /dev/null +++ b/python/algorithms/bfs_spla.py @@ -0,0 +1,17 @@ +from pyspla import * + +def bfs(s: int, A: Matrix): + v = Vector(A.n_rows, INT) + front = Vector.from_lists([s], [1], A.n_rows, INT) + front_size = 1 + depth = Scalar(INT, 0) + count = 0 + + while front_size > 0: + depth += 1 + count += front_size + v.assign(front, depth, op_assign=INT.SECOND, op_select=INT.NQZERO) + front = front.vxm(v, A, op_mult=INT.LAND, op_add=INT.LOR, op_select=INT.EQZERO) + front_size = front.reduce(op_reduce=INT.PLUS).get() + + return v, count, depth.get() \ No newline at end of file diff --git a/python/algorithms/graph.txt b/python/algorithms/graph.txt new file mode 100644 index 000000000..6a811d94e --- /dev/null +++ b/python/algorithms/graph.txt @@ -0,0 +1,4 @@ +3 +0 1 2 +1 2 0 +5 3 2 diff --git a/python/algorithms/graph_classic.py b/python/algorithms/graph_classic.py new file mode 100644 index 000000000..1096d47a3 --- /dev/null +++ b/python/algorithms/graph_classic.py @@ -0,0 +1,115 @@ +from collections import defaultdict + +def read_mtx_unweighted(filename): + n = 0 + graph = defaultdict(list) + with open(filename, 'r') as f: + for line in f: + if line.startswith('%'): + continue + parts = line.split() + if n == 0: + n = int(parts[0]) + continue + i = int(parts[0]) - 1 + j = int(parts[1]) - 1 + graph[i].append(j) + if i != j: + graph[j].append(i) + return graph, n + +def read_vectors_unweighted(filename): + with open(filename, 'r') as f: + n_line = f.readline().strip() + if not n_line: + return defaultdict(list), 0 + n = int(n_line) + I = list(map(int, f.readline().split())) + J = list(map(int, f.readline().split())) + graph = defaultdict(list) + for k in range(len(I)): + i = I[k] + j = J[k] + graph[i].append(j) + if i != j: + graph[j].append(i) + return graph, n + +def read_mtx_weighted(filename): + n = 0 + graph = defaultdict(list) + with open(filename, 'r') as f: + for line in f: + if line.startswith('%'): + continue + parts = line.split() + if n == 0: + n = int(parts[0]) + continue + i = int(parts[0]) - 1 + j = int(parts[1]) - 1 + v = float(parts[2]) if len(parts) > 2 else 1.0 + graph[i].append((j, v)) + if i != j: + graph[j].append((i, v)) + return graph, n + +def read_vectors_weighted(filename): + with open(filename, 'r') as f: + n_line = f.readline().strip() + if not n_line: + return defaultdict(list), 0 + n = int(n_line) + I = list(map(int, f.readline().split())) + J = list(map(int, f.readline().split())) + V = list(map(float, f.readline().split())) + graph = defaultdict(list) + for k in range(len(I)): + i = I[k] + j = J[k] + v = V[k] + graph[i].append((j, v)) + if i != j: + graph[j].append((i, v)) + return graph, n + +def read_mtx_pr_classic(filename): + n = 0 + adj_in = defaultdict(list) + with open(filename, 'r') as f: + for line in f: + if line.startswith('%'): + continue + parts = line.split() + if n == 0: + n = int(parts[0]) + out_degree = [0] * n + continue + i = int(parts[0]) - 1 + j = int(parts[1]) - 1 + adj_in[j].append(i) + out_degree[i] += 1 + if i != j: + adj_in[i].append(j) + out_degree[j] += 1 + return adj_in, out_degree, n + +def read_vectors_pr_classic(filename): + with open(filename, 'r') as f: + n_line = f.readline().strip() + if not n_line: + return defaultdict(list), [], 0 + n = int(n_line) + I = list(map(int, f.readline().split())) + J = list(map(int, f.readline().split())) + adj_in = defaultdict(list) + out_degree = [0] * n + for k in range(len(I)): + i = I[k] + j = J[k] + adj_in[j].append(i) + out_degree[i] += 1 + if i != j: + adj_in[i].append(j) + out_degree[j] += 1 + return adj_in, out_degree, n \ No newline at end of file diff --git a/python/algorithms/graph_spla.py b/python/algorithms/graph_spla.py new file mode 100644 index 000000000..39883b935 --- /dev/null +++ b/python/algorithms/graph_spla.py @@ -0,0 +1,117 @@ +from pyspla import * + +def read_mtx_int(filename): + row_indices, col_indices, values = [], [], [] + n = 0 + with open(filename, 'r') as f: + for line in f: + if line.startswith('%'): + continue + parts = line.split() + if n == 0: + n = int(parts[0]) + continue + i = int(parts[0]) - 1 + j = int(parts[1]) - 1 + row_indices.append(i) + col_indices.append(j) + values.append(1) + if i != j: + row_indices.append(j) + col_indices.append(i) + values.append(1) + return Matrix.from_lists(row_indices, col_indices, values, shape=(n, n), dtype=INT) + +def read_vectors_int(filename): + with open(filename, 'r') as f: + n_line = f.readline().strip() + if not n_line: + return Matrix.from_lists([], [], [], shape=(0,0), dtype=INT) + n = int(n_line) + rows = list(map(int, f.readline().split())) + cols = list(map(int, f.readline().split())) + row_indices, col_indices, values = [], [], [] + for k in range(len(rows)): + i = rows[k]; j = cols[k] + row_indices.append(i); col_indices.append(j); values.append(1) + if i != j: + row_indices.append(j); col_indices.append(i); values.append(1) + return Matrix.from_lists(row_indices, col_indices, values, shape=(n, n), dtype=INT) + +def read_mtx_float(filename): + row_indices, col_indices, values = [], [], [] + n = 0 + with open(filename, 'r') as f: + for line in f: + if line.startswith('%'): + continue + parts = line.split() + if n == 0: + n = int(parts[0]) + continue + i = int(parts[0]) - 1 + j = int(parts[1]) - 1 + w = float(parts[2]) if len(parts) > 2 else 1.0 + row_indices.append(i); col_indices.append(j); values.append(w) + if i != j: + row_indices.append(j); col_indices.append(i); values.append(w) + return Matrix.from_lists(row_indices, col_indices, values, shape=(n, n), dtype=FLOAT) + +def read_vectors_float(filename): + with open(filename, 'r') as f: + n_line = f.readline().strip() + if not n_line: + return Matrix.from_lists([], [], [], shape=(0,0), dtype=FLOAT) + n = int(n_line) + rows = list(map(int, f.readline().split())) + cols = list(map(int, f.readline().split())) + weights = list(map(float, f.readline().split())) + row_indices, col_indices, values = [], [], [] + for k in range(len(rows)): + i = rows[k]; j = cols[k]; w = weights[k] + row_indices.append(i); col_indices.append(j); values.append(w) + if i != j: + row_indices.append(j); col_indices.append(i); values.append(w) + return Matrix.from_lists(row_indices, col_indices, values, shape=(n, n), dtype=FLOAT) + +def read_mtx_pr(filename, alpha): + row_indices, col_indices = [], [] + n = 0 + with open(filename, 'r') as f: + for line in f: + if line.startswith('%'): + continue + parts = line.split() + if n == 0: + n = int(parts[0]) + out_degree = [0] * n + continue + i = int(parts[0]) - 1 + j = int(parts[1]) - 1 + row_indices.append(i); col_indices.append(j) + out_degree[i] += 1 + if i != j: + row_indices.append(j); col_indices.append(i) + out_degree[j] += 1 + values = [alpha / out_degree[u] for u in row_indices] + return Matrix.from_lists(row_indices, col_indices, values, shape=(n, n), dtype=FLOAT) + +def read_vectors_pr(filename, alpha): + with open(filename, 'r') as f: + n_line = f.readline().strip() + if not n_line: + return Matrix.from_lists([], [], [], shape=(0,0), dtype=FLOAT) + n = int(n_line) + rows = list(map(int, f.readline().split())) + cols = list(map(int, f.readline().split())) + out_degree = [0] * n + row_indices, col_indices = [], [] + for k in range(len(rows)): + i = rows[k]; j = cols[k] + row_indices.append(i); col_indices.append(j) + out_degree[i] += 1 + if i != j: + row_indices.append(j); col_indices.append(i) + out_degree[j] += 1 + values = [alpha / out_degree[u] for u in row_indices] + return Matrix.from_lists(row_indices, col_indices, values, shape=(n, n), dtype=FLOAT) \ No newline at end of file diff --git a/python/algorithms/main_classic.py b/python/algorithms/main_classic.py new file mode 100644 index 000000000..1d7c778de --- /dev/null +++ b/python/algorithms/main_classic.py @@ -0,0 +1,77 @@ +import sys +import argparse +from pathlib import Path + +from bfs_classic import bfs, INF as BFS_INF +from sssp_classic import sssp, INF as SSSP_INF +from pr_classic import pagerank_classic +from tc_classic import tc_simple + +from graph_classic import read_mtx_unweighted, read_vectors_unweighted +from graph_classic import read_mtx_weighted, read_vectors_weighted +from graph_classic import read_mtx_pr_classic, read_vectors_pr_classic + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--algo", choices=["bfs", "sssp", "pr", "tc"], required=True) + parser.add_argument("-m", "--matrix", type=Path) + parser.add_argument("-v", "--vectors", type=Path) + parser.add_argument("-o", "--output", type=Path, default="result_classic.txt") + parser.add_argument("-s", "--start", type=int, default=0) + parser.add_argument("-a", "--alpha", type=float, default=0.85) + parser.add_argument("-e", "--eps", type=float, default=1e-4) + args = parser.parse_args() + + if not args.matrix and not args.vectors: + sys.exit("Error: no graph file provided") + + if args.algo == "bfs": + if args.matrix: + graph, n = read_mtx_unweighted(str(args.matrix)) + else: + graph, n = read_vectors_unweighted(str(args.vectors)) + distances = bfs(args.start, graph, n) + with open(args.output, 'w') as f_out: + for i in range(len(distances)): + if distances[i] != BFS_INF: + f_out.write(f"{i} {distances[i]}\n") + print(f"Result saved to {args.output}") + + elif args.algo == "sssp": + if args.matrix: + graph, n = read_mtx_weighted(str(args.matrix)) + else: + graph, n = read_vectors_weighted(str(args.vectors)) + distances = sssp(args.start, graph, n) + with open(args.output, 'w') as f_out: + for i in range(len(distances)): + if distances[i] != SSSP_INF: + f_out.write(f"{i} {distances[i]}\n") + print(f"Result saved to {args.output}") + + elif args.algo == "pr": + if args.matrix: + adj_in, out_degree, n = read_mtx_pr_classic(str(args.matrix)) + else: + adj_in, out_degree, n = read_vectors_pr_classic(str(args.vectors)) + p, iters = pagerank_classic(adj_in, out_degree, n, args.alpha, args.eps) + with open(args.output, 'w') as f_out: + f_out.write(f"Iterations: {iters}\n") + for i in range(len(p)): + f_out.write(f"{i} {p[i]:.6f}\n") + print(f"Result saved to {args.output}") + + elif args.algo == "tc": + if args.matrix: + graph, n = read_mtx_unweighted(str(args.matrix)) + else: + graph, n = read_vectors_unweighted(str(args.vectors)) + for i in range(n): + graph[i].sort() + triangles = tc_simple(graph, n) + with open(args.output, 'w') as f_out: + f_out.write(f"Triangles: {triangles}\n") + print(f"Result saved to {args.output}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/python/algorithms/main_spla.py b/python/algorithms/main_spla.py new file mode 100644 index 000000000..15eb82e7e --- /dev/null +++ b/python/algorithms/main_spla.py @@ -0,0 +1,79 @@ +import sys +import argparse +from pathlib import Path + +from bfs_spla import bfs +from sssp_spla import sssp_spla, INF +from pr_spla import pagerank +from tc_spla import cohen + +from graph_spla import read_mtx_int, read_vectors_int +from graph_spla import read_mtx_float, read_vectors_float +from graph_spla import read_mtx_pr, read_vectors_pr + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--algo", choices=["bfs", "sssp", "pr", "tc"], required=True) + parser.add_argument("-m", "--matrix", type=Path) + parser.add_argument("-v", "--vectors", type=Path) + parser.add_argument("-o", "--output", type=Path, default="result.txt") + parser.add_argument("-s", "--start", type=int, default=0) + parser.add_argument("-a", "--alpha", type=float, default=0.85) + parser.add_argument("-e", "--eps", type=float, default=1e-4) + args = parser.parse_args() + + if not args.matrix and not args.vectors: + sys.exit("Error: no graph file provided") + + if args.algo == "bfs": + if args.matrix: + A = read_mtx_int(str(args.matrix)) + else: + A = read_vectors_int(str(args.vectors)) + v, count, depth = bfs(args.start, A) + with open(args.output, 'w') as f_out: + f_out.write(f"Reached vertices: {count}\n") + f_out.write(f"Max depth: {depth - 1}\n") + idx, vals = v.to_lists() + for k in range(len(idx)): + f_out.write(f"{idx[k]} {vals[k] - 1}\n") + print(f"Result saved to {args.output}") + + elif args.algo == "sssp": + if args.matrix: + A = read_mtx_float(str(args.matrix)) + else: + A = read_vectors_float(str(args.vectors)) + v = sssp_spla(args.start, A) + with open(args.output, 'w') as f_out: + idx, vals = v.to_lists() + for k in range(len(idx)): + if vals[k] < INF: + f_out.write(f"{idx[k]} {vals[k]}\n") + print(f"Result saved to {args.output}") + + elif args.algo == "pr": + if args.matrix: + A = read_mtx_pr(str(args.matrix), args.alpha) + else: + A = read_vectors_pr(str(args.vectors), args.alpha) + p, iters = pagerank(A, args.alpha, args.eps) + with open(args.output, 'w') as f_out: + f_out.write(f"Iterations: {iters}\n") + idx, vals = p.to_lists() + for k in range(len(idx)): + f_out.write(f"{idx[k]} {vals[k]:.6f}\n") + print(f"Result saved to {args.output}") + + elif args.algo == "tc": + if args.matrix: + A = read_mtx_int(str(args.matrix)) + else: + A = read_vectors_int(str(args.vectors)) + triangles = cohen(A) + with open(args.output, 'w') as f_out: + f_out.write(f"Triangles: {triangles}\n") + print(f"Result saved to {args.output}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/python/algorithms/pr_classic.py b/python/algorithms/pr_classic.py new file mode 100644 index 000000000..9c7262f18 --- /dev/null +++ b/python/algorithms/pr_classic.py @@ -0,0 +1,26 @@ +import math + +def pagerank_classic(adj_in, out_degree, n, alpha, eps): + p = [1.0 / n] * n + addition = (1.0 - alpha) / n + error = eps + 1.0 + iterations = 0 + + while error > eps: + p_next = [0.0] * n + for i in range(n): + sum_pr = 0.0 + for j in adj_in[i]: + sum_pr += p[j] / out_degree[j] + p_next[i] = alpha * sum_pr + addition + + error2 = 0.0 + for i in range(n): + diff = p_next[i] - p[i] + error2 += diff * diff + + error = math.sqrt(error2) + p = p_next + iterations += 1 + + return p, iterations \ No newline at end of file diff --git a/python/algorithms/pr_spla.py b/python/algorithms/pr_spla.py new file mode 100644 index 000000000..c7228bda6 --- /dev/null +++ b/python/algorithms/pr_spla.py @@ -0,0 +1,22 @@ +import math +from pyspla import * + +def pagerank(A: Matrix, alpha: float, eps: float): + N = A.n_rows + p = Vector.dense(N, FLOAT, 1.0 / N) + addition = Vector.dense(N, FLOAT, (1.0 - alpha) / N) + dummy_mask = Vector.dense(N, FLOAT, 1.0) + zero = Scalar(FLOAT, 0.0) + error = eps + 1.0 + iterations = 0 + + while error > eps: + p_prev = p + p_tmp = p_prev.vxm(dummy_mask, A, op_mult=FLOAT.MULT, op_add=FLOAT.PLUS, op_select=FLOAT.ALWAYS, init=zero) + p = p_tmp.eadd(FLOAT.PLUS, addition) + diff = p.eadd(FLOAT.MINUS_POW2, p_prev) + error2 = diff.reduce(FLOAT.PLUS, init=zero) + error = math.sqrt(error2.get()) + iterations += 1 + + return p, iterations \ No newline at end of file diff --git a/python/algorithms/sssp_classic.py b/python/algorithms/sssp_classic.py new file mode 100644 index 000000000..eb8766915 --- /dev/null +++ b/python/algorithms/sssp_classic.py @@ -0,0 +1,14 @@ +INF = 1e9 + +def sssp(start, graph, n): + dist = [INF] * n + dist[start] = 0 + + for i in range(n - 1): + for u in range(n): + if dist[u] != INF: + for v, w in graph[u]: + dist[v] = min(dist[v], dist[u] + w) + + return dist + diff --git a/python/algorithms/sssp_spla.py b/python/algorithms/sssp_spla.py new file mode 100644 index 000000000..bbc886d98 --- /dev/null +++ b/python/algorithms/sssp_spla.py @@ -0,0 +1,38 @@ +from pyspla import * + +INF = float(1e9) + +def sssp_spla(start: int, A: Matrix): + n = A.n_rows + initial_indices = list(range(n)) + initial_values = [INF] * n + initial_values[start] = 0.0 + dist = Vector.from_lists(initial_indices, initial_values, n, FLOAT) + + mask = Vector.dense(n, FLOAT, 1.0) + inf_scalar = Scalar(FLOAT, INF) + + for iteration in range(n - 1): + new_dist = dist.vxm(mask, A, + op_mult=FLOAT.PLUS, + op_add=FLOAT.MIN, + op_select=FLOAT.ALWAYS, + init=inf_scalar) + relaxed = dist.eadd(FLOAT.MIN, new_dist) + indices, values = relaxed.to_lists() + if start not in indices: + indices.append(start) + values.append(0.0) + dist = Vector.from_lists(indices, values, n, FLOAT) + + final_indices, final_values = dist.to_lists() + final_map = {i: INF for i in range(n)} + for k in range(len(final_indices)): + val = final_values[k] + if val == 0.0 and final_indices[k] != start: + val = INF + final_map[final_indices[k]] = val + + all_indices = list(range(n)) + all_values = [final_map[i] for i in all_indices] + return Vector.from_lists(all_indices, all_values, n, FLOAT) \ No newline at end of file diff --git a/python/algorithms/tc_classic.py b/python/algorithms/tc_classic.py new file mode 100644 index 000000000..f60ed7b72 --- /dev/null +++ b/python/algorithms/tc_classic.py @@ -0,0 +1,11 @@ +def tc_simple(graph, n): + triangles = 0 + + for i in range(n): + for j in graph[i]: + for k in graph[j]: + if k in graph[i]: + triangles += 1 + + return triangles // 6 + diff --git a/python/algorithms/tc_spla.py b/python/algorithms/tc_spla.py new file mode 100644 index 000000000..02fafa4fa --- /dev/null +++ b/python/algorithms/tc_spla.py @@ -0,0 +1,31 @@ +from pyspla import * + +def split_into_lower_upper(A: Matrix): + I, J, V = A.to_lists() + L_I, L_J, L_V = [], [], [] + U_I, U_J, U_V = [], [], [] + + for k in range(len(I)): + i = I[k] + j = J[k] + val = V[k] + if i < j: + U_I.append(i) + U_J.append(j) + U_V.append(val) + elif i > j: + L_I.append(i) + L_J.append(j) + L_V.append(val) + + rows, cols = A.shape + lower = Matrix.from_lists(L_I, L_J, L_V, shape=(rows, cols), dtype=INT) + upper = Matrix.from_lists(U_I, U_J, U_V, shape=(rows, cols), dtype=INT) + return lower, upper + +def cohen(A: Matrix): + L, U = split_into_lower_upper(A) + B = L.mxm(M=U, op_mult=INT.MULT, op_add=INT.PLUS) + C = A.emult(op_mult=INT.MULT, M=B) + total_sum = C.reduce(op_reduce=INT.PLUS).get() + return total_sum // 2 \ No newline at end of file diff --git a/python/algorithms/test_compare.py b/python/algorithms/test_compare.py new file mode 100644 index 000000000..13920f998 --- /dev/null +++ b/python/algorithms/test_compare.py @@ -0,0 +1,172 @@ +import unittest +import math +from collections import defaultdict +from pyspla import Matrix, INT, FLOAT + +from bfs_classic import bfs as bfs_c, INF as BFS_INF +from sssp_classic import sssp as sssp_c, INF as SSSP_INF +from pr_classic import pagerank_classic as pr_c +from tc_classic import tc_simple as tc_c + +from bfs_spla import bfs as bfs_s +from sssp_spla import sssp_spla as sssp_s +from pr_spla import pagerank as pr_s +from tc_spla import cohen as tc_s + +class TestCompareAlgorithms(unittest.TestCase): + + def build_bfs_graph(self, edges, n): + graph_c = defaultdict(list) + I, J, V = [], [], [] + for u, v in edges: + graph_c[u].append(v) + graph_c[v].append(u) + I.extend([u, v]) + J.extend([v, u]) + V.extend([1, 1]) + return graph_c, Matrix.from_lists(I, J, V, shape=(n, n), dtype=INT) + + def build_sssp_graph(self, edges, n): + graph_c = defaultdict(list) + I, J, V = [], [], [] + for u, v, w in edges: + graph_c[u].append((v, w)) + graph_c[v].append((u, w)) + I.extend([u, v]) + J.extend([v, u]) + V.extend([w, w]) + return graph_c, Matrix.from_lists(I, J, V, shape=(n, n), dtype=FLOAT) + + def build_tc_graph(self, edges, n): + graph_c = defaultdict(list) + I, J, V = [], [], [] + for u, v in edges: + graph_c[u].append(v) + graph_c[v].append(u) + I.extend([u, v]) + J.extend([v, u]) + V.extend([1, 1]) + return graph_c, Matrix.from_lists(I, J, V, shape=(n, n), dtype=INT) + + def build_pr_graph(self, edges, n, alpha=0.85): + adj_in = defaultdict(list) + out_degree = [0] * n + I, J, V = [], [], [] + for u, v in edges: + out_degree[u] += 1 + if u != v: + out_degree[v] += 1 + for u, v in edges: + adj_in[v].append(u) + I.append(u); J.append(v); V.append(alpha / out_degree[u]) + if u != v: + adj_in[u].append(v) + I.append(v); J.append(u); V.append(alpha / out_degree[v]) + return adj_in, out_degree, Matrix.from_lists(I, J, V, shape=(n, n), dtype=FLOAT) + + def check_lists_equal(self, list1, list2): + self.assertEqual(len(list1), len(list2)) + for k in range(len(list1)): + self.assertEqual(list1[k], list2[k], f"Mismatch at index {k}: {list1[k]} != {list2[k]}") + + def check_lists_close(self, list1, list2, tol=1e-3): + self.assertEqual(len(list1), len(list2)) + for k in range(len(list1)): + self.assertTrue(math.isclose(list1[k], list2[k], rel_tol=tol), f"Mismatch at index {k}: {list1[k]} != {list2[k]}") + + def run_bfs_test(self, edges, n, start=0): + graph_c, A_s = self.build_bfs_graph(edges, n) + dist_c = bfs_c(start, graph_c, n) + v_s, _, _ = bfs_s(start, A_s) + idx, vals = v_s.to_lists() + dist_s = [BFS_INF] * n + for k in range(len(idx)): + dist_s[idx[k]] = vals[k] - 1 + self.check_lists_equal(dist_c, dist_s) + + def test_bfs_linear(self): + self.run_bfs_test([(0, 1), (1, 2), (2, 3), (3, 4)], n=5) + + def test_bfs_star(self): + self.run_bfs_test([(0, 1), (0, 2), (0, 3), (0, 4)], n=5) + + def test_bfs_disconnected(self): + self.run_bfs_test([(0, 1), (2, 3)], n=4) + + def test_bfs_cycle(self): + self.run_bfs_test([(0, 1), (1, 2), (2, 3), (3, 0)], n=4) + + def test_bfs_fully_connected(self): + self.run_bfs_test([(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)], n=4) + + def run_sssp_test(self, edges, n, start=0): + graph_c, A_s = self.build_sssp_graph(edges, n) + dist_c = sssp_c(start, graph_c, n) + v_s = sssp_s(start, A_s) + idx, vals = v_s.to_lists() + dist_s = [int(1e9)] * n + for k in range(len(idx)): + dist_s[idx[k]] = vals[k] + self.check_lists_equal(dist_c, dist_s) + + def test_sssp_linear(self): + self.run_sssp_test([(0, 1, 5), (1, 2, 2), (2, 3, 1)], n=4) + + def test_sssp_triangle_inequality(self): + self.run_sssp_test([(0, 1, 5), (0, 2, 10), (1, 2, 1)], n=3) + + def test_sssp_isolated(self): + self.run_sssp_test([(0, 1, 3)], n=4) + + def test_sssp_two_paths(self): + self.run_sssp_test([(0, 1, 10), (1, 3, 10), (0, 2, 2), (2, 3, 2)], n=4) + + def test_sssp_complex_graph(self): + edges = [(0, 1, 4), (0, 2, 1), (2, 1, 2), (1, 3, 1), (2, 3, 5), (3, 4, 3)] + self.run_sssp_test(edges, n=5) + + def run_tc_test(self, edges, n): + graph_c, A_s = self.build_tc_graph(edges, n) + for i in range(n): + graph_c[i].sort() + self.assertEqual(tc_c(graph_c, n), tc_s(A_s)) + + def test_tc_k4(self): + self.run_tc_test([(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)], n=4) + + def test_tc_bipartite(self): + self.run_tc_test([(0,1), (1,2), (2,3), (3,0)], n=4) + + def test_tc_two_triangles(self): + self.run_tc_test([(0,1), (1,2), (2,0), (3,4), (4,5), (5,3)], n=6) + + def test_tc_empty(self): + self.run_tc_test([], n=3) + + def test_tc_one_edge(self): + self.run_tc_test([(0,1)], n=3) + + def run_pr_test(self, edges, n, alpha=0.85, eps=1e-5): + adj_in, out_degree, A_s = self.build_pr_graph(edges, n, alpha) + p_c, _ = pr_c(adj_in, out_degree, n, alpha, eps) + p_s_vec, _ = pr_s(A_s, alpha, eps) + idx, vals = p_s_vec.to_lists() + p_s = [0.0] * n + for k in range(len(idx)): + p_s[idx[k]] = vals[k] + self.check_lists_close(p_c, p_s, tol=1e-3) + + def test_pr_star(self): + self.run_pr_test([(1, 0), (2, 0), (3, 0)], n=4) + + def test_pr_chain(self): + self.run_pr_test([(0, 1), (1, 2)], n=3) + + def test_pr_complete(self): + self.run_pr_test([(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)], n=4) + + def test_pr_disconnected(self): + self.run_pr_test([(0, 1), (2, 3)], n=4) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file