From 87235313552602e8a4ee3c4f0a3c28205292d490 Mon Sep 17 00:00:00 2001 From: Matthias Schimek Date: Wed, 5 Nov 2025 22:35:58 +0100 Subject: [PATCH 1/6] edge range unifying access to a graph's edges --- kagen/edge_range.cpp | 201 +++++++++++++++++++++++++++++++++++++++++++ kagen/edge_range.h | 81 +++++++++++++++++ tests/CMakeLists.txt | 3 + tests/edge_range.cpp | 108 +++++++++++++++++++++++ 4 files changed, 393 insertions(+) create mode 100644 kagen/edge_range.cpp create mode 100644 kagen/edge_range.h create mode 100644 tests/edge_range.cpp diff --git a/kagen/edge_range.cpp b/kagen/edge_range.cpp new file mode 100644 index 0000000..66a4f42 --- /dev/null +++ b/kagen/edge_range.cpp @@ -0,0 +1,201 @@ +#include "kagen/edge_range.h" + +#include +#include + +namespace kagen { + +EdgeRange::EdgeRange(const Edgelist& edgelist) noexcept + : representation_(GraphRepresentation::EDGE_LIST), + edgelist_ptr_(std::addressof(edgelist)), + xadj_ptr_(nullptr), + adjncy_ptr_(nullptr), + vertex_base_(Vertex(0)) {} + +EdgeRange::EdgeRange(const XadjArray& xadj, const AdjncyArray& adjncy, VertexRange vertex_range) noexcept + : representation_(GraphRepresentation::CSR), + edgelist_ptr_(nullptr), + xadj_ptr_(std::addressof(xadj)), + adjncy_ptr_(std::addressof(adjncy)), + vertex_base_(vertex_range.first) { + assert(xadj_ptr_->size() >= 1); +} + +EdgeRange::EdgeRange(const Graph& graph) noexcept : EdgeRange(FromGraph(graph)) {} + +EdgeRange EdgeRange::FromGraph(const Graph& g) noexcept { + if (g.representation == GraphRepresentation::EDGE_LIST) { + return EdgeRange(g.edges); + } else { + return EdgeRange(g.xadj, g.adjncy, g.vertex_range); + } +} + +EdgeRange::iterator EdgeRange::iterator::edgelist_begin(const EdgeRange* parent) noexcept { + iterator it; + it.parent_ = parent; + it.idx_ = 0; + it.load_current(); + return it; +} + +EdgeRange::iterator EdgeRange::iterator::edgelist_end(const EdgeRange* parent) noexcept { + iterator it; + it.parent_ = parent; + it.idx_ = parent->edgelist_ptr_->size(); + return it; +} + +EdgeRange::iterator EdgeRange::iterator::csr_begin(const EdgeRange* parent) noexcept { + iterator it; + it.parent_ = parent; + it.u_ = 0; + it.off_ = 0; + it.init_csr_begin(); // finds first valid (u,off) or sets to end + it.load_current(); + return it; +} + +EdgeRange::iterator EdgeRange::iterator::csr_end(const EdgeRange* parent) noexcept { + iterator it; + it.parent_ = parent; + it.u_ = parent->xadj_ptr_->size() == 0 ? 0u : parent->xadj_ptr_->size() - 1; + it.off_ = parent->adjncy_ptr_->size(); + return it; +} + +std::size_t EdgeRange::iterator::edge_index() const noexcept { + if (parent_->representation_ == GraphRepresentation::EDGE_LIST) { + return idx_; + } else { + return off_; + } +} + +EdgeRange::iterator::value_type EdgeRange::iterator::operator*() const noexcept { + return cur_; +} + +EdgeRange::iterator& EdgeRange::iterator::operator++() { + assert(parent_ != nullptr); + + if (parent_->representation_ == GraphRepresentation::EDGE_LIST) { + ++idx_; + load_current(); + return *this; + } + + advance_to_next_valid_csr(); + load_current(); + return *this; +} + +EdgeRange::iterator EdgeRange::iterator::operator++(int) { + iterator tmp = *this; + ++(*this); + return tmp; +} + +bool EdgeRange::iterator::operator==(const iterator& other) const noexcept { + // must belong to same parent to compare reliably + if (parent_ != other.parent_) + return false; + if (parent_ == nullptr) + return true; // both default constructed? + if (parent_->representation_ != other.parent_->representation_) + return false; + if (parent_->representation_ == GraphRepresentation::EDGE_LIST) { + return idx_ == other.idx_; + } else { + return (u_ == other.u_) && (off_ == other.off_); + } +} + +bool EdgeRange::iterator::operator!=(const iterator& other) const noexcept { + return !(*this == other); +} + +void EdgeRange::iterator::load_current() noexcept { + if (parent_->representation_ == GraphRepresentation::EDGE_LIST) { + const auto& elist = *parent_->edgelist_ptr_; + if (idx_ < elist.size()) { + cur_ = elist[idx_]; + } + // else leave cur_ unspecified for end() + } else { + const auto& xadj = *parent_->xadj_ptr_; + const auto& adjncy = *parent_->adjncy_ptr_; + const std::size_t n_local = xadj.empty() ? 0 : (xadj.size() - 1); + if (u_ >= n_local || off_ >= adjncy.size()) { + // end iterator; cur_ remains unspecified + return; + } + Vertex u_global = static_cast(u_) + parent_->vertex_base_; + Vertex v_global = adjncy[off_]; + cur_.first = u_global; + cur_.second = v_global; + } +} + +void EdgeRange::iterator::init_csr_begin() noexcept { + const auto& xadj = *parent_->xadj_ptr_; + const auto& adjncy = *parent_->adjncy_ptr_; + const std::size_t n_local = xadj.empty() ? 0 : (xadj.size() - 1); + if (n_local == 0) { + // empty CSR: set end + u_ = 0; + off_ = 0; + return; + } + off_ = xadj[0]; + u_ = 0; + // skip vertices with empty adjacency ranges + while (u_ < n_local && off_ >= xadj[u_ + 1]) { + ++u_; + if (u_ < n_local) + off_ = xadj[u_]; + } + if (u_ >= n_local) { + // set to end state + u_ = n_local; + off_ = adjncy.size(); + } +} + +void EdgeRange::iterator::advance_to_next_valid_csr() noexcept { + // CSR: advance off_, move to next vertex if necessary + ++off_; + const auto& xadj = *parent_->xadj_ptr_; + const std::size_t n_local = xadj.empty() ? 0 : (xadj.size() - 1); + while (u_ < n_local && off_ >= xadj[u_ + 1]) { + ++u_; + if (u_ < n_local) + off_ = xadj[u_]; + } + if (u_ >= n_local) { + // set canonical end state + off_ = parent_->adjncy_ptr_->size(); + } +} + +EdgeRange::iterator EdgeRange::begin() const noexcept { + if (representation_ == GraphRepresentation::EDGE_LIST) + return iterator::edgelist_begin(this); + return iterator::csr_begin(this); +} + +EdgeRange::iterator EdgeRange::end() const noexcept { + if (representation_ == GraphRepresentation::EDGE_LIST) + return iterator::edgelist_end(this); + return iterator::csr_end(this); +} + +std::size_t EdgeRange::size() const noexcept { + if (representation_ == GraphRepresentation::EDGE_LIST) { + return edgelist_ptr_->size(); + } else { + return adjncy_ptr_->size(); + } +} + +} // namespace kagen diff --git a/kagen/edge_range.h b/kagen/edge_range.h new file mode 100644 index 0000000..1b92507 --- /dev/null +++ b/kagen/edge_range.h @@ -0,0 +1,81 @@ +#pragma once + +#include "kagen/kagen.h" + +#include +#include +#include + +namespace kagen { + +class EdgeRange { +public: + using Vertex = SInt; + using Edge = std::pair; + + explicit EdgeRange(const Edgelist& edgelist) noexcept; + EdgeRange(const XadjArray& xadj, const AdjncyArray& adjncy, VertexRange vertex_range) noexcept; + EdgeRange(const Graph& graph)noexcept; + + static EdgeRange FromGraph(const Graph& graph) noexcept; + + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = Edge; + using difference_type = std::ptrdiff_t; + + iterator() noexcept = default; + + static iterator edgelist_begin(const EdgeRange* parent) noexcept; + static iterator edgelist_end(const EdgeRange* parent) noexcept; + + // begin / end for CSR + static iterator csr_begin(const EdgeRange* parent) noexcept; + static iterator csr_end(const EdgeRange* parent) noexcept; + + std::size_t edge_index() const noexcept; + + value_type operator*() const noexcept; + + iterator& operator++(); + + iterator operator++(int); + + bool operator==(const iterator& other) const noexcept; + bool operator!=(const iterator& other) const noexcept; + + private: + const EdgeRange* parent_{nullptr}; + + // EDGELIST state + std::size_t idx_{0}; + + // CSR state + std::size_t u_{0}; // current vertex index (0..n_local-1) + std::size_t off_{0}; // current offset into adjncy + + Edge cur_{}; + + void load_current() noexcept; + void init_csr_begin() noexcept; + void advance_to_next_valid_csr() noexcept; + }; + + iterator begin() const noexcept; + iterator end() const noexcept; + + std::size_t size() const noexcept; + +private: + GraphRepresentation representation_; + + const Edgelist* edgelist_ptr_; + const XadjArray* xadj_ptr_; + const AdjncyArray* adjncy_ptr_; + Vertex vertex_base_; + + friend class iterator; +}; + +} // namespace kagen diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4dca900..3131939 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -74,3 +74,6 @@ kagen_add_test(test_permutation FILES permutation/permutation_test.cpp CORES 1 2 4) +kagen_add_test(test_edge_range + FILES edge_range.cpp + CORES 1) diff --git a/tests/edge_range.cpp b/tests/edge_range.cpp new file mode 100644 index 0000000..836c97e --- /dev/null +++ b/tests/edge_range.cpp @@ -0,0 +1,108 @@ +#include "kagen/edge_range.h" + +#include "kagen/kagen.h" + +#include + +#include "tests/gather.h" +#include "tests/utils.h" +#include "tools/converter.h" +#include "tools/geometry.h" + +using namespace kagen; + +void check_edge_range(const Graph& graph) { + Edgelist edgelist = graph.edges; + if (graph.representation == GraphRepresentation::CSR) { + edgelist = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); + } + EdgeRange edge_range(graph); + + { + std::size_t expected_index = 0; + for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { + auto edge = *it; + EXPECT_EQ(it.edge_index(), expected_index); + EXPECT_EQ(*it, edge); + ++expected_index; + } + } + + { + EXPECT_EQ(edge_range.size(), edgelist.size()); + for (std::size_t i = 0; auto elem: edge_range) { + EXPECT_EQ(elem, edgelist[i]); + ++i; + } + } +} + +void check_edge_range(KaGen& generator, SInt n, SInt m) { + // GNM + { + kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); + check_edge_range(graph); + } + // RMAT + { + kagen::Graph graph = generator.GenerateRMAT(n, m, 0.56, 0.19, 0.19); + check_edge_range(graph); + } + // RGG2D + { + kagen::Graph graph = generator.GenerateRGG2D_NM(n, m); + check_edge_range(graph); + } + // RGG3D + { + kagen::Graph graph = generator.GenerateRGG3D_NM(n, m); + check_edge_range(graph); + } + // RHG + { + kagen::Graph graph = generator.GenerateRHG_NM(2.6, n, m); + check_edge_range(graph); + } + // GRID2D + { + kagen::Graph graph = generator.GenerateGrid2D_NM(n, m); + check_edge_range(graph); + } + // GRID2D + { + kagen::Graph graph = generator.GenerateGrid3D_NM(n, m); + check_edge_range(graph); + } +} + +TEST(EdgeRangeTest, iterate_edgelist_representation) { + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseEdgeListRepresentation(); + const SInt n = 1000; + const SInt m = 16 * n; + check_edge_range(generator, n, m); +} + +TEST(EdgeRangeTest, iterate_sparse_edgelist_representation) { + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseEdgeListRepresentation(); + const SInt n = 1000; + const SInt m = 2 * n; + check_edge_range(generator, n, m); +} + +TEST(EdgeRangeTest, iterate_csr_representation) { + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseCSRRepresentation(); + const SInt n = 1000; + const SInt m = 16 * n; + check_edge_range(generator, n, m); +} + +TEST(EdgeRangeTest, iterate_sparse_csr_representation) { + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseCSRRepresentation(); + const SInt n = 1000; + const SInt m = 2 * n; + check_edge_range(generator, n, m); +} From 66459b5c07019aa20a3349d45231216a1c9fd7ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 07:25:41 +0000 Subject: [PATCH 2/6] Initial plan From 87143cc143043b833c0b24a4cfdfc89842b65ecb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 07:38:32 +0000 Subject: [PATCH 3/6] Upgrade EdgeRange iterator to bidirectional iterator - Changed iterator_category from forward_iterator_tag to bidirectional_iterator_tag - Implemented operator-- (both prefix and postfix) for backward iteration - Added retreat_to_prev_valid_csr() helper for CSR backward traversal - Added comprehensive bidirectional iteration tests for both EdgeList and CSR representations - All existing and new tests pass Co-authored-by: niklas-uhl <23433189+niklas-uhl@users.noreply.github.com> --- kagen/edge_range.cpp | 66 ++++++++++++++++++++++++++++++++++++++++ kagen/edge_range.h | 7 ++++- tests/edge_range.cpp | 72 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/kagen/edge_range.cpp b/kagen/edge_range.cpp index 66a4f42..ed00079 100644 --- a/kagen/edge_range.cpp +++ b/kagen/edge_range.cpp @@ -96,6 +96,27 @@ EdgeRange::iterator EdgeRange::iterator::operator++(int) { return tmp; } +EdgeRange::iterator& EdgeRange::iterator::operator--() { + assert(parent_ != nullptr); + + if (parent_->representation_ == GraphRepresentation::EDGE_LIST) { + assert(idx_ > 0 && "Cannot decrement begin iterator"); + --idx_; + load_current(); + return *this; + } + + retreat_to_prev_valid_csr(); + load_current(); + return *this; +} + +EdgeRange::iterator EdgeRange::iterator::operator--(int) { + iterator tmp = *this; + --(*this); + return tmp; +} + bool EdgeRange::iterator::operator==(const iterator& other) const noexcept { // must belong to same parent to compare reliably if (parent_ != other.parent_) @@ -178,6 +199,51 @@ void EdgeRange::iterator::advance_to_next_valid_csr() noexcept { } } +void EdgeRange::iterator::retreat_to_prev_valid_csr() noexcept { + const auto& xadj = *parent_->xadj_ptr_; + const auto& adjncy = *parent_->adjncy_ptr_; + const std::size_t n_local = xadj.empty() ? 0 : (xadj.size() - 1); + + // If we're at end(), move to the last valid edge + if (off_ >= adjncy.size()) { + assert(adjncy.size() > 0 && "Cannot decrement begin iterator"); + off_ = adjncy.size() - 1; + // Find which vertex this edge belongs to + for (std::size_t v = 0; v < n_local; ++v) { + if (xadj[v] <= off_ && off_ < xadj[v + 1]) { + u_ = v; + return; + } + } + // Should not reach here if data is consistent + assert(false && "Invalid state in retreat_to_prev_valid_csr"); + return; + } + + // Check if we can move back within the current vertex's adjacency list + if (off_ > 0 && xadj[u_] < off_) { + --off_; + return; + } + + // Move to the previous vertex + assert(u_ > 0 && "Cannot decrement begin iterator"); + --u_; + + // Find the last edge of the previous vertex (skip empty vertices going backward) + while (u_ < n_local && xadj[u_] >= xadj[u_ + 1]) { + if (u_ == 0) { + // No valid edges before this point + assert(false && "Cannot decrement begin iterator"); + return; + } + --u_; + } + + // Set off_ to the last edge of vertex u_ + off_ = xadj[u_ + 1] - 1; +} + EdgeRange::iterator EdgeRange::begin() const noexcept { if (representation_ == GraphRepresentation::EDGE_LIST) return iterator::edgelist_begin(this); diff --git a/kagen/edge_range.h b/kagen/edge_range.h index 1b92507..7e965af 100644 --- a/kagen/edge_range.h +++ b/kagen/edge_range.h @@ -21,7 +21,7 @@ class EdgeRange { class iterator { public: - using iterator_category = std::forward_iterator_tag; + using iterator_category = std::bidirectional_iterator_tag; using value_type = Edge; using difference_type = std::ptrdiff_t; @@ -42,6 +42,10 @@ class EdgeRange { iterator operator++(int); + iterator& operator--(); + + iterator operator--(int); + bool operator==(const iterator& other) const noexcept; bool operator!=(const iterator& other) const noexcept; @@ -60,6 +64,7 @@ class EdgeRange { void load_current() noexcept; void init_csr_begin() noexcept; void advance_to_next_valid_csr() noexcept; + void retreat_to_prev_valid_csr() noexcept; }; iterator begin() const noexcept; diff --git a/tests/edge_range.cpp b/tests/edge_range.cpp index 836c97e..5cedeaf 100644 --- a/tests/edge_range.cpp +++ b/tests/edge_range.cpp @@ -106,3 +106,75 @@ TEST(EdgeRangeTest, iterate_sparse_csr_representation) { const SInt m = 2 * n; check_edge_range(generator, n, m); } + +void check_bidirectional_iteration(const Graph& graph) { + Edgelist edgelist = graph.edges; + if (graph.representation == GraphRepresentation::CSR) { + edgelist = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); + } + EdgeRange edge_range(graph); + + if (edgelist.empty()) { + return; + } + + // Forward then backward iteration + { + std::vector forward_edges; + for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { + forward_edges.push_back(*it); + } + + std::vector backward_edges; + auto it = edge_range.end(); + while (it != edge_range.begin()) { + --it; + backward_edges.push_back(*it); + } + + std::reverse(backward_edges.begin(), backward_edges.end()); + EXPECT_EQ(forward_edges.size(), backward_edges.size()); + for (std::size_t i = 0; i < forward_edges.size(); ++i) { + EXPECT_EQ(forward_edges[i], backward_edges[i]); + } + } + + // Mixed forward and backward iteration + { + auto it = edge_range.begin(); + ++it; + ++it; + auto edge1 = *it; + --it; + auto edge2 = *it; + ++it; + EXPECT_EQ(*it, edge1); + EXPECT_EQ(edge2, edgelist[1]); + } +} + +TEST(EdgeRangeTest, bidirectional_iteration_edgelist) { + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseEdgeListRepresentation(); + const SInt n = 100; + const SInt m = 10 * n; + + kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); + check_bidirectional_iteration(graph); + + graph = generator.GenerateRGG2D_NM(n, m); + check_bidirectional_iteration(graph); +} + +TEST(EdgeRangeTest, bidirectional_iteration_csr) { + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseCSRRepresentation(); + const SInt n = 100; + const SInt m = 10 * n; + + kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); + check_bidirectional_iteration(graph); + + graph = generator.GenerateRGG2D_NM(n, m); + check_bidirectional_iteration(graph); +} From f0894da5b77b9aa773f0cd6f8264ad3820af7ded Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:52:00 +0000 Subject: [PATCH 4/6] Adapt tests to new parameterized test layout from PR #76 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored tests to use GoogleTest matchers and parameterized tests - Converted bidirectional iteration tests to parameterized format - Now generates 42 test cases (7 generators × 6 test variations) - Uses ElementsAreArray matcher for cleaner edge comparison - Tests now clearly indicate which generator is being tested - All 42 tests pass successfully Co-authored-by: niklas-uhl <23433189+niklas-uhl@users.noreply.github.com> --- tests/edge_range.cpp | 310 +++++++++++++++++++++++++------------------ 1 file changed, 180 insertions(+), 130 deletions(-) diff --git a/tests/edge_range.cpp b/tests/edge_range.cpp index 5cedeaf..d6d4c4a 100644 --- a/tests/edge_range.cpp +++ b/tests/edge_range.cpp @@ -2,179 +2,229 @@ #include "kagen/kagen.h" +#include #include +#include +#include +#include + #include "tests/gather.h" #include "tests/utils.h" #include "tools/converter.h" -#include "tools/geometry.h" using namespace kagen; -void check_edge_range(const Graph& graph) { - Edgelist edgelist = graph.edges; - if (graph.representation == GraphRepresentation::CSR) { - edgelist = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); - } - EdgeRange edge_range(graph); - - { - std::size_t expected_index = 0; - for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { - auto edge = *it; - EXPECT_EQ(it.edge_index(), expected_index); - EXPECT_EQ(*it, edge); - ++expected_index; - } - } +using GeneratorFunc = std::function; - { - EXPECT_EQ(edge_range.size(), edgelist.size()); - for (std::size_t i = 0; auto elem: edge_range) { - EXPECT_EQ(elem, edgelist[i]); - ++i; - } - } +MATCHER(EdgeIndexMatches, "") { + auto [iter, expected_idx] = arg; + return iter.edge_index() == expected_idx; } -void check_edge_range(KaGen& generator, SInt n, SInt m) { - // GNM - { - kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); - check_edge_range(graph); - } - // RMAT - { - kagen::Graph graph = generator.GenerateRMAT(n, m, 0.56, 0.19, 0.19); - check_edge_range(graph); - } - // RGG2D - { - kagen::Graph graph = generator.GenerateRGG2D_NM(n, m); - check_edge_range(graph); - } - // RGG3D - { - kagen::Graph graph = generator.GenerateRGG3D_NM(n, m); - check_edge_range(graph); - } - // RHG - { - kagen::Graph graph = generator.GenerateRHG_NM(2.6, n, m); - check_edge_range(graph); - } - // GRID2D - { - kagen::Graph graph = generator.GenerateGrid2D_NM(n, m); - check_edge_range(graph); - } - // GRID2D - { - kagen::Graph graph = generator.GenerateGrid3D_NM(n, m); - check_edge_range(graph); - } -} +struct EdgeRangeTestFixture : public ::testing::TestWithParam> {}; -TEST(EdgeRangeTest, iterate_edgelist_representation) { - kagen::KaGen generator(MPI_COMM_WORLD); - generator.UseEdgeListRepresentation(); + +INSTANTIATE_TEST_SUITE_P( + EdgeRangeTests, EdgeRangeTestFixture, + ::testing::Values( + std::make_tuple("GNM", GeneratorFunc([](KaGen& gen, SInt n, SInt m) { return gen.GenerateUndirectedGNM(n, m); })), + std::make_tuple("RMAT", GeneratorFunc([](KaGen& gen, SInt n, SInt m) { return gen.GenerateRMAT(n, m, 0.56, 0.19, 0.19); })), + std::make_tuple("RGG2D", GeneratorFunc([](KaGen& gen, SInt n, SInt m) { return gen.GenerateRGG2D_NM(n, m); })), + std::make_tuple("RGG3D", GeneratorFunc([](KaGen& gen, SInt n, SInt m) { return gen.GenerateRGG3D_NM(n, m); })), + std::make_tuple("RHG", GeneratorFunc([](KaGen& gen, SInt n, SInt m) { return gen.GenerateRHG_NM(2.6, n, m); })), + std::make_tuple("Grid2D", GeneratorFunc([](KaGen& gen, SInt n, SInt m) { return gen.GenerateGrid2D_NM(n, m); })), + std::make_tuple("Grid3D", GeneratorFunc([](KaGen& gen, SInt n, SInt m) { return gen.GenerateGrid3D_NM(n, m); }))), + [](const ::testing::TestParamInfo& info) { + return std::get<0>(info.param); + }); + +TEST_P(EdgeRangeTestFixture, iterate_edgelist_representation) { + using ::testing::ElementsAreArray; + using ::testing::Pointwise; + + auto [name, generate] = GetParam(); const SInt n = 1000; const SInt m = 16 * n; - check_edge_range(generator, n, m); -} -TEST(EdgeRangeTest, iterate_sparse_edgelist_representation) { kagen::KaGen generator(MPI_COMM_WORLD); generator.UseEdgeListRepresentation(); + Graph graph = generate(generator, n, m); + + Edgelist expected = graph.edges; + EdgeRange edge_range(graph); + + // Check edges match and indices are consecutive + EXPECT_THAT(std::vector(edge_range.begin(), edge_range.end()), ElementsAreArray(expected)); + + std::vector iterators; + for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { + iterators.push_back(it); + } + std::vector expected_indices(edge_range.size()); + std::iota(expected_indices.begin(), expected_indices.end(), 0); + EXPECT_THAT(iterators, Pointwise(EdgeIndexMatches(), expected_indices)); +} + +TEST_P(EdgeRangeTestFixture, iterate_sparse_edgelist_representation) { + using ::testing::ElementsAreArray; + using ::testing::Pointwise; + + auto [name, generate] = GetParam(); const SInt n = 1000; const SInt m = 2 * n; - check_edge_range(generator, n, m); -} -TEST(EdgeRangeTest, iterate_csr_representation) { kagen::KaGen generator(MPI_COMM_WORLD); - generator.UseCSRRepresentation(); + generator.UseEdgeListRepresentation(); + Graph graph = generate(generator, n, m); + + Edgelist expected = graph.edges; + EdgeRange edge_range(graph); + + // Check edges match and indices are consecutive + EXPECT_THAT(std::vector(edge_range.begin(), edge_range.end()), ElementsAreArray(expected)); + + std::vector iterators; + for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { + iterators.push_back(it); + } + std::vector expected_indices(edge_range.size()); + std::iota(expected_indices.begin(), expected_indices.end(), 0); + EXPECT_THAT(iterators, Pointwise(EdgeIndexMatches(), expected_indices)); +} + +TEST_P(EdgeRangeTestFixture, iterate_csr_representation) { + using ::testing::ElementsAreArray; + using ::testing::Pointwise; + + auto [name, generate] = GetParam(); const SInt n = 1000; const SInt m = 16 * n; - check_edge_range(generator, n, m); -} -TEST(EdgeRangeTest, iterate_sparse_csr_representation) { kagen::KaGen generator(MPI_COMM_WORLD); generator.UseCSRRepresentation(); + Graph graph = generate(generator, n, m); + + Edgelist expected = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); + EdgeRange edge_range(graph); + + // Check edges match and indices are consecutive + EXPECT_THAT(std::vector(edge_range.begin(), edge_range.end()), ElementsAreArray(expected)); + + std::vector iterators; + for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { + iterators.push_back(it); + } + std::vector expected_indices(edge_range.size()); + std::iota(expected_indices.begin(), expected_indices.end(), 0); + EXPECT_THAT(iterators, Pointwise(EdgeIndexMatches(), expected_indices)); +} + +TEST_P(EdgeRangeTestFixture, iterate_sparse_csr_representation) { + using ::testing::ElementsAreArray; + using ::testing::Pointwise; + + auto [name, generate] = GetParam(); const SInt n = 1000; const SInt m = 2 * n; - check_edge_range(generator, n, m); -} -void check_bidirectional_iteration(const Graph& graph) { - Edgelist edgelist = graph.edges; - if (graph.representation == GraphRepresentation::CSR) { - edgelist = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseCSRRepresentation(); + Graph graph = generate(generator, n, m); + + Edgelist expected = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); + EdgeRange edge_range(graph); + + // Check edges match and indices are consecutive + EXPECT_THAT(std::vector(edge_range.begin(), edge_range.end()), ElementsAreArray(expected)); + + std::vector iterators; + for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { + iterators.push_back(it); } + std::vector expected_indices(edge_range.size()); + std::iota(expected_indices.begin(), expected_indices.end(), 0); + EXPECT_THAT(iterators, Pointwise(EdgeIndexMatches(), expected_indices)); +} + +TEST_P(EdgeRangeTestFixture, bidirectional_iteration_edgelist) { + using ::testing::ElementsAreArray; + + auto [name, generate] = GetParam(); + const SInt n = 100; + const SInt m = 10 * n; + + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseEdgeListRepresentation(); + Graph graph = generate(generator, n, m); + + Edgelist expected = graph.edges; EdgeRange edge_range(graph); - if (edgelist.empty()) { + if (expected.empty()) { return; } - // Forward then backward iteration - { - std::vector forward_edges; - for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { - forward_edges.push_back(*it); - } - - std::vector backward_edges; - auto it = edge_range.end(); - while (it != edge_range.begin()) { - --it; - backward_edges.push_back(*it); - } - - std::reverse(backward_edges.begin(), backward_edges.end()); - EXPECT_EQ(forward_edges.size(), backward_edges.size()); - for (std::size_t i = 0; i < forward_edges.size(); ++i) { - EXPECT_EQ(forward_edges[i], backward_edges[i]); - } + // Forward then backward iteration - edges should match when reversed + std::vector forward_edges(edge_range.begin(), edge_range.end()); + std::vector backward_edges; + auto it = edge_range.end(); + while (it != edge_range.begin()) { + --it; + backward_edges.push_back(*it); } + std::reverse(backward_edges.begin(), backward_edges.end()); + EXPECT_THAT(backward_edges, ElementsAreArray(forward_edges)); // Mixed forward and backward iteration - { - auto it = edge_range.begin(); - ++it; - ++it; - auto edge1 = *it; - --it; - auto edge2 = *it; - ++it; - EXPECT_EQ(*it, edge1); - EXPECT_EQ(edge2, edgelist[1]); - } + auto it2 = edge_range.begin(); + ++it2; + ++it2; + auto edge_at_2 = *it2; + --it2; + auto edge_at_1 = *it2; + ++it2; + EXPECT_EQ(*it2, edge_at_2); + EXPECT_EQ(edge_at_1, expected[1]); } -TEST(EdgeRangeTest, bidirectional_iteration_edgelist) { - kagen::KaGen generator(MPI_COMM_WORLD); - generator.UseEdgeListRepresentation(); +TEST_P(EdgeRangeTestFixture, bidirectional_iteration_csr) { + using ::testing::ElementsAreArray; + + auto [name, generate] = GetParam(); const SInt n = 100; const SInt m = 10 * n; - - kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); - check_bidirectional_iteration(graph); - - graph = generator.GenerateRGG2D_NM(n, m); - check_bidirectional_iteration(graph); -} -TEST(EdgeRangeTest, bidirectional_iteration_csr) { kagen::KaGen generator(MPI_COMM_WORLD); generator.UseCSRRepresentation(); - const SInt n = 100; - const SInt m = 10 * n; - - kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); - check_bidirectional_iteration(graph); - - graph = generator.GenerateRGG2D_NM(n, m); - check_bidirectional_iteration(graph); + Graph graph = generate(generator, n, m); + + Edgelist expected = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); + EdgeRange edge_range(graph); + + if (expected.empty()) { + return; + } + + // Forward then backward iteration - edges should match when reversed + std::vector forward_edges(edge_range.begin(), edge_range.end()); + std::vector backward_edges; + auto it = edge_range.end(); + while (it != edge_range.begin()) { + --it; + backward_edges.push_back(*it); + } + std::reverse(backward_edges.begin(), backward_edges.end()); + EXPECT_THAT(backward_edges, ElementsAreArray(forward_edges)); + + // Mixed forward and backward iteration + auto it2 = edge_range.begin(); + ++it2; + ++it2; + auto edge_at_2 = *it2; + --it2; + auto edge_at_1 = *it2; + ++it2; + EXPECT_EQ(*it2, edge_at_2); + EXPECT_EQ(edge_at_1, expected[1]); } From 46876206491bf6cd65fd0dd056f286e59275924b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 07:38:32 +0000 Subject: [PATCH 5/6] Upgrade EdgeRange iterator to bidirectional iterator - Changed iterator_category from forward_iterator_tag to bidirectional_iterator_tag - Implemented operator-- (both prefix and postfix) for backward iteration - Added retreat_to_prev_valid_csr() helper for CSR backward traversal - Added comprehensive bidirectional iteration tests for both EdgeList and CSR representations - All existing and new tests pass Co-authored-by: niklas-uhl <23433189+niklas-uhl@users.noreply.github.com> --- kagen/edge_range.cpp | 66 ++++++++++++++++++++++++++++++++++++++++ kagen/edge_range.h | 7 ++++- tests/edge_range.cpp | 72 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/kagen/edge_range.cpp b/kagen/edge_range.cpp index 66a4f42..ed00079 100644 --- a/kagen/edge_range.cpp +++ b/kagen/edge_range.cpp @@ -96,6 +96,27 @@ EdgeRange::iterator EdgeRange::iterator::operator++(int) { return tmp; } +EdgeRange::iterator& EdgeRange::iterator::operator--() { + assert(parent_ != nullptr); + + if (parent_->representation_ == GraphRepresentation::EDGE_LIST) { + assert(idx_ > 0 && "Cannot decrement begin iterator"); + --idx_; + load_current(); + return *this; + } + + retreat_to_prev_valid_csr(); + load_current(); + return *this; +} + +EdgeRange::iterator EdgeRange::iterator::operator--(int) { + iterator tmp = *this; + --(*this); + return tmp; +} + bool EdgeRange::iterator::operator==(const iterator& other) const noexcept { // must belong to same parent to compare reliably if (parent_ != other.parent_) @@ -178,6 +199,51 @@ void EdgeRange::iterator::advance_to_next_valid_csr() noexcept { } } +void EdgeRange::iterator::retreat_to_prev_valid_csr() noexcept { + const auto& xadj = *parent_->xadj_ptr_; + const auto& adjncy = *parent_->adjncy_ptr_; + const std::size_t n_local = xadj.empty() ? 0 : (xadj.size() - 1); + + // If we're at end(), move to the last valid edge + if (off_ >= adjncy.size()) { + assert(adjncy.size() > 0 && "Cannot decrement begin iterator"); + off_ = adjncy.size() - 1; + // Find which vertex this edge belongs to + for (std::size_t v = 0; v < n_local; ++v) { + if (xadj[v] <= off_ && off_ < xadj[v + 1]) { + u_ = v; + return; + } + } + // Should not reach here if data is consistent + assert(false && "Invalid state in retreat_to_prev_valid_csr"); + return; + } + + // Check if we can move back within the current vertex's adjacency list + if (off_ > 0 && xadj[u_] < off_) { + --off_; + return; + } + + // Move to the previous vertex + assert(u_ > 0 && "Cannot decrement begin iterator"); + --u_; + + // Find the last edge of the previous vertex (skip empty vertices going backward) + while (u_ < n_local && xadj[u_] >= xadj[u_ + 1]) { + if (u_ == 0) { + // No valid edges before this point + assert(false && "Cannot decrement begin iterator"); + return; + } + --u_; + } + + // Set off_ to the last edge of vertex u_ + off_ = xadj[u_ + 1] - 1; +} + EdgeRange::iterator EdgeRange::begin() const noexcept { if (representation_ == GraphRepresentation::EDGE_LIST) return iterator::edgelist_begin(this); diff --git a/kagen/edge_range.h b/kagen/edge_range.h index 1b92507..7e965af 100644 --- a/kagen/edge_range.h +++ b/kagen/edge_range.h @@ -21,7 +21,7 @@ class EdgeRange { class iterator { public: - using iterator_category = std::forward_iterator_tag; + using iterator_category = std::bidirectional_iterator_tag; using value_type = Edge; using difference_type = std::ptrdiff_t; @@ -42,6 +42,10 @@ class EdgeRange { iterator operator++(int); + iterator& operator--(); + + iterator operator--(int); + bool operator==(const iterator& other) const noexcept; bool operator!=(const iterator& other) const noexcept; @@ -60,6 +64,7 @@ class EdgeRange { void load_current() noexcept; void init_csr_begin() noexcept; void advance_to_next_valid_csr() noexcept; + void retreat_to_prev_valid_csr() noexcept; }; iterator begin() const noexcept; diff --git a/tests/edge_range.cpp b/tests/edge_range.cpp index 4256f8e..9641961 100644 --- a/tests/edge_range.cpp +++ b/tests/edge_range.cpp @@ -146,3 +146,75 @@ TEST_P(EdgeRangeTestFixture, iterate_sparse_csr_representation) { std::iota(expected_indices.begin(), expected_indices.end(), 0); EXPECT_THAT(iterators, Pointwise(EdgeIndexMatches(), expected_indices)); } + +void check_bidirectional_iteration(const Graph& graph) { + Edgelist edgelist = graph.edges; + if (graph.representation == GraphRepresentation::CSR) { + edgelist = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); + } + EdgeRange edge_range(graph); + + if (edgelist.empty()) { + return; + } + + // Forward then backward iteration + { + std::vector forward_edges; + for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { + forward_edges.push_back(*it); + } + + std::vector backward_edges; + auto it = edge_range.end(); + while (it != edge_range.begin()) { + --it; + backward_edges.push_back(*it); + } + + std::reverse(backward_edges.begin(), backward_edges.end()); + EXPECT_EQ(forward_edges.size(), backward_edges.size()); + for (std::size_t i = 0; i < forward_edges.size(); ++i) { + EXPECT_EQ(forward_edges[i], backward_edges[i]); + } + } + + // Mixed forward and backward iteration + { + auto it = edge_range.begin(); + ++it; + ++it; + auto edge1 = *it; + --it; + auto edge2 = *it; + ++it; + EXPECT_EQ(*it, edge1); + EXPECT_EQ(edge2, edgelist[1]); + } +} + +TEST(EdgeRangeTest, bidirectional_iteration_edgelist) { + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseEdgeListRepresentation(); + const SInt n = 100; + const SInt m = 10 * n; + + kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); + check_bidirectional_iteration(graph); + + graph = generator.GenerateRGG2D_NM(n, m); + check_bidirectional_iteration(graph); +} + +TEST(EdgeRangeTest, bidirectional_iteration_csr) { + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseCSRRepresentation(); + const SInt n = 100; + const SInt m = 10 * n; + + kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); + check_bidirectional_iteration(graph); + + graph = generator.GenerateRGG2D_NM(n, m); + check_bidirectional_iteration(graph); +} From 8080cbae966d3e3a861c8e51e78f619dc83a0f2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:52:00 +0000 Subject: [PATCH 6/6] Adapt tests to new parameterized test layout from PR #76 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactored tests to use GoogleTest matchers and parameterized tests - Converted bidirectional iteration tests to parameterized format - Now generates 42 test cases (7 generators × 6 test variations) - Uses ElementsAreArray matcher for cleaner edge comparison - Tests now clearly indicate which generator is being tested - All 42 tests pass successfully Co-authored-by: niklas-uhl <23433189+niklas-uhl@users.noreply.github.com> --- tests/edge_range.cpp | 120 +++++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 55 deletions(-) diff --git a/tests/edge_range.cpp b/tests/edge_range.cpp index 9641961..d6d4c4a 100644 --- a/tests/edge_range.cpp +++ b/tests/edge_range.cpp @@ -147,74 +147,84 @@ TEST_P(EdgeRangeTestFixture, iterate_sparse_csr_representation) { EXPECT_THAT(iterators, Pointwise(EdgeIndexMatches(), expected_indices)); } -void check_bidirectional_iteration(const Graph& graph) { - Edgelist edgelist = graph.edges; - if (graph.representation == GraphRepresentation::CSR) { - edgelist = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); - } +TEST_P(EdgeRangeTestFixture, bidirectional_iteration_edgelist) { + using ::testing::ElementsAreArray; + + auto [name, generate] = GetParam(); + const SInt n = 100; + const SInt m = 10 * n; + + kagen::KaGen generator(MPI_COMM_WORLD); + generator.UseEdgeListRepresentation(); + Graph graph = generate(generator, n, m); + + Edgelist expected = graph.edges; EdgeRange edge_range(graph); - if (edgelist.empty()) { + if (expected.empty()) { return; } - // Forward then backward iteration - { - std::vector forward_edges; - for (auto it = edge_range.begin(); it != edge_range.end(); ++it) { - forward_edges.push_back(*it); - } - - std::vector backward_edges; - auto it = edge_range.end(); - while (it != edge_range.begin()) { - --it; - backward_edges.push_back(*it); - } - - std::reverse(backward_edges.begin(), backward_edges.end()); - EXPECT_EQ(forward_edges.size(), backward_edges.size()); - for (std::size_t i = 0; i < forward_edges.size(); ++i) { - EXPECT_EQ(forward_edges[i], backward_edges[i]); - } + // Forward then backward iteration - edges should match when reversed + std::vector forward_edges(edge_range.begin(), edge_range.end()); + std::vector backward_edges; + auto it = edge_range.end(); + while (it != edge_range.begin()) { + --it; + backward_edges.push_back(*it); } + std::reverse(backward_edges.begin(), backward_edges.end()); + EXPECT_THAT(backward_edges, ElementsAreArray(forward_edges)); // Mixed forward and backward iteration - { - auto it = edge_range.begin(); - ++it; - ++it; - auto edge1 = *it; - --it; - auto edge2 = *it; - ++it; - EXPECT_EQ(*it, edge1); - EXPECT_EQ(edge2, edgelist[1]); - } + auto it2 = edge_range.begin(); + ++it2; + ++it2; + auto edge_at_2 = *it2; + --it2; + auto edge_at_1 = *it2; + ++it2; + EXPECT_EQ(*it2, edge_at_2); + EXPECT_EQ(edge_at_1, expected[1]); } -TEST(EdgeRangeTest, bidirectional_iteration_edgelist) { - kagen::KaGen generator(MPI_COMM_WORLD); - generator.UseEdgeListRepresentation(); +TEST_P(EdgeRangeTestFixture, bidirectional_iteration_csr) { + using ::testing::ElementsAreArray; + + auto [name, generate] = GetParam(); const SInt n = 100; const SInt m = 10 * n; - - kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); - check_bidirectional_iteration(graph); - - graph = generator.GenerateRGG2D_NM(n, m); - check_bidirectional_iteration(graph); -} -TEST(EdgeRangeTest, bidirectional_iteration_csr) { kagen::KaGen generator(MPI_COMM_WORLD); generator.UseCSRRepresentation(); - const SInt n = 100; - const SInt m = 10 * n; - - kagen::Graph graph = generator.GenerateUndirectedGNM(n, m); - check_bidirectional_iteration(graph); - - graph = generator.GenerateRGG2D_NM(n, m); - check_bidirectional_iteration(graph); + Graph graph = generate(generator, n, m); + + Edgelist expected = BuildEdgeListFromCSR(graph.vertex_range, graph.xadj, graph.adjncy); + EdgeRange edge_range(graph); + + if (expected.empty()) { + return; + } + + // Forward then backward iteration - edges should match when reversed + std::vector forward_edges(edge_range.begin(), edge_range.end()); + std::vector backward_edges; + auto it = edge_range.end(); + while (it != edge_range.begin()) { + --it; + backward_edges.push_back(*it); + } + std::reverse(backward_edges.begin(), backward_edges.end()); + EXPECT_THAT(backward_edges, ElementsAreArray(forward_edges)); + + // Mixed forward and backward iteration + auto it2 = edge_range.begin(); + ++it2; + ++it2; + auto edge_at_2 = *it2; + --it2; + auto edge_at_1 = *it2; + ++it2; + EXPECT_EQ(*it2, edge_at_2); + EXPECT_EQ(edge_at_1, expected[1]); }