diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eac04601..1f5d9104 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,20 +16,20 @@ jobs: build: runs-on: ubuntu-latest steps: - # - uses: actions/checkout@v2 - # with: - # fetch all history to find the base tg_x.y.z_dev branch - # fetch-depth: 0 - # token: ${{ secrets.QA_TOKEN }} - - uses: actions/cache@v2 + # - uses: actions/checkout@v2 + # with: + # fetch all history to find the base tg_x.y.z_dev branch + # fetch-depth: 0 + # token: ${{ secrets.QA_TOKEN }} + - uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ hashFiles('**/build.gradle', 'gradle.properties') }} - # - uses: actions/setup-java@v1 - # with: - # java-version: '11.0.10' - # - run: ./gradlew build -Psubmodule=true - # env: - # QA_TOKEN: ${{ secrets.QA_TOKEN }} + # - uses: actions/setup-java@v1 + # with: + # java-version: '11.0.10' + # - run: ./gradlew build -Psubmodule=true + # env: + # QA_TOKEN: ${{ secrets.QA_TOKEN }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3d0de516..b3bafd29 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -16,17 +16,17 @@ jobs: checkstyle: runs-on: ubuntu-latest steps: - # - uses: actions/checkout@v2 - # with: - # token: ${{ secrets.QA_TOKEN }} - - uses: actions/cache@v2 + # - uses: actions/checkout@v2 + # with: + # token: ${{ secrets.QA_TOKEN }} + - uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-${{ hashFiles('**/build.gradle', 'gradle.properties') }} - # - uses: actions/setup-java@v1 - # with: - # java-version: '11.0.10' - # - name: Run codestylechecker - # run: GLEPATH=$(pwd); source tools/scripts/bash_functions && codestylechecker + # - uses: actions/setup-java@v1 + # with: + # java-version: '11.0.10' + # - name: Run codestylechecker + # run: GLEPATH=$(pwd); source tools/scripts/bash_functions && codestylechecker diff --git a/GDBMS_ALGO/centrality/betweenness_cent.gsql b/GDBMS_ALGO/centrality/betweenness_cent.gsql index db3e1413..29e42044 100644 --- a/GDBMS_ALGO/centrality/betweenness_cent.gsql +++ b/GDBMS_ALGO/centrality/betweenness_cent.gsql @@ -2,14 +2,7 @@ CREATE TEMPLATE QUERY GDBMS_ALGO.centrality.betweenness_cent(SET v_type_ INT top_k = 100, BOOL print_results = True, STRING result_attribute = "", STRING file_path = "", BOOL display_edges = FALSE) SYNTAX V1 { - /* - First Author: karimsaraipour - First Commit Date: Sep 2, 2021 - - Recent Author: Boyu Jiang - Recent Commit Date: Mar 14, 2022 - - + /* Repository: https://github.com/tigergraph/gsql-graph-algorithms/tree/master/algorithms/Centrality @@ -46,155 +39,228 @@ CREATE TEMPLATE QUERY GDBMS_ALGO.centrality.betweenness_cent(SET v_type_ display_edges: If True, output edges for visualization */ + TYPEDEF TUPLE Vertex_Score; + HeapAccum(top_k, score DESC) @@top_scores_heap; + + /* Brandes state */ + SumAccum @C_B = 0; + MapAccum> @sigma; + MapAccum> @delta; + MapAccum @S; + + MapAccum> @sigma_next; + MapAccum> @delta_next; + + /* BFS machinery */ + BitwiseOrAccum @seen; + BitwiseOrAccum @frontier; + BitwiseOrAccum @frontier_next; + SumAccum @bit; + + /* Global */ + SumAccum @@d; + SumAccum @@batch_id = 0; + SetAccum @@batch_set; + MapAccum @@id_map; + SetAccum @@edge_set; + + BOOL is_undirected; + is_undirected = (reverse_e_type == e_type_set); - TYPEDEF TUPLE Vertex_Score; #tuple to store betweenness centrality score - HeapAccum(top_k, score DESC) @@top_scores_heap; #heap to store top K score - SumAccum @@sum_curr_dist; #current distance - BitwiseOrAccum @bitwise_or_visit_next; #use bitwise instead of setAccum - BitwiseOrAccum @bitwise_or_seen; - BitwiseOrAccum @bitwise_or_visit; - SumAccum @@sum_count = 1;#used to set unique ID - SumAccum @sum_id; #store the unique ID - SetAccum @@batch_set; #used to set unique ID - MapAccum @@map; #used to set unique ID - SetAccum @@edge_set; - SumAccum @sum_delta = 0; - MapAccum @times_map; - MapAccum> @sigma_map; - - INT empty=0; FILE f (file_path); INT num_vert; - INT batch_number; + INT num_batches; - # Compute betweenness all = {v_type_set}; num_vert = all.size(); - batch_number = num_vert/60; - - IF batch_number == 0 THEN - batch_number = 1; + num_batches = (num_vert + 59) / 60; + IF num_batches == 0 THEN + num_batches = 1; END; - #Calculate the sum of distance to other vertex for each vertex - FOREACH i IN RANGE[0, batch_number-1] DO - Current = SELECT s - FROM all:s - WHERE getvid(s)%batch_number == i - POST-ACCUM - @@map+=(getvid(s)->0), - @@batch_set+=getvid(s); - - FOREACH ver in @@batch_set DO - @@map += (ver->@@sum_count); @@sum_count += 1; - END; #set a unique ID for each vertex, ID from 1-63 + FOREACH batch_idx IN RANGE[0, num_batches - 1] DO - Start = SELECT s - FROM Current:s - POST-ACCUM - s.@sum_id=@@map.get(getvid(s)); + Current = SELECT s + FROM all:s + WHERE getvid(s) % num_batches == batch_idx + POST-ACCUM + @@id_map += (getvid(s) -> 0), + @@batch_set += getvid(s); + + FOREACH vid IN @@batch_set DO + @@id_map += (vid -> @@batch_id); + @@batch_id += 1; + END; - Start = SELECT s + /* OPTIMIZATION 1: Merged two init SELECTs into one. + Original needed two because the second SELECT read s.@bit + which was written in the first (snapshot issue). + Fix: use local variables my_bit / my_mask instead. */ + Start = SELECT s FROM Current:s - POST-ACCUM - s.@bitwise_or_seen = 1<1), - s.@times_map += (0->s.@bitwise_or_visit); # set initial seen and visit + POST-ACCUM + INT my_bit = @@id_map.get(getvid(s)), + INT my_mask = 1 << my_bit, + s.@bit = my_bit, + s.@seen = my_mask, + s.@frontier = my_mask, + s.@sigma += (my_mask -> 1), + s.@S += (0 -> my_mask); @@batch_set.clear(); - @@map.clear(); - @@sum_count=0; - - WHILE (Start.size() > 0) LIMIT max_hops DO - @@sum_curr_dist+=1; - - Start = SELECT t - FROM Start:s -(reverse_e_type:e)-v_type_set:t - WHERE s.@bitwise_or_visit&-t.@bitwise_or_seen-1>0 AND s!=t #use -t.@seen-1 to get the trverse of t.@seen - ACCUM #updatevisitNext - INT c = s.@bitwise_or_visit&-t.@bitwise_or_seen-1, - IF c>0 THEN - t.@bitwise_or_visit_next+=c, - t.@bitwise_or_seen+=c + @@id_map.clear(); + @@batch_id = 0; + @@d = 0; + + /* ═══════════════════════════════════════════ + Forward BFS + + Phase 1: expand frontier. + Reads @sigma (on s), writes @sigma_next (on t). + Phase 2: apply staged sigma. + Moves @sigma_next into @sigma. + ═══════════════════════════════════════════ */ + WHILE Start.size() > 0 LIMIT max_hops DO + @@d += 1; + + Start = SELECT t + FROM Start:s -(e_type_set:e)- v_type_set:t + WHERE (s.@frontier & (-t.@seen - 1)) > 0 AND s != t + ACCUM + INT c = s.@frontier & (-t.@seen - 1), + IF c > 0 THEN + t.@frontier_next += c, + t.@seen += c, + INT r = c, + WHILE r > 0 DO + INT b = r & (-r), + t.@sigma_next += (b -> s.@sigma.get(b)), + r = r & (r - 1) + END + END + POST-ACCUM + t.@frontier = t.@frontier_next, + t.@S += (@@d -> t.@frontier), + t.@frontier_next = 0; + + Start = SELECT s + FROM Start:s + POST-ACCUM + FOREACH (b, val) IN s.@sigma_next DO + s.@sigma += (b -> val) END, - t.@sigma_map+=(@@sum_curr_dist->s.@sigma_map.get(@@sum_curr_dist-1)) #set sigma based on depth - POST-ACCUM - t.@bitwise_or_visit=t.@bitwise_or_visit_next, - t.@times_map+=(@@sum_curr_dist->t.@bitwise_or_visit), - t.@bitwise_or_visit_next=0; + s.@sigma_next.clear(); END; - @@sum_curr_dist+=-1; + /* ═══════════════════════════════════════════ + Backward accumulation — Brandes Theorem 6 + + Phase 1: compute dependencies. + Reads @delta (on s), writes @delta_next (on t). + Phase 2 (OPTIMIZATION 2): apply staged delta + AND re-select next frontier in one SELECT. + Original had a separate SELECT for each. + ═══════════════════════════════════════════ */ + @@d += -1; + + Start = SELECT s + FROM all:s + WHERE s.@S.get(@@d) != 0; - Start = SELECT s - FROM all:s - WHERE s.@sigma_map.get(@@sum_curr_dist)!=0; + WHILE Start.size() > 0 LIMIT max_hops DO + @@d += -1; - WHILE (Start.size()>0) LIMIT max_hops DO - @@sum_curr_dist+=-1; - Start = SELECT t + tmp = SELECT t FROM Start:s -(reverse_e_type:e)- v_type_set:t - WHERE t.@times_map.get(@@sum_curr_dist)&s.@times_map.get(@@sum_curr_dist+1)!=0 - ACCUM - FLOAT currValue=t.@sigma_map.get(@@sum_curr_dist)/(s.@sigma_map.get(@@sum_curr_dist+1)*(1+s.@sum_delta)), - INT r=t.@times_map.get(@@sum_curr_dist)&s.@times_map.get(@@sum_curr_dist+1), - INT plus=0, - WHILE r>0 DO - r=r&(r-1),plus=plus+1 #count how many 1 in the number, same as setAccum,size() + WHERE (t.@S.get(@@d) & s.@S.get(@@d + 1)) != 0 + ACCUM + INT shared = t.@S.get(@@d) & s.@S.get(@@d + 1), + WHILE shared > 0 DO + INT b = shared & (-shared), + FLOAT sig_v = t.@sigma.get(b), + FLOAT sig_w = s.@sigma.get(b), + IF sig_w > 0 THEN + t.@delta_next += (b -> + (sig_v / sig_w) * (1 + s.@delta.get(b)) + ) + END, + shared = shared & (shared - 1) + END; + + /* OPTIMIZATION 2: Merged two SELECTs into one. + Original: + tmp = SELECT s FROM Start:s POST-ACCUM + Start = SELECT s FROM all:s WHERE s.@S.get(@@d) != 0; + Merged: every vertex with delta_next is at depth @@d + (by construction), so selecting FROM all WHERE @S.get(@@d)!=0 + covers them. Vertices with empty delta_next just iterate + an empty map — harmless no-op. */ + Start = SELECT s + FROM all:s + WHERE s.@S.get(@@d) != 0 + POST-ACCUM + FOREACH (b, val) IN s.@delta_next DO + s.@delta += (b -> val) END, - FLOAT value = currValue*plus/2.0, - t.@sum_delta+=value; - - Start = SELECT s - FROM all:s - WHERE s.@sigma_map.get(@@sum_curr_dist)!=0; + s.@delta_next.clear(); END; - @@sum_curr_dist=0; - Start = SELECT s - FROM all:s - POST-ACCUM - s.@bitwise_or_seen=0, - s.@bitwise_or_visit=0, - s.@sigma_map.clear(), - s.@times_map.clear(); + /* C_B(v) += Σ_{s≠v} δ_s•(v) */ + tmp = SELECT v + FROM all:v + POST-ACCUM + INT own = v.@S.get(0), + FOREACH (b, val) IN v.@delta DO + IF (b & own) == 0 THEN + v.@C_B += val + END + END; + + @@d = 0; + tmp = SELECT v + FROM all:v + POST-ACCUM + v.@seen = 0, + v.@frontier = 0, + v.@sigma.clear(), + v.@delta.clear(), + v.@sigma_next.clear(), + v.@delta_next.clear(), + v.@S.clear(); + END; + + IF is_undirected THEN + Start = SELECT v + FROM all:v + POST-ACCUM + v.@C_B = v.@C_B / 2.0; END; - #Output IF file_path != "" THEN f.println("Vertex_ID", "Betweenness"); END; - Start = SELECT s - FROM all:s - POST-ACCUM - IF result_attribute != "" THEN - s.setAttr(result_attribute, s.@sum_delta) + Start = SELECT v + FROM all:v + POST-ACCUM + IF result_attribute != "" THEN + v.setAttr(result_attribute, v.@C_B) END, - - IF print_results THEN - @@top_scores_heap += Vertex_Score(s, s.@sum_delta) + IF print_results THEN + @@top_scores_heap += Vertex_Score(v, v.@C_B) END, - - IF file_path != "" THEN - f.println(s, s.@sum_delta) + IF file_path != "" THEN + f.println(v, v.@C_B) END; IF print_results THEN PRINT @@top_scores_heap AS top_scores; - IF display_edges THEN - PRINT Start[Start.@sum_delta]; - + PRINT Start[Start.@C_B]; Start = SELECT s FROM Start:s -(e_type_set:e)-:t - ACCUM - @@edge_set += e; - + ACCUM @@edge_set += e; PRINT @@edge_set; - END; END; - } diff --git a/GDBMS_ALGO/centrality/eigenvector.gsql b/GDBMS_ALGO/centrality/eigenvector.gsql new file mode 100644 index 00000000..e747d88c --- /dev/null +++ b/GDBMS_ALGO/centrality/eigenvector.gsql @@ -0,0 +1,105 @@ +CREATE TEMPLATE QUERY GDBMS_ALGO.centrality.eigenvector (SET v_type_set, SET e_type_set, INT maximum_iteration = 100, FLOAT conv_limit = 0.000001, + INT top_k = 100, BOOL print_results = True, STRING result_attribute = "",STRING file_path = "" + ) SYNTAX V1 { + + /* + First Author: + First Commit Date: + + Recent Author: + Recent Commit Date: + + + Repository: + https://github.com/tigergraph/gsql-graph-algorithms/tree/master/algorithms/Centrality + + Maturity: + Production + + Description: + Compute eigenvector Centrality for each VERTEX. + + Publications: + NA + + TigerGraph Documentation: + https://docs.tigergraph.com/graph-ml/current/centrality-algorithms/eigenvector-centrality + + Parameters: + v_type_set: + vertex types to traverse + e_type_set: + edge types to traverse + maximum_iteration: + max iteration + conv_limit: + convergence limitation + top_k: + report only this many top scores + print_results: + If True, print the results + result_attribute: + attribute to write result to + file_path: + file to write CSV output to + */ + + TYPEDEF TUPLE Vertex_Score; + HeapAccum(top_k, score DESC) @@top_scores_heap; + SumAccum @@sum_squares_eigen_values; + SumAccum @sum_received_value; + SumAccum @sum_eigen_value = 1.0; + SumAccum @@sum_cur_norm_values; + SumAccum @@sum_prev_norm_values; + FLOAT conv_value = 9999; + FILE f (file_path); + Start = {v_type_set}; + WHILE conv_value > conv_limit LIMIT maximum_iteration DO + @@sum_squares_eigen_values = 0; + @@sum_cur_norm_values = 0; + V = SELECT s + FROM Start:s - (e_type_set:e) - v_type_set:t + ACCUM t.@sum_received_value += s.@sum_eigen_value; + V = SELECT s + FROM Start:s + POST-ACCUM s.@sum_eigen_value = s.@sum_received_value, + @@sum_squares_eigen_values += s.@sum_eigen_value * s.@sum_eigen_value, + s.@sum_received_value = 0; + + V = SELECT s + FROM V:s + POST-ACCUM s.@sum_eigen_value = s.@sum_eigen_value / sqrt(@@sum_squares_eigen_values), + @@sum_cur_norm_values += s.@sum_eigen_value; + + conv_value = abs(@@sum_cur_norm_values - @@sum_prev_norm_values); + @@sum_prev_norm_values = @@sum_cur_norm_values; + + END; + #Output + IF file_path != "" THEN + f.println("Vertex_ID", "egien vector"); + END; + Start = SELECT s + FROM Start:s + ACCUM + IF s.@sum_eigen_value==1.0 THEN + s.@sum_eigen_value+=-1 + END + POST-ACCUM + IF result_attribute != "" THEN + s.setAttr(result_attribute, s.@sum_eigen_value) + END, + + IF print_results THEN + @@top_scores_heap += Vertex_Score(s, s.@sum_eigen_value) + END, + + IF file_path != "" THEN + f.println(s, s.@sum_eigen_value) + END; + + IF print_results THEN + PRINT @@top_scores_heap AS top_scores; + END; + +} diff --git a/algorithms/Centrality/betweenness/tg_betweenness_cent.gsql b/algorithms/Centrality/betweenness/tg_betweenness_cent.gsql deleted file mode 100644 index 4de5cfbf..00000000 --- a/algorithms/Centrality/betweenness/tg_betweenness_cent.gsql +++ /dev/null @@ -1,200 +0,0 @@ -CREATE QUERY tg_betweenness_cent(SET v_type_set, SET e_type_set, SET reverse_e_type,INT max_hops = 10, - INT top_k = 100, BOOL print_results = True, STRING result_attribute = "", - STRING file_path = "", BOOL display_edges = FALSE) SYNTAX V1 { - - /* - First Author: karimsaraipour - First Commit Date: Sep 2, 2021 - - Recent Author: Boyu Jiang - Recent Commit Date: Mar 14, 2022 - - - Repository: - https://github.com/tigergraph/gsql-graph-algorithms/tree/master/algorithms/Centrality - - Maturity: - Production - - Description: - Compute Betweenness Centrality for each VERTEX. - Use multi-source BFS. - - Publications: - http://www.vldb.org/pvldb/vol8/p449-then.pdf - - TigerGraph Documentation: - https://docs.tigergraph.com/graph-ml/current/centrality-algorithms/betweenness-centrality - - Parameters: - v_type_set: - vertex types to traverse - print_results: - If True, print JSON output - e_type_set: - edge types to traverse - result_attribute: - INT attribute to store results to - reverse_e_type: - reverse edge type in directed graph, in undirected graph set reverse_e_type=e_type_set - max_hops: - look only this far from each vertex - file_path: - file to write CSV output to - top_k: - report only this many top scores - display_edges: - If True, output edges for visualization - */ - - TYPEDEF TUPLE Vertex_Score; #tuple to store betweenness centrality score - HeapAccum(top_k, score DESC) @@top_scores_heap; #heap to store top K score - SumAccum @@sum_curr_dist; #current distance - BitwiseOrAccum @bitwise_or_visit_next; #use bitwise instead of setAccum - BitwiseOrAccum @bitwise_or_seen; - BitwiseOrAccum @bitwise_or_visit; - SumAccum @@sum_count = 1;#used to set unique ID - SumAccum @sum_id; #store the unique ID - SetAccum @@batch_set; #used to set unique ID - MapAccum @@map; #used to set unique ID - SetAccum @@edge_set; - SumAccum @sum_delta = 0; - MapAccum @times_map; - MapAccum> @sigma_map; - - INT empty=0; - FILE f (file_path); - INT num_vert; - INT batch_number; - - # Compute betweenness - all = {v_type_set}; - num_vert = all.size(); - batch_number = num_vert/60; - - IF batch_number == 0 THEN - batch_number = 1; - END; - - #Calculate the sum of distance to other vertex for each vertex - FOREACH i IN RANGE[0, batch_number-1] DO - Current = SELECT s - FROM all:s - WHERE getvid(s)%batch_number == i - POST-ACCUM - @@map+=(getvid(s)->0), - @@batch_set+=getvid(s); - - FOREACH ver in @@batch_set DO - @@map += (ver->@@sum_count); @@sum_count += 1; - END; #set a unique ID for each vertex, ID from 1-63 - - Start = SELECT s - FROM Current:s - POST-ACCUM - s.@sum_id=@@map.get(getvid(s)); - - Start = SELECT s - FROM Current:s - POST-ACCUM - s.@bitwise_or_seen = 1<1), - s.@times_map += (0->s.@bitwise_or_visit); # set initial seen and visit - - @@batch_set.clear(); - @@map.clear(); - @@sum_count=0; - - WHILE (Start.size() > 0) LIMIT max_hops DO - @@sum_curr_dist+=1; - - Start = SELECT t - FROM Start:s -(reverse_e_type:e)-v_type_set:t - WHERE s.@bitwise_or_visit&-t.@bitwise_or_seen-1>0 AND s!=t #use -t.@seen-1 to get the trverse of t.@seen - ACCUM #updatevisitNext - INT c = s.@bitwise_or_visit&-t.@bitwise_or_seen-1, - IF c>0 THEN - t.@bitwise_or_visit_next+=c, - t.@bitwise_or_seen+=c - END, - t.@sigma_map+=(@@sum_curr_dist->s.@sigma_map.get(@@sum_curr_dist-1)) #set sigma based on depth - POST-ACCUM - t.@bitwise_or_visit=t.@bitwise_or_visit_next, - t.@times_map+=(@@sum_curr_dist->t.@bitwise_or_visit), - t.@bitwise_or_visit_next=0; - END; - - @@sum_curr_dist+=-1; - - Start = SELECT s - FROM all:s - WHERE s.@sigma_map.get(@@sum_curr_dist)!=0; - - WHILE (Start.size()>0) LIMIT max_hops DO - @@sum_curr_dist+=-1; - Start = SELECT t - FROM Start:s -(reverse_e_type:e)- v_type_set:t - WHERE t.@times_map.get(@@sum_curr_dist)&s.@times_map.get(@@sum_curr_dist+1)!=0 - ACCUM - FLOAT currValue=t.@sigma_map.get(@@sum_curr_dist)/(s.@sigma_map.get(@@sum_curr_dist+1)*(1+s.@sum_delta)), - INT r=t.@times_map.get(@@sum_curr_dist)&s.@times_map.get(@@sum_curr_dist+1), - INT plus=0, - WHILE r>0 DO - r=r&(r-1),plus=plus+1 #count how many 1 in the number, same as setAccum,size() - END, - FLOAT value = currValue*plus/2.0, - t.@sum_delta+=value; - - Start = SELECT s - FROM all:s - WHERE s.@sigma_map.get(@@sum_curr_dist)!=0; - END; - - @@sum_curr_dist=0; - Start = SELECT s - FROM all:s - POST-ACCUM - s.@bitwise_or_seen=0, - s.@bitwise_or_visit=0, - s.@sigma_map.clear(), - s.@times_map.clear(); - END; - - #Output - IF file_path != "" THEN - f.println("Vertex_ID", "Betweenness"); - END; - - Start = SELECT s - FROM all:s - POST-ACCUM - IF result_attribute != "" THEN - s.setAttr(result_attribute, s.@sum_delta) - END, - - IF print_results THEN - @@top_scores_heap += Vertex_Score(s, s.@sum_delta) - END, - - IF file_path != "" THEN - f.println(s, s.@sum_delta) - END; - - IF print_results THEN - PRINT @@top_scores_heap AS top_scores; - - IF display_edges THEN - PRINT Start[Start.@sum_delta]; - - Start = SELECT s - FROM Start:s -(e_type_set:e)-:t - ACCUM - @@edge_set += e; - - PRINT @@edge_set; - - END; - END; - -} diff --git a/algorithms/Centrality/betweenness/tg_betweenness_centrality.gsql b/algorithms/Centrality/betweenness/tg_betweenness_centrality.gsql new file mode 100644 index 00000000..8cd3f04e --- /dev/null +++ b/algorithms/Centrality/betweenness/tg_betweenness_centrality.gsql @@ -0,0 +1,273 @@ +CREATE OR REPLACE QUERY tg_betweenness_centrality( + SET v_type_set, + SET e_type_set, + SET reverse_e_type, + INT max_hops = 10, + INT top_k = 100, + BOOL print_results = True, + STRING result_attribute = "", + STRING file_path = "", + BOOL display_edges = FALSE +) SYNTAX V1 { + /* + Repository: + https://github.com/tigergraph/gsql-graph-algorithms/tree/master/algorithms/Centrality + + Maturity: + Production + + Description: + Compute Betweenness Centrality for each VERTEX. + Use multi-source BFS. + + Publications: + http://www.vldb.org/pvldb/vol8/p449-then.pdf + + TigerGraph Documentation: + https://docs.tigergraph.com/graph-ml/current/centrality-algorithms/betweenness-centrality + + Parameters: + v_type_set: + vertex types to traverse + print_results: + If True, print JSON output + e_type_set: + edge types to traverse + result_attribute: + INT attribute to store results to + reverse_e_type: + reverse edge type in directed graph, in undirected graph set reverse_e_type=e_type_set + max_hops: + look only this far from each vertex + file_path: + file to write CSV output to + top_k: + report only this many top scores + display_edges: + If True, output edges for visualization + */ + TYPEDEF TUPLE Vertex_Score; + HeapAccum(top_k, score DESC) @@top_scores_heap; + + /* Brandes state */ + SumAccum @C_B = 0; + MapAccum> @sigma; + MapAccum> @delta; + MapAccum @S; + + MapAccum> @sigma_next; + MapAccum> @delta_next; + + /* BFS machinery */ + BitwiseOrAccum @seen; + BitwiseOrAccum @frontier; + BitwiseOrAccum @frontier_next; + SumAccum @bit; + + /* Global */ + SumAccum @@d; + SumAccum @@batch_id = 0; + SetAccum @@batch_set; + MapAccum @@id_map; + SetAccum @@edge_set; + + BOOL is_undirected; + is_undirected = (reverse_e_type == e_type_set); + + FILE f (file_path); + INT num_vert; + INT num_batches; + + all = {v_type_set}; + num_vert = all.size(); + num_batches = (num_vert + 59) / 60; + IF num_batches == 0 THEN + num_batches = 1; + END; + + FOREACH batch_idx IN RANGE[0, num_batches - 1] DO + + Current = SELECT s + FROM all:s + WHERE getvid(s) % num_batches == batch_idx + POST-ACCUM + @@id_map += (getvid(s) -> 0), + @@batch_set += getvid(s); + + FOREACH vid IN @@batch_set DO + @@id_map += (vid -> @@batch_id); + @@batch_id += 1; + END; + + /* OPTIMIZATION 1: Merged two init SELECTs into one. + Original needed two because the second SELECT read s.@bit + which was written in the first (snapshot issue). + Fix: use local variables my_bit / my_mask instead. */ + Start = SELECT s + FROM Current:s + POST-ACCUM + INT my_bit = @@id_map.get(getvid(s)), + INT my_mask = 1 << my_bit, + s.@bit = my_bit, + s.@seen = my_mask, + s.@frontier = my_mask, + s.@sigma += (my_mask -> 1), + s.@S += (0 -> my_mask); + + @@batch_set.clear(); + @@id_map.clear(); + @@batch_id = 0; + @@d = 0; + + /* ═══════════════════════════════════════════ + Forward BFS + + Phase 1: expand frontier. + Reads @sigma (on s), writes @sigma_next (on t). + Phase 2: apply staged sigma. + Moves @sigma_next into @sigma. + ═══════════════════════════════════════════ */ + WHILE Start.size() > 0 LIMIT max_hops DO + @@d += 1; + + Start = SELECT t + FROM Start:s -(e_type_set:e)- v_type_set:t + WHERE (s.@frontier & (-t.@seen - 1)) > 0 AND s != t + ACCUM + INT c = s.@frontier & (-t.@seen - 1), + IF c > 0 THEN + t.@frontier_next += c, + t.@seen += c, + INT r = c, + WHILE r > 0 DO + INT b = r & (-r), + t.@sigma_next += (b -> s.@sigma.get(b)), + r = r & (r - 1) + END + END + POST-ACCUM + t.@frontier = t.@frontier_next, + t.@S += (@@d -> t.@frontier), + t.@frontier_next = 0; + + Start = SELECT s + FROM Start:s + POST-ACCUM + FOREACH (b, val) IN s.@sigma_next DO + s.@sigma += (b -> val) + END, + s.@sigma_next.clear(); + END; + + /* ═══════════════════════════════════════════ + Backward accumulation — Brandes Theorem 6 + + Phase 1: compute dependencies. + Reads @delta (on s), writes @delta_next (on t). + Phase 2 (OPTIMIZATION 2): apply staged delta + AND re-select next frontier in one SELECT. + Original had a separate SELECT for each. + ═══════════════════════════════════════════ */ + @@d += -1; + + Start = SELECT s + FROM all:s + WHERE s.@S.get(@@d) != 0; + + WHILE Start.size() > 0 LIMIT max_hops DO + @@d += -1; + + tmp = SELECT t + FROM Start:s -(reverse_e_type:e)- v_type_set:t + WHERE (t.@S.get(@@d) & s.@S.get(@@d + 1)) != 0 + ACCUM + INT shared = t.@S.get(@@d) & s.@S.get(@@d + 1), + WHILE shared > 0 DO + INT b = shared & (-shared), + FLOAT sig_v = t.@sigma.get(b), + FLOAT sig_w = s.@sigma.get(b), + IF sig_w > 0 THEN + t.@delta_next += (b -> + (sig_v / sig_w) * (1 + s.@delta.get(b)) + ) + END, + shared = shared & (shared - 1) + END; + + /* OPTIMIZATION 2: Merged two SELECTs into one. + Original: + tmp = SELECT s FROM Start:s POST-ACCUM + Start = SELECT s FROM all:s WHERE s.@S.get(@@d) != 0; + Merged: every vertex with delta_next is at depth @@d + (by construction), so selecting FROM all WHERE @S.get(@@d)!=0 + covers them. Vertices with empty delta_next just iterate + an empty map — harmless no-op. */ + Start = SELECT s + FROM all:s + WHERE s.@S.get(@@d) != 0 + POST-ACCUM + FOREACH (b, val) IN s.@delta_next DO + s.@delta += (b -> val) + END, + s.@delta_next.clear(); + END; + + /* C_B(v) += Σ_{s≠v} δ_s•(v) */ + tmp = SELECT v + FROM all:v + POST-ACCUM + INT own = v.@S.get(0), + FOREACH (b, val) IN v.@delta DO + IF (b & own) == 0 THEN + v.@C_B += val + END + END; + + @@d = 0; + tmp = SELECT v + FROM all:v + POST-ACCUM + v.@seen = 0, + v.@frontier = 0, + v.@sigma.clear(), + v.@delta.clear(), + v.@sigma_next.clear(), + v.@delta_next.clear(), + v.@S.clear(); + END; + + IF is_undirected THEN + Start = SELECT v + FROM all:v + POST-ACCUM + v.@C_B = v.@C_B / 2.0; + END; + + IF file_path != "" THEN + f.println("Vertex_ID", "Betweenness"); + END; + + Start = SELECT v + FROM all:v + POST-ACCUM + IF result_attribute != "" THEN + v.setAttr(result_attribute, v.@C_B) + END, + IF print_results THEN + @@top_scores_heap += Vertex_Score(v, v.@C_B) + END, + IF file_path != "" THEN + f.println(v, v.@C_B) + END; + + IF print_results THEN + PRINT @@top_scores_heap AS top_scores; + IF display_edges THEN + PRINT Start[Start.@C_B]; + Start = SELECT s + FROM Start:s -(e_type_set:e)-:t + ACCUM @@edge_set += e; + PRINT @@edge_set; + END; + END; +} diff --git a/algorithms/Centrality/eigenvector/tg_eigenvector_cent.gsql b/algorithms/Centrality/eigenvector/tg_eigenvector_cent.gsql index e3af4744..8294a9cf 100644 --- a/algorithms/Centrality/eigenvector/tg_eigenvector_cent.gsql +++ b/algorithms/Centrality/eigenvector/tg_eigenvector_cent.gsql @@ -1,4 +1,4 @@ -CREATE QUERY tg_eigenvector_cent(SET v_type_set, SET e_type_set, INT maximum_iteration = 100, FLOAT conv_limit = 0.000001, +CREATE OR REPLACE QUERY tg_eigenvector_cent(SET v_type_set, SET e_type_set, INT maximum_iteration = 100, FLOAT conv_limit = 0.000001, INT top_k = 100, BOOL print_results = True, STRING result_attribute = "",STRING file_path = "" ) SYNTAX V1 { @@ -68,7 +68,7 @@ CREATE QUERY tg_eigenvector_cent(SET v_type_set, SET e_type_set, V = SELECT s FROM V:s - POST-ACCUM s.@sum_eigen_value = s.@sum_eigen_value / sqrt(@@sum_squares_eigen_values), + POST-ACCUM s.@sum_eigen_value = s.@sum_eigen_value / (sqrt(@@sum_squares_eigen_values)+ 0.000001), @@sum_cur_norm_values += s.@sum_eigen_value; conv_value = abs(@@sum_cur_norm_values - @@sum_prev_norm_values); @@ -82,8 +82,8 @@ CREATE QUERY tg_eigenvector_cent(SET v_type_set, SET e_type_set, Start = SELECT s FROM Start:s ACCUM - IF s.@sum_eigen_value==1.0 THEN - s.@sum_eigen_value+=-1 + IF abs(s.@sum_eigen_value - 1.0) < 0.000001 THEN + s.@sum_eigen_value += -1 END POST-ACCUM IF result_attribute != "" THEN @@ -102,4 +102,4 @@ CREATE QUERY tg_eigenvector_cent(SET v_type_set, SET e_type_set, PRINT @@top_scores_heap AS top_scores; END; -} +} \ No newline at end of file diff --git a/algorithms/Centrality/pagerank/README.md b/algorithms/Centrality/pagerank/README.md index 31c4ab39..ff3c8c72 100644 --- a/algorithms/Centrality/pagerank/README.md +++ b/algorithms/Centrality/pagerank/README.md @@ -1,35 +1,207 @@ +# PageRank -# Pagerank +> **Official Documentation:** [TigerGraph PageRank Docs](https://docs.tigergraph.com/graph-ml/current/centrality-algorithms/pagerank) -#### [Pagerank Changelog](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/CHANGELOG.md) | [Discord](https://discord.gg/vFbmPyvJJN) | [Community](https://community.tigergraph.com) | [TigerGraph Starter Kits](https://github.com/zrougamed/TigerGraph-Starter-Kits-Parser) +--- -## [TigerGraph Pagerank Documentation](https://docs.tigergraph.com/graph-ml/current/centrality-algorithms/pagerank) +## Table of Contents -## Available Pagerank Algorithms +- [Overview](#overview) +- [How It Works](#how-it-works) +- [When to Use PageRank](#when-to-use-pagerank) +- [Available Algorithms](#available-algorithms) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Usage](#usage) +- [Parameters](#parameters) +- [Performance Notes](#performance-notes) +- [Resources](#resources) -* [`tg_pagerank_wt`](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/global/weighted/tg_pagerank_wt.gsql) +--- -* [`tg_pagerank`](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/global/unweighted/tg_pagerank.gsql) +## Overview -* [`tg_pagerank_pers`](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/personalized/multi_source/tg_pagerank_pers.gsql) +**PageRank** is a graph centrality algorithm originally developed by Google to rank web pages based on the structure of incoming links. In graph terms, it measures the relative importance of each vertex by considering both the number and quality of edges pointing to it — the underlying principle being that a vertex is more important if it is referenced by other important vertices. -* [`tg_pagerank_pers_ap_batch`](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/personalized/all_pairs/tg_pagerank_pers_ap_batch.gsql) +This implementation is part of the [TigerGraph Graph Data Science (GDS) Library](https://github.com/tigergraph/gsql-graph-algorithms) — a collection of open-source graph algorithms written in GSQL, TigerGraph's query language. -## Installation +--- -### Replace `` with desired algorithm listed above +## How It Works -#### Via TigerGraph CLI +PageRank assigns each vertex a score that is computed iteratively. At each iteration, a vertex distributes its current score equally (or proportionally, in the weighted variant) among its outgoing neighbors. The score of a vertex is updated as the sum of contributions received from all its incoming neighbors, adjusted by a **damping factor**. + +The formula for a vertex `v` is: + +``` +PR(v) = (1 - d) + d * Σ [ PR(u) / OutDegree(u) ] +``` + +| Symbol | Description | +|--------|-------------| +| `PR(v)` | PageRank score of vertex `v` | +| `d` | Damping factor (typically `0.85`) — models the probability of following a link vs. jumping to a random vertex | +| `PR(u)` | PageRank score of an incoming neighbor `u` | +| `OutDegree(u)` | Number of outgoing edges from vertex `u` | + +The algorithm runs for a fixed number of iterations or until scores converge below a defined threshold. + +> **Key Property:** PageRank is a global algorithm — it considers the entire graph structure, not just the local neighborhood of a vertex. + +--- + +## When to Use PageRank + +PageRank is best suited for scenarios where you need to identify the most **influential or authoritative vertices** in a graph based on connectivity patterns. + +**Common real-world applications include:** + +- **Web & Search** — Ranking web pages by authority based on hyperlink structure. +- **Social Networks** — Identifying influential users based on follower/mention relationships. +- **Citation Analysis** — Finding the most impactful research papers in academic citation graphs. +- **Fraud Detection** — Surfacing highly connected entities in transaction networks that may indicate coordinated behavior. +- **Recommendation Systems** — Ranking items or users by structural importance to improve recommendations. +- **Knowledge Graphs** — Identifying key concepts or entities in a semantic graph. + +--- + +## Available Algorithms + +| Algorithm | Variant | Description | Source | +|-----------|---------|-------------|--------| +| `tg_pagerank` | Global — Unweighted | Standard PageRank treating all edges equally. | [tg_pagerank.gsql](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/global/unweighted/tg_pagerank.gsql) | +| `tg_pagerank_wt` | Global — Weighted | PageRank where edge weights influence score distribution. | [tg_pagerank_wt.gsql](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/global/weighted/tg_pagerank_wt.gsql) | +| `tg_pagerank_pers` | Personalized — Multi-Source | Biases scores toward a specified set of source vertices. | [tg_pagerank_pers.gsql](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/personalized/multi_source/tg_pagerank_pers.gsql) | +| `tg_pagerank_pers_ap_batch` | Personalized — All Pairs Batch | Computes personalized PageRank for all vertices in batch mode. | [tg_pagerank_pers_ap_batch.gsql](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/personalized/all_pairs/tg_pagerank_pers_ap_batch.gsql) | + +### Choosing the Right Variant + +- Use **`tg_pagerank`** for general-purpose importance ranking when edge weights are not available or not relevant. +- Use **`tg_pagerank_wt`** when edges carry meaningful weights (e.g., interaction frequency, transaction volume) that should influence rank distribution. +- Use **`tg_pagerank_pers`** when you want scores biased toward a specific subset of vertices — useful for personalized recommendations or context-aware ranking. +- Use **`tg_pagerank_pers_ap_batch`** when you need personalized PageRank computed for every vertex in the graph, processed efficiently in batches. + +--- + +## Prerequisites + +Before installing, ensure the following are in place: + +- A running **TigerGraph instance** (v3.x or later recommended). +- A graph schema with directed or undirected edges, depending on your use case. +- For `tg_pagerank_wt`, edges must have a **numeric weight attribute**. +- Access to either the **TigerGraph CLI (`tg`)** or the **GSQL terminal**. + +--- + +## Installation + +Choose the installation method that matches your environment. Replace `` with the name of the desired algorithm from the table above (e.g., `tg_pagerank`). + +### Option 1 — Via TigerGraph CLI ```bash -$ tg box algos install +$ tg box algos install ``` -#### Via GSQL terminal +**Example:** ```bash +$ tg box algos install tg_pagerank +``` + +--- + +### Option 2 — Via GSQL Terminal + +1. Open the algorithm's `.gsql` source file from the links in the [Available Algorithms](#available-algorithms) section. +2. Copy the full query code. +3. In your GSQL terminal, run the following: + +```gsql +GSQL > BEGIN +# Paste the algorithm code here +GSQL > END +GSQL > INSTALL QUERY +``` + +**Example:** + +```gsql GSQL > BEGIN -# Paste code after BEGIN command -GSQL > END -GSQL > INSTALL QUERY +# Paste contents of tg_pagerank.gsql here +GSQL > END +GSQL > INSTALL QUERY tg_pagerank ``` + +--- + +## Usage + +Once installed, you can run the algorithm from the GSQL terminal or via the TigerGraph REST API. + +### Running via GSQL + +```gsql +RUN QUERY tg_pagerank( + v_type, + e_type, + max_change, + max_iter, + damping, + top_k, + print_accum, + result_attr, + file_path, + display_edges +) +``` + +### Running via REST API + +```bash +curl -X GET "http://:9000/query//tg_pagerank" \ + -d '{"v_type": "Page", "e_type": "Link", "max_iter": 25}' +``` + +--- + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `v_type` | `STRING` | The vertex type(s) to include in the computation. | +| `e_type` | `STRING` | The edge type(s) to traverse. | +| `max_change` | `FLOAT` | Convergence threshold — stops iteration when score changes fall below this value. | +| `max_iter` | `INT` | Maximum number of iterations to run. | +| `damping` | `FLOAT` | Damping factor `d` (default: `0.85`). Controls the probability of following an edge vs. teleporting. | +| `top_k` | `INT` | Number of top-ranked vertices to return in the output. | +| `print_accum` | `BOOL` | If `TRUE`, prints results to the console. | +| `result_attr` | `STRING` | Vertex attribute to write PageRank scores back to (optional). | +| `file_path` | `STRING` | File path to write results to (optional). | +| `display_edges` | `BOOL` | If `TRUE`, includes edges in the result for visualization. | +| `wt_attr` | `STRING` | *(Weighted variant only)* Edge attribute to use as the weight. | + +> **Note:** Parameter availability may vary between algorithm variants. Always refer to the [official documentation](https://docs.tigergraph.com/graph-ml/current/centrality-algorithms/pagerank) or the individual source files for exact signatures. + +--- + +## Performance Notes + +- **Convergence vs. Fixed Iterations:** Setting a `max_change` threshold allows the algorithm to stop early once scores stabilize, which is more efficient than always running to `max_iter`. For large graphs, a threshold of `0.001` is a reasonable starting point. +- **Damping Factor:** The standard damping factor is `0.85`. Lowering it causes scores to converge faster but may reduce ranking accuracy; raising it increases sensitivity to graph structure but slows convergence. +- **Graph Size & Scalability:** PageRank is computationally intensive on very large graphs. TigerGraph's distributed architecture allows the algorithm to scale across partitions — ensure your graph is distributed appropriately for optimal performance. +- **Directed vs. Undirected Graphs:** PageRank is most meaningful on **directed graphs**, where the direction of edges implies endorsement or influence. On undirected graphs, results may be less semantically meaningful. +- **Personalized Variants:** `tg_pagerank_pers_ap_batch` is designed for high-throughput scenarios. For single-source personalized queries, `tg_pagerank_pers` is more appropriate and has lower overhead. + +--- + +## Resources + +| Resource | Link | +|----------|------| +| Official PageRank Documentation | [docs.tigergraph.com](https://docs.tigergraph.com/graph-ml/current/centrality-algorithms/pagerank) | +| Algorithm Changelog | [CHANGELOG.md](https://github.com/tigergraph/gsql-graph-algorithms/blob/master/algorithms/Centrality/pagerank/CHANGELOG.md) | +| Full GDS Algorithm Library | [gsql-graph-algorithms](https://github.com/tigergraph/gsql-graph-algorithms) +| Community Forum | [community.tigergraph.com](https://community.tigergraph.com) | +| Discord | [discord.gg/vFbmPyvJJN](https://discord.gg/vFbmPyvJJN) | \ No newline at end of file diff --git a/tests/Dockerfile b/tests/Dockerfile index a9403893..1dfa1025 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -1,5 +1,5 @@ # Use an official Python runtime as a parent image -FROM python:3.9-slim +FROM python:3.11-slim # FROM ubuntu:latest # Set the working directory in the container diff --git a/tests/data/baseline/centrality/betweenness/Empty.json b/tests/data/baseline/centrality/betweenness/Empty.json new file mode 100644 index 00000000..c573a8d6 --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Empty.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 0.0}, {"Vertex_ID": "B", "score": 0.0}, {"Vertex_ID": "C", "score": 0.0}, {"Vertex_ID": "D", "score": 0.0}, {"Vertex_ID": "E", "score": 0.0}, {"Vertex_ID": "F", "score": 0.0}, {"Vertex_ID": "G", "score": 0.0}, {"Vertex_ID": "H", "score": 0.0}, {"Vertex_ID": "I", "score": 0.0}, {"Vertex_ID": "J", "score": 0.0}, {"Vertex_ID": "K", "score": 0.0}, {"Vertex_ID": "L", "score": 0.0}, {"Vertex_ID": "M", "score": 0.0}, {"Vertex_ID": "N", "score": 0.0}, {"Vertex_ID": "O", "score": 0.0}, {"Vertex_ID": "P", "score": 0.0}, {"Vertex_ID": "Q", "score": 0.0}, {"Vertex_ID": "R", "score": 0.0}, {"Vertex_ID": "S", "score": 0.0}, {"Vertex_ID": "T", "score": 0.0}]}] diff --git a/tests/data/baseline/centrality/betweenness/Hub_Spoke.json b/tests/data/baseline/centrality/betweenness/Hub_Spoke.json new file mode 100644 index 00000000..da285b12 --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Hub_Spoke.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 171.0}, {"Vertex_ID": "B", "score": 0.0}, {"Vertex_ID": "C", "score": 0.0}, {"Vertex_ID": "D", "score": 0.0}, {"Vertex_ID": "E", "score": 0.0}, {"Vertex_ID": "F", "score": 0.0}, {"Vertex_ID": "G", "score": 0.0}, {"Vertex_ID": "H", "score": 0.0}, {"Vertex_ID": "I", "score": 0.0}, {"Vertex_ID": "J", "score": 0.0}, {"Vertex_ID": "K", "score": 0.0}, {"Vertex_ID": "L", "score": 0.0}, {"Vertex_ID": "M", "score": 0.0}, {"Vertex_ID": "N", "score": 0.0}, {"Vertex_ID": "O", "score": 0.0}, {"Vertex_ID": "P", "score": 0.0}, {"Vertex_ID": "Q", "score": 0.0}, {"Vertex_ID": "R", "score": 0.0}, {"Vertex_ID": "S", "score": 0.0}, {"Vertex_ID": "T", "score": 0.0}]}] diff --git a/tests/data/baseline/centrality/betweenness/Hub_Spoke_Directed.json b/tests/data/baseline/centrality/betweenness/Hub_Spoke_Directed.json new file mode 100644 index 00000000..c573a8d6 --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Hub_Spoke_Directed.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 0.0}, {"Vertex_ID": "B", "score": 0.0}, {"Vertex_ID": "C", "score": 0.0}, {"Vertex_ID": "D", "score": 0.0}, {"Vertex_ID": "E", "score": 0.0}, {"Vertex_ID": "F", "score": 0.0}, {"Vertex_ID": "G", "score": 0.0}, {"Vertex_ID": "H", "score": 0.0}, {"Vertex_ID": "I", "score": 0.0}, {"Vertex_ID": "J", "score": 0.0}, {"Vertex_ID": "K", "score": 0.0}, {"Vertex_ID": "L", "score": 0.0}, {"Vertex_ID": "M", "score": 0.0}, {"Vertex_ID": "N", "score": 0.0}, {"Vertex_ID": "O", "score": 0.0}, {"Vertex_ID": "P", "score": 0.0}, {"Vertex_ID": "Q", "score": 0.0}, {"Vertex_ID": "R", "score": 0.0}, {"Vertex_ID": "S", "score": 0.0}, {"Vertex_ID": "T", "score": 0.0}]}] diff --git a/tests/data/baseline/centrality/betweenness/Line.json b/tests/data/baseline/centrality/betweenness/Line.json new file mode 100644 index 00000000..4f247e9f --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Line.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 0.0}, {"Vertex_ID": "B", "score": 18.0}, {"Vertex_ID": "C", "score": 34.0}, {"Vertex_ID": "D", "score": 48.0}, {"Vertex_ID": "E", "score": 60.0}, {"Vertex_ID": "F", "score": 70.0}, {"Vertex_ID": "G", "score": 78.0}, {"Vertex_ID": "H", "score": 84.0}, {"Vertex_ID": "I", "score": 88.0}, {"Vertex_ID": "J", "score": 90.0}, {"Vertex_ID": "K", "score": 90.0}, {"Vertex_ID": "L", "score": 88.0}, {"Vertex_ID": "M", "score": 84.0}, {"Vertex_ID": "N", "score": 78.0}, {"Vertex_ID": "O", "score": 70.0}, {"Vertex_ID": "P", "score": 60.0}, {"Vertex_ID": "Q", "score": 48.0}, {"Vertex_ID": "R", "score": 34.0}, {"Vertex_ID": "S", "score": 18.0}, {"Vertex_ID": "T", "score": 0.0}]}] diff --git a/tests/data/baseline/centrality/betweenness/Line_Directed.json b/tests/data/baseline/centrality/betweenness/Line_Directed.json new file mode 100644 index 00000000..4f247e9f --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Line_Directed.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 0.0}, {"Vertex_ID": "B", "score": 18.0}, {"Vertex_ID": "C", "score": 34.0}, {"Vertex_ID": "D", "score": 48.0}, {"Vertex_ID": "E", "score": 60.0}, {"Vertex_ID": "F", "score": 70.0}, {"Vertex_ID": "G", "score": 78.0}, {"Vertex_ID": "H", "score": 84.0}, {"Vertex_ID": "I", "score": 88.0}, {"Vertex_ID": "J", "score": 90.0}, {"Vertex_ID": "K", "score": 90.0}, {"Vertex_ID": "L", "score": 88.0}, {"Vertex_ID": "M", "score": 84.0}, {"Vertex_ID": "N", "score": 78.0}, {"Vertex_ID": "O", "score": 70.0}, {"Vertex_ID": "P", "score": 60.0}, {"Vertex_ID": "Q", "score": 48.0}, {"Vertex_ID": "R", "score": 34.0}, {"Vertex_ID": "S", "score": 18.0}, {"Vertex_ID": "T", "score": 0.0}]}] diff --git a/tests/data/baseline/centrality/betweenness/Ring.json b/tests/data/baseline/centrality/betweenness/Ring.json new file mode 100644 index 00000000..82bec2cf --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Ring.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 40.5}, {"Vertex_ID": "B", "score": 40.5}, {"Vertex_ID": "C", "score": 40.5}, {"Vertex_ID": "D", "score": 40.5}, {"Vertex_ID": "E", "score": 40.5}, {"Vertex_ID": "F", "score": 40.5}, {"Vertex_ID": "G", "score": 40.5}, {"Vertex_ID": "H", "score": 40.5}, {"Vertex_ID": "I", "score": 40.5}, {"Vertex_ID": "J", "score": 40.5}, {"Vertex_ID": "K", "score": 40.5}, {"Vertex_ID": "L", "score": 40.5}, {"Vertex_ID": "M", "score": 40.5}, {"Vertex_ID": "N", "score": 40.5}, {"Vertex_ID": "O", "score": 40.5}, {"Vertex_ID": "P", "score": 40.5}, {"Vertex_ID": "Q", "score": 40.5}, {"Vertex_ID": "R", "score": 40.5}, {"Vertex_ID": "S", "score": 40.5}, {"Vertex_ID": "T", "score": 40.5}]}] diff --git a/tests/data/baseline/centrality/betweenness/Ring_Directed.json b/tests/data/baseline/centrality/betweenness/Ring_Directed.json new file mode 100644 index 00000000..3736dd55 --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Ring_Directed.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 171.0}, {"Vertex_ID": "B", "score": 171.0}, {"Vertex_ID": "C", "score": 171.0}, {"Vertex_ID": "D", "score": 171.0}, {"Vertex_ID": "E", "score": 171.0}, {"Vertex_ID": "F", "score": 171.0}, {"Vertex_ID": "G", "score": 171.0}, {"Vertex_ID": "H", "score": 171.0}, {"Vertex_ID": "I", "score": 171.0}, {"Vertex_ID": "J", "score": 171.0}, {"Vertex_ID": "K", "score": 171.0}, {"Vertex_ID": "L", "score": 171.0}, {"Vertex_ID": "M", "score": 171.0}, {"Vertex_ID": "N", "score": 171.0}, {"Vertex_ID": "O", "score": 171.0}, {"Vertex_ID": "P", "score": 171.0}, {"Vertex_ID": "Q", "score": 171.0}, {"Vertex_ID": "R", "score": 171.0}, {"Vertex_ID": "S", "score": 171.0}, {"Vertex_ID": "T", "score": 171.0}]}] diff --git a/tests/data/baseline/centrality/betweenness/Tree.json b/tests/data/baseline/centrality/betweenness/Tree.json new file mode 100644 index 00000000..224fb68d --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Tree.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 84.0}, {"Vertex_ID": "B", "score": 116.0}, {"Vertex_ID": "C", "score": 87.0}, {"Vertex_ID": "D", "score": 87.0}, {"Vertex_ID": "E", "score": 50.0}, {"Vertex_ID": "F", "score": 35.0}, {"Vertex_ID": "G", "score": 35.0}, {"Vertex_ID": "H", "score": 35.0}, {"Vertex_ID": "I", "score": 35.0}, {"Vertex_ID": "J", "score": 18.0}, {"Vertex_ID": "K", "score": 0.0}, {"Vertex_ID": "L", "score": 0.0}, {"Vertex_ID": "M", "score": 0.0}, {"Vertex_ID": "N", "score": 0.0}, {"Vertex_ID": "O", "score": 0.0}, {"Vertex_ID": "P", "score": 0.0}, {"Vertex_ID": "Q", "score": 0.0}, {"Vertex_ID": "R", "score": 0.0}, {"Vertex_ID": "S", "score": 0.0}, {"Vertex_ID": "T", "score": 0.0}]}] diff --git a/tests/data/baseline/centrality/betweenness/Tree_Directed.json b/tests/data/baseline/centrality/betweenness/Tree_Directed.json new file mode 100644 index 00000000..a94f738b --- /dev/null +++ b/tests/data/baseline/centrality/betweenness/Tree_Directed.json @@ -0,0 +1 @@ +[{"top_scores": [{"Vertex_ID": "A", "score": 0.0}, {"Vertex_ID": "B", "score": 11.0}, {"Vertex_ID": "C", "score": 6.0}, {"Vertex_ID": "D", "score": 12.0}, {"Vertex_ID": "E", "score": 6.0}, {"Vertex_ID": "F", "score": 4.0}, {"Vertex_ID": "G", "score": 4.0}, {"Vertex_ID": "H", "score": 6.0}, {"Vertex_ID": "I", "score": 6.0}, {"Vertex_ID": "J", "score": 3.0}, {"Vertex_ID": "K", "score": 0.0}, {"Vertex_ID": "L", "score": 0.0}, {"Vertex_ID": "M", "score": 0.0}, {"Vertex_ID": "N", "score": 0.0}, {"Vertex_ID": "O", "score": 0.0}, {"Vertex_ID": "P", "score": 0.0}, {"Vertex_ID": "Q", "score": 0.0}, {"Vertex_ID": "R", "score": 0.0}, {"Vertex_ID": "S", "score": 0.0}, {"Vertex_ID": "T", "score": 0.0}]}] diff --git a/tests/data/baseline/centrality/eigenvector/Empty.json b/tests/data/baseline/centrality/eigenvector/Empty.json new file mode 100644 index 00000000..3f632dc6 --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Empty.json @@ -0,0 +1,5 @@ +[ + { + "start": [] + } +] \ No newline at end of file diff --git a/tests/data/baseline/centrality/eigenvector/Hub_Spoke.json b/tests/data/baseline/centrality/eigenvector/Hub_Spoke.json new file mode 100644 index 00000000..85423b10 --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Hub_Spoke.json @@ -0,0 +1,86 @@ +[ + { + "top_scores": [ + { + "Vertex_ID": "A", + "score": 0.2236068 + }, + { + "Vertex_ID": "J", + "score": 0.2236067 + }, + { + "Vertex_ID": "F", + "score": 0.2236067 + }, + { + "Vertex_ID": "I", + "score": 0.2236067 + }, + { + "Vertex_ID": "E", + "score": 0.2236067 + }, + { + "Vertex_ID": "P", + "score": 0.2236067 + }, + { + "Vertex_ID": "Q", + "score": 0.2236067 + }, + { + "Vertex_ID": "L", + "score": 0.2236067 + }, + { + "Vertex_ID": "K", + "score": 0.2236067 + }, + { + "Vertex_ID": "M", + "score": 0.2236067 + }, + { + "Vertex_ID": "T", + "score": 0.2236067 + }, + { + "Vertex_ID": "H", + "score": 0.2236067 + }, + { + "Vertex_ID": "R", + "score": 0.2236067 + }, + { + "Vertex_ID": "N", + "score": 0.2236067 + }, + { + "Vertex_ID": "O", + "score": 0.2236067 + }, + { + "Vertex_ID": "C", + "score": 0.2236067 + }, + { + "Vertex_ID": "G", + "score": 0.2236067 + }, + { + "Vertex_ID": "D", + "score": 0.2236067 + }, + { + "Vertex_ID": "S", + "score": 0.2236067 + }, + { + "Vertex_ID": "B", + "score": 0.2236067 + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/centrality/eigenvector/Hub_Spoke_Directed.json b/tests/data/baseline/centrality/eigenvector/Hub_Spoke_Directed.json new file mode 100644 index 00000000..5732a5bf --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Hub_Spoke_Directed.json @@ -0,0 +1,86 @@ +[ + { + "top_scores": [ + { + "Vertex_ID": "G", + "score": 0 + }, + { + "Vertex_ID": "L", + "score": 0 + }, + { + "Vertex_ID": "H", + "score": 0 + }, + { + "Vertex_ID": "I", + "score": 0 + }, + { + "Vertex_ID": "O", + "score": 0 + }, + { + "Vertex_ID": "T", + "score": 0 + }, + { + "Vertex_ID": "D", + "score": 0 + }, + { + "Vertex_ID": "E", + "score": 0 + }, + { + "Vertex_ID": "R", + "score": 0 + }, + { + "Vertex_ID": "A", + "score": 0 + }, + { + "Vertex_ID": "P", + "score": 0 + }, + { + "Vertex_ID": "F", + "score": 0 + }, + { + "Vertex_ID": "S", + "score": 0 + }, + { + "Vertex_ID": "M", + "score": 0 + }, + { + "Vertex_ID": "K", + "score": 0 + }, + { + "Vertex_ID": "N", + "score": 0 + }, + { + "Vertex_ID": "J", + "score": 0 + }, + { + "Vertex_ID": "C", + "score": 0 + }, + { + "Vertex_ID": "B", + "score": 0 + }, + { + "Vertex_ID": "Q", + "score": 0 + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/centrality/eigenvector/Line.json b/tests/data/baseline/centrality/eigenvector/Line.json new file mode 100644 index 00000000..723fcd99 --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Line.json @@ -0,0 +1,86 @@ +[ + { + "top_scores": [ + { + "Vertex_ID": "K", + "score": 0.3077177 + }, + { + "Vertex_ID": "J", + "score": 0.3077177 + }, + { + "Vertex_ID": "I", + "score": 0.3009113 + }, + { + "Vertex_ID": "L", + "score": 0.3009113 + }, + { + "Vertex_ID": "M", + "score": 0.2871923 + }, + { + "Vertex_ID": "H", + "score": 0.2871923 + }, + { + "Vertex_ID": "N", + "score": 0.2673597 + }, + { + "Vertex_ID": "G", + "score": 0.2673597 + }, + { + "Vertex_ID": "F", + "score": 0.2411713 + }, + { + "Vertex_ID": "O", + "score": 0.2411713 + }, + { + "Vertex_ID": "P", + "score": 0.2100265 + }, + { + "Vertex_ID": "E", + "score": 0.2100265 + }, + { + "Vertex_ID": "D", + "score": 0.1737472 + }, + { + "Vertex_ID": "Q", + "score": 0.1737472 + }, + { + "Vertex_ID": "R", + "score": 0.1339974 + }, + { + "Vertex_ID": "C", + "score": 0.1339974 + }, + { + "Vertex_ID": "B", + "score": 0.09090643 + }, + { + "Vertex_ID": "S", + "score": 0.09090643 + }, + { + "Vertex_ID": "A", + "score": 0.04603298 + }, + { + "Vertex_ID": "T", + "score": 0.04603298 + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/centrality/eigenvector/Line_Directed.json b/tests/data/baseline/centrality/eigenvector/Line_Directed.json new file mode 100644 index 00000000..9cf0b334 --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Line_Directed.json @@ -0,0 +1,86 @@ +[ + { + "top_scores": [ + { + "Vertex_ID": "Q", + "score": 0 + }, + { + "Vertex_ID": "D", + "score": 0 + }, + { + "Vertex_ID": "L", + "score": 0 + }, + { + "Vertex_ID": "T", + "score": 0 + }, + { + "Vertex_ID": "R", + "score": 0 + }, + { + "Vertex_ID": "S", + "score": 0 + }, + { + "Vertex_ID": "O", + "score": 0 + }, + { + "Vertex_ID": "C", + "score": 0 + }, + { + "Vertex_ID": "I", + "score": 0 + }, + { + "Vertex_ID": "G", + "score": 0 + }, + { + "Vertex_ID": "J", + "score": 0 + }, + { + "Vertex_ID": "H", + "score": 0 + }, + { + "Vertex_ID": "M", + "score": 0 + }, + { + "Vertex_ID": "K", + "score": 0 + }, + { + "Vertex_ID": "N", + "score": 0 + }, + { + "Vertex_ID": "P", + "score": 0 + }, + { + "Vertex_ID": "F", + "score": 0 + }, + { + "Vertex_ID": "A", + "score": 0 + }, + { + "Vertex_ID": "E", + "score": 0 + }, + { + "Vertex_ID": "B", + "score": 0 + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/centrality/eigenvector/Ring.json b/tests/data/baseline/centrality/eigenvector/Ring.json new file mode 100644 index 00000000..62311ea9 --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Ring.json @@ -0,0 +1,86 @@ +[ + { + "top_scores": [ + { + "Vertex_ID": "R", + "score": 0.2236067 + }, + { + "Vertex_ID": "S", + "score": 0.2236067 + }, + { + "Vertex_ID": "A", + "score": 0.2236067 + }, + { + "Vertex_ID": "N", + "score": 0.2236067 + }, + { + "Vertex_ID": "M", + "score": 0.2236067 + }, + { + "Vertex_ID": "O", + "score": 0.2236067 + }, + { + "Vertex_ID": "C", + "score": 0.2236067 + }, + { + "Vertex_ID": "Q", + "score": 0.2236067 + }, + { + "Vertex_ID": "L", + "score": 0.2236067 + }, + { + "Vertex_ID": "G", + "score": 0.2236067 + }, + { + "Vertex_ID": "P", + "score": 0.2236067 + }, + { + "Vertex_ID": "H", + "score": 0.2236067 + }, + { + "Vertex_ID": "D", + "score": 0.2236067 + }, + { + "Vertex_ID": "E", + "score": 0.2236067 + }, + { + "Vertex_ID": "F", + "score": 0.2236067 + }, + { + "Vertex_ID": "J", + "score": 0.2236067 + }, + { + "Vertex_ID": "I", + "score": 0.2236067 + }, + { + "Vertex_ID": "K", + "score": 0.2236067 + }, + { + "Vertex_ID": "T", + "score": 0.2236067 + }, + { + "Vertex_ID": "B", + "score": 0.2236067 + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/centrality/eigenvector/Ring_Directed.json b/tests/data/baseline/centrality/eigenvector/Ring_Directed.json new file mode 100644 index 00000000..413901ca --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Ring_Directed.json @@ -0,0 +1,86 @@ +[ + { + "top_scores": [ + { + "Vertex_ID": "B", + "score": 0.2236066 + }, + { + "Vertex_ID": "K", + "score": 0.2236066 + }, + { + "Vertex_ID": "S", + "score": 0.2236066 + }, + { + "Vertex_ID": "R", + "score": 0.2236066 + }, + { + "Vertex_ID": "D", + "score": 0.2236066 + }, + { + "Vertex_ID": "A", + "score": 0.2236066 + }, + { + "Vertex_ID": "O", + "score": 0.2236066 + }, + { + "Vertex_ID": "C", + "score": 0.2236066 + }, + { + "Vertex_ID": "G", + "score": 0.2236066 + }, + { + "Vertex_ID": "H", + "score": 0.2236066 + }, + { + "Vertex_ID": "J", + "score": 0.2236066 + }, + { + "Vertex_ID": "P", + "score": 0.2236066 + }, + { + "Vertex_ID": "Q", + "score": 0.2236066 + }, + { + "Vertex_ID": "L", + "score": 0.2236066 + }, + { + "Vertex_ID": "I", + "score": 0.2236066 + }, + { + "Vertex_ID": "T", + "score": 0.2236066 + }, + { + "Vertex_ID": "F", + "score": 0.2236066 + }, + { + "Vertex_ID": "E", + "score": 0.2236066 + }, + { + "Vertex_ID": "N", + "score": 0.2236066 + }, + { + "Vertex_ID": "M", + "score": 0.2236066 + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/centrality/eigenvector/Tree.json b/tests/data/baseline/centrality/eigenvector/Tree.json new file mode 100644 index 00000000..53ac914d --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Tree.json @@ -0,0 +1,86 @@ +[ + { + "top_scores": [ + { + "Vertex_ID": "B", + "score": 0.431496 + }, + { + "Vertex_ID": "D", + "score": 0.4238448 + }, + { + "Vertex_ID": "A", + "score": 0.3149156 + }, + { + "Vertex_ID": "E", + "score": 0.3112659 + }, + { + "Vertex_ID": "C", + "score": 0.2920688 + }, + { + "Vertex_ID": "H", + "score": 0.2711738 + }, + { + "Vertex_ID": "I", + "score": 0.2711738 + }, + { + "Vertex_ID": "F", + "score": 0.1979092 + }, + { + "Vertex_ID": "G", + "score": 0.1979092 + }, + { + "Vertex_ID": "J", + "score": 0.155771 + }, + { + "Vertex_ID": "K", + "score": 0.127911 + }, + { + "Vertex_ID": "P", + "score": 0.1180227 + }, + { + "Vertex_ID": "R", + "score": 0.1180227 + }, + { + "Vertex_ID": "S", + "score": 0.1180227 + }, + { + "Vertex_ID": "Q", + "score": 0.1180227 + }, + { + "Vertex_ID": "M", + "score": 0.08132886 + }, + { + "Vertex_ID": "O", + "score": 0.08132886 + }, + { + "Vertex_ID": "L", + "score": 0.08132886 + }, + { + "Vertex_ID": "N", + "score": 0.08132886 + }, + { + "Vertex_ID": "T", + "score": 0.06779598 + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/centrality/eigenvector/Tree_Directed.json b/tests/data/baseline/centrality/eigenvector/Tree_Directed.json new file mode 100644 index 00000000..234bd393 --- /dev/null +++ b/tests/data/baseline/centrality/eigenvector/Tree_Directed.json @@ -0,0 +1,86 @@ +[ + { + "top_scores": [ + { + "Vertex_ID": "D", + "score": 0 + }, + { + "Vertex_ID": "F", + "score": 0 + }, + { + "Vertex_ID": "E", + "score": 0 + }, + { + "Vertex_ID": "P", + "score": 0 + }, + { + "Vertex_ID": "O", + "score": 0 + }, + { + "Vertex_ID": "G", + "score": 0 + }, + { + "Vertex_ID": "H", + "score": 0 + }, + { + "Vertex_ID": "I", + "score": 0 + }, + { + "Vertex_ID": "M", + "score": 0 + }, + { + "Vertex_ID": "K", + "score": 0 + }, + { + "Vertex_ID": "S", + "score": 0 + }, + { + "Vertex_ID": "N", + "score": 0 + }, + { + "Vertex_ID": "Q", + "score": 0 + }, + { + "Vertex_ID": "L", + "score": 0 + }, + { + "Vertex_ID": "R", + "score": 0 + }, + { + "Vertex_ID": "A", + "score": 0 + }, + { + "Vertex_ID": "T", + "score": 0 + }, + { + "Vertex_ID": "C", + "score": 0 + }, + { + "Vertex_ID": "J", + "score": 0 + }, + { + "Vertex_ID": "B", + "score": 0 + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Empty.json b/tests/data/baseline/classification/graph_coloring/Empty.json new file mode 100644 index 00000000..9a4a8dbd --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Empty.json @@ -0,0 +1,5 @@ +[ + { + "start": [] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Hub_Spoke.json b/tests/data/baseline/classification/graph_coloring/Hub_Spoke.json new file mode 100644 index 00000000..f5e0025e --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Hub_Spoke.json @@ -0,0 +1,146 @@ +[ + { + "start": [ + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "B", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "S", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "Q", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "L", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "O", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "C", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "G", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "H", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "R", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "A", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "M", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "K", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "N", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "D", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "E", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "I", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "P", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "T", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "F", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "J", + "v_type": "V20" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Hub_Spoke_Directed.json b/tests/data/baseline/classification/graph_coloring/Hub_Spoke_Directed.json new file mode 100644 index 00000000..fc318f2b --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Hub_Spoke_Directed.json @@ -0,0 +1,139 @@ +[ + { + "start": [ + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "B", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "T", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "G", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "H", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "O", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "C", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "M", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "K", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "N", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "Q", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "L", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "I", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "R", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "P", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "S", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "F", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "J", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "D", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "E", + "v_type": "V20" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Line.json b/tests/data/baseline/classification/graph_coloring/Line.json new file mode 100644 index 00000000..41a130e1 --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Line.json @@ -0,0 +1,146 @@ +[ + { + "start": [ + { + "attributes": { + "start.@sum_color_vertex": 3 + }, + "v_id": "Q", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "L", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "I", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "B", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "R", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "A", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 3 + }, + "v_id": "G", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "H", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "O", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "C", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "S", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "P", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 3 + }, + "v_id": "M", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "K", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "N", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "F", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "J", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "T", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "D", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "E", + "v_type": "V20" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Line_Directed.json b/tests/data/baseline/classification/graph_coloring/Line_Directed.json new file mode 100644 index 00000000..3e82af00 --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Line_Directed.json @@ -0,0 +1,139 @@ +[ + { + "start": [ + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "P", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "G", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "H", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "B", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "F", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "J", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "R", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "I", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "M", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "K", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "N", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "Q", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "L", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "D", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "E", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "T", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "S", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "O", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "C", + "v_type": "V20" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Ring.json b/tests/data/baseline/classification/graph_coloring/Ring.json new file mode 100644 index 00000000..b3d1b984 --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Ring.json @@ -0,0 +1,146 @@ +[ + { + "start": [ + { + "attributes": { + "start.@sum_color_vertex": 3 + }, + "v_id": "G", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "H", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 3 + }, + "v_id": "Q", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "L", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 3 + }, + "v_id": "M", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "K", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "N", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "B", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "T", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "O", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "C", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 3 + }, + "v_id": "S", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "P", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "F", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "J", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "D", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "E", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "I", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "R", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "A", + "v_type": "V20" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Ring_Directed.json b/tests/data/baseline/classification/graph_coloring/Ring_Directed.json new file mode 100644 index 00000000..0cd16628 --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Ring_Directed.json @@ -0,0 +1,146 @@ +[ + { + "start": [ + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "B", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "S", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "F", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "J", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "R", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "A", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "Q", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "L", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "M", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "K", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "N", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "O", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "C", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "G", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "H", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "D", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "E", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "I", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "P", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "T", + "v_type": "V20" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Tree.json b/tests/data/baseline/classification/graph_coloring/Tree.json new file mode 100644 index 00000000..8b5c7290 --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Tree.json @@ -0,0 +1,146 @@ +[ + { + "start": [ + { + "attributes": { + "start.@sum_color_vertex": 3 + }, + "v_id": "B", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "D", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "E", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "Q", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "L", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "R", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "A", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "G", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "H", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "M", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "K", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "N", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "I", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "P", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "S", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "O", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "C", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "T", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "F", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "J", + "v_type": "V20" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/classification/graph_coloring/Tree_Directed.json b/tests/data/baseline/classification/graph_coloring/Tree_Directed.json new file mode 100644 index 00000000..fb0ceec1 --- /dev/null +++ b/tests/data/baseline/classification/graph_coloring/Tree_Directed.json @@ -0,0 +1,139 @@ +[ + { + "start": [ + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "I", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "P", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "Q", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "L", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "O", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "C", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "G", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "H", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "B", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "R", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "M", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "K", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "N", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "D", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "E", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "S", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 1 + }, + "v_id": "T", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "F", + "v_type": "V20" + }, + { + "attributes": { + "start.@sum_color_vertex": 2 + }, + "v_id": "J", + "v_type": "V20" + } + ] + } +] \ No newline at end of file diff --git a/tests/data/baseline/ml/fastRP.json.gz b/tests/data/baseline/ml/fastRP.json.gz index 2fa29708..246922d8 100644 Binary files a/tests/data/baseline/ml/fastRP.json.gz and b/tests/data/baseline/ml/fastRP.json.gz differ diff --git a/tests/requirements.txt b/tests/requirements.txt index af43a402..ae41afbe 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -19,14 +19,14 @@ jmespath==1.0.1 joblib==1.4.0 kiwisolver==1.4.5 MarkupSafe==2.1.5 -matplotlib==3.9.0 +matplotlib==3.10.8 mpmath==1.3.0 multidict==6.0.5 networkx==3.3 -numpy==1.26.4 +numpy==2.4.3 packaging==24.0 -pandas==2.1.1 -pillow==10.3.0 +pandas==3.0.1 +pillow==12.1.1 pluggy==1.5.0 psutil==5.9.8 py==1.11.0 @@ -35,12 +35,13 @@ pytest==8.2.1 pytest-xdist==3.5.0 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -pyTigerGraph==1.5.2 +pyTigerGraph[gds]==1.8.0 +pyTigerGraph==1.5.2 # use updated version when available on PyPI inorder to fix any issues pytz==2024.1 requests==2.31.0 s3transfer==0.7.0 -scikit-learn==1.4.2 -scipy==1.13.0 +scikit-learn==1.8.0 +scipy==1.17.1 six==1.16.0 sympy==1.12 threadpoolctl==3.4.0 diff --git a/tests/run.sh b/tests/run.sh index b3a6b214..0f198024 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,4 +1,4 @@ clear -python3 test/setup.py && - python3 test/baseline/create_baselines.py && - pytest test/test_centrality.py #test/test_ml.py +# python3 test/setup.py && +python3 test/baseline/create_baselines.py #&& + # pytest test/test_centrality.py #test/test_ml.py diff --git a/tests/test/baseline/algos/__init__.py b/tests/test/baseline/algos/__init__.py index 328e264d..4f10fb3a 100644 --- a/tests/test/baseline/algos/__init__.py +++ b/tests/test/baseline/algos/__init__.py @@ -1,2 +1,7 @@ from .degree_cent import * from .fastrp import fastrp_wrapper as fastrp +from .page_rank import ( + run_pagerank_baseline, + tg_pagerank, + tg_pagerank_wt +) \ No newline at end of file diff --git a/tests/test/baseline/algos/page_rank.py b/tests/test/baseline/algos/page_rank.py new file mode 100644 index 00000000..6ccfbf31 --- /dev/null +++ b/tests/test/baseline/algos/page_rank.py @@ -0,0 +1,123 @@ +import networkx as nx + + +def tg_pagerank( + g: nx.Graph, + damping: float = 0.85, + max_change: float = 0.05, + maximum_iteration: int = 10, +): + """Replicates TigerGraph's tg_pagerank GSQL query exactly. + + Key GSQL behaviour reproduced here: + - Every vertex starts with score = 1.0 + - Each iteration, ONLY vertices that have outgoing edges (appear as 's' + in the FROM clause) get their score updated via POST-ACCUM. + Vertices with out-degree 0 are never selected as 's', so their score + stays at 1.0 for the entire run. + - For undirected graphs every vertex has degree >= 1 (assuming no + isolated nodes), so all vertices are updated each iteration. + - Stops when max score change across updated vertices <= max_change, + or maximum_iteration is reached. + + Parameters match what the existing baselines were generated with: + damping=0.85, max_change=0.05, maximum_iteration=25 + """ + nodes = list(g.nodes()) + scores = {n: 1.0 for n in nodes} + + for _ in range(maximum_iteration): + # Accumulate received scores into a temp dict (all nodes start at 0) + recvd = {n: 0.0 for n in nodes} + for u in nodes: + out_deg = g.out_degree(u) if g.is_directed() else g.degree(u) + if out_deg == 0: + continue + for v in (g.successors(u) if g.is_directed() else g.neighbors(u)): + recvd[v] += scores[u] / out_deg + + max_diff = 0.0 + new_scores = dict(scores) # copy — preserves score=1.0 for out-deg-0 nodes + + for u in nodes: + out_deg = g.out_degree(u) if g.is_directed() else g.degree(u) + if out_deg == 0: + # GSQL never selects this vertex as 's' → score never changes + continue + updated = (1.0 - damping) + damping * recvd[u] + max_diff = max(max_diff, abs(updated - scores[u])) + new_scores[u] = updated + + scores = new_scores + if max_diff <= max_change: + break + + return scores + + +def tg_pagerank_wt( + g: nx.Graph, + damping: float = 0.85, + max_change: float = 0.05, + maximum_iteration: int = 10, +): + """Replicates TigerGraph's tg_pagerank_wt GSQL query exactly. + + Same POST-ACCUM scoping rule as tg_pagerank: only vertices with + outgoing edges (total_wt > 0) get their score updated each iteration. + + Parameters match what the existing baselines were generated with: + damping=0.85, max_change=0.05, maximum_iteration=25 + """ + nodes = list(g.nodes()) + scores = {n: 1.0 for n in nodes} + + # Pre-compute total outgoing weight per node (matches GSQL's @sum_total_wt) + total_wt = {} + for u in nodes: + neighbors = list(g.successors(u)) if g.is_directed() else list(g.neighbors(u)) + total_wt[u] = sum(g[u][nbr].get("weight", 1.0) for nbr in neighbors) + + for _ in range(maximum_iteration): + recvd = {n: 0.0 for n in nodes} + for u in nodes: + if total_wt[u] == 0: + continue + for v in (g.successors(u) if g.is_directed() else g.neighbors(u)): + edge_wt = g[u][v].get("weight", 1.0) + recvd[v] += scores[u] * edge_wt / total_wt[u] + + max_diff = 0.0 + new_scores = dict(scores) # preserves score=1.0 for zero-weight nodes + + for u in nodes: + if total_wt[u] == 0: + # GSQL never selects this vertex as 's' → score never changes + continue + updated = (1.0 - damping) + damping * recvd[u] + max_diff = max(max_diff, abs(updated - scores[u])) + new_scores[u] = updated + + scores = new_scores + if max_diff <= max_change: + break + + return scores + + +def run_pagerank_baseline(g: nx.Graph, metric): + """Generic runner for tg_pagerank / tg_pagerank_wt. + + Output format matches TigerGraph's GSQL PageRank baseline: + - Key is @@top_scores_heap + - Scores are raw (around 1.0), matching GSQL's initial score=1.0 + - Results are sorted descending by score + """ + res = metric(g) + + out = sorted( + [{"Vertex_ID": k, "score": round(v, 7)} for k, v in res.items()], + key=lambda x: x["score"], + reverse=True, + ) + return [{"@@top_scores_heap": out}] \ No newline at end of file diff --git a/tests/test/baseline/betweenness_baseline.py b/tests/test/baseline/betweenness_baseline.py new file mode 100644 index 00000000..b39c8643 --- /dev/null +++ b/tests/test/baseline/betweenness_baseline.py @@ -0,0 +1,388 @@ +""" +Betweenness Centrality Baselines (Unweighted) — Brandes (2001) + +This file generates baseline JSON outputs for TigerGraph's graph_algorithms_testing repo, +mirroring the JSON structure used by the other baseline generators. + +Algorithm: Brandes Algorithm 1 (unweighted) + - forward BFS builds: S, P, σ, d + - backward accumulation builds δ and adds into BC + +Output format: + [{"top_scores": [{"Vertex_ID": "...", "score": }, ...]}] + +TigerGraph note: + TigerGraph docs describe the final aggregation as: + BC(v) = Σ_s PD_{s*}(v) / 2 + so we apply a final division by 2 in this baseline to match that convention. :contentReference[oaicite:1]{index=1} +""" + +import csv +import json +from collections import deque +from dataclasses import dataclass +from pathlib import Path +from typing import Protocol + + +# ---------------------------- +# Types (match your repo style) +# ---------------------------- + +type VertexId = str +type Dist = int +type Sigma = float +type Delta = float +type BC = float + +type VertexMap[T] = dict[VertexId, T] + +type Neighbors = set[VertexId] +type Adjacency = VertexMap[Neighbors] + +type Predecessors = list[VertexId] +type VertexPredecessors = VertexMap[Predecessors] + +type VertexDist = VertexMap[Dist] +type VertexSigma = VertexMap[Sigma] +type VertexDelta = VertexMap[Delta] +type VertexBetweenness = VertexMap[BC] + + +# ---------------------------- +# Graph interface + adjacency +# ---------------------------- + + +class UnweightedGraph(Protocol): + @property + def directed(self) -> bool: ... + + def vertices(self) -> list[VertexId]: ... + + def neighbors(self, v: VertexId) -> Neighbors: ... + + +@dataclass(slots=True, kw_only=True) +class AdjacencyUnweighted: + adjacency: Adjacency + directed: bool = False + + def vertices(self) -> list[VertexId]: + return list(self.adjacency.keys()) + + def neighbors(self, v: VertexId) -> Neighbors: + return self.adjacency[v] + + +# ---------------------------- +# Brandes per-source state +# ---------------------------- + + +@dataclass(slots=True, kw_only=True) +class ShortestPathState: + """ + Per-source Brandes state: + + S : stack of vertices in nondecreasing distance from s + P[w] : predecessors of w on shortest paths from s + σ[w] : # shortest paths from s to w + d[w] : distance from s to w + """ + + source: VertexId + stack: list[VertexId] + pred: VertexPredecessors + sigma: VertexSigma + dist: VertexDist + + @classmethod + def initialize( + cls, *, source: VertexId, vertices: list[VertexId] + ) -> "ShortestPathState": + dist: VertexDist = {v: -1 for v in vertices} + sigma: VertexSigma = {v: 0.0 for v in vertices} + pred: VertexPredecessors = {v: [] for v in vertices} + stack: list[VertexId] = [] + return cls(source=source, stack=stack, pred=pred, sigma=sigma, dist=dist) + + +# ---------------------------- +# Forward pass (BFS) +# ---------------------------- + + +def forward_bfs( + graph: UnweightedGraph, + source: VertexId, + *, + vertices: list[VertexId], +) -> ShortestPathState: + """ + Brandes Algorithm 1 — forward BFS from a single source s. + + Builds: + - S: stack in nondecreasing distance order + - P[w]: predecessors on shortest paths + - σ[w]: number of shortest paths from s to w + - d[w]: hop distance from s + """ + state = ShortestPathState.initialize(source=source, vertices=vertices) + + state.dist[source] = 0 + state.sigma[source] = 1.0 + + q: deque[VertexId] = deque([source]) + + while q: + v = q.popleft() + state.stack.append(v) + + dv: Dist = state.dist[v] + + # Deterministic neighbor iteration (Neighbors is a set) + for w in sorted(graph.neighbors(v)): + if state.dist[w] < 0: + state.dist[w] = dv + 1 + q.append(w) + + if state.dist[w] == dv + 1: + state.sigma[w] += state.sigma[v] + state.pred[w].append(v) + + return state + + +# ---------------------------- +# Backward accumulation +# ---------------------------- + + +def dependencies(state: ShortestPathState) -> VertexDelta: + """ + Compute δ for a fixed source s (δ[v] ≡ δ_{s*}(v)). + """ + delta: VertexDelta = {v: 0.0 for v in state.dist.keys()} + + for w in reversed(state.stack): + sigma_w = state.sigma[w] + if sigma_w == 0.0: + # unreachable nodes shouldn't appear in stack; treat as invariant failure + raise ValueError(f"sigma[{w}] is 0. Forward pass invariant violated.") + + coeff = (1.0 + delta[w]) / sigma_w + for v in state.pred[w]: + delta[v] += state.sigma[v] * coeff + + return delta + + +def accumulate(state: ShortestPathState, bc: VertexBetweenness) -> None: + """ + Accumulate betweenness contribution for a single source s (endpoints excluded): + BC[w] += δ[w] for w != s + """ + delta = dependencies(state) + s = state.source + for w in state.stack: + if w != s: + bc[w] += delta[w] + + +def accumulate_endpoints(state: ShortestPathState, bc: VertexBetweenness) -> None: + """ + Endpoint-included variant (NetworkX-style shape): + BC[s] += |S| - 1 + BC[w] += δ[w] + 1 for w != s + """ + s = state.source + bc[s] += float(len(state.stack) - 1) + + delta: VertexDelta = {v: 0.0 for v in state.dist.keys()} + + for w in reversed(state.stack): + sigma_w = state.sigma[w] + if sigma_w == 0.0: + raise ValueError(f"sigma[{w}] is 0. Forward pass invariant violated.") + + coeff = (1.0 + delta[w]) / sigma_w + for v in state.pred[w]: + delta[v] += state.sigma[v] * coeff + + if w != s: + bc[w] += delta[w] + 1.0 + + +# ---------------------------- +# Post-processing +# ---------------------------- + + +def normalize( + bc: VertexBetweenness, + *, + n: int, + directed: bool, + endpoints: bool, +) -> None: + """ + Optional normalization (NetworkX convention): + directed: divide by N(N-1) + undirected: divide by N(N-1)/2 + where N = n if endpoints else n-1 + """ + if n <= 2: + return + + N = n if endpoints else (n - 1) + if N <= 1: + return + + denom = N * (N - 1) + scale = (1.0 / denom) if directed else (2.0 / denom) + + for v in bc: + bc[v] *= scale + + +def calculate_score( + graph: UnweightedGraph, + *, + normalized: bool = False, + endpoints: bool = False, +) -> VertexBetweenness: + """ + Compute betweenness centrality for all vertices (Brandes 2001, unweighted). + """ + vertices = graph.vertices() + bc: VertexBetweenness = {v: 0.0 for v in vertices} + + for s in vertices: + state = forward_bfs(graph, s, vertices=vertices) + if endpoints: + accumulate_endpoints(state, bc) + else: + accumulate(state, bc) + + # Compute BC(v) = Σ_s PD_{s*}(v) / 2 + if not graph.directed: + for v in bc: + bc[v] *= 0.5 + if normalized: + normalize(bc, n=len(vertices), directed=graph.directed, endpoints=endpoints) + + return bc + + +# ---------------------------- +# I/O helpers for this repo +# ---------------------------- + + +def _load_vertex_ids(path: Path) -> list[VertexId]: + """ + Load vertex IDs from a CSV (one id per line OR id in first column). + This repo has data/twenty_nodes.csv and data/eight_nodes.csv. + """ + ids: list[VertexId] = [] + with path.open(newline="") as f: + reader = csv.reader(f) + for row in reader: + if not row: + continue + v = row[0].strip() + if not v: + continue + ids.append(v) + return ids + + +def _load_unweighted_edge_csv(path: Path) -> list[tuple[VertexId, VertexId]]: + edges: list[tuple[VertexId, VertexId]] = [] + with path.open(newline="") as f: + reader = csv.reader(f) + for row in reader: + if not row: + continue + if len(row) == 1 and row[0].strip() == "(empty)": + continue + u = row[0].strip() + v = row[1].strip() + edges.append((u, v)) + return edges + + +def _build_graph( + *, + vertices: list[VertexId], + edges: list[tuple[VertexId, VertexId]], + directed: bool, +) -> AdjacencyUnweighted: + adjacency: Adjacency = {v: set() for v in vertices} + + def ensure(v: VertexId) -> None: + if v not in adjacency: + adjacency[v] = set() + + for u, v in edges: + ensure(u) + ensure(v) + adjacency[u].add(v) + if not directed: + adjacency[v].add(u) + + return AdjacencyUnweighted(adjacency=adjacency, directed=directed) + + +def _format_baseline( + scores: VertexBetweenness, +) -> list[dict[str, list[dict[str, float]]]]: + # Deterministic ordering by Vertex_ID (tests usually sort anyway) + out = [{"Vertex_ID": v, "score": float(scores[v])} for v in sorted(scores)] + return [{"top_scores": out}] + + +# ---------------------------- +# Baseline generator entrypoint +# ---------------------------- + + +def run() -> None: + data_root = Path("data") + edges_root = data_root / "unweighted_edges" + + out_root = data_root / "baseline" / "centrality" / "betweenness" + out_root.mkdir(parents=True, exist_ok=True) + + v20 = _load_vertex_ids(data_root / "twenty_nodes.csv") + + jobs: list[tuple[str, bool, str]] = [ + ("empty_graph_edges.csv", False, "Empty.json"), + ("hubspoke_edges.csv", False, "Hub_Spoke.json"), + ("hubspoke_edges.csv", True, "Hub_Spoke_Directed.json"), + ("line_edges.csv", False, "Line.json"), + ("line_edges.csv", True, "Line_Directed.json"), + ("ring_edges.csv", False, "Ring.json"), + ("ring_edges.csv", True, "Ring_Directed.json"), + ("tree_edges.csv", False, "Tree.json"), + ("tree_edges.csv", True, "Tree_Directed.json"), + ] + + for csv_name, directed, out_name in jobs: + edges = _load_unweighted_edge_csv(edges_root / csv_name) + graph = _build_graph(vertices=v20, edges=edges, directed=directed) + + scores = calculate_score(graph, normalized=False, endpoints=False) + payload = _format_baseline(scores) + + out_path = out_root / out_name + with out_path.open("w", encoding="utf-8", newline="\n") as f: + json.dump(payload, f) + _ = f.write("\n") + + print(f"Wrote {out_path}") + + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/tests/test/baseline/create_baselines.py b/tests/test/baseline/create_baselines.py index b2e0a946..683403ab 100644 --- a/tests/test/baseline/create_baselines.py +++ b/tests/test/baseline/create_baselines.py @@ -1,5 +1,7 @@ +import betweenness_baseline import degree_cent_baseline import fast_rp_baseline +import pagerank_baseline if __name__ == "__main__": degree_cent_baseline.run() diff --git a/tests/test/baseline/pagerank_baseline.py b/tests/test/baseline/pagerank_baseline.py new file mode 100644 index 00000000..ddedb320 --- /dev/null +++ b/tests/test/baseline/pagerank_baseline.py @@ -0,0 +1,152 @@ +import csv +import json + +import networkx as nx +import numpy as np +from algos import run_pagerank_baseline, tg_pagerank, tg_pagerank_wt +from tqdm import tqdm + +data_path_root = "data/" +baseline_path_root = f"{data_path_root}/baseline/" + + +def create_graph(edges, weights=False, directed=False): + if directed: + g = nx.DiGraph() + else: + g = nx.Graph() + if weights: + edges = [[a, b, float(c)] for a, b, c in edges] + g.add_weighted_edges_from(edges) + else: + g.add_edges_from(edges) + return g + + +def create_pagerank_baseline(paths): + t = tqdm(paths, desc="Creating PageRank baselines") + for p, out_path, fn, m in t: + t.set_postfix_str(out_path.split("/")[-1].split(".")[0]) + with open(p) as f: + edges = np.array(list(csv.reader(f))) + + directed = True if "Directed" in out_path else False + weights = True if "Weighted" in out_path else False + g = create_graph(edges, weights, directed) + + res = fn(g, m) + print(out_path.split("/")[-1].split(".")[0]) + print(res) + # with open(out_path, "w") as f: + # json.dump(res, f) + + +def run(): + # (data, output_path, fn, metric) + paths = [ + # ── PageRank (unweighted, undirected) ───────────────────────────────── + ( + f"{data_path_root}/unweighted_edges/line_edges.csv", + f"{baseline_path_root}/centrality/pagerank/Line.json", + run_pagerank_baseline, + tg_pagerank, + ), + ( + f"{data_path_root}/unweighted_edges/ring_edges.csv", + f"{baseline_path_root}/centrality/pagerank/Ring.json", + run_pagerank_baseline, + tg_pagerank, + ), + ( + f"{data_path_root}/unweighted_edges/hubspoke_edges.csv", + f"{baseline_path_root}/centrality/pagerank/Hub_Spoke.json", + run_pagerank_baseline, + tg_pagerank, + ), + ( + f"{data_path_root}/unweighted_edges/tree_edges.csv", + f"{baseline_path_root}/centrality/pagerank/Tree.json", + run_pagerank_baseline, + tg_pagerank, + ), + # ── PageRank (unweighted, directed) ─────────────────────────────────── + ( + f"{data_path_root}/unweighted_edges/line_edges.csv", + f"{baseline_path_root}/centrality/pagerank/Line_Directed.json", + run_pagerank_baseline, + tg_pagerank, + ), + ( + f"{data_path_root}/unweighted_edges/ring_edges.csv", + f"{baseline_path_root}/centrality/pagerank/Ring_Directed.json", + run_pagerank_baseline, + tg_pagerank, + ), + ( + f"{data_path_root}/unweighted_edges/hubspoke_edges.csv", + f"{baseline_path_root}/centrality/pagerank/Hub_Spoke_Directed.json", + run_pagerank_baseline, + tg_pagerank, + ), + ( + f"{data_path_root}/unweighted_edges/tree_edges.csv", + f"{baseline_path_root}/centrality/pagerank/Tree_Directed.json", + run_pagerank_baseline, + tg_pagerank, + ), + # ── Weighted PageRank ────────────────────────────────────────────────── + ( + f"{data_path_root}/weighted_edges/line_edges.csv", + f"{baseline_path_root}/centrality/pagerank_wt/Line_Weighted.json", + run_pagerank_baseline, + tg_pagerank_wt, + ), + ( + f"{data_path_root}/weighted_edges/ring_edges.csv", + f"{baseline_path_root}/centrality/pagerank_wt/Ring_Weighted.json", + run_pagerank_baseline, + tg_pagerank_wt, + ), + ( + f"{data_path_root}/weighted_edges/hubspoke_edges.csv", + f"{baseline_path_root}/centrality/pagerank_wt/Hub_Spoke_Weighted.json", + run_pagerank_baseline, + tg_pagerank_wt, + ), + ( + f"{data_path_root}/weighted_edges/tree_edges.csv", + f"{baseline_path_root}/centrality/pagerank_wt/Tree_Weighted.json", + run_pagerank_baseline, + tg_pagerank_wt, + ), + # ── Weighted PageRank (directed) ─────────────────────────────────────── + ( + f"{data_path_root}/weighted_edges/line_edges.csv", + f"{baseline_path_root}/centrality/pagerank_wt/Line_Directed_Weighted.json", + run_pagerank_baseline, + tg_pagerank_wt, + ), + ( + f"{data_path_root}/weighted_edges/ring_edges.csv", + f"{baseline_path_root}/centrality/pagerank_wt/Ring_Directed_Weighted.json", + run_pagerank_baseline, + tg_pagerank_wt, + ), + ( + f"{data_path_root}/weighted_edges/hubspoke_edges.csv", + f"{baseline_path_root}/centrality/pagerank_wt/Hub_Spoke_Directed_Weighted.json", + run_pagerank_baseline, + tg_pagerank_wt, + ), + ( + f"{data_path_root}/weighted_edges/tree_edges.csv", + f"{baseline_path_root}/centrality/pagerank_wt/Tree_Directed_Weighted.json", + run_pagerank_baseline, + tg_pagerank_wt, + ), + ] + create_pagerank_baseline(paths) + + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/tests/test/setup.py b/tests/test/setup.py index fad28088..e3903336 100644 --- a/tests/test/setup.py +++ b/tests/test/setup.py @@ -7,6 +7,7 @@ from dotenv import load_dotenv from pyTigerGraph.datasets import Datasets from tqdm import tqdm, trange +from pathlib import Path import util @@ -15,6 +16,26 @@ pattern = re.compile(r'"name":\s*"tg_.*"') +def install_brandes_betweenness_query(conn: tg.TigerGraphConnection) -> None: + query_path = ( + Path(__file__).resolve().parents[1] + / "algorithms" + / "Centrality" + / "betweenness" + / "brandes_betweenness.gsql" + ) + + query_text = query_path.read_text(encoding="utf-8") + + conn.gsql( + f""" +USE GRAPH {graph_name} +{query_text} +INSTALL QUERY brandes_betweenness +""" + ) + + def add_reverse_edge(ds: Datasets): with open(f"{dataset.tmp_dir}/{ds.name}/create_schema.gsql") as f: schema: str = f.read() @@ -61,6 +82,7 @@ def add_reverse_edge(ds: Datasets): if q not in installed_queries: print(q) feat.installAlgorithm(q) + install_brandes_betweenness_query(conn) for _ in trange(30, desc="Sleeping while data loads"): time.sleep(1) diff --git a/tests/test/test_betweenness.py b/tests/test/test_betweenness.py new file mode 100644 index 00000000..9ab7995c --- /dev/null +++ b/tests/test/test_betweenness.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import json +import os +from dataclasses import dataclass +from pathlib import Path + +import pytest +import pyTigerGraph as tg +from dotenv import load_dotenv + +load_dotenv() + +QUERY_NAME = "tg_betweenness_centrality" + + +@dataclass(frozen=True, slots=True) +class Case: + name: str + edge_type: str + vertex_type: str + directed: bool + + +CASES: tuple[Case, ...] = ( + Case("Empty", "Empty", "V20", False), + Case("Line", "Line", "V20", False), + Case("Ring", "Ring", "V20", False), + Case("Hub_Spoke", "Hub_Spoke", "V20", False), + Case("Tree", "Tree", "V20", False), + Case("Line_Directed", "Line_Directed", "V20", True), + Case("Ring_Directed", "Ring_Directed", "V20", True), + Case("Hub_Spoke_Directed", "Hub_Spoke_Directed", "V20", True), + Case("Tree_Directed", "Tree_Directed", "V20", True), +) + + +def _load_json(path: Path) -> object: + return json.loads(path.read_text()) + + +def _score_map(payload: object) -> dict[str, float]: + """ + Expected payload shape: + [ + { + "top_scores": [ + {"Vertex_ID": "...", "score": ...}, + ... + ] + } + ] + """ + if not isinstance(payload, list) or not payload: + raise AssertionError(f"Unexpected payload shape: {type(payload)} {payload!r}") + + first = payload[0] + if not isinstance(first, dict): + raise AssertionError(f"Unexpected payload[0] shape: {type(first)} {first!r}") + + top_scores = first.get("top_scores") + if not isinstance(top_scores, list): + raise AssertionError(f"Missing/invalid top_scores: {top_scores!r}") + + scores: dict[str, float] = {} + for row in top_scores: + if not isinstance(row, dict): + raise AssertionError(f"Bad score row: {row!r}") + + vertex_id = row.get("Vertex_ID") + score = row.get("score") + if vertex_id is None or score is None: + raise AssertionError(f"Missing keys in row: {row!r}") + + scores[str(vertex_id)] = float(score) + + return scores + + +def _reverse_e_type(edge_type: str, directed: bool) -> list[str]: + if directed: + return [f"reverse_{edge_type}"] + return [edge_type] + + +def _query_params(case: Case) -> dict[str, object]: + return { + "v_type_set": [case.vertex_type], + "e_type_set": [case.edge_type], + "reverse_e_type": _reverse_e_type(case.edge_type, case.directed), + "max_hops": 100, + "top_k": 1000, + "print_results": True, + "result_attribute": "", + "file_path": "", + "display_edges": False, + } + + +class TestBetweenness: + conn = tg.TigerGraphConnection( + host=os.getenv("HOST_NAME"), + username=os.getenv("USER_NAME"), + password=os.getenv("PASS"), + graphname="graph_algorithms_testing", + ) + + if os.environ.get("USE_TKN", "true").lower() == "true": + conn.getToken() + + @pytest.mark.parametrize("case", CASES, ids=lambda c: c.name) + def test_betweenness_unweighted(self, case: Case) -> None: + baseline_path = Path(f"data/baseline/centrality/betweenness/{case.name}.json") + baseline = _score_map(_load_json(baseline_path)) + + params = _query_params(case) + result_json = self.conn.runInstalledQuery(QUERY_NAME, params=params) + result = _score_map(result_json) + + assert result.keys() == baseline.keys(), ( + f"{case.name}: key mismatch.\n" + f"Missing: {sorted(baseline.keys() - result.keys())}\n" + f"Extra: {sorted(result.keys() - baseline.keys())}\n" + f"query={QUERY_NAME!r}, reverse_e_type={params['reverse_e_type']!r}" + ) + + for vertex_id, expected in baseline.items(): + got = result[vertex_id] + assert got == pytest.approx(expected, rel=1e-12, abs=1e-12), ( + f"{case.name}: {vertex_id}: got={got} expected={expected} " + f"(query={QUERY_NAME!r}, reverse_e_type={params['reverse_e_type']!r})" + ) diff --git a/tests/test/test_centrality.py b/tests/test/test_centrality.py index 6f36295b..891db7a1 100644 --- a/tests/test/test_centrality.py +++ b/tests/test/test_centrality.py @@ -7,6 +7,7 @@ class TestCentrality: feat = util.get_featurizer() + undirected_graphs = [ "Empty", "Line", @@ -37,6 +38,19 @@ class TestCentrality: "Complete", ] + @staticmethod + def _sorted_top_scores(payload, key="top_scores"): + return sorted(payload[0][key], key=lambda x: x["Vertex_ID"]) + + @staticmethod + def _assert_top_scores_match(result, baseline): + for b in baseline: + for r in result: + if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( + b["score"] + ): + pytest.fail(f"{r['Vertex_ID']}: {r['score']} != {b['score']}") + @pytest.mark.parametrize("test_name", undirected_graphs) def test_degree_centrality1(self, test_name): params = { @@ -54,16 +68,10 @@ def test_degree_centrality1(self, test_name): baseline = json.load(f) result = self.feat.runAlgorithm("tg_degree_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - # pytest.fail(str(result)) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", directed_graphs) def test_degree_centrality2(self, test_name): @@ -84,15 +92,10 @@ def test_degree_centrality2(self, test_name): baseline = json.load(f) result = self.feat.runAlgorithm("tg_degree_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", directed_graphs) def test_degree_centrality3(self, test_name): @@ -113,15 +116,10 @@ def test_degree_centrality3(self, test_name): baseline = json.load(f) result = self.feat.runAlgorithm("tg_degree_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", complete_graphs) def test_degree_centrality4(self, test_name): @@ -137,15 +135,10 @@ def test_degree_centrality4(self, test_name): baseline = json.load(f) result = self.feat.runAlgorithm("tg_degree_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", weighted_undirected_graphs) def test_weighted_degree_centrality1(self, test_name): @@ -165,17 +158,12 @@ def test_weighted_degree_centrality1(self, test_name): f"data/baseline/centrality/weighted_degree_centrality/{test_name}.json" ) as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_weighted_degree_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - print(result) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", weighted_directed_graphs) def test_weighted_degree_centrality2(self, test_name): @@ -196,16 +184,12 @@ def test_weighted_degree_centrality2(self, test_name): f"data/baseline/centrality/weighted_degree_centrality/in_degree/{test_name}.json" ) as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_weighted_degree_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", weighted_directed_graphs) def test_weighted_degree_centrality3(self, test_name): @@ -226,16 +210,12 @@ def test_weighted_degree_centrality3(self, test_name): f"data/baseline/centrality/weighted_degree_centrality/out_degree/{test_name}.json" ) as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_weighted_degree_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", undirected_graphs) def test_closeness_centrality(self, test_name): @@ -255,16 +235,12 @@ def test_closeness_centrality(self, test_name): f"data/baseline/centrality/closeness_centrality/{test_name}.json" ) as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_closeness_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", directed_graphs) def test_closeness_centrality2(self, test_name): @@ -284,7 +260,30 @@ def test_closeness_centrality2(self, test_name): f"data/baseline/centrality/closeness_centrality/{test_name}.json" ) as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_closeness_cent", params=params) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) + + self._assert_top_scores_match(result, baseline) + + @pytest.mark.parametrize("test_name", undirected_graphs) + def test_eigenvector_centrality(self, test_name): + params = { + "v_type_set": ["V20"], + "e_type_set": [test_name], + "maximum_iteration": 100, + "conv_limit": 0.000001, + "top_k": 100, + "print_results": True, + "result_attribute": "", + "file_path": "" + } + with open( + f"data/baseline/centrality/eigenvector/{test_name}.json" + ) as f: + baseline = json.load(f) + result = self.feat.runAlgorithm("tg_eigenvector_cent", params=params) result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) @@ -294,6 +293,7 @@ def test_closeness_centrality2(self, test_name): b["score"] ): pytest.fail(f'{r["score"]} != {b["score"]}') + @pytest.mark.parametrize("test_name", undirected_graphs) def test_harmonic_centrality(self, test_name): @@ -313,16 +313,12 @@ def test_harmonic_centrality(self, test_name): f"data/baseline/centrality/harmonic_centrality/{test_name}.json" ) as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_harmonic_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", directed_graphs) def test_harmonic_centrality2(self, test_name): @@ -342,16 +338,12 @@ def test_harmonic_centrality2(self, test_name): f"data/baseline/centrality/harmonic_centrality/{test_name}.json" ) as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_harmonic_cent", params=params) - result = sorted(result[0]["top_scores"], key=lambda x: x["Vertex_ID"]) - baseline = sorted(baseline[0]["top_scores"], key=lambda x: x["Vertex_ID"]) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", undirected_graphs + directed_graphs) def test_article_rank(self, test_name): @@ -368,18 +360,12 @@ def test_article_rank(self, test_name): } with open(f"data/baseline/centrality/article_rank/{test_name}.json") as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_article_rank", params=params) - result = sorted(result[0]["@@top_scores_heap"], key=lambda x: x["Vertex_ID"]) - baseline = sorted( - baseline[0]["@@top_scores_heap"], key=lambda x: x["Vertex_ID"] - ) + result = self._sorted_top_scores(result, key="@@top_scores_heap") + baseline = self._sorted_top_scores(baseline, key="@@top_scores_heap") - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) @pytest.mark.parametrize("test_name", undirected_graphs + directed_graphs) def test_pagerank(self, test_name): @@ -397,15 +383,59 @@ def test_pagerank(self, test_name): } with open(f"data/baseline/centrality/pagerank/{test_name}.json") as f: baseline = json.load(f) + result = self.feat.runAlgorithm("tg_pagerank", params=params) - result = sorted(result[0]["@@top_scores_heap"], key=lambda x: x["Vertex_ID"]) - baseline = sorted( - baseline[0]["@@top_scores_heap"], key=lambda x: x["Vertex_ID"] + result = self._sorted_top_scores(result, key="@@top_scores_heap") + baseline = self._sorted_top_scores(baseline, key="@@top_scores_heap") + + self._assert_top_scores_match(result, baseline) + + @pytest.mark.parametrize("test_name", undirected_graphs) + def test_betweenness_centrality1(self, test_name): + params = { + "v_type_set": ["V20"], + "e_type_set": [test_name], + "reverse_e_type": [test_name], + "max_hops": 100, + "top_k": 100, + "print_results": True, + "result_attribute": "", + "file_path": "", + "display_edges": False, + } + with open(f"data/baseline/centrality/betweenness/{test_name}.json") as f: + baseline = json.load(f) + + result = self.feat.conn.runInstalledQuery( + "brandes_betweenness", + params=params, ) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) - for b in baseline: - for r in result: - if r["Vertex_ID"] == b["Vertex_ID"] and r["score"] != pytest.approx( - b["score"] - ): - pytest.fail(f'{r["score"]} != {b["score"]}') + self._assert_top_scores_match(result, baseline) + + @pytest.mark.parametrize("test_name", directed_graphs) + def test_betweenness_centrality2(self, test_name): + params = { + "v_type_set": ["V20"], + "e_type_set": [test_name], + "reverse_e_type": ["reverse_" + test_name], + "max_hops": 100, + "top_k": 1000, + "print_results": True, + "result_attribute": "", + "file_path": "", + "display_edges": False, + } + with open(f"data/baseline/centrality/betweenness/{test_name}.json") as f: + baseline = json.load(f) + + result = self.feat.conn.runInstalledQuery( + "brandes_betweenness", + params=params, + ) + result = self._sorted_top_scores(result) + baseline = self._sorted_top_scores(baseline) + + self._assert_top_scores_match(result, baseline) diff --git a/tests/test/test_classification.py b/tests/test/test_classification.py new file mode 100644 index 00000000..fa0c2d41 --- /dev/null +++ b/tests/test/test_classification.py @@ -0,0 +1,72 @@ +import json + +import pytest + +import util + + +class TestClassification: + feat = util.get_featurizer() + undirected_graphs = [ + "Empty", + "Line", + "Ring", + "Hub_Spoke", + "Tree", + ] + directed_graphs = [ + "Line_Directed", + "Ring_Directed", + "Hub_Spoke_Directed", + "Tree_Directed", + ] + weighted_undirected_graphs = [ + "Line_Weighted", + "Ring_Weighted", + "Hub_Spoke_Weighted", + "Tree_Weighted", + ] + weighted_directed_graphs = [ + "Line_Directed_Weighted", + "Ring_Directed_Weighted", + "Hub_Spoke_Directed_Weighted", + "Tree_Directed_Weighted", + "Complete_Directed_Weighted", + ] + complete_graphs = [ + "Complete", + ] + + @pytest.mark.parametrize("test_name", undirected_graphs) + def test_graph_coloring(self, test_name): + params = { + "v_type_set": ["V20"], + "e_type_set": [test_name], + "max_colors": 999999, + "print_color_count": False, + "print_stats": True, + "file_path": "" + } + + with open(f"data/baseline/classification/graph_coloring/{test_name}.json") as f: + baseline_data = json.load(f) + + response = self.feat.runAlgorithm("tg_greedy_graph_coloring", params=params) + + # Sort both by v_id to ensure a 1:1 comparison + result = sorted(response[0]["start"], key=lambda x: x["v_id"]) + baseline = sorted(baseline_data[0]["start"], key=lambda x: x["v_id"]) + + # 1. Check if the number of vertices matches + assert len(result) == len(baseline), f"Vertex count mismatch for {test_name}" + + # 2. Compare values directly using zip (O(n) complexity) + for r, b in zip(result, baseline): + v_id = r["v_id"] + res_color = r["attributes"]["start.@sum_color_vertex"] + base_color = b["attributes"]["start.@sum_color_vertex"] + + assert v_id == b["v_id"], f"ID mismatch at index: {v_id} vs {b['v_id']}" + assert res_color == base_color, f"Color mismatch for vertex {v_id}: Expected {base_color}, got {res_color}" + + \ No newline at end of file diff --git a/tests/test/test_community.py b/tests/test/test_community.py index 642a4e8e..196b0311 100644 --- a/tests/test/test_community.py +++ b/tests/test/test_community.py @@ -6,7 +6,7 @@ class TestCommunity: feat = util.get_featurizer() - base_path = "data/baseline/graph_algorithms_baselines/community" + base_path = "data/baseline/community" #"data/baseline/graph_algorithms_baselines/community" graph_types1 = [ "Empty", "Empty_Directed", diff --git a/tests/test/test_path_finding.py b/tests/test/test_path_finding.py index 10377d11..4d2e5ea5 100644 --- a/tests/test/test_path_finding.py +++ b/tests/test/test_path_finding.py @@ -7,7 +7,7 @@ class TestPathFinding: pass feat = util.get_featurizer() - base_path = "data/baseline/graph_algorithms_baselines/path_finding" + base_path = "data/baseline/path_finding" #graph_algorithms_baselines # includes unweighted directed and undirected graphs, as well as one weighted graph (Line_Weighted) test_graphs1 = [