diff --git a/include/gl/impl/adjacency_matrix.hpp b/include/gl/impl/adjacency_matrix.hpp index eac51bc..eb879ff 100644 --- a/include/gl/impl/adjacency_matrix.hpp +++ b/include/gl/impl/adjacency_matrix.hpp @@ -107,17 +107,17 @@ class adjacency_matrix final { } [[nodiscard]] gl_attr_force_inline bool has_edge(id_type source_id, id_type target_id) const { - return this->_matrix[to_idx(source_id)][to_idx(target_id)] != invalid_id; + return specialized_impl::get_entry(*this, source_id, target_id) != invalid_id; } [[nodiscard]] bool has_edge(const edge_type& edge) const { - return this->_matrix[to_idx(edge.source())][to_idx(edge.target())] == edge.id(); + return specialized_impl::get_entry(*this, edge.source(), edge.target()) == edge.id(); } [[nodiscard]] std::optional get_edge(id_type source_id, id_type target_id) const requires(traits::c_has_empty_properties) { - const auto edge_id = this->_matrix[to_idx(source_id)][to_idx(target_id)]; + const auto edge_id = specialized_impl::get_entry(*this, source_id, target_id); if (edge_id == invalid_id) return std::nullopt; return std::make_optional(edge_id, source_id, target_id); @@ -128,7 +128,7 @@ class adjacency_matrix final { ) const requires(traits::c_has_non_empty_properties) { - const auto edge_id = this->_matrix[to_idx(source_id)][to_idx(target_id)]; + const auto edge_id = specialized_impl::get_entry(*this, source_id, target_id); if (edge_id == invalid_id) return std::nullopt; return std::make_optional( @@ -139,7 +139,7 @@ class adjacency_matrix final { [[nodiscard]] std::vector get_edges(id_type source_id, id_type target_id) const requires(traits::c_has_empty_properties) { - const auto edge_id = this->_matrix[to_idx(source_id)][to_idx(target_id)]; + const auto edge_id = specialized_impl::get_entry(*this, source_id, target_id); if (edge_id == invalid_id) return std::vector(); return std::vector{ @@ -152,7 +152,7 @@ class adjacency_matrix final { ) const requires(traits::c_has_non_empty_properties) { - const auto edge_id = this->_matrix[to_idx(source_id)][to_idx(target_id)]; + const auto edge_id = specialized_impl::get_entry(*this, source_id, target_id); if (edge_id == invalid_id) return std::vector(); return std::vector{ @@ -197,11 +197,13 @@ class adjacency_matrix final { { return std::views::iota(initial_id_v, this->_matrix.size()) | std::views::filter([this, vertex_id](const auto source_id) { - return this->_matrix[to_idx(source_id)][to_idx(vertex_id)] != invalid_id; + return specialized_impl::get_entry(*this, source_id, vertex_id) != invalid_id; }) | std::views::transform([this, vertex_id](const auto source_id) { return edge_type{ - this->_matrix[to_idx(source_id)][to_idx(vertex_id)], source_id, vertex_id + specialized_impl::get_entry(*this, source_id, vertex_id), + source_id, + vertex_id }; }); } @@ -213,10 +215,10 @@ class adjacency_matrix final { { return std::views::iota(initial_id_v, this->_matrix.size()) | std::views::filter([this, vertex_id](const auto source_id) { - return this->_matrix[to_idx(source_id)][to_idx(vertex_id)] != invalid_id; + return specialized_impl::get_entry(*this, source_id, vertex_id) != invalid_id; }) | std::views::transform([this, vertex_id, &edge_properties_map](const auto source_id) { - const auto edge_id = this->_matrix[to_idx(source_id)][to_idx(vertex_id)]; + const auto edge_id = specialized_impl::get_entry(*this, source_id, vertex_id); return edge_type{ edge_id, source_id, vertex_id, *edge_properties_map[to_idx(edge_id)] }; diff --git a/include/gl/impl/specialized/adjacency_list.hpp b/include/gl/impl/specialized/adjacency_list.hpp index 4e680af..fcb3fa1 100644 --- a/include/gl/impl/specialized/adjacency_list.hpp +++ b/include/gl/impl/specialized/adjacency_list.hpp @@ -154,7 +154,7 @@ struct directed_adjacency_list { } // remove the list of edges incident from the vertex entirely - self._list.erase(self._list.begin() + static_cast(vertex_id)); + self._list.erase(self._list.begin() + to_diff(vertex_id)); return removed_edges; } @@ -255,7 +255,7 @@ struct undirected_adjacency_list { const auto removed_edges = self._list[vertex_idx] | std::views::transform(&item_type::edge_id) | std::ranges::to(); - self._list.erase(self._list.begin() + static_cast(vertex_id)); + self._list.erase(self._list.begin() + to_diff(vertex_id)); return removed_edges; } diff --git a/include/gl/impl/specialized/adjacency_matrix.hpp b/include/gl/impl/specialized/adjacency_matrix.hpp index 2006ede..017d24f 100644 --- a/include/gl/impl/specialized/adjacency_matrix.hpp +++ b/include/gl/impl/specialized/adjacency_matrix.hpp @@ -154,17 +154,21 @@ struct directed_adjacency_matrix { | std::views::filter([](auto edge_id) { return edge_id != invalid_id; }) | std::ranges::to(); - self._matrix.erase(self._matrix.begin() + static_cast(vertex_id)); - + const auto vertex_pos = to_diff(vertex_id); + self._matrix.erase(self._matrix.begin() + vertex_pos); for (auto& row : self._matrix) { if (const auto edge_id = row[vertex_idx]; edge_id != invalid_id) removed_edges.push_back(edge_id); - row.erase(row.begin() + static_cast(vertex_id)); + row.erase(row.begin() + vertex_pos); } return removed_edges; } + static id_type get_entry(const impl_type& self, id_type source_id, id_type target_id) { + return self._matrix[to_idx(source_id)][to_idx(target_id)]; + } + static inline void add_edge( impl_type& self, id_type edge_id, id_type source_id, id_type target_id ) { @@ -277,7 +281,7 @@ struct undirected_adjacency_matrix { | std::views::filter([](auto edge_id) { return edge_id != invalid_id; }) | std::ranges::to(); - const auto vertex_pos = static_cast(vertex_id); + const auto vertex_pos = to_diff(vertex_id); self._matrix.erase(self._matrix.begin() + vertex_pos); for (auto& row : self._matrix) row.erase(row.begin() + vertex_pos); @@ -285,6 +289,10 @@ struct undirected_adjacency_matrix { return removed_edges; } + static id_type get_entry(const impl_type& self, id_type source_id, id_type target_id) { + return self._matrix[to_idx(source_id)][to_idx(target_id)]; + } + static void add_edge(impl_type& self, id_type edge_id, id_type source_id, id_type target_id) { detail::check_edge_override(self._matrix, source_id, target_id); diff --git a/include/gl/impl/specialized/flat_adjacency_matrix.hpp b/include/gl/impl/specialized/flat_adjacency_matrix.hpp index 74394ea..a0aa5dc 100644 --- a/include/gl/impl/specialized/flat_adjacency_matrix.hpp +++ b/include/gl/impl/specialized/flat_adjacency_matrix.hpp @@ -4,6 +4,7 @@ #pragma once +#include "gl/attributes/diagnostics.hpp" #include "gl/constants.hpp" #include "gl/decl/impl_tags.hpp" #include "gl/graph_traits.hpp" @@ -19,6 +20,36 @@ namespace gl::impl::specialized { +namespace detail { + +template +[[nodiscard]] auto& strict_get(flat_matrix& id_matrix, const auto& edge) { + // get the edge and validate the address + const auto [source_id, target_id] = edge.incident_vertices(); + auto& edge_id = id_matrix[to_idx(source_id), to_idx(target_id)]; + if (edge.id() != edge_id) + throw std::invalid_argument(std::format( + "Got invalid edge [id = {} | vertices = ({}, {})]", edge.id(), source_id, target_id + )); + + return edge_id; +} + +template +inline void check_edge_override( + const flat_matrix& id_matrix, const IdType source_id, const IdType target_id +) { + if (const auto edge_id = id_matrix[to_idx(source_id), to_idx(target_id)]; edge_id != invalid_id) + throw std::logic_error(std::format( + "Cannot override an existing edge: [id = {}, vertices = ({}, {})]", + edge_id, + source_id, + target_id + )); +} + +} // namespace detail + template AdjacencyMatrix> requires(traits::c_directed_edge) struct directed_flat_adjacency_matrix { @@ -66,9 +97,10 @@ struct directed_flat_adjacency_matrix { const auto row = self._matrix[vertex_idx]; const auto col = self._matrix.col(vertex_idx); - for (auto v_idx = 0uz; v_idx < self._matrix.n_rows(); ++v_idx) - deg += static_cast(row[v_idx] != invalid_id) - + static_cast(col[static_cast(v_idx)] != invalid_id); + const auto n_rows_bound = to_diff(self._matrix.n_rows()); + for (auto v_pos = 0z; v_pos < n_rows_bound; ++v_pos) + deg += static_cast(row[v_pos] != invalid_id) + + static_cast(col[v_pos] != invalid_id); return deg; } @@ -121,7 +153,7 @@ struct directed_flat_adjacency_matrix { if (r_idx == vertex_idx) continue; - const auto edge_id = col[static_cast(r_idx)]; + const auto edge_id = col[to_diff(r_idx)]; if (edge_id != invalid_id) removed_edges.push_back(edge_id); } @@ -133,6 +165,10 @@ struct directed_flat_adjacency_matrix { return removed_edges; } + static id_type get_entry(const impl_type& self, id_type source_id, id_type target_id) { + return self._matrix[to_idx(source_id), to_idx(target_id)]; + } + static inline void add_edge( impl_type& self, id_type edge_id, id_type source_id, id_type target_id ) { @@ -151,7 +187,7 @@ struct directed_flat_adjacency_matrix { auto matrix_source_row = self._matrix[to_idx(source_id)]; for (auto [edge_id, target_id] : std::views::zip(edge_ids, target_ids)) - matrix_source_row[to_idx(target_id)] = edge_id; + matrix_source_row[to_diff(target_id)] = edge_id; } static inline void remove_edge(impl_type& self, const edge_type& edge) { @@ -246,6 +282,10 @@ struct undirected_flat_adjacency_matrix { return removed_edges; } + static id_type get_entry(const impl_type& self, id_type source_id, id_type target_id) { + return self._matrix[to_idx(source_id), to_idx(target_id)]; + } + static void add_edge(impl_type& self, id_type edge_id, id_type source_id, id_type target_id) { detail::check_edge_override(self._matrix, source_id, target_id); @@ -271,7 +311,7 @@ struct undirected_flat_adjacency_matrix { for (auto [edge_id, target_id] : std::views::zip(edge_ids, target_ids)) { const auto target_idx = to_idx(target_id); - matrix_source_row[target_idx] = edge_id; + matrix_source_row[to_diff(target_id)] = edge_id; if (target_idx != source_idx) self._matrix[target_idx, source_idx] = edge_id; } diff --git a/include/gl/types/core.hpp b/include/gl/types/core.hpp index 90250df..57ecb00 100644 --- a/include/gl/types/core.hpp +++ b/include/gl/types/core.hpp @@ -7,6 +7,7 @@ #include "gl/attributes/force_inline.hpp" #include +#include #include #include #include @@ -27,6 +28,10 @@ concept c_id_type = std::unsigned_integral; return static_cast(id); } +[[nodiscard]] gl_attr_force_inline constexpr std::ptrdiff_t to_diff(std::integral auto i) noexcept { + return static_cast(i); +} + template using homogeneous_pair = std::pair; diff --git a/include/gl/types/flat_jagged_vector.hpp b/include/gl/types/flat_jagged_vector.hpp index 9687f62..6aa1a73 100644 --- a/include/gl/types/flat_jagged_vector.hpp +++ b/include/gl/types/flat_jagged_vector.hpp @@ -4,8 +4,9 @@ #pragma once +#include "gl/types/core.hpp" + #include -#include #include #include #include @@ -30,7 +31,7 @@ namespace gl { /// @todo Implement assign, and swap methods. /// @todo Implement iterator-based insert, emplace and erase methods. /// @todo Add `operator<<` overload for `std::ostream` and specialize `std::formatter`. -/// @todo Use `std::ptrdiff_t` instead of `std::size_t` for offset values. +/// @todo Use std::ptrdiff_t instead of std::size_t for offset values template class flat_jagged_vector { public: @@ -38,20 +39,22 @@ class flat_jagged_vector { using value_type = T; /// @brief Unsigned integral type used for sizes and indices using size_type = std::size_t; + /// @brief The underlying contiguous storage container + using container_type = std::vector; /// @brief Reference to an element - using reference = value_type&; + using reference = typename container_type::reference; /// @brief Const reference to an element - using const_reference = const value_type&; - /// @brief Span type representing a non-owning segment of elements - using segment_type = std::span; - /// @brief Const span type representing a non-owning const segment of elements - using const_segment_type = std::span; + using const_reference = typename container_type::const_reference; + /// @brief Subrange type representing a non-owning segment of elements + using segment_type = std::ranges::subrange; + /// @brief Const subrange type representing a non-owning const segment of elements + using const_segment_type = std::ranges::subrange; // --- iterators --- /// @brief Random access iterator over segments of the `flat_jagged_vector`. /// - /// This iterator dereferences to a `segment_type` (span of elements in a single segment), + /// This iterator dereferences to a `segment_type` (subrange of elements in a single segment), /// allowing efficient iteration and random access to individual segments. The iterator maintains /// pointers to the element data and the offsets array for dereferencing. /// @@ -60,7 +63,10 @@ class flat_jagged_vector { /// @warning Invalidated when the `flat_jagged_vector` is modified (structure changes or element insertions/deletions). template class segment_iterator { - using data_ptr_type = std::conditional_t; + using data_iter_type = std::conditional_t< + Const, + typename container_type::const_iterator, + typename container_type::iterator>; using offset_ptr_type = const size_type*; public: @@ -68,38 +74,39 @@ class flat_jagged_vector { using iterator_concept = std::random_access_iterator_tag; /// @brief Legacy iterator category (random access) using iterator_category = std::random_access_iterator_tag; - /// @brief Type of segment this iterator dereferences to (span or const span) + /// @brief Type of segment this iterator dereferences to (subrange or const subrange) using value_type = std::conditional_t; /// @brief Signed integral difference type using difference_type = std::ptrdiff_t; - /// @brief Pointer type (void because segment iterators dereference to spans) + /// @brief Pointer type (void because segment iterators dereference to subranges) using pointer = void; - /// @brief Reference type (span of elements) + /// @brief Reference type (subrange of elements) using reference = value_type; /// @brief Default constructor creates a null iterator segment_iterator() = default; /// @brief Constructs an iterator pointing to a specific segment. - /// @param data_ptr Pointer to the underlying element data (may be null for null iterator) + /// @param data_iter Iterator to the underlying element data (may be null for null iterator) /// @param offset_ptr Pointer to the offsets array at the position of this segment - segment_iterator(data_ptr_type data_ptr, offset_ptr_type offset_ptr) noexcept - : _data_ptr(data_ptr), _offset_ptr(offset_ptr) {} + segment_iterator(data_iter_type data_iter, offset_ptr_type offset_ptr) noexcept + : _data_iter(data_iter), _offset_ptr(offset_ptr) {} /// @brief Implicit conversion from mutable to const iterator /// @return A const iterator pointing to the same segment operator segment_iterator() const noexcept requires(not Const) { - return segment_iterator(this->_data_ptr, this->_offset_ptr); + return segment_iterator(this->_data_iter, this->_offset_ptr); } /// @brief Dereferences the iterator to the current segment. - /// @return A span representing the segment at the current position + /// @return A subrange representing the segment at the current position [[nodiscard]] reference operator*() const noexcept { - const auto beg = *this->_offset_ptr; - const auto end = *(this->_offset_ptr + 1uz); - return reference(this->_data_ptr + beg, end - beg); + return reference( + this->_data_iter + to_diff(*this->_offset_ptr), + this->_data_iter + to_diff(*(this->_offset_ptr + 1uz)) + ); } /// @brief Random access to a segment at offset from current position. @@ -217,7 +224,7 @@ class flat_jagged_vector { } private: - data_ptr_type _data_ptr{nullptr}; + data_iter_type _data_iter; offset_ptr_type _offset_ptr{nullptr}; }; @@ -466,26 +473,28 @@ class flat_jagged_vector { /// @brief Returns the segment at the given index without bounds checking. /// @param i The index of the segment to access - /// @return A span representing the segment at index i + /// @return A subrange representing the segment at index i /// @pre `i < size()`; otherwise Undefined Behavior /// @warning No bounds checking is performed for performance. Use `at()` for bounds-checked access. /// Calling on an out-of-bounds index results in Undefined Behavior. [[nodiscard]] segment_type operator[](size_type i) { - const auto beg = this->_offsets[i]; - const auto end = this->_offsets[i + 1uz]; - return segment_type(this->_data.data() + beg, end - beg); + return segment_type( + this->_data.begin() + to_diff(this->_offsets[i]), + this->_data.begin() + to_diff(this->_offsets[i + 1uz]) + ); } /// @brief Returns a const segment at the given index without bounds checking. /// @param i The index of the segment to access - /// @return A const span representing the segment at index i + /// @return A const subrange representing the segment at index i /// @pre `i < size()`; otherwise Undefined Behavior /// @warning No bounds checking is performed for performance. Use `at()` for bounds-checked access. /// Calling on an out-of-bounds index results in Undefined Behavior. [[nodiscard]] const_segment_type operator[](size_type i) const { - const auto beg = this->_offsets[i]; - const auto end = this->_offsets[i + 1uz]; - return const_segment_type(this->_data.data() + beg, end - beg); + return const_segment_type( + this->_data.begin() + to_diff(this->_offsets[i]), + this->_data.begin() + to_diff(this->_offsets[i + 1uz]) + ); } /// @brief Returns a reference to an element within a segment without bounds checking. @@ -512,7 +521,7 @@ class flat_jagged_vector { /// @brief Returns the segment at the given index with bounds checking. /// @param i The index of the segment - /// @return A span representing the segment at index i + /// @return A subrange representing the segment at index i /// @exception std::out_of_range If `i >= size()` /// @note Provides the same safety as `std::vector::at()` [[nodiscard]] segment_type at(size_type i) { @@ -522,7 +531,7 @@ class flat_jagged_vector { /// @brief Returns a const segment at the given index with bounds checking. /// @param i The index of the segment - /// @return A const span representing the segment at index i + /// @return A const subrange representing the segment at index i /// @exception std::out_of_range If `i >= size()` /// @note Provides the same safety as `std::vector::at()` [[nodiscard]] const_segment_type at(size_type i) const { @@ -555,7 +564,7 @@ class flat_jagged_vector { } /// @brief Returns the first segment without bounds checking. - /// @return A span representing the first segment + /// @return A subrange representing the first segment /// @pre Container must not be empty; otherwise Undefined Behavior /// @warning No bounds checking. Results in Undefined Behavior if container is empty. [[nodiscard]] segment_type front() noexcept { @@ -563,7 +572,7 @@ class flat_jagged_vector { } /// @brief Returns a const reference to the first segment without bounds checking. - /// @return A const span representing the first segment + /// @return A const subrange representing the first segment /// @pre Container must not be empty; otherwise Undefined Behavior /// @warning No bounds checking. Results in Undefined Behavior if container is empty. [[nodiscard]] const_segment_type front() const noexcept { @@ -571,7 +580,7 @@ class flat_jagged_vector { } /// @brief Returns the last segment without bounds checking. - /// @return A span representing the last segment + /// @return A subrange representing the last segment /// @pre Container must not be empty; otherwise Undefined Behavior /// @warning No bounds checking. Results in Undefined Behavior if container is empty. [[nodiscard]] segment_type back() noexcept { @@ -579,7 +588,7 @@ class flat_jagged_vector { } /// @brief Returns a const reference to the last segment without bounds checking. - /// @return A const span representing the last segment + /// @return A const subrange representing the last segment /// @pre Container must not be empty; otherwise Undefined Behavior /// @warning No bounds checking. Results in Undefined Behavior if container is empty. [[nodiscard]] const_segment_type back() const noexcept { @@ -664,15 +673,15 @@ class flat_jagged_vector { return this->_data.size(); } - /// @brief Returns a span over all element data in flattened form. - /// @return A span of all elements in the underlying `_data` array. + /// @brief Returns a subrange of all element data in flattened form. + /// @return A subrange of all elements in the underlying `_data` array. /// @note Allows direct access to the flattened representation of all segments. [[nodiscard]] segment_type data_view() noexcept { return segment_type(this->_data); } - /// @brief Returns a const span over all element data in flattened form. - /// @return A const span of all elements in the underlying `_data` array. + /// @brief Returns a const subrange of all element data in flattened form. + /// @return A const subrange of all elements in the underlying `_data` array. /// @note Allows direct access to the flattened representation of all segments. [[nodiscard]] const_segment_type data_view() const noexcept { return const_segment_type(this->_data); @@ -693,14 +702,20 @@ class flat_jagged_vector { /// @brief Returns a raw pointer to the underlying flat data array. /// @return A raw pointer to the first element in the `_data` array. - /// @warning No bounds checking is performed. Structural modifications (like resizing) cannot be done via this pointer; use `data_storage()` instead. - [[nodiscard]] value_type* data_ptr() noexcept { + /// @warning Structural modifications (like resizing) cannot be done via this pointer; use `data_storage()` instead. + /// @warning Not available for boolean vectors. + [[nodiscard]] value_type* data_ptr() noexcept + requires(not std::same_as) + { return this->_data.data(); } /// @brief Returns a const raw pointer to the underlying flat data array. /// @return A const raw pointer to the first element in the `_data` array. - [[nodiscard]] const value_type* data_ptr() const noexcept { + /// @warning Not available for boolean vectors. + [[nodiscard]] const value_type* data_ptr() const noexcept + requires(not std::same_as) + { return this->_data.data(); } @@ -749,28 +764,28 @@ class flat_jagged_vector { /// @return Iterator to the first segment /// @note Iterator invalidated by structural modifications [[nodiscard]] iterator begin() noexcept { - return iterator(this->_data.data(), this->_offsets.data()); + return iterator(this->_data.begin(), this->_offsets.data()); } /// @brief Returns a mutable iterator past the last segment (end sentinel). /// @return Iterator one position past the last segment /// @note Iterator invalidated by structural modifications [[nodiscard]] iterator end() noexcept { - return iterator(this->_data.data(), this->_offsets.data() + this->size()); + return iterator(this->_data.begin(), this->_offsets.data() + this->size()); } /// @brief Returns a const iterator to the first segment. /// @return Const iterator to the first segment /// @note Iterator invalidated by structural modifications [[nodiscard]] const_iterator begin() const noexcept { - return const_iterator(this->_data.data(), this->_offsets.data()); + return const_iterator(this->_data.begin(), this->_offsets.data()); } /// @brief Returns a const iterator past the last segment (end sentinel). /// @return Const iterator one position past the last segment /// @note Iterator invalidated by structural modifications [[nodiscard]] const_iterator end() const noexcept { - return const_iterator(this->_data.data(), this->_offsets.data() + this->size()); + return const_iterator(this->_data.begin(), this->_offsets.data() + this->size()); } /// @brief Returns a const iterator to the first segment (explicit const form). @@ -905,7 +920,7 @@ class flat_jagged_vector { requires std::convertible_to, value_type> void insert(size_type pos, R&& r) { const auto beg = this->_offsets[pos]; - const auto beg_pos = static_cast(beg); + const auto beg_pos = to_diff(beg); const auto old_size = this->_data.size(); this->_ensure_offset_capacity(); @@ -922,7 +937,7 @@ class flat_jagged_vector { } const auto inserted = this->_data.size() - old_size; - this->_offsets.insert(this->_offsets.begin() + static_cast(pos), beg); + this->_offsets.insert(this->_offsets.begin() + to_diff(pos), beg); for (size_type i = pos + 1uz; i < this->_offsets.size(); i++) this->_offsets[i] += inserted; } @@ -952,12 +967,12 @@ class flat_jagged_vector { /// the erased segment in the underlying vector, $S$ is the number of segments after `pos`, /// and $L$ is the size of the erased segment. Erasing the **last** segment is $O(L)$. void erase(size_type pos) { - const auto start = static_cast(this->_offsets[pos]); - const auto end = static_cast(this->_offsets[pos + 1uz]); + const auto start = to_diff(this->_offsets[pos]); + const auto end = to_diff(this->_offsets[pos + 1uz]); const auto len = static_cast(end - start); this->_data.erase(this->_data.begin() + start, this->_data.begin() + end); - this->_offsets.erase(this->_offsets.begin() + static_cast(pos)); + this->_offsets.erase(this->_offsets.begin() + to_diff(pos)); for (size_type i = pos; i < this->_offsets.size(); i++) this->_offsets[i] -= len; } @@ -1022,7 +1037,7 @@ class flat_jagged_vector { /// @note **Time Complexity:** Amortized $O(E + S)$ where $E$ is the number of elements after /// the insertion point in the underlying vector, and $S$ is the number of segments after `seg`. void insert(size_type seg, size_type pos, const value_type& value) { - const auto insert_pos = static_cast(this->_offsets[seg] + pos); + const auto insert_pos = to_diff(this->_offsets[seg] + pos); this->_data.insert(this->_data.begin() + insert_pos, value); for (size_type i = seg + 1uz; i < this->_offsets.size(); i++) this->_offsets[i]++; @@ -1041,7 +1056,7 @@ class flat_jagged_vector { /// the insertion point in the underlying vector, and $S$ is the number of segments after `seg`. template void emplace(size_type seg, size_type pos, Args&&... args) { - const auto insert_pos = static_cast(this->_offsets[seg] + pos); + const auto insert_pos = to_diff(this->_offsets[seg] + pos); this->_data.emplace(this->_data.begin() + insert_pos, std::forward(args)...); for (size_type i = seg + 1uz; i < this->_offsets.size(); i++) this->_offsets[i]++; @@ -1055,7 +1070,7 @@ class flat_jagged_vector { /// @note **Time Complexity:** $O(E + S)$ where $E$ is the number of elements after the erased /// position in the underlying vector, and $S$ is the number of segments after `seg`. void erase(size_type seg, size_type pos) { - const auto erase_pos = static_cast(this->_offsets[seg] + pos); + const auto erase_pos = to_diff(this->_offsets[seg] + pos); this->_data.erase(this->_data.begin() + erase_pos); for (size_type i = seg + 1uz; i < this->_offsets.size(); i++) this->_offsets[i]--; @@ -1080,8 +1095,8 @@ class flat_jagged_vector { const auto curr_count = this->segment_size(seg); if (n < curr_count) { const auto diff = curr_count - n; - const auto start = static_cast(this->_offsets[seg] + n); - const auto end = static_cast(this->_offsets[seg + 1uz]); + const auto start = to_diff(this->_offsets[seg] + n); + const auto end = to_diff(this->_offsets[seg + 1uz]); this->_data.erase(this->_data.begin() + start, this->_data.begin() + end); for (size_type i = seg + 1uz; i < this->_offsets.size(); i++) @@ -1089,7 +1104,7 @@ class flat_jagged_vector { } else if (n > curr_count) { const auto diff = n - curr_count; - const auto pos = static_cast(this->_offsets[seg + 1uz]); + const auto pos = to_diff(this->_offsets[seg + 1uz]); this->_data.insert(this->_data.begin() + pos, diff, value_type()); for (size_type i = seg + 1uz; i < this->_offsets.size(); i++) @@ -1117,8 +1132,8 @@ class flat_jagged_vector { const auto curr_count = this->segment_size(seg); if (n < curr_count) { const auto diff = curr_count - n; - const auto start = static_cast(this->_offsets[seg] + n); - const auto end = static_cast(this->_offsets[seg + 1uz]); + const auto start = to_diff(this->_offsets[seg] + n); + const auto end = to_diff(this->_offsets[seg + 1uz]); this->_data.erase(this->_data.begin() + start, this->_data.begin() + end); for (size_type i = seg + 1uz; i < this->_offsets.size(); i++) @@ -1126,7 +1141,7 @@ class flat_jagged_vector { } else if (n > curr_count) { const auto diff = n - curr_count; - const auto pos = static_cast(this->_offsets[seg + 1uz]); + const auto pos = to_diff(this->_offsets[seg + 1uz]); this->_data.insert(this->_data.begin() + pos, diff, value); for (size_type i = seg + 1uz; i < this->_offsets.size(); i++) diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp index 0265098..26d2dc2 100644 --- a/include/gl/types/flat_matrix.hpp +++ b/include/gl/types/flat_matrix.hpp @@ -4,8 +4,9 @@ #pragma once +#include "gl/types/core.hpp" + #include -#include #include #include #include @@ -27,6 +28,7 @@ namespace gl { /// /// @warning Iterator invalidation follows `std::vector` semantics: modifying the dimensions or structural /// capacity of the matrix invalidates all iterators, pointers, and references to its elements. +/// @todo Implement the row_unchecked and col_unchecked methods. template class flat_matrix { public: @@ -34,20 +36,22 @@ class flat_matrix { using value_type = T; /// @brief Unsigned integral type used for sizes and indices using size_type = std::size_t; + /// @brief The underlying contiguous storage container + using container_type = std::vector; /// @brief Reference to an element - using reference = value_type&; + using reference = typename container_type::reference; /// @brief Const reference to an element - using const_reference = const value_type&; - /// @brief Span type representing a non-owning uniform row of elements - using row_type = std::span; - /// @brief Const span type representing a non-owning uniform const row of elements - using const_row_type = std::span; + using const_reference = typename container_type::const_reference; + /// @brief Subrange type representing a non-owning uniform row of elements + using row_type = std::ranges::subrange; + /// @brief Const subrange type representing a non-owning uniform const row of elements + using const_row_type = std::ranges::subrange; // --- iterators --- /// @brief Random access iterator over the rows of the `flat_matrix`. /// - /// This iterator dereferences to a `row_type` (span of elements representing a single matrix row), + /// This iterator dereferences to a `row_type` (subrange of elements representing a single matrix row), /// allowing efficient iteration and random access. It calculates the memory offsets mathematically /// based on the column dimension. /// @@ -56,44 +60,48 @@ class flat_matrix { /// @warning Invalidated when the `flat_matrix` structural dimensions are modified or memory is reallocated. template class row_iterator { - using data_ptr_type = std::conditional_t; + using data_iter_type = std::conditional_t< + Const, + typename container_type::const_iterator, + typename container_type::iterator>; public: /// @brief Satisfies random access iterator concept using iterator_concept = std::random_access_iterator_tag; /// @brief Legacy iterator category (random access) using iterator_category = std::random_access_iterator_tag; - /// @brief Type of row this iterator dereferences to (span or const span) + /// @brief Type of row this iterator dereferences to (subrange or const subrange) using value_type = std::conditional_t; /// @brief Signed integral difference type using difference_type = std::ptrdiff_t; /// @brief Pointer type (void because iterators dereference to spans) using pointer = void; - /// @brief Reference type (span of elements) + /// @brief Reference type (subrange of elements) using reference = value_type; /// @brief Default constructor creates a null iterator row_iterator() = default; /// @brief Constructs an iterator pointing to a specific row. - /// @param data_ptr Pointer to the underlying flat element data + /// @param data_iter Iterator to the underlying flat element data /// @param n_cols The number of columns in the matrix /// @param row_idx The index of the row this iterator currently points to - row_iterator(data_ptr_type data_ptr, size_type n_cols, size_type row_idx) noexcept - : _data_ptr(data_ptr), _row_size(n_cols), _row_idx(row_idx) {} + row_iterator(data_iter_type data_iter, size_type n_cols, size_type row_idx) noexcept + : _data_iter(data_iter), _row_size(n_cols), _row_idx(row_idx) {} /// @brief Implicit conversion from mutable to const iterator /// @return A const iterator pointing to the same row operator row_iterator() const noexcept requires(not Const) { - return row_iterator(this->_data_ptr, this->_row_size, this->_row_idx); + return row_iterator(this->_data_iter, this->_row_size, this->_row_idx); } /// @brief Dereferences the iterator to the current row. - /// @return A span representing the row at the current position + /// @return A subrange representing the row at the current position [[nodiscard]] reference operator*() const noexcept { - return reference(this->_data_ptr + this->_row_idx * this->_row_size, this->_row_size); + const auto row_beg = this->_data_iter + to_diff(this->_row_idx * this->_row_size); + return reference(row_beg, row_beg + to_diff(this->_row_size)); } /// @brief Random access to a row at an offset from the current position. @@ -181,7 +189,7 @@ class flat_matrix { [[nodiscard]] friend difference_type operator-( const row_iterator& lhs, const row_iterator& rhs ) noexcept { - return static_cast(lhs._row_idx - rhs._row_idx); + return to_diff(lhs._row_idx - rhs._row_idx); } /// @brief Tests equality of two iterators. @@ -205,7 +213,7 @@ class flat_matrix { } private: - data_ptr_type _data_ptr{nullptr}; + data_iter_type _data_iter; size_type _row_size{0uz}; size_type _row_idx{0uz}; }; @@ -438,7 +446,7 @@ class flat_matrix { return; } - std::vector new_data(new_rows * new_cols, value); + container_type new_data(new_rows * new_cols, value); const auto min_rows = std::min(this->_n_rows, new_rows); const auto min_cols = std::min(this->_n_cols, new_cols); @@ -473,20 +481,22 @@ class flat_matrix { /// @brief Returns the row at the given index without bounds checking. /// @param r The index of the row to access - /// @return A span representing the row at index r + /// @return A subrange representing the row at index r /// @pre `r < n_rows()`; otherwise Undefined Behavior /// @warning No bounds checking is performed for performance. [[nodiscard]] row_type operator[](size_type r) { - return row_type(this->_data.data() + r * this->_n_cols, this->_n_cols); + const auto row_beg = this->_data.begin() + to_diff(r * this->_n_cols); + return row_type(row_beg, row_beg + to_diff(this->_n_cols)); } /// @brief Returns a const row at the given index without bounds checking. /// @param r The index of the row to access - /// @return A const span representing the row at index r + /// @return A const subrange representing the row at index r /// @pre `r < n_rows()`; otherwise Undefined Behavior /// @warning No bounds checking is performed. [[nodiscard]] const_row_type operator[](size_type r) const { - return const_row_type(this->_data.data() + r * this->_n_cols, this->_n_cols); + const auto row_beg = this->_data.begin() + to_diff(r * this->_n_cols); + return const_row_type(row_beg, row_beg + to_diff(this->_n_cols)); } /// @brief Returns a reference to an element without bounds checking. @@ -511,7 +521,7 @@ class flat_matrix { /// @brief Returns the row at the given index with bounds checking. /// @param r The index of the row - /// @return A span representing the row at index r + /// @return A subrange representing the row at index r /// @exception std::out_of_range If `r >= n_rows()` [[nodiscard]] row_type at(size_type r) { this->_check_row(r); @@ -520,7 +530,7 @@ class flat_matrix { /// @brief Returns a const row at the given index with bounds checking. /// @param r The index of the row - /// @return A const span representing the row at index r + /// @return A const subrange representing the row at index r /// @exception std::out_of_range If `r >= n_rows()` [[nodiscard]] const_row_type at(size_type r) const { this->_check_row(r); @@ -550,7 +560,7 @@ class flat_matrix { } /// @brief Returns the first row without bounds checking. - /// @return A span representing the first row + /// @return A subrange representing the first row /// @pre Matrix must not be empty; otherwise Undefined Behavior /// @warning No bounds checking is performed. [[nodiscard]] row_type front() noexcept { @@ -558,7 +568,7 @@ class flat_matrix { } /// @brief Returns a const reference to the first row without bounds checking. - /// @return A const span representing the first row + /// @return A const subrange representing the first row /// @pre Matrix must not be empty; otherwise Undefined Behavior /// @warning No bounds checking is performed. [[nodiscard]] const_row_type front() const noexcept { @@ -566,7 +576,7 @@ class flat_matrix { } /// @brief Returns the last row without bounds checking. - /// @return A span representing the last row + /// @return A subrange representing the last row /// @pre Matrix must not be empty; otherwise Undefined Behavior /// @warning No bounds checking is performed. [[nodiscard]] row_type back() noexcept { @@ -574,7 +584,7 @@ class flat_matrix { } /// @brief Returns a const reference to the last row without bounds checking. - /// @return A const span representing the last row + /// @return A const subrange representing the last row /// @pre Matrix must not be empty; otherwise Undefined Behavior /// @warning No bounds checking is performed. [[nodiscard]] const_row_type back() const noexcept { @@ -582,28 +592,28 @@ class flat_matrix { } /// @brief Explicitly named alias for `front()` yielding the first row. - /// @return A span representing the first row + /// @return A subrange representing the first row /// @pre Matrix must not be empty; otherwise Undefined Behavior [[nodiscard]] row_type front_row() noexcept { return this->front(); } /// @brief Explicitly named alias for `front()` yielding the first const row. - /// @return A const span representing the first row + /// @return A const subrange representing the first row /// @pre Matrix must not be empty; otherwise Undefined Behavior [[nodiscard]] const_row_type front_row() const noexcept { return this->front(); } /// @brief Explicitly named alias for `back()` yielding the last row. - /// @return A span representing the last row + /// @return A subrange representing the last row /// @pre Matrix must not be empty; otherwise Undefined Behavior [[nodiscard]] row_type back_row() noexcept { return this->back(); } /// @brief Explicitly named alias for `back()` yielding the last const row. - /// @return A const span representing the last row + /// @return A const subrange representing the last row /// @pre Matrix must not be empty; otherwise Undefined Behavior [[nodiscard]] const_row_type back_row() const noexcept { return this->back(); @@ -643,7 +653,7 @@ class flat_matrix { /// @brief Semantically symmetric alias for `at(r)` returning a bounds-checked row. /// @param r The row index - /// @return A span representing the row + /// @return A subrange representing the row /// @exception std::out_of_range If `r >= n_rows()` [[nodiscard]] row_type row(size_type r) { return this->at(r); @@ -651,7 +661,7 @@ class flat_matrix { /// @brief Semantically symmetric alias for `at(r)` returning a bounds-checked const row. /// @param r The row index - /// @return A const span representing the row + /// @return A const subrange representing the row /// @exception std::out_of_range If `r >= n_rows()` [[nodiscard]] const_row_type row(size_type r) const { return this->at(r); @@ -711,14 +721,14 @@ class flat_matrix { return this->_data.size(); } - /// @brief Returns a span over all element data in flattened 1D form. - /// @return A span of all elements in the underlying `_data` array. + /// @brief Returns a subrange of all element data in flattened 1D form. + /// @return A subrange of all elements in the underlying `_data` array. [[nodiscard]] row_type data_view() noexcept { return row_type(this->_data); } - /// @brief Returns a const span over all element data in flattened 1D form. - /// @return A const span of all elements in the underlying `_data` array. + /// @brief Returns a const subrange of all element data in flattened 1D form. + /// @return A const subrange of all elements in the underlying `_data` array. [[nodiscard]] const_row_type data_view() const noexcept { return const_row_type(this->_data); } @@ -726,26 +736,31 @@ class flat_matrix { /// @brief Returns a reference to the underlying flat data container. /// @return A mutable reference to the underlying `_data` array. /// @warning Modifying this vector directly can fatally corrupt the matrix structure. Use for advanced operations only. - [[nodiscard]] std::vector& data_storage() noexcept { + [[nodiscard]] container_type& data_storage() noexcept { return this->_data; } /// @brief Returns a const reference to the underlying flat data container. /// @return A const reference to the underlying `_data` array. - [[nodiscard]] const std::vector& data_storage() const noexcept { + [[nodiscard]] const container_type& data_storage() const noexcept { return this->_data; } /// @brief Returns a raw pointer to the underlying flat data array. /// @return A raw pointer to the first element in the `_data` array. - /// @warning No bounds checking is performed. - [[nodiscard]] value_type* data_ptr() noexcept { + /// @warning Not available for boolean matrices. + [[nodiscard]] value_type* data_ptr() noexcept + requires(not std::same_as) + { return this->_data.data(); } /// @brief Returns a const raw pointer to the underlying flat data array. /// @return A const raw pointer to the first element in the `_data` array. - [[nodiscard]] const value_type* data_ptr() const noexcept { + /// @warning Not available for boolean matrices. + [[nodiscard]] const value_type* data_ptr() const noexcept + requires(not std::same_as) + { return this->_data.data(); } @@ -785,7 +800,6 @@ class flat_matrix { /// @post `n_rows()` increases by 1 /// @exception std::bad_alloc If memory allocation fails /// @warning Invalidates all iterators, pointers, and references if reallocation occurs. - /// @note If the matrix is empty, nothing will happen. /// @note **Time Complexity:** Amortized $O(C)$ where $C$ is the number of columns. void push_row(const value_type& value) { this->insert_row(this->_n_rows, value); @@ -814,7 +828,7 @@ class flat_matrix { )); } - const auto insert_pos = static_cast(pos * this->_n_cols); + const auto insert_pos = to_diff(pos * this->_n_cols); if constexpr (std::ranges::sized_range) { const auto row_size = static_cast(std::ranges::size(r)); @@ -902,7 +916,7 @@ class flat_matrix { )); } - const auto insert_pos = static_cast(pos * this->_n_cols); + const auto insert_pos = to_diff(pos * this->_n_cols); this->_data.insert(this->_data.begin() + insert_pos, this->_n_cols, value); ++this->_n_rows; } @@ -937,9 +951,8 @@ class flat_matrix { return; } - const auto start_it = - this->_data.begin() + static_cast(pos * this->_n_cols); - this->_data.erase(start_it, start_it + static_cast(this->_n_cols)); + const auto start_it = this->_data.begin() + to_diff(pos * this->_n_cols); + this->_data.erase(start_it, start_it + to_diff(this->_n_cols)); --this->_n_rows; } @@ -1023,13 +1036,13 @@ class flat_matrix { this->_n_rows = col_size; // pre-allocate new vector to guarantee O(RxC) structural shift, avoiding cubic complexity with multiple inserts - std::vector new_data; + container_type new_data; new_data.reserve(this->_n_rows * (this->_n_cols + 1uz)); auto r_it = std::ranges::begin(r); - const auto n_rows_bound = static_cast(this->_n_rows); - const auto row_size = static_cast(this->_n_cols); - const auto c_pos = static_cast(pos); + const auto n_rows_bound = to_diff(this->_n_rows); + const auto row_size = to_diff(this->_n_cols); + const auto c_pos = to_diff(pos); for (auto r_pos = 0z; r_pos < n_rows_bound; ++r_pos) { auto row_begin = this->_data.begin() + r_pos * row_size; @@ -1055,7 +1068,7 @@ class flat_matrix { else { // create a temporary sized range and recursively call the method to leverage the sized range logic, // ensuring strong exception guarantee for unsized input - this->insert_col(pos, std::ranges::to>(std::forward(r))); + this->insert_col(pos, std::ranges::to(std::forward(r))); } } @@ -1095,12 +1108,12 @@ class flat_matrix { )); } - std::vector new_data; + container_type new_data; new_data.reserve(this->_n_rows * (this->_n_cols + 1uz)); - const auto n_rows_bound = static_cast(this->_n_rows); - const auto row_size = static_cast(this->_n_cols); - const auto c_pos = static_cast(pos); + const auto n_rows_bound = to_diff(this->_n_rows); + const auto row_size = to_diff(this->_n_cols); + const auto c_pos = to_diff(pos); for (auto r_pos = 0z; r_pos < n_rows_bound; ++r_pos) { auto row_begin = this->_data.begin() + r_pos * row_size; @@ -1147,12 +1160,12 @@ class flat_matrix { return; } - std::vector new_data; + container_type new_data; new_data.reserve(this->_n_rows * (this->_n_cols - 1uz)); - const auto n_rows_bound = static_cast(this->_n_rows); - const auto row_size = static_cast(this->_n_cols); - const auto c_pos = static_cast(pos); + const auto n_rows_bound = to_diff(this->_n_rows); + const auto row_size = to_diff(this->_n_cols); + const auto c_pos = to_diff(pos); for (auto r_pos = 0z; r_pos < n_rows_bound; ++r_pos) { auto row_begin = this->_data.begin() + r_pos * row_size; @@ -1178,28 +1191,28 @@ class flat_matrix { /// @return Iterator to the first row /// @note Iterator invalidated by structural modifications [[nodiscard]] iterator begin() noexcept { - return iterator(this->_data.data(), this->_n_cols, 0uz); + return iterator(this->_data.begin(), this->_n_cols, 0uz); } /// @brief Returns a mutable iterator past the last row (end sentinel). /// @return Iterator one position past the last row /// @note Iterator invalidated by structural modifications [[nodiscard]] iterator end() noexcept { - return iterator(this->_data.data(), this->_n_cols, this->_n_rows); + return iterator(this->_data.begin(), this->_n_cols, this->_n_rows); } /// @brief Returns a const iterator to the first row. /// @return Const iterator to the first row /// @note Iterator invalidated by structural modifications [[nodiscard]] const_iterator begin() const noexcept { - return const_iterator(this->_data.data(), this->_n_cols, 0uz); + return const_iterator(this->_data.begin(), this->_n_cols, 0uz); } /// @brief Returns a const iterator past the last row (end sentinel). /// @return Const iterator one position past the last row /// @note Iterator invalidated by structural modifications [[nodiscard]] const_iterator end() const noexcept { - return const_iterator(this->_data.data(), this->_n_cols, this->_n_rows); + return const_iterator(this->_data.begin(), this->_n_cols, this->_n_rows); } /// @brief Returns a const iterator to the first row (explicit const form). @@ -1304,21 +1317,19 @@ class flat_matrix { /// @param c The column index (assumed valid) /// @return A zero-overhead `std::views::stride` representing the column elements [[nodiscard]] auto _col_impl(size_type c) noexcept { - return std::views::drop(this->_data, static_cast(c)) - | std::views::stride(this->_n_cols); + return std::views::drop(this->_data, to_diff(c)) | std::views::stride(this->_n_cols); } /// @brief Internal non-throwing helper generating a const strided view over a column. /// @param c The column index (assumed valid) /// @return A zero-overhead `std::views::stride` representing the const column elements [[nodiscard]] auto _col_impl(size_type c) const noexcept { - return std::views::drop(this->_data, static_cast(c)) - | std::views::stride(this->_n_cols); + return std::views::drop(this->_data, to_diff(c)) | std::views::stride(this->_n_cols); } size_type _n_rows{0uz}; size_type _n_cols{0uz}; - std::vector _data; + container_type _data; }; } // namespace gl diff --git a/include/hgl/decl/impl_tags.hpp b/include/hgl/decl/impl_tags.hpp index a1a255c..88efd47 100644 --- a/include/hgl/decl/impl_tags.hpp +++ b/include/hgl/decl/impl_tags.hpp @@ -26,6 +26,11 @@ template < traits::c_id_type IdType = default_id_type> struct matrix_t; +template < + traits::c_hypergraph_asymmetric_layout_tag LayoutTag = hyperedge_major_t, + traits::c_id_type IdType = default_id_type> +struct flat_matrix_t; + } // namespace impl namespace traits { @@ -44,7 +49,11 @@ template concept c_hypergraph_matrix_impl = c_instantiation_of; template -concept c_hypergraph_incidence_matrix_impl = c_hypergraph_matrix_impl; +concept c_hypergraph_flat_matrix_impl = c_instantiation_of; + +template +concept c_hypergraph_incidence_matrix_impl = + c_hypergraph_matrix_impl or c_hypergraph_flat_matrix_impl; template concept c_hypergraph_impl_tag = diff --git a/include/hgl/hypergraph_traits.hpp b/include/hgl/hypergraph_traits.hpp index b3ba8b6..173d2cb 100644 --- a/include/hgl/hypergraph_traits.hpp +++ b/include/hgl/hypergraph_traits.hpp @@ -66,6 +66,18 @@ using matrix_hypergraph_traits = hypergraph_traits< HyperedgeProperties, impl::matrix_t>; +template < + traits::c_hypergraph_asymmetric_layout_tag LayoutTag = impl::hyperedge_major_t, + traits::c_hypergraph_directional_tag DirectionalTag = undirected_t, + traits::c_properties VertexProperties = empty_properties, + traits::c_properties HyperedgeProperties = empty_properties, + traits::c_id_type IdType = default_id_type> +using flat_matrix_hypergraph_traits = hypergraph_traits< + DirectionalTag, + VertexProperties, + HyperedgeProperties, + impl::flat_matrix_t>; + template < traits::c_properties VertexProperties = empty_properties, traits::c_properties HyperedgeProperties = empty_properties, @@ -101,8 +113,14 @@ concept c_matrix_hypergraph_traits = c_instantiation_of and c_hypergraph_matrix_impl; -template -concept c_incidence_matrix_hypergraph_traits = c_matrix_hypergraph_traits; +template +concept c_flat_matrix_hypergraph_traits = + c_instantiation_of + and c_hypergraph_flat_matrix_impl; + +template +concept c_incidence_matrix_hypergraph_traits = + c_matrix_hypergraph_traits or c_flat_matrix_hypergraph_traits; template concept c_undirected_hypergraph_traits = diff --git a/include/hgl/impl/bf_incidence.hpp b/include/hgl/impl/bf_incidence.hpp new file mode 100644 index 0000000..3cc38de --- /dev/null +++ b/include/hgl/impl/bf_incidence.hpp @@ -0,0 +1,29 @@ +// 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 + +namespace hgl::impl { + +enum class bf_incidence : std::int8_t { + none = 0, // v not in E + backward = -1, // v in Tail(E) + forward = 1, // v in Head(E) +}; + +[[nodiscard]] constexpr bool bf_is_incident(const bf_incidence i) noexcept { + return i != bf_incidence::none; +} + +[[nodiscard]] constexpr bool bf_is_tail(const bf_incidence i) noexcept { + return i == bf_incidence::backward; +} + +[[nodiscard]] constexpr bool bf_is_head(const bf_incidence i) noexcept { + return i == bf_incidence::forward; +} + +} // namespace hgl::impl diff --git a/include/hgl/impl/flat_incidence_matrix.hpp b/include/hgl/impl/flat_incidence_matrix.hpp new file mode 100644 index 0000000..8bfe954 --- /dev/null +++ b/include/hgl/impl/flat_incidence_matrix.hpp @@ -0,0 +1,500 @@ +// 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 "gl/types/core.hpp" +#include "hgl/constants.hpp" +#include "hgl/decl/impl_tags.hpp" +#include "hgl/directional_tags.hpp" +#include "hgl/impl/bf_incidence.hpp" +#include "hgl/impl/layout_tags.hpp" +#include "hgl/types.hpp" + +#include +#include +#include +#include + +#ifdef HGL_TESTING +namespace hgl_testing { +struct test_flat_incidence_matrix; +} // namespace hgl_testing +#endif + +namespace hgl { + +namespace detail { + +template +struct to_impl; + +} // namespace detail + +namespace impl { + +template < + traits::c_hypergraph_directional_tag DirectionalTag, + traits::c_hypergraph_flat_matrix_impl ImplTag> +class flat_incidence_matrix; + +template +requires traits::c_hypergraph_asymmetric_layout_tag +class flat_incidence_matrix final { +public: + using directional_tag = hgl::undirected_t; + using implementation_tag = ImplTag; + using layout_tag = typename implementation_tag::layout_tag; + using id_type = typename implementation_tag::id_type; + + flat_incidence_matrix() = default; + + flat_incidence_matrix(const size_type n_vertices, const size_type n_hyperedges) + : _matrix( + layout_tag::major(n_vertices, n_hyperedges), + layout_tag::minor(n_vertices, n_hyperedges), + false + ) {} + + flat_incidence_matrix(const flat_incidence_matrix&) = default; + flat_incidence_matrix& operator=(const flat_incidence_matrix&) = default; + + flat_incidence_matrix(flat_incidence_matrix&&) noexcept = default; + flat_incidence_matrix& operator=(flat_incidence_matrix&&) noexcept = default; + + ~flat_incidence_matrix() = default; + + // --- vertex methods --- + + gl_attr_force_inline void add_vertices(const size_type n) noexcept { + this->_add(n); + } + + gl_attr_force_inline void remove_vertex(const id_type vertex_id) noexcept { + this->_remove(vertex_id); + } + + [[nodiscard]] gl_attr_force_inline auto incident_hyperedges(const id_type vertex_id + ) const noexcept { + return this->_incident_with(vertex_id); + } + + [[nodiscard]] size_type degree(const id_type vertex_id) const noexcept { + return this->_count(vertex_id); + } + + [[nodiscard]] std::vector degree_map(const size_type n_vertices) const noexcept { + return this->_count_map(n_vertices); + } + + // --- hyperedge methods --- + + gl_attr_force_inline void add_hyperedges(const size_type n) noexcept { + this->_add(n); + } + + gl_attr_force_inline void remove_hyperedge(const id_type hyperedge_id) noexcept { + this->_remove(hyperedge_id); + } + + [[nodiscard]] gl_attr_force_inline auto incident_vertices(const id_type hyperedge_id + ) const noexcept { + return this->_incident_with(hyperedge_id); + } + + [[nodiscard]] size_type hyperedge_size(const id_type hyperedge_id) const noexcept { + return this->_count(hyperedge_id); + } + + [[nodiscard]] std::vector hyperedge_size_map(const size_type n_hyperedges + ) const noexcept { + return this->_count_map(n_hyperedges); + } + + // --- binding methods --- + + gl_attr_force_inline void bind(const id_type vertex_id, const id_type hyperedge_id) noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + this->_matrix[to_idx(major_id), to_idx(minor_id)] = true; + } + + gl_attr_force_inline void unbind(const id_type vertex_id, const id_type hyperedge_id) noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + this->_matrix[to_idx(major_id), to_idx(minor_id)] = false; + } + + [[nodiscard]] gl_attr_force_inline bool are_bound( + const id_type vertex_id, const id_type hyperedge_id + ) const noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + return this->_matrix[to_idx(major_id), to_idx(minor_id)]; + } + + // --- comparison --- + + [[nodiscard]] friend bool + operator==(const flat_incidence_matrix&, const flat_incidence_matrix&) = default; + + // --- friend declarations --- + + template < + traits::c_hypergraph_impl_tag TargetImplTag, + traits::c_hypergraph_impl_tag SourceImplTag> + friend struct hgl::detail::to_impl; + +#ifdef HGL_TESTING + friend struct hgl_testing::test_flat_incidence_matrix; +#endif + +private: + // using matrix_row_type = std::vector; + using hypergraph_storage_type = flat_matrix; + + template + void _add(const size_type n) noexcept { + if constexpr (Element == layout_tag::major_element) // add major + this->_matrix.resize(this->_matrix.n_rows() + n, this->_matrix.n_cols(), false); + else // add minor + this->_matrix.resize(this->_matrix.n_rows(), this->_matrix.n_cols() + n, false); + } + + template + void _remove(const id_type id) noexcept { + if constexpr (Element == layout_tag::major_element) // remove major + this->_matrix.erase_row(to_idx(id)); + else // remove minor + this->_matrix.erase_col(to_idx(id)); + } + + template + gl_attr_force_inline auto _incident_with(const id_type id) const noexcept { + if constexpr (Element == layout_tag::major_element) { // incident with major + return std::views::iota(initial_id_v, this->_matrix.n_cols()) + | std::views::filter([row = this->_matrix[to_idx(id)]](const id_type minor_id) { + return row[to_diff(minor_id)]; + }); + } + else { // incident with minor + return std::views::iota(initial_id_v, this->_matrix.n_rows()) + | std::views::filter([this, minor_idx = to_idx(id)](const id_type major_id) { + return this->_matrix[to_idx(major_id), minor_idx]; + }); + } + } + + template + [[nodiscard]] gl_attr_force_inline size_type _count(const id_type id) const noexcept { + if constexpr (Element == layout_tag::major_element) { // count major + return static_cast(std::ranges::count(this->_matrix[to_idx(id)], true)); + } + else { // count minor + const auto pos = to_diff(id); + size_type count = 0uz; + for (const auto& row : this->_matrix) + count += static_cast(row[pos]); + return count; + } + } + + template + [[nodiscard]] std::vector _count_map(const size_type n_elements) const noexcept { + std::vector size_map(n_elements, 0uz); + + if constexpr (Element == layout_tag::major_element) { // count map major + const size_type limit = std::min(n_elements, this->_matrix.n_rows()); + for (auto i = 0uz; i < limit; ++i) + size_map[i] = static_cast(std::ranges::count(this->_matrix[i], true)); + } + else { // count map minor + const size_type limit = std::min(n_elements, this->_matrix.n_cols()); + for (const auto& row : this->_matrix) + for (auto j = 0uz; j < limit; ++j) + size_map[j] += static_cast(row[to_diff(j)]); + } + return size_map; + } + + hypergraph_storage_type _matrix; +}; + +template +requires traits::c_hypergraph_asymmetric_layout_tag +class flat_incidence_matrix final { +public: + using directional_tag = hgl::bf_directed_t; + using implementation_tag = ImplTag; + using layout_tag = typename implementation_tag::layout_tag; + using id_type = typename implementation_tag::id_type; + +public: + flat_incidence_matrix() = default; + + flat_incidence_matrix(const size_type n_vertices, const size_type n_hyperedges) + : _matrix( + layout_tag::major(n_vertices, n_hyperedges), + layout_tag::minor(n_vertices, n_hyperedges), + bf_incidence::none + ) {} + + flat_incidence_matrix(const flat_incidence_matrix&) = default; + flat_incidence_matrix& operator=(const flat_incidence_matrix&) = default; + + flat_incidence_matrix(flat_incidence_matrix&&) noexcept = default; + flat_incidence_matrix& operator=(flat_incidence_matrix&&) noexcept = default; + + ~flat_incidence_matrix() = default; + + // --- vertex methods : general --- + + gl_attr_force_inline void add_vertices(const size_type n) noexcept { + this->_add(n); + } + + gl_attr_force_inline void remove_vertex(const id_type vertex_id) noexcept { + this->_remove(vertex_id); + } + + // --- vertex methods : incidence queries --- + + [[nodiscard]] gl_attr_force_inline auto incident_hyperedges(const id_type vertex_id + ) const noexcept { + return this->_query(vertex_id, bf_is_incident); + } + + [[nodiscard]] size_type degree(const id_type vertex_id) const noexcept { + return this->_count(vertex_id, bf_is_incident); + } + + [[nodiscard]] std::vector degree_map(const size_type n_vertices) const noexcept { + return this->_count_map(n_vertices, bf_is_incident); + } + + [[nodiscard]] gl_attr_force_inline auto out_hyperedges(const id_type vertex_id) const noexcept { + return this->_query(vertex_id, bf_is_tail); + } + + [[nodiscard]] size_type out_degree(const id_type vertex_id) const noexcept { + return this->_count(vertex_id, bf_is_tail); + } + + [[nodiscard]] std::vector out_degree_map(const size_type n_vertices) const noexcept { + return this->_count_map(n_vertices, bf_is_tail); + } + + [[nodiscard]] gl_attr_force_inline auto in_hyperedges(const id_type vertex_id) const noexcept { + return this->_query(vertex_id, bf_is_head); + } + + [[nodiscard]] size_type in_degree(const id_type vertex_id) const noexcept { + return this->_count(vertex_id, bf_is_head); + } + + [[nodiscard]] std::vector in_degree_map(const size_type n_vertices) const noexcept { + return this->_count_map(n_vertices, bf_is_head); + } + + // --- hyperedge methods : general --- + + gl_attr_force_inline void add_hyperedges(const size_type n) noexcept { + this->_add(n); + } + + gl_attr_force_inline void remove_hyperedge(const id_type hyperedge_id) noexcept { + this->_remove(hyperedge_id); + } + + // --- hyperedge methods : incidence queries --- + + [[nodiscard]] gl_attr_force_inline auto incident_vertices(const id_type hyperedge_id + ) const noexcept { + return this->_query(hyperedge_id, bf_is_incident); + } + + [[nodiscard]] size_type hyperedge_size(const id_type hyperedge_id) const noexcept { + return this->_count(hyperedge_id, bf_is_incident); + } + + [[nodiscard]] std::vector hyperedge_size_map(const size_type n_hyperedges + ) const noexcept { + return this->_count_map(n_hyperedges, bf_is_incident); + } + + [[nodiscard]] gl_attr_force_inline auto tail_vertices(const id_type hyperedge_id + ) const noexcept { + return this->_query(hyperedge_id, bf_is_tail); + } + + [[nodiscard]] size_type tail_size(const id_type hyperedge_id) const noexcept { + return this->_count(hyperedge_id, bf_is_tail); + } + + [[nodiscard]] std::vector tail_size_map(const size_type n_hyperedges + ) const noexcept { + return this->_count_map(n_hyperedges, bf_is_tail); + } + + [[nodiscard]] gl_attr_force_inline auto head_vertices(const id_type hyperedge_id + ) const noexcept { + return this->_query(hyperedge_id, bf_is_head); + } + + [[nodiscard]] size_type head_size(const id_type hyperedge_id) const noexcept { + return this->_count(hyperedge_id, bf_is_head); + } + + [[nodiscard]] std::vector head_size_map(const size_type n_hyperedges + ) const noexcept { + return this->_count_map(n_hyperedges, bf_is_head); + } + + // --- binding methods --- + + gl_attr_force_inline void bind_tail( + const id_type vertex_id, const id_type hyperedge_id + ) noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + this->_matrix[to_idx(major_id), to_idx(minor_id)] = bf_incidence::backward; + } + + gl_attr_force_inline void bind_head( + const id_type vertex_id, const id_type hyperedge_id + ) noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + this->_matrix[to_idx(major_id), to_idx(minor_id)] = bf_incidence::forward; + } + + gl_attr_force_inline void unbind(const id_type vertex_id, const id_type hyperedge_id) noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + this->_matrix[to_idx(major_id), to_idx(minor_id)] = bf_incidence::none; + } + + [[nodiscard]] gl_attr_force_inline bool are_bound( + const id_type vertex_id, const id_type hyperedge_id + ) const noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + return this->_matrix[to_idx(major_id), to_idx(minor_id)] != bf_incidence::none; + } + + [[nodiscard]] gl_attr_force_inline bool is_tail( + const id_type vertex_id, const id_type hyperedge_id + ) const noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + return this->_matrix[to_idx(major_id), to_idx(minor_id)] == bf_incidence::backward; + } + + [[nodiscard]] gl_attr_force_inline bool is_head( + const id_type vertex_id, const id_type hyperedge_id + ) const noexcept { + const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); + return this->_matrix[to_idx(major_id), to_idx(minor_id)] == bf_incidence::forward; + } + + // --- comparison --- + + [[nodiscard]] friend bool + operator==(const flat_incidence_matrix&, const flat_incidence_matrix&) = default; + + // --- friend declarations --- + + template < + traits::c_hypergraph_impl_tag TargetImplTag, + traits::c_hypergraph_impl_tag SourceImplTag> + friend struct hgl::detail::to_impl; + +#ifdef HGL_TESTING + friend struct hgl_testing::test_flat_incidence_matrix; +#endif + +private: + // --- storage management --- + + using hypergraph_storage_type = flat_matrix; + + template + void _add(const size_type n) noexcept { + if constexpr (Element == layout_tag::major_element) // add major + this->_matrix.resize( + this->_matrix.n_rows() + n, this->_matrix.n_cols(), bf_incidence::none + ); + else // add minor + this->_matrix.resize( + this->_matrix.n_rows(), this->_matrix.n_cols() + n, bf_incidence::none + ); + } + + template + void _remove(const id_type id) noexcept { + if constexpr (Element == layout_tag::major_element) // remove major + this->_matrix.erase_row(to_idx(id)); + else // remove minor + this->_matrix.erase_col(to_idx(id)); + } + + template + [[nodiscard]] gl_attr_force_inline auto _query( + const id_type id, std::predicate auto&& pred + ) const noexcept { + if constexpr (Element == layout_tag::major_element) { // query major + return std::views::iota(initial_id_v, this->_matrix.n_cols()) + | std::views::filter( + [row = this->_matrix[to_idx(id)], + pred = std::forward(pred)](const id_type minor_id) { + return pred(row[to_diff(minor_id)]); + } + ); + } + else { // query minor + return std::views::iota(initial_id_v, this->_matrix.n_rows()) + | std::views::filter( + [this, minor_idx = to_idx(id), pred = std::forward(pred)]( + const id_type major_id + ) { return pred(this->_matrix[to_idx(major_id), minor_idx]); } + ); + } + } + + template + [[nodiscard]] gl_attr_force_inline size_type + _count(const id_type id, std::predicate auto&& pred) const noexcept { + if constexpr (Element == layout_tag::major_element) { // count major + size_type count = 0uz; + for (const bf_incidence t : this->_matrix[to_idx(id)]) + count += static_cast(pred(t)); + return count; + } + else { // count minor + const auto pos = to_diff(id); + size_type count = 0uz; + for (const auto& row : this->_matrix) + count += static_cast(pred(row[pos])); + return count; + } + } + + template + [[nodiscard]] std::vector _count_map( + const size_type n_elements, std::predicate auto&& pred + ) const noexcept { + std::vector size_map(n_elements, 0uz); + + if constexpr (Element == layout_tag::major_element) { // count map major + const size_type limit = std::min(n_elements, this->_matrix.n_rows()); + for (auto i = 0uz; i < limit; ++i) + for (const bf_incidence val : this->_matrix[i]) + size_map[i] += static_cast(pred(val)); + } + else { // count map minor + const size_type limit = std::min(n_elements, this->_matrix.n_cols()); + for (const auto& row : this->_matrix) + for (auto j = 0uz; j < limit; ++j) + size_map[j] += static_cast(pred(row[to_diff(j)])); + } + return size_map; + } + + hypergraph_storage_type _matrix; +}; + +} // namespace impl +} // namespace hgl diff --git a/include/hgl/impl/impl_tags.hpp b/include/hgl/impl/impl_tags.hpp index e11207c..e967411 100644 --- a/include/hgl/impl/impl_tags.hpp +++ b/include/hgl/impl/impl_tags.hpp @@ -6,6 +6,7 @@ #include "hgl/decl/impl_tags.hpp" #include "hgl/impl/flat_incidence_list.hpp" +#include "hgl/impl/flat_incidence_matrix.hpp" #include "hgl/impl/incidence_list.hpp" #include "hgl/impl/incidence_matrix.hpp" #include "hgl/impl/layout_tags.hpp" @@ -45,4 +46,15 @@ struct matrix_t { using implementation_type = incidence_matrix; }; +template +struct flat_matrix_t { + using type = flat_matrix_t; + + using layout_tag = LayoutTag; + using id_type = IdType; + + template + using implementation_type = flat_incidence_matrix; +}; + } // namespace hgl::impl diff --git a/include/hgl/impl/incidence_list.hpp b/include/hgl/impl/incidence_list.hpp index 3590782..74a66f9 100644 --- a/include/hgl/impl/incidence_list.hpp +++ b/include/hgl/impl/incidence_list.hpp @@ -169,9 +169,7 @@ class incidence_list final { template void _remove(const id_type id) noexcept { if constexpr (Element == layout_tag::major_element) { // remove major - this->_major_storage.erase( - this->_major_storage.begin() + static_cast(id) - ); + this->_major_storage.erase(this->_major_storage.begin() + to_diff(id)); } else { // remove minor for (auto& minor_storage : this->_major_storage) { @@ -449,12 +447,8 @@ class incidence_list final { template void _remove(const id_type id) noexcept { if constexpr (Element == layout_tag::major_element) { // remove major - this->_tail_storage.erase( - this->_tail_storage.begin() + static_cast(id) - ); - this->_head_storage.erase( - this->_head_storage.begin() + static_cast(id) - ); + this->_tail_storage.erase(this->_tail_storage.begin() + to_diff(id)); + this->_head_storage.erase(this->_head_storage.begin() + to_diff(id)); } else { // remove minor for (auto& minor_storage : this->_tail_storage) diff --git a/include/hgl/impl/incidence_matrix.hpp b/include/hgl/impl/incidence_matrix.hpp index abb81fc..5d421a1 100644 --- a/include/hgl/impl/incidence_matrix.hpp +++ b/include/hgl/impl/incidence_matrix.hpp @@ -4,9 +4,11 @@ #pragma once +#include "gl/types/core.hpp" #include "hgl/constants.hpp" #include "hgl/decl/impl_tags.hpp" #include "hgl/directional_tags.hpp" +#include "hgl/impl/bf_incidence.hpp" #include "hgl/impl/layout_tags.hpp" #include "hgl/types.hpp" @@ -166,15 +168,16 @@ class incidence_matrix final { template void _remove(const id_type id) noexcept { if constexpr (Element == layout_tag::major_element) { // remove major - this->_matrix.erase(this->_matrix.begin() + static_cast(id)); + this->_matrix.erase(this->_matrix.begin() + to_diff(id)); } else { // remove minor if (this->_matrix_row_size == 0uz) return; + this->_matrix_row_size--; - for (auto& row : this->_matrix) { - row.erase(row.begin() + static_cast(id)); - } + const auto pos = to_diff(id); + for (auto& row : this->_matrix) + row.erase(row.begin() + pos); } } @@ -248,7 +251,7 @@ class incidence_matrix final { : _matrix_row_size{layout_tag::minor(n_vertices, n_hyperedges)}, _matrix( layout_tag::major(n_vertices, n_hyperedges), - matrix_row_type(_matrix_row_size, incidence_type::none) + matrix_row_type(_matrix_row_size, bf_incidence::none) ) {} incidence_matrix(const incidence_matrix&) = default; @@ -273,39 +276,39 @@ class incidence_matrix final { [[nodiscard]] gl_attr_force_inline auto incident_hyperedges(const id_type vertex_id ) const noexcept { - return this->_query(vertex_id, _is_incident); + return this->_query(vertex_id, bf_is_incident); } [[nodiscard]] size_type degree(const id_type vertex_id) const noexcept { - return this->_count(vertex_id, _is_incident); + return this->_count(vertex_id, bf_is_incident); } [[nodiscard]] std::vector degree_map(const size_type n_vertices) const noexcept { - return this->_count_map(n_vertices, _is_incident); + return this->_count_map(n_vertices, bf_is_incident); } [[nodiscard]] gl_attr_force_inline auto out_hyperedges(const id_type vertex_id) const noexcept { - return this->_query(vertex_id, _is_tail); + return this->_query(vertex_id, bf_is_tail); } [[nodiscard]] size_type out_degree(const id_type vertex_id) const noexcept { - return this->_count(vertex_id, _is_tail); + return this->_count(vertex_id, bf_is_tail); } [[nodiscard]] std::vector out_degree_map(const size_type n_vertices) const noexcept { - return this->_count_map(n_vertices, _is_tail); + return this->_count_map(n_vertices, bf_is_tail); } [[nodiscard]] gl_attr_force_inline auto in_hyperedges(const id_type vertex_id) const noexcept { - return this->_query(vertex_id, _is_head); + return this->_query(vertex_id, bf_is_head); } [[nodiscard]] size_type in_degree(const id_type vertex_id) const noexcept { - return this->_count(vertex_id, _is_head); + return this->_count(vertex_id, bf_is_head); } [[nodiscard]] std::vector in_degree_map(const size_type n_vertices) const noexcept { - return this->_count_map(n_vertices, _is_head); + return this->_count_map(n_vertices, bf_is_head); } // --- hyperedge methods : general --- @@ -322,44 +325,44 @@ class incidence_matrix final { [[nodiscard]] gl_attr_force_inline auto incident_vertices(const id_type hyperedge_id ) const noexcept { - return this->_query(hyperedge_id, _is_incident); + return this->_query(hyperedge_id, bf_is_incident); } [[nodiscard]] size_type hyperedge_size(const id_type hyperedge_id) const noexcept { - return this->_count(hyperedge_id, _is_incident); + return this->_count(hyperedge_id, bf_is_incident); } [[nodiscard]] std::vector hyperedge_size_map(const size_type n_hyperedges ) const noexcept { - return this->_count_map(n_hyperedges, _is_incident); + return this->_count_map(n_hyperedges, bf_is_incident); } [[nodiscard]] gl_attr_force_inline auto tail_vertices(const id_type hyperedge_id ) const noexcept { - return this->_query(hyperedge_id, _is_tail); + return this->_query(hyperedge_id, bf_is_tail); } [[nodiscard]] size_type tail_size(const id_type hyperedge_id) const noexcept { - return this->_count(hyperedge_id, _is_tail); + return this->_count(hyperedge_id, bf_is_tail); } [[nodiscard]] std::vector tail_size_map(const size_type n_hyperedges ) const noexcept { - return this->_count_map(n_hyperedges, _is_tail); + return this->_count_map(n_hyperedges, bf_is_tail); } [[nodiscard]] gl_attr_force_inline auto head_vertices(const id_type hyperedge_id ) const noexcept { - return this->_query(hyperedge_id, _is_head); + return this->_query(hyperedge_id, bf_is_head); } [[nodiscard]] size_type head_size(const id_type hyperedge_id) const noexcept { - return this->_count(hyperedge_id, _is_head); + return this->_count(hyperedge_id, bf_is_head); } [[nodiscard]] std::vector head_size_map(const size_type n_hyperedges ) const noexcept { - return this->_count_map(n_hyperedges, _is_head); + return this->_count_map(n_hyperedges, bf_is_head); } // --- binding methods --- @@ -368,40 +371,40 @@ class incidence_matrix final { const id_type vertex_id, const id_type hyperedge_id ) noexcept { const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); - this->_matrix[to_idx(major_id)][to_idx(minor_id)] = incidence_type::backward; + this->_matrix[to_idx(major_id)][to_idx(minor_id)] = bf_incidence::backward; } gl_attr_force_inline void bind_head( const id_type vertex_id, const id_type hyperedge_id ) noexcept { const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); - this->_matrix[to_idx(major_id)][to_idx(minor_id)] = incidence_type::forward; + this->_matrix[to_idx(major_id)][to_idx(minor_id)] = bf_incidence::forward; } gl_attr_force_inline void unbind(const id_type vertex_id, const id_type hyperedge_id) noexcept { const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); - this->_matrix[to_idx(major_id)][to_idx(minor_id)] = incidence_type::none; + this->_matrix[to_idx(major_id)][to_idx(minor_id)] = bf_incidence::none; } [[nodiscard]] gl_attr_force_inline bool are_bound( const id_type vertex_id, const id_type hyperedge_id ) const noexcept { const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); - return this->_matrix[to_idx(major_id)][to_idx(minor_id)] != incidence_type::none; + return this->_matrix[to_idx(major_id)][to_idx(minor_id)] != bf_incidence::none; } [[nodiscard]] gl_attr_force_inline bool is_tail( const id_type vertex_id, const id_type hyperedge_id ) const noexcept { const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); - return this->_matrix[to_idx(major_id)][to_idx(minor_id)] == incidence_type::backward; + return this->_matrix[to_idx(major_id)][to_idx(minor_id)] == bf_incidence::backward; } [[nodiscard]] gl_attr_force_inline bool is_head( const id_type vertex_id, const id_type hyperedge_id ) const noexcept { const auto [major_id, minor_id] = layout_tag::majmin(vertex_id, hyperedge_id); - return this->_matrix[to_idx(major_id)][to_idx(minor_id)] == incidence_type::forward; + return this->_matrix[to_idx(major_id)][to_idx(minor_id)] == bf_incidence::forward; } // --- comparison --- @@ -421,29 +424,9 @@ class incidence_matrix final { #endif private: - // --- incidence utility --- - - enum class incidence_type : std::int8_t { - none = 0, // v not in E - backward = -1, // v in Tail(E) - forward = 1, // v in Head(E) - }; - - static constexpr auto _is_incident = [](const incidence_type t) { - return t != incidence_type::none; - }; - - static constexpr auto _is_tail = [](const incidence_type t) { - return t == incidence_type::backward; - }; - - static constexpr auto _is_head = [](const incidence_type t) { - return t == incidence_type::forward; - }; - // --- storage management --- - using matrix_row_type = std::vector; + using matrix_row_type = std::vector; using hypergraph_storage_type = std::vector; template @@ -451,33 +434,35 @@ class incidence_matrix final { if constexpr (Element == layout_tag::major_element) { // add major this->_matrix.resize( this->_matrix.size() + n, - matrix_row_type(this->_matrix_row_size, incidence_type::none) + matrix_row_type(this->_matrix_row_size, bf_incidence::none) ); } else { // add minor this->_matrix_row_size += n; for (auto& row : this->_matrix) - row.resize(this->_matrix_row_size, incidence_type::none); + row.resize(this->_matrix_row_size, bf_incidence::none); } } template void _remove(const id_type id) noexcept { if constexpr (Element == layout_tag::major_element) { // remove major - this->_matrix.erase(this->_matrix.begin() + static_cast(id)); + this->_matrix.erase(this->_matrix.begin() + to_diff(id)); } else { // remove minor if (this->_matrix_row_size == 0uz) return; + this->_matrix_row_size--; + const auto pos = to_diff(id); for (auto& row : this->_matrix) - row.erase(row.begin() + static_cast(id)); + row.erase(row.begin() + pos); } } template [[nodiscard]] gl_attr_force_inline auto _query( - const id_type id, std::predicate auto&& pred + const id_type id, std::predicate auto&& pred ) const noexcept { if constexpr (Element == layout_tag::major_element) { // query major return std::views::iota(initial_id_v, this->_matrix_row_size) @@ -495,10 +480,10 @@ class incidence_matrix final { template [[nodiscard]] gl_attr_force_inline size_type - _count(const id_type id, std::predicate auto&& pred) const noexcept { + _count(const id_type id, std::predicate auto&& pred) const noexcept { size_type count = 0uz; if constexpr (Element == layout_tag::major_element) { // count major - for (const incidence_type t : this->_matrix[to_idx(id)]) + for (const bf_incidence t : this->_matrix[to_idx(id)]) count += static_cast(pred(t)); } else { // count minor @@ -511,7 +496,7 @@ class incidence_matrix final { template [[nodiscard]] std::vector _count_map( - const size_type n_elements, std::predicate auto&& pred + const size_type n_elements, std::predicate auto&& pred ) const noexcept { std::vector size_map(n_elements, 0uz); diff --git a/include/hgl/types.hpp b/include/hgl/types.hpp index 21eab23..9b34faf 100644 --- a/include/hgl/types.hpp +++ b/include/hgl/types.hpp @@ -6,6 +6,7 @@ #include "gl/types/core.hpp" #include "gl/types/flat_jagged_vector.hpp" +#include "gl/types/flat_matrix.hpp" #include "gl/types/properties.hpp" namespace hgl { @@ -15,11 +16,13 @@ namespace hgl { using gl::default_id_type; using gl::size_type; +using gl::to_diff; using gl::to_idx; // --- generic data structures --- using gl::flat_jagged_vector; +using gl::flat_matrix; using gl::homogeneous_pair; // --- property types --- diff --git a/tests/source/gl/test_flat_jagged_vector.cpp b/tests/source/gl/test_flat_jagged_vector.cpp index c45b0e2..d049f51 100644 --- a/tests/source/gl/test_flat_jagged_vector.cpp +++ b/tests/source/gl/test_flat_jagged_vector.cpp @@ -1,4 +1,5 @@ #include "doctest.h" +#include "gl/types/core.hpp" #include #include @@ -1293,7 +1294,7 @@ TEST_CASE_FIXTURE( for (int i = 0; i < 100; ++i) { auto seg = sut[static_cast(i)]; for (int j = 0; j < 10; ++j) - CHECK_EQ(seg[static_cast(j)], i * 10 + j); + CHECK_EQ(seg[gl::to_diff(j)], i * 10 + j); } } diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp index 4013cc7..7e756ab 100644 --- a/tests/source/gl/test_flat_matrix.cpp +++ b/tests/source/gl/test_flat_matrix.cpp @@ -1449,7 +1449,7 @@ TEST_CASE_FIXTURE( for (int i = 0; i < 100; ++i) { auto row = sut[static_cast(i)]; for (int j = 0; j < 10; ++j) - CHECK_EQ(row[static_cast(j)], i * 10 + j); + CHECK_EQ(row[gl::to_diff(j)], i * 10 + j); } } diff --git a/tests/source/gl/test_graph_topology_builders.cpp b/tests/source/gl/test_graph_topology_builders.cpp index bbcf6d2..b69d054 100644 --- a/tests/source/gl/test_graph_topology_builders.cpp +++ b/tests/source/gl/test_graph_topology_builders.cpp @@ -285,7 +285,7 @@ TEST_CASE_TEMPLATE_DEFINE( SUBCASE("path(n_vertices) should build a one-way path graph of size n_vertices") { const auto path = gl::topology::path(constants::n_elements_top); const auto n_source_vertices = path.order() - 1uz; - const auto last_vertex_pos = static_cast(n_source_vertices); + const auto last_vertex_pos = gl::to_diff(n_source_vertices); verify_graph_size(path, constants::n_elements_top, n_source_vertices); @@ -299,7 +299,7 @@ TEST_CASE_TEMPLATE_DEFINE( SUBCASE("bidirectional_path(n_vertices) should build a two-way path graph of size n_vertices") { const auto path = gl::topology::bidirectional_path(constants::n_elements_top); const auto n_source_vertices = path.order() - 1uz; - const auto last_vertex_pos = static_cast(n_source_vertices); + const auto last_vertex_pos = gl::to_diff(n_source_vertices); verify_graph_size(path, constants::n_elements_top, 2uz * n_source_vertices); @@ -389,7 +389,7 @@ TEST_CASE_TEMPLATE_DEFINE( CAPTURE(path); const auto n_source_vertices = path.order() - 1uz; - const auto last_vertex_pos = static_cast(n_source_vertices); + const auto last_vertex_pos = gl::to_diff(n_source_vertices); verify_graph_size(path, constants::n_elements_top, n_source_vertices); const auto vertices = path.vertices(); diff --git a/tests/source/hgl/test_conversion.cpp b/tests/source/hgl/test_conversion.cpp index 3f23db5..05f9c54 100644 --- a/tests/source/hgl/test_conversion.cpp +++ b/tests/source/hgl/test_conversion.cpp @@ -162,6 +162,14 @@ TEST_CASE_TEMPLATE_DEFINE( using emajor_matrix_hypergraph = hgl::hypergraph>; + using vmajor_flat_matrix_tag = hgl::impl::flat_matrix_t; + using emajor_flat_matrix_tag = hgl::impl::flat_matrix_t; + + using vmajor_flat_matrix_hypergraph = + hgl::hypergraph>; + using emajor_flat_matrix_hypergraph = + hgl::hypergraph>; + test_hypergraph_conversion fixture; auto test_conversion_for = @@ -207,6 +215,16 @@ TEST_CASE_TEMPLATE_DEFINE( auto dst = hgl::to(std::move(src)); fixture.validate_hypergraph(dst); } + SUBCASE("to vertex-major flat-matrix") { + auto src = fixture.create_test_hypergraph(); + auto dst = hgl::to(std::move(src)); + fixture.validate_hypergraph(dst); + } + SUBCASE("to hyperedge-major flat-matrix") { + auto src = fixture.create_test_hypergraph(); + auto dst = hgl::to(std::move(src)); + fixture.validate_hypergraph(dst); + } } }; @@ -232,6 +250,13 @@ TEST_CASE_TEMPLATE_DEFINE( test_conversion_for( std::type_identity{}, "source: hyperedge-major matrix" ); + + test_conversion_for( + std::type_identity{}, "source: vertex-major flat-matrix" + ); + test_conversion_for( + std::type_identity{}, "source: hyperedge-major flat-matrix" + ); } TEST_CASE_TEMPLATE_INSTANTIATE( @@ -403,15 +428,15 @@ TEST_CASE_TEMPLATE_INSTANTIATE( hgl::flat_list_hypergraph_traits< hgl::impl::hyperedge_major_t, hgl::undirected_t>, // undirected hyperedge-major flat incidence list - hgl::flat_list_hypergraph_traits< - hgl::impl::vertex_major_t, - hgl::undirected_t>, // undirected vertex-major flat incidence list hgl::matrix_hypergraph_traits< + hgl::impl::vertex_major_t, + hgl::undirected_t>, // vertex-major incidence matrix + hgl::flat_matrix_hypergraph_traits< hgl::impl::hyperedge_major_t, - hgl::undirected_t>, // hyperedge-major incidence matrix - hgl::matrix_hypergraph_traits< + hgl::undirected_t>, // hyperedge-major flat incidence matrix + hgl::flat_matrix_hypergraph_traits< hgl::impl::vertex_major_t, - hgl::undirected_t> // vertex-major incidence matrix + hgl::undirected_t> // vertex-major flat incidence matrix ); TEST_CASE_TEMPLATE_DEFINE( @@ -547,7 +572,13 @@ TEST_CASE_TEMPLATE_INSTANTIATE( hgl::bf_directed_t>, // hyperedge-major incidence matrix hgl::matrix_hypergraph_traits< hgl::impl::vertex_major_t, - hgl::bf_directed_t> // vertex-major incidence matrix + hgl::bf_directed_t>, // vertex-major incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::hyperedge_major_t, + hgl::bf_directed_t>, // hyperedge-major flat incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::vertex_major_t, + hgl::bf_directed_t> // vertex-major flat incidence matrix ); TEST_SUITE_END(); // test_converters diff --git a/tests/source/hgl/test_flat_incidence_matrix.cpp b/tests/source/hgl/test_flat_incidence_matrix.cpp new file mode 100644 index 0000000..00b2a64 --- /dev/null +++ b/tests/source/hgl/test_flat_incidence_matrix.cpp @@ -0,0 +1,1637 @@ +#include "doctest.h" +#include "testing/hgl/constants.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +namespace hgl_testing { + +TEST_SUITE_BEGIN("test_flat_incidence_matrix"); + +struct test_flat_incidence_matrix { + template + typename IncidenceMatrix::hypergraph_storage_type& matrix(IncidenceMatrix& sut) const noexcept { + return sut._matrix; + } +}; + +struct test_undirected_vertex_major_flat_incidence_matrix : public test_flat_incidence_matrix { + using impl_tag = hgl::impl::flat_matrix_t; + using sut_type = hgl::impl::flat_incidence_matrix; +}; + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, "should initialize empty matrix by default" +) { + sut_type sut{}; + CHECK(matrix(sut).empty()); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "initialization with size parameters should properly initialize the matrix" +) { + sut_type sut(constants::n_vertices, constants::n_hyperedges); + CHECK_EQ(matrix(sut).size(), constants::n_vertices); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_hyperedges; + })); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return std::ranges::none_of(row, [](bool bit) { return bit; }); + })); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "add_vertices should properly extend the matrix" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + const auto initial_size = matrix(sut).size(); + + sut.add_vertices(2uz); + + CHECK_EQ(matrix(sut).size(), initial_size + 2uz); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_hyperedges; + })); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "remove_vertex should properly remove the row and implicitly shift vertex IDs" +) { + constexpr hgl::size_type n_vertices = 5uz, n_hyperedges = 1uz; + constexpr auto hyperedge_id = constants::id1; + + sut_type sut{n_vertices, n_hyperedges}; + sut.bind(constants::id2, hyperedge_id); + sut.bind(constants::id4, hyperedge_id); + + hgl::default_id_type rem_vid; + hgl::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(matrix(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_undirected_vertex_major_flat_incidence_matrix, + "incident_hyperedges should return a view of the vertex's incident hyperedge ids" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto vertex_id = constants::id1; + REQUIRE(std::ranges::empty(sut.incident_hyperedges(vertex_id))); + + for (const auto hyperedge_id : constants::hyperedge_ids_view) + sut.bind(vertex_id, hyperedge_id); + + CHECK(std::ranges::equal(sut.incident_hyperedges(vertex_id), constants::hyperedge_ids_view)); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "degree should return the number of the vertex's incident hyperedges" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto vertex_id = constants::id1; + REQUIRE_EQ(sut.degree(vertex_id), 0uz); + + for (const auto hyperedge_id : constants::hyperedge_ids_view) + sut.bind(vertex_id, hyperedge_id); + + CHECK_EQ(sut.degree(vertex_id), constants::n_hyperedges); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "add_hyperedges should properly extend the hypergraph matrix (resize columns)" +) { + sut_type sut{constants::n_vertices, 0uz}; + REQUIRE_EQ(matrix(sut).size(), constants::n_vertices); + REQUIRE(std::ranges::all_of(matrix(sut), [](const auto& row) { return row.size() == 0uz; })); + + sut.add_hyperedges(constants::n_hyperedges); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_hyperedges; + })); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "remove_hyperedge should properly erase the proper hyperedge matrix entries (column)" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.remove_hyperedge(constants::id1); + + CHECK_EQ(matrix(sut).size(), constants::n_vertices); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_hyperedges - 1uz; + })); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "remove_hyperedge should properly remove the column and implicitly shift hyperedge IDs" +) { + constexpr hgl::size_type n_vertices = 1uz, n_hyperedges = 5uz; + constexpr auto vertex_id = constants::id1; + + sut_type sut{n_vertices, n_hyperedges}; + sut.bind(vertex_id, constants::id2); + sut.bind(vertex_id, constants::id4); + + hgl::default_id_type rem_eid; + hgl::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(matrix(sut)[vertex_id].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_undirected_vertex_major_flat_incidence_matrix, + "incident_vertices should return 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.incident_vertices(hyperedge_id)); + })); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, "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_vertex_major_flat_incidence_matrix, "bind should set the corresponding bit" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.incident_vertices(constants::id1))); + + sut.bind(constants::id1, constants::id1); + + CHECK(matrix(sut)[constants::id1][constants::id1]); + + const auto vertices1 = sut.incident_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)); + + sut.bind(constants::id1, constants::id1); + const auto vertices2 = sut.incident_vertices(constants::id1) | std::ranges::to(); + CHECK_EQ(std::ranges::size(vertices2), 1uz); + CHECK(std::ranges::equal(vertices2, vertices1)); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, "unbind should clear the corresponding bit" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.bind(constants::id1, constants::id1); + REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz); + REQUIRE(matrix(sut)[constants::id1][constants::id1] == true); + + 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))); + CHECK(matrix(sut)[constants::id1][constants::id1] == false); +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "are_bound should return true only when the bit is set" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.bind(constants::id1, 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_vertex_major_flat_incidence_matrix, + "element size map getters should return maps of properly calculated element sizes" +) { + constexpr auto n_elements = 5uz; + sut_type sut{n_elements, n_elements}; + + constexpr auto is_zero = [](const auto& size) { return size == 0uz; }; + REQUIRE(std::ranges::all_of(sut.degree_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.hyperedge_size_map(n_elements), is_zero)); + + for (auto i = 0u; i < n_elements; i++) + for (auto j = 0u; j <= i; j++) + sut.bind(i, j); + + const auto deg_map = sut.degree_map(n_elements); + const auto esize_map = sut.hyperedge_size_map(n_elements); + for (auto i = 0uz; i < n_elements; i++) { + CHECK_EQ(deg_map[i], i + 1uz); + CHECK_EQ(esize_map[i], n_elements - i); + } +} + +TEST_CASE_FIXTURE( + test_undirected_vertex_major_flat_incidence_matrix, + "equality operator should correctly compare undirected incidence matrices" +) { + sut_type sut1{constants::n_vertices, constants::n_hyperedges}; + sut1.bind(constants::id1, constants::id1); + sut1.bind(constants::id2, constants::id1); + + SUBCASE("identical matrices are equal") { + const sut_type sut2 = sut1; + CHECK_EQ(sut1, sut2); + } + + SUBCASE("matrices with different bindings are not equal") { + sut_type sut2 = sut1; + sut2.bind(constants::id3, constants::id1); + CHECK_NE(sut1, sut2); + } + + SUBCASE("matrices with different dimensions are not equal") { + sut_type sut2{constants::n_vertices + 1uz, constants::n_hyperedges}; + sut2.bind(constants::id1, constants::id1); + sut2.bind(constants::id2, constants::id1); + CHECK_NE(sut1, sut2); + } +} + +struct test_undirected_hyperedge_major_flat_incidence_matrix : public test_flat_incidence_matrix { + using impl_tag = hgl::impl::flat_matrix_t; + using sut_type = hgl::impl::flat_incidence_matrix; +}; + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "should initialize empty matrix by default" +) { + sut_type sut{}; + CHECK(matrix(sut).empty()); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "initialization with size parameters should properly initialize the matrix" +) { + sut_type sut(constants::n_vertices, constants::n_hyperedges); + CHECK_EQ(matrix(sut).size(), constants::n_hyperedges); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_vertices; + })); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return std::ranges::none_of(row, [](bool bit) { return bit; }); + })); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "add_vertices should properly extend the hypergraph matrix (resize columns)" +) { + sut_type sut{0uz, constants::n_hyperedges}; + REQUIRE_EQ(matrix(sut).size(), constants::n_hyperedges); + REQUIRE(std::ranges::all_of(matrix(sut), [](const auto& row) { return row.size() == 0uz; })); + + sut.add_vertices(constants::n_vertices); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_vertices; + })); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "remove_vertex should properly erase the proper vertex matrix entries (column)" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.remove_vertex(constants::id1); + + CHECK_EQ(matrix(sut).size(), constants::n_hyperedges); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_vertices - 1uz; + })); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "remove_vertex should properly remove the column and implicitly shift vertex IDs" +) { + constexpr hgl::size_type n_vertices = 5uz, n_hyperedges = 1uz; + constexpr auto hyperedge_id = constants::id1; + + sut_type sut{n_vertices, n_hyperedges}; + sut.bind(constants::id2, hyperedge_id); + sut.bind(constants::id4, hyperedge_id); + + hgl::default_id_type rem_vid; + hgl::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(matrix(sut)[hyperedge_id].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_undirected_hyperedge_major_flat_incidence_matrix, + "incident_hyperedges should return a view of the vertex's incident hyperedge ids" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto vertex_id = constants::id1; + REQUIRE(std::ranges::empty(sut.incident_hyperedges(vertex_id))); + + for (const auto hyperedge_id : constants::hyperedge_ids_view) + sut.bind(vertex_id, hyperedge_id); + + CHECK(std::ranges::equal(sut.incident_hyperedges(vertex_id), constants::hyperedge_ids_view)); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "degree should return the number of the vertex's incident hyperedges" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + constexpr auto vertex_id = constants::id1; + REQUIRE_EQ(sut.degree(vertex_id), 0uz); + + for (const auto hyperedge_id : constants::hyperedge_ids_view) + sut.bind(vertex_id, hyperedge_id); + + CHECK_EQ(sut.degree(vertex_id), constants::n_hyperedges); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "add_hyperedges should properly extend matrix" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + const auto initial_size = matrix(sut).size(); + + sut.add_hyperedges(2uz); + + CHECK_EQ(matrix(sut).size(), initial_size + 2uz); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_vertices; + })); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "remove_hyperedge should properly remove the row and implicitly shift hyperedge IDs" +) { + constexpr hgl::size_type n_vertices = 1uz, n_hyperedges = 5uz; + constexpr auto vertex_id = constants::id1; + + sut_type sut{n_vertices, n_hyperedges}; + sut.bind(vertex_id, constants::id2); + sut.bind(vertex_id, constants::id4); + + hgl::default_id_type rem_eid; + hgl::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(matrix(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_undirected_hyperedge_major_flat_incidence_matrix, + "incident_vertices should return 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.incident_vertices(hyperedge_id)); + })); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "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_major_flat_incidence_matrix, "bind should set the corresponding bit" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + REQUIRE(std::ranges::empty(sut.incident_vertices(constants::id1))); + + sut.bind(constants::id1, constants::id1); + + CHECK(matrix(sut)[constants::id1][constants::id1]); + + const auto vertices1 = sut.incident_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)); + + sut.bind(constants::id1, constants::id1); + const auto vertices2 = sut.incident_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_major_flat_incidence_matrix, + "unbind should clear the corresponding bit" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.bind(constants::id1, constants::id1); + REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz); + REQUIRE(matrix(sut)[constants::id1][constants::id1] == true); + + 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))); + CHECK(matrix(sut)[constants::id1][constants::id1] == false); +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "are_bound should return true only when the bit is set" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.bind(constants::id1, 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_major_flat_incidence_matrix, + "element size map getters should return maps of properly calculated element sizes" +) { + constexpr auto n_elements = 5uz; + sut_type sut{n_elements, n_elements}; + + constexpr auto is_zero = [](const auto& size) { return size == 0uz; }; + REQUIRE(std::ranges::all_of(sut.degree_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.hyperedge_size_map(n_elements), is_zero)); + + for (auto i = 0u; i < n_elements; i++) + for (auto j = 0u; j <= i; j++) + sut.bind(i, j); + + const auto deg_map = sut.degree_map(n_elements); + const auto esize_map = sut.hyperedge_size_map(n_elements); + for (auto i = 0uz; i < n_elements; i++) { + CHECK_EQ(deg_map[i], i + 1uz); + CHECK_EQ(esize_map[i], n_elements - i); + } +} + +TEST_CASE_FIXTURE( + test_undirected_hyperedge_major_flat_incidence_matrix, + "equality operator should correctly compare undirected incidence matrices" +) { + sut_type sut1{constants::n_vertices, constants::n_hyperedges}; + sut1.bind(constants::id1, constants::id1); + sut1.bind(constants::id2, constants::id1); + + SUBCASE("identical matrices are equal") { + const sut_type sut2 = sut1; + CHECK_EQ(sut1, sut2); + } + + SUBCASE("matrices with different bindings are not equal") { + sut_type sut2 = sut1; + sut2.bind(constants::id3, constants::id1); + CHECK_NE(sut1, sut2); + } + + SUBCASE("matrices with different dimensions are not equal") { + sut_type sut2{constants::n_vertices, constants::n_hyperedges + 1uz}; + sut2.bind(constants::id1, constants::id1); + sut2.bind(constants::id2, constants::id1); + CHECK_NE(sut1, sut2); + } +} + +struct test_bf_directed_flat_incidence_matrix : public test_flat_incidence_matrix { + auto altbind_to_vertex( + auto& sut, const hgl::default_id_type vertex_id, const hgl::size_type n_hyperedges + ) { + std::vector tail_bound, head_bound; + + for (auto i = 0u; i < n_hyperedges; ++i) { + if (i % 2u == 0u) { + 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::default_id_type hyperedge_id, const hgl::size_type n_vertices + ) { + std::vector tail_bound, head_bound; + + for (auto i = 0u; i < n_vertices; ++i) { + if (i % 2u == 0u) { + 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)); + } +}; + +struct test_bf_directed_vertex_major_flat_incidence_matrix +: public test_bf_directed_flat_incidence_matrix { + using impl_tag = hgl::impl::flat_matrix_t; + using sut_type = hgl::impl::flat_incidence_matrix; + using incidence_type = hgl::impl::bf_incidence; +}; + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, "should initialize empty matrix by default" +) { + sut_type sut{}; + CHECK(matrix(sut).empty()); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "initialization with size parameters should properly initialize the matrix" +) { + sut_type sut(constants::n_vertices, constants::n_hyperedges); + CHECK_EQ(matrix(sut).size(), constants::n_vertices); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_hyperedges; + })); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return std::ranges::none_of(row, hgl::impl::bf_is_incident); + })); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "add_vertices should properly extend the matrix" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + const auto initial_size = matrix(sut).size(); + + sut.add_vertices(2uz); + + CHECK_EQ(matrix(sut).size(), initial_size + 2uz); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_hyperedges; + })); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "remove_vertex should properly remove the row and implicitly shift vertex IDs" +) { + constexpr hgl::size_type n_vertices = 5uz, n_hyperedges = 1uz; + constexpr auto 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::default_id_type rem_vid; + hgl::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 - 1u, constants::id4 - 1u}; + } + + SUBCASE("present vertex = first incident vertex") { + rem_vid = constants::id2; + expected_hyperedge_size = 1uz; + expected_vertices = {constants::id4 - 1u}; + } + + SUBCASE("not present vertex > first incident vertex") { + rem_vid = constants::id3; + expected_hyperedge_size = 2uz; + expected_vertices = {constants::id2, constants::id4 - 1u}; + } + + 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 + 1u; + 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(matrix(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_flat_incidence_matrix, + "incident_hyperedges should return a view of the vertex's incident hyperedge ids," + "out_hyperedges should return a view of the vertex's outgoing hyperedge ids (v in T(e))," + "in_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::equal(sut.incident_hyperedges(vertex_id), constants::hyperedge_ids_view)); + CHECK(std::ranges::equal(sut.out_hyperedges(vertex_id), tail_bound_hyperedges)); + CHECK(std::ranges::equal(sut.in_hyperedges(vertex_id), head_bound_hyperedges)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "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_flat_incidence_matrix, + "add_hyperedges should properly extend the hypergraph matrix (resize columns)" +) { + sut_type sut{constants::n_vertices, 0uz}; + REQUIRE_EQ(matrix(sut).size(), constants::n_vertices); + REQUIRE(std::ranges::all_of(matrix(sut), [](const auto& row) { return row.size() == 0uz; })); + + sut.add_hyperedges(constants::n_hyperedges); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_hyperedges; + })); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "remove_hyperedge should properly erase the proper hyperedge matrix entries (column)" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.remove_hyperedge(constants::id1); + + CHECK_EQ(matrix(sut).size(), constants::n_vertices); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_hyperedges - 1uz; + })); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "remove_hyperedge should properly remove the column and implicitly shift hyperedge IDs" +) { + constexpr hgl::size_type n_vertices = 1uz, n_hyperedges = 5uz; + constexpr auto 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::default_id_type rem_eid; + hgl::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(matrix(sut)[vertex_id].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_flat_incidence_matrix, + "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_flat_incidence_matrix, + "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_flat_incidence_matrix, + "bind_tail should set the corresponding matrix entry to backward incidence" +) { + 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); + + CHECK_EQ(matrix(sut)[constants::id1][constants::id1], incidence_type::backward); + + 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_flat_incidence_matrix, + "bind_head should set the corresponding matrix entry to forward incidence" +) { + 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); + + CHECK_EQ(matrix(sut)[constants::id1][constants::id1], incidence_type::forward); + + 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_flat_incidence_matrix, "unbind should clear the corresponding bit" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + SUBCASE("tail bound") { + sut.bind_tail(constants::id1, constants::id1); + } + SUBCASE("head bound") { + sut.bind_head(constants::id1, constants::id1); + } + CAPTURE(sut); + + REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz); + REQUIRE_NE(matrix(sut)[constants::id1][constants::id1], incidence_type::none); + + 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))); + CHECK_EQ(matrix(sut)[constants::id1][constants::id1], incidence_type::none); +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "are_bound, is_tail, is_head should return true only when the corresponding matrix entry is " + "set to a valid, matching incidence type" +) { + 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_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "element size map getters should return maps of properly calculated element sizes" +) { + constexpr auto n_elements = 5uz; + sut_type sut{n_elements, n_elements}; + + constexpr auto is_zero = [](const auto& size) { return size == 0uz; }; + REQUIRE(std::ranges::all_of(sut.degree_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.out_degree_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.in_degree_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.hyperedge_size_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.tail_size_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.head_size_map(n_elements), is_zero)); + + SUBCASE("tail bind") { + for (auto i = 0u; i < n_elements; i++) + for (auto j = 0u; j <= i; j++) + sut.bind_tail(i, j); + + const auto deg_map = sut.degree_map(n_elements); + const auto out_deg_map = sut.out_degree_map(n_elements); + const auto esize_map = sut.hyperedge_size_map(n_elements); + const auto tsize_map = sut.tail_size_map(n_elements); + for (auto i = 0uz; i < n_elements; i++) { + CHECK_EQ(deg_map[i], i + 1uz); + CHECK_EQ(out_deg_map[i], i + 1uz); + CHECK_EQ(esize_map[i], n_elements - i); + CHECK_EQ(tsize_map[i], n_elements - i); + } + CHECK(std::ranges::all_of(sut.in_degree_map(n_elements), is_zero)); + CHECK(std::ranges::all_of(sut.head_size_map(n_elements), is_zero)); + } + + SUBCASE("head bind") { + for (auto i = 0u; i < n_elements; i++) + for (auto j = 0u; j <= i; j++) + sut.bind_head(i, j); + + const auto deg_map = sut.degree_map(n_elements); + const auto in_deg_map = sut.in_degree_map(n_elements); + const auto esize_map = sut.hyperedge_size_map(n_elements); + const auto hsize_map = sut.head_size_map(n_elements); + for (auto i = 0uz; i < n_elements; i++) { + CHECK_EQ(deg_map[i], i + 1uz); + CHECK_EQ(in_deg_map[i], i + 1uz); + CHECK_EQ(esize_map[i], n_elements - i); + CHECK_EQ(hsize_map[i], n_elements - i); + } + CHECK(std::ranges::all_of(sut.out_degree_map(n_elements), is_zero)); + CHECK(std::ranges::all_of(sut.tail_size_map(n_elements), is_zero)); + } + + // diagonal = tail, everything else is head + for (auto i = 0u; i < n_elements; i++) { + for (auto j = 0u; j <= i; j++) { + if (i == j) + sut.bind_tail(i, j); + else + sut.bind_head(i, j); + } + } + + const auto deg_map = sut.degree_map(n_elements); + const auto out_deg_map = sut.out_degree_map(n_elements); + const auto in_deg_map = sut.in_degree_map(n_elements); + + const auto esize_map = sut.hyperedge_size_map(n_elements); + const auto tsize_map = sut.tail_size_map(n_elements); + const auto hsize_map = sut.head_size_map(n_elements); + + for (auto i = 0uz; i < n_elements; i++) { + CHECK_EQ(deg_map[i], i + 1uz); + CHECK_EQ(out_deg_map[i], 1uz); + CHECK_EQ(in_deg_map[i], i); + + CHECK_EQ(esize_map[i], n_elements - i); + CHECK_EQ(tsize_map[i], 1uz); + CHECK_EQ(hsize_map[i], n_elements - i - 1uz); + } +} + +TEST_CASE_FIXTURE( + test_bf_directed_vertex_major_flat_incidence_matrix, + "equality operator should correctly compare directed incidence matrices" +) { + sut_type sut1{constants::n_vertices, constants::n_hyperedges}; + sut1.bind_tail(constants::id1, constants::id1); + sut1.bind_head(constants::id2, constants::id1); + + SUBCASE("identical matrices are equal") { + const sut_type sut2 = sut1; + CHECK_EQ(sut1, sut2); + } + + SUBCASE("matrices with different bindings are not equal") { + sut_type sut2 = sut1; + sut2.bind_tail(constants::id3, constants::id1); + CHECK_NE(sut1, sut2); + } + + SUBCASE("matrices with swapped tail/head bindings are not equal") { + sut_type sut2{constants::n_vertices, constants::n_hyperedges}; + sut2.bind_head(constants::id1, constants::id1); + sut2.bind_tail(constants::id2, constants::id1); + CHECK_NE(sut1, sut2); + } + + SUBCASE("matrices with different dimensions are not equal") { + sut_type sut2{constants::n_vertices + 1uz, constants::n_hyperedges}; + sut2.bind_tail(constants::id1, constants::id1); + sut2.bind_head(constants::id2, constants::id1); + CHECK_NE(sut1, sut2); + } +} + +struct test_bf_directed_hyperedge_major_flat_incidence_matrix +: public test_bf_directed_flat_incidence_matrix { + using impl_tag = hgl::impl::flat_matrix_t; + using sut_type = hgl::impl::flat_incidence_matrix; + using incidence_type = hgl::impl::bf_incidence; +}; + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "should initialize empty matrix by default" +) { + sut_type sut{}; + CHECK(matrix(sut).empty()); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "initialization with size parameters should properly initialize the matrix" +) { + sut_type sut(constants::n_vertices, constants::n_hyperedges); + CHECK_EQ(matrix(sut).size(), constants::n_hyperedges); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_vertices; + })); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return std::ranges::none_of(row, hgl::impl::bf_is_incident); + })); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "add_vertices should properly extend the hypergraph matrix (resize columns)" +) { + sut_type sut{0uz, constants::n_hyperedges}; + REQUIRE_EQ(matrix(sut).size(), constants::n_hyperedges); + REQUIRE(std::ranges::all_of(matrix(sut), [](const auto& row) { return row.size() == 0uz; })); + + sut.add_vertices(constants::n_vertices); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_vertices; + })); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "remove_vertex should properly erase the proper vertex matrix entries (column)" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + sut.remove_vertex(constants::id1); + + CHECK_EQ(matrix(sut).size(), constants::n_hyperedges); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_vertices - 1uz; + })); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "remove_vertex should properly remove the column and implicitly shift vertex IDs" +) { + constexpr hgl::size_type n_vertices = 5uz, n_hyperedges = 1uz; + constexpr auto 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::default_id_type rem_vid; + hgl::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 - 1u, constants::id4 - 1u}; + } + + SUBCASE("present vertex = first incident vertex") { + rem_vid = constants::id2; + expected_hyperedge_size = 1uz; + expected_vertices = {constants::id4 - 1u}; + } + + SUBCASE("not present vertex > first incident vertex") { + rem_vid = constants::id3; + expected_hyperedge_size = 2uz; + expected_vertices = {constants::id2, constants::id4 - 1u}; + } + + 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 + 1u; + 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(matrix(sut)[hyperedge_id].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_hyperedge_major_flat_incidence_matrix, + "incident_hyperedges should return a view of the vertex's incident hyperedge ids," + "out_hyperedges should return a view of the vertex's outgoing hyperedge ids (v in T(e))," + "in_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::equal(sut.incident_hyperedges(vertex_id), constants::hyperedge_ids_view)); + CHECK(std::ranges::equal(sut.out_hyperedges(vertex_id), tail_bound_hyperedges)); + CHECK(std::ranges::equal(sut.in_hyperedges(vertex_id), head_bound_hyperedges)); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "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_flat_incidence_matrix, + "add_hyperedges should properly extend matrix" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + const auto initial_size = matrix(sut).size(); + + sut.add_hyperedges(2uz); + + CHECK_EQ(matrix(sut).size(), initial_size + 2uz); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return row.size() == constants::n_vertices; + })); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "remove_hyperedge should properly remove the row and implicitly shift hyperedge IDs" +) { + constexpr hgl::size_type n_vertices = 1uz, n_hyperedges = 5uz; + constexpr auto 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::default_id_type rem_eid; + hgl::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(matrix(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_hyperedge_major_flat_incidence_matrix, + "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_hyperedge_major_flat_incidence_matrix, + "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_hyperedge_major_flat_incidence_matrix, + "bind_tail should set the corresponding matrix entry to backward incidence" +) { + 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); + + CHECK_EQ(matrix(sut)[constants::id1][constants::id1], incidence_type::backward); + + 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_hyperedge_major_flat_incidence_matrix, + "bind_head should set the corresponding matrix entry to forward incidence" +) { + 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); + + CHECK_EQ(matrix(sut)[constants::id1][constants::id1], incidence_type::forward); + + 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_hyperedge_major_flat_incidence_matrix, + "unbind should clear the corresponding bit" +) { + sut_type sut{constants::n_vertices, constants::n_hyperedges}; + + SUBCASE("tail bound") { + sut.bind_tail(constants::id1, constants::id1); + } + SUBCASE("head bound") { + sut.bind_head(constants::id1, constants::id1); + } + CAPTURE(sut); + + REQUIRE_EQ(sut.hyperedge_size(constants::id1), 1uz); + REQUIRE_NE(matrix(sut)[constants::id1][constants::id1], incidence_type::none); + + 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))); + CHECK_EQ(matrix(sut)[constants::id1][constants::id1], incidence_type::none); +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "are_bound, is_tail, is_head should return true only when the corresponding matrix entry is " + "set to a valid, matching incidence type" +) { + 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_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "element size map getters should return maps of properly calculated element sizes" +) { + constexpr auto n_elements = 5uz; + sut_type sut{n_elements, n_elements}; + + constexpr auto is_zero = [](const auto& size) { return size == 0uz; }; + REQUIRE(std::ranges::all_of(sut.degree_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.out_degree_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.in_degree_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.hyperedge_size_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.tail_size_map(n_elements), is_zero)); + REQUIRE(std::ranges::all_of(sut.head_size_map(n_elements), is_zero)); + + SUBCASE("tail bind") { + for (auto i = 0u; i < n_elements; i++) + for (auto j = 0u; j <= i; j++) + sut.bind_tail(i, j); + + const auto deg_map = sut.degree_map(n_elements); + const auto out_deg_map = sut.out_degree_map(n_elements); + const auto esize_map = sut.hyperedge_size_map(n_elements); + const auto tsize_map = sut.tail_size_map(n_elements); + for (auto i = 0uz; i < n_elements; i++) { + CHECK_EQ(deg_map[i], i + 1uz); + CHECK_EQ(out_deg_map[i], i + 1uz); + CHECK_EQ(esize_map[i], n_elements - i); + CHECK_EQ(tsize_map[i], n_elements - i); + } + CHECK(std::ranges::all_of(sut.in_degree_map(n_elements), is_zero)); + CHECK(std::ranges::all_of(sut.head_size_map(n_elements), is_zero)); + } + + SUBCASE("head bind") { + for (auto i = 0u; i < n_elements; i++) + for (auto j = 0u; j <= i; j++) + sut.bind_head(i, j); + + const auto deg_map = sut.degree_map(n_elements); + const auto in_deg_map = sut.in_degree_map(n_elements); + const auto esize_map = sut.hyperedge_size_map(n_elements); + const auto hsize_map = sut.head_size_map(n_elements); + for (auto i = 0uz; i < n_elements; i++) { + CHECK_EQ(deg_map[i], i + 1uz); + CHECK_EQ(in_deg_map[i], i + 1uz); + CHECK_EQ(esize_map[i], n_elements - i); + CHECK_EQ(hsize_map[i], n_elements - i); + } + CHECK(std::ranges::all_of(sut.out_degree_map(n_elements), is_zero)); + CHECK(std::ranges::all_of(sut.tail_size_map(n_elements), is_zero)); + } + + // diagonal = tail, everything else is head + for (auto i = 0u; i < n_elements; i++) { + for (auto j = 0u; j <= i; j++) { + if (i == j) + sut.bind_tail(i, j); + else + sut.bind_head(i, j); + } + } + + const auto deg_map = sut.degree_map(n_elements); + const auto out_deg_map = sut.out_degree_map(n_elements); + const auto in_deg_map = sut.in_degree_map(n_elements); + + const auto esize_map = sut.hyperedge_size_map(n_elements); + const auto tsize_map = sut.tail_size_map(n_elements); + const auto hsize_map = sut.head_size_map(n_elements); + + for (auto i = 0uz; i < n_elements; i++) { + CHECK_EQ(deg_map[i], i + 1uz); + CHECK_EQ(out_deg_map[i], 1uz); + CHECK_EQ(in_deg_map[i], i); + + CHECK_EQ(esize_map[i], n_elements - i); + CHECK_EQ(tsize_map[i], 1uz); + CHECK_EQ(hsize_map[i], n_elements - i - 1uz); + } +} + +TEST_CASE_FIXTURE( + test_bf_directed_hyperedge_major_flat_incidence_matrix, + "equality operator should correctly compare directed incidence matrices" +) { + sut_type sut1{constants::n_vertices, constants::n_hyperedges}; + sut1.bind_tail(constants::id1, constants::id1); + sut1.bind_head(constants::id2, constants::id1); + + SUBCASE("identical matrices are equal") { + const sut_type sut2 = sut1; + CHECK_EQ(sut1, sut2); + } + + SUBCASE("matrices with different bindings are not equal") { + sut_type sut2 = sut1; + sut2.bind_tail(constants::id3, constants::id1); + CHECK_NE(sut1, sut2); + } + + SUBCASE("matrices with swapped tail/head bindings are not equal") { + sut_type sut2{constants::n_vertices, constants::n_hyperedges}; + sut2.bind_head(constants::id1, constants::id1); + sut2.bind_tail(constants::id2, constants::id1); + CHECK_NE(sut1, sut2); + } + + SUBCASE("matrices with different dimensions are not equal") { + sut_type sut2{constants::n_vertices, constants::n_hyperedges + 1uz}; + sut2.bind_tail(constants::id1, constants::id1); + sut2.bind_head(constants::id2, constants::id1); + CHECK_NE(sut1, sut2); + } +} + +TEST_SUITE_END(); // test_flat_incidence_matrix + +} // namespace hgl_testing diff --git a/tests/source/hgl/test_hypergraph.cpp b/tests/source/hgl/test_hypergraph.cpp index 5602fc1..987fe15 100644 --- a/tests/source/hgl/test_hypergraph.cpp +++ b/tests/source/hgl/test_hypergraph.cpp @@ -391,6 +391,8 @@ TEST_CASE_TEMPLATE_DEFINE( SUBCASE("bind, unbind and are_incident should throw if either of the fiven elements is invalid" ) { + GL_SUPPRESS_WARNING_BEGIN("-Warray-bounds"); + sut_type sut{constants::n_vertices, constants::n_hyperedges}; if constexpr (std::same_as) { @@ -474,6 +476,8 @@ TEST_CASE_TEMPLATE_DEFINE( std::out_of_range ); } + + GL_SUPPRESS_WARNING_END; } SUBCASE("are_incident should return false by default") { @@ -944,6 +948,12 @@ TEST_CASE_TEMPLATE_INSTANTIATE( hgl::matrix_hypergraph_traits< hgl::impl::vertex_major_t, hgl::undirected_t>, // undirected vertex-major incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::hyperedge_major_t, + hgl::undirected_t>, // undirected hyperedge-major flat incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::vertex_major_t, + hgl::undirected_t>, // undirected vertex-major flat incidence matrix hgl::list_hypergraph_traits< hgl::impl::bidirectional_t, hgl::bf_directed_t>, // bf-directed bidirectional incidence list @@ -967,7 +977,13 @@ TEST_CASE_TEMPLATE_INSTANTIATE( hgl::bf_directed_t>, // bf-directed hyperedge-major incidence matrix hgl::matrix_hypergraph_traits< hgl::impl::vertex_major_t, - hgl::bf_directed_t> // bf-directed vertex-major incidence matrix + hgl::bf_directed_t>, // bf-directed vertex-major incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::hyperedge_major_t, + hgl::bf_directed_t>, // bf-directed hyperedge-major flat incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::vertex_major_t, + hgl::bf_directed_t> // bf-directed vertex-major flat incidence matrix ); TEST_CASE_TEMPLATE_DEFINE( @@ -1129,7 +1145,17 @@ TEST_CASE_TEMPLATE_INSTANTIATE( hgl::impl::vertex_major_t, hgl::undirected_t, hgl::name_property, - hgl::name_property> // undirected vertex-major incidence matrix + hgl::name_property>, // undirected vertex-major incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::hyperedge_major_t, + hgl::undirected_t, + hgl::name_property, + hgl::name_property>, // undirected hyperedge-major flat incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::vertex_major_t, + hgl::undirected_t, + hgl::name_property, + hgl::name_property> // undirected vertex-major flat incidence matrix ); TEST_CASE_TEMPLATE_DEFINE( @@ -1365,7 +1391,17 @@ TEST_CASE_TEMPLATE_INSTANTIATE( hgl::impl::vertex_major_t, hgl::bf_directed_t, hgl::name_property, - hgl::name_property> // bf-directed vertex-major incidence matrix + hgl::name_property>, // bf-directed vertex-major incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::hyperedge_major_t, + hgl::bf_directed_t, + hgl::name_property, + hgl::name_property>, // bf-directed hyperedge-major flat incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::vertex_major_t, + hgl::bf_directed_t, + hgl::name_property, + hgl::name_property> // bf-directed vertex-major flat incidence matrix ); TEST_CASE_TEMPLATE_DEFINE( @@ -1566,6 +1602,12 @@ TEST_CASE_TEMPLATE_INSTANTIATE( hgl::matrix_hypergraph_traits< hgl::impl::vertex_major_t, hgl::undirected_t>, // undirected vertex-major incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::hyperedge_major_t, + hgl::undirected_t>, // undirected hyperedge-major flat incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::vertex_major_t, + hgl::undirected_t>, // undirected vertex-major flat incidence matrix hgl::list_hypergraph_traits< hgl::impl::bidirectional_t, hgl::bf_directed_t>, // bf-directed bidirectional incidence list @@ -1589,7 +1631,13 @@ TEST_CASE_TEMPLATE_INSTANTIATE( hgl::bf_directed_t>, // bf-directed hyperedge-major incidence matrix hgl::matrix_hypergraph_traits< hgl::impl::vertex_major_t, - hgl::bf_directed_t> // bf-directed vertex-major incidence matrix + hgl::bf_directed_t>, // bf-directed vertex-major incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::hyperedge_major_t, + hgl::bf_directed_t>, // bf-directed hyperedge-major flat incidence matrix + hgl::flat_matrix_hypergraph_traits< + hgl::impl::vertex_major_t, + hgl::bf_directed_t> // bf-directed vertex-major flat incidence matrix ); TEST_SUITE_END(); // test_hypergraph diff --git a/tests/source/hgl/test_incidence_matrix.cpp b/tests/source/hgl/test_incidence_matrix.cpp index 45dd558..76df652 100644 --- a/tests/source/hgl/test_incidence_matrix.cpp +++ b/tests/source/hgl/test_incidence_matrix.cpp @@ -1,3 +1,4 @@ +#include "hgl/impl/bf_incidence.hpp" #include "hgl/impl/layout_tags.hpp" #include "testing/hgl/constants.hpp" @@ -18,17 +19,6 @@ struct test_incidence_matrix { typename IncidenceMatrix::hypergraph_storage_type& matrix(IncidenceMatrix& sut) const noexcept { return sut._matrix; } - - template - struct incidence_descriptor { - using type = std::conditional_t< - std::same_as, - typename IncidenceMatrix::incidence_type, - bool>; - }; - - template - using incidence_descriptor_type = typename incidence_descriptor::type; }; struct test_undirected_vertex_major_incidence_matrix : public test_incidence_matrix { @@ -688,12 +678,6 @@ TEST_CASE_FIXTURE( } struct test_bf_directed_incidence_matrix : public test_incidence_matrix { - template - auto is_incident_pred() { - using incidence_type = incidence_descriptor_type; - return [](const incidence_type t) { return t != incidence_type::none; }; - } - auto altbind_to_vertex( auto& sut, const hgl::default_id_type vertex_id, const hgl::size_type n_hyperedges ) { @@ -736,7 +720,7 @@ struct test_bf_directed_incidence_matrix : public test_incidence_matrix { struct test_bf_directed_vertex_major_incidence_matrix : public test_bf_directed_incidence_matrix { using impl_tag = hgl::impl::matrix_t; using sut_type = hgl::impl::incidence_matrix; - using incidence_type = incidence_descriptor_type; + using incidence_type = hgl::impl::bf_incidence; }; TEST_CASE_FIXTURE( @@ -755,8 +739,8 @@ TEST_CASE_FIXTURE( CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { return row.size() == constants::n_hyperedges; })); - CHECK(std::ranges::all_of(matrix(sut), [this](const auto& row) { - return std::ranges::none_of(row, is_incident_pred()); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return std::ranges::none_of(row, hgl::impl::bf_is_incident); })); } @@ -1189,7 +1173,7 @@ struct test_bf_directed_hyperedge_major_incidence_matrix : public test_bf_directed_incidence_matrix { using impl_tag = hgl::impl::matrix_t; using sut_type = hgl::impl::incidence_matrix; - using incidence_type = incidence_descriptor_type; + using incidence_type = hgl::impl::bf_incidence; }; TEST_CASE_FIXTURE( @@ -1208,8 +1192,8 @@ TEST_CASE_FIXTURE( CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { return row.size() == constants::n_vertices; })); - CHECK(std::ranges::all_of(matrix(sut), [this](const auto& row) { - return std::ranges::none_of(row, is_incident_pred()); + CHECK(std::ranges::all_of(matrix(sut), [](const auto& row) { + return std::ranges::none_of(row, hgl::impl::bf_is_incident); })); }