Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions include/hgl/impl/hyperedge_list.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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 <algorithm>
#include <vector>

#ifdef HGL_TESTING
namespace hgl_testing {
struct test_hyperedge_list;
} // namespace hgl_testing
#endif

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<types::id_type>;
using hypergraph_storage_type = std::vector<hyperedge_storage_type>;

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;
}

#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
) 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
157 changes: 157 additions & 0 deletions tests/source/hgl/test_hyperedge_list.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#include "testing/hgl/constants.hpp"

#include <doctest.h>
#include <hgl/impl/hyperedge_list.hpp>

#include <algorithm>

namespace hgl_testing {

TEST_SUITE_BEGIN("test_hyperedge_list");

struct test_hyperedge_list {
template <typename HyperedgeList>
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_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"
) {
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_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<std::vector>();
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<std::vector>();
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<std::vector>();
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<std::vector>();
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<std::vector>();
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