Skip to content

Commit 8d3eccf

Browse files
committed
feat: add create_empty and create_random_graph factory methods
Add `create_empty` and `create_random_graph` static methods to: - SizeBoundedGraph (C++ header) - DynamicExplorationGraph (C++ header) - DynamicExplorationGraph (Python pybind11 bindings) - DynamicExplorationGraph (Python facade) The `create_random_graph` implementation builds a random exploration graph by fully connecting the initial (edges_per_vertex + 1) vertices, then iteratively inserting remaining vertices by connecting to edges_per_vertex nearest neighbors. Uses DeterministicUniformIntDistribution for cross-platform reproducibility. Includes full test coverage: 7 C++ SizeBoundedGraph tests, 5 C++ DynamicExplorationGraph tests, and 6 Python tests.
1 parent 7f1046a commit 8d3eccf

7 files changed

Lines changed: 749 additions & 9 deletions

File tree

cpp/deglib/include/deglib/graph.h

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
#include "deglib/graph/readonly_graph.h"
1313
#include "deglib/graph/sizebounded_graph.h"
1414

15-
#include <vector>
15+
#include <algorithm>
16+
#include <memory>
17+
#include <random>
1618
#include <span>
1719
#include <stdexcept>
18-
#include <memory>
20+
#include <vector>
1921

