From 04221e26168ff8c643b5267d692740c98e06fa2c Mon Sep 17 00:00:00 2001 From: Ian Date: Thu, 25 Jun 2026 17:11:40 -0500 Subject: [PATCH 01/11] header for inducedsubgraphview --- .../networkit/graph/InducedSubgraphView.hpp | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 include/networkit/graph/InducedSubgraphView.hpp diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp new file mode 100644 index 000000000..ece75f628 --- /dev/null +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -0,0 +1,107 @@ +/* + * InducedSubgraphView.hpp + * + * Created on: 06.25.2026 + * Author: Ian Chen (ianchen3@illinois.edu) + * + * Zero-copy induced subgraph. + */ + +#ifndef NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ +#define NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ + +#include + +namespace NetworKit { + +/** + * @ingroup graph + * InducedSubgraphView - Zero-copy induced subgraph. + * + * This class provides a memory-efficient graph view into a + * base graph, providing the same interface. + * Nodes can be added and removed efficiently. + * + * A mutable GraphW can be realized. + */ +class InducedSubgraphView : public Graph { +public: + /** + * Construct an induced subgraph view from the original graph and the node subset. + * @param originalGraph The original CSR graph + * @param subset The defining nodes of the induced subgraph. + */ + InducedSubgraphView(const Graph &originalGraph, std::vector subset); + + /** + * Construct an explicit mutable subgraph equivalent to this view. + */ + GraphW Realize() const; + + /** + * Copy constructor + */ + InducedSubgraphView(const InducedSubgraphView &other) : Graph(other) {} + + /** + * Move constructor + */ + InducedSubgraphView(InducedSubgraphView &&other) noexcept : Graph(std::move(other)) {} + + /** + * Copy assignment + */ + InducedSubgraphView &operator=(const InducedSubgraphView &other) { + Graph::operator=(other); + return *this; + } + + /** + * Move assignment + */ + InducedSubgraphView &operator=(InducedSubgraphView &&other) noexcept { + Graph::operator=(std::move(other)); + return *this; + } + + /** Default destructor */ + ~InducedSubgraphView() override = default; + + // Implement pure virtual methods from Graph base class + + count degree(node v) const override; + count degreeIn(node v) const override; + bool isIsolated(node v) const override; + + /** + * Return edge weight of edge {@a u,@a v}. Returns 0 if edge does not + * exist. If weights are provided during construction, returns the actual + * edge weight; otherwise returns defaultEdgeWeight (1.0). + * + * @param u Endpoint of edge. + * @param v Endpoint of edge. + * @return Edge weight of edge {@a u,@a v} or 0 if edge does not exist. + */ + edgeweight weight(node u, node v) const override; + + edgeid edgeId(node u, node v) const override; + + node getIthNeighbor(Unsafe, node u, index i) const override; + edgeweight getIthNeighborWeight(node u, index i) const override; + node getIthNeighbor(node u, index i) const override; + node getIthInNeighbor(node u, index i) const override; + std::pair getIthNeighborWithWeight(node u, index i) const override; + std::pair getIthNeighborWithId(node u, index i) const override; + +protected: + std::vector getNeighborsVector(node u, bool inEdges = false) const override; + std::pair, std::vector> + getNeighborsWithWeightsVector(node u, bool inEdges = false) const override; + + index indexInInEdgeArray(node v, node u) const override; + index indexInOutEdgeArray(node u, node v) const override; +}; + +} // namespace NetworKit + +#endif // NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ From 25c22ea89b1759f7e59b86451b7f9ec83f40a9bf Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 26 Jun 2026 13:12:24 -0500 Subject: [PATCH 02/11] add necesasry overrides to inducedsubgraphview header --- .../networkit/graph/InducedSubgraphView.hpp | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index ece75f628..d2a7c82e8 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -3,14 +3,14 @@ * * Created on: 06.25.2026 * Author: Ian Chen (ianchen3@illinois.edu) - * - * Zero-copy induced subgraph. */ #ifndef NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ #define NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ -#include +#include + +#include namespace NetworKit { @@ -20,18 +20,36 @@ namespace NetworKit { * * This class provides a memory-efficient graph view into a * base graph, providing the same interface. - * Nodes can be added and removed efficiently. + * Nodes can be added or removed as a batch. * * A mutable GraphW can be realized. + * Neighbors are computed on-the-fly. */ -class InducedSubgraphView : public Graph { +class InducedSubgraphView final : public GraphR { + const Graph &originalGraph; + std::set nodeSubset; + + count inducedNumberOfEdges = 0; + count inducedNumberOfSelfLoops = 0; + std::vector inducedDegree; + public: /** * Construct an induced subgraph view from the original graph and the node subset. * @param originalGraph The original CSR graph * @param subset The defining nodes of the induced subgraph. */ - InducedSubgraphView(const Graph &originalGraph, std::vector subset); + InducedSubgraphView(const Graph &originalGraph, std::set subset); + + /** + * Add nodes to the subset. + */ + void AddNodes(const std::set &subset); + + /** + * Remove nodes from the subset. + */ + void RemoveNodes(const std::set &subset); /** * Construct an explicit mutable subgraph equivalent to this view. @@ -41,12 +59,12 @@ class InducedSubgraphView : public Graph { /** * Copy constructor */ - InducedSubgraphView(const InducedSubgraphView &other) : Graph(other) {} + InducedSubgraphView(const InducedSubgraphView &other) = default; /** * Move constructor */ - InducedSubgraphView(InducedSubgraphView &&other) noexcept : Graph(std::move(other)) {} + InducedSubgraphView(InducedSubgraphView &&other) noexcept = default; /** * Copy assignment @@ -67,11 +85,16 @@ class InducedSubgraphView : public Graph { /** Default destructor */ ~InducedSubgraphView() override = default; + // Override from Graph base + bool hasNode(node v) const noexcept; + count numberOfNodes() noexcept; + count numberOfEdges() noexcept; + count numberOfSelfLoops() noexcept; + // Implement pure virtual methods from Graph base class count degree(node v) const override; count degreeIn(node v) const override; - bool isIsolated(node v) const override; /** * Return edge weight of edge {@a u,@a v}. Returns 0 if edge does not From 3eab0e66fc235e4087eba2d23391af489cda647c Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 26 Jun 2026 13:19:02 -0500 Subject: [PATCH 03/11] ci: stop workflows for draft PRs --- .github/workflows/ci-matrix.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-matrix.yml b/.github/workflows/ci-matrix.yml index 3055245c8..c3afa2dba 100644 --- a/.github/workflows/ci-matrix.yml +++ b/.github/workflows/ci-matrix.yml @@ -26,6 +26,7 @@ jobs: build: name: "${{ matrix.os }} (${{ matrix.compiler }})" runs-on: ${{ matrix.runner }} + if: github.event_name != 'pull_request' || !github.event.pull_request.draft strategy: fail-fast: false matrix: From 01ef0bb90a5f9c0ea067e6abb20f668fcfb0c270 Mon Sep 17 00:00:00 2001 From: Ian Date: Sat, 27 Jun 2026 17:27:26 -0500 Subject: [PATCH 04/11] draft impl of inducedsubgraphview + starting tests --- .../networkit/graph/InducedSubgraphView.hpp | 91 +- networkit/cpp/graph/InducedSubgraphView.cpp | 202 ++ .../graph/test/InducedSubgraphViewGTest.cpp | 2599 +++++++++++++++++ 3 files changed, 2868 insertions(+), 24 deletions(-) create mode 100644 networkit/cpp/graph/InducedSubgraphView.cpp create mode 100644 networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index d2a7c82e8..222e57e23 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -25,36 +25,52 @@ namespace NetworKit { * A mutable GraphW can be realized. * Neighbors are computed on-the-fly. */ -class InducedSubgraphView final : public GraphR { +class InducedSubgraphView : public Graph { const Graph &originalGraph; - std::set nodeSubset; + std::set nodeSubset; // all nodes guaranteed to be in the original graph - count inducedNumberOfEdges = 0; - count inducedNumberOfSelfLoops = 0; + // store the induced degrees of all nodes in originalGraph for fast access + // for nodes not in nodeSubset, may contain garbage std::vector inducedDegree; + std::vector inducedInDegree; + +protected: + // count n; + // count m; + // count storedNumberOfSelfLoops; + // bool weighted; + // bool directed; public: /** * Construct an induced subgraph view from the original graph and the node subset. * @param originalGraph The original CSR graph * @param subset The defining nodes of the induced subgraph. + * + * Note: directed graphs are not yet supported. */ - InducedSubgraphView(const Graph &originalGraph, std::set subset); + InducedSubgraphView(const Graph &originalGraph, const std::set &subset); /** * Add nodes to the subset. */ - void AddNodes(const std::set &subset); + void addNode(node u) { addNodes({u}); } + void addNodes(const std::set &subset); /** * Remove nodes from the subset. */ - void RemoveNodes(const std::set &subset); + void removeNodes(const std::set &subset); + + /** + * Find all nodes that have source inside the subgraph view, and outNeighbor outside. + */ + std::set boundary(); /** * Construct an explicit mutable subgraph equivalent to this view. */ - GraphW Realize() const; + GraphW realize() const; /** * Copy constructor @@ -85,30 +101,27 @@ class InducedSubgraphView final : public GraphR { /** Default destructor */ ~InducedSubgraphView() override = default; - // Override from Graph base - bool hasNode(node v) const noexcept; - count numberOfNodes() noexcept; - count numberOfEdges() noexcept; - count numberOfSelfLoops() noexcept; + /** + * Access the current nodes that are in the graph. + */ + const std::set &getNodeSubset() const noexcept { return nodeSubset; }; - // Implement pure virtual methods from Graph base class + /** + * Access the underlying graph. + */ + const Graph &getOriginalGraph() const noexcept { return originalGraph; }; + // Override from Graph base count degree(node v) const override; count degreeIn(node v) const override; + bool isIsolated(node v) const override; - /** - * Return edge weight of edge {@a u,@a v}. Returns 0 if edge does not - * exist. If weights are provided during construction, returns the actual - * edge weight; otherwise returns defaultEdgeWeight (1.0). - * - * @param u Endpoint of edge. - * @param v Endpoint of edge. - * @return Edge weight of edge {@a u,@a v} or 0 if edge does not exist. - */ + bool hasNode(node v) const noexcept; + bool hasEdge(node u, node v) const noexcept; edgeweight weight(node u, node v) const override; + // FIXME: NOT PLANNED TO IMPLEMENT edgeid edgeId(node u, node v) const override; - node getIthNeighbor(Unsafe, node u, index i) const override; edgeweight getIthNeighborWeight(node u, index i) const override; node getIthNeighbor(node u, index i) const override; @@ -121,8 +134,38 @@ class InducedSubgraphView final : public GraphR { std::pair, std::vector> getNeighborsWithWeightsVector(node u, bool inEdges = false) const override; + // FIXME: NOT PLANNED TO IMPLEMENT index indexInInEdgeArray(node v, node u) const override; index indexInOutEdgeArray(node u, node v) const override; + +private: + /** + * @brief Virtual method for edge iteration + */ + void + forEdgesVirtualImpl(bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const override; + + /** + * @brief Virtual method for forEdgesOf + */ + void forEdgesOfVirtualImpl( + node u, bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const override; + + /** + * @brief Virtual method for forInEdgesOf + */ + void forInEdgesVirtualImpl( + node u, bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const override; + + /** + * @brief Virtual method for parallelSumForEdges + */ + double parallelSumForEdgesVirtualImpl( + bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const override; }; } // namespace NetworKit diff --git a/networkit/cpp/graph/InducedSubgraphView.cpp b/networkit/cpp/graph/InducedSubgraphView.cpp new file mode 100644 index 000000000..517e5e6b7 --- /dev/null +++ b/networkit/cpp/graph/InducedSubgraphView.cpp @@ -0,0 +1,202 @@ +/* + * InducedSubgraphView.cpp + * + * Created on: June 26th, 2026 + * Ian Chen (ianchen3@illinois.edu) + */ + +#include "networkit/graph/GraphTools.hpp" +#include "networkit/graph/GraphW.hpp" +#include + +// TODO: check if attributes work transparently, or if special handling needs to be done +// TODO: document that EdgeIterators probably won't work (getIthNeighbor not impl-ed) +// but because we only need ++ and --, we can probably still implement this. + +namespace NetworKit { + +InducedSubgraphView::InducedSubgraphView(const Graph &originalGraph, const std::set &subset) + : Graph(0, originalGraph.isWeighted(), originalGraph.isDirected()), + originalGraph(originalGraph) { + AddNodes(subset); +} + +std::set InducedSubgraphView::boundary() { + std::set result; + for (node u : nodeSubset) { + originalGraph.forNeighborsOf(u, [&](node v) { + if (!hasNode(v)) + result.insert(v); + }); + } + return result; +} + +count InducedSubgraphView::degree(node v) const { + assert(hasNode(v)); + return inducedDegree[v]; +} + +count InducedSubgraphView::degreeIn(node v) const { + assert(hasNode(v)); + return isDirected() ? inducedInDegree[v] : degree(v); +}; + +bool InducedSubgraphView::isIsolated(node v) const { + assert(hasNode(v)); + return degree(v) == 0 && (!directed || degreeIn(v) == 0); +} + +bool InducedSubgraphView::hasNode(node v) const noexcept { + return nodeSubset.contains(v); +} + +bool InducedSubgraphView::hasEdge(node u, node v) const noexcept { + return hasNode(u) && hasNode(v) && originalGraph.hasEdge(v, v); +} + +edgeweight InducedSubgraphView::weight(node u, node v) const { + if (!hasEdge(u, v)) + return 0; + return originalGraph.weight(u, v); +} + +void InducedSubgraphView::AddNodes(const std::set &subset) { + std::vector added(subset.size(), true); + + for (auto it = subset.begin(); it != subset.end(); ++it) { + if (hasNode(*it)) { + added[std::distance(subset.begin(), it)] = false; + continue; + } + nodeSubset.insert(*it); + } + + for (auto it = subset.begin(); it != subset.end(); ++it) { + if (!added[std::distance(subset.begin(), it)]) + continue; + count degree = 0; + originalGraph.forNeighborsOf(*it, [&](node u) { + if (!hasNode(u)) + return; + if (u == *it) + ++storedNumberOfSelfLoops; + ++inducedDegree[u]; + ++degree; + }); + inducedDegree[*it] = degree; + m += degree; + n += 1; + } +} + +void InducedSubgraphView::RemoveNodes(const std::set &subset) { + std::vector removed(subset.size(), true); + + for (auto it = subset.begin(); it != subset.end(); ++it) { + if (!hasNode(*it)) { + removed[std::distance(subset.begin(), it)] = false; + continue; + } + nodeSubset.erase(*it); + } + + for (auto it = subset.begin(); it != subset.end(); ++it) { + if (!removed[std::distance(subset.begin(), it)]) + continue; + count degree = 0; + originalGraph.forNeighborsOf(*it, [&](node u) { + if (!hasNode(u)) + return; + if (u == *it) + --storedNumberOfSelfLoops; + --inducedDegree[u]; + --degree; + }); + m -= degree; + n -= 1; + } +} + +GraphW InducedSubgraphView::Realize() const { + return GraphTools::subgraphFromNodes(originalGraph, nodeSubset.begin(), nodeSubset.end()); +} + +std::vector InducedSubgraphView::getNeighborsVector(node u, bool inEdges) const { + std::vector result; + auto cb = [&](node v) { + if (hasNode(v)) + result.push_back(v); + }; + + if (inEdges) { + result.reserve(degreeIn(u)); + originalGraph.forInNeighborsOf(u, cb); + } else { + result.reserve(degree(u)); + originalGraph.forNeighborsOf(u, cb); + } + + return result; +} + +std::pair, std::vector> +InducedSubgraphView::getNeighborsWithWeightsVector(node u, bool inEdges) const { + std::vector nodeVec; + std::vector weightVec; + + auto cb = [&](node v) { + if (hasNode(v)) { + nodeVec.push_back(v); + weightVec.push_back(weight(u, v)); + } + }; + + if (inEdges) { + nodeVec.reserve(degreeIn(u)); + weightVec.reserve(degreeIn(u)); + originalGraph.forInNeighborsOf(u, cb); + } else { + nodeVec.reserve(degree(u)); + weightVec.reserve(degree(u)); + originalGraph.forNeighborsOf(u, cb); + } + + return {nodeVec, weightVec}; +} + +void InducedSubgraphView::forEdgesVirtualImpl( + bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const { + for (node u : nodeSubset) + forEdgesOf(u, handle); +} + +void InducedSubgraphView::forEdgesOfVirtualImpl( + node u, bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const { + originalGraph.forEdgesOf(u, [&](node v, edgeweight w, edgeid x) { + if (hasEdge(u, v)) + handle(u, v, w, x); + }); +} + +void InducedSubgraphView::forInEdgesVirtualImpl( + node u, bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const { + originalGraph.forInEdgesOf(u, [&](node v, edgeweight w, edgeid x) { + if (hasEdge(u, v)) + handle(u, v, w, x); + }); +} + +double InducedSubgraphView::parallelSumForEdgesVirtualImpl( + bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const { + return originalGraph.parallelSumForEdges([&](node u, node v, edgeweight w, edgeid x) { + if (hasEdge(u, v)) + handle(u, v, w, x); + }); +} + +} // namespace NetworKit diff --git a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp new file mode 100644 index 000000000..c587c3a4c --- /dev/null +++ b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp @@ -0,0 +1,2599 @@ +/* + * InducedSubgraphViewGTest.cpp + * + * Created on: 06.26.2026 + * Author: Ian Chen (ianchen3@illinois.edu) + */ + +#include +#include + +#include "networkit/auxiliary/Random.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NetworKit { + +class InducedSubgraphViewGTest : public testing::TestWithParam> { +public: + virtual void SetUp(); + +protected: + GraphW Ghouse; + std::vector> houseEdgesOut; + std::vector> Ahouse; + count n_house; + count m_house; + + bool isInducedSubgraphView() const { return !isWeighted() && !isDirected(); } + bool isWeightedInducedSubgraphView() const { return isWeighted() && !isDirected(); } + bool isDirectedInducedSubgraphView() const { return !isWeighted() && isDirected(); } + bool isWeightedDirectedInducedSubgraphView() const { return isWeighted() && isDirected(); } + + bool isWeighted() const; + bool isDirected() const; + GraphW createGraphW(count n = 0) const; + GraphW createGraphW(count n, count m) const; + std::pair createInducedSubgraphView(count n, count m, + bool random = false) const; + std::pair + createInducedSubgraphView(count n, count m, count N, count M, bool random = false) const; + count countSelfLoopsManually(const InducedSubgraphView &G); +}; + +INSTANTIATE_TEST_SUITE_P(InstantiationName, InducedSubgraphViewGTest, + testing::Values(std::make_tuple(false, false), + std::make_tuple(true, false), std::make_tuple(false, true), + std::make_tuple(true, true))); + +bool InducedSubgraphViewGTest::isWeighted() const { + return std::get<0>(GetParam()); +} +bool InducedSubgraphViewGTest::isDirected() const { + return std::get<1>(GetParam()); +} + +GraphW InducedSubgraphViewGTest::createGraphW(count n) const { + bool weighted, directed; + std::tie(weighted, directed) = GetParam(); + GraphW G(n, weighted, directed); + return G; +} + +GraphW InducedSubgraphViewGTest::createGraphW(count n, count m) const { + GraphW G = createGraphW(n); + while (G.numberOfEdges() < m) { + const auto u = Aux::Random::index(n); + const auto v = Aux::Random::index(n); + if (u == v) + continue; + if (G.hasEdge(u, v)) + continue; + + const auto p = Aux::Random::probability(); + G.addEdge(u, v, p); + } + return G; +} + +std::pair +InducedSubgraphViewGTest::createInducedSubgraphView(count n, count m, bool random) const { + // Create bigger E-R graph, and then subset it + count N = static_cast(n * Aux::Random::real(2, 10)); + count M = static_cast(m * Aux::Random::real(2, 10)); + return createInducedSubgraphView(n, m, N, M, random); +} + +std::pair +InducedSubgraphViewGTest::createInducedSubgraphView(count n, count m, count N, count M, + bool random) const { + GraphW G = createGraphW(N); + while (G.numberOfEdges() < M) { + const auto u = Aux::Random::index(N); + const auto v = Aux::Random::index(N); + if (u == v) + continue; + if (G.hasEdge(u, v)) + continue; + + const auto p = Aux::Random::probability(); + G.addEdge(u, v, p); + } + + std::set subset; + index idx; + auto cb = [&](node u) { + if (idx++ < n) + subset.insert(u); + }; + + if (random) + G.forNodesInRandomOrder(cb); + else + G.forNodes(cb); + + return {G, InducedSubgraphView(G, subset)}; +} + +count InducedSubgraphViewGTest::countSelfLoopsManually(const InducedSubgraphView &G) { + count c = 0; + G.getOriginalGraph().forEdges([&](node u, node v) { + if (u == v && G.getNodeSubset().contains(u)) { + c += 1; + } + }); + return c; +} + +void InducedSubgraphViewGTest::SetUp() { + /* + * 0 + * . \ + * / \ + * / . + * 1 <-- 2 + * ^ \ .| + * | \/ | + * | / \ | + * |/ .. + * 3 <-- 4 + * + * move you pen from node to node: + * 3 -> 1 -> 0 -> 2 -> 1 -> 4 -> 3 -> 2 -> 4 + */ + n_house = 5; + m_house = 8; + + Ghouse = createGraphW(5); + houseEdgesOut = {{3, 1}, {1, 0}, {0, 2}, {2, 1}, {1, 4}, {4, 3}, {3, 2}, {2, 4}}; + Ahouse = {n_house, std::vector(n_house, 0.0)}; + edgeweight ew = 1.0; + for (auto &e : houseEdgesOut) { + node u = e.first; + node v = e.second; + Ghouse.addEdge(u, v, ew); + + Ahouse[u][v] = ew; + + if (!Ghouse.isDirected()) { + Ahouse[v][u] = ew; + } + + if (Ghouse.isWeighted()) { + ew += 1.0; + } + } +} + +/** CONSTRUCTORS **/ + +// TODO + +/** NODE MODIFIERS **/ + +TEST_P(InducedSubgraphViewGTest, testAddNode) { + auto [H, G] = createInducedSubgraphView(0, 0, 10, 25); + + ASSERT_FALSE(G.hasNode(0)); + ASSERT_FALSE(G.hasNode(1)); + ASSERT_EQ(0u, G.numberOfNodes()); + + G.addNode(0); + ASSERT_TRUE(G.hasNode(0)); + ASSERT_FALSE(G.hasNode(1)); + ASSERT_EQ(1u, G.numberOfNodes()); + + auto [H2, G2] = createInducedSubgraphView(2, 0, 10, 25); + ASSERT_TRUE(G2.hasNode(0)); + ASSERT_TRUE(G2.hasNode(1)); + ASSERT_FALSE(G2.hasNode(2)); + ASSERT_EQ(2u, G2.numberOfNodes()); + + G2.addNode(2); + G2.addNode(3); + ASSERT_TRUE(G2.hasNode(2)); + ASSERT_TRUE(G2.hasNode(3)); + ASSERT_FALSE(G2.hasNode(4)); + ASSERT_EQ(4u, G2.numberOfNodes()); +} + +TEST_P(InducedSubgraphViewGTest, testAddNodes) { + InducedSubgraphView G = createInducedSubgraphView(5); + G.addNodes(5); + + ASSERT_EQ(G.numberOfNodes(), 10); + ASSERT_TRUE(G.hasNode(9)); + + G.addNodes(90); + + ASSERT_EQ(G.numberOfNodes(), 100); + ASSERT_TRUE(G.hasNode(99)); +} + +TEST_P(InducedSubgraphViewGTest, testRemoveNode) { + auto testInducedSubgraphView = [&](InducedSubgraphView &G) { + count n = G.numberOfNodes(); + count z = n; + count m = G.numberOfEdges(); + for (node u = 0; u < z; ++u) { + count deg = G.degreeOut(u); + count degIn = G.isDirected() ? G.degreeIn(u) : 0; + G.removeNode(u); + --n; + m -= deg + degIn; + ASSERT_EQ(G.numberOfNodes(), n); + ASSERT_EQ(G.numberOfEdges(), m); + G.forNodes([&](node v) { ASSERT_EQ(G.hasNode(v), v > u); }); + } + }; + + InducedSubgraphView G1 = ErdosRenyiGenerator(200, 0.2, false).generate(); + InducedSubgraphView G2 = ErdosRenyiGenerator(200, 0.2, true).generate(); + testInducedSubgraphView(G1); + testInducedSubgraphView(G2); +} + +TEST_P(InducedSubgraphViewGTest, testHasNode) { + InducedSubgraphView G = createInducedSubgraphView(5); + + ASSERT_TRUE(G.hasNode(0)); + ASSERT_TRUE(G.hasNode(1)); + ASSERT_TRUE(G.hasNode(2)); + ASSERT_TRUE(G.hasNode(3)); + ASSERT_TRUE(G.hasNode(4)); + ASSERT_FALSE(G.hasNode(5)); + ASSERT_FALSE(G.hasNode(6)); + + G.removeNode(0); + G.removeNode(2); + G.addNode(); + + ASSERT_FALSE(G.hasNode(0)); + ASSERT_TRUE(G.hasNode(1)); + ASSERT_FALSE(G.hasNode(2)); + ASSERT_TRUE(G.hasNode(3)); + ASSERT_TRUE(G.hasNode(4)); + ASSERT_TRUE(G.hasNode(5)); + ASSERT_FALSE(G.hasNode(6)); +} + +TEST_P(InducedSubgraphViewGTest, testRestoreNode) { + InducedSubgraphView G = createInducedSubgraphView(4); + + ASSERT_EQ(4u, G.numberOfNodes()); + ASSERT_TRUE(G.hasNode(0)); + ASSERT_TRUE(G.hasNode(1)); + ASSERT_TRUE(G.hasNode(2)); + ASSERT_TRUE(G.hasNode(3)); + + G.removeNode(0); + + ASSERT_EQ(3u, G.numberOfNodes()); + ASSERT_FALSE(G.hasNode(0)); + ASSERT_TRUE(G.hasNode(1)); + ASSERT_TRUE(G.hasNode(2)); + ASSERT_TRUE(G.hasNode(3)); + + G.restoreNode(0); + + ASSERT_EQ(4u, G.numberOfNodes()); + ASSERT_TRUE(G.hasNode(0)); + ASSERT_TRUE(G.hasNode(1)); + ASSERT_TRUE(G.hasNode(2)); + ASSERT_TRUE(G.hasNode(3)); +} + +/** NODE PROPERTIES **/ + +TEST_P(InducedSubgraphViewGTest, testDegree) { + if (isDirected()) { + ASSERT_EQ(1u, this->Ghouse.degree(0)); + ASSERT_EQ(2u, this->Ghouse.degree(1)); + ASSERT_EQ(2u, this->Ghouse.degree(2)); + ASSERT_EQ(2u, this->Ghouse.degree(3)); + ASSERT_EQ(1u, this->Ghouse.degree(4)); + } else { + ASSERT_EQ(2u, this->Ghouse.degree(0)); + ASSERT_EQ(4u, this->Ghouse.degree(1)); + ASSERT_EQ(4u, this->Ghouse.degree(2)); + ASSERT_EQ(3u, this->Ghouse.degree(3)); + ASSERT_EQ(3u, this->Ghouse.degree(4)); + } +} + +TEST_P(InducedSubgraphViewGTest, testDegreeIn) { + if (isDirected()) { + ASSERT_EQ(1u, this->Ghouse.degreeIn(0)); + ASSERT_EQ(2u, this->Ghouse.degreeIn(1)); + ASSERT_EQ(2u, this->Ghouse.degreeIn(2)); + ASSERT_EQ(1u, this->Ghouse.degreeIn(3)); + ASSERT_EQ(2u, this->Ghouse.degreeIn(4)); + } else { + ASSERT_EQ(2u, this->Ghouse.degreeIn(0)); + ASSERT_EQ(4u, this->Ghouse.degreeIn(1)); + ASSERT_EQ(4u, this->Ghouse.degreeIn(2)); + ASSERT_EQ(3u, this->Ghouse.degreeIn(3)); + ASSERT_EQ(3u, this->Ghouse.degreeIn(4)); + } +} + +TEST_P(InducedSubgraphViewGTest, testDegreeOut) { + if (isDirected()) { + ASSERT_EQ(1u, this->Ghouse.degreeOut(0)); + ASSERT_EQ(2u, this->Ghouse.degreeOut(1)); + ASSERT_EQ(2u, this->Ghouse.degreeOut(2)); + ASSERT_EQ(2u, this->Ghouse.degreeOut(3)); + ASSERT_EQ(1u, this->Ghouse.degreeOut(4)); + } else { + ASSERT_EQ(2u, this->Ghouse.degreeOut(0)); + ASSERT_EQ(4u, this->Ghouse.degreeOut(1)); + ASSERT_EQ(4u, this->Ghouse.degreeOut(2)); + ASSERT_EQ(3u, this->Ghouse.degreeOut(3)); + ASSERT_EQ(3u, this->Ghouse.degreeOut(4)); + } +} + +TEST_P(InducedSubgraphViewGTest, testIsIsolated) { + ASSERT_FALSE(this->Ghouse.isIsolated(0)); + ASSERT_FALSE(this->Ghouse.isIsolated(1)); + ASSERT_FALSE(this->Ghouse.isIsolated(2)); + ASSERT_FALSE(this->Ghouse.isIsolated(3)); + ASSERT_FALSE(this->Ghouse.isIsolated(4)); + + this->Ghouse.addNode(); + ASSERT_TRUE(this->Ghouse.isIsolated(5)); + + this->Ghouse.removeEdge(1, 0); + ASSERT_FALSE(this->Ghouse.isIsolated(0)); + + this->Ghouse.removeEdge(0, 2); + ASSERT_TRUE(this->Ghouse.isIsolated(0)); + + this->Ghouse.addEdge(1, 0); + ASSERT_FALSE(this->Ghouse.isIsolated(0)); +} + +TEST_P(InducedSubgraphViewGTest, testWeightedDegree) { + // add self-loop + this->Ghouse.addEdge(2, 2, 0.75); + + if (isInducedSubgraphView()) { + ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(0)); + ASSERT_EQ(4 * defaultEdgeWeight, this->Ghouse.weightedDegree(1)); + ASSERT_EQ(5 * defaultEdgeWeight, this->Ghouse.weightedDegree(2)); + ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(3)); + ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(4)); + } + + if (isWeightedInducedSubgraphView()) { + ASSERT_EQ(5.0, this->Ghouse.weightedDegree(0)); + ASSERT_EQ(12.0, this->Ghouse.weightedDegree(1)); + ASSERT_EQ(22.75, this->Ghouse.weightedDegree(2)); + ASSERT_EQ(14.0, this->Ghouse.weightedDegree(3)); + ASSERT_EQ(19.0, this->Ghouse.weightedDegree(4)); + } + + if (isDirectedInducedSubgraphView()) { + // only count outgoing edges + ASSERT_EQ(1 * defaultEdgeWeight, this->Ghouse.weightedDegree(0)); + ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(1)); + ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(2)); + ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(3)); + ASSERT_EQ(1 * defaultEdgeWeight, this->Ghouse.weightedDegree(4)); + } + + if (isWeightedDirectedInducedSubgraphView()) { + // only sum weight of outgoing edges + ASSERT_EQ(3.0, this->Ghouse.weightedDegree(0)); + ASSERT_EQ(7.0, this->Ghouse.weightedDegree(1)); + ASSERT_EQ(12.75, this->Ghouse.weightedDegree(2)); + ASSERT_EQ(8.0, this->Ghouse.weightedDegree(3)); + ASSERT_EQ(6.0, this->Ghouse.weightedDegree(4)); + } +} + +TEST_P(InducedSubgraphViewGTest, testWeightedDegree2) { + // add self-loop + this->Ghouse.addEdge(2, 2, 0.75); + + if (isInducedSubgraphView()) { + ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(0, true)); + ASSERT_EQ(4 * defaultEdgeWeight, this->Ghouse.weightedDegree(1, true)); + ASSERT_EQ(6 * defaultEdgeWeight, this->Ghouse.weightedDegree(2, true)); + ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(3, true)); + ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(4, true)); + } + + if (isWeightedInducedSubgraphView()) { + ASSERT_EQ(5.0, this->Ghouse.weightedDegree(0, true)); + ASSERT_EQ(12.0, this->Ghouse.weightedDegree(1, true)); + ASSERT_EQ(23.5, this->Ghouse.weightedDegree(2, true)); + ASSERT_EQ(14.0, this->Ghouse.weightedDegree(3, true)); + ASSERT_EQ(19.0, this->Ghouse.weightedDegree(4, true)); + } + + if (isDirectedInducedSubgraphView()) { + // only count outgoing edges + ASSERT_EQ(1 * defaultEdgeWeight, this->Ghouse.weightedDegree(0, true)); + ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(1, true)); + ASSERT_EQ(4 * defaultEdgeWeight, this->Ghouse.weightedDegree(2, true)); + ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(3, true)); + ASSERT_EQ(1 * defaultEdgeWeight, this->Ghouse.weightedDegree(4, true)); + } + + if (isWeightedDirectedInducedSubgraphView()) { + // only sum weight of outgoing edges + ASSERT_EQ(3.0, this->Ghouse.weightedDegree(0, true)); + ASSERT_EQ(7.0, this->Ghouse.weightedDegree(1, true)); + ASSERT_EQ(13.5, this->Ghouse.weightedDegree(2, true)); + ASSERT_EQ(8.0, this->Ghouse.weightedDegree(3, true)); + ASSERT_EQ(6.0, this->Ghouse.weightedDegree(4, true)); + } +} + +TEST_P(InducedSubgraphViewGTest, testWeightedDegree3) { + constexpr count n = 100; + constexpr double p = 0.1; + + for (int seed : {1, 2, 3}) { + Aux::Random::setSeed(seed, false); + auto G = ErdosRenyiGenerator(n, p, isDirected()).generate(); + if (isWeighted()) { + GraphTools::randomizeWeights(G); + } + G.forNodes([&](node u) { + edgeweight wDeg = 0, wDegTwice = 0; + G.forNeighborsOf(u, [&](node v, edgeweight w) { + wDeg += w; + wDegTwice += (u == v) ? 2. * w : w; + }); + + EXPECT_DOUBLE_EQ(G.weightedDegree(u), wDeg); + EXPECT_DOUBLE_EQ(G.weightedDegree(u, true), wDegTwice); + + edgeweight wInDeg = 0, wInDegTwice = 0; + G.forInNeighborsOf(u, [&](node v, edgeweight w) { + wInDeg += w; + wInDegTwice += (u == v) ? 2. * w : w; + }); + + EXPECT_DOUBLE_EQ(G.weightedDegreeIn(u), wInDeg); + EXPECT_DOUBLE_EQ(G.weightedDegreeIn(u, true), wInDegTwice); + }); + } +} + +/** EDGE MODIFIERS **/ + +TEST_P(InducedSubgraphViewGTest, testAddEdge) { + InducedSubgraphView G = createInducedSubgraphView(3); + + // InducedSubgraphView without edges + ASSERT_EQ(0u, G.numberOfEdges()); + ASSERT_FALSE(G.hasEdge(0, 2)); + ASSERT_FALSE(G.hasEdge(0, 1)); + ASSERT_FALSE(G.hasEdge(1, 2)); + ASSERT_FALSE(G.hasEdge(2, 2)); + ASSERT_EQ(nullWeight, G.weight(0, 2)); + ASSERT_EQ(nullWeight, G.weight(0, 1)); + ASSERT_EQ(nullWeight, G.weight(1, 2)); + ASSERT_EQ(nullWeight, G.weight(2, 2)); + + // InducedSubgraphView with 2 normal edges + G.addEdge(0, 1, 4.51); + G.addEdge(1, 2, 2.39); + ASSERT_EQ(2u, G.numberOfEdges()); + ASSERT_FALSE(G.hasEdge(0, 2)); // was never added + ASSERT_TRUE(G.hasEdge(0, 1)); + ASSERT_TRUE(G.hasEdge(1, 2)); + ASSERT_FALSE(G.hasEdge(2, 2)); // will be added later + + // check weights + if (G.isWeighted()) { + ASSERT_EQ(4.51, G.weight(0, 1)); + ASSERT_EQ(2.39, G.weight(1, 2)); + } else { + ASSERT_EQ(defaultEdgeWeight, G.weight(0, 1)); + ASSERT_EQ(defaultEdgeWeight, G.weight(1, 2)); + } + + if (G.isDirected()) { + ASSERT_FALSE(G.hasEdge(1, 0)); + ASSERT_FALSE(G.hasEdge(2, 1)); + + // add edge in the other direction + // note: bidirectional edges are not supported, so both edges have different + // weights + G.addEdge(2, 1, 6.23); + ASSERT_TRUE(G.hasEdge(2, 1)); + if (G.isWeighted()) { + ASSERT_EQ(2.39, G.weight(1, 2)); + ASSERT_EQ(6.23, G.weight(2, 1)); + } else { + ASSERT_EQ(defaultEdgeWeight, G.weight(2, 1)); + } + } else { + ASSERT_TRUE(G.hasEdge(1, 0)); + ASSERT_TRUE(G.hasEdge(2, 1)); + if (G.isWeighted()) { + ASSERT_EQ(4.51, G.weight(1, 0)); + ASSERT_EQ(2.39, G.weight(2, 1)); + } else { + ASSERT_EQ(defaultEdgeWeight, G.weight(1, 0)); + ASSERT_EQ(defaultEdgeWeight, G.weight(2, 1)); + } + } + + // add self loop + G.addEdge(2, 2, 0.72); + ASSERT_TRUE(G.hasEdge(2, 2)); + if (G.isWeighted()) { + ASSERT_EQ(0.72, G.weight(2, 2)); + } else { + ASSERT_EQ(defaultEdgeWeight, G.weight(2, 2)); + } +} + +TEST_P(InducedSubgraphViewGTest, testRemoveEdge) { + double epsilon = 1e-6; + InducedSubgraphView G = createInducedSubgraphView(3); + + edgeweight ewBefore = G.totalEdgeWeight(); + + G.addEdge(0, 1, 3.14); + + if (G.isWeighted()) { + ASSERT_NEAR(ewBefore + 3.14, G.totalEdgeWeight(), epsilon); + } else { + ASSERT_NEAR(ewBefore + defaultEdgeWeight, G.totalEdgeWeight(), epsilon); + } + + G.addEdge(0, 0); + + ASSERT_EQ(2u, G.numberOfEdges()); + ASSERT_TRUE(G.hasEdge(0, 0)); + ASSERT_TRUE(G.hasEdge(0, 1)); + ASSERT_FALSE(G.hasEdge(2, 1)); + + // test remove regular edge + ewBefore = G.totalEdgeWeight(); + G.removeEdge(0, 1); + if (G.isWeighted()) { + ASSERT_NEAR(ewBefore - 3.14, G.totalEdgeWeight(), epsilon); + } else { + ASSERT_NEAR(ewBefore - defaultEdgeWeight, G.totalEdgeWeight(), epsilon); + } + + ASSERT_EQ(1u, G.numberOfEdges()); + ASSERT_TRUE(G.hasEdge(0, 0)); + ASSERT_FALSE(G.hasEdge(0, 1)); + ASSERT_FALSE(G.hasEdge(2, 1)); + + // test remove self-loop + G.addEdge(2, 1); + + ewBefore = G.totalEdgeWeight(); + G.removeEdge(0, 0); + if (G.isWeighted()) { + ASSERT_NEAR(ewBefore - defaultEdgeWeight, G.totalEdgeWeight(), epsilon); + } else { + ASSERT_NEAR(ewBefore - defaultEdgeWeight, G.totalEdgeWeight(), epsilon); + } + + ASSERT_EQ(1u, G.numberOfEdges()); + ASSERT_FALSE(G.hasEdge(0, 0)); + ASSERT_FALSE(G.hasEdge(0, 1)); + ASSERT_TRUE(G.hasEdge(2, 1)); + + // test from removeselfloops adapted for removeEdge + G = createInducedSubgraphView(2); + + ewBefore = G.totalEdgeWeight(); + + G.addEdge(0, 1); + G.addEdge(0, 0, 3.14); + G.addEdge(1, 1); + + if (G.isWeighted()) { + EXPECT_NEAR(ewBefore + 3.14 + 2 * defaultEdgeWeight, G.totalEdgeWeight(), epsilon); + } else { + EXPECT_NEAR(ewBefore + 3 * defaultEdgeWeight, G.totalEdgeWeight(), epsilon); + } + + EXPECT_EQ(3u, G.numberOfEdges()); + EXPECT_TRUE(G.hasEdge(0, 0)); + EXPECT_TRUE(G.hasEdge(0, 1)); + EXPECT_TRUE(G.hasEdge(1, 1)); + EXPECT_EQ(G.numberOfSelfLoops(), 2u); + + // remove self-loops + ewBefore = G.totalEdgeWeight(); + + G.removeEdge(0, 0); + G.removeEdge(1, 1); + + if (G.isWeighted()) { + EXPECT_NEAR(ewBefore - defaultEdgeWeight - 3.14, G.totalEdgeWeight(), epsilon); + } else { + EXPECT_NEAR(ewBefore - 2 * defaultEdgeWeight, G.totalEdgeWeight(), epsilon) + << "Weighted, directed: " << G.isWeighted() << ", " << G.isDirected(); + } + + EXPECT_EQ(1u, G.numberOfEdges()); + EXPECT_FALSE(G.hasEdge(0, 0)); + EXPECT_FALSE(G.hasEdge(1, 1)); + EXPECT_TRUE(G.hasEdge(0, 1)); + EXPECT_EQ(0u, G.numberOfSelfLoops()) + << "Weighted, directed: " << G.isWeighted() << ", " << G.isDirected(); +} + +TEST_P(InducedSubgraphViewGTest, testRemoveAllEdges) { + constexpr count n = 100; + constexpr double p = 0.2; + + for (int seed : {1, 2, 3}) { + Aux::Random::setSeed(seed, false); + auto g = ErdosRenyiGenerator(n, p, isDirected()).generate(); + if (isWeighted()) { + g = InducedSubgraphView(g, true, isDirected()); + } + + g.removeAllEdges(); + + EXPECT_EQ(g.numberOfEdges(), 0); + + count edgeCount = 0; + g.forEdges([&edgeCount](node, node) { ++edgeCount; }); + EXPECT_EQ(edgeCount, 0); + + g.forNodes([&](node u) { + EXPECT_EQ(g.degree(u), 0); + EXPECT_EQ(g.degree(u), 0); + }); + } +} + +TEST_P(InducedSubgraphViewGTest, testRemoveSelfLoops) { + constexpr count n = 100; + constexpr count nSelfLoops = 100; + constexpr double p = 0.2; + + for (int seed : {1, 2, 3}) { + Aux::Random::setSeed(seed, false); + auto g = ErdosRenyiGenerator(n, p, isDirected()).generate(); + if (isWeighted()) { + g = InducedSubgraphView(g, true, isDirected()); + } + + for (count i = 0; i < nSelfLoops; ++i) { + const auto u = GraphTools::randomNode(g); + g.addEdge(u, u); + } + + const auto numberOfSelfLoops = g.numberOfSelfLoops(); + const auto numberOfEdges = g.numberOfEdges(); + g.removeSelfLoops(); + + EXPECT_EQ(numberOfEdges - numberOfSelfLoops, g.numberOfEdges()); + EXPECT_EQ(g.numberOfSelfLoops(), 0); + g.forNodes([&g](const node u) { EXPECT_FALSE(g.hasEdge(u, u)); }); + } +} + +TEST_P(InducedSubgraphViewGTest, testRemoveMultiEdges) { + constexpr count n = 200; + constexpr double p = 0.1; + constexpr count nMultiEdges = 10; + constexpr count nMultiSelfLoops = 10; + + auto getGraphEdges = [](const InducedSubgraphView &G) { + std::vector> edges; + edges.reserve(G.numberOfEdges()); + + G.forEdges([&](const node u, const node v) { edges.push_back({u, v}); }); + + return edges; + }; + + for (int seed : {1, 2, 3}) { + Aux::Random::setSeed(seed, false); + auto g = ErdosRenyiGenerator(n, p, isDirected()).generate(); + if (isWeighted()) { + g = InducedSubgraphView(g, true, isDirected()); + } + + const auto edgeSet = getGraphEdges(g); + const auto m = g.numberOfEdges(); + + // Adding multiedges at random + for (count i = 0; i < nMultiEdges; ++i) { + const auto e = GraphTools::randomEdge(g); + g.addEdge(e.first, e.second); + } + + std::unordered_set uniqueSelfLoops; + // Adding multiple self-loops at random + for (count i = 0; i < nMultiSelfLoops; ++i) { + const auto u = GraphTools::randomNode(g); + g.addEdge(u, u); + g.addEdge(u, u); + uniqueSelfLoops.insert(u); + } + + EXPECT_EQ(g.numberOfEdges(), m + nMultiEdges + 2 * nMultiSelfLoops); + + g.removeMultiEdges(); + + EXPECT_EQ(g.numberOfEdges(), m + uniqueSelfLoops.size()); + g.removeSelfLoops(); + + EXPECT_EQ(g.numberOfEdges(), m); + auto edgeSet_ = getGraphEdges(g); + + for (count i = 0; i < g.numberOfEdges(); ++i) + EXPECT_EQ(edgeSet[i], edgeSet_[i]); + } +} + +TEST_P(InducedSubgraphViewGTest, testHasEdge) { + auto containsEdge = [&](std::pair e) { + auto it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e); + return it != this->houseEdgesOut.end(); + }; + + for (node u = 0; u < this->Ghouse.upperNodeIdBound(); u++) { + for (node v = 0; v < this->Ghouse.upperNodeIdBound(); v++) { + auto edge = std::make_pair(u, v); + auto edgeReverse = std::make_pair(v, u); + bool hasEdge = containsEdge(edge); + bool hasEdgeReverse = containsEdge(edgeReverse); + if (this->Ghouse.isDirected()) { + ASSERT_EQ(hasEdge, this->Ghouse.hasEdge(u, v)); + } else { + ASSERT_EQ(hasEdge || hasEdgeReverse, this->Ghouse.hasEdge(u, v)); + } + } + } +} + +/** GLOBAL PROPERTIES **/ + +TEST_P(InducedSubgraphViewGTest, testSelfLoopCountSimple) { + InducedSubgraphView G(Ghouse); + G.addEdge(0, 0); + EXPECT_EQ(1, G.numberOfSelfLoops()); +} + +TEST_P(InducedSubgraphViewGTest, testIsWeighted) { + ASSERT_EQ(isWeighted(), this->Ghouse.isWeighted()); +} + +TEST_P(InducedSubgraphViewGTest, testIsDirected) { + ASSERT_EQ(isDirected(), this->Ghouse.isDirected()); +} + +TEST_P(InducedSubgraphViewGTest, testIsEmpty) { + InducedSubgraphView G1 = createInducedSubgraphView(0); + InducedSubgraphView G2 = createInducedSubgraphView(2); + + ASSERT_TRUE(G1.isEmpty()); + ASSERT_FALSE(G2.isEmpty()); + + node v = G1.addNode(); + G2.removeNode(GraphTools::randomNode(G2)); + ASSERT_FALSE(G1.isEmpty()); + ASSERT_FALSE(G2.isEmpty()); + + G1.removeNode(v); + G2.removeNode(GraphTools::randomNode(G2)); + ASSERT_TRUE(G1.isEmpty()); + ASSERT_TRUE(G2.isEmpty()); +} + +TEST_P(InducedSubgraphViewGTest, testNumberOfNodes) { + ASSERT_EQ(this->n_house, this->Ghouse.numberOfNodes()); + + InducedSubgraphView G1 = createInducedSubgraphView(0); + ASSERT_EQ(0u, G1.numberOfNodes()); + G1.addNode(); + ASSERT_EQ(1u, G1.numberOfNodes()); + G1.addNode(); + ASSERT_EQ(2u, G1.numberOfNodes()); + G1.removeNode(0); + ASSERT_EQ(1u, G1.numberOfNodes()); + G1.removeNode(1); + ASSERT_EQ(0u, G1.numberOfNodes()); +} + +TEST_P(InducedSubgraphViewGTest, testNumberOfEdges) { + ASSERT_EQ(this->m_house, this->Ghouse.numberOfEdges()); + + InducedSubgraphView G1 = createInducedSubgraphView(5); + ASSERT_EQ(0u, G1.numberOfEdges()); + G1.addEdge(0, 1); + ASSERT_EQ(1u, G1.numberOfEdges()); + G1.addEdge(1, 2); + ASSERT_EQ(2u, G1.numberOfEdges()); + G1.removeEdge(0, 1); + ASSERT_EQ(1u, G1.numberOfEdges()); + G1.removeEdge(1, 2); + ASSERT_EQ(0u, G1.numberOfEdges()); +} + +TEST_P(InducedSubgraphViewGTest, testNumberOfSelfLoops) { + InducedSubgraphView G = createInducedSubgraphView(3); + G.addEdge(0, 1); + ASSERT_EQ(0u, G.numberOfSelfLoops()); + G.addEdge(0, 0); + ASSERT_EQ(1u, G.numberOfSelfLoops()); + G.addEdge(1, 1); + G.addEdge(1, 2); + ASSERT_EQ(2u, G.numberOfSelfLoops()); + G.removeEdge(0, 0); + ASSERT_EQ(1u, G.numberOfSelfLoops()); +} + +TEST_P(InducedSubgraphViewGTest, testSelfLoopConversion) { + Aux::Random::setSeed(1, false); + const count runs = 100; + const count n_max = 200; + for (index i = 0; i < runs; i++) { + bool directed = Aux::Random::probability() < 0.5; + count n = Aux::Random::integer(n_max); + InducedSubgraphView G(n, false, directed); + + G.forNodes([&](node v) { + double p = Aux::Random::probability(); + + if (p < 0.1) { // new node + n++; + G.addNode(); + } else { // new edge + node u = Aux::Random::integer(v, n - 1); // self-loops possible + G.addEdge(v, u); + } + }); + count measuredSelfLoops = countSelfLoopsManually(G); + EXPECT_EQ(G.numberOfSelfLoops(), measuredSelfLoops); + InducedSubgraphView G_converted(G, false, !directed); + EXPECT_EQ(G_converted.numberOfSelfLoops(), measuredSelfLoops); + } +} + +TEST_P(InducedSubgraphViewGTest, testUpperNodeIdBound) { + ASSERT_EQ(5u, this->Ghouse.upperNodeIdBound()); + + InducedSubgraphView G1 = createInducedSubgraphView(0); + ASSERT_EQ(0u, G1.upperNodeIdBound()); + G1.addNode(); + ASSERT_EQ(1u, G1.upperNodeIdBound()); + G1.addNode(); + ASSERT_EQ(2u, G1.upperNodeIdBound()); + G1.removeNode(1); + ASSERT_EQ(2u, G1.upperNodeIdBound()); + G1.addNode(); + ASSERT_EQ(3u, G1.upperNodeIdBound()); +} + +TEST_P(InducedSubgraphViewGTest, testCheckConsistency_MultiEdgeDetection) { + InducedSubgraphView G = createInducedSubgraphView(3); + ASSERT_TRUE(G.checkConsistency()); + G.addEdge(0, 1); + ASSERT_TRUE(G.checkConsistency()); + G.addEdge(0, 2); + G.addEdge(0, 1); + ASSERT_FALSE(G.checkConsistency()); + G.removeEdge(0, 1); + ASSERT_TRUE(G.checkConsistency()); + G.removeEdge(0, 1); + ASSERT_TRUE(G.checkConsistency()); +} + +/** EDGE ATTRIBUTES **/ + +TEST_P(InducedSubgraphViewGTest, testWeight) { + this->Ghouse.forNodes([&](node u) { + this->Ghouse.forNodes( + [&](node v) { ASSERT_EQ(this->Ahouse[u][v], this->Ghouse.weight(u, v)); }); + }); +} + +TEST_P(InducedSubgraphViewGTest, testSetWeight) { + InducedSubgraphView G = createInducedSubgraphView(10); + G.addEdge(0, 1); + G.addEdge(1, 2); + + if (isWeighted()) { + // edges should get weight defaultWeight on creation and setWeight should + // overwrite this + G.setWeight(1, 2, 2.718); + EXPECT_EQ(defaultEdgeWeight, G.weight(0, 1)); + EXPECT_EQ(2.718, G.weight(1, 2)); + if (isDirected()) { + EXPECT_EQ(nullWeight, G.weight(1, 0)); + EXPECT_EQ(nullWeight, G.weight(2, 1)); + } else { + // undirected graph is symmetric + EXPECT_EQ(defaultEdgeWeight, G.weight(1, 0)); + EXPECT_EQ(2.718, G.weight(2, 1)); + } + + // setting an edge weight should create the edge if it doesn't exists + ASSERT_FALSE(G.hasEdge(5, 6)); + G.setWeight(5, 6, 56.0); + ASSERT_EQ(56.0, G.weight(5, 6)); + ASSERT_EQ(isDirected() ? nullWeight : 56.0, G.weight(6, 5)); + ASSERT_TRUE(G.hasEdge(5, 6)); + + // directed graphs are not symmetric, undirected are + G.setWeight(2, 1, 5.243); + if (isDirected()) { + EXPECT_EQ(2.718, G.weight(1, 2)); + EXPECT_EQ(5.243, G.weight(2, 1)); + } else { + EXPECT_EQ(5.243, G.weight(1, 2)); + EXPECT_EQ(5.243, G.weight(2, 1)); + } + + // self-loop + G.addEdge(4, 4, 2.5); + ASSERT_EQ(2.5, G.weight(4, 4)); + G.setWeight(4, 4, 3.14); + ASSERT_EQ(3.14, G.weight(4, 4)); + } else { + EXPECT_ANY_THROW(G.setWeight(0, 1, 1.5)); + } +} + +TEST_P(InducedSubgraphViewGTest, increaseWeight) { + InducedSubgraphView G = createInducedSubgraphView(5); + G.addEdge(0, 1); + G.addEdge(1, 2); + G.addEdge(3, 4, 3.14); + + if (G.isWeighted()) { + G.increaseWeight(1, 2, 0.5); + G.increaseWeight(3, 4, -0.5); + + ASSERT_EQ(defaultEdgeWeight, G.weight(0, 1)); + ASSERT_EQ(defaultEdgeWeight + 0.5, G.weight(1, 2)); + ASSERT_EQ(3.14 - 0.5, G.weight(3, 4)); + + if (G.isDirected()) { + // reverse edges do net exist => weight should be nullWeight + ASSERT_EQ(nullWeight, G.weight(1, 0)); + ASSERT_EQ(nullWeight, G.weight(2, 1)); + ASSERT_EQ(nullWeight, G.weight(4, 3)); + } else { + ASSERT_EQ(defaultEdgeWeight, G.weight(1, 0)); + ASSERT_EQ(defaultEdgeWeight + 0.5, G.weight(2, 1)); + ASSERT_EQ(3.14 - 0.5, G.weight(3, 4)); + } + } else { + EXPECT_ANY_THROW(G.increaseWeight(1, 2, 0.3)); + EXPECT_ANY_THROW(G.increaseWeight(2, 3, 0.3)); // edge does not exists + } +} + +/** SUMS **/ + +TEST_P(InducedSubgraphViewGTest, testTotalEdgeWeight) { + InducedSubgraphView G1 = createInducedSubgraphView(5); + InducedSubgraphView G2 = createInducedSubgraphView(5); + G2.addEdge(0, 1, 3.14); + + if (this->Ghouse.isWeighted()) { + ASSERT_EQ(0.0, G1.totalEdgeWeight()); + ASSERT_EQ(3.14, G2.totalEdgeWeight()); + ASSERT_EQ(36.0, this->Ghouse.totalEdgeWeight()); + } else { + ASSERT_EQ(0 * defaultEdgeWeight, G1.totalEdgeWeight()); + ASSERT_EQ(1 * defaultEdgeWeight, G2.totalEdgeWeight()); + ASSERT_EQ(8 * defaultEdgeWeight, this->Ghouse.totalEdgeWeight()); + } +} + +/** Collections **/ + +TEST_P(InducedSubgraphViewGTest, testNodeIterator) { + Aux::Random::setSeed(42, false); + + auto testForward = [](const InducedSubgraphView &G) { + auto preIter = G.nodeRange().begin(); + auto postIter = G.nodeRange().begin(); + + G.forNodes([&](const node u) { + ASSERT_EQ(*preIter, u); + ASSERT_EQ(*postIter, u); + ++preIter; + postIter++; + }); + + ASSERT_EQ(preIter, G.nodeRange().end()); + ASSERT_EQ(postIter, G.nodeRange().end()); + + InducedSubgraphView G1(G); + + for (const auto u : Graph::NodeRange(G)) { + ASSERT_TRUE(G1.hasNode(u)); + G1.removeNode(u); + } + + ASSERT_EQ(G1.numberOfNodes(), 0); + }; + + auto testBackward = [](const InducedSubgraphView &G) { + const std::vector nodes(Graph::NodeRange(G).begin(), Graph::NodeRange(G).end()); + std::vector v; + G.forNodes([&](node u) { v.push_back(u); }); + + ASSERT_EQ(std::unordered_set(nodes.begin(), nodes.end()).size(), nodes.size()); + ASSERT_EQ(nodes.size(), G.numberOfNodes()); + + auto preIter = G.nodeRange().begin(); + auto postIter = G.nodeRange().begin(); + for (count i = 0; i < G.numberOfNodes(); ++i) { + ++preIter; + postIter++; + } + + ASSERT_EQ(preIter, G.nodeRange().end()); + ASSERT_EQ(postIter, G.nodeRange().end()); + auto vecIter = nodes.rbegin(); + while (vecIter != nodes.rend()) { + ASSERT_EQ(*vecIter, *(--preIter)); + if (postIter != G.nodeRange().end()) { + ASSERT_NE(*vecIter, *(postIter--)); + } else { + postIter--; + } + ASSERT_EQ(*vecIter, *postIter); + ++vecIter; + } + + ASSERT_EQ(preIter, G.nodeRange().begin()); + ASSERT_EQ(postIter, G.nodeRange().begin()); + }; + + InducedSubgraphView G(this->Ghouse); + testForward(G); + testBackward(G); + + G.removeNode(GraphTools::randomNode(G)); + G.removeNode(GraphTools::randomNode(G)); + + testForward(G); + testBackward(G); +} + +TEST_P(InducedSubgraphViewGTest, testEdgeIterator) { + InducedSubgraphView G(this->Ghouse); + + auto testForward = [&](const InducedSubgraphView &G) { + InducedSubgraphView G1(G); + auto preIter = G.edgeRange().begin(); + auto postIter = G.edgeRange().begin(); + + G.forEdges([&](node, node) { + ASSERT_EQ(preIter, postIter); + const auto edge = *preIter; + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + G1.removeEdge(edge.u, edge.v); + ++preIter; + postIter++; + }); + + ASSERT_EQ(G1.numberOfEdges(), 0); + ASSERT_EQ(preIter, G.edgeRange().end()); + ASSERT_EQ(postIter, G.edgeRange().end()); + + G1 = G; + for (const auto edge : Graph::EdgeRange(G)) { + ASSERT_TRUE(G1.hasEdge(edge.u, edge.v)); + G1.removeEdge(edge.u, edge.v); + } + + ASSERT_EQ(G1.numberOfEdges(), 0); + }; + + auto testForwardWeighted = [&](const InducedSubgraphView &G) { + InducedSubgraphView G1(G); + auto preIter = G.edgeWeightRange().begin(); + auto postIter = preIter; + + G.forEdges([&](node, node) { + ASSERT_EQ(preIter, postIter); + + const auto edge = *preIter; + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + ASSERT_DOUBLE_EQ(G.weight(edge.u, edge.v), edge.weight); + G1.removeEdge(edge.u, edge.v); + ++preIter; + postIter++; + }); + + ASSERT_EQ(G1.numberOfEdges(), 0); + ASSERT_EQ(preIter, G.edgeWeightRange().end()); + ASSERT_EQ(postIter, G.edgeWeightRange().end()); + + G1 = G; + for (const auto edge : Graph::EdgeWeightRange(G)) { + ASSERT_TRUE(G1.hasEdge(edge.u, edge.v)); + ASSERT_DOUBLE_EQ(G1.weight(edge.u, edge.v), edge.weight); + G1.removeEdge(edge.u, edge.v); + } + + ASSERT_EQ(G1.numberOfEdges(), 0); + }; + + auto testBackward = [&](const InducedSubgraphView &G) { + InducedSubgraphView G1(G); + auto preIter = G.edgeRange().begin(); + auto postIter = preIter; + G.forEdges([&](node, node) { + ++preIter; + postIter++; + }); + + ASSERT_EQ(preIter, G.edgeRange().end()); + ASSERT_EQ(postIter, G.edgeRange().end()); + + G.forEdges([&](node, node) { + --preIter; + postIter--; + ASSERT_EQ(preIter, postIter); + const auto edge = *preIter; + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + G1.removeEdge(edge.u, edge.v); + }); + + ASSERT_EQ(G1.numberOfEdges(), 0); + }; + + auto testBackwardWeighted = [&](const InducedSubgraphView &G) { + InducedSubgraphView G1(G); + auto preIter = G.edgeWeightRange().begin(); + auto postIter = preIter; + G.forEdges([&](node, node) { + ++preIter; + postIter++; + }); + + G.forEdges([&](node, node) { + --preIter; + postIter--; + ASSERT_EQ(preIter, postIter); + + const auto edge = *preIter; + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + ASSERT_DOUBLE_EQ(G.weight(edge.u, edge.v), edge.weight); + G1.removeEdge(edge.u, edge.v); + }); + + ASSERT_EQ(G1.numberOfEdges(), 0); + }; + + auto doTests = [&](const InducedSubgraphView &G) { + testForward(G); + testBackward(G); + testForwardWeighted(G); + testBackwardWeighted(G); + }; + + doTests(G); + + for (int seed : {1, 2, 3, 4, 5}) { + Aux::Random::setSeed(seed, false); + InducedSubgraphView G1(G); + for (int i = 0; i < 3; ++i) { + auto e = GraphTools::randomEdge(G1); + G1.removeEdge(e.first, e.second); + } + + doTests(G1); + } +} + +TEST_P(InducedSubgraphViewGTest, testNeighborsIterators) { + auto iter = this->Ghouse.neighborRange(1).begin(); + this->Ghouse.forNeighborsOf(1, [&](node v) { + ASSERT_TRUE(*iter == v); + ++iter; + }); + ASSERT_TRUE(iter == this->Ghouse.neighborRange(1).end()); + + if (this->Ghouse.isWeighted()) { + auto iterW = this->Ghouse.weightNeighborRange(1).begin(); + this->Ghouse.forNeighborsOf(1, [&](node v, edgeweight w) { + ASSERT_TRUE((*iterW).first == v); + ASSERT_TRUE((*iterW).second == w); + ++iterW; + }); + ASSERT_TRUE(iterW == this->Ghouse.weightNeighborRange(1).end()); + } + + if (this->Ghouse.isDirected()) { + auto inIter = this->Ghouse.inNeighborRange(1).begin(); + this->Ghouse.forInNeighborsOf(1, [&](node v) { + ASSERT_TRUE(*inIter == v); + ++inIter; + }); + ASSERT_TRUE(inIter == this->Ghouse.inNeighborRange(1).end()); + + if (this->Ghouse.isWeighted()) { + auto iterW = this->Ghouse.weightInNeighborRange(1).begin(); + this->Ghouse.forInNeighborsOf(1, [&](node v, edgeweight w) { + ASSERT_TRUE((*iterW).first == v); + ASSERT_TRUE((*iterW).second == w); + ++iterW; + }); + ASSERT_TRUE(iterW == this->Ghouse.weightInNeighborRange(1).end()); + } + } +} + +/** NODE ITERATORS **/ + +TEST_P(InducedSubgraphViewGTest, testForNodes) { + InducedSubgraphView G = createInducedSubgraphView(3); + std::vector visited(4, false); + G.forNodes([&](node v) { + ASSERT_FALSE(visited[v]); + if (v == 2) { + G.addNode(); + } + visited[v] = true; + }); + for (bool b : visited) { + ASSERT_TRUE(b); + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelForNodes) { + std::vector visited(Ghouse.upperNodeIdBound()); + this->Ghouse.parallelForNodes([&](node u) { visited[u] = u; }); + + Aux::Parallel::sort(visited.begin(), visited.end()); + + ASSERT_EQ(5u, visited.size()); + for (index i = 0; i < this->Ghouse.upperNodeIdBound(); i++) { + ASSERT_EQ(i, visited[i]); + } +} + +TEST_P(InducedSubgraphViewGTest, forNodesWhile) { + count n = 100; + InducedSubgraphView G = createInducedSubgraphView(n); + count stopAfter = 10; + count nodesSeen = 0; + + G.forNodesWhile([&]() { return nodesSeen < stopAfter; }, [&](node) { nodesSeen++; }); + + ASSERT_EQ(stopAfter, nodesSeen); +} + +TEST_P(InducedSubgraphViewGTest, testForNodesInRandomOrder) { + count n = 1000; + count samples = 100; + double maxAbsoluteError = 0.005; + InducedSubgraphView G = createInducedSubgraphView(n); + + node lastNode = n / 2; + count greaterLastNode = 0; + count smallerLastNode = 0; + std::vector visitCount(n, 0); + + for (count i = 0; i < samples; i++) { + G.forNodesInRandomOrder([&](node v) { + if (v > lastNode) { + greaterLastNode++; + } else { + smallerLastNode++; + } + visitCount[v]++; + lastNode = v; + }); + } + + for (node v = 0; v < n; v++) { + ASSERT_EQ(samples, visitCount[v]); + } + + ASSERT_NEAR(0.5, (double)greaterLastNode / n / samples, maxAbsoluteError); + ASSERT_NEAR(0.5, (double)smallerLastNode / n / samples, maxAbsoluteError); +} + +TEST_P(InducedSubgraphViewGTest, testForNodePairs) { + count n = 10; + count m = n * (n - 1) / 2; + InducedSubgraphView G = createInducedSubgraphView(n); + + // add all edges + G.forNodePairs([&](node u, node v) { + ASSERT_FALSE(G.hasEdge(u, v)); + G.addEdge(u, v); + ASSERT_TRUE(G.hasEdge(u, v)); + }); + + EXPECT_EQ(m, G.numberOfEdges()); + + // remove all edges + G.forNodePairs([&](node u, node v) { + ASSERT_TRUE(G.hasEdge(u, v)); + G.removeEdge(u, v); + ASSERT_FALSE(G.hasEdge(u, v)); + }); + + EXPECT_EQ(0u, G.numberOfEdges()); +} + +/** EDGE ITERATORS **/ + +TEST_P(InducedSubgraphViewGTest, testForEdges) { + InducedSubgraphView G = createInducedSubgraphView(4); + G.addEdge(0, 1); // 0 * 1 = 0 + G.addEdge(1, 2); // 1 * 2 = 2 + G.addEdge(3, 2); // 3 * 2 = 1 (mod 5) + G.addEdge(2, 2); // 2 * 2 = 4 + G.addEdge(3, 1); // 3 * 1 = 3 + + std::vector edgesSeen(5, false); + + G.forEdges([&](node u, node v) { + ASSERT_TRUE(G.hasEdge(u, v)); + index id = (u * v) % 5; + edgesSeen[id] = true; + }); + + for (auto b : edgesSeen) { + ASSERT_TRUE(b); + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedEdges) { + double epsilon = 1e-6; + + InducedSubgraphView G = createInducedSubgraphView(4); + G.addEdge(0, 1, 0.1); // 0 * 1 = 0 + G.addEdge(3, 2, 0.2); // 3 * 2 = 1 (mod 5) + G.addEdge(1, 2, 0.3); // 1 * 2 = 2 + G.addEdge(3, 1, 0.4); // 3 * 1 = 3 + G.addEdge(2, 2, 0.5); // 2 * 2 = 4 + + std::vector edgesSeen(5, false); + + edgeweight weightSum = 0; + G.forEdges([&](node u, node v, edgeweight ew) { + ASSERT_TRUE(G.hasEdge(u, v)); + ASSERT_EQ(G.weight(u, v), ew); + + index id = (u * v) % 5; + edgesSeen[id] = true; + if (G.isWeighted()) { + ASSERT_NEAR((id + 1) * 0.1, ew, epsilon); + } else { + ASSERT_EQ(defaultEdgeWeight, ew); + } + weightSum += ew; + }); + + for (auto b : edgesSeen) { + ASSERT_TRUE(b); + } + if (G.isWeighted()) { + ASSERT_NEAR(1.5, weightSum, epsilon); + } else { + ASSERT_NEAR(5 * defaultEdgeWeight, weightSum, epsilon); + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelForWeightedEdges) { + count n = 4; + InducedSubgraphView G = createInducedSubgraphView(n); + G.forNodePairs([&](node u, node v) { G.addEdge(u, v, 1.0); }); + + edgeweight weightSum = 0.0; + G.parallelForEdges([&](node, node, edgeweight ew) { +#pragma omp atomic + weightSum += ew; + }); + + ASSERT_EQ(6.0, weightSum) << "sum of edge weights should be 6 in every case"; +} + +TEST_P(InducedSubgraphViewGTest, testParallelForEdges) { + count n = 4; + InducedSubgraphView G = createInducedSubgraphView(n); + G.forNodePairs([&](node u, node v) { G.addEdge(u, v); }); + + edgeweight weightSum = 0.0; + G.parallelForEdges([&](node, node) { +#pragma omp atomic + weightSum += 1; + }); + + ASSERT_EQ(6.0, weightSum) << "sum of edge weights should be 6 in every case"; +} + +/** NEIGHBORHOOD ITERATORS **/ + +TEST_P(InducedSubgraphViewGTest, testForNeighborsOf) { + std::vector visited; + this->Ghouse.forNeighborsOf(3, [&](node u) { visited.push_back(u); }); + + Aux::Parallel::sort(visited.begin(), visited.end()); + + if (isDirected()) { + ASSERT_EQ(2u, visited.size()); + ASSERT_EQ(1u, visited[0]); + ASSERT_EQ(2u, visited[1]); + } else { + ASSERT_EQ(3u, visited.size()); + ASSERT_EQ(1u, visited[0]); + ASSERT_EQ(2u, visited[1]); + ASSERT_EQ(4u, visited[2]); + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedNeighborsOf) { + std::vector> visited; + this->Ghouse.forNeighborsOf( + 3, [&](node u, edgeweight ew) { visited.push_back(std::make_pair(u, ew)); }); + + // should sort after the first element + Aux::Parallel::sort(visited.begin(), visited.end()); + + if (isInducedSubgraphView()) { + ASSERT_EQ(3u, visited.size()); + ASSERT_EQ(1u, visited[0].first); + ASSERT_EQ(2u, visited[1].first); + ASSERT_EQ(4u, visited[2].first); + ASSERT_EQ(defaultEdgeWeight, visited[0].second); + ASSERT_EQ(defaultEdgeWeight, visited[1].second); + ASSERT_EQ(defaultEdgeWeight, visited[2].second); + } + + if (isWeightedInducedSubgraphView()) { + ASSERT_EQ(3u, visited.size()); + ASSERT_EQ(1u, visited[0].first); + ASSERT_EQ(2u, visited[1].first); + ASSERT_EQ(4u, visited[2].first); + ASSERT_EQ(1.0, visited[0].second); + ASSERT_EQ(7.0, visited[1].second); + ASSERT_EQ(6.0, visited[2].second); + } + + if (isDirectedInducedSubgraphView()) { + ASSERT_EQ(2u, visited.size()); + ASSERT_EQ(1u, visited[0].first); + ASSERT_EQ(2u, visited[1].first); + ASSERT_EQ(defaultEdgeWeight, visited[0].second); + ASSERT_EQ(defaultEdgeWeight, visited[1].second); + } + + if (isWeightedDirectedInducedSubgraphView()) { + ASSERT_EQ(2u, visited.size()); + ASSERT_EQ(1u, visited[0].first); + ASSERT_EQ(2u, visited[1].first); + ASSERT_EQ(1.0, visited[0].second); + ASSERT_EQ(7.0, visited[1].second); + } +} + +TEST_P(InducedSubgraphViewGTest, testForEdgesOf) { + count m = 0; + std::vector visited(this->m_house, 0); + + this->Ghouse.forNodes([&](node u) { + this->Ghouse.forEdgesOf(u, [&](node v, node w) { + // edges should be v to w, so if we iterate over edges from u, u should be + // equal v + EXPECT_EQ(u, v); + + auto e = std::make_pair(v, w); + auto it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e); + if (!isDirected() && it == this->houseEdgesOut.end()) { + auto e2 = std::make_pair(w, v); + it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e2); + } + + EXPECT_TRUE(it != this->houseEdgesOut.end()); + + // find index in edge array + int i = std::distance(this->houseEdgesOut.begin(), it); + if (isDirected()) { + // make sure edge was not visited before (would be visited twice) + EXPECT_EQ(0, visited[i]); + } + + // mark edge as visited + visited[i]++; + m++; + }); + }); + + if (isDirected()) { + // we iterated over all outgoing edges once + EXPECT_EQ(this->m_house, m); + for (auto c : visited) { + EXPECT_EQ(1, c); + } + } else { + // we iterated over all edges in both directions + EXPECT_EQ(2 * this->m_house, m); + for (auto c : visited) { + EXPECT_EQ(2, c); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedEdgesOf) { + count m = 0; + std::vector visited(this->m_house, 0); + double sumOfWeights = 0; + + this->Ghouse.forNodes([&](node u) { + this->Ghouse.forEdgesOf(u, [&](node v, node w, edgeweight ew) { + // edges should be v to w, so if we iterate over edges from u, u should be + // equal v + EXPECT_EQ(u, v); + sumOfWeights += ew; + auto e = std::make_pair(v, w); + auto it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e); + if (!isDirected() && it == this->houseEdgesOut.end()) { + auto e2 = std::make_pair(w, v); + it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e2); + } + + EXPECT_TRUE(it != this->houseEdgesOut.end()); + + // find index in edge array + int i = std::distance(this->houseEdgesOut.begin(), it); + if (isDirected()) { + // make sure edge was not visited before (would be visited twice) + EXPECT_EQ(0, visited[i]); + } + + // mark edge as visited + visited[i]++; + m++; + }); + }); + + if (isInducedSubgraphView()) { + EXPECT_EQ(sumOfWeights, m); + EXPECT_EQ(2 * this->m_house, m); + for (auto c : visited) { + EXPECT_EQ(2, c); + } + } + + if (isWeightedInducedSubgraphView()) { + // we iterated over all edges in both directions + EXPECT_EQ(2 * this->m_house, m); + EXPECT_EQ(sumOfWeights, 72); + for (auto c : visited) { + EXPECT_EQ(2, c); + } + } + + if (isDirectedInducedSubgraphView()) { + // we iterated over all outgoing edges once + EXPECT_EQ(this->m_house, m); + EXPECT_EQ(sumOfWeights, m); + for (auto c : visited) { + EXPECT_EQ(1, c); + } + } + + if (isWeightedDirectedInducedSubgraphView()) { + EXPECT_EQ(sumOfWeights, 36); + EXPECT_EQ(this->m_house, m); + for (auto c : visited) { + EXPECT_EQ(1, c); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testForInNeighborsOf) { + std::vector visited; + this->Ghouse.forInNeighborsOf(2, [&](node v) { visited.push_back(v); }); + Aux::Parallel::sort(visited.begin(), visited.end()); + + if (isDirected()) { + EXPECT_EQ(2u, visited.size()); + EXPECT_EQ(0u, visited[0]); + EXPECT_EQ(3u, visited[1]); + } else { + EXPECT_EQ(4u, visited.size()); + EXPECT_EQ(0u, visited[0]); + EXPECT_EQ(1u, visited[1]); + EXPECT_EQ(3u, visited[2]); + EXPECT_EQ(4u, visited[3]); + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedInNeighborsOf) { + std::vector> visited; + this->Ghouse.forInNeighborsOf(3, [&](node v, edgeweight ew) { visited.push_back({v, ew}); }); + Aux::Parallel::sort(visited.begin(), visited.end()); + + if (isInducedSubgraphView()) { + ASSERT_EQ(3u, visited.size()); + ASSERT_EQ(1u, visited[0].first); + ASSERT_EQ(2u, visited[1].first); + ASSERT_EQ(4u, visited[2].first); + ASSERT_EQ(defaultEdgeWeight, visited[0].second); + ASSERT_EQ(defaultEdgeWeight, visited[1].second); + ASSERT_EQ(defaultEdgeWeight, visited[2].second); + } + + if (isWeightedInducedSubgraphView()) { + ASSERT_EQ(3u, visited.size()); + ASSERT_EQ(1u, visited[0].first); + ASSERT_EQ(2u, visited[1].first); + ASSERT_EQ(4u, visited[2].first); + ASSERT_EQ(1.0, visited[0].second); + ASSERT_EQ(7.0, visited[1].second); + ASSERT_EQ(6.0, visited[2].second); + } + + if (isDirectedInducedSubgraphView()) { + ASSERT_EQ(1u, visited.size()); + ASSERT_EQ(4u, visited[0].first); + ASSERT_EQ(defaultEdgeWeight, visited[0].second); + } + + if (isWeightedDirectedInducedSubgraphView()) { + ASSERT_EQ(1u, visited.size()); + ASSERT_EQ(4u, visited[0].first); + ASSERT_EQ(6.0, visited[0].second); + } +} + +TEST_P(InducedSubgraphViewGTest, testForInEdgesOf) { + std::vector visited(this->n_house, false); + this->Ghouse.forInEdgesOf(3, [&](node u, node v) { + ASSERT_EQ(3u, u); + if (isDirected()) { + ASSERT_TRUE(this->Ahouse[v][u] > 0.0); + ASSERT_TRUE(this->Ghouse.hasEdge(v, u)); + } + ASSERT_FALSE(visited[v]); + visited[v] = true; + }); + + if (isDirected()) { + EXPECT_FALSE(visited[0]); + EXPECT_FALSE(visited[1]); + EXPECT_FALSE(visited[2]); + EXPECT_FALSE(visited[3]); + EXPECT_TRUE(visited[4]); + } else { + EXPECT_FALSE(visited[0]); + EXPECT_TRUE(visited[1]); + EXPECT_TRUE(visited[2]); + EXPECT_FALSE(visited[3]); + EXPECT_TRUE(visited[4]); + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedInEdgesOf) { + // add self-loop + this->Ghouse.addEdge(3, 3, 2.5); + this->Ahouse[3][3] = 2.5; + + std::vector visited(this->n_house, -1.0); + this->Ghouse.forInEdgesOf(3, [&](node v, node u, edgeweight ew) { + ASSERT_EQ(-1.0, visited[v]); + visited[u] = ew; + }); + + if (isInducedSubgraphView()) { + ASSERT_EQ(-1.0, visited[0]); + ASSERT_EQ(defaultEdgeWeight, visited[1]); + ASSERT_EQ(defaultEdgeWeight, visited[2]); + ASSERT_EQ(defaultEdgeWeight, visited[3]); + ASSERT_EQ(defaultEdgeWeight, visited[4]); + } + + if (isWeightedInducedSubgraphView()) { + ASSERT_EQ(-1.0, visited[0]); + ASSERT_EQ(this->Ahouse[3][1], visited[1]); + ASSERT_EQ(this->Ahouse[3][2], visited[2]); + ASSERT_EQ(this->Ahouse[3][3], visited[3]); + ASSERT_EQ(this->Ahouse[3][4], visited[4]); + } + + if (isDirectedInducedSubgraphView()) { + ASSERT_EQ(-1.0, visited[0]); + ASSERT_EQ(-1.0, visited[1]); + ASSERT_EQ(-1.0, visited[2]); + ASSERT_EQ(defaultEdgeWeight, visited[3]); + ASSERT_EQ(defaultEdgeWeight, visited[4]); + } + + if (isWeightedDirectedInducedSubgraphView()) { + ASSERT_EQ(-1.0, visited[0]); + ASSERT_EQ(-1.0, visited[1]); + ASSERT_EQ(-1.0, visited[2]); + ASSERT_EQ(this->Ahouse[3][3], visited[3]); + ASSERT_EQ(this->Ahouse[4][3], visited[4]); + } +} + +/** REDUCTION ITERATORS **/ + +TEST_P(InducedSubgraphViewGTest, testParallelSumForNodes) { + count n = 10; + InducedSubgraphView G = createInducedSubgraphView(n); + double sum = G.parallelSumForNodes([](node v) { return 2 * v + 0.5; }); + + double expected_sum = n * (n - 1) + n * 0.5; + ASSERT_EQ(expected_sum, sum); +} + +TEST_P(InducedSubgraphViewGTest, testParallelSumForWeightedEdges) { + double sum = + this->Ghouse.parallelSumForEdges([](node, node, edgeweight ew) { return 1.5 * ew; }); + + double expected_sum = 1.5 * this->Ghouse.totalEdgeWeight(); + ASSERT_EQ(expected_sum, sum); +} + +/** GRAPH SEARCHES **/ + +TEST_P(InducedSubgraphViewGTest, testEdgeIndexGenerationDirected) { + InducedSubgraphView G = InducedSubgraphView(10, false, true); + G.addEdge(2, 0); + G.addEdge(2, 1); + G.addEdge(2, 2); + G.addEdge(5, 6); + G.addEdge(6, 5); + G.addEdge(1, 2); + + G.indexEdges(); + + // Check consecutiveness of edgeids according to edge iterators + edgeid expectedId = 0; + G.forEdges([&](node u, node v) { EXPECT_EQ(expectedId++, G.edgeId(u, v)); }); + + // Add some more edges + EXPECT_EQ(6, G.upperEdgeIdBound()); + G.addEdge(8, 9); + EXPECT_EQ(7, G.upperEdgeIdBound()); + G.addEdge(9, 8); + + // Check that asymmetric edges do not have the same id + EXPECT_NE(G.edgeId(6, 5), G.edgeId(5, 6)); + EXPECT_NE(G.edgeId(2, 1), G.edgeId(1, 2)); + EXPECT_NE(G.edgeId(9, 8), G.edgeId(8, 9)); + EXPECT_EQ(7, G.edgeId(9, 8)); + EXPECT_EQ(8, G.upperEdgeIdBound()); +} + +TEST_P(InducedSubgraphViewGTest, testEdgeIndexGenerationUndirected) { + InducedSubgraphView G = InducedSubgraphView(10, false, false); + + G.addEdge(0, 0); + G.addEdge(2, 0); + G.addEdge(2, 1); + G.addEdge(2, 2); + G.addEdge(5, 6); + + G.indexEdges(); + + // Check consecutiveness of edgeids according to edge iterators + edgeid expectedId = 0; + G.forEdges([&](node u, node v) { EXPECT_EQ(expectedId++, G.edgeId(u, v)); }); + + // Add some more edges. This will likely destroy consecutiveness... + G.addEdge(3, 4); + G.addEdge(7, 8); + EXPECT_EQ(6, G.edgeId(7, 8)); + EXPECT_EQ(7, G.upperEdgeIdBound()); + + // Anyway, check uniqueness and validity of the edgeids + std::set ids; + edgeid upperEdgeIdBound = G.upperEdgeIdBound(); + + G.forEdges([&](node u, node v) { + edgeid id = G.edgeId(u, v); + EXPECT_EQ(id, G.edgeId(v, u)); + EXPECT_LT(id, upperEdgeIdBound); + + EXPECT_NE(none, id); + EXPECT_FALSE(ids.erase(id)); + ids.insert(id); + }); +} + +TEST_P(InducedSubgraphViewGTest, testEdgeIndexResolver) { + InducedSubgraphView G = createInducedSubgraphView(10); + G.indexEdges(); + + G.addEdge(0, 0); + G.addEdge(5, 6); + G.addEdge(2, 2); + + if (G.isDirected()) + G.addEdge(3, 2); + + std::map, edgeid> expectedEdges; + expectedEdges[std::make_pair(0, 0)] = 0; + expectedEdges[std::make_pair(5, 6)] = 1; + expectedEdges[std::make_pair(2, 2)] = 2; + expectedEdges[std::make_pair(3, 2)] = 3; + + G.forEdges([&](node, node, edgeid eid) { + auto edge = G.edgeById(eid); + EXPECT_EQ(expectedEdges[edge], eid); + }); +} + +TEST_P(InducedSubgraphViewGTest, testForEdgesWithIds) { + std::vector graphs; + graphs.emplace_back(10, false, false); + graphs.emplace_back(10, false, true); + graphs.emplace_back(10, true, false); + graphs.emplace_back(10, true, true); + + for (auto graph = graphs.begin(); graph != graphs.end(); ++graph) { + graph->addEdge(0, 0); + graph->addEdge(1, 2); + graph->addEdge(4, 5); + + // No edge indices + + count m = 0; + graph->forEdges([&](node, node, edgeid eid) { + EXPECT_EQ(none, eid); + m++; + }); + ASSERT_EQ(3u, m); + + // With edge indices + graph->indexEdges(); + + edgeid expectedId = 0; + m = 0; + graph->forEdges([&](node, node, edgeid eid) { + EXPECT_EQ(expectedId++, eid); + EXPECT_LT(eid, graph->upperEdgeIdBound()); + m++; + }); + ASSERT_EQ(3u, m); + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedEdgesWithIds) { + std::vector graphs; + graphs.emplace_back(10, false, false); + graphs.emplace_back(10, false, true); + graphs.emplace_back(10, true, false); + graphs.emplace_back(10, true, true); + + for (auto graph = graphs.begin(); graph != graphs.end(); ++graph) { + graph->addEdge(0, 0, 2); + graph->addEdge(1, 2, 2); + graph->addEdge(4, 5, 2); + + // No edge indices + + count m = 0; + edgeweight sum = 0; + graph->forEdges([&](node, node, edgeweight ew, edgeid eid) { + EXPECT_EQ(none, eid); + m++; + sum += ew; + }); + ASSERT_EQ(3u, m); + + if (graph->isWeighted()) { + ASSERT_EQ(6.0, sum); + } else { + ASSERT_EQ(3.0, sum); + } + + // With edge indices + graph->indexEdges(); + + edgeid expectedId = 0; + m = 0; + sum = .0; + graph->forEdges([&](node, node, edgeweight ew, edgeid eid) { + EXPECT_EQ(expectedId++, eid); + EXPECT_LT(eid, graph->upperEdgeIdBound()); + m++; + sum += ew; + }); + ASSERT_EQ(3u, m); + + if (graph->isWeighted()) { + ASSERT_EQ(6.0, sum); + } else { + ASSERT_EQ(3.0, sum); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelForEdgesWithIds) { + std::vector graphs; + graphs.emplace_back(10, false, false); + graphs.emplace_back(10, false, true); + graphs.emplace_back(10, true, false); + graphs.emplace_back(10, true, true); + + for (auto graph = graphs.begin(); graph != graphs.end(); ++graph) { + graph->addEdge(0, 0); + graph->addEdge(1, 2); + graph->addEdge(4, 5); + + // No edge indices + count m = 0; + edgeid sumedgeid = 0; + graph->parallelForEdges([&](node, node, edgeid eid) { +#pragma omp atomic + m++; + ASSERT_EQ(none, eid); + }); + ASSERT_EQ(3u, m); + + // With edge indices + graph->indexEdges(); + + m = 0; + edgeid expectedId = 0; + sumedgeid = 0; + graph->parallelForEdges([&](node, node, edgeid eid) { +#pragma omp atomic + expectedId++; +#pragma omp atomic + sumedgeid += eid; +#pragma omp atomic + m++; + }); + ASSERT_EQ(expectedId, graph->upperEdgeIdBound()); + ASSERT_EQ(sumedgeid, ((graph->upperEdgeIdBound() - 1) * graph->upperEdgeIdBound()) / 2); + ASSERT_EQ(3u, m); + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelForWeightedEdgesWithIds) { + std::vector graphs; + graphs.emplace_back(10, false, false); + graphs.emplace_back(10, false, true); + graphs.emplace_back(10, true, false); + graphs.emplace_back(10, true, true); + + for (auto graph = graphs.begin(); graph != graphs.end(); ++graph) { + graph->addEdge(0, 0, 2); + graph->addEdge(1, 2, 2); + graph->addEdge(4, 5, 2); + + // No edge indices + + count m = 0; + edgeweight sum = 0; + edgeid sumedgeid = 0; + graph->parallelForEdges([&](node, node, edgeweight ew, edgeid eid) { +#pragma omp atomic + m++; +#pragma omp atomic + sum += ew; + ASSERT_EQ(none, eid); + }); + ASSERT_EQ(3u, m); + + if (graph->isWeighted()) { + ASSERT_EQ(6.0, sum); + } else { + ASSERT_EQ(3.0, sum); + } + + // With edge indices + graph->indexEdges(); + + m = 0; + sum = .0; + edgeid expectedId = 0; + sumedgeid = 0; + graph->parallelForEdges([&](node, node, edgeweight ew, edgeid eid) { +#pragma omp atomic + expectedId++; +#pragma omp atomic + sumedgeid += eid; +#pragma omp atomic + m++; +#pragma omp atomic + sum += ew; + }); + ASSERT_EQ(expectedId, graph->upperEdgeIdBound()); + ASSERT_EQ(sumedgeid, ((graph->upperEdgeIdBound() - 1) * graph->upperEdgeIdBound()) / 2); + ASSERT_EQ(3u, m); + + if (graph->isWeighted()) { + ASSERT_EQ(6.0, sum); + } else { + ASSERT_EQ(3.0, sum); + } + } +} + +/*TEST_P(GraphGTest, testInForEdgesUndirected) { + METISGraphReader reader; + InducedSubgraphView G = reader.read("input/PGPgiantcompo.graph"); + DEBUG(G.upperNodeIdBound()); + node u = 5474; + G.forInEdgesOf(u, [&](node u, node z, edgeweight w){ + DEBUG("(1) node: ", u, " neigh:", z, " weight: ", w); + }); + G.forEdgesOf(u, [&](node u, node z, edgeweight w){ + DEBUG("(2) node: ", u, " neigh:", z, " weight: ", w); + }); + + + node source = 1492; + DynBFS bfs(G, source, false); + bfs.run(); + + std::vector > choices1; + G.forInEdgesOf(5474, [&](node t, node z, edgeweight w){ + INFO("considered edge (1): ", t, z, w); + if (Aux::NumericTools::logically_equal(bfs.distance(t), bfs.distance(z) + +w)) { + // workaround for integer overflow in large graphs + bigfloat tmp = bfs.getNumberOfPaths(z) / bfs.getNumberOfPaths(t); + double weight; + tmp.ToDouble(weight); + choices1.emplace_back(z, weight); + } + }); + std::vector > choices2; + G.forEdgesOf(5474, [&](node t, node z, edgeweight w){ + INFO("considered edge (2): ", t, z, w); + if (Aux::NumericTools::logically_equal(bfs.distance(t), bfs.distance(z) + +w)) { + // workaround for integer overflow in large graphs + bigfloat tmp = bfs.getNumberOfPaths(z) / bfs.getNumberOfPaths(t); + double weight; + tmp.ToDouble(weight); + choices2.emplace_back(z, weight); + } + }); + + INFO(choices1); + INFO(choices2); +} +*/ + +TEST_P(InducedSubgraphViewGTest, testSortEdges) { + InducedSubgraphView G = this->Ghouse; + + for (int i = 0; i < 2; ++i) { + if (i > 0) { + G.indexEdges(); + } + + InducedSubgraphView origG = G; + + G.sortEdges(); + + std::vector> edges; + edges.reserve(origG.numberOfEdges() * 4); + + std::vector> outEdges; + origG.forNodes([&](node u) { + origG.forEdgesOf(u, [&](node, node v, edgeweight w, edgeid eid) { + outEdges.emplace_back(v, w, eid); + }); + + Aux::Parallel::sort(outEdges.begin(), outEdges.end()); + + for (auto x : outEdges) { + edges.emplace_back(u, std::get<0>(x), std::get<1>(x), std::get<2>(x)); + } + + outEdges.clear(); + + origG.forInEdgesOf(u, [&](node, node v, edgeweight w, edgeid eid) { + outEdges.emplace_back(v, w, eid); + }); + + Aux::Parallel::sort(outEdges.begin(), outEdges.end()); + + for (auto x : outEdges) { + edges.emplace_back(u, std::get<0>(x), std::get<1>(x), std::get<2>(x)); + } + + outEdges.clear(); + }); + + auto it = edges.begin(); + + G.forNodes([&](node u) { + G.forEdgesOf(u, [&](node u, node v, edgeweight w, edgeid eid) { + ASSERT_NE(it, edges.end()); + EXPECT_EQ(*it, std::make_tuple(u, v, w, eid)) + << "Out edge (" << u << ", " << v << ", " << w << ", " << eid + << ") was expected to be (" << std::get<0>(*it) << ", " << std::get<1>(*it) + << ", " << std::get<2>(*it) << ", " << std::get<3>(*it) << ")"; + ++it; + }); + G.forInEdgesOf(u, [&](node u, node v, edgeweight w, edgeid eid) { + ASSERT_NE(it, edges.end()); + EXPECT_EQ(*it, std::make_tuple(u, v, w, eid)) + << "In edge (" << u << ", " << v << ", " << w << ", " << eid + << ") was expected to be (" << std::get<0>(*it) << ", " << std::get<1>(*it) + << ", " << std::get<2>(*it) << ", " << std::get<3>(*it) << ")"; + ++it; + }); + }); + } +} + +TEST_P(InducedSubgraphViewGTest, testEdgeIdsSortingAfterRemove) { + constexpr node n = 100; + + Aux::Random::setSeed(42, true); + InducedSubgraphView G = createInducedSubgraphView(n, 10 * n); + G.sortEdges(); + G.indexEdges(); + auto original = G; + + // remove edges + while (2 * G.numberOfEdges() > original.numberOfEdges()) { + const auto e = GraphTools::randomEdge(G, false); + G.setKeepEdgesSorted(); + G.removeEdge(e.first, e.second); // with sorting after each removal + original.removeEdge(e.first, e.second); // without sorting + } + + original.sortEdges(); // calling sort only once + + G.forNodes([&](node u) { + std::vector allNeighborsOfG; + + G.forNeighborsOf(u, + [&](node, node v, edgeweight, edgeid) { allNeighborsOfG.push_back(v); }); + + std::vector allNeighborsOfOriginal; + + original.forNeighborsOf( + u, [&](node, node v, edgeweight, edgeid) { allNeighborsOfOriginal.push_back(v); }); + + // check that both neighbor vectors are equivalent + EXPECT_EQ(allNeighborsOfG.size(), allNeighborsOfOriginal.size()); + for (index i = 0; i < allNeighborsOfG.size(); ++i) { + EXPECT_EQ(allNeighborsOfG[i], allNeighborsOfOriginal[i]); + } + + if (!isDirected()) + return; + + // directed + + std::vector allInNeighborsOfG; + + G.forInNeighborsOf( + u, [&](node, node v, edgeweight, edgeid) { allInNeighborsOfG.push_back(v); }); + std::vector allInNeighborsOfOriginal; + + original.forInNeighborsOf( + u, [&](node, node v, edgeweight, edgeid) { allInNeighborsOfOriginal.push_back(v); }); + + // check that both in-neighbor vectors are equivalent + EXPECT_EQ(allInNeighborsOfG.size(), allInNeighborsOfOriginal.size()); + for (index i = 0; i < allInNeighborsOfG.size(); ++i) { + EXPECT_EQ(allInNeighborsOfG[i], allInNeighborsOfOriginal[i]); + } + }); +} + +TEST_P(InducedSubgraphViewGTest, testEdgeIdsConsistencyAfterRemove) { + constexpr node n = 100; + + Aux::Random::setSeed(42, true); + InducedSubgraphView G = createInducedSubgraphView(n, 10 * n); + G.sortEdges(); + G.indexEdges(); + auto original = G; + + // remove edges + G.setMaintainCompactEdges(); + while (2 * G.numberOfEdges() > original.numberOfEdges()) { + const auto e = GraphTools::randomEdge(G, false); + G.removeEdge(e.first, e.second); // re-indexing after each removal + original.removeEdge(e.first, e.second); // not re-indexing + } + + original.indexEdges(true); // re-indexing only once + + std::vector existingIDs(G.upperEdgeIdBound(), false); + + G.forNodes([&](node u) { + G.forNeighborsOf(u, [&](node, node v, edgeweight, edgeid id) { + existingIDs[id] = true; + // check that both graphs have the same edge IDs + ASSERT_EQ(id, original.edgeId(u, v)); + }); + + if (!isDirected()) + return; + + G.forInNeighborsOf( + u, [&](node, node v, edgeweight, edgeid id) { ASSERT_EQ(id, original.edgeId(v, u)); }); + }); + + // check that all IDs exist without gaps in between + for (auto ID : existingIDs) { + ASSERT_TRUE(ID); + } +} + +TEST_P(InducedSubgraphViewGTest, testEdgeIdsAfterRemoveWithoutSortingOrIDs) { + constexpr node n = 100; + + Aux::Random::setSeed(42, true); + InducedSubgraphView G = createInducedSubgraphView(n, 10 * n); + G.indexEdges(); + auto original = G; + + // remove some nodes and edges + G.removeNode(5); + G.removeNode(10); + while (2 * G.numberOfEdges() > original.numberOfEdges()) { + const auto e = GraphTools::randomEdge(G, false); + G.removeEdge(e.first, e.second); + } + ASSERT_GT(G.numberOfEdges(), original.numberOfEdges() / 3); + + // check that the remaining edges still have the same ids + G.forNodes([&](node u) { + G.forNeighborsOf( + u, [&](node, node v, edgeweight, edgeid id) { ASSERT_EQ(id, original.edgeId(u, v)); }); + + if (!isDirected()) + return; + + G.forInNeighborsOf( + u, [&](node, node v, edgeweight, edgeid id) { ASSERT_EQ(id, original.edgeId(v, u)); }); + }); +} + +TEST(GraphGTest, testSortNeighborsUndirectedGraph) { + InducedSubgraphView G(6); + G.addEdge(0, 3); + G.addEdge(0, 5); + G.addEdge(0, 4); + G.addEdge(1, 3); + G.addEdge(1, 5); + G.addEdge(1, 4); + G.addEdge(2, 5); + G.addEdge(2, 4); + G.addEdge(2, 3); + G.addEdge(4, 3); + G.addEdge(4, 5); + + std::unordered_map> originalNeighbors; + + G.forNodes([&](const node currentNode) { + originalNeighbors[currentNode] = std::vector(G.neighborRange(currentNode).begin(), + G.neighborRange(currentNode).end()); + }); + + // Sort neighbors + G.forNodes([&](const node currentNode) { + G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { + return neighbor1 < neighbor2; + }); + }); + + // Validate sorting for outgoing neighbors + G.forNodes([&](const node currentNode) { + const auto &sortedNeighbors = G.neighborRange(currentNode); + std::vector sortedNeighborVector(sortedNeighbors.begin(), sortedNeighbors.end()); + EXPECT_TRUE(std::ranges::is_sorted(sortedNeighborVector)); + if (!std::ranges::is_sorted(originalNeighbors[currentNode])) { + EXPECT_NE(originalNeighbors[currentNode], sortedNeighborVector); + } + }); +} + +TEST(GraphGTest, testSortNeighborsUndirectedIndexedGraph) { + InducedSubgraphView G(6); + G.addEdge(0, 3); + G.addEdge(0, 5); + G.addEdge(0, 4); + G.addEdge(1, 3); + G.addEdge(1, 5); + G.addEdge(1, 4); + G.addEdge(2, 5); + G.addEdge(2, 4); + G.addEdge(2, 3); + G.indexEdges(); + + std::unordered_map> originalNeighbors; + std::unordered_map> originalEdgeIds; + + G.forNodes([&](const node currentNode) { + originalNeighbors[currentNode] = std::vector(G.neighborRange(currentNode).begin(), + G.neighborRange(currentNode).end()); + for (size_t i = 0; i < G.degreeOut(currentNode); ++i) { + originalEdgeIds[currentNode].push_back(G.getIthNeighborWithId(currentNode, i).second); + } + }); + + // Sort neighbors + G.forNodes([&](const node currentNode) { + G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { + return neighbor1 < neighbor2; + }); + }); + + G.forNodes([&](const node currentNode) { + const auto &sortedNeighbors = G.neighborRange(currentNode); + std::vector sortedNeighborVector(sortedNeighbors.begin(), sortedNeighbors.end()); + EXPECT_TRUE(std::ranges::is_sorted(sortedNeighborVector)); + if (!std::ranges::is_sorted(originalNeighbors[currentNode])) { + EXPECT_NE(originalNeighbors[currentNode], sortedNeighborVector); + } + + // Validate that indices are sorted according to the sorting of the neighbors + for (size_t i = 0; i < sortedNeighborVector.size(); ++i) { + node neighbor = sortedNeighborVector[i]; + auto it = std::ranges::find(originalNeighbors[currentNode], neighbor); + EXPECT_NE(it, originalNeighbors[currentNode].end()); + size_t originalIndex = std::distance(originalNeighbors[currentNode].begin(), it); + EXPECT_EQ(G.getIthNeighborWithId(currentNode, i).second, + originalEdgeIds[currentNode][originalIndex]); + } + }); +} + +TEST(GraphGTest, testSortNeighborsWeightedUndirectedGraphByWeights) { + InducedSubgraphView G(6, true); + G.addEdge(0, 3, 9.0); + G.addEdge(0, 5, 7.0); + G.addEdge(0, 4, 8.0); + G.addEdge(1, 5, 1.0); + G.addEdge(1, 4, 3.0); + G.addEdge(1, 3, 4.0); + G.addEdge(2, 5, 5.0); + G.addEdge(2, 4, 2.0); + G.addEdge(2, 3, 6.0); + G.addEdge(3, 4, 7.0); + + G.forNodes([&](const node currentNode) { + G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { + return G.weight(currentNode, neighbor1) < G.weight(currentNode, neighbor2); + }); + }); + + // Validate that neighbors are sorted according to weights + G.forNodes([&](const node currentNode) { + const auto sortedNeighbors = G.neighborRange(currentNode); + std::vector sortedWeights; + for (const node neighbor : sortedNeighbors) { + sortedWeights.push_back(G.weight(currentNode, neighbor)); + } + // Ensure weights are sorted in ascending order + EXPECT_TRUE(std::ranges::is_sorted(sortedWeights.begin(), sortedWeights.end())); + }); +} + +TEST(GraphGTest, testSortNeighborsDirectedGraph) { + InducedSubgraphView G(6, false, true); + G.addEdge(0, 3); + G.addEdge(0, 5); + G.addEdge(0, 4); + G.addEdge(1, 3); + G.addEdge(1, 5); + G.addEdge(1, 4); + G.addEdge(5, 2); + G.addEdge(3, 2); + G.addEdge(4, 2); + + std::unordered_map> originalNeighbors; + std::unordered_map> originalInNeighbors; + G.forNodes([&](const node currentNode) { + originalNeighbors[currentNode] = std::vector(G.neighborRange(currentNode).begin(), + G.neighborRange(currentNode).end()); + originalInNeighbors[currentNode] = std::vector(G.inNeighborRange(currentNode).begin(), + G.inNeighborRange(currentNode).end()); + }); + + // Sort neighbors + G.forNodes([&](const node currentNode) { + G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { + return neighbor1 < neighbor2; + }); + }); + + G.forNodes([&](const node currentNode) { + // Validate sorting of outgoing neighbors + const auto &sortedNeighbors = G.neighborRange(currentNode); + std::vector sortedNeighborVector(sortedNeighbors.begin(), sortedNeighbors.end()); + EXPECT_TRUE(std::ranges::is_sorted(sortedNeighborVector)); + if (!std::ranges::is_sorted(originalNeighbors[currentNode])) { + EXPECT_NE(originalNeighbors[currentNode], sortedNeighborVector); + } + + // Validate sorting of incoming neighbors + const auto &sortedInNeighbors = G.inNeighborRange(currentNode); + std::vector sortedInNeighborVector(sortedInNeighbors.begin(), + sortedInNeighbors.end()); + EXPECT_TRUE(std::ranges::is_sorted(sortedInNeighborVector)); + + if (!std::ranges::is_sorted(originalInNeighbors[currentNode])) { + EXPECT_NE(originalInNeighbors[currentNode], sortedInNeighborVector); + } + }); +} + +TEST(GraphGTest, testSortNeighborsWeightedDirectedGraphByWeights) { + InducedSubgraphView G(6, true, true); + G.addEdge(0, 3, 1.0); + G.addEdge(0, 5, 3.0); + G.addEdge(0, 4, 4.0); + G.addEdge(0, 1, 12.0); + G.addEdge(2, 1, 13.0); + G.addEdge(1, 3, 7.0); + G.addEdge(1, 5, 11.0); + G.addEdge(1, 4, 3.0); + G.addEdge(2, 5, 6.0); + G.addEdge(2, 3, 5.0); + G.addEdge(2, 4, 10.0); + + // Sort neighbors according to weights + G.forNodes([&](const node currentNode) { + G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { + return G.weight(currentNode, neighbor1) < G.weight(currentNode, neighbor2); + }); + }); + + // Validate that neighbors are sorted according to weights + G.forNodes([&](const node currentNode) { + const auto sortedNeighbors = G.neighborRange(currentNode); + std::vector sortedWeights; + for (const node neighbor : sortedNeighbors) { + sortedWeights.push_back(G.weight(currentNode, neighbor)); + } + // Ensure weights are sorted in ascending order + EXPECT_TRUE(std::ranges::is_sorted(sortedWeights.begin(), sortedWeights.end())); + + const auto sortedInNeighbors = G.neighborRange(currentNode); + std::vector sortedInWeights; + for (const node neighbor : sortedInNeighbors) { + sortedInWeights.push_back(G.weight(currentNode, neighbor)); + } + // Ensure inWeights are sorted in ascending order + EXPECT_TRUE(std::ranges::is_sorted(sortedInWeights.begin(), sortedInWeights.end())); + }); +} + +TEST(GraphGTest, testSortNeighborsWeightedDirectedIndexedGraph) { + InducedSubgraphView G(6, true, true, true); + G.addEdge(0, 3, 1.0); + G.addEdge(0, 5, 3.0); + G.addEdge(0, 4, 4.0); + G.addEdge(0, 1, 12.0); + G.addEdge(2, 1, 13.0); + G.addEdge(1, 3, 7.0); + G.addEdge(1, 5, 11.0); + G.addEdge(1, 4, 3.0); + G.addEdge(2, 5, 6.0); + G.addEdge(2, 3, 5.0); + G.addEdge(2, 4, 10.0); + G.addEdge(5, 0, 15.0); + + // Store original neighbors and weights + std::unordered_map> originalNeighbors; + std::unordered_map> originalInNeighbors; + std::unordered_map> originalWeights; + std::unordered_map> originalInWeights; + std::unordered_map> originalEdgeIds; + std::unordered_map> originalInEdgeIds; + + G.forNodes([&](const node currentNode) { + originalNeighbors[currentNode] = std::vector(G.neighborRange(currentNode).begin(), + G.neighborRange(currentNode).end()); + for (const auto &[neighbor, weight] : G.weightNeighborRange(currentNode)) { + originalWeights[currentNode].push_back(weight); + } + for (size_t i = 0; i < G.degreeOut(currentNode); ++i) { + originalEdgeIds[currentNode].push_back(G.getIthNeighborWithId(currentNode, i).second); + } + + originalInNeighbors[currentNode] = std::vector(G.inNeighborRange(currentNode).begin(), + G.inNeighborRange(currentNode).end()); + + for (const auto &[neighbor, weight] : G.weightInNeighborRange(currentNode)) { + originalInWeights[currentNode].push_back(weight); + } + for (size_t i = 0; i < G.degreeIn(currentNode); ++i) { + originalInEdgeIds[currentNode].push_back(G.getIthInNeighbor(currentNode, i)); + } + }); + + // Sort neighbors + G.forNodes([&](const node currentNode) { + G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { + return neighbor1 < neighbor2; + }); + }); + + // Validate sorting for outgoing neighbors + G.forNodes([&](const node currentNode) { + const auto &sortedNeighbors = G.neighborRange(currentNode); + std::vector sortedNeighborVector(sortedNeighbors.begin(), sortedNeighbors.end()); + EXPECT_TRUE(std::ranges::is_sorted(sortedNeighborVector)); + + if (!std::ranges::is_sorted(originalNeighbors[currentNode])) { + EXPECT_NE(originalNeighbors[currentNode], sortedNeighborVector); + } + + for (size_t i{}; i < sortedNeighborVector.size(); ++i) { + node neighbor = sortedNeighborVector[i]; + auto it = std::ranges::find(originalNeighbors[currentNode], neighbor); + EXPECT_NE(it, originalNeighbors[currentNode].end()); + size_t originalIndex = std::distance(originalNeighbors[currentNode].begin(), it); + EXPECT_DOUBLE_EQ(G.getIthNeighborWeight(currentNode, i), + originalWeights[currentNode][originalIndex]); + } + for (size_t i = 0; i < sortedNeighborVector.size(); ++i) { + node neighbor = sortedNeighborVector[i]; + auto it = std::ranges::find(originalNeighbors[currentNode], neighbor); + EXPECT_NE(it, originalNeighbors[currentNode].end()); + size_t originalIndex = std::distance(originalNeighbors[currentNode].begin(), it); + EXPECT_EQ(G.getIthNeighborWithId(currentNode, i).second, + originalEdgeIds[currentNode][originalIndex]); + } + }); + + // Validate sorting for incoming neighbors + G.forNodes([&](const node currentNode) { + const auto &sortedInNeighbors = G.inNeighborRange(currentNode); + std::vector sortedInNeighborVector(sortedInNeighbors.begin(), + sortedInNeighbors.end()); + EXPECT_TRUE(std::ranges::is_sorted(sortedInNeighborVector)); + + if (!std::ranges::is_sorted(originalInNeighbors[currentNode])) { + EXPECT_NE(originalInNeighbors[currentNode], sortedInNeighborVector); + } + + for (size_t i = 0; i < sortedInNeighborVector.size(); ++i) { + node neighbor = sortedInNeighborVector[i]; + auto originalIterator = std::ranges::find(originalInNeighbors[currentNode], neighbor); + EXPECT_NE(originalIterator, originalInNeighbors[currentNode].end()); + size_t originalIndex = + std::distance(originalInNeighbors[currentNode].begin(), originalIterator); + + // Extract weight directly from weightInNeighborRange + auto weightIterator = G.weightInNeighborRange(currentNode).begin(); + std::advance(weightIterator, i); + EXPECT_DOUBLE_EQ((*weightIterator).second, + originalInWeights[currentNode][originalIndex]); + } + for (size_t i = 0; i < sortedInNeighborVector.size(); ++i) { + node neighbor = sortedInNeighborVector[i]; + auto originalIterator = std::ranges::find(originalInNeighbors[currentNode], neighbor); + EXPECT_NE(originalIterator, originalInNeighbors[currentNode].end()); + size_t originalIndex = + std::distance(originalInNeighbors[currentNode].begin(), originalIterator); + EXPECT_EQ(G.getIthInNeighbor(currentNode, i), + originalInEdgeIds[currentNode][originalIndex]); + } + }); +} + +} /* namespace NetworKit */ From a1c3875d49446bfff5da715126b3f468f0bf5585 Mon Sep 17 00:00:00 2001 From: Ian Date: Sat, 27 Jun 2026 23:54:07 -0500 Subject: [PATCH 05/11] inducedsubgraphview tests + impl; degrees, addnodes, removenodes --- .../networkit/graph/InducedSubgraphView.hpp | 27 +- networkit/cpp/graph/CMakeLists.txt | 1 + networkit/cpp/graph/InducedSubgraphView.cpp | 176 +- networkit/cpp/graph/test/CMakeLists.txt | 2 + .../graph/test/InducedSubgraphViewGTest.cpp | 2464 +---------------- 5 files changed, 287 insertions(+), 2383 deletions(-) diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index 222e57e23..f2d283cde 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -8,10 +8,13 @@ #ifndef NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ #define NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ +#include #include #include +// TODO: add an option to make the IDs contiguous + namespace NetworKit { /** @@ -31,8 +34,8 @@ class InducedSubgraphView : public Graph { // store the induced degrees of all nodes in originalGraph for fast access // for nodes not in nodeSubset, may contain garbage - std::vector inducedDegree; - std::vector inducedInDegree; + std::map inducedDegree; + std::map inducedInDegree; protected: // count n; @@ -46,8 +49,6 @@ class InducedSubgraphView : public Graph { * Construct an induced subgraph view from the original graph and the node subset. * @param originalGraph The original CSR graph * @param subset The defining nodes of the induced subgraph. - * - * Note: directed graphs are not yet supported. */ InducedSubgraphView(const Graph &originalGraph, const std::set &subset); @@ -60,6 +61,7 @@ class InducedSubgraphView : public Graph { /** * Remove nodes from the subset. */ + void removeNode(node u) { removeNodes({u}); } void removeNodes(const std::set &subset); /** @@ -69,8 +71,9 @@ class InducedSubgraphView : public Graph { /** * Construct an explicit mutable subgraph equivalent to this view. + * @param compact; whether the subgraph should get compact node IDs */ - GraphW realize() const; + GraphW realize(bool compact = false) const; /** * Copy constructor @@ -115,6 +118,8 @@ class InducedSubgraphView : public Graph { count degree(node v) const override; count degreeIn(node v) const override; bool isIsolated(node v) const override; + edgeweight weightedDegree(node u, bool countSelfLoopsTwice = false) const; + edgeweight weightedDegreeIn(node u, bool countSelfLoopsTwice = false) const; bool hasNode(node v) const noexcept; bool hasEdge(node u, node v) const noexcept; @@ -166,6 +171,18 @@ class InducedSubgraphView : public Graph { double parallelSumForEdgesVirtualImpl( bool directed, bool weighted, bool hasEdgeIds, std::function handle) const override; + + /** + * Computes the weighted in/out degree of node @a u. + * + * @param u Node. + * @param inDegree whether to compute the in degree or the out degree. + * @param countSelfLoopsTwice If set to true, self-loops will be counted twice. + * + * @return Weighted in/out degree of node @a u. + */ + edgeweight computeWeightedDegree(node u, bool inDegree = false, + bool countSelfLoopsTwice = false) const; }; } // namespace NetworKit diff --git a/networkit/cpp/graph/CMakeLists.txt b/networkit/cpp/graph/CMakeLists.txt index e4db13b37..a5205f8f4 100644 --- a/networkit/cpp/graph/CMakeLists.txt +++ b/networkit/cpp/graph/CMakeLists.txt @@ -5,6 +5,7 @@ networkit_add_module(graph GraphBuilder.cpp GraphTools.cpp GraphW.cpp + InducedSubgraphView.cpp KruskalMSF.cpp PrimMSF.cpp RandomMaximumSpanningForest.cpp diff --git a/networkit/cpp/graph/InducedSubgraphView.cpp b/networkit/cpp/graph/InducedSubgraphView.cpp index 517e5e6b7..f0a9753e3 100644 --- a/networkit/cpp/graph/InducedSubgraphView.cpp +++ b/networkit/cpp/graph/InducedSubgraphView.cpp @@ -18,7 +18,7 @@ namespace NetworKit { InducedSubgraphView::InducedSubgraphView(const Graph &originalGraph, const std::set &subset) : Graph(0, originalGraph.isWeighted(), originalGraph.isDirected()), originalGraph(originalGraph) { - AddNodes(subset); + addNodes(subset); } std::set InducedSubgraphView::boundary() { @@ -34,14 +34,24 @@ std::set InducedSubgraphView::boundary() { count InducedSubgraphView::degree(node v) const { assert(hasNode(v)); - return inducedDegree[v]; + return inducedDegree.at(v); } count InducedSubgraphView::degreeIn(node v) const { assert(hasNode(v)); - return isDirected() ? inducedInDegree[v] : degree(v); + return isDirected() ? inducedInDegree.at(v) : degree(v); }; +edgeweight InducedSubgraphView ::weightedDegree(node u, bool countSelfLoopsTwice) const { + assert(hasNode(u)); + return computeWeightedDegree(u, false, countSelfLoopsTwice); +} + +edgeweight InducedSubgraphView ::weightedDegreeIn(node u, bool countSelfLoopsTwice) const { + assert(hasNode(u)); + return computeWeightedDegree(u, true, countSelfLoopsTwice); +} + bool InducedSubgraphView::isIsolated(node v) const { assert(hasNode(v)); return degree(v) == 0 && (!directed || degreeIn(v) == 0); @@ -61,65 +71,77 @@ edgeweight InducedSubgraphView::weight(node u, node v) const { return originalGraph.weight(u, v); } -void InducedSubgraphView::AddNodes(const std::set &subset) { - std::vector added(subset.size(), true); - - for (auto it = subset.begin(); it != subset.end(); ++it) { - if (hasNode(*it)) { - added[std::distance(subset.begin(), it)] = false; +void InducedSubgraphView::addNodes(const std::set &subset) { + for (node v : subset) { + if (hasNode(v) || !originalGraph.hasNode(v)) continue; - } - nodeSubset.insert(*it); - } - for (auto it = subset.begin(); it != subset.end(); ++it) { - if (!added[std::distance(subset.begin(), it)]) - continue; + nodeSubset.insert(v); + n += 1; + count degree = 0; - originalGraph.forNeighborsOf(*it, [&](node u) { + originalGraph.forNeighborsOf(v, [&](node u, edgeweight ew) { if (!hasNode(u)) return; - if (u == *it) + if (u == v) ++storedNumberOfSelfLoops; - ++inducedDegree[u]; + isDirected() ? ++inducedInDegree[u] : ++inducedDegree[u]; ++degree; }); - inducedDegree[*it] = degree; - m += degree; - n += 1; - } -} -void InducedSubgraphView::RemoveNodes(const std::set &subset) { - std::vector removed(subset.size(), true); + inducedDegree[v] = degree; + m += degree; - for (auto it = subset.begin(); it != subset.end(); ++it) { - if (!hasNode(*it)) { - removed[std::distance(subset.begin(), it)] = false; - continue; + if (isDirected()) { + count inDegree = 0; + originalGraph.forInNeighborsOf(v, [&](node u, edgeweight ew) { + if (!hasNode(u) || u == v) + return; + ++inducedDegree[u]; + ++inDegree; + }); + inducedInDegree[v] = inDegree; + m += inDegree; } - nodeSubset.erase(*it); } +} - for (auto it = subset.begin(); it != subset.end(); ++it) { - if (!removed[std::distance(subset.begin(), it)]) +void InducedSubgraphView::removeNodes(const std::set &subset) { + for (node v : subset) { + if (!hasNode(v)) continue; - count degree = 0; - originalGraph.forNeighborsOf(*it, [&](node u) { + + n -= 1; + m -= inducedDegree[v]; + if (isDirected()) + m -= inducedInDegree[v]; + + originalGraph.forNeighborsOf(v, [&](node u, edgeweight ew) { if (!hasNode(u)) return; - if (u == *it) + if (u == v) --storedNumberOfSelfLoops; - --inducedDegree[u]; - --degree; + isDirected() ? --inducedInDegree[u] : --inducedDegree[u]; }); - m -= degree; - n -= 1; + + if (isDirected()) { + originalGraph.forInNeighborsOf(v, [&](node u, edgeweight ew) { + if (!hasNode(u) || u == v) + return; + --inducedDegree[u]; + }); + } + + nodeSubset.erase(v); + inducedDegree.erase(v); + if (isDirected()) + inducedInDegree.erase(v); } } -GraphW InducedSubgraphView::Realize() const { - return GraphTools::subgraphFromNodes(originalGraph, nodeSubset.begin(), nodeSubset.end()); +GraphW InducedSubgraphView::realize(bool compact) const { + return GraphTools::subgraphFromNodes(originalGraph, nodeSubset.begin(), nodeSubset.end(), + compact); } std::vector InducedSubgraphView::getNeighborsVector(node u, bool inEdges) const { @@ -175,7 +197,7 @@ void InducedSubgraphView::forEdgesVirtualImpl( void InducedSubgraphView::forEdgesOfVirtualImpl( node u, bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { - originalGraph.forEdgesOf(u, [&](node v, edgeweight w, edgeid x) { + originalGraph.forEdgesOf(u, [&](node u, node v, edgeweight w, edgeid x) { if (hasEdge(u, v)) handle(u, v, w, x); }); @@ -184,7 +206,7 @@ void InducedSubgraphView::forEdgesOfVirtualImpl( void InducedSubgraphView::forInEdgesVirtualImpl( node u, bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { - originalGraph.forInEdgesOf(u, [&](node v, edgeweight w, edgeid x) { + originalGraph.forInEdgesOf(u, [&](node u, node v, edgeweight w, edgeid x) { if (hasEdge(u, v)) handle(u, v, w, x); }); @@ -194,9 +216,73 @@ double InducedSubgraphView::parallelSumForEdgesVirtualImpl( bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { return originalGraph.parallelSumForEdges([&](node u, node v, edgeweight w, edgeid x) { - if (hasEdge(u, v)) - handle(u, v, w, x); + return hasEdge(u, v) ? handle(u, v, w, x) : 0; }); } +edgeid InducedSubgraphView::edgeId(node u, node v) const { + throw std::runtime_error("edgeId not supported for graph view; realize() into GraphW"); +} +node InducedSubgraphView::getIthNeighbor(Unsafe, node u, index i) const { + throw std::runtime_error("getIthNeighbor not supported for graph view; realize() into GraphW"); +} +edgeweight InducedSubgraphView::getIthNeighborWeight(node u, index i) const { + throw std::runtime_error( + "getIthNeighborWeight not supported for graph view; realize() into GraphW"); +} +node InducedSubgraphView::getIthNeighbor(node u, index i) const { + throw std::runtime_error("getIthNeighbor not supported for graph view; realize() into GraphW"); +} +node InducedSubgraphView::getIthInNeighbor(node u, index i) const { + throw std::runtime_error( + "getIthInNeighbor not supported for graph view; realize() into GraphW"); +} +std::pair InducedSubgraphView::getIthNeighborWithWeight(node u, index i) const { + throw std::runtime_error( + "getIthNeighborWithWeight not supported for graph view; realize() into GraphW"); +} +std::pair InducedSubgraphView::getIthNeighborWithId(node u, index i) const { + throw std::runtime_error( + "getIthNeighborWithId not supported for graph view; realize() into GraphW"); +} +index InducedSubgraphView::indexInInEdgeArray(node v, node u) const { + throw std::runtime_error( + "indexInInEdgeArray not supported for graph view; realize() into GraphW"); +} +index InducedSubgraphView::indexInOutEdgeArray(node u, node v) const { + throw std::runtime_error( + "indexInOutEdgeArray not supported for graph view; realize() into GraphW"); +} + +edgeweight InducedSubgraphView::computeWeightedDegree(node u, bool inDegree, + bool countSelfLoopsTwice) const { + // TODO: clean this up using forNeighbors in the subgraph, when that works + if (weighted) { + edgeweight sum = 0.0; + auto sumWeights = [&](node v, edgeweight w) { + if (hasNode(v)) + sum += (countSelfLoopsTwice && u == v) ? 2. * w : w; + }; + if (inDegree) { + originalGraph.forInNeighborsOf(u, sumWeights); + } else { + originalGraph.forNeighborsOf(u, sumWeights); + } + return sum; + } + + count sum = inDegree ? degreeIn(u) : degreeOut(u); + auto countSelfLoops = [&](node v) { sum += (u == v); }; + + if (countSelfLoopsTwice && numberOfSelfLoops()) { + if (inDegree) { + originalGraph.forInNeighborsOf(u, countSelfLoops); + } else { + originalGraph.forNeighborsOf(u, countSelfLoops); + } + } + + return static_cast(sum); +} + } // namespace NetworKit diff --git a/networkit/cpp/graph/test/CMakeLists.txt b/networkit/cpp/graph/test/CMakeLists.txt index d9768ca79..aa96e843e 100644 --- a/networkit/cpp/graph/test/CMakeLists.txt +++ b/networkit/cpp/graph/test/CMakeLists.txt @@ -1,6 +1,8 @@ networkit_add_test(graph GraphBuilderAutoCompleteGTest auxiliary) networkit_add_test(graph GraphGTest auxiliary dyn_distance io generators) +networkit_add_test(graph InducedSubgraphViewGTest + auxiliary dyn_distance io generators) networkit_add_test(graph GraphToolsGTest generators io) networkit_add_test(graph TraversalGTest generators) networkit_add_test(graph SpanningGTest io) diff --git a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp index c587c3a4c..c11fd1e96 100644 --- a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp +++ b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp @@ -6,6 +6,7 @@ */ #include +#include #include #include "networkit/auxiliary/Random.hpp" @@ -43,10 +44,16 @@ class InducedSubgraphViewGTest : public testing::TestWithParam createInducedSubgraphView(count n, count m, - bool random = false) const; - std::pair - createInducedSubgraphView(count n, count m, count N, count M, bool random = false) const; + std::set allNodes(const Graph &G) const; + std::set someNodes(const Graph &G, count k, bool random = false) const; + + struct NodeSubset { + std::string label; + std::set nodes; + }; + std::vector subsetVariants(const Graph &G) const; + GraphW inducedSubgraph(const Graph &G, const std::set &subset) const; + count countSelfLoopsManually(const InducedSubgraphView &G); }; @@ -85,43 +92,39 @@ GraphW InducedSubgraphViewGTest::createGraphW(count n, count m) const { return G; } -std::pair -InducedSubgraphViewGTest::createInducedSubgraphView(count n, count m, bool random) const { - // Create bigger E-R graph, and then subset it - count N = static_cast(n * Aux::Random::real(2, 10)); - count M = static_cast(m * Aux::Random::real(2, 10)); - return createInducedSubgraphView(n, m, N, M, random); +std::set InducedSubgraphViewGTest::allNodes(const Graph &G) const { + std::set subset; + G.forNodes([&](node u) { subset.insert(u); }); + return subset; } -std::pair -InducedSubgraphViewGTest::createInducedSubgraphView(count n, count m, count N, count M, - bool random) const { - GraphW G = createGraphW(N); - while (G.numberOfEdges() < M) { - const auto u = Aux::Random::index(N); - const auto v = Aux::Random::index(N); - if (u == v) - continue; - if (G.hasEdge(u, v)) - continue; - - const auto p = Aux::Random::probability(); - G.addEdge(u, v, p); - } - +std::set InducedSubgraphViewGTest::someNodes(const Graph &G, count k, bool random) const { std::set subset; - index idx; + index idx = 0; auto cb = [&](node u) { - if (idx++ < n) + if (idx++ < k) subset.insert(u); }; - if (random) G.forNodesInRandomOrder(cb); else G.forNodes(cb); + return subset; +} + +std::vector +InducedSubgraphViewGTest::subsetVariants(const Graph &G) const { + const count n = G.numberOfNodes(); + const count small = std::max(2, static_cast(0.1 * n)); + const count half = (n + 1) / 2; + return {{"small", someNodes(G, small, true)}, + {"half", someNodes(G, half, true)}, + {"all", allNodes(G)}}; +} - return {G, InducedSubgraphView(G, subset)}; +GraphW InducedSubgraphViewGTest::inducedSubgraph(const Graph &G, + const std::set &subset) const { + return GraphTools::subgraphFromNodes(G, subset.begin(), subset.end()); } count InducedSubgraphViewGTest::countSelfLoopsManually(const InducedSubgraphView &G) { @@ -181,8 +184,9 @@ void InducedSubgraphViewGTest::SetUp() { /** NODE MODIFIERS **/ TEST_P(InducedSubgraphViewGTest, testAddNode) { - auto [H, G] = createInducedSubgraphView(0, 0, 10, 25); + GraphW H = createGraphW(10, 25); + InducedSubgraphView G(H, std::set{}); ASSERT_FALSE(G.hasNode(0)); ASSERT_FALSE(G.hasNode(1)); ASSERT_EQ(0u, G.numberOfNodes()); @@ -192,7 +196,7 @@ TEST_P(InducedSubgraphViewGTest, testAddNode) { ASSERT_FALSE(G.hasNode(1)); ASSERT_EQ(1u, G.numberOfNodes()); - auto [H2, G2] = createInducedSubgraphView(2, 0, 10, 25); + InducedSubgraphView G2(H, someNodes(H, 2)); ASSERT_TRUE(G2.hasNode(0)); ASSERT_TRUE(G2.hasNode(1)); ASSERT_FALSE(G2.hasNode(2)); @@ -207,43 +211,54 @@ TEST_P(InducedSubgraphViewGTest, testAddNode) { } TEST_P(InducedSubgraphViewGTest, testAddNodes) { - InducedSubgraphView G = createInducedSubgraphView(5); - G.addNodes(5); + GraphW H = createGraphW(100); - ASSERT_EQ(G.numberOfNodes(), 10); - ASSERT_TRUE(G.hasNode(9)); + InducedSubgraphView G(H, someNodes(H, 5)); + ASSERT_EQ(G.numberOfNodes(), 5u); - G.addNodes(90); + std::set batch1; + for (node u = 5; u < 10; ++u) + batch1.insert(u); + G.addNodes(batch1); + ASSERT_EQ(G.numberOfNodes(), 10u); + ASSERT_TRUE(G.hasNode(9)); - ASSERT_EQ(G.numberOfNodes(), 100); + std::set batch2; + for (node u = 10; u < 100; ++u) + batch2.insert(u); + G.addNodes(batch2); + ASSERT_EQ(G.numberOfNodes(), 100u); ASSERT_TRUE(G.hasNode(99)); } TEST_P(InducedSubgraphViewGTest, testRemoveNode) { - auto testInducedSubgraphView = [&](InducedSubgraphView &G) { + auto testInducedSubgraphView = [&](const GraphW &base) { + InducedSubgraphView G(base, allNodes(base)); + count n = G.numberOfNodes(); - count z = n; count m = G.numberOfEdges(); + const count z = base.upperNodeIdBound(); for (node u = 0; u < z; ++u) { - count deg = G.degreeOut(u); - count degIn = G.isDirected() ? G.degreeIn(u) : 0; - G.removeNode(u); + const count deg = G.degreeOut(u); + const count degIn = G.isDirected() ? G.degreeIn(u) : 0; + G.removeNodes({u}); --n; m -= deg + degIn; ASSERT_EQ(G.numberOfNodes(), n); ASSERT_EQ(G.numberOfEdges(), m); - G.forNodes([&](node v) { ASSERT_EQ(G.hasNode(v), v > u); }); + base.forNodes([&](node v) { ASSERT_EQ(G.hasNode(v), v > u); }); } }; - InducedSubgraphView G1 = ErdosRenyiGenerator(200, 0.2, false).generate(); - InducedSubgraphView G2 = ErdosRenyiGenerator(200, 0.2, true).generate(); + GraphW G1 = ErdosRenyiGenerator(200, 0.2, false).generate(); + GraphW G2 = ErdosRenyiGenerator(200, 0.2, true).generate(); testInducedSubgraphView(G1); testInducedSubgraphView(G2); } TEST_P(InducedSubgraphViewGTest, testHasNode) { - InducedSubgraphView G = createInducedSubgraphView(5); + GraphW H = createGraphW(7); + InducedSubgraphView G(H, someNodes(H, 5)); ASSERT_TRUE(G.hasNode(0)); ASSERT_TRUE(G.hasNode(1)); @@ -253,9 +268,8 @@ TEST_P(InducedSubgraphViewGTest, testHasNode) { ASSERT_FALSE(G.hasNode(5)); ASSERT_FALSE(G.hasNode(6)); - G.removeNode(0); - G.removeNode(2); - G.addNode(); + G.removeNodes({0, 2}); + G.addNodes({5}); ASSERT_FALSE(G.hasNode(0)); ASSERT_TRUE(G.hasNode(1)); @@ -266,177 +280,80 @@ TEST_P(InducedSubgraphViewGTest, testHasNode) { ASSERT_FALSE(G.hasNode(6)); } -TEST_P(InducedSubgraphViewGTest, testRestoreNode) { - InducedSubgraphView G = createInducedSubgraphView(4); - - ASSERT_EQ(4u, G.numberOfNodes()); - ASSERT_TRUE(G.hasNode(0)); - ASSERT_TRUE(G.hasNode(1)); - ASSERT_TRUE(G.hasNode(2)); - ASSERT_TRUE(G.hasNode(3)); +TEST_P(InducedSubgraphViewGTest, testAddRemoveNodesOutsideOriginalGraph) { + GraphW H = createGraphW(5); + InducedSubgraphView G(H, allNodes(H)); + ASSERT_EQ(5u, G.numberOfNodes()); - G.removeNode(0); + G.addNodes({10, 11, 12}); + ASSERT_EQ(5u, G.numberOfNodes()); + ASSERT_FALSE(G.hasNode(10)); + ASSERT_FALSE(G.hasNode(11)); + ASSERT_FALSE(G.hasNode(12)); + G.removeNodes({3, 4}); ASSERT_EQ(3u, G.numberOfNodes()); - ASSERT_FALSE(G.hasNode(0)); - ASSERT_TRUE(G.hasNode(1)); - ASSERT_TRUE(G.hasNode(2)); - ASSERT_TRUE(G.hasNode(3)); - - G.restoreNode(0); - ASSERT_EQ(4u, G.numberOfNodes()); + G.removeNodes({3, 4, 42}); + ASSERT_EQ(3u, G.numberOfNodes()); ASSERT_TRUE(G.hasNode(0)); ASSERT_TRUE(G.hasNode(1)); ASSERT_TRUE(G.hasNode(2)); - ASSERT_TRUE(G.hasNode(3)); } /** NODE PROPERTIES **/ TEST_P(InducedSubgraphViewGTest, testDegree) { - if (isDirected()) { - ASSERT_EQ(1u, this->Ghouse.degree(0)); - ASSERT_EQ(2u, this->Ghouse.degree(1)); - ASSERT_EQ(2u, this->Ghouse.degree(2)); - ASSERT_EQ(2u, this->Ghouse.degree(3)); - ASSERT_EQ(1u, this->Ghouse.degree(4)); - } else { - ASSERT_EQ(2u, this->Ghouse.degree(0)); - ASSERT_EQ(4u, this->Ghouse.degree(1)); - ASSERT_EQ(4u, this->Ghouse.degree(2)); - ASSERT_EQ(3u, this->Ghouse.degree(3)); - ASSERT_EQ(3u, this->Ghouse.degree(4)); + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) + EXPECT_EQ(expected.degree(u), Gv.degree(u)); } } TEST_P(InducedSubgraphViewGTest, testDegreeIn) { - if (isDirected()) { - ASSERT_EQ(1u, this->Ghouse.degreeIn(0)); - ASSERT_EQ(2u, this->Ghouse.degreeIn(1)); - ASSERT_EQ(2u, this->Ghouse.degreeIn(2)); - ASSERT_EQ(1u, this->Ghouse.degreeIn(3)); - ASSERT_EQ(2u, this->Ghouse.degreeIn(4)); - } else { - ASSERT_EQ(2u, this->Ghouse.degreeIn(0)); - ASSERT_EQ(4u, this->Ghouse.degreeIn(1)); - ASSERT_EQ(4u, this->Ghouse.degreeIn(2)); - ASSERT_EQ(3u, this->Ghouse.degreeIn(3)); - ASSERT_EQ(3u, this->Ghouse.degreeIn(4)); + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) + EXPECT_EQ(expected.degreeIn(u), Gv.degreeIn(u)); } } TEST_P(InducedSubgraphViewGTest, testDegreeOut) { - if (isDirected()) { - ASSERT_EQ(1u, this->Ghouse.degreeOut(0)); - ASSERT_EQ(2u, this->Ghouse.degreeOut(1)); - ASSERT_EQ(2u, this->Ghouse.degreeOut(2)); - ASSERT_EQ(2u, this->Ghouse.degreeOut(3)); - ASSERT_EQ(1u, this->Ghouse.degreeOut(4)); - } else { - ASSERT_EQ(2u, this->Ghouse.degreeOut(0)); - ASSERT_EQ(4u, this->Ghouse.degreeOut(1)); - ASSERT_EQ(4u, this->Ghouse.degreeOut(2)); - ASSERT_EQ(3u, this->Ghouse.degreeOut(3)); - ASSERT_EQ(3u, this->Ghouse.degreeOut(4)); + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) + EXPECT_EQ(expected.degreeOut(u), Gv.degreeOut(u)); } } -TEST_P(InducedSubgraphViewGTest, testIsIsolated) { - ASSERT_FALSE(this->Ghouse.isIsolated(0)); - ASSERT_FALSE(this->Ghouse.isIsolated(1)); - ASSERT_FALSE(this->Ghouse.isIsolated(2)); - ASSERT_FALSE(this->Ghouse.isIsolated(3)); - ASSERT_FALSE(this->Ghouse.isIsolated(4)); - - this->Ghouse.addNode(); - ASSERT_TRUE(this->Ghouse.isIsolated(5)); - - this->Ghouse.removeEdge(1, 0); - ASSERT_FALSE(this->Ghouse.isIsolated(0)); - - this->Ghouse.removeEdge(0, 2); - ASSERT_TRUE(this->Ghouse.isIsolated(0)); - - this->Ghouse.addEdge(1, 0); - ASSERT_FALSE(this->Ghouse.isIsolated(0)); -} - TEST_P(InducedSubgraphViewGTest, testWeightedDegree) { - // add self-loop this->Ghouse.addEdge(2, 2, 0.75); - if (isInducedSubgraphView()) { - ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(0)); - ASSERT_EQ(4 * defaultEdgeWeight, this->Ghouse.weightedDegree(1)); - ASSERT_EQ(5 * defaultEdgeWeight, this->Ghouse.weightedDegree(2)); - ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(3)); - ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(4)); - } - - if (isWeightedInducedSubgraphView()) { - ASSERT_EQ(5.0, this->Ghouse.weightedDegree(0)); - ASSERT_EQ(12.0, this->Ghouse.weightedDegree(1)); - ASSERT_EQ(22.75, this->Ghouse.weightedDegree(2)); - ASSERT_EQ(14.0, this->Ghouse.weightedDegree(3)); - ASSERT_EQ(19.0, this->Ghouse.weightedDegree(4)); - } - - if (isDirectedInducedSubgraphView()) { - // only count outgoing edges - ASSERT_EQ(1 * defaultEdgeWeight, this->Ghouse.weightedDegree(0)); - ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(1)); - ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(2)); - ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(3)); - ASSERT_EQ(1 * defaultEdgeWeight, this->Ghouse.weightedDegree(4)); - } - - if (isWeightedDirectedInducedSubgraphView()) { - // only sum weight of outgoing edges - ASSERT_EQ(3.0, this->Ghouse.weightedDegree(0)); - ASSERT_EQ(7.0, this->Ghouse.weightedDegree(1)); - ASSERT_EQ(12.75, this->Ghouse.weightedDegree(2)); - ASSERT_EQ(8.0, this->Ghouse.weightedDegree(3)); - ASSERT_EQ(6.0, this->Ghouse.weightedDegree(4)); + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) + EXPECT_EQ(expected.weightedDegree(u), Gv.weightedDegree(u)); } } TEST_P(InducedSubgraphViewGTest, testWeightedDegree2) { - // add self-loop this->Ghouse.addEdge(2, 2, 0.75); - if (isInducedSubgraphView()) { - ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(0, true)); - ASSERT_EQ(4 * defaultEdgeWeight, this->Ghouse.weightedDegree(1, true)); - ASSERT_EQ(6 * defaultEdgeWeight, this->Ghouse.weightedDegree(2, true)); - ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(3, true)); - ASSERT_EQ(3 * defaultEdgeWeight, this->Ghouse.weightedDegree(4, true)); - } - - if (isWeightedInducedSubgraphView()) { - ASSERT_EQ(5.0, this->Ghouse.weightedDegree(0, true)); - ASSERT_EQ(12.0, this->Ghouse.weightedDegree(1, true)); - ASSERT_EQ(23.5, this->Ghouse.weightedDegree(2, true)); - ASSERT_EQ(14.0, this->Ghouse.weightedDegree(3, true)); - ASSERT_EQ(19.0, this->Ghouse.weightedDegree(4, true)); - } - - if (isDirectedInducedSubgraphView()) { - // only count outgoing edges - ASSERT_EQ(1 * defaultEdgeWeight, this->Ghouse.weightedDegree(0, true)); - ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(1, true)); - ASSERT_EQ(4 * defaultEdgeWeight, this->Ghouse.weightedDegree(2, true)); - ASSERT_EQ(2 * defaultEdgeWeight, this->Ghouse.weightedDegree(3, true)); - ASSERT_EQ(1 * defaultEdgeWeight, this->Ghouse.weightedDegree(4, true)); - } - - if (isWeightedDirectedInducedSubgraphView()) { - // only sum weight of outgoing edges - ASSERT_EQ(3.0, this->Ghouse.weightedDegree(0, true)); - ASSERT_EQ(7.0, this->Ghouse.weightedDegree(1, true)); - ASSERT_EQ(13.5, this->Ghouse.weightedDegree(2, true)); - ASSERT_EQ(8.0, this->Ghouse.weightedDegree(3, true)); - ASSERT_EQ(6.0, this->Ghouse.weightedDegree(4, true)); + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) + EXPECT_EQ(expected.weightedDegree(u, true), Gv.weightedDegree(u, true)); } } @@ -446,2154 +363,35 @@ TEST_P(InducedSubgraphViewGTest, testWeightedDegree3) { for (int seed : {1, 2, 3}) { Aux::Random::setSeed(seed, false); - auto G = ErdosRenyiGenerator(n, p, isDirected()).generate(); - if (isWeighted()) { - GraphTools::randomizeWeights(G); - } - G.forNodes([&](node u) { - edgeweight wDeg = 0, wDegTwice = 0; - G.forNeighborsOf(u, [&](node v, edgeweight w) { - wDeg += w; - wDegTwice += (u == v) ? 2. * w : w; - }); - - EXPECT_DOUBLE_EQ(G.weightedDegree(u), wDeg); - EXPECT_DOUBLE_EQ(G.weightedDegree(u, true), wDegTwice); - - edgeweight wInDeg = 0, wInDegTwice = 0; - G.forInNeighborsOf(u, [&](node v, edgeweight w) { - wInDeg += w; - wInDegTwice += (u == v) ? 2. * w : w; - }); - - EXPECT_DOUBLE_EQ(G.weightedDegreeIn(u), wInDeg); - EXPECT_DOUBLE_EQ(G.weightedDegreeIn(u, true), wInDegTwice); - }); - } -} - -/** EDGE MODIFIERS **/ - -TEST_P(InducedSubgraphViewGTest, testAddEdge) { - InducedSubgraphView G = createInducedSubgraphView(3); - - // InducedSubgraphView without edges - ASSERT_EQ(0u, G.numberOfEdges()); - ASSERT_FALSE(G.hasEdge(0, 2)); - ASSERT_FALSE(G.hasEdge(0, 1)); - ASSERT_FALSE(G.hasEdge(1, 2)); - ASSERT_FALSE(G.hasEdge(2, 2)); - ASSERT_EQ(nullWeight, G.weight(0, 2)); - ASSERT_EQ(nullWeight, G.weight(0, 1)); - ASSERT_EQ(nullWeight, G.weight(1, 2)); - ASSERT_EQ(nullWeight, G.weight(2, 2)); - - // InducedSubgraphView with 2 normal edges - G.addEdge(0, 1, 4.51); - G.addEdge(1, 2, 2.39); - ASSERT_EQ(2u, G.numberOfEdges()); - ASSERT_FALSE(G.hasEdge(0, 2)); // was never added - ASSERT_TRUE(G.hasEdge(0, 1)); - ASSERT_TRUE(G.hasEdge(1, 2)); - ASSERT_FALSE(G.hasEdge(2, 2)); // will be added later - - // check weights - if (G.isWeighted()) { - ASSERT_EQ(4.51, G.weight(0, 1)); - ASSERT_EQ(2.39, G.weight(1, 2)); - } else { - ASSERT_EQ(defaultEdgeWeight, G.weight(0, 1)); - ASSERT_EQ(defaultEdgeWeight, G.weight(1, 2)); - } - - if (G.isDirected()) { - ASSERT_FALSE(G.hasEdge(1, 0)); - ASSERT_FALSE(G.hasEdge(2, 1)); - - // add edge in the other direction - // note: bidirectional edges are not supported, so both edges have different - // weights - G.addEdge(2, 1, 6.23); - ASSERT_TRUE(G.hasEdge(2, 1)); - if (G.isWeighted()) { - ASSERT_EQ(2.39, G.weight(1, 2)); - ASSERT_EQ(6.23, G.weight(2, 1)); - } else { - ASSERT_EQ(defaultEdgeWeight, G.weight(2, 1)); - } - } else { - ASSERT_TRUE(G.hasEdge(1, 0)); - ASSERT_TRUE(G.hasEdge(2, 1)); - if (G.isWeighted()) { - ASSERT_EQ(4.51, G.weight(1, 0)); - ASSERT_EQ(2.39, G.weight(2, 1)); - } else { - ASSERT_EQ(defaultEdgeWeight, G.weight(1, 0)); - ASSERT_EQ(defaultEdgeWeight, G.weight(2, 1)); - } - } - - // add self loop - G.addEdge(2, 2, 0.72); - ASSERT_TRUE(G.hasEdge(2, 2)); - if (G.isWeighted()) { - ASSERT_EQ(0.72, G.weight(2, 2)); - } else { - ASSERT_EQ(defaultEdgeWeight, G.weight(2, 2)); - } -} - -TEST_P(InducedSubgraphViewGTest, testRemoveEdge) { - double epsilon = 1e-6; - InducedSubgraphView G = createInducedSubgraphView(3); - - edgeweight ewBefore = G.totalEdgeWeight(); - - G.addEdge(0, 1, 3.14); - - if (G.isWeighted()) { - ASSERT_NEAR(ewBefore + 3.14, G.totalEdgeWeight(), epsilon); - } else { - ASSERT_NEAR(ewBefore + defaultEdgeWeight, G.totalEdgeWeight(), epsilon); - } - - G.addEdge(0, 0); - - ASSERT_EQ(2u, G.numberOfEdges()); - ASSERT_TRUE(G.hasEdge(0, 0)); - ASSERT_TRUE(G.hasEdge(0, 1)); - ASSERT_FALSE(G.hasEdge(2, 1)); - - // test remove regular edge - ewBefore = G.totalEdgeWeight(); - G.removeEdge(0, 1); - if (G.isWeighted()) { - ASSERT_NEAR(ewBefore - 3.14, G.totalEdgeWeight(), epsilon); - } else { - ASSERT_NEAR(ewBefore - defaultEdgeWeight, G.totalEdgeWeight(), epsilon); - } - - ASSERT_EQ(1u, G.numberOfEdges()); - ASSERT_TRUE(G.hasEdge(0, 0)); - ASSERT_FALSE(G.hasEdge(0, 1)); - ASSERT_FALSE(G.hasEdge(2, 1)); - - // test remove self-loop - G.addEdge(2, 1); - - ewBefore = G.totalEdgeWeight(); - G.removeEdge(0, 0); - if (G.isWeighted()) { - ASSERT_NEAR(ewBefore - defaultEdgeWeight, G.totalEdgeWeight(), epsilon); - } else { - ASSERT_NEAR(ewBefore - defaultEdgeWeight, G.totalEdgeWeight(), epsilon); - } - - ASSERT_EQ(1u, G.numberOfEdges()); - ASSERT_FALSE(G.hasEdge(0, 0)); - ASSERT_FALSE(G.hasEdge(0, 1)); - ASSERT_TRUE(G.hasEdge(2, 1)); - - // test from removeselfloops adapted for removeEdge - G = createInducedSubgraphView(2); - - ewBefore = G.totalEdgeWeight(); - - G.addEdge(0, 1); - G.addEdge(0, 0, 3.14); - G.addEdge(1, 1); - - if (G.isWeighted()) { - EXPECT_NEAR(ewBefore + 3.14 + 2 * defaultEdgeWeight, G.totalEdgeWeight(), epsilon); - } else { - EXPECT_NEAR(ewBefore + 3 * defaultEdgeWeight, G.totalEdgeWeight(), epsilon); - } - - EXPECT_EQ(3u, G.numberOfEdges()); - EXPECT_TRUE(G.hasEdge(0, 0)); - EXPECT_TRUE(G.hasEdge(0, 1)); - EXPECT_TRUE(G.hasEdge(1, 1)); - EXPECT_EQ(G.numberOfSelfLoops(), 2u); - - // remove self-loops - ewBefore = G.totalEdgeWeight(); - - G.removeEdge(0, 0); - G.removeEdge(1, 1); - - if (G.isWeighted()) { - EXPECT_NEAR(ewBefore - defaultEdgeWeight - 3.14, G.totalEdgeWeight(), epsilon); - } else { - EXPECT_NEAR(ewBefore - 2 * defaultEdgeWeight, G.totalEdgeWeight(), epsilon) - << "Weighted, directed: " << G.isWeighted() << ", " << G.isDirected(); - } - - EXPECT_EQ(1u, G.numberOfEdges()); - EXPECT_FALSE(G.hasEdge(0, 0)); - EXPECT_FALSE(G.hasEdge(1, 1)); - EXPECT_TRUE(G.hasEdge(0, 1)); - EXPECT_EQ(0u, G.numberOfSelfLoops()) - << "Weighted, directed: " << G.isWeighted() << ", " << G.isDirected(); -} - -TEST_P(InducedSubgraphViewGTest, testRemoveAllEdges) { - constexpr count n = 100; - constexpr double p = 0.2; - - for (int seed : {1, 2, 3}) { - Aux::Random::setSeed(seed, false); - auto g = ErdosRenyiGenerator(n, p, isDirected()).generate(); - if (isWeighted()) { - g = InducedSubgraphView(g, true, isDirected()); - } - - g.removeAllEdges(); - - EXPECT_EQ(g.numberOfEdges(), 0); - - count edgeCount = 0; - g.forEdges([&edgeCount](node, node) { ++edgeCount; }); - EXPECT_EQ(edgeCount, 0); - - g.forNodes([&](node u) { - EXPECT_EQ(g.degree(u), 0); - EXPECT_EQ(g.degree(u), 0); - }); - } -} - -TEST_P(InducedSubgraphViewGTest, testRemoveSelfLoops) { - constexpr count n = 100; - constexpr count nSelfLoops = 100; - constexpr double p = 0.2; - - for (int seed : {1, 2, 3}) { - Aux::Random::setSeed(seed, false); - auto g = ErdosRenyiGenerator(n, p, isDirected()).generate(); - if (isWeighted()) { - g = InducedSubgraphView(g, true, isDirected()); - } - - for (count i = 0; i < nSelfLoops; ++i) { - const auto u = GraphTools::randomNode(g); - g.addEdge(u, u); - } - - const auto numberOfSelfLoops = g.numberOfSelfLoops(); - const auto numberOfEdges = g.numberOfEdges(); - g.removeSelfLoops(); - - EXPECT_EQ(numberOfEdges - numberOfSelfLoops, g.numberOfEdges()); - EXPECT_EQ(g.numberOfSelfLoops(), 0); - g.forNodes([&g](const node u) { EXPECT_FALSE(g.hasEdge(u, u)); }); - } -} - -TEST_P(InducedSubgraphViewGTest, testRemoveMultiEdges) { - constexpr count n = 200; - constexpr double p = 0.1; - constexpr count nMultiEdges = 10; - constexpr count nMultiSelfLoops = 10; - - auto getGraphEdges = [](const InducedSubgraphView &G) { - std::vector> edges; - edges.reserve(G.numberOfEdges()); - - G.forEdges([&](const node u, const node v) { edges.push_back({u, v}); }); - - return edges; - }; - - for (int seed : {1, 2, 3}) { - Aux::Random::setSeed(seed, false); - auto g = ErdosRenyiGenerator(n, p, isDirected()).generate(); + auto base = ErdosRenyiGenerator(n, p, isDirected()).generate(); if (isWeighted()) { - g = InducedSubgraphView(g, true, isDirected()); - } - - const auto edgeSet = getGraphEdges(g); - const auto m = g.numberOfEdges(); - - // Adding multiedges at random - for (count i = 0; i < nMultiEdges; ++i) { - const auto e = GraphTools::randomEdge(g); - g.addEdge(e.first, e.second); - } - - std::unordered_set uniqueSelfLoops; - // Adding multiple self-loops at random - for (count i = 0; i < nMultiSelfLoops; ++i) { - const auto u = GraphTools::randomNode(g); - g.addEdge(u, u); - g.addEdge(u, u); - uniqueSelfLoops.insert(u); - } - - EXPECT_EQ(g.numberOfEdges(), m + nMultiEdges + 2 * nMultiSelfLoops); - - g.removeMultiEdges(); - - EXPECT_EQ(g.numberOfEdges(), m + uniqueSelfLoops.size()); - g.removeSelfLoops(); - - EXPECT_EQ(g.numberOfEdges(), m); - auto edgeSet_ = getGraphEdges(g); - - for (count i = 0; i < g.numberOfEdges(); ++i) - EXPECT_EQ(edgeSet[i], edgeSet_[i]); - } -} - -TEST_P(InducedSubgraphViewGTest, testHasEdge) { - auto containsEdge = [&](std::pair e) { - auto it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e); - return it != this->houseEdgesOut.end(); - }; - - for (node u = 0; u < this->Ghouse.upperNodeIdBound(); u++) { - for (node v = 0; v < this->Ghouse.upperNodeIdBound(); v++) { - auto edge = std::make_pair(u, v); - auto edgeReverse = std::make_pair(v, u); - bool hasEdge = containsEdge(edge); - bool hasEdgeReverse = containsEdge(edgeReverse); - if (this->Ghouse.isDirected()) { - ASSERT_EQ(hasEdge, this->Ghouse.hasEdge(u, v)); - } else { - ASSERT_EQ(hasEdge || hasEdgeReverse, this->Ghouse.hasEdge(u, v)); - } - } - } -} - -/** GLOBAL PROPERTIES **/ - -TEST_P(InducedSubgraphViewGTest, testSelfLoopCountSimple) { - InducedSubgraphView G(Ghouse); - G.addEdge(0, 0); - EXPECT_EQ(1, G.numberOfSelfLoops()); -} - -TEST_P(InducedSubgraphViewGTest, testIsWeighted) { - ASSERT_EQ(isWeighted(), this->Ghouse.isWeighted()); -} - -TEST_P(InducedSubgraphViewGTest, testIsDirected) { - ASSERT_EQ(isDirected(), this->Ghouse.isDirected()); -} - -TEST_P(InducedSubgraphViewGTest, testIsEmpty) { - InducedSubgraphView G1 = createInducedSubgraphView(0); - InducedSubgraphView G2 = createInducedSubgraphView(2); - - ASSERT_TRUE(G1.isEmpty()); - ASSERT_FALSE(G2.isEmpty()); - - node v = G1.addNode(); - G2.removeNode(GraphTools::randomNode(G2)); - ASSERT_FALSE(G1.isEmpty()); - ASSERT_FALSE(G2.isEmpty()); - - G1.removeNode(v); - G2.removeNode(GraphTools::randomNode(G2)); - ASSERT_TRUE(G1.isEmpty()); - ASSERT_TRUE(G2.isEmpty()); -} - -TEST_P(InducedSubgraphViewGTest, testNumberOfNodes) { - ASSERT_EQ(this->n_house, this->Ghouse.numberOfNodes()); - - InducedSubgraphView G1 = createInducedSubgraphView(0); - ASSERT_EQ(0u, G1.numberOfNodes()); - G1.addNode(); - ASSERT_EQ(1u, G1.numberOfNodes()); - G1.addNode(); - ASSERT_EQ(2u, G1.numberOfNodes()); - G1.removeNode(0); - ASSERT_EQ(1u, G1.numberOfNodes()); - G1.removeNode(1); - ASSERT_EQ(0u, G1.numberOfNodes()); -} - -TEST_P(InducedSubgraphViewGTest, testNumberOfEdges) { - ASSERT_EQ(this->m_house, this->Ghouse.numberOfEdges()); - - InducedSubgraphView G1 = createInducedSubgraphView(5); - ASSERT_EQ(0u, G1.numberOfEdges()); - G1.addEdge(0, 1); - ASSERT_EQ(1u, G1.numberOfEdges()); - G1.addEdge(1, 2); - ASSERT_EQ(2u, G1.numberOfEdges()); - G1.removeEdge(0, 1); - ASSERT_EQ(1u, G1.numberOfEdges()); - G1.removeEdge(1, 2); - ASSERT_EQ(0u, G1.numberOfEdges()); -} - -TEST_P(InducedSubgraphViewGTest, testNumberOfSelfLoops) { - InducedSubgraphView G = createInducedSubgraphView(3); - G.addEdge(0, 1); - ASSERT_EQ(0u, G.numberOfSelfLoops()); - G.addEdge(0, 0); - ASSERT_EQ(1u, G.numberOfSelfLoops()); - G.addEdge(1, 1); - G.addEdge(1, 2); - ASSERT_EQ(2u, G.numberOfSelfLoops()); - G.removeEdge(0, 0); - ASSERT_EQ(1u, G.numberOfSelfLoops()); -} - -TEST_P(InducedSubgraphViewGTest, testSelfLoopConversion) { - Aux::Random::setSeed(1, false); - const count runs = 100; - const count n_max = 200; - for (index i = 0; i < runs; i++) { - bool directed = Aux::Random::probability() < 0.5; - count n = Aux::Random::integer(n_max); - InducedSubgraphView G(n, false, directed); - - G.forNodes([&](node v) { - double p = Aux::Random::probability(); - - if (p < 0.1) { // new node - n++; - G.addNode(); - } else { // new edge - node u = Aux::Random::integer(v, n - 1); // self-loops possible - G.addEdge(v, u); + GraphTools::randomizeWeights(base); + } + for (const auto &variant : subsetVariants(base)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView G(base, variant.nodes); + GraphW expected = inducedSubgraph(base, variant.nodes); + for (node u : variant.nodes) { + EXPECT_DOUBLE_EQ(expected.weightedDegree(u), G.weightedDegree(u)); + EXPECT_DOUBLE_EQ(expected.weightedDegree(u, true), G.weightedDegree(u, true)); + EXPECT_DOUBLE_EQ(expected.weightedDegreeIn(u), G.weightedDegreeIn(u)); + EXPECT_DOUBLE_EQ(expected.weightedDegreeIn(u, true), G.weightedDegreeIn(u, true)); } - }); - count measuredSelfLoops = countSelfLoopsManually(G); - EXPECT_EQ(G.numberOfSelfLoops(), measuredSelfLoops); - InducedSubgraphView G_converted(G, false, !directed); - EXPECT_EQ(G_converted.numberOfSelfLoops(), measuredSelfLoops); - } -} - -TEST_P(InducedSubgraphViewGTest, testUpperNodeIdBound) { - ASSERT_EQ(5u, this->Ghouse.upperNodeIdBound()); - - InducedSubgraphView G1 = createInducedSubgraphView(0); - ASSERT_EQ(0u, G1.upperNodeIdBound()); - G1.addNode(); - ASSERT_EQ(1u, G1.upperNodeIdBound()); - G1.addNode(); - ASSERT_EQ(2u, G1.upperNodeIdBound()); - G1.removeNode(1); - ASSERT_EQ(2u, G1.upperNodeIdBound()); - G1.addNode(); - ASSERT_EQ(3u, G1.upperNodeIdBound()); -} - -TEST_P(InducedSubgraphViewGTest, testCheckConsistency_MultiEdgeDetection) { - InducedSubgraphView G = createInducedSubgraphView(3); - ASSERT_TRUE(G.checkConsistency()); - G.addEdge(0, 1); - ASSERT_TRUE(G.checkConsistency()); - G.addEdge(0, 2); - G.addEdge(0, 1); - ASSERT_FALSE(G.checkConsistency()); - G.removeEdge(0, 1); - ASSERT_TRUE(G.checkConsistency()); - G.removeEdge(0, 1); - ASSERT_TRUE(G.checkConsistency()); -} - -/** EDGE ATTRIBUTES **/ - -TEST_P(InducedSubgraphViewGTest, testWeight) { - this->Ghouse.forNodes([&](node u) { - this->Ghouse.forNodes( - [&](node v) { ASSERT_EQ(this->Ahouse[u][v], this->Ghouse.weight(u, v)); }); - }); -} - -TEST_P(InducedSubgraphViewGTest, testSetWeight) { - InducedSubgraphView G = createInducedSubgraphView(10); - G.addEdge(0, 1); - G.addEdge(1, 2); - - if (isWeighted()) { - // edges should get weight defaultWeight on creation and setWeight should - // overwrite this - G.setWeight(1, 2, 2.718); - EXPECT_EQ(defaultEdgeWeight, G.weight(0, 1)); - EXPECT_EQ(2.718, G.weight(1, 2)); - if (isDirected()) { - EXPECT_EQ(nullWeight, G.weight(1, 0)); - EXPECT_EQ(nullWeight, G.weight(2, 1)); - } else { - // undirected graph is symmetric - EXPECT_EQ(defaultEdgeWeight, G.weight(1, 0)); - EXPECT_EQ(2.718, G.weight(2, 1)); - } - - // setting an edge weight should create the edge if it doesn't exists - ASSERT_FALSE(G.hasEdge(5, 6)); - G.setWeight(5, 6, 56.0); - ASSERT_EQ(56.0, G.weight(5, 6)); - ASSERT_EQ(isDirected() ? nullWeight : 56.0, G.weight(6, 5)); - ASSERT_TRUE(G.hasEdge(5, 6)); - - // directed graphs are not symmetric, undirected are - G.setWeight(2, 1, 5.243); - if (isDirected()) { - EXPECT_EQ(2.718, G.weight(1, 2)); - EXPECT_EQ(5.243, G.weight(2, 1)); - } else { - EXPECT_EQ(5.243, G.weight(1, 2)); - EXPECT_EQ(5.243, G.weight(2, 1)); - } - - // self-loop - G.addEdge(4, 4, 2.5); - ASSERT_EQ(2.5, G.weight(4, 4)); - G.setWeight(4, 4, 3.14); - ASSERT_EQ(3.14, G.weight(4, 4)); - } else { - EXPECT_ANY_THROW(G.setWeight(0, 1, 1.5)); - } -} - -TEST_P(InducedSubgraphViewGTest, increaseWeight) { - InducedSubgraphView G = createInducedSubgraphView(5); - G.addEdge(0, 1); - G.addEdge(1, 2); - G.addEdge(3, 4, 3.14); - - if (G.isWeighted()) { - G.increaseWeight(1, 2, 0.5); - G.increaseWeight(3, 4, -0.5); - - ASSERT_EQ(defaultEdgeWeight, G.weight(0, 1)); - ASSERT_EQ(defaultEdgeWeight + 0.5, G.weight(1, 2)); - ASSERT_EQ(3.14 - 0.5, G.weight(3, 4)); - - if (G.isDirected()) { - // reverse edges do net exist => weight should be nullWeight - ASSERT_EQ(nullWeight, G.weight(1, 0)); - ASSERT_EQ(nullWeight, G.weight(2, 1)); - ASSERT_EQ(nullWeight, G.weight(4, 3)); - } else { - ASSERT_EQ(defaultEdgeWeight, G.weight(1, 0)); - ASSERT_EQ(defaultEdgeWeight + 0.5, G.weight(2, 1)); - ASSERT_EQ(3.14 - 0.5, G.weight(3, 4)); } - } else { - EXPECT_ANY_THROW(G.increaseWeight(1, 2, 0.3)); - EXPECT_ANY_THROW(G.increaseWeight(2, 3, 0.3)); // edge does not exists - } -} - -/** SUMS **/ - -TEST_P(InducedSubgraphViewGTest, testTotalEdgeWeight) { - InducedSubgraphView G1 = createInducedSubgraphView(5); - InducedSubgraphView G2 = createInducedSubgraphView(5); - G2.addEdge(0, 1, 3.14); - - if (this->Ghouse.isWeighted()) { - ASSERT_EQ(0.0, G1.totalEdgeWeight()); - ASSERT_EQ(3.14, G2.totalEdgeWeight()); - ASSERT_EQ(36.0, this->Ghouse.totalEdgeWeight()); - } else { - ASSERT_EQ(0 * defaultEdgeWeight, G1.totalEdgeWeight()); - ASSERT_EQ(1 * defaultEdgeWeight, G2.totalEdgeWeight()); - ASSERT_EQ(8 * defaultEdgeWeight, this->Ghouse.totalEdgeWeight()); } } - -/** Collections **/ - -TEST_P(InducedSubgraphViewGTest, testNodeIterator) { - Aux::Random::setSeed(42, false); - - auto testForward = [](const InducedSubgraphView &G) { - auto preIter = G.nodeRange().begin(); - auto postIter = G.nodeRange().begin(); - - G.forNodes([&](const node u) { - ASSERT_EQ(*preIter, u); - ASSERT_EQ(*postIter, u); - ++preIter; - postIter++; - }); - - ASSERT_EQ(preIter, G.nodeRange().end()); - ASSERT_EQ(postIter, G.nodeRange().end()); - - InducedSubgraphView G1(G); - - for (const auto u : Graph::NodeRange(G)) { - ASSERT_TRUE(G1.hasNode(u)); - G1.removeNode(u); - } - - ASSERT_EQ(G1.numberOfNodes(), 0); - }; - - auto testBackward = [](const InducedSubgraphView &G) { - const std::vector nodes(Graph::NodeRange(G).begin(), Graph::NodeRange(G).end()); - std::vector v; - G.forNodes([&](node u) { v.push_back(u); }); - - ASSERT_EQ(std::unordered_set(nodes.begin(), nodes.end()).size(), nodes.size()); - ASSERT_EQ(nodes.size(), G.numberOfNodes()); - - auto preIter = G.nodeRange().begin(); - auto postIter = G.nodeRange().begin(); - for (count i = 0; i < G.numberOfNodes(); ++i) { - ++preIter; - postIter++; - } - - ASSERT_EQ(preIter, G.nodeRange().end()); - ASSERT_EQ(postIter, G.nodeRange().end()); - auto vecIter = nodes.rbegin(); - while (vecIter != nodes.rend()) { - ASSERT_EQ(*vecIter, *(--preIter)); - if (postIter != G.nodeRange().end()) { - ASSERT_NE(*vecIter, *(postIter--)); - } else { - postIter--; - } - ASSERT_EQ(*vecIter, *postIter); - ++vecIter; - } - - ASSERT_EQ(preIter, G.nodeRange().begin()); - ASSERT_EQ(postIter, G.nodeRange().begin()); - }; - - InducedSubgraphView G(this->Ghouse); - testForward(G); - testBackward(G); - - G.removeNode(GraphTools::randomNode(G)); - G.removeNode(GraphTools::randomNode(G)); - - testForward(G); - testBackward(G); -} - -TEST_P(InducedSubgraphViewGTest, testEdgeIterator) { - InducedSubgraphView G(this->Ghouse); - - auto testForward = [&](const InducedSubgraphView &G) { - InducedSubgraphView G1(G); - auto preIter = G.edgeRange().begin(); - auto postIter = G.edgeRange().begin(); - - G.forEdges([&](node, node) { - ASSERT_EQ(preIter, postIter); - const auto edge = *preIter; - ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); - G1.removeEdge(edge.u, edge.v); - ++preIter; - postIter++; - }); - - ASSERT_EQ(G1.numberOfEdges(), 0); - ASSERT_EQ(preIter, G.edgeRange().end()); - ASSERT_EQ(postIter, G.edgeRange().end()); - - G1 = G; - for (const auto edge : Graph::EdgeRange(G)) { - ASSERT_TRUE(G1.hasEdge(edge.u, edge.v)); - G1.removeEdge(edge.u, edge.v); - } - - ASSERT_EQ(G1.numberOfEdges(), 0); - }; - - auto testForwardWeighted = [&](const InducedSubgraphView &G) { - InducedSubgraphView G1(G); - auto preIter = G.edgeWeightRange().begin(); - auto postIter = preIter; - - G.forEdges([&](node, node) { - ASSERT_EQ(preIter, postIter); - - const auto edge = *preIter; - ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); - ASSERT_DOUBLE_EQ(G.weight(edge.u, edge.v), edge.weight); - G1.removeEdge(edge.u, edge.v); - ++preIter; - postIter++; - }); - - ASSERT_EQ(G1.numberOfEdges(), 0); - ASSERT_EQ(preIter, G.edgeWeightRange().end()); - ASSERT_EQ(postIter, G.edgeWeightRange().end()); - - G1 = G; - for (const auto edge : Graph::EdgeWeightRange(G)) { - ASSERT_TRUE(G1.hasEdge(edge.u, edge.v)); - ASSERT_DOUBLE_EQ(G1.weight(edge.u, edge.v), edge.weight); - G1.removeEdge(edge.u, edge.v); - } - - ASSERT_EQ(G1.numberOfEdges(), 0); - }; - - auto testBackward = [&](const InducedSubgraphView &G) { - InducedSubgraphView G1(G); - auto preIter = G.edgeRange().begin(); - auto postIter = preIter; - G.forEdges([&](node, node) { - ++preIter; - postIter++; - }); - - ASSERT_EQ(preIter, G.edgeRange().end()); - ASSERT_EQ(postIter, G.edgeRange().end()); - - G.forEdges([&](node, node) { - --preIter; - postIter--; - ASSERT_EQ(preIter, postIter); - const auto edge = *preIter; - ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); - G1.removeEdge(edge.u, edge.v); - }); - - ASSERT_EQ(G1.numberOfEdges(), 0); - }; - - auto testBackwardWeighted = [&](const InducedSubgraphView &G) { - InducedSubgraphView G1(G); - auto preIter = G.edgeWeightRange().begin(); - auto postIter = preIter; - G.forEdges([&](node, node) { - ++preIter; - postIter++; - }); - - G.forEdges([&](node, node) { - --preIter; - postIter--; - ASSERT_EQ(preIter, postIter); - - const auto edge = *preIter; - ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); - ASSERT_DOUBLE_EQ(G.weight(edge.u, edge.v), edge.weight); - G1.removeEdge(edge.u, edge.v); - }); - - ASSERT_EQ(G1.numberOfEdges(), 0); - }; - - auto doTests = [&](const InducedSubgraphView &G) { - testForward(G); - testBackward(G); - testForwardWeighted(G); - testBackwardWeighted(G); - }; - - doTests(G); - - for (int seed : {1, 2, 3, 4, 5}) { - Aux::Random::setSeed(seed, false); - InducedSubgraphView G1(G); - for (int i = 0; i < 3; ++i) { - auto e = GraphTools::randomEdge(G1); - G1.removeEdge(e.first, e.second); - } - - doTests(G1); - } -} - -TEST_P(InducedSubgraphViewGTest, testNeighborsIterators) { - auto iter = this->Ghouse.neighborRange(1).begin(); - this->Ghouse.forNeighborsOf(1, [&](node v) { - ASSERT_TRUE(*iter == v); - ++iter; - }); - ASSERT_TRUE(iter == this->Ghouse.neighborRange(1).end()); - - if (this->Ghouse.isWeighted()) { - auto iterW = this->Ghouse.weightNeighborRange(1).begin(); - this->Ghouse.forNeighborsOf(1, [&](node v, edgeweight w) { - ASSERT_TRUE((*iterW).first == v); - ASSERT_TRUE((*iterW).second == w); - ++iterW; - }); - ASSERT_TRUE(iterW == this->Ghouse.weightNeighborRange(1).end()); - } - - if (this->Ghouse.isDirected()) { - auto inIter = this->Ghouse.inNeighborRange(1).begin(); - this->Ghouse.forInNeighborsOf(1, [&](node v) { - ASSERT_TRUE(*inIter == v); - ++inIter; - }); - ASSERT_TRUE(inIter == this->Ghouse.inNeighborRange(1).end()); - - if (this->Ghouse.isWeighted()) { - auto iterW = this->Ghouse.weightInNeighborRange(1).begin(); - this->Ghouse.forInNeighborsOf(1, [&](node v, edgeweight w) { - ASSERT_TRUE((*iterW).first == v); - ASSERT_TRUE((*iterW).second == w); - ++iterW; - }); - ASSERT_TRUE(iterW == this->Ghouse.weightInNeighborRange(1).end()); - } - } -} - -/** NODE ITERATORS **/ - -TEST_P(InducedSubgraphViewGTest, testForNodes) { - InducedSubgraphView G = createInducedSubgraphView(3); - std::vector visited(4, false); - G.forNodes([&](node v) { - ASSERT_FALSE(visited[v]); - if (v == 2) { - G.addNode(); - } - visited[v] = true; - }); - for (bool b : visited) { - ASSERT_TRUE(b); - } -} - -TEST_P(InducedSubgraphViewGTest, testParallelForNodes) { - std::vector visited(Ghouse.upperNodeIdBound()); - this->Ghouse.parallelForNodes([&](node u) { visited[u] = u; }); - - Aux::Parallel::sort(visited.begin(), visited.end()); - - ASSERT_EQ(5u, visited.size()); - for (index i = 0; i < this->Ghouse.upperNodeIdBound(); i++) { - ASSERT_EQ(i, visited[i]); - } -} - -TEST_P(InducedSubgraphViewGTest, forNodesWhile) { - count n = 100; - InducedSubgraphView G = createInducedSubgraphView(n); - count stopAfter = 10; - count nodesSeen = 0; - - G.forNodesWhile([&]() { return nodesSeen < stopAfter; }, [&](node) { nodesSeen++; }); - - ASSERT_EQ(stopAfter, nodesSeen); -} - -TEST_P(InducedSubgraphViewGTest, testForNodesInRandomOrder) { - count n = 1000; - count samples = 100; - double maxAbsoluteError = 0.005; - InducedSubgraphView G = createInducedSubgraphView(n); - - node lastNode = n / 2; - count greaterLastNode = 0; - count smallerLastNode = 0; - std::vector visitCount(n, 0); - - for (count i = 0; i < samples; i++) { - G.forNodesInRandomOrder([&](node v) { - if (v > lastNode) { - greaterLastNode++; - } else { - smallerLastNode++; - } - visitCount[v]++; - lastNode = v; - }); - } - - for (node v = 0; v < n; v++) { - ASSERT_EQ(samples, visitCount[v]); - } - - ASSERT_NEAR(0.5, (double)greaterLastNode / n / samples, maxAbsoluteError); - ASSERT_NEAR(0.5, (double)smallerLastNode / n / samples, maxAbsoluteError); -} - -TEST_P(InducedSubgraphViewGTest, testForNodePairs) { - count n = 10; - count m = n * (n - 1) / 2; - InducedSubgraphView G = createInducedSubgraphView(n); - - // add all edges - G.forNodePairs([&](node u, node v) { - ASSERT_FALSE(G.hasEdge(u, v)); - G.addEdge(u, v); - ASSERT_TRUE(G.hasEdge(u, v)); - }); - - EXPECT_EQ(m, G.numberOfEdges()); - - // remove all edges - G.forNodePairs([&](node u, node v) { - ASSERT_TRUE(G.hasEdge(u, v)); - G.removeEdge(u, v); - ASSERT_FALSE(G.hasEdge(u, v)); - }); - - EXPECT_EQ(0u, G.numberOfEdges()); -} - -/** EDGE ITERATORS **/ - -TEST_P(InducedSubgraphViewGTest, testForEdges) { - InducedSubgraphView G = createInducedSubgraphView(4); - G.addEdge(0, 1); // 0 * 1 = 0 - G.addEdge(1, 2); // 1 * 2 = 2 - G.addEdge(3, 2); // 3 * 2 = 1 (mod 5) - G.addEdge(2, 2); // 2 * 2 = 4 - G.addEdge(3, 1); // 3 * 1 = 3 - - std::vector edgesSeen(5, false); - - G.forEdges([&](node u, node v) { - ASSERT_TRUE(G.hasEdge(u, v)); - index id = (u * v) % 5; - edgesSeen[id] = true; - }); - - for (auto b : edgesSeen) { - ASSERT_TRUE(b); - } -} - -TEST_P(InducedSubgraphViewGTest, testForWeightedEdges) { - double epsilon = 1e-6; - - InducedSubgraphView G = createInducedSubgraphView(4); - G.addEdge(0, 1, 0.1); // 0 * 1 = 0 - G.addEdge(3, 2, 0.2); // 3 * 2 = 1 (mod 5) - G.addEdge(1, 2, 0.3); // 1 * 2 = 2 - G.addEdge(3, 1, 0.4); // 3 * 1 = 3 - G.addEdge(2, 2, 0.5); // 2 * 2 = 4 - - std::vector edgesSeen(5, false); - - edgeweight weightSum = 0; - G.forEdges([&](node u, node v, edgeweight ew) { - ASSERT_TRUE(G.hasEdge(u, v)); - ASSERT_EQ(G.weight(u, v), ew); - - index id = (u * v) % 5; - edgesSeen[id] = true; - if (G.isWeighted()) { - ASSERT_NEAR((id + 1) * 0.1, ew, epsilon); - } else { - ASSERT_EQ(defaultEdgeWeight, ew); - } - weightSum += ew; - }); - - for (auto b : edgesSeen) { - ASSERT_TRUE(b); - } - if (G.isWeighted()) { - ASSERT_NEAR(1.5, weightSum, epsilon); - } else { - ASSERT_NEAR(5 * defaultEdgeWeight, weightSum, epsilon); - } -} - -TEST_P(InducedSubgraphViewGTest, testParallelForWeightedEdges) { - count n = 4; - InducedSubgraphView G = createInducedSubgraphView(n); - G.forNodePairs([&](node u, node v) { G.addEdge(u, v, 1.0); }); - - edgeweight weightSum = 0.0; - G.parallelForEdges([&](node, node, edgeweight ew) { -#pragma omp atomic - weightSum += ew; - }); - - ASSERT_EQ(6.0, weightSum) << "sum of edge weights should be 6 in every case"; -} - -TEST_P(InducedSubgraphViewGTest, testParallelForEdges) { - count n = 4; - InducedSubgraphView G = createInducedSubgraphView(n); - G.forNodePairs([&](node u, node v) { G.addEdge(u, v); }); - - edgeweight weightSum = 0.0; - G.parallelForEdges([&](node, node) { -#pragma omp atomic - weightSum += 1; - }); - - ASSERT_EQ(6.0, weightSum) << "sum of edge weights should be 6 in every case"; -} - -/** NEIGHBORHOOD ITERATORS **/ - -TEST_P(InducedSubgraphViewGTest, testForNeighborsOf) { - std::vector visited; - this->Ghouse.forNeighborsOf(3, [&](node u) { visited.push_back(u); }); - - Aux::Parallel::sort(visited.begin(), visited.end()); - - if (isDirected()) { - ASSERT_EQ(2u, visited.size()); - ASSERT_EQ(1u, visited[0]); - ASSERT_EQ(2u, visited[1]); - } else { - ASSERT_EQ(3u, visited.size()); - ASSERT_EQ(1u, visited[0]); - ASSERT_EQ(2u, visited[1]); - ASSERT_EQ(4u, visited[2]); - } -} - -TEST_P(InducedSubgraphViewGTest, testForWeightedNeighborsOf) { - std::vector> visited; - this->Ghouse.forNeighborsOf( - 3, [&](node u, edgeweight ew) { visited.push_back(std::make_pair(u, ew)); }); - - // should sort after the first element - Aux::Parallel::sort(visited.begin(), visited.end()); - - if (isInducedSubgraphView()) { - ASSERT_EQ(3u, visited.size()); - ASSERT_EQ(1u, visited[0].first); - ASSERT_EQ(2u, visited[1].first); - ASSERT_EQ(4u, visited[2].first); - ASSERT_EQ(defaultEdgeWeight, visited[0].second); - ASSERT_EQ(defaultEdgeWeight, visited[1].second); - ASSERT_EQ(defaultEdgeWeight, visited[2].second); - } - - if (isWeightedInducedSubgraphView()) { - ASSERT_EQ(3u, visited.size()); - ASSERT_EQ(1u, visited[0].first); - ASSERT_EQ(2u, visited[1].first); - ASSERT_EQ(4u, visited[2].first); - ASSERT_EQ(1.0, visited[0].second); - ASSERT_EQ(7.0, visited[1].second); - ASSERT_EQ(6.0, visited[2].second); - } - - if (isDirectedInducedSubgraphView()) { - ASSERT_EQ(2u, visited.size()); - ASSERT_EQ(1u, visited[0].first); - ASSERT_EQ(2u, visited[1].first); - ASSERT_EQ(defaultEdgeWeight, visited[0].second); - ASSERT_EQ(defaultEdgeWeight, visited[1].second); - } - - if (isWeightedDirectedInducedSubgraphView()) { - ASSERT_EQ(2u, visited.size()); - ASSERT_EQ(1u, visited[0].first); - ASSERT_EQ(2u, visited[1].first); - ASSERT_EQ(1.0, visited[0].second); - ASSERT_EQ(7.0, visited[1].second); - } -} - -TEST_P(InducedSubgraphViewGTest, testForEdgesOf) { - count m = 0; - std::vector visited(this->m_house, 0); - - this->Ghouse.forNodes([&](node u) { - this->Ghouse.forEdgesOf(u, [&](node v, node w) { - // edges should be v to w, so if we iterate over edges from u, u should be - // equal v - EXPECT_EQ(u, v); - - auto e = std::make_pair(v, w); - auto it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e); - if (!isDirected() && it == this->houseEdgesOut.end()) { - auto e2 = std::make_pair(w, v); - it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e2); - } - - EXPECT_TRUE(it != this->houseEdgesOut.end()); - - // find index in edge array - int i = std::distance(this->houseEdgesOut.begin(), it); - if (isDirected()) { - // make sure edge was not visited before (would be visited twice) - EXPECT_EQ(0, visited[i]); - } - - // mark edge as visited - visited[i]++; - m++; - }); - }); - - if (isDirected()) { - // we iterated over all outgoing edges once - EXPECT_EQ(this->m_house, m); - for (auto c : visited) { - EXPECT_EQ(1, c); - } - } else { - // we iterated over all edges in both directions - EXPECT_EQ(2 * this->m_house, m); - for (auto c : visited) { - EXPECT_EQ(2, c); - } - } -} - -TEST_P(InducedSubgraphViewGTest, testForWeightedEdgesOf) { - count m = 0; - std::vector visited(this->m_house, 0); - double sumOfWeights = 0; - - this->Ghouse.forNodes([&](node u) { - this->Ghouse.forEdgesOf(u, [&](node v, node w, edgeweight ew) { - // edges should be v to w, so if we iterate over edges from u, u should be - // equal v - EXPECT_EQ(u, v); - sumOfWeights += ew; - auto e = std::make_pair(v, w); - auto it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e); - if (!isDirected() && it == this->houseEdgesOut.end()) { - auto e2 = std::make_pair(w, v); - it = std::find(this->houseEdgesOut.begin(), this->houseEdgesOut.end(), e2); - } - - EXPECT_TRUE(it != this->houseEdgesOut.end()); - - // find index in edge array - int i = std::distance(this->houseEdgesOut.begin(), it); - if (isDirected()) { - // make sure edge was not visited before (would be visited twice) - EXPECT_EQ(0, visited[i]); - } - - // mark edge as visited - visited[i]++; - m++; - }); - }); - - if (isInducedSubgraphView()) { - EXPECT_EQ(sumOfWeights, m); - EXPECT_EQ(2 * this->m_house, m); - for (auto c : visited) { - EXPECT_EQ(2, c); - } - } - - if (isWeightedInducedSubgraphView()) { - // we iterated over all edges in both directions - EXPECT_EQ(2 * this->m_house, m); - EXPECT_EQ(sumOfWeights, 72); - for (auto c : visited) { - EXPECT_EQ(2, c); - } - } - - if (isDirectedInducedSubgraphView()) { - // we iterated over all outgoing edges once - EXPECT_EQ(this->m_house, m); - EXPECT_EQ(sumOfWeights, m); - for (auto c : visited) { - EXPECT_EQ(1, c); - } - } - - if (isWeightedDirectedInducedSubgraphView()) { - EXPECT_EQ(sumOfWeights, 36); - EXPECT_EQ(this->m_house, m); - for (auto c : visited) { - EXPECT_EQ(1, c); - } - } -} - -TEST_P(InducedSubgraphViewGTest, testForInNeighborsOf) { - std::vector visited; - this->Ghouse.forInNeighborsOf(2, [&](node v) { visited.push_back(v); }); - Aux::Parallel::sort(visited.begin(), visited.end()); - - if (isDirected()) { - EXPECT_EQ(2u, visited.size()); - EXPECT_EQ(0u, visited[0]); - EXPECT_EQ(3u, visited[1]); - } else { - EXPECT_EQ(4u, visited.size()); - EXPECT_EQ(0u, visited[0]); - EXPECT_EQ(1u, visited[1]); - EXPECT_EQ(3u, visited[2]); - EXPECT_EQ(4u, visited[3]); - } -} - -TEST_P(InducedSubgraphViewGTest, testForWeightedInNeighborsOf) { - std::vector> visited; - this->Ghouse.forInNeighborsOf(3, [&](node v, edgeweight ew) { visited.push_back({v, ew}); }); - Aux::Parallel::sort(visited.begin(), visited.end()); - - if (isInducedSubgraphView()) { - ASSERT_EQ(3u, visited.size()); - ASSERT_EQ(1u, visited[0].first); - ASSERT_EQ(2u, visited[1].first); - ASSERT_EQ(4u, visited[2].first); - ASSERT_EQ(defaultEdgeWeight, visited[0].second); - ASSERT_EQ(defaultEdgeWeight, visited[1].second); - ASSERT_EQ(defaultEdgeWeight, visited[2].second); - } - - if (isWeightedInducedSubgraphView()) { - ASSERT_EQ(3u, visited.size()); - ASSERT_EQ(1u, visited[0].first); - ASSERT_EQ(2u, visited[1].first); - ASSERT_EQ(4u, visited[2].first); - ASSERT_EQ(1.0, visited[0].second); - ASSERT_EQ(7.0, visited[1].second); - ASSERT_EQ(6.0, visited[2].second); - } - - if (isDirectedInducedSubgraphView()) { - ASSERT_EQ(1u, visited.size()); - ASSERT_EQ(4u, visited[0].first); - ASSERT_EQ(defaultEdgeWeight, visited[0].second); - } - - if (isWeightedDirectedInducedSubgraphView()) { - ASSERT_EQ(1u, visited.size()); - ASSERT_EQ(4u, visited[0].first); - ASSERT_EQ(6.0, visited[0].second); - } -} - -TEST_P(InducedSubgraphViewGTest, testForInEdgesOf) { - std::vector visited(this->n_house, false); - this->Ghouse.forInEdgesOf(3, [&](node u, node v) { - ASSERT_EQ(3u, u); - if (isDirected()) { - ASSERT_TRUE(this->Ahouse[v][u] > 0.0); - ASSERT_TRUE(this->Ghouse.hasEdge(v, u)); - } - ASSERT_FALSE(visited[v]); - visited[v] = true; - }); - - if (isDirected()) { - EXPECT_FALSE(visited[0]); - EXPECT_FALSE(visited[1]); - EXPECT_FALSE(visited[2]); - EXPECT_FALSE(visited[3]); - EXPECT_TRUE(visited[4]); - } else { - EXPECT_FALSE(visited[0]); - EXPECT_TRUE(visited[1]); - EXPECT_TRUE(visited[2]); - EXPECT_FALSE(visited[3]); - EXPECT_TRUE(visited[4]); - } -} - -TEST_P(InducedSubgraphViewGTest, testForWeightedInEdgesOf) { - // add self-loop - this->Ghouse.addEdge(3, 3, 2.5); - this->Ahouse[3][3] = 2.5; - - std::vector visited(this->n_house, -1.0); - this->Ghouse.forInEdgesOf(3, [&](node v, node u, edgeweight ew) { - ASSERT_EQ(-1.0, visited[v]); - visited[u] = ew; - }); - - if (isInducedSubgraphView()) { - ASSERT_EQ(-1.0, visited[0]); - ASSERT_EQ(defaultEdgeWeight, visited[1]); - ASSERT_EQ(defaultEdgeWeight, visited[2]); - ASSERT_EQ(defaultEdgeWeight, visited[3]); - ASSERT_EQ(defaultEdgeWeight, visited[4]); - } - - if (isWeightedInducedSubgraphView()) { - ASSERT_EQ(-1.0, visited[0]); - ASSERT_EQ(this->Ahouse[3][1], visited[1]); - ASSERT_EQ(this->Ahouse[3][2], visited[2]); - ASSERT_EQ(this->Ahouse[3][3], visited[3]); - ASSERT_EQ(this->Ahouse[3][4], visited[4]); - } - - if (isDirectedInducedSubgraphView()) { - ASSERT_EQ(-1.0, visited[0]); - ASSERT_EQ(-1.0, visited[1]); - ASSERT_EQ(-1.0, visited[2]); - ASSERT_EQ(defaultEdgeWeight, visited[3]); - ASSERT_EQ(defaultEdgeWeight, visited[4]); - } - - if (isWeightedDirectedInducedSubgraphView()) { - ASSERT_EQ(-1.0, visited[0]); - ASSERT_EQ(-1.0, visited[1]); - ASSERT_EQ(-1.0, visited[2]); - ASSERT_EQ(this->Ahouse[3][3], visited[3]); - ASSERT_EQ(this->Ahouse[4][3], visited[4]); - } -} - -/** REDUCTION ITERATORS **/ - -TEST_P(InducedSubgraphViewGTest, testParallelSumForNodes) { - count n = 10; - InducedSubgraphView G = createInducedSubgraphView(n); - double sum = G.parallelSumForNodes([](node v) { return 2 * v + 0.5; }); - - double expected_sum = n * (n - 1) + n * 0.5; - ASSERT_EQ(expected_sum, sum); -} - -TEST_P(InducedSubgraphViewGTest, testParallelSumForWeightedEdges) { - double sum = - this->Ghouse.parallelSumForEdges([](node, node, edgeweight ew) { return 1.5 * ew; }); - - double expected_sum = 1.5 * this->Ghouse.totalEdgeWeight(); - ASSERT_EQ(expected_sum, sum); -} - -/** GRAPH SEARCHES **/ - -TEST_P(InducedSubgraphViewGTest, testEdgeIndexGenerationDirected) { - InducedSubgraphView G = InducedSubgraphView(10, false, true); - G.addEdge(2, 0); - G.addEdge(2, 1); - G.addEdge(2, 2); - G.addEdge(5, 6); - G.addEdge(6, 5); - G.addEdge(1, 2); - - G.indexEdges(); - - // Check consecutiveness of edgeids according to edge iterators - edgeid expectedId = 0; - G.forEdges([&](node u, node v) { EXPECT_EQ(expectedId++, G.edgeId(u, v)); }); - - // Add some more edges - EXPECT_EQ(6, G.upperEdgeIdBound()); - G.addEdge(8, 9); - EXPECT_EQ(7, G.upperEdgeIdBound()); - G.addEdge(9, 8); - - // Check that asymmetric edges do not have the same id - EXPECT_NE(G.edgeId(6, 5), G.edgeId(5, 6)); - EXPECT_NE(G.edgeId(2, 1), G.edgeId(1, 2)); - EXPECT_NE(G.edgeId(9, 8), G.edgeId(8, 9)); - EXPECT_EQ(7, G.edgeId(9, 8)); - EXPECT_EQ(8, G.upperEdgeIdBound()); -} - -TEST_P(InducedSubgraphViewGTest, testEdgeIndexGenerationUndirected) { - InducedSubgraphView G = InducedSubgraphView(10, false, false); - - G.addEdge(0, 0); - G.addEdge(2, 0); - G.addEdge(2, 1); - G.addEdge(2, 2); - G.addEdge(5, 6); - - G.indexEdges(); - - // Check consecutiveness of edgeids according to edge iterators - edgeid expectedId = 0; - G.forEdges([&](node u, node v) { EXPECT_EQ(expectedId++, G.edgeId(u, v)); }); - - // Add some more edges. This will likely destroy consecutiveness... - G.addEdge(3, 4); - G.addEdge(7, 8); - EXPECT_EQ(6, G.edgeId(7, 8)); - EXPECT_EQ(7, G.upperEdgeIdBound()); - - // Anyway, check uniqueness and validity of the edgeids - std::set ids; - edgeid upperEdgeIdBound = G.upperEdgeIdBound(); - - G.forEdges([&](node u, node v) { - edgeid id = G.edgeId(u, v); - EXPECT_EQ(id, G.edgeId(v, u)); - EXPECT_LT(id, upperEdgeIdBound); - - EXPECT_NE(none, id); - EXPECT_FALSE(ids.erase(id)); - ids.insert(id); - }); -} - -TEST_P(InducedSubgraphViewGTest, testEdgeIndexResolver) { - InducedSubgraphView G = createInducedSubgraphView(10); - G.indexEdges(); - - G.addEdge(0, 0); - G.addEdge(5, 6); - G.addEdge(2, 2); - - if (G.isDirected()) - G.addEdge(3, 2); - - std::map, edgeid> expectedEdges; - expectedEdges[std::make_pair(0, 0)] = 0; - expectedEdges[std::make_pair(5, 6)] = 1; - expectedEdges[std::make_pair(2, 2)] = 2; - expectedEdges[std::make_pair(3, 2)] = 3; - - G.forEdges([&](node, node, edgeid eid) { - auto edge = G.edgeById(eid); - EXPECT_EQ(expectedEdges[edge], eid); - }); -} - -TEST_P(InducedSubgraphViewGTest, testForEdgesWithIds) { - std::vector graphs; - graphs.emplace_back(10, false, false); - graphs.emplace_back(10, false, true); - graphs.emplace_back(10, true, false); - graphs.emplace_back(10, true, true); - - for (auto graph = graphs.begin(); graph != graphs.end(); ++graph) { - graph->addEdge(0, 0); - graph->addEdge(1, 2); - graph->addEdge(4, 5); - - // No edge indices - - count m = 0; - graph->forEdges([&](node, node, edgeid eid) { - EXPECT_EQ(none, eid); - m++; - }); - ASSERT_EQ(3u, m); - - // With edge indices - graph->indexEdges(); - - edgeid expectedId = 0; - m = 0; - graph->forEdges([&](node, node, edgeid eid) { - EXPECT_EQ(expectedId++, eid); - EXPECT_LT(eid, graph->upperEdgeIdBound()); - m++; - }); - ASSERT_EQ(3u, m); - } -} - -TEST_P(InducedSubgraphViewGTest, testForWeightedEdgesWithIds) { - std::vector graphs; - graphs.emplace_back(10, false, false); - graphs.emplace_back(10, false, true); - graphs.emplace_back(10, true, false); - graphs.emplace_back(10, true, true); - - for (auto graph = graphs.begin(); graph != graphs.end(); ++graph) { - graph->addEdge(0, 0, 2); - graph->addEdge(1, 2, 2); - graph->addEdge(4, 5, 2); - - // No edge indices - - count m = 0; - edgeweight sum = 0; - graph->forEdges([&](node, node, edgeweight ew, edgeid eid) { - EXPECT_EQ(none, eid); - m++; - sum += ew; - }); - ASSERT_EQ(3u, m); - - if (graph->isWeighted()) { - ASSERT_EQ(6.0, sum); - } else { - ASSERT_EQ(3.0, sum); - } - - // With edge indices - graph->indexEdges(); - - edgeid expectedId = 0; - m = 0; - sum = .0; - graph->forEdges([&](node, node, edgeweight ew, edgeid eid) { - EXPECT_EQ(expectedId++, eid); - EXPECT_LT(eid, graph->upperEdgeIdBound()); - m++; - sum += ew; - }); - ASSERT_EQ(3u, m); - - if (graph->isWeighted()) { - ASSERT_EQ(6.0, sum); - } else { - ASSERT_EQ(3.0, sum); - } - } -} - -TEST_P(InducedSubgraphViewGTest, testParallelForEdgesWithIds) { - std::vector graphs; - graphs.emplace_back(10, false, false); - graphs.emplace_back(10, false, true); - graphs.emplace_back(10, true, false); - graphs.emplace_back(10, true, true); - - for (auto graph = graphs.begin(); graph != graphs.end(); ++graph) { - graph->addEdge(0, 0); - graph->addEdge(1, 2); - graph->addEdge(4, 5); - - // No edge indices - count m = 0; - edgeid sumedgeid = 0; - graph->parallelForEdges([&](node, node, edgeid eid) { -#pragma omp atomic - m++; - ASSERT_EQ(none, eid); - }); - ASSERT_EQ(3u, m); - - // With edge indices - graph->indexEdges(); - - m = 0; - edgeid expectedId = 0; - sumedgeid = 0; - graph->parallelForEdges([&](node, node, edgeid eid) { -#pragma omp atomic - expectedId++; -#pragma omp atomic - sumedgeid += eid; -#pragma omp atomic - m++; - }); - ASSERT_EQ(expectedId, graph->upperEdgeIdBound()); - ASSERT_EQ(sumedgeid, ((graph->upperEdgeIdBound() - 1) * graph->upperEdgeIdBound()) / 2); - ASSERT_EQ(3u, m); - } -} - -TEST_P(InducedSubgraphViewGTest, testParallelForWeightedEdgesWithIds) { - std::vector graphs; - graphs.emplace_back(10, false, false); - graphs.emplace_back(10, false, true); - graphs.emplace_back(10, true, false); - graphs.emplace_back(10, true, true); - - for (auto graph = graphs.begin(); graph != graphs.end(); ++graph) { - graph->addEdge(0, 0, 2); - graph->addEdge(1, 2, 2); - graph->addEdge(4, 5, 2); - - // No edge indices - - count m = 0; - edgeweight sum = 0; - edgeid sumedgeid = 0; - graph->parallelForEdges([&](node, node, edgeweight ew, edgeid eid) { -#pragma omp atomic - m++; -#pragma omp atomic - sum += ew; - ASSERT_EQ(none, eid); - }); - ASSERT_EQ(3u, m); - - if (graph->isWeighted()) { - ASSERT_EQ(6.0, sum); - } else { - ASSERT_EQ(3.0, sum); - } - - // With edge indices - graph->indexEdges(); - - m = 0; - sum = .0; - edgeid expectedId = 0; - sumedgeid = 0; - graph->parallelForEdges([&](node, node, edgeweight ew, edgeid eid) { -#pragma omp atomic - expectedId++; -#pragma omp atomic - sumedgeid += eid; -#pragma omp atomic - m++; -#pragma omp atomic - sum += ew; - }); - ASSERT_EQ(expectedId, graph->upperEdgeIdBound()); - ASSERT_EQ(sumedgeid, ((graph->upperEdgeIdBound() - 1) * graph->upperEdgeIdBound()) / 2); - ASSERT_EQ(3u, m); - - if (graph->isWeighted()) { - ASSERT_EQ(6.0, sum); - } else { - ASSERT_EQ(3.0, sum); - } - } -} - -/*TEST_P(GraphGTest, testInForEdgesUndirected) { - METISGraphReader reader; - InducedSubgraphView G = reader.read("input/PGPgiantcompo.graph"); - DEBUG(G.upperNodeIdBound()); - node u = 5474; - G.forInEdgesOf(u, [&](node u, node z, edgeweight w){ - DEBUG("(1) node: ", u, " neigh:", z, " weight: ", w); - }); - G.forEdgesOf(u, [&](node u, node z, edgeweight w){ - DEBUG("(2) node: ", u, " neigh:", z, " weight: ", w); - }); - - - node source = 1492; - DynBFS bfs(G, source, false); - bfs.run(); - - std::vector > choices1; - G.forInEdgesOf(5474, [&](node t, node z, edgeweight w){ - INFO("considered edge (1): ", t, z, w); - if (Aux::NumericTools::logically_equal(bfs.distance(t), bfs.distance(z) + -w)) { - // workaround for integer overflow in large graphs - bigfloat tmp = bfs.getNumberOfPaths(z) / bfs.getNumberOfPaths(t); - double weight; - tmp.ToDouble(weight); - choices1.emplace_back(z, weight); - } - }); - std::vector > choices2; - G.forEdgesOf(5474, [&](node t, node z, edgeweight w){ - INFO("considered edge (2): ", t, z, w); - if (Aux::NumericTools::logically_equal(bfs.distance(t), bfs.distance(z) + -w)) { - // workaround for integer overflow in large graphs - bigfloat tmp = bfs.getNumberOfPaths(z) / bfs.getNumberOfPaths(t); - double weight; - tmp.ToDouble(weight); - choices2.emplace_back(z, weight); - } - }); - - INFO(choices1); - INFO(choices2); -} -*/ - -TEST_P(InducedSubgraphViewGTest, testSortEdges) { - InducedSubgraphView G = this->Ghouse; - - for (int i = 0; i < 2; ++i) { - if (i > 0) { - G.indexEdges(); - } - - InducedSubgraphView origG = G; - - G.sortEdges(); - - std::vector> edges; - edges.reserve(origG.numberOfEdges() * 4); - - std::vector> outEdges; - origG.forNodes([&](node u) { - origG.forEdgesOf(u, [&](node, node v, edgeweight w, edgeid eid) { - outEdges.emplace_back(v, w, eid); - }); - - Aux::Parallel::sort(outEdges.begin(), outEdges.end()); - - for (auto x : outEdges) { - edges.emplace_back(u, std::get<0>(x), std::get<1>(x), std::get<2>(x)); - } - - outEdges.clear(); - - origG.forInEdgesOf(u, [&](node, node v, edgeweight w, edgeid eid) { - outEdges.emplace_back(v, w, eid); - }); - - Aux::Parallel::sort(outEdges.begin(), outEdges.end()); - - for (auto x : outEdges) { - edges.emplace_back(u, std::get<0>(x), std::get<1>(x), std::get<2>(x)); - } - - outEdges.clear(); - }); - - auto it = edges.begin(); - - G.forNodes([&](node u) { - G.forEdgesOf(u, [&](node u, node v, edgeweight w, edgeid eid) { - ASSERT_NE(it, edges.end()); - EXPECT_EQ(*it, std::make_tuple(u, v, w, eid)) - << "Out edge (" << u << ", " << v << ", " << w << ", " << eid - << ") was expected to be (" << std::get<0>(*it) << ", " << std::get<1>(*it) - << ", " << std::get<2>(*it) << ", " << std::get<3>(*it) << ")"; - ++it; - }); - G.forInEdgesOf(u, [&](node u, node v, edgeweight w, edgeid eid) { - ASSERT_NE(it, edges.end()); - EXPECT_EQ(*it, std::make_tuple(u, v, w, eid)) - << "In edge (" << u << ", " << v << ", " << w << ", " << eid - << ") was expected to be (" << std::get<0>(*it) << ", " << std::get<1>(*it) - << ", " << std::get<2>(*it) << ", " << std::get<3>(*it) << ")"; - ++it; - }); - }); - } -} - -TEST_P(InducedSubgraphViewGTest, testEdgeIdsSortingAfterRemove) { - constexpr node n = 100; - - Aux::Random::setSeed(42, true); - InducedSubgraphView G = createInducedSubgraphView(n, 10 * n); - G.sortEdges(); - G.indexEdges(); - auto original = G; - - // remove edges - while (2 * G.numberOfEdges() > original.numberOfEdges()) { - const auto e = GraphTools::randomEdge(G, false); - G.setKeepEdgesSorted(); - G.removeEdge(e.first, e.second); // with sorting after each removal - original.removeEdge(e.first, e.second); // without sorting - } - - original.sortEdges(); // calling sort only once - - G.forNodes([&](node u) { - std::vector allNeighborsOfG; - - G.forNeighborsOf(u, - [&](node, node v, edgeweight, edgeid) { allNeighborsOfG.push_back(v); }); - - std::vector allNeighborsOfOriginal; - - original.forNeighborsOf( - u, [&](node, node v, edgeweight, edgeid) { allNeighborsOfOriginal.push_back(v); }); - - // check that both neighbor vectors are equivalent - EXPECT_EQ(allNeighborsOfG.size(), allNeighborsOfOriginal.size()); - for (index i = 0; i < allNeighborsOfG.size(); ++i) { - EXPECT_EQ(allNeighborsOfG[i], allNeighborsOfOriginal[i]); - } - - if (!isDirected()) - return; - - // directed - - std::vector allInNeighborsOfG; - - G.forInNeighborsOf( - u, [&](node, node v, edgeweight, edgeid) { allInNeighborsOfG.push_back(v); }); - std::vector allInNeighborsOfOriginal; - - original.forInNeighborsOf( - u, [&](node, node v, edgeweight, edgeid) { allInNeighborsOfOriginal.push_back(v); }); - - // check that both in-neighbor vectors are equivalent - EXPECT_EQ(allInNeighborsOfG.size(), allInNeighborsOfOriginal.size()); - for (index i = 0; i < allInNeighborsOfG.size(); ++i) { - EXPECT_EQ(allInNeighborsOfG[i], allInNeighborsOfOriginal[i]); - } - }); -} - -TEST_P(InducedSubgraphViewGTest, testEdgeIdsConsistencyAfterRemove) { - constexpr node n = 100; - - Aux::Random::setSeed(42, true); - InducedSubgraphView G = createInducedSubgraphView(n, 10 * n); - G.sortEdges(); - G.indexEdges(); - auto original = G; - - // remove edges - G.setMaintainCompactEdges(); - while (2 * G.numberOfEdges() > original.numberOfEdges()) { - const auto e = GraphTools::randomEdge(G, false); - G.removeEdge(e.first, e.second); // re-indexing after each removal - original.removeEdge(e.first, e.second); // not re-indexing - } - - original.indexEdges(true); // re-indexing only once - - std::vector existingIDs(G.upperEdgeIdBound(), false); - - G.forNodes([&](node u) { - G.forNeighborsOf(u, [&](node, node v, edgeweight, edgeid id) { - existingIDs[id] = true; - // check that both graphs have the same edge IDs - ASSERT_EQ(id, original.edgeId(u, v)); - }); - - if (!isDirected()) - return; - - G.forInNeighborsOf( - u, [&](node, node v, edgeweight, edgeid id) { ASSERT_EQ(id, original.edgeId(v, u)); }); - }); - - // check that all IDs exist without gaps in between - for (auto ID : existingIDs) { - ASSERT_TRUE(ID); - } -} - -TEST_P(InducedSubgraphViewGTest, testEdgeIdsAfterRemoveWithoutSortingOrIDs) { - constexpr node n = 100; - - Aux::Random::setSeed(42, true); - InducedSubgraphView G = createInducedSubgraphView(n, 10 * n); - G.indexEdges(); - auto original = G; - - // remove some nodes and edges - G.removeNode(5); - G.removeNode(10); - while (2 * G.numberOfEdges() > original.numberOfEdges()) { - const auto e = GraphTools::randomEdge(G, false); - G.removeEdge(e.first, e.second); - } - ASSERT_GT(G.numberOfEdges(), original.numberOfEdges() / 3); - - // check that the remaining edges still have the same ids - G.forNodes([&](node u) { - G.forNeighborsOf( - u, [&](node, node v, edgeweight, edgeid id) { ASSERT_EQ(id, original.edgeId(u, v)); }); - - if (!isDirected()) - return; - - G.forInNeighborsOf( - u, [&](node, node v, edgeweight, edgeid id) { ASSERT_EQ(id, original.edgeId(v, u)); }); - }); -} - -TEST(GraphGTest, testSortNeighborsUndirectedGraph) { - InducedSubgraphView G(6); - G.addEdge(0, 3); - G.addEdge(0, 5); - G.addEdge(0, 4); - G.addEdge(1, 3); - G.addEdge(1, 5); - G.addEdge(1, 4); - G.addEdge(2, 5); - G.addEdge(2, 4); - G.addEdge(2, 3); - G.addEdge(4, 3); - G.addEdge(4, 5); - - std::unordered_map> originalNeighbors; - - G.forNodes([&](const node currentNode) { - originalNeighbors[currentNode] = std::vector(G.neighborRange(currentNode).begin(), - G.neighborRange(currentNode).end()); - }); - - // Sort neighbors - G.forNodes([&](const node currentNode) { - G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { - return neighbor1 < neighbor2; - }); - }); - - // Validate sorting for outgoing neighbors - G.forNodes([&](const node currentNode) { - const auto &sortedNeighbors = G.neighborRange(currentNode); - std::vector sortedNeighborVector(sortedNeighbors.begin(), sortedNeighbors.end()); - EXPECT_TRUE(std::ranges::is_sorted(sortedNeighborVector)); - if (!std::ranges::is_sorted(originalNeighbors[currentNode])) { - EXPECT_NE(originalNeighbors[currentNode], sortedNeighborVector); - } - }); -} - -TEST(GraphGTest, testSortNeighborsUndirectedIndexedGraph) { - InducedSubgraphView G(6); - G.addEdge(0, 3); - G.addEdge(0, 5); - G.addEdge(0, 4); - G.addEdge(1, 3); - G.addEdge(1, 5); - G.addEdge(1, 4); - G.addEdge(2, 5); - G.addEdge(2, 4); - G.addEdge(2, 3); - G.indexEdges(); - - std::unordered_map> originalNeighbors; - std::unordered_map> originalEdgeIds; - - G.forNodes([&](const node currentNode) { - originalNeighbors[currentNode] = std::vector(G.neighborRange(currentNode).begin(), - G.neighborRange(currentNode).end()); - for (size_t i = 0; i < G.degreeOut(currentNode); ++i) { - originalEdgeIds[currentNode].push_back(G.getIthNeighborWithId(currentNode, i).second); - } - }); - - // Sort neighbors - G.forNodes([&](const node currentNode) { - G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { - return neighbor1 < neighbor2; - }); - }); - - G.forNodes([&](const node currentNode) { - const auto &sortedNeighbors = G.neighborRange(currentNode); - std::vector sortedNeighborVector(sortedNeighbors.begin(), sortedNeighbors.end()); - EXPECT_TRUE(std::ranges::is_sorted(sortedNeighborVector)); - if (!std::ranges::is_sorted(originalNeighbors[currentNode])) { - EXPECT_NE(originalNeighbors[currentNode], sortedNeighborVector); - } - - // Validate that indices are sorted according to the sorting of the neighbors - for (size_t i = 0; i < sortedNeighborVector.size(); ++i) { - node neighbor = sortedNeighborVector[i]; - auto it = std::ranges::find(originalNeighbors[currentNode], neighbor); - EXPECT_NE(it, originalNeighbors[currentNode].end()); - size_t originalIndex = std::distance(originalNeighbors[currentNode].begin(), it); - EXPECT_EQ(G.getIthNeighborWithId(currentNode, i).second, - originalEdgeIds[currentNode][originalIndex]); - } - }); -} - -TEST(GraphGTest, testSortNeighborsWeightedUndirectedGraphByWeights) { - InducedSubgraphView G(6, true); - G.addEdge(0, 3, 9.0); - G.addEdge(0, 5, 7.0); - G.addEdge(0, 4, 8.0); - G.addEdge(1, 5, 1.0); - G.addEdge(1, 4, 3.0); - G.addEdge(1, 3, 4.0); - G.addEdge(2, 5, 5.0); - G.addEdge(2, 4, 2.0); - G.addEdge(2, 3, 6.0); - G.addEdge(3, 4, 7.0); - - G.forNodes([&](const node currentNode) { - G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { - return G.weight(currentNode, neighbor1) < G.weight(currentNode, neighbor2); - }); - }); - - // Validate that neighbors are sorted according to weights - G.forNodes([&](const node currentNode) { - const auto sortedNeighbors = G.neighborRange(currentNode); - std::vector sortedWeights; - for (const node neighbor : sortedNeighbors) { - sortedWeights.push_back(G.weight(currentNode, neighbor)); - } - // Ensure weights are sorted in ascending order - EXPECT_TRUE(std::ranges::is_sorted(sortedWeights.begin(), sortedWeights.end())); - }); -} - -TEST(GraphGTest, testSortNeighborsDirectedGraph) { - InducedSubgraphView G(6, false, true); - G.addEdge(0, 3); - G.addEdge(0, 5); - G.addEdge(0, 4); - G.addEdge(1, 3); - G.addEdge(1, 5); - G.addEdge(1, 4); - G.addEdge(5, 2); - G.addEdge(3, 2); - G.addEdge(4, 2); - - std::unordered_map> originalNeighbors; - std::unordered_map> originalInNeighbors; - G.forNodes([&](const node currentNode) { - originalNeighbors[currentNode] = std::vector(G.neighborRange(currentNode).begin(), - G.neighborRange(currentNode).end()); - originalInNeighbors[currentNode] = std::vector(G.inNeighborRange(currentNode).begin(), - G.inNeighborRange(currentNode).end()); - }); - - // Sort neighbors - G.forNodes([&](const node currentNode) { - G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { - return neighbor1 < neighbor2; - }); - }); - - G.forNodes([&](const node currentNode) { - // Validate sorting of outgoing neighbors - const auto &sortedNeighbors = G.neighborRange(currentNode); - std::vector sortedNeighborVector(sortedNeighbors.begin(), sortedNeighbors.end()); - EXPECT_TRUE(std::ranges::is_sorted(sortedNeighborVector)); - if (!std::ranges::is_sorted(originalNeighbors[currentNode])) { - EXPECT_NE(originalNeighbors[currentNode], sortedNeighborVector); - } - - // Validate sorting of incoming neighbors - const auto &sortedInNeighbors = G.inNeighborRange(currentNode); - std::vector sortedInNeighborVector(sortedInNeighbors.begin(), - sortedInNeighbors.end()); - EXPECT_TRUE(std::ranges::is_sorted(sortedInNeighborVector)); - - if (!std::ranges::is_sorted(originalInNeighbors[currentNode])) { - EXPECT_NE(originalInNeighbors[currentNode], sortedInNeighborVector); - } - }); -} - -TEST(GraphGTest, testSortNeighborsWeightedDirectedGraphByWeights) { - InducedSubgraphView G(6, true, true); - G.addEdge(0, 3, 1.0); - G.addEdge(0, 5, 3.0); - G.addEdge(0, 4, 4.0); - G.addEdge(0, 1, 12.0); - G.addEdge(2, 1, 13.0); - G.addEdge(1, 3, 7.0); - G.addEdge(1, 5, 11.0); - G.addEdge(1, 4, 3.0); - G.addEdge(2, 5, 6.0); - G.addEdge(2, 3, 5.0); - G.addEdge(2, 4, 10.0); - - // Sort neighbors according to weights - G.forNodes([&](const node currentNode) { - G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { - return G.weight(currentNode, neighbor1) < G.weight(currentNode, neighbor2); - }); - }); - - // Validate that neighbors are sorted according to weights - G.forNodes([&](const node currentNode) { - const auto sortedNeighbors = G.neighborRange(currentNode); - std::vector sortedWeights; - for (const node neighbor : sortedNeighbors) { - sortedWeights.push_back(G.weight(currentNode, neighbor)); - } - // Ensure weights are sorted in ascending order - EXPECT_TRUE(std::ranges::is_sorted(sortedWeights.begin(), sortedWeights.end())); - - const auto sortedInNeighbors = G.neighborRange(currentNode); - std::vector sortedInWeights; - for (const node neighbor : sortedInNeighbors) { - sortedInWeights.push_back(G.weight(currentNode, neighbor)); - } - // Ensure inWeights are sorted in ascending order - EXPECT_TRUE(std::ranges::is_sorted(sortedInWeights.begin(), sortedInWeights.end())); - }); -} - -TEST(GraphGTest, testSortNeighborsWeightedDirectedIndexedGraph) { - InducedSubgraphView G(6, true, true, true); - G.addEdge(0, 3, 1.0); - G.addEdge(0, 5, 3.0); - G.addEdge(0, 4, 4.0); - G.addEdge(0, 1, 12.0); - G.addEdge(2, 1, 13.0); - G.addEdge(1, 3, 7.0); - G.addEdge(1, 5, 11.0); - G.addEdge(1, 4, 3.0); - G.addEdge(2, 5, 6.0); - G.addEdge(2, 3, 5.0); - G.addEdge(2, 4, 10.0); - G.addEdge(5, 0, 15.0); - - // Store original neighbors and weights - std::unordered_map> originalNeighbors; - std::unordered_map> originalInNeighbors; - std::unordered_map> originalWeights; - std::unordered_map> originalInWeights; - std::unordered_map> originalEdgeIds; - std::unordered_map> originalInEdgeIds; - - G.forNodes([&](const node currentNode) { - originalNeighbors[currentNode] = std::vector(G.neighborRange(currentNode).begin(), - G.neighborRange(currentNode).end()); - for (const auto &[neighbor, weight] : G.weightNeighborRange(currentNode)) { - originalWeights[currentNode].push_back(weight); - } - for (size_t i = 0; i < G.degreeOut(currentNode); ++i) { - originalEdgeIds[currentNode].push_back(G.getIthNeighborWithId(currentNode, i).second); - } - - originalInNeighbors[currentNode] = std::vector(G.inNeighborRange(currentNode).begin(), - G.inNeighborRange(currentNode).end()); - - for (const auto &[neighbor, weight] : G.weightInNeighborRange(currentNode)) { - originalInWeights[currentNode].push_back(weight); - } - for (size_t i = 0; i < G.degreeIn(currentNode); ++i) { - originalInEdgeIds[currentNode].push_back(G.getIthInNeighbor(currentNode, i)); - } - }); - - // Sort neighbors - G.forNodes([&](const node currentNode) { - G.sortNeighbors(currentNode, [&](const node neighbor1, const node neighbor2) { - return neighbor1 < neighbor2; - }); - }); - - // Validate sorting for outgoing neighbors - G.forNodes([&](const node currentNode) { - const auto &sortedNeighbors = G.neighborRange(currentNode); - std::vector sortedNeighborVector(sortedNeighbors.begin(), sortedNeighbors.end()); - EXPECT_TRUE(std::ranges::is_sorted(sortedNeighborVector)); - - if (!std::ranges::is_sorted(originalNeighbors[currentNode])) { - EXPECT_NE(originalNeighbors[currentNode], sortedNeighborVector); - } - - for (size_t i{}; i < sortedNeighborVector.size(); ++i) { - node neighbor = sortedNeighborVector[i]; - auto it = std::ranges::find(originalNeighbors[currentNode], neighbor); - EXPECT_NE(it, originalNeighbors[currentNode].end()); - size_t originalIndex = std::distance(originalNeighbors[currentNode].begin(), it); - EXPECT_DOUBLE_EQ(G.getIthNeighborWeight(currentNode, i), - originalWeights[currentNode][originalIndex]); - } - for (size_t i = 0; i < sortedNeighborVector.size(); ++i) { - node neighbor = sortedNeighborVector[i]; - auto it = std::ranges::find(originalNeighbors[currentNode], neighbor); - EXPECT_NE(it, originalNeighbors[currentNode].end()); - size_t originalIndex = std::distance(originalNeighbors[currentNode].begin(), it); - EXPECT_EQ(G.getIthNeighborWithId(currentNode, i).second, - originalEdgeIds[currentNode][originalIndex]); - } - }); - - // Validate sorting for incoming neighbors - G.forNodes([&](const node currentNode) { - const auto &sortedInNeighbors = G.inNeighborRange(currentNode); - std::vector sortedInNeighborVector(sortedInNeighbors.begin(), - sortedInNeighbors.end()); - EXPECT_TRUE(std::ranges::is_sorted(sortedInNeighborVector)); - - if (!std::ranges::is_sorted(originalInNeighbors[currentNode])) { - EXPECT_NE(originalInNeighbors[currentNode], sortedInNeighborVector); - } - - for (size_t i = 0; i < sortedInNeighborVector.size(); ++i) { - node neighbor = sortedInNeighborVector[i]; - auto originalIterator = std::ranges::find(originalInNeighbors[currentNode], neighbor); - EXPECT_NE(originalIterator, originalInNeighbors[currentNode].end()); - size_t originalIndex = - std::distance(originalInNeighbors[currentNode].begin(), originalIterator); - - // Extract weight directly from weightInNeighborRange - auto weightIterator = G.weightInNeighborRange(currentNode).begin(); - std::advance(weightIterator, i); - EXPECT_DOUBLE_EQ((*weightIterator).second, - originalInWeights[currentNode][originalIndex]); - } - for (size_t i = 0; i < sortedInNeighborVector.size(); ++i) { - node neighbor = sortedInNeighborVector[i]; - auto originalIterator = std::ranges::find(originalInNeighbors[currentNode], neighbor); - EXPECT_NE(originalIterator, originalInNeighbors[currentNode].end()); - size_t originalIndex = - std::distance(originalInNeighbors[currentNode].begin(), originalIterator); - EXPECT_EQ(G.getIthInNeighbor(currentNode, i), - originalInEdgeIds[currentNode][originalIndex]); - } - }); -} +/* + * ---------------------------------------------------------------------------- + * Conversion in progress. Remaining GraphW-derived tests are not yet translated. + * + * Tests intentionally DROPPED (a read-only view cannot exercise them): + * - graph mutation: testRestoreNode, testIsIsolated, testAddEdge, testRemoveEdge, + * testRemoveAllEdges, testRemoveSelfLoops, testRemoveMultiEdges, testSetWeight, + * increaseWeight, testSelfLoopConversion, testCheckConsistency_MultiEdgeDetection + * - edge ids / indexed graphs: testEdgeIndexGeneration*, testEdgeIndexResolver, + * testFor*EdgesWithIds, testParallelFor*EdgesWithIds, testSortEdges, testEdgeIds* + * - neighbor reordering: all testSortNeighbors* + * ---------------------------------------------------------------------------- + */ } /* namespace NetworKit */ From fbae54bf8225ef9025a69670b62bb0b6821451ce Mon Sep 17 00:00:00 2001 From: Ian Date: Sun, 28 Jun 2026 02:17:43 -0500 Subject: [PATCH 06/11] inducedsubgraphview nodeiterators tests --- .../networkit/graph/InducedSubgraphView.hpp | 206 +++++++++++- include/networkit/graph/NodeIterators.hpp | 4 +- networkit/cpp/graph/InducedSubgraphView.cpp | 42 +-- .../graph/test/InducedSubgraphViewGTest.cpp | 293 ++++++++++++++++++ 4 files changed, 521 insertions(+), 24 deletions(-) diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index f2d283cde..eee7fe559 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -29,7 +29,7 @@ namespace NetworKit { * Neighbors are computed on-the-fly. */ class InducedSubgraphView : public Graph { - const Graph &originalGraph; + std::reference_wrapper originalGraph; std::set nodeSubset; // all nodes guaranteed to be in the original graph // store the induced degrees of all nodes in originalGraph for fast access @@ -78,12 +78,16 @@ class InducedSubgraphView : public Graph { /** * Copy constructor */ - InducedSubgraphView(const InducedSubgraphView &other) = default; + InducedSubgraphView(const InducedSubgraphView &other) + : Graph(other, true), originalGraph(other.originalGraph), nodeSubset(other.nodeSubset), + inducedDegree(other.inducedDegree), inducedInDegree(other.inducedInDegree) {} /** * Move constructor */ - InducedSubgraphView(InducedSubgraphView &&other) noexcept = default; + InducedSubgraphView(const InducedSubgraphView &&other) noexcept + : Graph(other, true), originalGraph(other.originalGraph), nodeSubset(other.nodeSubset), + inducedDegree(other.inducedDegree), inducedInDegree(other.inducedInDegree) {} /** * Copy assignment @@ -114,6 +118,83 @@ class InducedSubgraphView : public Graph { */ const Graph &getOriginalGraph() const noexcept { return originalGraph; }; + // For support of API: NetworKit::Graph::NodeIterator + using NodeIterator = NodeIteratorBase; + // For support of API: NetworKit::Graph::NodeRange + using NodeRange = NodeRangeBase; + + /** + * Get an iterable range over the nodes of the graph. + * + * @return Iterator range over the nodes of the graph. + */ + NodeRange nodeRange() const noexcept { return NodeRange(*this); } + + /** + * Iterate over all nodes of the graph and call @a handle (lambda + * closure). + * + * @param handle Takes parameter (node). + */ + template + void forNodes(L handle) const; + + /** + * Iterate randomly over all nodes of the graph and call @a handle (lambda + * closure). + * + * @param handle Takes parameter (node). + */ + template + void parallelForNodes(L handle) const; + + /** Iterate over all nodes of the graph and call @a handle (lambda + * closure) as long as @a condition remains true. This allows for breaking + * from a node loop. + * + * @param condition Returning false breaks the loop. + * @param handle Takes parameter (node). + */ + template + void forNodesWhile(C condition, L handle) const; + + /** + * Iterate randomly over all nodes of the graph and call @a handle (lambda + * closure). + * + * @param handle Takes parameter (node). + */ + template + void forNodesInRandomOrder(L handle) const; + + /** + * Iterate in parallel over all nodes of the graph and call handler + * (lambda closure). Using schedule(guided) to remedy load-imbalances due + * to e.g. unequal degree distribution. + * + * @param handle Takes parameter (node). + */ + template + void balancedParallelForNodes(L handle) const; + + /** + * Iterate over all undirected pairs of nodes and call @a handle (lambda + * closure). + * + * @param handle Takes parameters (node, node). + */ + template + void forNodePairs(L handle) const; + + /** + * Iterate over all undirected pairs of nodes in parallel and call @a + * handle (lambda closure). + * + * @param handle Takes parameters (node, node). + */ + template + void parallelForNodePairs(L handle) const; + // Override from Graph base count degree(node v) const override; count degreeIn(node v) const override; @@ -121,6 +202,8 @@ class InducedSubgraphView : public Graph { edgeweight weightedDegree(node u, bool countSelfLoopsTwice = false) const; edgeweight weightedDegreeIn(node u, bool countSelfLoopsTwice = false) const; + index upperNodeIdBound() const noexcept; + bool hasNode(node v) const noexcept; bool hasEdge(node u, node v) const noexcept; edgeweight weight(node u, node v) const override; @@ -185,6 +268,123 @@ class InducedSubgraphView : public Graph { bool countSelfLoopsTwice = false) const; }; +/** + * Class to iterate over the nodes of a graph. + * Specialized for the InducedSubgraphView. + */ +template <> +class NodeIteratorBase { + const InducedSubgraphView *G; + std::set::iterator iter; + +public: + // The value type of the nodes (i.e. nodes). Returned by + // operator*(). + using value_type = node; + + // Reference to the value_type, required by STL. + using reference = value_type &; + + // Pointer to the value_type, required by STL. + using pointer = value_type *; + + // STL iterator category. + using iterator_category = std::forward_iterator_tag; + + // Signed integer type of the result of subtracting two pointers, + // required by STL. + using difference_type = ptrdiff_t; + + // Own type. + using self = NodeIteratorBase; + + NodeIteratorBase(const InducedSubgraphView *G, node u) + : G(G), iter(G->getNodeSubset().lower_bound(u)) {} + + /** + * @brief WARNING: This constructor is required for Python and should not be used as the + * iterator is not initialized. + */ + NodeIteratorBase() : G(nullptr) {} + + ~NodeIteratorBase() = default; + + NodeIteratorBase &operator++() { + assert(iter != G->getNodeSubset().end()); + ++iter; + return *this; + } + + NodeIteratorBase operator++(int) { + const auto tmp = *this; + ++(*this); + return tmp; + } + + NodeIteratorBase &operator--() { + assert(iter != G->getNodeSubset().begin()); + --iter; + return *this; + } + + NodeIteratorBase operator--(int) { + const auto tmp = *this; + --(*this); + return tmp; + } + + bool operator==(const NodeIteratorBase &rhs) const noexcept { return iter == rhs.iter; } + + bool operator!=(const NodeIteratorBase &rhs) const noexcept { return !(*this == rhs); } + + node operator*() const noexcept { + assert(iter != G->getNodeSubset().end()); + return *iter; + } +}; + +template +void InducedSubgraphView::forNodes(L handle) const { + for (auto it = nodeSubset.begin(); it != nodeSubset.end(); ++it) + handle(*it); +} + +template +void InducedSubgraphView::forNodesWhile(C condition, L handle) const { + for (auto it = nodeSubset.begin(); it != nodeSubset.end() && !condition(); ++it) + handle(*it); +} + +template +void InducedSubgraphView::forNodesInRandomOrder(L handle) const { + std::vector randVec(nodeSubset.begin(), nodeSubset.end()); + std::ranges::shuffle(randVec, Aux::Random::getURNG()); + for (node v : randVec) + handle(v); +} + +template +void InducedSubgraphView::forNodePairs(L handle) const { + for (auto it1 = nodeSubset.begin(); it1 != nodeSubset.end(); ++it1) + for (auto it2 = std::next(it1); it2 != nodeSubset.end(); ++it2) + handle(*it1, *it2); +} + +template +void InducedSubgraphView::parallelForNodes(L handle) const { + throw std::runtime_error("parallelForNodes not implemented for InducedSubgraphView"); +} + +template +void InducedSubgraphView::balancedParallelForNodes(L handle) const { + throw std::runtime_error("balancedParallelForNodes not implemented for InducedSubgraphView"); +} + +template +void InducedSubgraphView::parallelForNodePairs(L handle) const { + throw std::runtime_error("parallelForNodePairs not implemented for InducedSubgraphView"); +} + } // namespace NetworKit #endif // NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ diff --git a/include/networkit/graph/NodeIterators.hpp b/include/networkit/graph/NodeIterators.hpp index b50dffb7d..292785367 100644 --- a/include/networkit/graph/NodeIterators.hpp +++ b/include/networkit/graph/NodeIterators.hpp @@ -72,7 +72,7 @@ class NodeIteratorBase { return tmp; } - NodeIteratorBase operator--() { + NodeIteratorBase &operator--() { assert(u); do { --u; @@ -107,7 +107,7 @@ class NodeRangeBase { public: NodeRangeBase(const GraphType &G) : G(&G) {} - NodeRangeBase() : G(nullptr){}; + NodeRangeBase() : G(nullptr) {}; ~NodeRangeBase() = default; diff --git a/networkit/cpp/graph/InducedSubgraphView.cpp b/networkit/cpp/graph/InducedSubgraphView.cpp index f0a9753e3..963f0c6e9 100644 --- a/networkit/cpp/graph/InducedSubgraphView.cpp +++ b/networkit/cpp/graph/InducedSubgraphView.cpp @@ -24,7 +24,7 @@ InducedSubgraphView::InducedSubgraphView(const Graph &originalGraph, const std:: std::set InducedSubgraphView::boundary() { std::set result; for (node u : nodeSubset) { - originalGraph.forNeighborsOf(u, [&](node v) { + originalGraph.get().forNeighborsOf(u, [&](node v) { if (!hasNode(v)) result.insert(v); }); @@ -52,6 +52,10 @@ edgeweight InducedSubgraphView ::weightedDegreeIn(node u, bool countSelfLoopsTwi return computeWeightedDegree(u, true, countSelfLoopsTwice); } +index InducedSubgraphView::upperNodeIdBound() const noexcept { + return originalGraph.get().upperNodeIdBound(); +} + bool InducedSubgraphView::isIsolated(node v) const { assert(hasNode(v)); return degree(v) == 0 && (!directed || degreeIn(v) == 0); @@ -62,25 +66,25 @@ bool InducedSubgraphView::hasNode(node v) const noexcept { } bool InducedSubgraphView::hasEdge(node u, node v) const noexcept { - return hasNode(u) && hasNode(v) && originalGraph.hasEdge(v, v); + return hasNode(u) && hasNode(v) && originalGraph.get().hasEdge(u, v); } edgeweight InducedSubgraphView::weight(node u, node v) const { if (!hasEdge(u, v)) return 0; - return originalGraph.weight(u, v); + return originalGraph.get().weight(u, v); } void InducedSubgraphView::addNodes(const std::set &subset) { for (node v : subset) { - if (hasNode(v) || !originalGraph.hasNode(v)) + if (hasNode(v) || !originalGraph.get().hasNode(v)) continue; nodeSubset.insert(v); n += 1; count degree = 0; - originalGraph.forNeighborsOf(v, [&](node u, edgeweight ew) { + originalGraph.get().forNeighborsOf(v, [&](node u, edgeweight ew) { if (!hasNode(u)) return; if (u == v) @@ -94,7 +98,7 @@ void InducedSubgraphView::addNodes(const std::set &subset) { if (isDirected()) { count inDegree = 0; - originalGraph.forInNeighborsOf(v, [&](node u, edgeweight ew) { + originalGraph.get().forInNeighborsOf(v, [&](node u, edgeweight ew) { if (!hasNode(u) || u == v) return; ++inducedDegree[u]; @@ -116,7 +120,7 @@ void InducedSubgraphView::removeNodes(const std::set &subset) { if (isDirected()) m -= inducedInDegree[v]; - originalGraph.forNeighborsOf(v, [&](node u, edgeweight ew) { + originalGraph.get().forNeighborsOf(v, [&](node u, edgeweight ew) { if (!hasNode(u)) return; if (u == v) @@ -125,7 +129,7 @@ void InducedSubgraphView::removeNodes(const std::set &subset) { }); if (isDirected()) { - originalGraph.forInNeighborsOf(v, [&](node u, edgeweight ew) { + originalGraph.get().forInNeighborsOf(v, [&](node u, edgeweight ew) { if (!hasNode(u) || u == v) return; --inducedDegree[u]; @@ -153,10 +157,10 @@ std::vector InducedSubgraphView::getNeighborsVector(node u, bool inEdges) if (inEdges) { result.reserve(degreeIn(u)); - originalGraph.forInNeighborsOf(u, cb); + originalGraph.get().forInNeighborsOf(u, cb); } else { result.reserve(degree(u)); - originalGraph.forNeighborsOf(u, cb); + originalGraph.get().forNeighborsOf(u, cb); } return result; @@ -177,11 +181,11 @@ InducedSubgraphView::getNeighborsWithWeightsVector(node u, bool inEdges) const { if (inEdges) { nodeVec.reserve(degreeIn(u)); weightVec.reserve(degreeIn(u)); - originalGraph.forInNeighborsOf(u, cb); + originalGraph.get().forInNeighborsOf(u, cb); } else { nodeVec.reserve(degree(u)); weightVec.reserve(degree(u)); - originalGraph.forNeighborsOf(u, cb); + originalGraph.get().forNeighborsOf(u, cb); } return {nodeVec, weightVec}; @@ -197,7 +201,7 @@ void InducedSubgraphView::forEdgesVirtualImpl( void InducedSubgraphView::forEdgesOfVirtualImpl( node u, bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { - originalGraph.forEdgesOf(u, [&](node u, node v, edgeweight w, edgeid x) { + originalGraph.get().forEdgesOf(u, [&](node u, node v, edgeweight w, edgeid x) { if (hasEdge(u, v)) handle(u, v, w, x); }); @@ -206,7 +210,7 @@ void InducedSubgraphView::forEdgesOfVirtualImpl( void InducedSubgraphView::forInEdgesVirtualImpl( node u, bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { - originalGraph.forInEdgesOf(u, [&](node u, node v, edgeweight w, edgeid x) { + originalGraph.get().forInEdgesOf(u, [&](node u, node v, edgeweight w, edgeid x) { if (hasEdge(u, v)) handle(u, v, w, x); }); @@ -215,7 +219,7 @@ void InducedSubgraphView::forInEdgesVirtualImpl( double InducedSubgraphView::parallelSumForEdgesVirtualImpl( bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { - return originalGraph.parallelSumForEdges([&](node u, node v, edgeweight w, edgeid x) { + return originalGraph.get().parallelSumForEdges([&](node u, node v, edgeweight w, edgeid x) { return hasEdge(u, v) ? handle(u, v, w, x) : 0; }); } @@ -264,9 +268,9 @@ edgeweight InducedSubgraphView::computeWeightedDegree(node u, bool inDegree, sum += (countSelfLoopsTwice && u == v) ? 2. * w : w; }; if (inDegree) { - originalGraph.forInNeighborsOf(u, sumWeights); + originalGraph.get().forInNeighborsOf(u, sumWeights); } else { - originalGraph.forNeighborsOf(u, sumWeights); + originalGraph.get().forNeighborsOf(u, sumWeights); } return sum; } @@ -276,9 +280,9 @@ edgeweight InducedSubgraphView::computeWeightedDegree(node u, bool inDegree, if (countSelfLoopsTwice && numberOfSelfLoops()) { if (inDegree) { - originalGraph.forInNeighborsOf(u, countSelfLoops); + originalGraph.get().forInNeighborsOf(u, countSelfLoops); } else { - originalGraph.forNeighborsOf(u, countSelfLoops); + originalGraph.get().forNeighborsOf(u, countSelfLoops); } } diff --git a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp index c11fd1e96..1d90368e5 100644 --- a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp +++ b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp @@ -6,8 +6,10 @@ */ #include +#include #include #include +#include #include "networkit/auxiliary/Random.hpp" #include @@ -380,6 +382,297 @@ TEST_P(InducedSubgraphViewGTest, testWeightedDegree3) { } } } + +/** EDGE MODIFIERS **/ + +TEST_P(InducedSubgraphViewGTest, testHasEdge) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u = 0; u < Ghouse.upperNodeIdBound(); ++u) + for (node v = 0; v < Ghouse.upperNodeIdBound(); ++v) + EXPECT_EQ(expected.hasEdge(u, v), Gv.hasEdge(u, v)); + } +} + +/** GLOBAL PROPERTIES **/ + +TEST_P(InducedSubgraphViewGTest, testSelfLoopCountSimple) { + this->Ghouse.addEdge(0, 0); + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + EXPECT_EQ(expected.numberOfSelfLoops(), Gv.numberOfSelfLoops()); + } +} + +TEST_P(InducedSubgraphViewGTest, testIsWeighted) { + InducedSubgraphView G(Ghouse, allNodes(Ghouse)); + ASSERT_EQ(isWeighted(), G.isWeighted()); +} + +TEST_P(InducedSubgraphViewGTest, testIsDirected) { + InducedSubgraphView G(Ghouse, allNodes(Ghouse)); + ASSERT_EQ(isDirected(), G.isDirected()); +} + +TEST_P(InducedSubgraphViewGTest, testIsEmpty) { + GraphW H = createGraphW(2); + InducedSubgraphView G1(H, std::set{}); + InducedSubgraphView G2(H, someNodes(H, 2)); + + ASSERT_TRUE(G1.isEmpty()); + ASSERT_FALSE(G2.isEmpty()); + + G1.addNode(0); + G2.removeNodes({0}); + ASSERT_FALSE(G1.isEmpty()); + ASSERT_FALSE(G2.isEmpty()); + + G1.removeNodes({0}); + G2.removeNodes({1}); + ASSERT_TRUE(G1.isEmpty()); + ASSERT_TRUE(G2.isEmpty()); +} + +TEST_P(InducedSubgraphViewGTest, testNumberOfNodes) { + GraphW H = createGraphW(2); + InducedSubgraphView G1(H, std::set{}); + ASSERT_EQ(0u, G1.numberOfNodes()); + G1.addNode(0); + ASSERT_EQ(1u, G1.numberOfNodes()); + G1.addNode(1); + ASSERT_EQ(2u, G1.numberOfNodes()); + G1.removeNodes({0}); + ASSERT_EQ(1u, G1.numberOfNodes()); + G1.removeNodes({1}); + ASSERT_EQ(0u, G1.numberOfNodes()); +} + +TEST_P(InducedSubgraphViewGTest, testNumberOfEdges) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + EXPECT_EQ(expected.numberOfEdges(), Gv.numberOfEdges()); + } +} + +TEST_P(InducedSubgraphViewGTest, testNumberOfSelfLoops) { + this->Ghouse.addEdge(0, 0); + this->Ghouse.addEdge(1, 1); + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + EXPECT_EQ(expected.numberOfSelfLoops(), Gv.numberOfSelfLoops()); + } +} + +TEST_P(InducedSubgraphViewGTest, testUpperNodeIdBound) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + EXPECT_EQ(Ghouse.upperNodeIdBound(), Gv.upperNodeIdBound()); + } +} + +/** EDGE ATTRIBUTES **/ + +TEST_P(InducedSubgraphViewGTest, testWeight) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u = 0; u < Ghouse.upperNodeIdBound(); ++u) + for (node v = 0; v < Ghouse.upperNodeIdBound(); ++v) + EXPECT_EQ(expected.weight(u, v), Gv.weight(u, v)); + } +} + +/** SUMS **/ + +TEST_P(InducedSubgraphViewGTest, testTotalEdgeWeight) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + EXPECT_DOUBLE_EQ(expected.totalEdgeWeight(), Gv.totalEdgeWeight()); + } +} + +/** Collections **/ + +TEST_P(InducedSubgraphViewGTest, testNodeIterator) { + Aux::Random::setSeed(42, false); + + auto testForward = [](const InducedSubgraphView &G) { + auto preIter = G.nodeRange().begin(); + auto postIter = G.nodeRange().begin(); + + G.forNodes([&](const node u) { + ASSERT_EQ(*preIter, u); + ASSERT_EQ(*postIter, u); + ++preIter; + postIter++; + }); + + ASSERT_EQ(preIter, G.nodeRange().end()); + ASSERT_EQ(postIter, G.nodeRange().end()); + + InducedSubgraphView G1(G); + for (const auto u : InducedSubgraphView::NodeRange(G)) { + ASSERT_TRUE(G1.hasNode(u)); + G1.removeNodes({u}); + } + ASSERT_EQ(G1.numberOfNodes(), 0u); + }; + + auto testBackward = [](const InducedSubgraphView &G) { + const std::vector nodes(InducedSubgraphView::NodeRange(G).begin(), + InducedSubgraphView::NodeRange(G).end()); + std::vector v; + G.forNodes([&](node u) { v.push_back(u); }); + + ASSERT_EQ(std::unordered_set(nodes.begin(), nodes.end()).size(), nodes.size()); + ASSERT_EQ(nodes.size(), G.numberOfNodes()); + + auto preIter = G.nodeRange().begin(); + auto postIter = G.nodeRange().begin(); + for (count i = 0; i < G.numberOfNodes(); ++i) { + ++preIter; + postIter++; + } + + ASSERT_EQ(preIter, G.nodeRange().end()); + ASSERT_EQ(postIter, G.nodeRange().end()); + auto vecIter = nodes.rbegin(); + while (vecIter != nodes.rend()) { + ASSERT_EQ(*vecIter, *(--preIter)); + if (postIter != G.nodeRange().end()) { + ASSERT_NE(*vecIter, *(postIter--)); + } else { + postIter--; + } + ASSERT_EQ(*vecIter, *postIter); + ++vecIter; + } + + ASSERT_EQ(preIter, G.nodeRange().begin()); + ASSERT_EQ(postIter, G.nodeRange().begin()); + }; + + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView G(Ghouse, variant.nodes); + testForward(G); + testBackward(G); + } +} + +/** NODE ITERATORS **/ + +TEST_P(InducedSubgraphViewGTest, testForNodes) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView G(Ghouse, variant.nodes); + std::set visited; + G.forNodes([&](node v) { ASSERT_TRUE(visited.insert(v).second); }); + ASSERT_EQ(variant.nodes, visited); + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelForNodes) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView G(Ghouse, variant.nodes); + std::vector visited(G.upperNodeIdBound(), none); + G.parallelForNodes([&](node u) { visited[u] = u; }); + + std::set seen; + for (node u = 0; u < G.upperNodeIdBound(); ++u) + if (visited[u] != none) + seen.insert(visited[u]); + ASSERT_EQ(variant.nodes, seen); + } +} + +TEST_P(InducedSubgraphViewGTest, forNodesWhile) { + GraphW H = createGraphW(100); + InducedSubgraphView G(H, allNodes(H)); + count stopAfter = 10; + count nodesSeen = 0; + + G.forNodesWhile([&]() { return nodesSeen < stopAfter; }, [&](node) { nodesSeen++; }); + + ASSERT_EQ(stopAfter, nodesSeen); +} + +TEST_P(InducedSubgraphViewGTest, testForNodesInRandomOrder) { + count n = 1000; + count samples = 100; + double maxAbsoluteError = 0.005; + GraphW H = createGraphW(n); + InducedSubgraphView G(H, allNodes(H)); + + node lastNode = n / 2; + count greaterLastNode = 0; + count smallerLastNode = 0; + std::vector visitCount(n, 0); + + for (count i = 0; i < samples; i++) { + G.forNodesInRandomOrder([&](node v) { + if (v > lastNode) { + greaterLastNode++; + } else { + smallerLastNode++; + } + visitCount[v]++; + lastNode = v; + }); + } + + for (node v = 0; v < n; v++) { + ASSERT_EQ(samples, visitCount[v]); + } + + ASSERT_NEAR(0.5, (double)greaterLastNode / n / samples, maxAbsoluteError); + ASSERT_NEAR(0.5, (double)smallerLastNode / n / samples, maxAbsoluteError); +} + +TEST_P(InducedSubgraphViewGTest, testForNodePairs) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView G(Ghouse, variant.nodes); + const count k = variant.nodes.size(); + + count pairs = 0; + std::set> seen; + G.forNodePairs([&](node u, node v) { + ++pairs; + ASSERT_NE(u, v); + ASSERT_TRUE(variant.nodes.count(u) && variant.nodes.count(v)); + node a = std::min(u, v), b = std::max(u, v); + ASSERT_TRUE(seen.insert({a, b}).second); + }); + EXPECT_EQ(k * (k - 1) / 2, pairs); + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelSumForNodes) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView G(Ghouse, variant.nodes); + double sum = G.parallelSumForNodes([](node v) { return 2 * v + 0.5; }); + + double expected = 0; + for (node v : variant.nodes) + expected += 2 * v + 0.5; + ASSERT_DOUBLE_EQ(expected, sum); + } +} /* * ---------------------------------------------------------------------------- * Conversion in progress. Remaining GraphW-derived tests are not yet translated. From 076f36d04683dcf1cbfa4e3a62ff0322459e27d3 Mon Sep 17 00:00:00 2001 From: Ian Date: Sun, 28 Jun 2026 10:47:04 -0500 Subject: [PATCH 07/11] inducedsubgraphview nodeiterators --- include/networkit/graph/Graph.hpp | 20 ++++++---- include/networkit/graph/GraphW.hpp | 6 +-- .../networkit/graph/InducedSubgraphView.hpp | 39 ++++++++++++++----- networkit/cpp/graph/Graph.cpp | 2 +- networkit/cpp/graph/GraphW.cpp | 2 +- networkit/cpp/graph/InducedSubgraphView.cpp | 22 +++++------ 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/include/networkit/graph/Graph.hpp b/include/networkit/graph/Graph.hpp index c34dd99d7..0427bb9c3 100644 --- a/include/networkit/graph/Graph.hpp +++ b/include/networkit/graph/Graph.hpp @@ -470,8 +470,8 @@ class Graph { * @brief Virtual method for forInEdgesOf - overridden by GraphW for vector-based iteration */ virtual void - forInEdgesVirtualImpl(node u, bool directed, bool weighted, bool hasEdgeIds, - std::function handle) const; + forInEdgesOfVirtualImpl(node u, bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const; /** * @brief Virtual method for parallelSumForEdges - overridden by GraphW for vector-based @@ -933,7 +933,7 @@ class Graph { * @return @c true if @a v exists, @c false otherwise. */ - bool hasNode(node v) const noexcept { return (v < z) && this->exists[v]; } + virtual bool hasNode(node v) const noexcept { return (v < z) && this->exists[v]; } /** * Check if edge (u, v) exists in the graph. @@ -942,7 +942,7 @@ class Graph { * @param v Second endpoint of edge. * @return @c true if edge exists, @c false otherwise. */ - bool hasEdge(node u, node v) const; + virtual bool hasEdge(node u, node v) const; /** * @brief Virtual method for hasEdge - overridden by GraphW for vector-based graphs @@ -1007,7 +1007,7 @@ class Graph { * * @return Weighted degree of @a u. */ - edgeweight weightedDegree(node u, bool countSelfLoopsTwice = false) const; + virtual edgeweight weightedDegree(node u, bool countSelfLoopsTwice = false) const; /** * Returns the weighted in-degree of @a u. @@ -1017,7 +1017,7 @@ class Graph { * * @return Weighted in-degree of @a v. */ - edgeweight weightedDegreeIn(node u, bool countSelfLoopsTwice = false) const; + virtual edgeweight weightedDegreeIn(node u, bool countSelfLoopsTwice = false) const; /** * Returns true if this graph supports edge weights other @@ -1732,12 +1732,12 @@ inline void Graph::forInEdgesOfImpl(node u, L handle) const { } else { // For vector-based graphs (GraphW), use the virtual dispatch // Check exists for mutable graphs - if (!exists[u]) + if (!hasNode(u)) return; // GraphW's forInEdgesVirtualImpl calls handle(v, u, ...) where v is neighbor and u is // current node We need to swap so that edgeLambda receives (current, neighbor, ...) and // passes neighbor to f - forInEdgesVirtualImpl( + forInEdgesOfVirtualImpl( u, graphIsDirected, hasWeights, graphHasEdgeIds, [&](node v, node u, edgeweight w, edgeid e) { edgeLambda(handle, u, v, w, e); }); } @@ -1936,6 +1936,10 @@ void Graph::forEdgesOf(node u, L handle) const { } } } else { + // For vector-based graphs (GraphW), use the virtual dispatch + // Check exists for mutable graphs + if (!hasNode(u)) + return; // For vector-based graphs, use virtual dispatch forEdgesOfVirtualImpl(u, directed, weighted, edgesIndexed, [&](node uu, node vv, edgeweight ww, edgeid ee) { diff --git a/include/networkit/graph/GraphW.hpp b/include/networkit/graph/GraphW.hpp index f0f514377..dabe044b3 100644 --- a/include/networkit/graph/GraphW.hpp +++ b/include/networkit/graph/GraphW.hpp @@ -958,7 +958,7 @@ class GraphW final : public Graph { /** * @brief Override for vector-based forInEdgesOf */ - void forInEdgesVirtualImpl( + void forInEdgesOfVirtualImpl( node u, bool directed, bool weighted, bool hasEdgeIds, std::function handle) const override; @@ -999,7 +999,7 @@ class GraphW final : public Graph { public: NeighborRange(const GraphW &G, node u) : G(&G), u(u) { assert(G.hasNode(u)); }; - NeighborRange() : G(nullptr){}; + NeighborRange() : G(nullptr) {}; NeighborIterator begin() const { assert(G); @@ -1037,7 +1037,7 @@ class GraphW final : public Graph { public: NeighborWeightRange(const GraphW &G, node u) : G(&G), u(u) { assert(G.hasNode(u)); }; - NeighborWeightRange() : G(nullptr){}; + NeighborWeightRange() : G(nullptr) {}; NeighborWeightIterator begin() const { assert(G); diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index eee7fe559..682e5bb51 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -195,17 +195,24 @@ class InducedSubgraphView : public Graph { template void parallelForNodePairs(L handle) const; + /** + * Iterate in parallel over all nodes and sum (reduce +) the values + * returned by the handler + */ + template + double parallelSumForNodes(L handle) const; + // Override from Graph base count degree(node v) const override; count degreeIn(node v) const override; bool isIsolated(node v) const override; - edgeweight weightedDegree(node u, bool countSelfLoopsTwice = false) const; - edgeweight weightedDegreeIn(node u, bool countSelfLoopsTwice = false) const; + edgeweight weightedDegree(node u, bool countSelfLoopsTwice = false) const override; + edgeweight weightedDegreeIn(node u, bool countSelfLoopsTwice = false) const override; index upperNodeIdBound() const noexcept; - bool hasNode(node v) const noexcept; - bool hasEdge(node u, node v) const noexcept; + bool hasNode(node v) const noexcept override; + bool hasEdge(node u, node v) const noexcept override; edgeweight weight(node u, node v) const override; // FIXME: NOT PLANNED TO IMPLEMENT @@ -244,7 +251,7 @@ class InducedSubgraphView : public Graph { /** * @brief Virtual method for forInEdgesOf */ - void forInEdgesVirtualImpl( + void forInEdgesOfVirtualImpl( node u, bool directed, bool weighted, bool hasEdgeIds, std::function handle) const override; @@ -351,7 +358,7 @@ void InducedSubgraphView::forNodes(L handle) const { template void InducedSubgraphView::forNodesWhile(C condition, L handle) const { - for (auto it = nodeSubset.begin(); it != nodeSubset.end() && !condition(); ++it) + for (auto it = nodeSubset.begin(); it != nodeSubset.end() && condition(); ++it) handle(*it); } @@ -372,17 +379,31 @@ void InducedSubgraphView::forNodePairs(L handle) const { template void InducedSubgraphView::parallelForNodes(L handle) const { - throw std::runtime_error("parallelForNodes not implemented for InducedSubgraphView"); + WARN("parallelForNodes not implemented for InducedSubgraphView... fallback to sequential"); + return forNodes(handle); } template void InducedSubgraphView::balancedParallelForNodes(L handle) const { - throw std::runtime_error("balancedParallelForNodes not implemented for InducedSubgraphView"); + WARN("balancedParallelForNodes not implemented for InducedSubgraphView... fallback to " + "sequential"); + return forNodes(handle); } template void InducedSubgraphView::parallelForNodePairs(L handle) const { - throw std::runtime_error("parallelForNodePairs not implemented for InducedSubgraphView"); + WARN("parallelForNodePairs not implemented for InducedSubgraphView... fallback to " + "sequential"); + return forNodePairs(handle); +} + +template +double InducedSubgraphView::parallelSumForNodes(L handle) const { + WARN("parallelSumForNodes not implemented for InducedSubgraphView... fallback to " + "sequential"); + double sum = 0.0; + forNodes([&](node u) { sum += handle(u); }); + return sum; } } // namespace NetworKit diff --git a/networkit/cpp/graph/Graph.cpp b/networkit/cpp/graph/Graph.cpp index 33346b48d..785d42653 100644 --- a/networkit/cpp/graph/Graph.cpp +++ b/networkit/cpp/graph/Graph.cpp @@ -416,7 +416,7 @@ void Graph::forEdgesOfVirtualImpl( } } -void Graph::forInEdgesVirtualImpl( +void Graph::forInEdgesOfVirtualImpl( node u, [[maybe_unused]] bool directed, [[maybe_unused]] bool weighted, [[maybe_unused]] bool hasEdgeIds, std::function handle) const { diff --git a/networkit/cpp/graph/GraphW.cpp b/networkit/cpp/graph/GraphW.cpp index 3d7395156..98d3559b8 100644 --- a/networkit/cpp/graph/GraphW.cpp +++ b/networkit/cpp/graph/GraphW.cpp @@ -888,7 +888,7 @@ void GraphW::forEdgesOfVirtualImpl( } } -void GraphW::forInEdgesVirtualImpl( +void GraphW::forInEdgesOfVirtualImpl( node u, [[maybe_unused]] bool directed, [[maybe_unused]] bool weighted, [[maybe_unused]] bool hasEdgeIds, std::function handle) const { diff --git a/networkit/cpp/graph/InducedSubgraphView.cpp b/networkit/cpp/graph/InducedSubgraphView.cpp index 963f0c6e9..b98bdded5 100644 --- a/networkit/cpp/graph/InducedSubgraphView.cpp +++ b/networkit/cpp/graph/InducedSubgraphView.cpp @@ -195,24 +195,24 @@ void InducedSubgraphView::forEdgesVirtualImpl( bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { for (node u : nodeSubset) - forEdgesOf(u, handle); + forEdgesOfVirtualImpl(u, directed, weighted, hasEdgeIds, handle); } void InducedSubgraphView::forEdgesOfVirtualImpl( node u, bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { originalGraph.get().forEdgesOf(u, [&](node u, node v, edgeweight w, edgeid x) { - if (hasEdge(u, v)) + if (hasNode(v)) handle(u, v, w, x); }); } -void InducedSubgraphView::forInEdgesVirtualImpl( +void InducedSubgraphView::forInEdgesOfVirtualImpl( node u, bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { originalGraph.get().forInEdgesOf(u, [&](node u, node v, edgeweight w, edgeid x) { - if (hasEdge(u, v)) - handle(u, v, w, x); + if (hasNode(v)) + handle(v, u, w, x); }); } @@ -260,17 +260,15 @@ index InducedSubgraphView::indexInOutEdgeArray(node u, node v) const { edgeweight InducedSubgraphView::computeWeightedDegree(node u, bool inDegree, bool countSelfLoopsTwice) const { - // TODO: clean this up using forNeighbors in the subgraph, when that works if (weighted) { edgeweight sum = 0.0; auto sumWeights = [&](node v, edgeweight w) { - if (hasNode(v)) - sum += (countSelfLoopsTwice && u == v) ? 2. * w : w; + sum += (countSelfLoopsTwice && u == v) ? 2. * w : w; }; if (inDegree) { - originalGraph.get().forInNeighborsOf(u, sumWeights); + forInNeighborsOf(u, sumWeights); } else { - originalGraph.get().forNeighborsOf(u, sumWeights); + forNeighborsOf(u, sumWeights); } return sum; } @@ -280,9 +278,9 @@ edgeweight InducedSubgraphView::computeWeightedDegree(node u, bool inDegree, if (countSelfLoopsTwice && numberOfSelfLoops()) { if (inDegree) { - originalGraph.get().forInNeighborsOf(u, countSelfLoops); + forInNeighborsOf(u, countSelfLoops); } else { - originalGraph.get().forNeighborsOf(u, countSelfLoops); + forNeighborsOf(u, countSelfLoops); } } From 9e1ab68befd5f752221fcde4726a3a8d6b9a87cf Mon Sep 17 00:00:00 2001 From: Ian Date: Sun, 28 Jun 2026 11:34:50 -0500 Subject: [PATCH 08/11] inducedsubgraph test for all but edge iterators --- include/networkit/graph/Graph.hpp | 8 +- .../networkit/graph/InducedSubgraphView.hpp | 15 + networkit/cpp/graph/InducedSubgraphView.cpp | 11 +- .../graph/test/InducedSubgraphViewGTest.cpp | 377 ++++++++++++++++++ 4 files changed, 404 insertions(+), 7 deletions(-) diff --git a/include/networkit/graph/Graph.hpp b/include/networkit/graph/Graph.hpp index 0427bb9c3..6defe10fc 100644 --- a/include/networkit/graph/Graph.hpp +++ b/include/networkit/graph/Graph.hpp @@ -1198,7 +1198,7 @@ class Graph { * @return Iterator range over the neighbors of @a u. */ NeighborRange neighborRange(node u) const { - assert(exists[u]); + assert(hasNode(u)); return NeighborRange(*this, u); } @@ -1212,7 +1212,7 @@ class Graph { */ NeighborWeightRange weightNeighborRange(node u) const { assert(isWeighted()); - assert(exists[u]); + assert(hasNode(u)); return NeighborWeightRange(*this, u); } @@ -1224,7 +1224,7 @@ class Graph { */ NeighborRange inNeighborRange(node u) const { assert(isDirected()); - assert(exists[u]); + assert(hasNode(u)); return NeighborRange(*this, u); } @@ -1238,7 +1238,7 @@ class Graph { */ NeighborWeightRange weightInNeighborRange(node u) const { assert(isDirected() && isWeighted()); - assert(exists[u]); + assert(hasNode(u)); return NeighborWeightRange(*this, u); } diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index 682e5bb51..65484aaa3 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -123,6 +123,21 @@ class InducedSubgraphView : public Graph { // For support of API: NetworKit::Graph::NodeRange using NodeRange = NodeRangeBase; + // For support of API: NetworKit::Graph:EdgeIterator + using EdgeIterator = EdgeTypeIterator; + // For support of API: NetworKit::Graph:EdgeWeightIterator + using EdgeWeightIterator = EdgeTypeIterator; + // For support of API: NetworKit::Graph:EdgeRange + using EdgeRange = EdgeTypeRange; + // For support of API: NetworKit::Graph:EdgeWeightRange + using EdgeWeightRange = EdgeTypeRange; + + // For support of API: NetworKit::Graph::NeighborIterator; + using NeighborIterator = NeighborIteratorBase>; + // For support of API: NetworKit::Graph::NeighborIterator; + using NeighborWeightIterator = + NeighborWeightIteratorBase, std::vector>; + /** * Get an iterable range over the nodes of the graph. * diff --git a/networkit/cpp/graph/InducedSubgraphView.cpp b/networkit/cpp/graph/InducedSubgraphView.cpp index b98bdded5..81f844840 100644 --- a/networkit/cpp/graph/InducedSubgraphView.cpp +++ b/networkit/cpp/graph/InducedSubgraphView.cpp @@ -174,7 +174,10 @@ InducedSubgraphView::getNeighborsWithWeightsVector(node u, bool inEdges) const { auto cb = [&](node v) { if (hasNode(v)) { nodeVec.push_back(v); - weightVec.push_back(weight(u, v)); + if (inEdges) + weightVec.push_back(weight(v, u)); + else + weightVec.push_back(weight(u, v)); } }; @@ -194,8 +197,10 @@ InducedSubgraphView::getNeighborsWithWeightsVector(node u, bool inEdges) const { void InducedSubgraphView::forEdgesVirtualImpl( bool directed, bool weighted, bool hasEdgeIds, std::function handle) const { - for (node u : nodeSubset) - forEdgesOfVirtualImpl(u, directed, weighted, hasEdgeIds, handle); + originalGraph.get().forEdges([&](node u, node v, edgeweight w, edgeid x) { + if (hasEdge(u, v)) + handle(u, v, w, x); + }); } void InducedSubgraphView::forEdgesOfVirtualImpl( diff --git a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp index 1d90368e5..6d93626d7 100644 --- a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp +++ b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp @@ -572,6 +572,154 @@ TEST_P(InducedSubgraphViewGTest, testNodeIterator) { } } +TEST_P(InducedSubgraphViewGTest, testEdgeIterator) { + auto testForward = [&](const InducedSubgraphView &G) { + auto preIter = G.edgeRange().begin(); + auto postIter = G.edgeRange().begin(); + + G.forEdges([&](node, node) { + ASSERT_EQ(preIter, postIter); + const auto edge = *preIter; + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + ++preIter; + postIter++; + }); + + ASSERT_EQ(preIter, G.edgeRange().end()); + ASSERT_EQ(postIter, G.edgeRange().end()); + + count edges = 0; + for (const auto edge : InducedSubgraphView::EdgeRange(G)) { + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + ++edges; + } + ASSERT_EQ(edges, G.numberOfEdges()); + }; + + auto testForwardWeighted = [&](const InducedSubgraphView &G) { + auto preIter = G.edgeWeightRange().begin(); + auto postIter = preIter; + + G.forEdges([&](node, node) { + ASSERT_EQ(preIter, postIter); + const auto edge = *preIter; + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + ASSERT_DOUBLE_EQ(G.weight(edge.u, edge.v), edge.weight); + ++preIter; + postIter++; + }); + + ASSERT_EQ(preIter, G.edgeWeightRange().end()); + ASSERT_EQ(postIter, G.edgeWeightRange().end()); + + count edges = 0; + for (const auto edge : InducedSubgraphView::EdgeWeightRange(G)) { + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + ASSERT_DOUBLE_EQ(G.weight(edge.u, edge.v), edge.weight); + ++edges; + } + ASSERT_EQ(edges, G.numberOfEdges()); + }; + + auto testBackward = [&](const InducedSubgraphView &G) { + auto preIter = G.edgeRange().begin(); + auto postIter = preIter; + G.forEdges([&](node, node) { + ++preIter; + postIter++; + }); + + ASSERT_EQ(preIter, G.edgeRange().end()); + ASSERT_EQ(postIter, G.edgeRange().end()); + + G.forEdges([&](node, node) { + --preIter; + postIter--; + ASSERT_EQ(preIter, postIter); + const auto edge = *preIter; + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + }); + + ASSERT_EQ(preIter, G.edgeRange().begin()); + ASSERT_EQ(postIter, G.edgeRange().begin()); + }; + + auto testBackwardWeighted = [&](const InducedSubgraphView &G) { + auto preIter = G.edgeWeightRange().begin(); + auto postIter = preIter; + G.forEdges([&](node, node) { + ++preIter; + postIter++; + }); + + G.forEdges([&](node, node) { + --preIter; + postIter--; + ASSERT_EQ(preIter, postIter); + const auto edge = *preIter; + ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); + ASSERT_DOUBLE_EQ(G.weight(edge.u, edge.v), edge.weight); + }); + }; + + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView G(Ghouse, variant.nodes); + testForward(G); + testBackward(G); + testForwardWeighted(G); + testBackwardWeighted(G); + } +} + +TEST_P(InducedSubgraphViewGTest, testNeighborsIterators) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView G(Ghouse, variant.nodes); + for (node u : variant.nodes) { + auto range = G.neighborRange(u); + auto iter = range.begin(); + G.forNeighborsOf(u, [&](node v) { + ASSERT_TRUE(*iter == v); + ++iter; + }); + ASSERT_TRUE(iter == range.end()); + + if (G.isWeighted()) { + auto range = G.weightNeighborRange(u); + auto iterW = range.begin(); + G.forNeighborsOf(u, [&](node v, edgeweight w) { + ASSERT_TRUE((*iterW).first == v); + ASSERT_TRUE((*iterW).second == w); + ++iterW; + }); + ASSERT_TRUE(iterW == range.end()); + } + + if (G.isDirected()) { + auto range = G.inNeighborRange(u); + auto inIter = range.begin(); + G.forInNeighborsOf(u, [&](node v) { + ASSERT_TRUE(*inIter == v); + ++inIter; + }); + ASSERT_TRUE(inIter == range.end()); + + if (G.isWeighted()) { + auto range = G.weightInNeighborRange(u); + auto iterWin = range.begin(); + G.forInNeighborsOf(u, [&](node v, edgeweight w) { + ASSERT_TRUE((*iterWin).first == v); + ASSERT_TRUE((*iterWin).second == w); + ++iterWin; + }); + ASSERT_TRUE(iterWin == range.end()); + } + } + } + } +} + /** NODE ITERATORS **/ TEST_P(InducedSubgraphViewGTest, testForNodes) { @@ -673,6 +821,235 @@ TEST_P(InducedSubgraphViewGTest, testParallelSumForNodes) { ASSERT_DOUBLE_EQ(expected, sum); } } + +/** EDGE ITERATORS **/ + +TEST_P(InducedSubgraphViewGTest, testForEdges) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + auto norm = [&](node u, node v) { + return Gv.isDirected() ? std::make_pair(u, v) + : std::make_pair(std::min(u, v), std::max(u, v)); + }; + + std::multiset> seen, ref; + Gv.forEdges([&](node u, node v) { + ASSERT_TRUE(Gv.hasEdge(u, v)); + seen.insert(norm(u, v)); + }); + expected.forEdges([&](node u, node v) { ref.insert(norm(u, v)); }); + EXPECT_EQ(ref, seen); + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedEdges) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + auto norm = [&](node u, node v) { + return Gv.isDirected() ? std::make_pair(u, v) + : std::make_pair(std::min(u, v), std::max(u, v)); + }; + + std::multiset> seen, ref; + edgeweight viewSum = 0, refSum = 0; + Gv.forEdges([&](node u, node v, edgeweight ew) { + ASSERT_TRUE(Gv.hasEdge(u, v)); + ASSERT_EQ(Gv.weight(u, v), ew); + seen.insert(norm(u, v)); + viewSum += ew; + }); + expected.forEdges([&](node u, node v, edgeweight ew) { + ref.insert(norm(u, v)); + refSum += ew; + }); + EXPECT_EQ(ref, seen); + EXPECT_DOUBLE_EQ(refSum, viewSum); + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelForWeightedEdges) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + + double weightSum = 0.0; + Gv.parallelForEdges([&](node, node, edgeweight ew) { +#pragma omp atomic + weightSum += ew; + }); + EXPECT_DOUBLE_EQ(expected.totalEdgeWeight(), weightSum); + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelForEdges) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + + count edges = 0; + Gv.parallelForEdges([&](node, node) { +#pragma omp atomic + ++edges; + }); + EXPECT_EQ(expected.numberOfEdges(), edges); + } +} + +/** NEIGHBORHOOD ITERATORS **/ + +TEST_P(InducedSubgraphViewGTest, testForNeighborsOf) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) { + std::multiset seen, ref; + Gv.forNeighborsOf(u, [&](node v) { seen.insert(v); }); + expected.forNeighborsOf(u, [&](node v) { ref.insert(v); }); + EXPECT_EQ(ref, seen); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedNeighborsOf) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) { + std::multiset> seen, ref; + Gv.forNeighborsOf(u, [&](node v, edgeweight w) { seen.insert({v, w}); }); + expected.forNeighborsOf(u, [&](node v, edgeweight w) { ref.insert({v, w}); }); + EXPECT_EQ(ref, seen); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testForEdgesOf) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) { + std::multiset seen, ref; + Gv.forEdgesOf(u, [&](node v, node w) { + EXPECT_EQ(u, v); + seen.insert(w); + }); + expected.forEdgesOf(u, [&](node v, node w) { + EXPECT_EQ(u, v); + ref.insert(w); + }); + EXPECT_EQ(ref, seen); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedEdgesOf) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) { + std::multiset> seen, ref; + Gv.forEdgesOf(u, [&](node v, node w, edgeweight ew) { + EXPECT_EQ(u, v); + seen.insert({w, ew}); + }); + expected.forEdgesOf(u, [&](node v, node w, edgeweight ew) { + EXPECT_EQ(u, v); + ref.insert({w, ew}); + }); + EXPECT_EQ(ref, seen); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testParallelSumForWeightedEdges) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + + double sum = Gv.parallelSumForEdges([](node, node, edgeweight ew) { return 1.5 * ew; }); + EXPECT_DOUBLE_EQ(1.5 * expected.totalEdgeWeight(), sum); + } +} + +TEST_P(InducedSubgraphViewGTest, testForInNeighborsOf) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) { + std::multiset seen, ref; + Gv.forInNeighborsOf(u, [&](node v) { seen.insert(v); }); + expected.forInNeighborsOf(u, [&](node v) { ref.insert(v); }); + EXPECT_EQ(ref, seen); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedInNeighborsOf) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) { + std::multiset> seen, ref; + Gv.forInNeighborsOf(u, [&](node v, edgeweight w) { seen.insert({v, w}); }); + expected.forInNeighborsOf(u, [&](node v, edgeweight w) { ref.insert({v, w}); }); + EXPECT_EQ(ref, seen); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testForInEdgesOf) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) { + std::multiset seen, ref; + Gv.forInEdgesOf(u, [&](node x, node v) { + EXPECT_EQ(u, x); + seen.insert(v); + }); + expected.forInEdgesOf(u, [&](node x, node v) { + EXPECT_EQ(u, x); + ref.insert(v); + }); + EXPECT_EQ(ref, seen); + } + } +} + +TEST_P(InducedSubgraphViewGTest, testForWeightedInEdgesOf) { + this->Ghouse.addEdge(3, 3, 2.5); + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + for (node u : variant.nodes) { + std::multiset> seen, ref; + Gv.forInEdgesOf(u, [&](node x, node v, edgeweight ew) { + EXPECT_EQ(u, x); + seen.insert({v, ew}); + }); + expected.forInEdgesOf(u, [&](node x, node v, edgeweight ew) { + EXPECT_EQ(u, x); + ref.insert({v, ew}); + }); + EXPECT_EQ(ref, seen); + } + } +} /* * ---------------------------------------------------------------------------- * Conversion in progress. Remaining GraphW-derived tests are not yet translated. From a1598a16fbd167853b4b1a7b1a2435a68d0b2991 Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 29 Jun 2026 14:09:26 -0500 Subject: [PATCH 09/11] edge iterators --- include/networkit/graph/EdgeIterators.hpp | 2 +- .../networkit/graph/InducedSubgraphView.hpp | 175 +++++++++++++++++- include/networkit/graph/NeighborIterators.hpp | 5 +- .../graph/test/InducedSubgraphViewGTest.cpp | 4 +- 4 files changed, 175 insertions(+), 11 deletions(-) diff --git a/include/networkit/graph/EdgeIterators.hpp b/include/networkit/graph/EdgeIterators.hpp index 2340b58f8..1e469f114 100644 --- a/include/networkit/graph/EdgeIterators.hpp +++ b/include/networkit/graph/EdgeIterators.hpp @@ -151,7 +151,7 @@ class EdgeTypeRange { public: EdgeTypeRange(const GraphType &G) : G(&G) {} - EdgeTypeRange() : G(nullptr){}; + EdgeTypeRange() : G(nullptr) {}; ~EdgeTypeRange() = default; diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index 65484aaa3..1e52c5e43 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -10,6 +10,10 @@ #include #include +#include "networkit/graph/EdgeIterators.hpp" +#include "networkit/graph/Graph.hpp" +#include "networkit/graph/NeighborIterators.hpp" +#include "networkit/graph/NodeIterators.hpp" #include @@ -38,6 +42,9 @@ class InducedSubgraphView : public Graph { std::map inducedInDegree; protected: + friend class EdgeTypeIterator; + friend class EdgeTypeIterator; + // count n; // count m; // count storedNumberOfSelfLoops; @@ -124,13 +131,13 @@ class InducedSubgraphView : public Graph { using NodeRange = NodeRangeBase; // For support of API: NetworKit::Graph:EdgeIterator - using EdgeIterator = EdgeTypeIterator; - // For support of API: NetworKit::Graph:EdgeWeightIterator - using EdgeWeightIterator = EdgeTypeIterator; - // For support of API: NetworKit::Graph:EdgeRange - using EdgeRange = EdgeTypeRange; - // For support of API: NetworKit::Graph:EdgeWeightRange - using EdgeWeightRange = EdgeTypeRange; + using EdgeIterator = EdgeTypeIterator; + // For support of API: NetworKit::InducedSubgraphView:EdgeWeightIterator + using EdgeWeightIterator = EdgeTypeIterator; + // For support of API: NetworKit::InducedSubgraphView:EdgeRange + using EdgeRange = EdgeTypeRange; + // For support of API: NetworKit::InducedSubgraphView:EdgeWeightRange + using EdgeWeightRange = EdgeTypeRange; // For support of API: NetworKit::Graph::NeighborIterator; using NeighborIterator = NeighborIteratorBase>; @@ -145,6 +152,20 @@ class InducedSubgraphView : public Graph { */ NodeRange nodeRange() const noexcept { return NodeRange(*this); } + /** + * Get an iterable range over the edges of the graph. + * + * @return Iterator range over the edges of the graph. + */ + EdgeRange edgeRange() const noexcept { return EdgeRange(*this); } + + /** + * Get an iterable range over the edges of the graph and their weights. + * + * @return Iterator range over the edges of the graph and their weights. + */ + EdgeWeightRange edgeWeightRange() const noexcept { return EdgeWeightRange(*this); } + /** * Iterate over all nodes of the graph and call @a handle (lambda * closure). @@ -365,6 +386,146 @@ class NodeIteratorBase { } }; +template +class EdgeTypeIterator { + using NeighborType = + std::conditional_t, InducedSubgraphView::NeighborIterator, + InducedSubgraphView::NeighborWeightIterator>; + using NeighborRangeType = + std::conditional_t, InducedSubgraphView::OutNeighborRange, + InducedSubgraphView::OutNeighborWeightRange>; + +protected: + const InducedSubgraphView *G; + NodeIteratorBase nodeIter; + NeighborType nbrIter; + NeighborRangeType nbrRange; + index i{none}; + + NeighborRangeType makeRange(node u) const { + if constexpr (std::is_same_v) + return Graph::NeighborRange(*G, u); + else + return Graph::NeighborWeightRange(*G, u); + } + +public: + using GraphType = InducedSubgraphView; + + // The value type of the edges (i.e. a pair). Returned by operator*(). + using value_type = EdgeType; + + // Reference to the value_type, required by STL. + using reference = value_type &; + + // Pointer to the value_type, required by STL. + using pointer = value_type *; + + // STL iterator category. + using iterator_category = std::forward_iterator_tag; + + // Signed integer type of the result of subtracting two pointers, + // required by STL. + using difference_type = ptrdiff_t; + + // Own type. + using self = EdgeTypeIterator; + + EdgeTypeIterator(const GraphType *G, NodeIteratorBase nodeIter) + : G(G), nodeIter(nodeIter), i(index{0}) { + if (nodeIter != G->nodeRange().end()) { + nbrRange = makeRange(*nodeIter); + nbrIter = nbrRange.begin(); + if (!G->degree(*nodeIter)) + nextEdge(); + } + } + + EdgeTypeIterator() {}; + + bool operator==(const EdgeTypeIterator &rhs) const noexcept { + return nodeIter == rhs.nodeIter && i == rhs.i; + } + + bool operator!=(const EdgeTypeIterator &rhs) const noexcept { return !(*this == rhs); } + + // The returned edge depends on both the type of edge and graph + EdgeType operator*() const noexcept { + assert(nodeIter != G->nodeRange().end()); + if constexpr (std::is_same_v) + return Edge(*nodeIter, *nbrIter); + else + return WeightedEdge(*nodeIter, (*nbrIter).first, (*nbrIter).second); + } + + EdgeTypeIterator &operator++() { + nextEdge(); + return *this; + } + + EdgeTypeIterator operator++(int) { + const auto tmp = *this; + ++(*this); + return tmp; + } + + EdgeTypeIterator operator--() { + prevEdge(); + return *this; + } + + EdgeTypeIterator operator--(int) { + const auto tmp = *this; + --(*this); + return tmp; + } + + bool validEdge() const noexcept { + if constexpr (std::is_same_v) + return G->isDirected() || *nodeIter <= *nbrIter; + else + return G->isDirected() || *nodeIter <= (*nbrIter).first; + } + +private: + void nextEdge() { + do { + if (++i >= G->degree(*nodeIter)) { + i = 0; + do { + assert(nodeIter != G->nodeRange().end()); + ++nodeIter; + if (nodeIter == G->nodeRange().end()) + return; + nbrRange = makeRange(*nodeIter); + nbrIter = nbrRange.begin(); + } while (!G->degree(*nodeIter)); + } else { + ++nbrIter; + } + } while (!validEdge()); + } + + void prevEdge() { + do { + if (!i) { + do { + assert(nodeIter != G->nodeRange().begin()); + --nodeIter; + nbrRange = makeRange(*nodeIter); + nbrIter = nbrRange.end(); + } while (!G->degree(*nodeIter)); + + i = G->degree(*nodeIter); + nbrRange = makeRange(*nodeIter); + nbrIter = nbrRange.end(); + } + --i; + --nbrIter; + } while (!validEdge()); + } +}; + template void InducedSubgraphView::forNodes(L handle) const { for (auto it = nodeSubset.begin(); it != nodeSubset.end(); ++it) diff --git a/include/networkit/graph/NeighborIterators.hpp b/include/networkit/graph/NeighborIterators.hpp index 6fa045454..98622f569 100644 --- a/include/networkit/graph/NeighborIterators.hpp +++ b/include/networkit/graph/NeighborIterators.hpp @@ -7,6 +7,7 @@ #ifndef NETWORKIT_GRAPH_NEIGHBOR_ITERATORS_HPP_ #define NETWORKIT_GRAPH_NEIGHBOR_ITERATORS_HPP_ +#include namespace NetworKit { /** @@ -29,7 +30,7 @@ class NeighborIteratorBase { using pointer = value_type *; // STL iterator category. - using iterator_category = std::forward_iterator_tag; + using iterator_category = std::bidirectional_iterator_tag; // Signed integer type of the result of subtracting two pointers, // required by STL. @@ -103,7 +104,7 @@ class NeighborWeightIteratorBase { using pointer = value_type *; // STL iterator category. - using iterator_category = std::forward_iterator_tag; + using iterator_category = std::bidirectional_iterator_tag; // Signed integer type of the result of subtracting two pointers, // required by STL. diff --git a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp index 6d93626d7..e25831603 100644 --- a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp +++ b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp @@ -577,18 +577,20 @@ TEST_P(InducedSubgraphViewGTest, testEdgeIterator) { auto preIter = G.edgeRange().begin(); auto postIter = G.edgeRange().begin(); + count edges = 0; G.forEdges([&](node, node) { ASSERT_EQ(preIter, postIter); const auto edge = *preIter; ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); ++preIter; postIter++; + ++edges; }); ASSERT_EQ(preIter, G.edgeRange().end()); ASSERT_EQ(postIter, G.edgeRange().end()); - count edges = 0; + edges = 0; for (const auto edge : InducedSubgraphView::EdgeRange(G)) { ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); ++edges; From 829499de78e5f1cac00b75a635f0c4da05e74be3 Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 29 Jun 2026 16:31:35 -0500 Subject: [PATCH 10/11] inducedsubgraphview finish tests --- .../networkit/graph/InducedSubgraphView.hpp | 4 +- networkit/cpp/graph/InducedSubgraphView.cpp | 4 +- .../graph/test/InducedSubgraphViewGTest.cpp | 112 +++++++++++++----- 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index 1e52c5e43..bc149926c 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -72,9 +72,9 @@ class InducedSubgraphView : public Graph { void removeNodes(const std::set &subset); /** - * Find all nodes that have source inside the subgraph view, and outNeighbor outside. + * Find all nodes in (V - S) that have source inside the subgraph view, and outNeighbor outside. */ - std::set boundary(); + std::set frontier(); /** * Construct an explicit mutable subgraph equivalent to this view. diff --git a/networkit/cpp/graph/InducedSubgraphView.cpp b/networkit/cpp/graph/InducedSubgraphView.cpp index 81f844840..ad657373a 100644 --- a/networkit/cpp/graph/InducedSubgraphView.cpp +++ b/networkit/cpp/graph/InducedSubgraphView.cpp @@ -10,8 +10,6 @@ #include // TODO: check if attributes work transparently, or if special handling needs to be done -// TODO: document that EdgeIterators probably won't work (getIthNeighbor not impl-ed) -// but because we only need ++ and --, we can probably still implement this. namespace NetworKit { @@ -21,7 +19,7 @@ InducedSubgraphView::InducedSubgraphView(const Graph &originalGraph, const std:: addNodes(subset); } -std::set InducedSubgraphView::boundary() { +std::set InducedSubgraphView::frontier() { std::set result; for (node u : nodeSubset) { originalGraph.get().forNeighborsOf(u, [&](node v) { diff --git a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp index e25831603..f8db09417 100644 --- a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp +++ b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp @@ -55,8 +55,7 @@ class InducedSubgraphViewGTest : public testing::TestWithParam subsetVariants(const Graph &G) const; GraphW inducedSubgraph(const Graph &G, const std::set &subset) const; - - count countSelfLoopsManually(const InducedSubgraphView &G); + void expectGraphsEqual(const Graph &expected, const Graph &actual) const; }; INSTANTIATE_TEST_SUITE_P(InstantiationName, InducedSubgraphViewGTest, @@ -129,14 +128,28 @@ GraphW InducedSubgraphViewGTest::inducedSubgraph(const Graph &G, return GraphTools::subgraphFromNodes(G, subset.begin(), subset.end()); } -count InducedSubgraphViewGTest::countSelfLoopsManually(const InducedSubgraphView &G) { - count c = 0; - G.getOriginalGraph().forEdges([&](node u, node v) { - if (u == v && G.getNodeSubset().contains(u)) { - c += 1; - } - }); - return c; +void InducedSubgraphViewGTest::expectGraphsEqual(const Graph &expected, const Graph &actual) const { + EXPECT_EQ(expected.isWeighted(), actual.isWeighted()); + EXPECT_EQ(expected.isDirected(), actual.isDirected()); + EXPECT_EQ(expected.numberOfNodes(), actual.numberOfNodes()); + EXPECT_EQ(expected.numberOfEdges(), actual.numberOfEdges()); + EXPECT_EQ(expected.upperNodeIdBound(), actual.upperNodeIdBound()); + + std::set expectedNodes, actualNodes; + expected.forNodes([&](node u) { expectedNodes.insert(u); }); + actual.forNodes([&](node u) { actualNodes.insert(u); }); + EXPECT_EQ(expectedNodes, actualNodes); + + auto edges = [](const Graph &g) { + std::multiset> s; + g.forEdges([&](node u, node v, edgeweight w) { + node a = g.isDirected() ? u : std::min(u, v); + node b = g.isDirected() ? v : std::max(u, v); + s.insert({a, b, w}); + }); + return s; + }; + EXPECT_EQ(edges(expected), edges(actual)); } void InducedSubgraphViewGTest::SetUp() { @@ -179,9 +192,67 @@ void InducedSubgraphViewGTest::SetUp() { } } -/** CONSTRUCTORS **/ +/** VIEW OPERATIONS **/ + +TEST_P(InducedSubgraphViewGTest, testFrontier) { + { + InducedSubgraphView G(Ghouse, {0, 2}); + if (isDirected()) { + EXPECT_EQ((std::set{1, 4}), G.frontier()); + } else { + EXPECT_EQ((std::set{1, 3, 4}), G.frontier()); + } + } + { + InducedSubgraphView G(Ghouse, {0, 1, 2}); + if (isDirected()) { + EXPECT_EQ((std::set{4}), G.frontier()); + } else { + EXPECT_EQ((std::set{3, 4}), G.frontier()); + } + } + { + InducedSubgraphView G(Ghouse, {0, 1, 2, 3, 4}); + EXPECT_EQ(std::set{}, G.frontier()); + } +} + +TEST_P(InducedSubgraphViewGTest, testRealize) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); -// TODO + GraphW expectedKeep = inducedSubgraph(Ghouse, variant.nodes); + expectGraphsEqual(expectedKeep, Gv.realize()); + expectGraphsEqual(expectedKeep, Gv.realize(false)); + + GraphW expectedCompact = + GraphTools::subgraphFromNodes(Ghouse, variant.nodes.begin(), variant.nodes.end(), true); + GraphW compact = Gv.realize(true); + expectGraphsEqual(expectedCompact, compact); + EXPECT_EQ(variant.nodes.size(), compact.numberOfNodes()); + EXPECT_EQ(variant.nodes.size(), compact.upperNodeIdBound()); + } +} + +TEST_P(InducedSubgraphViewGTest, testCopyAndMove) { + InducedSubgraphView original(Ghouse, someNodes(Ghouse, 3)); + + InducedSubgraphView copy(original); + EXPECT_EQ(original.getNodeSubset(), copy.getNodeSubset()); + expectGraphsEqual(original.realize(), copy.realize()); + + const count originalCount = original.numberOfNodes(); + copy.removeNodes({*copy.getNodeSubset().begin()}); + EXPECT_EQ(originalCount, original.numberOfNodes()); + EXPECT_NE(original.getNodeSubset(), copy.getNodeSubset()); + + InducedSubgraphView source(Ghouse, someNodes(Ghouse, 4)); + std::set expectedSubset = source.getNodeSubset(); + InducedSubgraphView moved(std::move(source)); + EXPECT_EQ(expectedSubset, moved.getNodeSubset()); + expectGraphsEqual(inducedSubgraph(Ghouse, expectedSubset), moved.realize()); +} /** NODE MODIFIERS **/ @@ -577,20 +648,18 @@ TEST_P(InducedSubgraphViewGTest, testEdgeIterator) { auto preIter = G.edgeRange().begin(); auto postIter = G.edgeRange().begin(); - count edges = 0; G.forEdges([&](node, node) { ASSERT_EQ(preIter, postIter); const auto edge = *preIter; ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); ++preIter; postIter++; - ++edges; }); ASSERT_EQ(preIter, G.edgeRange().end()); ASSERT_EQ(postIter, G.edgeRange().end()); - edges = 0; + count edges = 0; for (const auto edge : InducedSubgraphView::EdgeRange(G)) { ASSERT_TRUE(G.hasEdge(edge.u, edge.v)); ++edges; @@ -1052,18 +1121,5 @@ TEST_P(InducedSubgraphViewGTest, testForWeightedInEdgesOf) { } } } -/* - * ---------------------------------------------------------------------------- - * Conversion in progress. Remaining GraphW-derived tests are not yet translated. - * - * Tests intentionally DROPPED (a read-only view cannot exercise them): - * - graph mutation: testRestoreNode, testIsIsolated, testAddEdge, testRemoveEdge, - * testRemoveAllEdges, testRemoveSelfLoops, testRemoveMultiEdges, testSetWeight, - * increaseWeight, testSelfLoopConversion, testCheckConsistency_MultiEdgeDetection - * - edge ids / indexed graphs: testEdgeIndexGeneration*, testEdgeIndexResolver, - * testFor*EdgesWithIds, testParallelFor*EdgesWithIds, testSortEdges, testEdgeIds* - * - neighbor reordering: all testSortNeighbors* - * ---------------------------------------------------------------------------- - */ } /* namespace NetworKit */ From 01d9ec12cc95b6157bb39e04ef6915e311ca0ff0 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 30 Jun 2026 12:55:11 -0500 Subject: [PATCH 11/11] correctly use virtual funcs --- include/networkit/graph/Graph.hpp | 18 +++++++++--------- .../networkit/graph/InducedSubgraphView.hpp | 1 + networkit/cpp/graph/InducedSubgraphView.cpp | 3 +++ networkit/cpp/graph/test/CMakeLists.txt | 2 +- .../graph/test/InducedSubgraphViewGTest.cpp | 19 +++++++++++++++++++ 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/include/networkit/graph/Graph.hpp b/include/networkit/graph/Graph.hpp index 6defe10fc..cad85ad09 100644 --- a/include/networkit/graph/Graph.hpp +++ b/include/networkit/graph/Graph.hpp @@ -1473,7 +1473,7 @@ class Graph { template void Graph::forNodes(L handle) const { for (node v = 0; v < z; ++v) { - if (exists[v]) { + if (hasNode(v)) { handle(v); } } @@ -1491,7 +1491,7 @@ void Graph::parallelForNodes(L handle) const { // For mutable graphs, check exists #pragma omp parallel for for (omp_index v = 0; v < static_cast(z); ++v) { - if (exists[v]) { + if (hasNode(v)) { handle(v); } } @@ -1501,7 +1501,7 @@ void Graph::parallelForNodes(L handle) const { template void Graph::forNodesWhile(C condition, L handle) const { for (node v = 0; v < z; ++v) { - if (exists[v]) { + if (hasNode(v)) { if (!condition()) { break; } @@ -1534,7 +1534,7 @@ void Graph::balancedParallelForNodes(L handle) const { // For mutable graphs, check exists #pragma omp parallel for schedule(guided) for (omp_index v = 0; v < static_cast(z); ++v) { - if (exists[v]) { + if (hasNode(v)) { handle(v); } } @@ -1544,9 +1544,9 @@ void Graph::balancedParallelForNodes(L handle) const { template void Graph::forNodePairs(L handle) const { for (node u = 0; u < z; ++u) { - if (exists[u]) { + if (hasNode(u)) { for (node v = u + 1; v < z; ++v) { - if (exists[v]) { + if (hasNode(v)) { handle(u, v); } } @@ -1568,9 +1568,9 @@ void Graph::parallelForNodePairs(L handle) const { // For mutable graphs, check exists #pragma omp parallel for schedule(guided) for (omp_index u = 0; u < static_cast(z); ++u) { - if (exists[u]) { + if (hasNode(u)) { for (node v = u + 1; v < z; ++v) { - if (exists[v]) { + if (hasNode(v)) { handle(u, v); } } @@ -1678,7 +1678,7 @@ inline void Graph::forOutEdgesOfImpl(node u, L handle) const { } else { // Vector-based graphs should use GraphW // Check exists for mutable graphs - if (!exists[u]) + if (!hasNode(u)) return; throw std::runtime_error("forOutEdgesOfImpl not supported for vector-based graphs in base " "Graph class - use GraphW"); diff --git a/include/networkit/graph/InducedSubgraphView.hpp b/include/networkit/graph/InducedSubgraphView.hpp index bc149926c..071590c50 100644 --- a/include/networkit/graph/InducedSubgraphView.hpp +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -50,6 +50,7 @@ class InducedSubgraphView : public Graph { // count storedNumberOfSelfLoops; // bool weighted; // bool directed; + // node z; public: /** diff --git a/networkit/cpp/graph/InducedSubgraphView.cpp b/networkit/cpp/graph/InducedSubgraphView.cpp index ad657373a..98c435026 100644 --- a/networkit/cpp/graph/InducedSubgraphView.cpp +++ b/networkit/cpp/graph/InducedSubgraphView.cpp @@ -16,12 +16,15 @@ namespace NetworKit { InducedSubgraphView::InducedSubgraphView(const Graph &originalGraph, const std::set &subset) : Graph(0, originalGraph.isWeighted(), originalGraph.isDirected()), originalGraph(originalGraph) { + z = originalGraph.upperNodeIdBound(); addNodes(subset); } std::set InducedSubgraphView::frontier() { std::set result; for (node u : nodeSubset) { + if (degree(u) == originalGraph.get().degree(u)) + continue; originalGraph.get().forNeighborsOf(u, [&](node v) { if (!hasNode(v)) result.insert(v); diff --git a/networkit/cpp/graph/test/CMakeLists.txt b/networkit/cpp/graph/test/CMakeLists.txt index aa96e843e..a3f723840 100644 --- a/networkit/cpp/graph/test/CMakeLists.txt +++ b/networkit/cpp/graph/test/CMakeLists.txt @@ -2,7 +2,7 @@ networkit_add_test(graph GraphBuilderAutoCompleteGTest auxiliary) networkit_add_test(graph GraphGTest auxiliary dyn_distance io generators) networkit_add_test(graph InducedSubgraphViewGTest - auxiliary dyn_distance io generators) + auxiliary dyn_distance io generators centrality) networkit_add_test(graph GraphToolsGTest generators io) networkit_add_test(graph TraversalGTest generators) networkit_add_test(graph SpanningGTest io) diff --git a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp index f8db09417..c14b3cbb0 100644 --- a/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp +++ b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -1122,4 +1123,22 @@ TEST_P(InducedSubgraphViewGTest, testForWeightedInEdgesOf) { } } +/** COMPATIBILITY WITH ALGORITHMS **/ + +TEST_P(InducedSubgraphViewGTest, testCoreDecomp) { + for (const auto &variant : subsetVariants(Ghouse)) { + SCOPED_TRACE(variant.label); + InducedSubgraphView Gv(Ghouse, variant.nodes); + GraphW expected = inducedSubgraph(Ghouse, variant.nodes); + + auto coreDecompView = CoreDecomposition(Gv); + auto coreDecomp = CoreDecomposition(expected); + coreDecompView.run(); + coreDecomp.run(); + + for (node u : variant.nodes) + EXPECT_DOUBLE_EQ(coreDecomp.score(u), coreDecompView.score(u)); + } +} + } /* namespace NetworKit */