Skip to content

Commit 6d5316e

Browse files
lums658claude
andcommitted
Add algorithm unit tests and fix prim.hpp bugs
New test files: - betweenness_centrality_test.cpp: Brandes BC algorithm tests - delta_stepping_test.cpp: Delta-stepping SSSP tests - jaccard_test.cpp: Jaccard similarity tests - prim_test.cpp: Prim's MST algorithm tests Bug fixes in prim.hpp: - Add missing Q.push({source, 0}) to initialize priority queue - Add missing finished[u] = true after popping from queue - Fix variable name g[u] -> graph[u] - Fix predecessor vector type from Distance to vertex_id_type Test coverage improved from 171 to 189 tests (all passing). Algorithm coverage: 16/18 headers tested (89%). 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bf78f9b commit 6d5316e

File tree

7 files changed

+786
-9
lines changed

7 files changed

+786
-9
lines changed

agents/nwgraph_test_spec.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ This section maps every header file to its corresponding test(s).
7070

7171
| Header | Test File | Status |
7272
|--------|-----------|--------|
73-
| `betweenness_centrality.hpp` | `bk_test.cpp` | ❌ Disabled |
73+
| `betweenness_centrality.hpp` | `betweenness_centrality_test.cpp` | ✅ Active |
7474
| `bfs.hpp` | `bfs_test_0.cpp`, `bfs_test_1.cpp` | ✅ Active |
7575
| `boykov_kolmogorov.hpp` | `max_flow_test.cpp` | ❌ Disabled |
7676
| `connected_components.hpp` | `connected_component_test.cpp` | ✅ Active |
77-
| `dag_based_mis.hpp` | `dag_mis_test.cpp` | ✅ Active |
77+
| `dag_based_mis.hpp` | `mis_test.cpp` | ✅ Active |
7878
| `delta_stepping.hpp` | `delta_stepping_test.cpp` | ✅ Active |
7979
| `dijkstra.hpp` | `dijkstra_test.cpp` | ✅ Active |
8080
| `jaccard.hpp` | `jaccard_test.cpp` | ✅ Active |
@@ -85,7 +85,7 @@ This section maps every header file to its corresponding test(s).
8585
| `max_flow.hpp` | `max_flow_test.cpp` | ❌ Disabled |
8686
| `maximal_independent_set.hpp` | `mis_test.cpp` | ✅ Active |
8787
| `page_rank.hpp` | `page_rank_test.cpp` | ✅ Active |
88-
| `prim.hpp` | - | ❌ Header has bugs |
88+
| `prim.hpp` | `prim_test.cpp` | ✅ Active |
8989
| `spMatspMat.hpp` | `spMatspMat_test.cpp` | ✅ Active |
9090
| `triangle_count.hpp` | `tc_test.cpp` | ✅ Active |
9191

@@ -168,10 +168,10 @@ Experimental parallel implementations - not unit tested as they mirror main algo
168168
- `size_test`
169169

170170
### Algorithm Tests
171+
- `betweenness_centrality_test`
171172
- `bfs_test_0`
172173
- `bfs_test_1`
173174
- `connected_component_test`
174-
- `dag_mis_test`
175175
- `delta_stepping_test`
176176
- `dijkstra_test`
177177
- `jaccard_test`
@@ -180,6 +180,7 @@ Experimental parallel implementations - not unit tested as they mirror main algo
180180
- `mis_test`
181181
- `new_dfs_test`
182182
- `page_rank_test`
183+
- `prim_test`
183184
- `spanning_tree_test`
184185
- `spMatspMat_test`
185186
- `tc_test`
@@ -281,14 +282,15 @@ In-memory karate graph for quick tests without file I/O.
281282
|----------|---------------|--------|----------|
282283
| Adaptors | 17 | 14 | 82% |
283284
| Core | 13 | 13 | 100% |
284-
| Algorithms | 18 | 14 | 78% |
285+
| Algorithms | 18 | 16 | 89% |
285286
| Containers | 4 | 4 | 100% |
286287
| Graphs | 3 | 3 | 100% |
287288
| I/O | 2 | 2 | 100% |
288289
| Utilities | 18 | 15 | 83% |
289-
| **Total** | **75** | **65** | **87%** |
290+
| **Total** | **75** | **67** | **89%** |
290291

291292
Note: Some headers are not tested due to:
292293
- TBB-specific functionality (requires TBB for testing)
293294
- Broken headers (syntax errors or bugs)
294295
- Type definitions only (no testable behavior)
296+
- Empty/stub implementations (k_truss.hpp)

