From 4bb901762522fdbb8dc8f1899a37ed47556bf9c0 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Thu, 4 Dec 2025 22:46:42 +0100 Subject: [PATCH 1/3] initial impl of an undirected hyperedge list --- .../hgl/impl/undirected_hyperedge_list.hpp | 106 ++++++++++++++++++ .../hgl/test_undirected_hyperedge_list.cpp | 16 +++ 2 files changed, 122 insertions(+) create mode 100644 include/hgl/impl/undirected_hyperedge_list.hpp create mode 100644 tests/source/hgl/test_undirected_hyperedge_list.cpp diff --git a/include/hgl/impl/undirected_hyperedge_list.hpp b/include/hgl/impl/undirected_hyperedge_list.hpp new file mode 100644 index 00000000..f9010713 --- /dev/null +++ b/include/hgl/impl/undirected_hyperedge_list.hpp @@ -0,0 +1,106 @@ +// Copyright (c) 2024-2026 Jakub MusiaƂ +// This file is part of the CPP-GL project (https://github.com/SpectraL519/cpp-gl). +// Licensed under the MIT License. See the LICENSE file in the project root for full license information. + +#pragma once + +#include "hgl/types/types.hpp" + +#include +#include + +namespace hgl::impl { + +// *** BENCHMARKS *** +// - vertex removal: https://quick-bench.com/q/w6QztI4UIZMv6D7cKYbFtSoA9oY +// - complete hypergraph generation: https://quick-bench.com/q/yLyh1XQoJ1Wg63FM-2ZIpx7SDRA +// - element searching: https://quick-bench.com/q/hk8SOkwk1IfaBCEP_NdFEwWQulU +// binary_search = lower_bound + template equality check with std::invoke + +class undirected_hyperedge_list final { +public: + using hyperedge_storage_type = std::vector; + using hypergraph_storage_type = std::vector; + + undirected_hyperedge_list(const undirected_hyperedge_list&) = delete; + undirected_hyperedge_list& operator=(const undirected_hyperedge_list&) = delete; + + undirected_hyperedge_list() = default; + + undirected_hyperedge_list( + [[maybe_unused]] const types::size_type n_vertices, const types::size_type n_hyperedges + ) + : _storage{n_hyperedges} {} + + undirected_hyperedge_list(undirected_hyperedge_list&&) = default; + undirected_hyperedge_list& operator=(undirected_hyperedge_list&&) = default; + + ~undirected_hyperedge_list() = default; + + // --- vertex methods --- + + gl_attr_force_inline void add_vertices(const types::size_type) const noexcept {} + + void remove_vertex(const types::id_type vertex_id) noexcept { + for (auto& hyperedge_vertices : this->_storage) + this->_unbind_impl(hyperedge_vertices, vertex_id); + } + + // --- hyperedge methods --- + + gl_attr_force_inline void add_hyperedges(const types::size_type n) noexcept { + this->_storage.resize(this->_storage.size() + n); + } + + gl_attr_force_inline void remove_hyperedge(const types::id_type hyperedge_id) noexcept { + this->_storage.erase(this->_storage.begin() + hyperedge_id); + } + + [[nodiscard]] gl_attr_force_inline types::size_type hyperedge_size( + const types::id_type hyperedge_id + ) const noexcept { + return this->_storage[hyperedge_id].size(); + } + + [[nodiscard]] gl_attr_force_inline auto hyperedge_vertices(const types::id_type hyperedge_id + ) const noexcept { + return std::views::all(this->_storage[hyperedge_id]); + } + + // --- binding methods --- + + void bind(const types::id_type hyperedge_id, const types::id_type vertex_id) noexcept { + auto& hyperedge_vertices = this->_storage[hyperedge_id]; + + // insert the id at the correct position to keep the vertex-id collection sorted + const auto it = std::ranges::lower_bound(hyperedge_vertices, vertex_id); + if (it == hyperedge_vertices.end() or *it != vertex_id) + hyperedge_vertices.insert(it, vertex_id); + } + + gl_attr_force_inline void unbind( + const types::id_type hyperedge_id, const types::id_type vertex_id + ) noexcept { + this->_unbind_impl(this->_storage[hyperedge_id], vertex_id); + } + + [[nodiscard]] bool are_bound(const types::id_type hyperedge_id, const types::id_type vertex_id) + const noexcept { + auto& hyperedge_vertices = this->_storage[hyperedge_id]; + const auto vertex_it = std::ranges::lower_bound(hyperedge_vertices, vertex_id); + return vertex_it != hyperedge_vertices.end() and *vertex_it == vertex_id; + } + +private: + void _unbind_impl( + hyperedge_storage_type& hyperedge_vertices, const types::id_type vertex_id + ) noexcept { + const auto vertex_it = std::ranges::lower_bound(hyperedge_vertices, vertex_id); + if (vertex_it != hyperedge_vertices.end() and *vertex_it == vertex_id) + hyperedge_vertices.erase(vertex_it); + } + + hypergraph_storage_type _storage; +}; + +} // namespace hgl::impl diff --git a/tests/source/hgl/test_undirected_hyperedge_list.cpp b/tests/source/hgl/test_undirected_hyperedge_list.cpp new file mode 100644 index 00000000..a7907fce --- /dev/null +++ b/tests/source/hgl/test_undirected_hyperedge_list.cpp @@ -0,0 +1,16 @@ +#include "testing/hgl/constants.hpp" + +#include +#include + +namespace hgl_testing { + +TEST_SUITE_BEGIN("test_undirected_hyperedge_list"); + +TEST_CASE("") { + CHECK(true); +} + +TEST_SUITE_END(); // test_undirected_hyperedge_list + +} // namespace hgl_testing From 1365e2ddd041c76cb62df97ea0672c387918a7ed Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Fri, 5 Dec 2025 12:02:23 +0100 Subject: [PATCH 2/3] wip: undirected hyperedge list tests --- ..._hyperedge_list.hpp => hyperedge_list.hpp} | 10 +++ tests/source/hgl/test_hyperedge_list.cpp | 72 +++++++++++++++++++ .../hgl/test_undirected_hyperedge_list.cpp | 16 ----- 3 files changed, 82 insertions(+), 16 deletions(-) rename include/hgl/impl/{undirected_hyperedge_list.hpp => hyperedge_list.hpp} (95%) create mode 100644 tests/source/hgl/test_hyperedge_list.cpp delete mode 100644 tests/source/hgl/test_undirected_hyperedge_list.cpp diff --git a/include/hgl/impl/undirected_hyperedge_list.hpp b/include/hgl/impl/hyperedge_list.hpp similarity index 95% rename from include/hgl/impl/undirected_hyperedge_list.hpp rename to include/hgl/impl/hyperedge_list.hpp index f9010713..a16d182a 100644 --- a/include/hgl/impl/undirected_hyperedge_list.hpp +++ b/include/hgl/impl/hyperedge_list.hpp @@ -9,6 +9,12 @@ #include #include +#ifdef HGL_TESTING +namespace hgl_testing { +struct test_hyperedge_list; +} // namespace hgl_testing +#endif + namespace hgl::impl { // *** BENCHMARKS *** @@ -91,6 +97,10 @@ class undirected_hyperedge_list final { return vertex_it != hyperedge_vertices.end() and *vertex_it == vertex_id; } +#ifdef HGL_TESTING + friend struct hgl_testing::test_hyperedge_list; +#endif + private: void _unbind_impl( hyperedge_storage_type& hyperedge_vertices, const types::id_type vertex_id diff --git a/tests/source/hgl/test_hyperedge_list.cpp b/tests/source/hgl/test_hyperedge_list.cpp new file mode 100644 index 00000000..dfa80c74 --- /dev/null +++ b/tests/source/hgl/test_hyperedge_list.cpp @@ -0,0 +1,72 @@ +#include "testing/hgl/constants.hpp" + +#include +#include + +#include + +namespace hgl_testing { + +TEST_SUITE_BEGIN("test_hyperedge_list"); + +struct test_hyperedge_list { + template + typename HyperedgeList::hypergraph_storage_type& storage(HyperedgeList& sut) const noexcept { + return sut._storage; + } +}; + +struct test_undirected_hyperedge_list : public test_hyperedge_list { + using sut_type = hgl::impl::undirected_hyperedge_list; +}; + +TEST_CASE_FIXTURE(test_undirected_hyperedge_list, "should initialize empty storage by default") { + sut_type sut{}; + CHECK(storage(sut).empty()); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_list, + "initialization with size parameters should properly initialize storage" +) { + sut_type sut(constants::n_vertices, constants::n_hyperedges); + CHECK_EQ(storage(sut).size(), constants::n_hyperedges); + CHECK(std::ranges::all_of(storage(sut), [](const auto& hyperedge_storage) { + return hyperedge_storage.empty(); + })); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_list, "add_hyperedges should properly extend the hypergraph storage" +) { + sut_type sut{}; + sut.add_hyperedges(constants::n_hyperedges); + CHECK_EQ(storage(sut).size(), constants::n_hyperedges); + CHECK(std::ranges::all_of(storage(sut), [](const auto& hyperedge_storage) { + return hyperedge_storage.empty(); + })); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_list, + "remove_hyperedge should properly erase the proper hyperedge storage from the hypergraph" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE_EQ(storage(sut).size(), constants::n_hyperedges); + + sut.remove_hyperedge(constants::id1); + CHECK_EQ(storage(sut).size(), constants::n_hyperedges - 1uz); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_list, "hyperedge_vertices should remove an empty view by default" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + CHECK(std::ranges::all_of(constants::hyperedge_ids_view, [&sut](const auto hyperedge_id) { + return std::ranges::empty(sut.hyperedge_vertices(hyperedge_id)); + })); +} + +TEST_SUITE_END(); // test_hyperedge_list + +} // namespace hgl_testing diff --git a/tests/source/hgl/test_undirected_hyperedge_list.cpp b/tests/source/hgl/test_undirected_hyperedge_list.cpp deleted file mode 100644 index a7907fce..00000000 --- a/tests/source/hgl/test_undirected_hyperedge_list.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "testing/hgl/constants.hpp" - -#include -#include - -namespace hgl_testing { - -TEST_SUITE_BEGIN("test_undirected_hyperedge_list"); - -TEST_CASE("") { - CHECK(true); -} - -TEST_SUITE_END(); // test_undirected_hyperedge_list - -} // namespace hgl_testing From a77cd619f60423601fdf3330ad7da11b0a08061a Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Fri, 5 Dec 2025 12:30:17 +0100 Subject: [PATCH 3/3] undirected hyperedge list tests --- tests/source/hgl/test_hyperedge_list.cpp | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/source/hgl/test_hyperedge_list.cpp b/tests/source/hgl/test_hyperedge_list.cpp index dfa80c74..c96720ec 100644 --- a/tests/source/hgl/test_hyperedge_list.cpp +++ b/tests/source/hgl/test_hyperedge_list.cpp @@ -58,6 +58,13 @@ TEST_CASE_FIXTURE( CHECK_EQ(storage(sut).size(), constants::n_hyperedges - 1uz); } +TEST_CASE_FIXTURE(test_undirected_hyperedge_list, "hyperedge_size should return 0 by default") { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + CHECK(std::ranges::all_of(constants::hyperedge_ids_view, [&sut](const auto hyperedge_id) { + return sut.hyperedge_size(hyperedge_id) == 0uz; + })); +} + TEST_CASE_FIXTURE( test_undirected_hyperedge_list, "hyperedge_vertices should remove an empty view by default" ) { @@ -67,6 +74,84 @@ TEST_CASE_FIXTURE( })); } +TEST_CASE_FIXTURE( + test_undirected_hyperedge_list, + "bind(hyperedge, vertex) should add the vertex to the given hyperedge's storage only if the " + "they are not bound" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.hyperedge_vertices(constants::id1))); + + sut.bind(constants::id1, constants::id1); + const auto vertices1 = sut.hyperedge_vertices(constants::id1) | std::ranges::to(); + CHECK_EQ(sut.hyperedge_size(constants::id1), 1uz); + CHECK_EQ(std::ranges::size(vertices1), 1uz); + CHECK(std::ranges::contains(vertices1, constants::id1)); + CHECK(std::ranges::equal(vertices1, storage(sut)[constants::id1])); + + sut.bind(constants::id1, constants::id1); + const auto vertices2 = sut.hyperedge_vertices(constants::id1) | std::ranges::to(); + CHECK_EQ(std::ranges::size(vertices2), 1uz); + CHECK(std::ranges::equal(vertices2, vertices1)); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_list, + "unbind(hyperedge, vertex) should remove the vertex from the given hyperedge's storage only if " + "the they are bound" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.hyperedge_vertices(constants::id1))); + + sut.bind(constants::id1, constants::id1); + const auto vertices1 = sut.hyperedge_vertices(constants::id1) | std::ranges::to(); + REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz); + REQUIRE(std::ranges::contains(vertices1, constants::id1)); + + sut.unbind(constants::id1, constants::id2); + const auto vertices2 = sut.hyperedge_vertices(constants::id1) | std::ranges::to(); + REQUIRE_EQ(std::ranges::size(vertices2), 1uz); + REQUIRE(std::ranges::equal(vertices2, vertices1)); + + sut.unbind(constants::id1, constants::id1); + CHECK(std::ranges::empty(sut.hyperedge_vertices(constants::id1))); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_list, + "are_bound(hyperedge, vertex) should return true only when the given vertex is present in the " + "hyperedge's storage" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.hyperedge_vertices(constants::id1))); + + sut.bind(constants::id1, constants::id1); + const auto vertices1 = sut.hyperedge_vertices(constants::id1) | std::ranges::to(); + REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz); + REQUIRE(std::ranges::contains(vertices1, constants::id1)); + + CHECK(sut.are_bound(constants::id1, constants::id1)); + CHECK_FALSE(sut.are_bound(constants::id1, constants::id2)); + CHECK_FALSE(sut.are_bound(constants::id2, constants::id1)); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_list, + "remove_vertex should unbind the given vertex from all hyperedges" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + for (const auto hyperedge_id : constants::hyperedge_ids_view) + sut.bind(hyperedge_id, constants::id1); + REQUIRE(std::ranges::all_of(constants::hyperedge_ids_view, [&sut](const auto hyperedge_id) { + return sut.are_bound(hyperedge_id, constants::id1); + })); + + sut.remove_vertex(constants::id1); + CHECK(std::ranges::all_of(constants::hyperedge_ids_view, [&sut](const auto hyperedge_id) { + return std::ranges::empty(sut.hyperedge_vertices(hyperedge_id)); + })); +} + TEST_SUITE_END(); // test_hyperedge_list } // namespace hgl_testing