Skip to content

Commit afa3fc9

Browse files
authored
YT-CPPGHL-6: Implement the undirected hyperedge-list model
Created the `impl::undirected_hyperedge_list` model class, which stores a list of hyperedges and their incident vertices and defines the following operations: - Adding and removing vertices - Adding and removing hyperedges - Binding and unbinding vertices to hyperedges - Retrieving the number of vertices incident with a given hyperedge - Retrieving the set of vertices incident with a given hyperedge - Validating whether a vertex is bound to a hyperedge
1 parent 04a53ef commit afa3fc9

2 files changed

Lines changed: 273 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) 2024-2026 Jakub Musiał
2+
// This file is part of the CPP-GL project (https://github.com/SpectraL519/cpp-gl).
3+
// Licensed under the MIT License. See the LICENSE file in the project root for full license information.
4+
5+
#pragma once
6+
7+
#include "hgl/types/types.hpp"
8+
9+
#include <algorithm>
10+
#include <vector>
11+
12+
#ifdef HGL_TESTING
13+
namespace hgl_testing {
14+
struct test_hyperedge_list;
15+
} // namespace hgl_testing
16+
#endif
17+
18+
namespace hgl::impl {
19+
20+
// *** BENCHMARKS ***
21+
// - vertex removal: https://quick-bench.com/q/w6QztI4UIZMv6D7cKYbFtSoA9oY
22+
// - complete hypergraph generation: https://quick-bench.com/q/yLyh1XQoJ1Wg63FM-2ZIpx7SDRA
23+
// - element searching: https://quick-bench.com/q/hk8SOkwk1IfaBCEP_NdFEwWQulU
24+
// binary_search = lower_bound + template equality check with std::invoke
25+
26+
class undirected_hyperedge_list final {
27+
public:
28+
using hyperedge_storage_type = std::vector<types::id_type>;
29+
using hypergraph_storage_type = std::vector<hyperedge_storage_type>;
30+
31+
undirected_hyperedge_list(const undirected_hyperedge_list&) = delete;
32+
undirected_hyperedge_list& operator=(const undirected_hyperedge_list&) = delete;
33+
34+
undirected_hyperedge_list() = default;
35+
36+
undirected_hyperedge_list(
37+
[[maybe_unused]] const types::size_type n_vertices, const types::size_type n_hyperedges
38+
)
39+
: _storage{n_hyperedges} {}
40+
41+
undirected_hyperedge_list(undirected_hyperedge_list&&) = default;
42+
undirected_hyperedge_list& operator=(undirected_hyperedge_list&&) = default;
43+
44+
~undirected_hyperedge_list() = default;
45+
46+
// --- vertex methods ---
47+
48+
gl_attr_force_inline void add_vertices(const types::size_type) const noexcept {}
49+
50+
void remove_vertex(const types::id_type vertex_id) noexcept {
51+
for (auto& hyperedge_vertices : this->_storage)
52+
this->_unbind_impl(hyperedge_vertices, vertex_id);
53+
}
54+
55+
// --- hyperedge methods ---
56+
57+
gl_attr_force_inline void add_hyperedges(const types::size_type n) noexcept {
58+
this->_storage.resize(this->_storage.size() + n);
59+
}
60+
61+
gl_attr_force_inline void remove_hyperedge(const types::id_type hyperedge_id) noexcept {
62+
this->_storage.erase(this->_storage.begin() + hyperedge_id);
63+
}
64+
65+
[[nodiscard]] gl_attr_force_inline types::size_type hyperedge_size(
66+
const types::id_type hyperedge_id
67+
) const noexcept {
68+
return this->_storage[hyperedge_id].size();
69+
}
70+
71+
[[nodiscard]] gl_attr_force_inline auto hyperedge_vertices(const types::id_type hyperedge_id
72+
) const noexcept {
73+
return std::views::all(this->_storage[hyperedge_id]);
74+
}
75+
76+
// --- binding methods ---
77+
78+
void bind(const types::id_type hyperedge_id, const types::id_type vertex_id) noexcept {
79+
auto& hyperedge_vertices = this->_storage[hyperedge_id];
80+
81+
// insert the id at the correct position to keep the vertex-id collection sorted
82+
const auto it = std::ranges::lower_bound(hyperedge_vertices, vertex_id);
83+
if (it == hyperedge_vertices.end() or *it != vertex_id)
84+
hyperedge_vertices.insert(it, vertex_id);
85+
}
86+
87+
gl_attr_force_inline void unbind(
88+
const types::id_type hyperedge_id, const types::id_type vertex_id
89+
) noexcept {
90+
this->_unbind_impl(this->_storage[hyperedge_id], vertex_id);
91+
}
92+
93+
[[nodiscard]] bool are_bound(const types::id_type hyperedge_id, const types::id_type vertex_id)
94+
const noexcept {
95+
auto& hyperedge_vertices = this->_storage[hyperedge_id];
96+
const auto vertex_it = std::ranges::lower_bound(hyperedge_vertices, vertex_id);
97+
return vertex_it != hyperedge_vertices.end() and *vertex_it == vertex_id;
98+
}
99+
100+
#ifdef HGL_TESTING
101+
friend struct hgl_testing::test_hyperedge_list;
102+
#endif
103+
104+
private:
105+
void _unbind_impl(
106+
hyperedge_storage_type& hyperedge_vertices, const types::id_type vertex_id
107+
) noexcept {
108+
const auto vertex_it = std::ranges::lower_bound(hyperedge_vertices, vertex_id);
109+
if (vertex_it != hyperedge_vertices.end() and *vertex_it == vertex_id)
110+
hyperedge_vertices.erase(vertex_it);
111+
}
112+
113+
hypergraph_storage_type _storage;
114+
};
115+
116+
} // namespace hgl::impl
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#include "testing/hgl/constants.hpp"
2+
3+
#include <doctest.h>
4+
#include <hgl/impl/hyperedge_list.hpp>
5+
6+
#include <algorithm>
7+
8+
namespace hgl_testing {
9+
10+
TEST_SUITE_BEGIN("test_hyperedge_list");
11+
12+
struct test_hyperedge_list {
13+
template <typename HyperedgeList>
14+
typename HyperedgeList::hypergraph_storage_type& storage(HyperedgeList& sut) const noexcept {
15+
return sut._storage;
16+
}
17+
};
18+
19+
struct test_undirected_hyperedge_list : public test_hyperedge_list {
20+
using sut_type = hgl::impl::undirected_hyperedge_list;
21+
};
22+
23+
TEST_CASE_FIXTURE(test_undirected_hyperedge_list, "should initialize empty storage by default") {
24+
sut_type sut{};
25+
CHECK(storage(sut).empty());
26+
}
27+
28+
TEST_CASE_FIXTURE(
29+
test_undirected_hyperedge_list,
30+
"initialization with size parameters should properly initialize storage"
31+
) {
32+
sut_type sut(constants::n_vertices, constants::n_hyperedges);
33+
CHECK_EQ(storage(sut).size(), constants::n_hyperedges);
34+
CHECK(std::ranges::all_of(storage(sut), [](const auto& hyperedge_storage) {
35+
return hyperedge_storage.empty();
36+
}));
37+
}
38+
39+
TEST_CASE_FIXTURE(
40+
test_undirected_hyperedge_list, "add_hyperedges should properly extend the hypergraph storage"
41+
) {
42+
sut_type sut{};
43+
sut.add_hyperedges(constants::n_hyperedges);
44+
CHECK_EQ(storage(sut).size(), constants::n_hyperedges);
45+
CHECK(std::ranges::all_of(storage(sut), [](const auto& hyperedge_storage) {
46+
return hyperedge_storage.empty();
47+
}));
48+
}
49+
50+
TEST_CASE_FIXTURE(
51+
test_undirected_hyperedge_list,
52+
"remove_hyperedge should properly erase the proper hyperedge storage from the hypergraph"
53+
) {
54+
sut_type sut{constants::n_vertices, constants::n_hyperedges};
55+
REQUIRE_EQ(storage(sut).size(), constants::n_hyperedges);
56+
57+
sut.remove_hyperedge(constants::id1);
58+
CHECK_EQ(storage(sut).size(), constants::n_hyperedges - 1uz);
59+
}
60+
61+
TEST_CASE_FIXTURE(test_undirected_hyperedge_list, "hyperedge_size should return 0 by default") {
62+
sut_type sut{constants::n_vertices, constants::n_hyperedges};
63+
CHECK(std::ranges::all_of(constants::hyperedge_ids_view, [&sut](const auto hyperedge_id) {
64+
return sut.hyperedge_size(hyperedge_id) == 0uz;
65+
}));
66+
}
67+
68+
TEST_CASE_FIXTURE(
69+
test_undirected_hyperedge_list, "hyperedge_vertices should remove an empty view by default"
70+
) {
71+
sut_type sut{constants::n_vertices, constants::n_hyperedges};
72+
CHECK(std::ranges::all_of(constants::hyperedge_ids_view, [&sut](const auto hyperedge_id) {
73+
return std::ranges::empty(sut.hyperedge_vertices(hyperedge_id));
74+
}));
75+
}
76+
77+
TEST_CASE_FIXTURE(
78+
test_undirected_hyperedge_list,
79+
"bind(hyperedge, vertex) should add the vertex to the given hyperedge's storage only if the "
80+
"they are not bound"
81+
) {
82+
sut_type sut{constants::n_vertices, constants::n_hyperedges};
83+
REQUIRE(std::ranges::empty(sut.hyperedge_vertices(constants::id1)));
84+
85+
sut.bind(constants::id1, constants::id1);
86+
const auto vertices1 = sut.hyperedge_vertices(constants::id1) | std::ranges::to<std::vector>();
87+
CHECK_EQ(sut.hyperedge_size(constants::id1), 1uz);
88+
CHECK_EQ(std::ranges::size(vertices1), 1uz);
89+
CHECK(std::ranges::contains(vertices1, constants::id1));
90+
CHECK(std::ranges::equal(vertices1, storage(sut)[constants::id1]));
91+
92+
sut.bind(constants::id1, constants::id1);
93+
const auto vertices2 = sut.hyperedge_vertices(constants::id1) | std::ranges::to<std::vector>();
94+
CHECK_EQ(std::ranges::size(vertices2), 1uz);
95+
CHECK(std::ranges::equal(vertices2, vertices1));
96+
}
97+
98+
TEST_CASE_FIXTURE(
99+
test_undirected_hyperedge_list,
100+
"unbind(hyperedge, vertex) should remove the vertex from the given hyperedge's storage only if "
101+
"the they are bound"
102+
) {
103+
sut_type sut{constants::n_vertices, constants::n_hyperedges};
104+
REQUIRE(std::ranges::empty(sut.hyperedge_vertices(constants::id1)));
105+
106+
sut.bind(constants::id1, constants::id1);
107+
const auto vertices1 = sut.hyperedge_vertices(constants::id1) | std::ranges::to<std::vector>();
108+
REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz);
109+
REQUIRE(std::ranges::contains(vertices1, constants::id1));
110+
111+
sut.unbind(constants::id1, constants::id2);
112+
const auto vertices2 = sut.hyperedge_vertices(constants::id1) | std::ranges::to<std::vector>();
113+
REQUIRE_EQ(std::ranges::size(vertices2), 1uz);
114+
REQUIRE(std::ranges::equal(vertices2, vertices1));
115+
116+
sut.unbind(constants::id1, constants::id1);
117+
CHECK(std::ranges::empty(sut.hyperedge_vertices(constants::id1)));
118+
}
119+
120+
TEST_CASE_FIXTURE(
121+
test_undirected_hyperedge_list,
122+
"are_bound(hyperedge, vertex) should return true only when the given vertex is present in the "
123+
"hyperedge's storage"
124+
) {
125+
sut_type sut{constants::n_vertices, constants::n_hyperedges};
126+
REQUIRE(std::ranges::empty(sut.hyperedge_vertices(constants::id1)));
127+
128+
sut.bind(constants::id1, constants::id1);
129+
const auto vertices1 = sut.hyperedge_vertices(constants::id1) | std::ranges::to<std::vector>();
130+
REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz);
131+
REQUIRE(std::ranges::contains(vertices1, constants::id1));
132+
133+
CHECK(sut.are_bound(constants::id1, constants::id1));
134+
CHECK_FALSE(sut.are_bound(constants::id1, constants::id2));
135+
CHECK_FALSE(sut.are_bound(constants::id2, constants::id1));
136+
}
137+
138+
TEST_CASE_FIXTURE(
139+
test_undirected_hyperedge_list,
140+
"remove_vertex should unbind the given vertex from all hyperedges"
141+
) {
142+
sut_type sut{constants::n_vertices, constants::n_hyperedges};
143+
for (const auto hyperedge_id : constants::hyperedge_ids_view)
144+
sut.bind(hyperedge_id, constants::id1);
145+
REQUIRE(std::ranges::all_of(constants::hyperedge_ids_view, [&sut](const auto hyperedge_id) {
146+
return sut.are_bound(hyperedge_id, constants::id1);
147+
}));
148+
149+
sut.remove_vertex(constants::id1);
150+
CHECK(std::ranges::all_of(constants::hyperedge_ids_view, [&sut](const auto hyperedge_id) {
151+
return std::ranges::empty(sut.hyperedge_vertices(hyperedge_id));
152+
}));
153+
}
154+
155+
TEST_SUITE_END(); // test_hyperedge_list
156+
157+
} // namespace hgl_testing

0 commit comments

Comments
 (0)