include/nwgraph/algorithms/prim.hpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ std::vector<vertex_id_t<Graph>> prim(const Graph& graph, vertex_id_t<Graph> sour
4343
size_t N { num_vertices(graph) };
4444
assert(source < N);
4545

46-
std::vector<Distance> distance(N, std::numeric_limits<Distance>::max());
47-
std::vector<Distance> predecessor(N, std::numeric_limits<Distance>::max());
46+
std::vector<Distance> distance(N, std::numeric_limits<Distance>::max());
47+
std::vector<vertex_id_type> predecessor(N, std::numeric_limits<vertex_id_type>::max());
4848
std::vector<uint8_t> finished(N, false);
4949
distance[source] = 0;
5050

@@ -53,6 +53,8 @@ std::vector<vertex_id_t<Graph>> prim(const Graph& graph, vertex_id_t<Graph> sour
5353

5454
std::priority_queue<weighted_vertex, std::vector<weighted_vertex>, std::greater<weighted_vertex>> Q;
5555

56+
Q.push({source, 0});
57+
5658
while (!Q.empty()) {
5759

5860
auto u = std::get<0>(Q.top());
@@ -61,8 +63,9 @@ std::vector<vertex_id_t<Graph>> prim(const Graph& graph, vertex_id_t<Graph> sour
6163
if (finished[u]) {
6264
continue;
6365
}
66+
finished[u] = true;
6467

65-
std::for_each(g[u].begin(), g[u].end(), [&](auto&& e) {
68+
std::for_each(graph[u].begin(), graph[u].end(), [&](auto&& e) {
6669
auto v = target(graph, e);
6770
auto w = weight(e);
6871

test/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,19 @@ nwgraph_add_test(size_test)
4747
# -----------------------------------------------------------------------------
4848
# Algorithm Tests
4949
# -----------------------------------------------------------------------------
50+
nwgraph_add_test(betweenness_centrality_test)
5051
nwgraph_add_test(bfs_test_0)
5152
nwgraph_add_test(bfs_test_1)
5253
nwgraph_add_test(connected_component_test)
54+
nwgraph_add_test(delta_stepping_test)
5355
nwgraph_add_test(dijkstra_test)
56+
nwgraph_add_test(jaccard_test)
5457
nwgraph_add_test(jp_coloring_test)
5558
nwgraph_add_test(kruskal_test)
5659
nwgraph_add_test(mis_test)
5760
nwgraph_add_test(new_dfs_test)
5861
nwgraph_add_test(page_rank_test)
62+
nwgraph_add_test(prim_test)
5963
nwgraph_add_test(spanning_tree_test)
6064
nwgraph_add_test(spMatspMat_test)
6165
nwgraph_add_test(tc_test)
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* @file betweenness_centrality_test.cpp
3+
*
4+
* @copyright SPDX-FileCopyrightText: 2022 Battelle Memorial Institute
5+
* @copyright SPDX-FileCopyrightText: 2022 University of Washington
6+
*
7+
* SPDX-License-Identifier: BSD-3-Clause
8+
*
9+
* @authors
10+
* Andrew Lumsdaine
11+
*
12+
*/
13+
14+
#include <vector>
15+
16+
#include "common/test_header.hpp"
17+
18+
#include "nwgraph/adjacency.hpp"
19+
#include "nwgraph/algorithms/betweenness_centrality.hpp"
20+
#include "nwgraph/edge_list.hpp"
21+
22+
using namespace nw::graph;
23+
24+
// Helper to create a simple line graph: 0 -- 1 -- 2 -- 3 -- 4
25+
// In a line graph, the middle vertex has highest betweenness centrality
26+
auto create_line_graph() {
27+
edge_list<directedness::directed> edges(5);
28+
edges.open_for_push_back();
29+
// Bidirectional edges for undirected behavior
30+
edges.push_back(0, 1);
31+
edges.push_back(1, 0);
32+
edges.push_back(1, 2);
33+
edges.push_back(2, 1);
34+
edges.push_back(2, 3);
35+
edges.push_back(3, 2);
36+
edges.push_back(3, 4);
37+
edges.push_back(4, 3);
38+
edges.close_for_push_back();
39+
return edges;
40+
}
41+
42+
// Helper to create a star graph with center at vertex 0
43+
// Center has highest BC, all leaves have zero BC
44+
auto create_star_graph() {
45+
edge_list<directedness::directed> edges(5);
46+
edges.open_for_push_back();
47+
// 0 is the center, connected to 1, 2, 3, 4
48+
edges.push_back(0, 1);
49+
edges.push_back(1, 0);
50+
edges.push_back(0, 2);
51+
edges.push_back(2, 0);
52+
edges.push_back(0, 3);
53+
edges.push_back(3, 0);
54+
edges.push_back(0, 4);
55+
edges.push_back(4, 0);
56+
edges.close_for_push_back();
57+
return edges;
58+
}
59+
60+
// Helper to create a simple triangle graph
61+
auto create_triangle_graph() {
62+
edge_list<directedness::directed> edges(3);
63+
edges.open_for_push_back();
64+
edges.push_back(0, 1);
65+
edges.push_back(1, 0);
66+
edges.push_back(1, 2);
67+
edges.push_back(2, 1);
68+
edges.push_back(2, 0);
69+
edges.push_back(0, 2);
70+
edges.close_for_push_back();
71+
return edges;
72+
}
73+
74+
TEST_CASE("Brandes betweenness centrality basic", "[betweenness]") {
75+
76+
SECTION("Line graph - middle vertex has highest BC") {
77+
auto edges = create_line_graph();
78+
adjacency<0> G(edges);
79+
80+
auto bc = brandes_bc<adjacency<0>, float, size_t>(G, false);
81+
82+
REQUIRE(bc.size() == 5);
83+
84+
// In a line graph 0-1-2-3-4, vertex 2 is on the most shortest paths
85+
// Vertex 0 and 4 have BC = 0 (endpoints)
86+
// Vertex 2 should have highest BC
87+
REQUIRE(bc[0] == 0.0f);
88+
REQUIRE(bc[4] == 0.0f);
89+
REQUIRE(bc[2] > bc[1]);
90+
REQUIRE(bc[2] > bc[3]);
91+
// Symmetry: bc[1] == bc[3]
92+
REQUIRE(bc[1] == bc[3]);
93+
}
94+
95+
SECTION("Star graph - center has highest BC") {
96+
auto edges = create_star_graph();
97+
adjacency<0> G(edges);
98+
99+
auto bc = brandes_bc<adjacency<0>, float, size_t>(G, false);
100+
101+
REQUIRE(bc.size() == 5);
102+
103+
// All shortest paths between leaf nodes go through center (vertex 0)
104+
// Leaf nodes have BC = 0
105+
REQUIRE(bc[1] == 0.0f);
106+
REQUIRE(bc[2] == 0.0f);
107+
REQUIRE(bc[3] == 0.0f);
108+
REQUIRE(bc[4] == 0.0f);
109+
// Center should have highest BC
110+
REQUIRE(bc[0] > 0.0f);
111+
}
112+
113+
SECTION("Triangle graph - all vertices have equal BC") {
114+
auto edges = create_triangle_graph();
115+
adjacency<0> G(edges);
116+
117+
auto bc = brandes_bc<adjacency<0>, float, size_t>(G, false);
118+
119+
REQUIRE(bc.size() == 3);
120+
121+
// In a complete triangle, all vertices have same BC (0)
122+
// because there are direct edges between all pairs
123+
REQUIRE(bc[0] == 0.0f);
124+
REQUIRE(bc[1] == 0.0f);
125+
REQUIRE(bc[2] == 0.0f);
126+
}
127+
}
128+
129+
TEST_CASE("Brandes BC with normalization", "[betweenness]") {
130+
131+
SECTION("Normalized BC values are in [0,1]") {
132+
auto edges = create_line_graph();
133+
adjacency<0> G(edges);
134+
135+
auto bc = brandes_bc<adjacency<0>, float, size_t>(G, true);
136+
137+
REQUIRE(bc.size() == 5);
138+
139+
for (auto& score : bc) {
140+
REQUIRE(score >= 0.0f);
141+
REQUIRE(score <= 1.0f);
142+
}
143+
144+
// Highest BC vertex should have score 1.0 after normalization
145+
float max_bc = *std::max_element(bc.begin(), bc.end());
146+
REQUIRE(max_bc == 1.0f);
147+
}
148+
}
149+
150+
TEST_CASE("BC single vertex graph", "[betweenness]") {
151+
152+
SECTION("Single vertex has BC = 0") {
153+
edge_list<directedness::directed> edges(1);
154+
adjacency<0> G(edges);
155+
156+
auto bc = brandes_bc<adjacency<0>, float, size_t>(G, false);
157+
158+
REQUIRE(bc.size() == 1);
159+
REQUIRE(bc[0] == 0.0f);
160+
}
161+
}
162+
163+
TEST_CASE("BC disconnected graph", "[betweenness]") {
164+
165+
SECTION("Disconnected components") {
166+
// Two separate edges: 0-1 and 2-3
167+
edge_list<directedness::directed> edges(4);
168+
edges.open_for_push_back();
169+
edges.push_back(0, 1);
170+
edges.push_back(1, 0);
171+
edges.push_back(2, 3);
172+
edges.push_back(3, 2);
173+
edges.close_for_push_back();
174+
175+
adjacency<0> G(edges);
176+
177+
auto bc = brandes_bc<adjacency<0>, float, size_t>(G, false);
178+
179+
REQUIRE(bc.size() == 4);
180+
181+
// All vertices have BC = 0 (no vertex lies on paths between others)
182+
REQUIRE(bc[0] == 0.0f);
183+
REQUIRE(bc[1] == 0.0f);
184+
REQUIRE(bc[2] == 0.0f);
185+
REQUIRE(bc[3] == 0.0f);
186+
}
187+
}

0 commit comments

Comments
 (0)