2022
namespace deglib {
2123

@@ -31,6 +33,43 @@ class DynamicExplorationGraph {
3133
public:
3234
explicit DynamicExplorationGraph(deglib::graph::InternalGraph& graph) : internal_graph_(graph) {}
3335

36+
/**
37+
* Create an empty mutable DynamicExplorationGraph with the given capacity,
38+
* edges per vertex, and feature space.
39+
*/
40+
static DynamicExplorationGraph create_empty(
41+
const uint32_t max_vertex_count,
42+
const uint8_t edges_per_vertex,
43+
const deglib::distances::FloatSpace& feature_space)
44+
{
45+
auto* graph = new deglib::graph::SizeBoundedGraph(max_vertex_count, edges_per_vertex, feature_space);
46+
return DynamicExplorationGraph(*graph);
47+
}
48+
49+
/**
50+
* Create a random exploration graph from the given feature data.
51+
*
52+
* @param feature_data Pointer to a contiguous array of feature vectors.
53+
* Each vector is feature_space.get_data_size() bytes.
54+
* @param vertex_count Number of vertices to insert.
55+
* @param edges_per_vertex Number of edges per vertex (must be even).
56+
* @param feature_space The feature space defining dimensionality and metric.
57+
* @param seed Random seed for deterministic graph construction.
58+
* @return A new DynamicExplorationGraph wrapping the created random graph.
59+
*/
60+
static DynamicExplorationGraph create_random_graph(
61+
const std::byte* feature_data,
62+
const uint32_t vertex_count,
63+
const uint8_t edges_per_vertex,
64+
const deglib::distances::FloatSpace& feature_space,
65+
const uint32_t seed = 7)
66+
{
67+
auto* graph = new deglib::graph::SizeBoundedGraph(
68+
deglib::graph::SizeBoundedGraph::create_random_graph(feature_data, vertex_count, edges_per_vertex, feature_space, seed)
69+
);
70+
return DynamicExplorationGraph(*graph);
71+
}
72+
3473
const uint32_t size() const {
3574
return internal_graph_.size();
3675
}

cpp/deglib/include/deglib/graph/sizebounded_graph.h

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <array>
4+
#include <algorithm>
45
#include <cstdint> // for types like uint32_t
56
#include <cstring>
67
#include <limits>
@@ -10,11 +11,13 @@
1011
#include <unordered_map>
1112
#include <fstream>
1213
#include <iostream>
14+
#include <random>
1315

14-
#include "deglib/graph.h"
16+
#include "deglib/graph/mutable_graph.h"
1517
#include "deglib/search.h"
1618
#include "deglib/graph/visited_list_pool.h"
1719
#include "deglib/utils/memory.h"
20+
#include "deglib/utils/random.h"
1821

1922
namespace deglib::graph
2023
{
@@ -196,6 +199,132 @@ class SizeBoundedGraph : public deglib::graph::MutableGraph {
196199
}
197200
}
198201

202+
/**
203+
* Create an empty SizeBoundedGraph with the given capacity, edges per vertex, and feature space.
204+
* The graph starts with zero vertices; vertices can be added via addVertex().
205+
*/
206+
static SizeBoundedGraph create_empty(
207+
const uint32_t max_vertex_count,
208+
const uint8_t edges_per_vertex,
209+
const deglib::distances::FloatSpace& feature_space)
210+
{
211+
return SizeBoundedGraph(max_vertex_count, edges_per_vertex, feature_space);
212+
}
213+
214+
/**
215+
* Create a random exploration graph from the given feature data.
216+
*
217+
* The graph is built by first fully connecting the initial (edges_per_vertex + 1)
218+
* vertices, then iteratively inserting each remaining vertex by connecting it
219+
* to edges_per_vertex neighbors chosen from existing vertices.
220+
*
221+
* @param feature_data Pointer to a contiguous array of feature vectors.
222+
* Each vector is feature_space.get_data_size() bytes.
223+
* @param vertex_count Number of vertices to insert (must be <= max_vertex_count).
224+
* @param edges_per_vertex Number of edges per vertex (must be even).
225+
* @param feature_space The feature space defining dimensionality and metric.
226+
* @param seed Random seed for deterministic graph construction.
227+
* @return A fully constructed random exploration graph.
228+
*/
229+
static SizeBoundedGraph create_random_graph(
230+
const std::byte* feature_data,
231+
const uint32_t vertex_count,
232+
const uint8_t edges_per_vertex,
233+
const deglib::distances::FloatSpace& feature_space,
234+
const uint32_t seed = 7)
235+
{
236+
const auto dist_func = feature_space.get_dist_func();
237+
const auto dist_func_param = feature_space.get_dist_func_param();
238+
239+
auto graph = SizeBoundedGraph(vertex_count, edges_per_vertex, feature_space);
240+
241+
// add the initial vertices (edges_per_vertex + 1)
242+
{
243+
const auto size = (uint32_t)(edges_per_vertex + 1);
244+
for (uint32_t y = 0; y < size; y++) {
245+
const auto query = feature_data + size_t(y) * feature_space.get_data_size();
246+
const auto internal_index = graph.addVertex(y, query);
247+
248+
auto neighbor_indices = std::vector<uint32_t>();
249+
auto neighbor_weights = std::vector<float>();
250+
for (uint32_t x = 0; x < size; x++) {
251+
if (x == internal_index) continue;
252+
neighbor_indices.emplace_back(x);
253+
neighbor_weights.emplace_back(dist_func(query, feature_data + size_t(x) * feature_space.get_data_size(), dist_func_param));
254+
}
255+
graph.changeEdges(internal_index, neighbor_indices.data(), neighbor_weights.data());
256+
}
257+
}
258+
259+
// random order of vertices
260+
auto rnd = std::mt19937(seed);
261+
auto rnd_neighbor = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, edges_per_vertex - 1);
262+
263+
// add the remaining vertices
264+
for (uint32_t label = edges_per_vertex + 1; label < vertex_count; label++) {
265+
const auto new_vertex_feature = feature_data + size_t(label) * feature_space.get_data_size();
266+
const auto internal_index = graph.addVertex(label, new_vertex_feature);
267+
auto top_list = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, label - 1);
268+
269+
// remove the worst edge of the good neighbors and connect them with this new vertex
270+
auto new_neighbors = std::vector<std::pair<uint32_t, float>>();
271+
while (new_neighbors.size() < edges_per_vertex) {
272+
const auto candidate_index = (uint32_t)top_list(rnd);
273+
274+
// check if the vertex is already in the edge list of the new vertex (added during a previous loop-run)
275+
// since all edges are undirected and the edge information of the new vertex does not yet exist, we search the other way around.
276+
if (graph.hasEdge(candidate_index, internal_index))
277+
continue;
278+
279+
// find a new random neighbor
280+
uint32_t new_neighbor_index = 0;
281+
float new_neighbor_weight = -1;
282+
const auto neighbor_weights = graph.getNeighborWeights(candidate_index);
283+
const auto neighbor_indices = graph.getNeighborIndices(candidate_index);
284+
while (new_neighbor_weight < 0) {
285+
const auto edge_idx = (uint32_t)rnd_neighbor(rnd);
286+
const auto neighbor_index = neighbor_indices[edge_idx];
287+
const auto neighbor_weight = neighbor_weights[edge_idx];
288+
289+
// the suggest neighbors might already be in the edge list of the new vertex
290+
if (graph.hasEdge(neighbor_index, internal_index) == false) {
291+
new_neighbor_index = neighbor_index;
292+
new_neighbor_weight = neighbor_weight;
293+
}
294+
}
295+
296+
// this should not be possible, otherwise the new vertex is connected to every vertex in the neighbor-list of the result-vertex and still has space for more
297+
if (new_neighbor_weight < 0) {
298+
std::fprintf(stderr, "it was not possible to find an edge (best weight %f) in the neighbor list of vertex %u which would connect to vertex %u \n", new_neighbor_weight, candidate_index, internal_index);
299+
std::perror("");
300+
std::abort();
301+
}
302+
303+
// place the new vertex in the edge list of the result-vertex
304+
const auto candidate_dist = dist_func(new_vertex_feature, graph.getFeatureVector(candidate_index), dist_func_param);
305+
graph.changeEdge(candidate_index, new_neighbor_index, internal_index, candidate_dist);
306+
new_neighbors.emplace_back(candidate_index, candidate_dist);
307+
308+
// place the new vertex in the edge list of the worst edge neighbor
309+
const auto new_neighbor_dist = dist_func(new_vertex_feature, graph.getFeatureVector(new_neighbor_index), dist_func_param);
310+
graph.changeEdge(new_neighbor_index, candidate_index, internal_index, new_neighbor_dist);
311+
new_neighbors.emplace_back(new_neighbor_index, new_neighbor_dist);
312+
}
313+
314+
// sort the neighbors by their neighbor indices and store them in the new vertex
315+
std::sort(new_neighbors.begin(), new_neighbors.end(), [](const auto& x, const auto& y) { return x.first < y.first; });
316+
auto neighbor_indices = std::vector<uint32_t>();
317+
auto neighbor_weights = std::vector<float>();
318+
for (auto &&neighbor : new_neighbors) {
319+
neighbor_indices.emplace_back(neighbor.first);
320+
neighbor_weights.emplace_back(neighbor.second);
321+
}
322+
graph.changeEdges(internal_index, neighbor_indices.data(), neighbor_weights.data());
323+
}
324+
325+
return graph;
326+
}
327+
199328
/**
200329
* Current maximal capacity of vertices
201330
*/

0 commit comments

Comments
 (0)