Skip to content

Commit 892b626

Browse files
committed
refactor(pruning): move pruning methods to deglib::optimization with tests
- Add cpp/deglib/include/deglib/optimization/pruning.h with 4 pruning methods - Expose via optimization.h high-level API - Update benchmark to use new optimization::pruning methods - Add unit tests for all pruning methods
1 parent 94982e4 commit 892b626

10 files changed

Lines changed: 709 additions & 214 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## Refactoring Plan: Move Pruning Methods to `cpp/deglib/include/optimization`
2+
3+
### Goal
4+
Consolidate pruning logic from `cpp/benchmark/src/deglib_build_bench.cpp` and `examples/knng/main.py` into a new `cpp/deglib/include/deglib/optimization/pruning.h` file, and expose it through `optimization.h` as a high-level API.
5+
6+
### Current Pruning Methods Found
7+
8+
| Method | Location | Strategy |
9+
|--------|----------|----------|
10+
| `remove_non_mrng_edges` (multi-threaded) | `builder.h:1841` | Parallel MRNG conformance pruning (current core impl) |
11+
| `remove_non_mrng_edges_1` (iterative) | `deglib_build_bench.cpp:102` | Per-vertex iterative removal, single-threaded |
12+
| `remove_non_mrng_edges_2` (weight-sorted) | `deglib_build_bench.cpp:61` | Collect all non-RNG edges, sort by weight, remove, single-threaded |
13+
| `remove_non_mrng_edges` (file wrapper) | `deglib_build_bench.cpp:166` | Loads graph from file, calls builder version, saves |
14+
| `prune_worst_edges` | `main.py:40` | Prunes worst N (highest-weight) neighbors per vertex (Python only) |
15+
16+
### New File: `cpp/deglib/include/deglib/optimization/pruning.h`
17+
18+
A new header with `deglib::optimization::pruning` namespace containing:
19+
20+
1. **`prune_worst_edges(MutableGraph& graph, uint8_t prune_worst, size_t numThreads = 0)`**
21+
- C++ port of the Python `prune_worst_edges()`.
22+
- For each vertex: sort neighbors by descending weight, replace top `prune_worst` with self-loops (index=u, weight=0.0), re-sort by index.
23+
- Multi-threaded via `parallel_for`.
24+
25+
2. **`remove_non_mrng_edges(MutableGraph& graph, size_t numThreads = 0)`**
26+
- The multi-threaded MRNG pruning (moved from `builder.h`), now in optimization namespace.
27+
- Uses `analysis::checkRNG()` + `parallel_for`.
28+
29+
3. **`remove_non_mrng_edges_weight_sorted(MutableGraph& graph, size_t numThreads = 0)`**
30+
- Benchmark variant 2: collect all non-RNG edges, sort by weight ascending, remove in order.
31+
- Multi-threaded collection, single-threaded sorted removal.
32+
33+
4. **`remove_non_mrng_edges_iterative(MutableGraph& graph, size_t numThreads = 0)`**
34+
- Benchmark variant 1: per-vertex iterative removal in a do-while loop until stable.
35+
- Multi-threaded per-vertex.
36+
37+
### Modified: `cpp/deglib/include/deglib/optimization.h`
38+
39+
- Add `#include "deglib/optimization/pruning.h"`
40+
- Add inline wrapper functions in `deglib::optimization` namespace delegating to `deglib::optimization::pruning::*`.
41+
42+
### Modified: `cpp/benchmark/src/deglib_build_bench.cpp`
43+
44+
- Replace local `remove_non_mrng_edges_1`, `remove_non_mrng_edges_2`, and wrapper with calls to `deglib::optimization::pruning::remove_non_mrng_edges_iterative()` and `deglib::optimization::pruning::remove_non_mrng_edges_weight_sorted()`.
45+
- Keep `remove_non_mrng_edges()` wrapper calling `deglib::optimization::pruning::remove_non_mrng_edges()`.
46+
47+
### Backward Compatibility
48+
49+
- `deglib::builder::remove_non_mrng_edges` and `deglib::builder::optimize_edges` remain unchanged.
50+
- `GraphEdge` struct stays in `builder.h`.
51+
- `deglib.h` already includes `optimization.h`, so the new API is automatically available.

cpp/benchmark/src/deglib_build_bench.cpp

