Skip to content

Commit dc7734b

Browse files
Implement undirected graph
1 parent 351bb39 commit dc7734b

9 files changed

Lines changed: 714 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ find_package(nanobind CONFIG REQUIRED)
1212
nanobind_add_module(_core
1313
NB_STATIC
1414
src/bindings/module.cxx
15+
src/bindings/graph.cxx
1516
src/bindings/segmentation.cxx
1617
src/bindings/utils.cxx
1718
src/cpp/segmentation/mutex_watershed.cxx

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ segmentation = bic.segmentation.mutex_watershed(
4141
)
4242
```
4343

44+
```python
45+
graph = bic.graph.UndirectedGraph.from_edges(4, [[0, 1], [1, 2], [2, 3]])
46+
graph.find_edge(2, 1)
47+
# 1
48+
```
49+
4450
## Scope
4551

4652
The project is not a compatibility layer for `nifty`, `vigra`, or other large libraries. It keeps I/O and heavy dependencies out of the C++ core; callers should use existing Python packages for file formats and pass NumPy arrays into `bioimage-cpp`.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <cstddef>
5+
#include <cstdint>
6+
#include <stdexcept>
7+
#include <string>
8+
#include <unordered_map>
9+
#include <unordered_set>
10+
#include <utility>
11+
#include <vector>
12+
13+
namespace bioimage_cpp::graph {
14+
15+
struct Adjacency {
16+
std::uint64_t node;
17+
std::uint64_t edge;
18+
};
19+
20+
class UndirectedGraph {
21+
public:
22+
using NodeId = std::uint64_t;
23+
using EdgeId = std::uint64_t;
24+
using Edge = std::pair<NodeId, NodeId>;
25+
using AdjacencyList = std::vector<Adjacency>;
26+
27+
explicit UndirectedGraph(
28+
const NodeId number_of_nodes = 0,
29+
const EdgeId reserve_number_of_edges = 0
30+
)
31+
: number_of_nodes_(number_of_nodes),
32+
adjacency_(static_cast<std::size_t>(number_of_nodes)) {
33+
edges_.reserve(static_cast<std::size_t>(reserve_number_of_edges));
34+
}
35+
36+
virtual ~UndirectedGraph() = default;
37+
38+
void assign(
39+
const NodeId number_of_nodes = 0,
40+
const EdgeId reserve_number_of_edges = 0
41+
) {
42+
number_of_nodes_ = number_of_nodes;
43+
edges_.clear();
44+
edge_lookup_.clear();
45+
adjacency_.clear();
46+
adjacency_.resize(static_cast<std::size_t>(number_of_nodes));
47+
edges_.reserve(static_cast<std::size_t>(reserve_number_of_edges));
48+
}
49+
50+
[[nodiscard]] NodeId number_of_nodes() const {
51+
return number_of_nodes_;
52+
}
53+
54+
[[nodiscard]] EdgeId number_of_edges() const {
55+
return static_cast<EdgeId>(edges_.size());
56+
}
57+
58+
[[nodiscard]] NodeId node_id_upper_bound() const {
59+
return number_of_nodes_ == 0 ? 0 : number_of_nodes_ - 1;
60+
}
61+
62+
[[nodiscard]] EdgeId edge_id_upper_bound() const {
63+
return edges_.empty() ? 0 : static_cast<EdgeId>(edges_.size() - 1);
64+
}
65+
66+
[[nodiscard]] std::vector<NodeId> nodes() const {
67+
std::vector<NodeId> result(static_cast<std::size_t>(number_of_nodes_));
68+
for (NodeId node = 0; node < number_of_nodes_; ++node) {
69+
result[static_cast<std::size_t>(node)] = node;
70+
}
71+
return result;
72+
}
73+
74+
[[nodiscard]] std::vector<EdgeId> edges() const {
75+
std::vector<EdgeId> result(edges_.size());
76+
for (EdgeId edge = 0; edge < edges_.size(); ++edge) {
77+
result[static_cast<std::size_t>(edge)] = edge;
78+
}
79+
return result;
80+
}
81+
82+
[[nodiscard]] Edge uv(const EdgeId edge) const {
83+
validate_edge(edge);
84+
return edges_[static_cast<std::size_t>(edge)];
85+
}
86+
87+
[[nodiscard]] NodeId u(const EdgeId edge) const {
88+
return uv(edge).first;
89+
}
90+
91+
[[nodiscard]] NodeId v(const EdgeId edge) const {
92+
return uv(edge).second;
93+
}
94+
95+
[[nodiscard]] const std::vector<Edge> &uv_ids() const {
96+
return edges_;
97+
}
98+
99+
[[nodiscard]] const AdjacencyList &node_adjacency(const NodeId node) const {
100+
validate_node(node);
101+
return adjacency_[static_cast<std::size_t>(node)];
102+
}
103+
104+
EdgeId insert_edge(const NodeId u, const NodeId v) {
105+
validate_node(u);
106+
validate_node(v);
107+
if (u == v) {
108+
throw std::invalid_argument("self edges are not supported");
109+
}
110+
111+
const auto key = edge_key(u, v);
112+
const auto found = edge_lookup_.find(key);
113+
if (found != edge_lookup_.end()) {
114+
return found->second;
115+
}
116+
return insert_new_edge(key.first, key.second);
117+
}
118+
119+
[[nodiscard]] std::int64_t find_edge(const NodeId u, const NodeId v) const {
120+
validate_node(u);
121+
validate_node(v);
122+
if (u == v) {
123+
return -1;
124+
}
125+
126+
const auto key = edge_key(u, v);
127+
const auto found = edge_lookup_.find(key);
128+
if (found == edge_lookup_.end()) {
129+
return -1;
130+
}
131+
return static_cast<std::int64_t>(found->second);
132+
}
133+
134+
[[nodiscard]] std::uint64_t serialization_size() const {
135+
return 2 + 2 * number_of_edges();
136+
}
137+
138+
[[nodiscard]] std::pair<std::vector<EdgeId>, std::vector<EdgeId>>
139+
extract_subgraph_from_nodes(const std::vector<NodeId> &nodes) const {
140+
std::unordered_set<NodeId> node_set;
141+
node_set.reserve(nodes.size());
142+
for (const auto node : nodes) {
143+
validate_node(node);
144+
node_set.insert(node);
145+
}
146+
147+
std::vector<EdgeId> inner_edges;
148+
std::vector<EdgeId> outer_edges;
149+
for (const auto u : nodes) {
150+
for (const auto adjacency : node_adjacency(u)) {
151+
const auto v = adjacency.node;
152+
const auto edge = adjacency.edge;
153+
if (node_set.find(v) != node_set.end()) {
154+
if (u < v) {
155+
inner_edges.push_back(edge);
156+
}
157+
} else {
158+
outer_edges.push_back(edge);
159+
}
160+
}
161+
}
162+
return {inner_edges, outer_edges};
163+
}
164+
165+
protected:
166+
EdgeId insert_new_edge(const NodeId u, const NodeId v) {
167+
const auto edge = static_cast<EdgeId>(edges_.size());
168+
edges_.emplace_back(u, v);
169+
edge_lookup_.emplace(Edge{u, v}, edge);
170+
adjacency_[static_cast<std::size_t>(u)].push_back(Adjacency{v, edge});
171+
adjacency_[static_cast<std::size_t>(v)].push_back(Adjacency{u, edge});
172+
return edge;
173+
}
174+
175+
void validate_node(const NodeId node) const {
176+
if (node >= number_of_nodes_) {
177+
throw std::out_of_range(
178+
"node id must be < number_of_nodes, got node id=" +
179+
std::to_string(node) + ", number_of_nodes=" +
180+
std::to_string(number_of_nodes_)
181+
);
182+
}
183+
}
184+
185+
void validate_edge(const EdgeId edge) const {
186+
if (edge >= edges_.size()) {
187+
throw std::out_of_range(
188+
"edge id must be < number_of_edges, got edge id=" +
189+
std::to_string(edge) + ", number_of_edges=" +
190+
std::to_string(edges_.size())
191+
);
192+
}
193+
}
194+
195+
private:
196+
struct EdgeHash {
197+
std::size_t operator()(const Edge &edge) const {
198+
const auto first = static_cast<std::size_t>(edge.first);
199+
const auto second = static_cast<std::size_t>(edge.second);
200+
return first ^ (second + 0x9e3779b97f4a7c15ULL + (first << 6U) + (first >> 2U));
201+
}
202+
};
203+
204+
static Edge edge_key(NodeId u, NodeId v) {
205+
if (v < u) {
206+
std::swap(u, v);
207+
}
208+
return {u, v};
209+
}
210+
211+
NodeId number_of_nodes_;
212+
std::vector<Edge> edges_;
213+
std::vector<AdjacencyList> adjacency_;
214+
std::unordered_map<Edge, EdgeId, EdgeHash> edge_lookup_;
215+
};
216+
217+
} // namespace bioimage_cpp::graph

0 commit comments

Comments
 (0)