diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml index 42b9d147..f1d5649c 100644 --- a/.github/workflows/format.yaml +++ b/.github/workflows/format.yaml @@ -2,7 +2,7 @@ name: format on: push: branches: - - '*' + - "*" paths: - .github/workflows/format.yaml - scripts/format.py @@ -11,7 +11,6 @@ on: - tests/source/** - tests/app/** - jobs: build: name: Test code formatting @@ -29,4 +28,4 @@ jobs: - name: Test formatting shell: bash run: | - python3 scripts/format.py --check + python3 scripts/format.py --check --clang-format-executable clang-format-18 diff --git a/.github/workflows/licence.yaml b/.github/workflows/licence.yaml index 0ac69af1..a7bef3c0 100644 --- a/.github/workflows/licence.yaml +++ b/.github/workflows/licence.yaml @@ -2,13 +2,12 @@ name: licence on: push: branches: - - '*' + - "*" paths: - .github/workflows/licence.yaml - scripts/check_licence.py - include/** - jobs: build: name: Test code formatting diff --git a/include/hgl/impl/incidence_list.hpp b/include/hgl/impl/incidence_list.hpp index bae01fdf..f0b41cfd 100644 --- a/include/hgl/impl/incidence_list.hpp +++ b/include/hgl/impl/incidence_list.hpp @@ -29,6 +29,7 @@ class incidence_list; template class incidence_list final { public: + using directional_tag = hgl::undirected_t; using layout_tag = LayoutTag; incidence_list(const incidence_list&) = delete; @@ -92,23 +93,29 @@ class incidence_list final { gl_attr_force_inline void bind( const types::id_type vertex_id, const types::id_type hyperedge_id ) noexcept { - this->_bind_impl( - layout_tag::major(vertex_id, hyperedge_id), layout_tag::minor(vertex_id, hyperedge_id) - ); + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + auto& minor_storage = this->_major_storage[major_id]; + + // insert the id at the correct position to keep the minor-id collection sorted + const auto minor_it = std::ranges::lower_bound(minor_storage, minor_id); + if (minor_it == minor_storage.end() or *minor_it != minor_id) + minor_storage.insert(minor_it, minor_id); } gl_attr_force_inline void unbind( const types::id_type vertex_id, const types::id_type hyperedge_id ) noexcept { - this->_unbind_impl( - layout_tag::major(vertex_id, hyperedge_id), layout_tag::minor(vertex_id, hyperedge_id) - ); + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + auto& minor_storage = this->_major_storage[major_id]; + const auto minor_it = std::ranges::lower_bound(minor_storage, minor_id); + if (minor_it != minor_storage.end() and *minor_it == minor_id) + minor_storage.erase(minor_it); } [[nodiscard]] gl_attr_force_inline bool are_bound( const types::id_type vertex_id, const types::id_type hyperedge_id ) const noexcept { - return this->_are_bound_impl( + return this->_contains( this->_major_storage[layout_tag::major(vertex_id, hyperedge_id)], layout_tag::minor(vertex_id, hyperedge_id) ); @@ -150,7 +157,7 @@ class incidence_list final { else { // incident with minor return std::views::iota(0uz, this->_major_storage.size()) | std::views::filter([this, minor_id = id](types::id_type major_id) { - return this->_are_bound_impl(this->_major_storage[major_id], minor_id); + return this->_contains(this->_major_storage[major_id], minor_id); }); } } @@ -163,38 +170,329 @@ class incidence_list final { else { // size minor types::size_type size = 0uz; for (const auto& major_el : this->_major_storage) - if (this->_are_bound_impl(major_el, id)) + if (this->_contains(major_el, id)) ++size; return size; } } - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - void _bind_impl(const types::id_type major_id, const types::id_type minor_id) noexcept { - auto& minor_storage = this->_major_storage[major_id]; - - // insert the id at the correct position to keep the minor-id collection sorted + [[nodiscard]] bool _contains( + const minor_storage_type& minor_storage, const types::id_type minor_id + ) const noexcept { const auto minor_it = std::ranges::lower_bound(minor_storage, minor_id); - if (minor_it == minor_storage.end() or *minor_it != minor_id) - minor_storage.insert(minor_it, minor_id); + return minor_it != minor_storage.end() and *minor_it == minor_id; + } + + major_storage_type _major_storage; +}; + +template +class incidence_list final { +public: + using directional_tag = hgl::bf_directed_t; + using layout_tag = LayoutTag; + + incidence_list(const incidence_list&) = delete; + incidence_list& operator=(const incidence_list&) = delete; + + incidence_list() = default; + + incidence_list(const types::size_type n_vertices, const types::size_type n_hyperedges) + : _major_storage{layout_tag::major(n_vertices, n_hyperedges)} {} + + incidence_list(incidence_list&&) = default; + incidence_list& operator=(incidence_list&&) = default; + + ~incidence_list() = default; + + // --- vertex methods : general --- + + gl_attr_force_inline void add_vertices(const types::size_type n) noexcept { + this->_add(n); + } + + gl_attr_force_inline void remove_vertex(const types::id_type vertex_id) noexcept { + this->_remove(vertex_id); + } + + // --- vertex methods : incidence queries --- + + [[nodiscard]] gl_attr_force_inline auto incident_hyperedges(const types::id_type vertex_id + ) const noexcept { + return this->_get(vertex_id); + } + + [[nodiscard]] types::size_type degree(const types::id_type vertex_id) const noexcept { + return this->_size(vertex_id); } - gl_attr_force_inline void _unbind_impl( - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - const types::id_type major_id, - const types::id_type minor_id + [[nodiscard]] gl_attr_force_inline auto outgoing_hyperedges(const types::id_type vertex_id + ) const noexcept { + return this->_get_tail(vertex_id); + } + + [[nodiscard]] types::size_type out_degree(const types::id_type vertex_id) const noexcept { + return this->_tail_size(vertex_id); + } + + [[nodiscard]] gl_attr_force_inline auto incoming_hyperedges(const types::id_type vertex_id + ) const noexcept { + return this->_get_head(vertex_id); + } + + [[nodiscard]] types::size_type in_degree(const types::id_type vertex_id) const noexcept { + return this->_head_size(vertex_id); + } + + // --- hyperedge methods : general --- + + gl_attr_force_inline void add_hyperedges(const types::size_type n) noexcept { + this->_add(n); + } + + gl_attr_force_inline void remove_hyperedge(const types::id_type hyperedge_id) noexcept { + this->_remove(hyperedge_id); + } + + // --- hyperedge methods : incidence queries --- + + [[nodiscard]] gl_attr_force_inline auto incident_vertices(const types::id_type hyperedge_id + ) const noexcept { + return this->_get(hyperedge_id); + } + + [[nodiscard]] types::size_type hyperedge_size(const types::id_type hyperedge_id + ) const noexcept { + return this->_size(hyperedge_id); + } + + [[nodiscard]] gl_attr_force_inline auto tail_vertices(const types::id_type hyperedge_id + ) const noexcept { + return this->_get_tail(hyperedge_id); + } + + [[nodiscard]] types::size_type tail_size(const types::id_type hyperedge_id) const noexcept { + return this->_tail_size(hyperedge_id); + } + + [[nodiscard]] gl_attr_force_inline auto head_vertices(const types::id_type hyperedge_id + ) const noexcept { + return this->_get_head(hyperedge_id); + } + + [[nodiscard]] types::size_type head_size(const types::id_type hyperedge_id) const noexcept { + return this->_head_size(hyperedge_id); + } + + // --- binding methods --- + + gl_attr_force_inline void bind_tail( + const types::id_type vertex_id, const types::id_type hyperedge_id ) noexcept { - auto& minor_storage = this->_major_storage[major_id]; - const auto minor_it = std::ranges::lower_bound(minor_storage, minor_id); - if (minor_it != minor_storage.end() and *minor_it == minor_id) - minor_storage.erase(minor_it); + // TODO: validate if minor_id is not in head + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + this->_unique_insert(this->_major_storage[major_id].tail, minor_id); + } + + gl_attr_force_inline void bind_head( + const types::id_type vertex_id, const types::id_type hyperedge_id + ) noexcept { + // TODO: validate if minor_id is not in tail + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + this->_unique_insert(this->_major_storage[major_id].head, minor_id); + } + + gl_attr_force_inline void unbind( + const types::id_type vertex_id, const types::id_type hyperedge_id + ) noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + auto& entry = this->_major_storage[major_id]; + this->_remove_no_align(entry.tail, minor_id); + this->_remove_no_align(entry.head, minor_id); + } + + [[nodiscard]] gl_attr_force_inline bool are_bound( + const types::id_type vertex_id, const types::id_type hyperedge_id + ) const noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + return this->_contains(this->_major_storage[major_id], minor_id); + } + + [[nodiscard]] gl_attr_force_inline bool is_tail( + const types::id_type vertex_id, const types::id_type hyperedge_id + ) const noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + return this->_contains(this->_major_storage[major_id].tail, minor_id); + } + + [[nodiscard]] gl_attr_force_inline bool is_head( + const types::id_type vertex_id, const types::id_type hyperedge_id + ) const noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + return this->_contains(this->_major_storage[major_id].head, minor_id); + } + +#ifdef HGL_TESTING + friend struct hgl_testing::test_incidence_list; +#endif + +private: + using minor_element_type = types::id_type; + using minor_storage_type = std::vector; + + /** + * For hyperedge-major representation: tail = T(e), head = H(e) + * For vertex-major representation: + * - tail = {e in E(G) : v in T(e)} + * - head = {e in E(G) : v in H(e)} + * where G is the hypergraph and v is the vertex entry in the outer list + */ + struct major_element_type { + minor_storage_type tail; + minor_storage_type head; + }; + + using major_storage_type = std::vector; + + template + void _add(const types::size_type n) noexcept { + if constexpr (Element == layout_tag::major_element) // add major + this->_major_storage.resize(this->_major_storage.size() + n); + } + + template + void _remove(const types::id_type id) noexcept { + if constexpr (Element == layout_tag::major_element) { // remove major + this->_major_storage.erase( + this->_major_storage.begin() + static_cast(id) + ); + } + else { // remove minor + for (auto& minor_storage : this->_major_storage) { + this->_remove(minor_storage.tail, id); + this->_remove(minor_storage.head, id); + } + } + } + + void _remove(minor_storage_type& minor_storage, const types::id_type id) noexcept { + auto minor_it = this->_remove_no_align(minor_storage, id); + while (minor_it != minor_storage.end()) + --(*minor_it++); // decrement ids > id + } + + auto _remove_no_align(minor_storage_type& minor_storage, const types::id_type id) noexcept { + auto minor_it = std::ranges::lower_bound(minor_storage, id); + if (minor_it != minor_storage.end() and *minor_it == id) + minor_it = minor_storage.erase(minor_it); // unbind the element + return minor_it; + } + + template + [[nodiscard]] gl_attr_force_inline auto _get(const types::id_type id) const noexcept { + if constexpr (Element == layout_tag::major_element) { // get major + const auto& entry = this->_major_storage[id]; + // TODO: use std::views::concat (C++26) + // NOTE: This is safe because the range operator | creates an owning view over the array + return std::array, 2>{entry.tail, entry.head} + | std::views::join; + } + else { // get minor + return std::views::iota(0uz, this->_major_storage.size()) + | std::views::filter([this, minor_id = id](types::id_type major_id) { + return this->_contains(this->_major_storage[major_id], minor_id); + }); + } + } + + template + [[nodiscard]] gl_attr_force_inline auto _get_tail(const types::id_type id) const noexcept { + if constexpr (Element == layout_tag::major_element) { // get major + return std::views::all(this->_major_storage[id].tail); + } + else { // get minor + return std::views::iota(0uz, this->_major_storage.size()) + | std::views::filter([this, minor_id = id](types::id_type major_id) { + return this->_contains(this->_major_storage[major_id].tail, minor_id); + }); + } + } + + template + [[nodiscard]] gl_attr_force_inline auto _get_head(const types::id_type id) const noexcept { + if constexpr (Element == layout_tag::major_element) { // get major + return std::views::all(this->_major_storage[id].head); + } + else { // get minor + return std::views::iota(0uz, this->_major_storage.size()) + | std::views::filter([this, minor_id = id](types::id_type major_id) { + return this->_contains(this->_major_storage[major_id].head, minor_id); + }); + } + } + + template + [[nodiscard]] gl_attr_force_inline types::size_type _size(const types::id_type id + ) const noexcept { + if constexpr (Element == layout_tag::major_element) { // size major + const auto& entry = this->_major_storage[id]; + return entry.tail.size() + entry.head.size(); + } + else { // size minor + types::size_type size = 0uz; + for (const auto& major_el : this->_major_storage) + if (this->_contains(major_el, id)) + ++size; + return size; + } + } + + template + [[nodiscard]] gl_attr_force_inline types::size_type _tail_size(const types::id_type id + ) const noexcept { + if constexpr (Element == layout_tag::major_element) { // size major + return this->_major_storage[id].tail.size(); + } + else { // size minor + types::size_type size = 0uz; + for (const auto& major_el : this->_major_storage) + if (this->_contains(major_el.tail, id)) + ++size; + return size; + } } - [[nodiscard]] bool _are_bound_impl( - const major_element_type& major_el, const types::id_type minor_id + template + [[nodiscard]] gl_attr_force_inline types::size_type _head_size(const types::id_type id ) const noexcept { - const auto minor_it = std::ranges::lower_bound(major_el, minor_id); - return minor_it != major_el.end() and *minor_it == minor_id; + if constexpr (Element == layout_tag::major_element) { // size major + return this->_major_storage[id].head.size(); + } + else { // size minor + types::size_type size = 0uz; + for (const auto& major_el : this->_major_storage) + if (this->_contains(major_el.head, id)) + ++size; + return size; + } + } + + void _unique_insert(minor_storage_type& minor_storage, const types::id_type id) noexcept { + const auto minor_it = std::ranges::lower_bound(minor_storage, id); + if (minor_it == minor_storage.end() or *minor_it != id) + minor_storage.insert(minor_it, id); + } + + [[nodiscard]] gl_attr_force_inline bool _contains( + const major_element_type& major_el, const types::id_type id + ) const noexcept { + return this->_contains(major_el.tail, id) or this->_contains(major_el.head, id); + } + + [[nodiscard]] bool _contains(const minor_storage_type& minor_storage, const types::id_type id) + const noexcept { + const auto minor_it = std::ranges::lower_bound(minor_storage, id); + return minor_it != minor_storage.end() and *minor_it == id; } major_storage_type _major_storage; diff --git a/include/hgl/impl/incidence_matrix.hpp b/include/hgl/impl/incidence_matrix.hpp index d2405357..295f0072 100644 --- a/include/hgl/impl/incidence_matrix.hpp +++ b/include/hgl/impl/incidence_matrix.hpp @@ -165,7 +165,8 @@ class incidence_matrix final { } template - gl_attr_force_inline types::size_type _count(const types::id_type id) const noexcept { + [[nodiscard]] gl_attr_force_inline types::size_type _count(const types::id_type id + ) const noexcept { types::size_type count = 0uz; if constexpr (Element == layout_tag::major_element) { // count major for (const bool bit : this->_matrix[id]) diff --git a/include/hgl/impl/layout_tags.hpp b/include/hgl/impl/layout_tags.hpp index 56e2118e..baec05f3 100644 --- a/include/hgl/impl/layout_tags.hpp +++ b/include/hgl/impl/layout_tags.hpp @@ -20,12 +20,16 @@ struct vertex_major_t { static constexpr element_type minor_element = element_type::hyperedge; template - [[nodiscard]] static constexpr T major(const T& vertex_el, const T& hyperedge_el) noexcept { + [[nodiscard]] static constexpr T major( + const T& vertex_el, [[maybe_unused]] const T& hyperedge_el + ) noexcept { return vertex_el; } template - [[nodiscard]] static constexpr T minor(const T& vertex_el, const T& hyperedge_el) noexcept { + [[nodiscard]] static constexpr T minor( + [[maybe_unused]] const T& vertex_el, const T& hyperedge_el + ) noexcept { return hyperedge_el; } @@ -42,12 +46,16 @@ struct hyperedge_major_t { static constexpr element_type minor_element = element_type::vertex; template - [[nodiscard]] static constexpr T major(const T& vertex_el, const T& hyperedge_el) noexcept { + [[nodiscard]] static constexpr T major( + [[maybe_unused]] const T& vertex_el, const T& hyperedge_el + ) noexcept { return hyperedge_el; } template - [[nodiscard]] static constexpr T minor(const T& vertex_el, const T& hyperedge_el) noexcept { + [[nodiscard]] static constexpr T minor( + const T& vertex_el, [[maybe_unused]] const T& hyperedge_el + ) noexcept { return vertex_el; } diff --git a/scripts/format.py b/scripts/format.py index 4c7daccf..11bc9c8d 100644 --- a/scripts/format.py +++ b/scripts/format.py @@ -12,6 +12,7 @@ class DefaultParameters: file_patterns: list[str] = ["*.cpp", "*.hpp", "*.c", "*.h"] exclude_paths: list[str] = ["tests/external"] check: bool = False + clang_format_executable: str = "clang-format" def parse_args(): @@ -19,7 +20,6 @@ def parse_args(): parser.add_argument( "-m", "--modified-files", - type=bool, default=DefaultParameters.modified_files, action=argparse.BooleanOptionalAction, help="run clang-format only on the files modified since last pushed commit", @@ -30,7 +30,6 @@ def parse_args(): type=str, default=DefaultParameters.search_paths, nargs="*", - action="extend", help="list of search directory paths", ) parser.add_argument( @@ -39,7 +38,6 @@ def parse_args(): type=str, default=DefaultParameters.file_patterns, nargs="*", - action="extend", help="list of file patterns to include", ) parser.add_argument( @@ -48,17 +46,22 @@ def parse_args(): type=str, default=DefaultParameters.exclude_paths, nargs="*", - action="extend", help="list of directory paths to exclude", ) parser.add_argument( "-c", "--check", - type=bool, default=DefaultParameters.check, action=argparse.BooleanOptionalAction, help="run format check", ) + parser.add_argument( + "-exe", + "--clang-format-executable", + type=str, + default=DefaultParameters.clang_format_executable, + help="path or name of the clang-format executable (default: clang-format)", + ) return vars(parser.parse_args()) @@ -81,7 +84,7 @@ def get_modified_files(files: set[Path]) -> set[Path]: raise RuntimeError("Failed to retrieve the modified files.") -def run_clang_format(files: set[Path], check: bool) -> int: +def run_clang_format(clang_format_exec: str, files: set[Path], check: bool) -> int: n_files = len(files) if check: print(f"Files to check: {n_files}") @@ -92,7 +95,7 @@ def run_clang_format(files: set[Path], check: bool) -> int: for i, file in enumerate(files): print(f"[{i + 1}/{n_files}] {file}") - cmd = ["clang-format-18", str(file)] + cmd = [clang_format_exec, str(file)] if check: cmd.extend(["--dry-run", "--Werror"]) else: @@ -114,12 +117,13 @@ def main( file_patterns: list[str], exclude_paths: list[str], check: bool, + clang_format_executable: str, ): files_to_format = find_files(search_paths, file_patterns, exclude_paths) if modified_files: files_to_format = get_modified_files(files_to_format) - sys.exit(run_clang_format(files_to_format, check)) + sys.exit(run_clang_format(clang_format_executable, files_to_format, check)) if __name__ == "__main__": diff --git a/tests/source/hgl/test_incidence_list.cpp b/tests/source/hgl/test_incidence_list.cpp index 9c72dda3..fc10e9ac 100644 --- a/tests/source/hgl/test_incidence_list.cpp +++ b/tests/source/hgl/test_incidence_list.cpp @@ -473,6 +473,688 @@ TEST_CASE_FIXTURE( CHECK_EQ(sut.degree(vertex_id), constants::n_hyperedges); } +struct test_bf_directed_incidence_list : public test_incidence_list { + auto altbind_to_vertex( + auto& sut, const hgl::types::id_type vertex_id, const hgl::types::size_type n_hyperedges + ) { + std::vector tail_bound, head_bound; + + for (std::size_t i = 0uz; i < n_hyperedges; ++i) { + if (i % 2 == 0) { + sut.bind_tail(vertex_id, i); + tail_bound.push_back(i); + } + else { + sut.bind_head(vertex_id, i); + head_bound.push_back(i); + } + } + + return std::make_pair(std::move(tail_bound), std::move(head_bound)); + } + + auto altbind_to_hyperedge( + auto& sut, const hgl::types::id_type hyperedge_id, const hgl::types::size_type n_vertices + ) { + std::vector tail_bound, head_bound; + + for (std::size_t i = 0uz; i < n_vertices; ++i) { + if (i % 2 == 0) { + sut.bind_tail(i, hyperedge_id); + tail_bound.push_back(i); + } + else { + sut.bind_head(i, hyperedge_id); + head_bound.push_back(i); + } + } + + return std::make_pair(std::move(tail_bound), std::move(head_bound)); + } +}; + +constexpr auto is_empty_entry = [](const auto& entry) { + return entry.tail.size() == 0uz and entry.head.size() == 0uz; +}; + +struct test_bf_directed_vertex_major_incidence_list : public test_bf_directed_incidence_list { + using sut_type = hgl::impl::incidence_list; +}; + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, "should initialize an empty list by default" +) { + sut_type sut{}; + CHECK(storage(sut).empty()); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "initialization with size parameters should properly initialize the list" +) { + sut_type sut(constants::n_vertices, constants::n_hyperedges); + CHECK_EQ(storage(sut).size(), constants::n_vertices); + CHECK(std::ranges::all_of(storage(sut), is_empty_entry)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, "add_vertices should properly extend the list" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + const auto initial_size = storage(sut).size(); + + sut.add_vertices(2uz); + + CHECK_EQ(storage(sut).size(), initial_size + 2uz); + CHECK(std::ranges::all_of(storage(sut), is_empty_entry)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "remove_vertex should properly remove the major entry and implicitly shift vertex IDs" +) { + constexpr hgl::types::size_type n_vertices = 5uz, n_hyperedges = 1uz; + constexpr hgl::types::id_type hyperedge_id = constants::id1; + + sut_type sut{n_vertices, n_hyperedges}; + sut.bind_tail(constants::id2, hyperedge_id); + sut.bind_head(constants::id4, hyperedge_id); + + hgl::types::id_type rem_vid; + hgl::types::size_type expected_hyperedge_size; + std::vector expected_vertices; + + SUBCASE("not present vertex < first incident vertex") { + rem_vid = constants::id1; + expected_hyperedge_size = 2uz; + expected_vertices = {constants::id2 - 1uz, constants::id4 - 1uz}; + } + + SUBCASE("present vertex = first incident vertex") { + rem_vid = constants::id2; + expected_hyperedge_size = 1uz; + expected_vertices = {constants::id4 - 1uz}; + } + + SUBCASE("not present vertex > first incident vertex") { + rem_vid = constants::id3; + expected_hyperedge_size = 2uz; + expected_vertices = {constants::id2, constants::id4 - 1uz}; + } + + SUBCASE("present vertex = last incident vertex") { + rem_vid = constants::id4; + expected_hyperedge_size = 1uz; + expected_vertices = {constants::id2}; + } + + SUBCASE("not present vertex > last incident vertex") { + rem_vid = constants::id4 + 1uz; + expected_hyperedge_size = 2uz; + expected_vertices = {constants::id2, constants::id4}; + } + + CAPTURE(rem_vid); + CAPTURE(expected_hyperedge_size); + CAPTURE(expected_vertices); + + sut.remove_vertex(rem_vid); + CHECK_EQ(storage(sut).size(), n_vertices - 1uz); + CHECK_EQ(sut.hyperedge_size(hyperedge_id), expected_hyperedge_size); + CHECK(std::ranges::equal(sut.incident_vertices(hyperedge_id), expected_vertices)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "incident_hyperedges should return a view of the vertex's incident hyperedge ids," + "outgoing_hyperedges should return a view of the vertex's outgoing hyperedge ids (v in T(e))," + "incoming_hyperedges should return a view of the vertex's incoming hyperedge ids (v in H(e))" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto vertex_id = constants::id1; + REQUIRE(std::ranges::empty(sut.incident_hyperedges(vertex_id))); + + const auto [tail_bound_hyperedges, head_bound_hyperedges] = + altbind_to_vertex(sut, vertex_id, constants::n_hyperedges); + + CHECK(std::ranges::is_permutation( + sut.incident_hyperedges(vertex_id), constants::hyperedge_ids_view + )); + CHECK(std::ranges::equal(sut.outgoing_hyperedges(vertex_id), tail_bound_hyperedges)); + CHECK(std::ranges::equal(sut.incoming_hyperedges(vertex_id), head_bound_hyperedges)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "degree should return the number of the vertex's incident hyperedges," + "out_degree should return the number of the vertex's outgoing hyperedges (v in T(e))," + "in_degree should return the number of the vertex's incoming hyperedges (v in H(e))" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto vertex_id = constants::id1; + REQUIRE_EQ(sut.degree(vertex_id), 0uz); + + const auto [tail_bound_hyperedges, head_bound_hyperedges] = + altbind_to_vertex(sut, vertex_id, constants::n_hyperedges); + + CHECK_EQ(sut.degree(vertex_id), constants::n_hyperedges); + CHECK_EQ(sut.out_degree(vertex_id), tail_bound_hyperedges.size()); + CHECK_EQ(sut.in_degree(vertex_id), head_bound_hyperedges.size()); +} + +TEST_CASE_FIXTURE(test_bf_directed_vertex_major_incidence_list, "add_hyperedges should do nothing") { + sut_type sut{constants::n_vertices, 0uz}; + REQUIRE_EQ(storage(sut).size(), constants::n_vertices); + REQUIRE(std::ranges::all_of(storage(sut), is_empty_entry)); + + sut.add_hyperedges(constants::n_hyperedges); + CHECK(std::ranges::all_of(storage(sut), is_empty_entry)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "remove_hyperedge should properly remove the minor entries and shift the hyperedge IDs" +) { + constexpr hgl::types::size_type n_vertices = 1uz, n_hyperedges = 5uz; + constexpr hgl::types::id_type vertex_id = constants::id1; + + sut_type sut{n_vertices, n_hyperedges}; + sut.bind_tail(vertex_id, constants::id2); + sut.bind_head(vertex_id, constants::id4); + + hgl::types::id_type rem_eid; + hgl::types::size_type expected_vertex_degree; + std::vector expected_hyperedges; + + SUBCASE("not present hyperedge < first incident hyperedge") { + rem_eid = constants::id1; + expected_vertex_degree = 2uz; + expected_hyperedges = {constants::id2 - 1uz, constants::id4 - 1uz}; + } + + SUBCASE("present hyperedge = first incident hyperedge") { + rem_eid = constants::id2; + expected_vertex_degree = 1uz; + expected_hyperedges = {constants::id4 - 1uz}; + } + + SUBCASE("not present hyperedge > first incident hyperedge") { + rem_eid = constants::id3; + expected_vertex_degree = 2uz; + expected_hyperedges = {constants::id2, constants::id4 - 1uz}; + } + + SUBCASE("present hyperedge = last incident hyperedge") { + rem_eid = constants::id4; + expected_vertex_degree = 1uz; + expected_hyperedges = {constants::id2}; + } + + SUBCASE("not present hyperedge > last incident hyperedge") { + rem_eid = constants::id4 + 1uz; + expected_vertex_degree = 2uz; + expected_hyperedges = {constants::id2, constants::id4}; + } + + CAPTURE(rem_eid); + CAPTURE(expected_vertex_degree); + CAPTURE(expected_hyperedges); + + sut.remove_hyperedge(rem_eid); + CHECK_EQ(sut.degree(vertex_id), expected_vertex_degree); + CHECK(std::ranges::equal(sut.incident_hyperedges(vertex_id), expected_hyperedges)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "incident_vertices should return a view of the hyperedge's incident vertex ids, " + "tail_vertices should return a view of the hyperedge's tail vertex ids: T(e), " + "head_vertices should return a view of the hyperedge's head vertex ids: H(e)" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto hyperedge_id = constants::id1; + REQUIRE(std::ranges::empty(sut.incident_vertices(hyperedge_id))); + + const auto [tail_bound_vertices, head_bound_vertices] = + altbind_to_hyperedge(sut, hyperedge_id, constants::n_vertices); + + CHECK(std::ranges::equal(sut.incident_vertices(hyperedge_id), constants::vertex_ids_view)); + CHECK(std::ranges::equal(sut.tail_vertices(hyperedge_id), tail_bound_vertices)); + CHECK(std::ranges::equal(sut.head_vertices(hyperedge_id), head_bound_vertices)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "hyperedge_size should return the number of the hyperedge's incident vertices, " + "tail_size should return the number of the hyperedge's tail vertices: |T(e)|, " + "head_size should return the number of the hyperedge's head vertices: |H(e)|" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto hyperedge_id = constants::id1; + REQUIRE_EQ(sut.hyperedge_size(hyperedge_id), 0uz); + + const auto [tail_bound_vertices, head_bound_vertices] = + altbind_to_hyperedge(sut, hyperedge_id, constants::n_vertices); + + CHECK_EQ(sut.hyperedge_size(hyperedge_id), constants::n_vertices); + CHECK_EQ(sut.tail_size(hyperedge_id), tail_bound_vertices.size()); + CHECK_EQ(sut.head_size(hyperedge_id), head_bound_vertices.size()); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "bind_tail should insert the vertex id to the tail list of an appropriate hyperedge entry" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.incident_vertices(constants::id1))); + + sut.bind_tail(constants::id1, constants::id1); + + REQUIRE_EQ(storage(sut)[constants::id1].tail.size(), 1uz); + REQUIRE_EQ(storage(sut)[constants::id1].head.size(), 0uz); + CHECK_EQ(storage(sut)[constants::id1].tail.front(), constants::id1); + + const auto vertices = sut.tail_vertices(constants::id1) | std::ranges::to(); + CHECK_EQ(sut.tail_size(constants::id1), 1uz); + CHECK_EQ(std::ranges::size(vertices), 1uz); + CHECK(std::ranges::contains(vertices, constants::id1)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "bind_head should insert the vertex id to the head list of an appropriate hyperedge entry" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.incident_vertices(constants::id1))); + + sut.bind_head(constants::id1, constants::id1); + + REQUIRE_EQ(storage(sut)[constants::id1].head.size(), 1uz); + REQUIRE_EQ(storage(sut)[constants::id1].tail.size(), 0uz); + CHECK_EQ(storage(sut)[constants::id1].head.front(), constants::id1); + + const auto vertices = sut.head_vertices(constants::id1) | std::ranges::to(); + CHECK_EQ(sut.head_size(constants::id1), 1uz); + CHECK_EQ(std::ranges::size(vertices), 1uz); + CHECK(std::ranges::contains(vertices, constants::id1)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "unbind should erase the hyperedge id from a proper vertex entry" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + std::vector expected_storage; + + SUBCASE("tail bound") { + sut.bind_tail(constants::id1, constants::id1); + expected_storage = storage(sut)[constants::id1].tail; + } + SUBCASE("head bound") { + sut.bind_head(constants::id1, constants::id1); + expected_storage = storage(sut)[constants::id1].head; + } + CAPTURE(sut); + CAPTURE(expected_storage); + + REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz); + REQUIRE_EQ(expected_storage.size(), 1uz); + REQUIRE_EQ(expected_storage.front(), constants::id1); + + sut.unbind(constants::id1, constants::id2); + CHECK_EQ(sut.hyperedge_size(constants::id1), 1uz); + + sut.unbind(constants::id1, constants::id1); + CHECK(std::ranges::empty(sut.incident_vertices(constants::id1))); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "are_bound, is_tail, is_head should return true only when the corresponding major list entries " + "contain the given minor ids" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.bind_tail(constants::id1, constants::id1); + sut.bind_head(constants::id2, constants::id1); + + CHECK(sut.are_bound(constants::id1, constants::id1)); + CHECK(sut.is_tail(constants::id1, constants::id1)); + CHECK_FALSE(sut.is_head(constants::id1, constants::id1)); + + CHECK(sut.are_bound(constants::id2, constants::id1)); + CHECK_FALSE(sut.is_tail(constants::id2, constants::id1)); + CHECK(sut.is_head(constants::id2, constants::id1)); + + CHECK_FALSE(sut.are_bound(constants::id3, constants::id1)); + CHECK_FALSE(sut.is_tail(constants::id3, constants::id1)); + CHECK_FALSE(sut.is_head(constants::id3, constants::id1)); +} + +struct test_bf_directed_hyperedge_major_incidence_list : public test_bf_directed_incidence_list { + using sut_type = hgl::impl::incidence_list; +}; + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_incidence_list, "should initialize an empty list by default" +) { + sut_type sut{}; + CHECK(storage(sut).empty()); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_incidence_list, + "initialization with size parameters should properly initialize the matrix" +) { + sut_type sut(constants::n_vertices, constants::n_hyperedges); + CHECK_EQ(storage(sut).size(), constants::n_hyperedges); + CHECK(std::ranges::all_of(storage(sut), is_empty_entry)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_incidence_list, "add_vertices should do nothing" +) { + sut_type sut{0uz, constants::n_hyperedges}; + REQUIRE_EQ(storage(sut).size(), constants::n_hyperedges); + REQUIRE(std::ranges::all_of(storage(sut), is_empty_entry)); + + sut.add_vertices(constants::n_vertices); + CHECK(std::ranges::all_of(storage(sut), is_empty_entry)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_incidence_list, + "remove_vertex should properly remove the minor entries and shift the vertex IDs" +) { + constexpr hgl::types::size_type n_vertices = 5uz, n_hyperedges = 1uz; + constexpr hgl::types::id_type hyperedge_id = constants::id1; + + sut_type sut{n_vertices, n_hyperedges}; + sut.bind_tail(constants::id2, hyperedge_id); + sut.bind_head(constants::id4, hyperedge_id); + + hgl::types::id_type rem_vid; + hgl::types::size_type expected_hyperedge_size; + std::vector expected_vertices; + + SUBCASE("not present vertex < first incident vertex") { + rem_vid = constants::id1; + expected_hyperedge_size = 2uz; + expected_vertices = {constants::id2 - 1uz, constants::id4 - 1uz}; + } + + SUBCASE("present vertex = first incident vertex") { + rem_vid = constants::id2; + expected_hyperedge_size = 1uz; + expected_vertices = {constants::id4 - 1uz}; + } + + SUBCASE("not present vertex > first incident vertex") { + rem_vid = constants::id3; + expected_hyperedge_size = 2uz; + expected_vertices = {constants::id2, constants::id4 - 1uz}; + } + + SUBCASE("present vertex = last incident vertex") { + rem_vid = constants::id4; + expected_hyperedge_size = 1uz; + expected_vertices = {constants::id2}; + } + + SUBCASE("not present vertex > last incident vertex") { + rem_vid = constants::id4 + 1uz; + expected_hyperedge_size = 2uz; + expected_vertices = {constants::id2, constants::id4}; + } + + CAPTURE(rem_vid); + CAPTURE(expected_hyperedge_size); + CAPTURE(expected_vertices); + + sut.remove_vertex(rem_vid); + CHECK_EQ(sut.hyperedge_size(hyperedge_id), expected_hyperedge_size); + CHECK(std::ranges::equal(sut.incident_vertices(hyperedge_id), expected_vertices)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "incident_hyperedges should return a view of the vertex's incident hyperedge ids," + "outgoing_hyperedges should return a view of the vertex's outgoing hyperedge ids (v in T(e))," + "incoming_hyperedges should return a view of the vertex's incoming hyperedge ids (v in H(e))" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto vertex_id = constants::id1; + REQUIRE(std::ranges::empty(sut.incident_hyperedges(vertex_id))); + + const auto [tail_bound_hyperedges, head_bound_hyperedges] = + altbind_to_vertex(sut, vertex_id, constants::n_hyperedges); + + CHECK(std::ranges::is_permutation( + sut.incident_hyperedges(vertex_id), constants::hyperedge_ids_view + )); + CHECK(std::ranges::equal(sut.outgoing_hyperedges(vertex_id), tail_bound_hyperedges)); + CHECK(std::ranges::equal(sut.incoming_hyperedges(vertex_id), head_bound_hyperedges)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "degree should return the number of the vertex's incident hyperedges," + "out_degree should return the number of the vertex's outgoing hyperedges (v in T(e))," + "in_degree should return the number of the vertex's incoming hyperedges (v in H(e))" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto vertex_id = constants::id1; + REQUIRE_EQ(sut.degree(vertex_id), 0uz); + + const auto [tail_bound_hyperedges, head_bound_hyperedges] = + altbind_to_vertex(sut, vertex_id, constants::n_hyperedges); + + CHECK_EQ(sut.degree(vertex_id), constants::n_hyperedges); + CHECK_EQ(sut.out_degree(vertex_id), tail_bound_hyperedges.size()); + CHECK_EQ(sut.in_degree(vertex_id), head_bound_hyperedges.size()); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_incidence_list, + "add_hyperedges should properly extend the list" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + const auto initial_size = storage(sut).size(); + + sut.add_hyperedges(2uz); + + CHECK_EQ(storage(sut).size(), initial_size + 2uz); + CHECK(std::ranges::all_of(storage(sut), is_empty_entry)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_incidence_list, + "remove_hyperedge should properly remove the row and implicitly shift hyperedge IDs" +) { + constexpr hgl::types::size_type n_vertices = 1uz, n_hyperedges = 5uz; + constexpr hgl::types::id_type vertex_id = constants::id1; + + sut_type sut{n_vertices, n_hyperedges}; + sut.bind_tail(vertex_id, constants::id2); + sut.bind_head(vertex_id, constants::id4); + + hgl::types::id_type rem_eid; + hgl::types::size_type expected_vertex_degree; + std::vector expected_hyperedges; + + SUBCASE("not present hyperedge < first incident hyperedge") { + rem_eid = constants::id1; + expected_vertex_degree = 2uz; + expected_hyperedges = {constants::id2 - 1uz, constants::id4 - 1uz}; + } + + SUBCASE("present hyperedge = first incident hyperedge") { + rem_eid = constants::id2; + expected_vertex_degree = 1uz; + expected_hyperedges = {constants::id4 - 1uz}; + } + + SUBCASE("not present hyperedge > first incident hyperedge") { + rem_eid = constants::id3; + expected_vertex_degree = 2uz; + expected_hyperedges = {constants::id2, constants::id4 - 1uz}; + } + + SUBCASE("present hyperedge = last incident hyperedge") { + rem_eid = constants::id4; + expected_vertex_degree = 1uz; + expected_hyperedges = {constants::id2}; + } + + SUBCASE("not present hyperedge > last incident hyperedge") { + rem_eid = constants::id4 + 1uz; + expected_vertex_degree = 2uz; + expected_hyperedges = {constants::id2, constants::id4}; + } + + CAPTURE(rem_eid); + CAPTURE(expected_vertex_degree); + CAPTURE(expected_hyperedges); + + sut.remove_hyperedge(rem_eid); + CHECK_EQ(storage(sut).size(), n_hyperedges - 1uz); + CHECK_EQ(sut.degree(vertex_id), expected_vertex_degree); + CHECK(std::ranges::equal(sut.incident_hyperedges(vertex_id), expected_hyperedges)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "incident_vertices should return a view of the hyperedge's incident vertex ids, " + "tail_vertices should return a view of the hyperedge's tail vertex ids: T(e), " + "head_vertices should return a view of the hyperedge's head vertex ids: H(e)" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto hyperedge_id = constants::id1; + REQUIRE(std::ranges::empty(sut.incident_vertices(hyperedge_id))); + + const auto [tail_bound_vertices, head_bound_vertices] = + altbind_to_hyperedge(sut, hyperedge_id, constants::n_vertices); + + CHECK(std::ranges::equal(sut.incident_vertices(hyperedge_id), constants::vertex_ids_view)); + CHECK(std::ranges::equal(sut.tail_vertices(hyperedge_id), tail_bound_vertices)); + CHECK(std::ranges::equal(sut.head_vertices(hyperedge_id), head_bound_vertices)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "hyperedge_size should return the number of the hyperedge's incident vertices, " + "tail_size should return the number of the hyperedge's tail vertices: |T(e)|, " + "head_size should return the number of the hyperedge's head vertices: |H(e)|" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto hyperedge_id = constants::id1; + REQUIRE_EQ(sut.hyperedge_size(hyperedge_id), 0uz); + + const auto [tail_bound_vertices, head_bound_vertices] = + altbind_to_hyperedge(sut, hyperedge_id, constants::n_vertices); + + CHECK_EQ(sut.hyperedge_size(hyperedge_id), constants::n_vertices); + CHECK_EQ(sut.tail_size(hyperedge_id), tail_bound_vertices.size()); + CHECK_EQ(sut.head_size(hyperedge_id), head_bound_vertices.size()); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "bind_tail should insert the hyperedge id to the tail list of an appropriate vertex entry" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.incident_vertices(constants::id1))); + + sut.bind_tail(constants::id1, constants::id1); + + REQUIRE_EQ(storage(sut)[constants::id1].tail.size(), 1uz); + REQUIRE_EQ(storage(sut)[constants::id1].head.size(), 0uz); + CHECK_EQ(storage(sut)[constants::id1].tail.front(), constants::id1); + + const auto hyperedges = sut.tail_vertices(constants::id1) | std::ranges::to(); + CHECK_EQ(sut.tail_size(constants::id1), 1uz); + CHECK_EQ(std::ranges::size(hyperedges), 1uz); + CHECK(std::ranges::contains(hyperedges, constants::id1)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "bind_head should insert the hyperedge id to the head list of an appropriate vertex entry" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.incident_vertices(constants::id1))); + + sut.bind_head(constants::id1, constants::id1); + + REQUIRE_EQ(storage(sut)[constants::id1].head.size(), 1uz); + REQUIRE_EQ(storage(sut)[constants::id1].tail.size(), 0uz); + CHECK_EQ(storage(sut)[constants::id1].head.front(), constants::id1); + + const auto hyperedges = sut.head_vertices(constants::id1) | std::ranges::to(); + CHECK_EQ(sut.head_size(constants::id1), 1uz); + CHECK_EQ(std::ranges::size(hyperedges), 1uz); + CHECK(std::ranges::contains(hyperedges, constants::id1)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, "unbind should clear the corresponding bit" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + std::vector expected_storage; + + SUBCASE("tail bound") { + sut.bind_tail(constants::id1, constants::id1); + expected_storage = storage(sut)[constants::id1].tail; + } + SUBCASE("head bound") { + sut.bind_head(constants::id1, constants::id1); + expected_storage = storage(sut)[constants::id1].head; + } + CAPTURE(sut); + CAPTURE(expected_storage); + + REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz); + REQUIRE_EQ(expected_storage.size(), 1uz); + REQUIRE_EQ(expected_storage.front(), constants::id1); + + sut.unbind(constants::id1, constants::id2); + CHECK_EQ(sut.hyperedge_size(constants::id1), 1uz); + + sut.unbind(constants::id1, constants::id1); + CHECK(std::ranges::empty(sut.incident_vertices(constants::id1))); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_incidence_list, + "are_bound, is_tail, is_head should return true only when the corresponding major list entries " + "contain the given minor ids" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.bind_tail(constants::id1, constants::id1); + sut.bind_head(constants::id2, constants::id1); + + CHECK(sut.are_bound(constants::id1, constants::id1)); + CHECK(sut.is_tail(constants::id1, constants::id1)); + CHECK_FALSE(sut.is_head(constants::id1, constants::id1)); + + CHECK(sut.are_bound(constants::id2, constants::id1)); + CHECK_FALSE(sut.is_tail(constants::id2, constants::id1)); + CHECK(sut.is_head(constants::id2, constants::id1)); + + CHECK_FALSE(sut.are_bound(constants::id3, constants::id1)); + CHECK_FALSE(sut.is_tail(constants::id3, constants::id1)); + CHECK_FALSE(sut.is_head(constants::id3, constants::id1)); +} + TEST_SUITE_END(); // test_incidence_list } // namespace hgl_testing