Lines changed: 6 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -61,102 +61,23 @@ void change_features(const std::string initial_graph_file, const std::string rep
6161
void remove_non_mrng_edges_2(const std::string initial_graph_file, const std::string graph_file) {
6262
fmt::print("Load graph {} \n", initial_graph_file);
6363
auto graph = deglib::graph::load_sizebounded_graph(initial_graph_file.c_str());
64-
fmt::print("Graph with {} vertices and containing {} non-RNG edges\n", graph.size(), deglib::analysis::calc_non_rng_edges(graph));
64+
fmt::print("Graph with {} vertices and containing {} non-RNG edges \n", graph.size(), deglib::analysis::calc_non_rng_edges(graph));
6565

66-
const auto vertex_count = graph.size();
67-
const auto edge_per_vertex = graph.getEdgesPerVertex();
68-
69-
const auto start = std::chrono::steady_clock::now();
70-
std::vector<deglib::builder::GraphEdge> nonMRNG_edges;
71-
for (uint32_t i = 0; i < vertex_count; i++) {
72-
const auto vertex_index = i;
73-
const auto neighbor_indices = graph.getNeighborIndices(vertex_index);
74-
const auto neighbor_weights = graph.getNeighborWeights(vertex_index);
75-
76-
// find all none rng conform neighbors
77-
for (uint32_t n = 0; n < edge_per_vertex; n++) {
78-
const auto neighbor_index = neighbor_indices[n];
79-
const auto neighbor_weight = neighbor_weights[n];
80-
if(deglib::analysis::checkRNG(graph, edge_per_vertex, vertex_index, neighbor_index, neighbor_weight) == false)
81-
nonMRNG_edges.emplace_back(vertex_index, neighbor_index, neighbor_weight);
82-
}
83-
}
84-
std::sort(nonMRNG_edges.begin(), nonMRNG_edges.end(), [](const auto& x, const auto& y){return x.weight < y.weight;});
85-
86-
size_t removed_rng_edges = 0;
87-
for (size_t i = 0; i < nonMRNG_edges.size(); i++) {
88-
const deglib::builder::GraphEdge& edge = nonMRNG_edges[i];
89-
if(deglib::analysis::checkRNG(graph, edge_per_vertex, edge.from_vertex, edge.to_vertex, edge.weight) == false) {
90-
graph.changeEdge(edge.from_vertex, edge.to_vertex, edge.from_vertex, 0);
91-
removed_rng_edges++;
92-
}
93-
}
94-
const auto duration_ms = uint32_t(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
66+
deglib::optimization::remove_non_mrng_edges_weight_sorted(graph);
9567

9668
// store the graph
9769
graph.saveGraph(graph_file.c_str());
98-
99-
fmt::print("Removed {} edges in {} ms. Final graph contains {} non-RNG edges\n", removed_rng_edges, duration_ms, deglib::analysis::calc_non_rng_edges(graph));
10070
}
10171

10272
void remove_non_mrng_edges_1(const std::string initial_graph_file, const std::string graph_file) {
10373
fmt::print("Load graph {} \n", initial_graph_file);
10474
auto graph = deglib::graph::load_sizebounded_graph(initial_graph_file.c_str());
105-
fmt::print("Graph with {} vertices and containing {} non-RNG edges\n", graph.size(), deglib::analysis::calc_non_rng_edges(graph));
75+
fmt::print("Graph with {} vertices and containing {} non-RNG edges \n", graph.size(), deglib::analysis::calc_non_rng_edges(graph));
10676

107-
const auto vertex_count = graph.size();
108-
const auto edge_per_vertex = graph.getEdgesPerVertex();
109-
110-
const auto start = std::chrono::steady_clock::now();
111-
size_t removed_rng_edges = 0;
112-
for (uint32_t i = 0; i < vertex_count; i++) {
113-
const auto vertex_index = i;
114-
115-
// sort neighbors by their weight (highest to lowest)
116-
std::vector<std::pair<uint32_t, float>> neighbors;
117-
{
118-
const auto neighbor_indices = graph.getNeighborIndices(vertex_index);
119-
const auto neighbor_weights = graph.getNeighborWeights(vertex_index);
120-
for (uint32_t n = 0; n < edge_per_vertex; n++) {
121-
const auto neighbor_index = neighbor_indices[n];
122-
const auto neighbor_weight = neighbor_weights[n];
123-
neighbors.emplace_back(neighbor_index, neighbor_weight);
124-
}
125-
std::sort(neighbors.begin(), neighbors.end(), [](const auto& x, const auto& y){return x.second < y.second;});
126-
}
127-
128-
// find all none rng conform neighbors
129-
std::vector<uint32_t> nonMRNG_edges;
130-
for (uint32_t n = 0; n < neighbors.size(); n++) {
131-
const auto neighbor_index = neighbors[n].first;
132-
const auto neighbor_weight = neighbors[n].second;
133-
if(deglib::analysis::checkRNG(graph, edge_per_vertex, vertex_index, neighbor_index, neighbor_weight) == false)
134-
nonMRNG_edges.emplace_back(n);
135-
}
136-
137-
bool removed_edge = false;
138-
do {
139-
removed_edge = false;
140-
for (uint32_t n = 0; n < nonMRNG_edges.size(); n++) {
141-
const auto neighbor_index = neighbors[nonMRNG_edges[n]].first;
142-
const auto neighbor_weight = neighbors[nonMRNG_edges[n]].second;
143-
144-
if(deglib::analysis::checkRNG(graph, edge_per_vertex, vertex_index, neighbor_index, neighbor_weight) == false) {
145-
nonMRNG_edges.erase(nonMRNG_edges.begin() + n);
146-
graph.changeEdge(vertex_index, neighbor_index, vertex_index, 0);
147-
removed_rng_edges++;
148-
removed_edge = true;
149-
break;
150-
}
151-
}
152-
} while(removed_edge);
153-
}
154-
const auto duration_ms = uint32_t(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
77+
deglib::optimization::remove_non_mrng_edges_iterative(graph);
15578

15679
// store the graph
15780
graph.saveGraph(graph_file.c_str());
158-
159-
fmt::print("Removed {} edges in {} ms. Final graph contains {} non-RNG edges\n", removed_rng_edges, duration_ms, deglib::analysis::calc_non_rng_edges(graph));
16081
}
16182

16283

@@ -168,7 +89,7 @@ void remove_non_mrng_edges(const std::string initial_graph_file, const std::stri
16889
auto graph = deglib::graph::load_sizebounded_graph(initial_graph_file.c_str());
16990
fmt::print("Graph with {} vertices and containing {} non-RNG edges\n", graph.size(), deglib::analysis::calc_non_rng_edges(graph));
17091

171-
deglib::builder::remove_non_mrng_edges(graph);
92+
deglib::optimization::remove_non_mrng_edges(graph);
17293

17394
// store the graph
17495
graph.saveGraph(graph_file.c_str());
@@ -186,7 +107,7 @@ void optimize_graph(const std::string initial_graph_file, const std::string grap
186107
auto graph = deglib::graph::load_sizebounded_graph(initial_graph_file.c_str());
187108
fmt::print("Graph with {} vertices and an avg edge weight of {} \n", graph.size(), deglib::analysis::calc_avg_edge_weight(graph, 100));
188109

189-
deglib::builder::optimize_edges(graph, k_opt, eps_opt, i_opt, iterations);
110+
deglib::optimization::optimize_edges(graph, k_opt, eps_opt, i_opt, iterations);
190111

191112
// store the graph
192113
graph.saveGraph(graph_file.c_str());

cpp/deglib/include/deglib/builder.h

Lines changed: 1 addition & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,100 +1830,5 @@ class EvenRegularGraphBuilder {
18301830
}
18311831
};
18321832

1833-
/**
1834-
* @brief Removes all edges from the graph that will never be in a MRNG (Monotonic Relative Neighborhood Graph).
1835-
*
1836-
* This function iterates over all vertices and their neighbors, removing any edge that does not satisfy the MRNG condition.
1837-
* The process is parallelized across available hardware threads for efficiency.
1838-
*
1839-
* @param graph Reference to the MutableGraph to be processed.
1840-
*/
1841-
void remove_non_mrng_edges(deglib::graph::MutableGraph& graph) {
1842-
1843-
const auto vertex_count = graph.size();
1844-
const auto edge_per_vertex = graph.getEdgesPerVertex();
1845-
1846-
const auto start = std::chrono::steady_clock::now();
1847-
const auto thread_count = std::thread::hardware_concurrency();
1848-
auto removed_rng_edges_per_thread = std::vector<uint32_t>(thread_count);
1849-
deglib::concurrent::parallel_for(0, vertex_count, thread_count, [&] (size_t vertex_index, size_t thread_id) {
1850-
uint32_t removed_rng_edges = 0;
1851-
1852-
const auto neighbor_indices = graph.getNeighborIndices(vertex_index);
1853-
const auto neighbor_weights = graph.getNeighborWeights(vertex_index);
1854-
1855-
// find all none rng conform neighbors
1856-
std::vector<uint32_t> remove_neighbor_ids;
1857-
for (uint32_t n = 0; n < edge_per_vertex; n++) {
1858-
const auto neighbor_index = neighbor_indices[n];
1859-
const auto neighbor_weight = neighbor_weights[n];
1860-
1861-
if(deglib::analysis::checkRNG(graph, edge_per_vertex, vertex_index, neighbor_index, neighbor_weight) == false) {
1862-
remove_neighbor_ids.emplace_back(neighbor_index);
1863-
}
1864-
}
1865-
1866-
for (uint32_t n = 0; n < remove_neighbor_ids.size(); n++) {
1867-
graph.changeEdge(vertex_index, remove_neighbor_ids[n], vertex_index, 0);
1868-
removed_rng_edges++;
1869-
}
1870-
removed_rng_edges_per_thread[thread_id] += removed_rng_edges;
1871-
});
1872-
1873-
// aggregate
1874-
uint32_t removed_rng_edges = 0;
1875-
for (uint32_t i = 0; i < thread_count; i++)
1876-
removed_rng_edges += removed_rng_edges_per_thread[i];
1877-
1878-
const auto duration_ms = uint32_t(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
1879-
1880-
std::cout << "Removed " << removed_rng_edges << " edges in " << duration_ms << " ms. Final graph contains " << deglib::analysis::calc_non_rng_edges(graph) << " non-RNG edges\n";
1881-
}
1882-
1883-
1884-
/**
1885-
* @brief Optimizes the edges of the graph using the builder's improvement routines.
1886-
*
1887-
* This function creates a builder and repeatedly attempts to improve the graph's edges for a given number of iterations.
1888-
* It reports progress and statistics during the optimization process.
1889-
*
1890-
* @param graph Reference to the MutableGraph to be optimized.
1891-
* @param k_opt Number of neighbors to consider during optimization.
1892-
* @param eps_opt Epsilon value for neighbor search during optimization.
1893-
* @param i_opt Number of improvement attempts per build step.
1894-
* @param iterations Number of optimization iterations to perform.
1895-
*/
1896-
void optimize_edges(deglib::graph::MutableGraph& graph, const uint8_t k_opt, const float eps_opt, const uint8_t i_opt, const uint32_t iterations) {
1897-
1898-
auto rnd = std::mt19937(7); // default 7
1899-
1900-
// create a graph builder to add vertices to the new graph and improve its edges
1901-
std::cout << "Start graph builder\n";
1902-
auto builder = deglib::builder::EvenRegularGraphBuilder(graph, rnd, deglib::builder::StreamingData, 0, 0.0f, k_opt, eps_opt, i_opt, 1, 0);
1903-
1904-
// check the integrity of the graph during the graph build process
1905-
auto start = std::chrono::steady_clock::now();
1906-
uint64_t duration_ms = 0;
1907-
const auto improvement_callback = [&](deglib::builder::BuilderStatus& status) {
1908-
const auto size = graph.size();
1909-
1910-
if(status.step % (iterations/10) == 0) {
1911-
duration_ms += uint32_t(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count());
1912-
auto avg_edge_weight = deglib::analysis::calc_avg_edge_weight(graph, 100);
1913-
auto valid_weights = deglib::analysis::check_graph_weights(graph) && deglib::analysis::check_graph_regularity(graph, uint32_t(size), true);
1914-
auto connected = deglib::analysis::check_graph_connectivity(graph);
1915-
1916-
auto duration = duration_ms / 1000;
1917-
std::cout << std::setw(7) << status.step << " step, " << std::setw(5) << duration << "s, AEW: " << std::fixed << std::setprecision(2) << std::setw(4) << avg_edge_weight << ", " << (connected ? "" : "not") << " connected, " << (valid_weights ? "valid" : "invalid") << "\n";
1918-
start = std::chrono::steady_clock::now();
1919-
}
1920-
1921-
if(status.step > iterations)
1922-
builder.stop();
1923-
};
1924-
1925-
// start the build process
1926-
builder.build(improvement_callback, true);
1927-
}
1928-
19291833
} // end namespace deglib::builder
1834+

0 commit comments

Comments
 (0)