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: 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/Graph.hpp b/include/networkit/graph/Graph.hpp index c34dd99d7..cad85ad09 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 @@ -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); } @@ -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"); @@ -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 new file mode 100644 index 000000000..071590c50 --- /dev/null +++ b/include/networkit/graph/InducedSubgraphView.hpp @@ -0,0 +1,588 @@ +/* + * InducedSubgraphView.hpp + * + * Created on: 06.25.2026 + * Author: Ian Chen (ianchen3@illinois.edu) + */ + +#ifndef NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ +#define NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ + +#include +#include +#include "networkit/graph/EdgeIterators.hpp" +#include "networkit/graph/Graph.hpp" +#include "networkit/graph/NeighborIterators.hpp" +#include "networkit/graph/NodeIterators.hpp" + +#include + +// TODO: add an option to make the IDs contiguous + +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 or removed as a batch. + * + * A mutable GraphW can be realized. + * Neighbors are computed on-the-fly. + */ +class InducedSubgraphView : public Graph { + 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 + // for nodes not in nodeSubset, may contain garbage + std::map inducedDegree; + std::map inducedInDegree; + +protected: + friend class EdgeTypeIterator; + friend class EdgeTypeIterator; + + // count n; + // count m; + // count storedNumberOfSelfLoops; + // bool weighted; + // bool directed; + // node z; + +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, const std::set &subset); + + /** + * Add nodes to the subset. + */ + void addNode(node u) { addNodes({u}); } + void addNodes(const std::set &subset); + + /** + * Remove nodes from the subset. + */ + void removeNode(node u) { removeNodes({u}); } + void removeNodes(const std::set &subset); + + /** + * Find all nodes in (V - S) that have source inside the subgraph view, and outNeighbor outside. + */ + std::set frontier(); + + /** + * Construct an explicit mutable subgraph equivalent to this view. + * @param compact; whether the subgraph should get compact node IDs + */ + GraphW realize(bool compact = false) const; + + /** + * Copy constructor + */ + InducedSubgraphView(const InducedSubgraphView &other) + : Graph(other, true), originalGraph(other.originalGraph), nodeSubset(other.nodeSubset), + inducedDegree(other.inducedDegree), inducedInDegree(other.inducedInDegree) {} + + /** + * Move constructor + */ + InducedSubgraphView(const InducedSubgraphView &&other) noexcept + : Graph(other, true), originalGraph(other.originalGraph), nodeSubset(other.nodeSubset), + inducedDegree(other.inducedDegree), inducedInDegree(other.inducedInDegree) {} + + /** + * 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; + + /** + * Access the current nodes that are in the graph. + */ + const std::set &getNodeSubset() const noexcept { return nodeSubset; }; + + /** + * Access the underlying 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; + + // For support of API: NetworKit::Graph:EdgeIterator + 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>; + // For support of API: NetworKit::Graph::NeighborIterator; + using NeighborWeightIterator = + NeighborWeightIteratorBase, std::vector>; + + /** + * 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); } + + /** + * 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). + * + * @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; + + /** + * 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 override; + edgeweight weightedDegreeIn(node u, bool countSelfLoopsTwice = false) const override; + + index upperNodeIdBound() 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 + 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; + + // 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 forInEdgesOfVirtualImpl( + 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; + + /** + * 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; +}; + +/** + * 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 +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) + 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 { + WARN("parallelForNodes not implemented for InducedSubgraphView... fallback to sequential"); + return forNodes(handle); +} + +template +void InducedSubgraphView::balancedParallelForNodes(L handle) const { + WARN("balancedParallelForNodes not implemented for InducedSubgraphView... fallback to " + "sequential"); + return forNodes(handle); +} + +template +void InducedSubgraphView::parallelForNodePairs(L handle) const { + 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 + +#endif // NETWORKIT_GRAPH_INDUCED_SUBGRAPH_VIEW_HPP_ 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/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/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/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 new file mode 100644 index 000000000..98c435026 --- /dev/null +++ b/networkit/cpp/graph/InducedSubgraphView.cpp @@ -0,0 +1,296 @@ +/* + * 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 + +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); + }); + } + return result; +} + +count InducedSubgraphView::degree(node v) const { + assert(hasNode(v)); + return inducedDegree.at(v); +} + +count InducedSubgraphView::degreeIn(node v) const { + assert(hasNode(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); +} + +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); +} + +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.get().hasEdge(u, v); +} + +edgeweight InducedSubgraphView::weight(node u, node v) const { + if (!hasEdge(u, v)) + return 0; + return originalGraph.get().weight(u, v); +} + +void InducedSubgraphView::addNodes(const std::set &subset) { + for (node v : subset) { + if (hasNode(v) || !originalGraph.get().hasNode(v)) + continue; + + nodeSubset.insert(v); + n += 1; + + count degree = 0; + originalGraph.get().forNeighborsOf(v, [&](node u, edgeweight ew) { + if (!hasNode(u)) + return; + if (u == v) + ++storedNumberOfSelfLoops; + isDirected() ? ++inducedInDegree[u] : ++inducedDegree[u]; + ++degree; + }); + + inducedDegree[v] = degree; + m += degree; + + if (isDirected()) { + count inDegree = 0; + originalGraph.get().forInNeighborsOf(v, [&](node u, edgeweight ew) { + if (!hasNode(u) || u == v) + return; + ++inducedDegree[u]; + ++inDegree; + }); + inducedInDegree[v] = inDegree; + m += inDegree; + } + } +} + +void InducedSubgraphView::removeNodes(const std::set &subset) { + for (node v : subset) { + if (!hasNode(v)) + continue; + + n -= 1; + m -= inducedDegree[v]; + if (isDirected()) + m -= inducedInDegree[v]; + + originalGraph.get().forNeighborsOf(v, [&](node u, edgeweight ew) { + if (!hasNode(u)) + return; + if (u == v) + --storedNumberOfSelfLoops; + isDirected() ? --inducedInDegree[u] : --inducedDegree[u]; + }); + + if (isDirected()) { + originalGraph.get().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(bool compact) const { + return GraphTools::subgraphFromNodes(originalGraph, nodeSubset.begin(), nodeSubset.end(), + compact); +} + +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.get().forInNeighborsOf(u, cb); + } else { + result.reserve(degree(u)); + originalGraph.get().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); + if (inEdges) + weightVec.push_back(weight(v, u)); + else + weightVec.push_back(weight(u, v)); + } + }; + + if (inEdges) { + nodeVec.reserve(degreeIn(u)); + weightVec.reserve(degreeIn(u)); + originalGraph.get().forInNeighborsOf(u, cb); + } else { + nodeVec.reserve(degree(u)); + weightVec.reserve(degree(u)); + originalGraph.get().forNeighborsOf(u, cb); + } + + return {nodeVec, weightVec}; +} + +void InducedSubgraphView::forEdgesVirtualImpl( + bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const { + originalGraph.get().forEdges([&](node u, node v, edgeweight w, edgeid x) { + if (hasEdge(u, v)) + handle(u, v, w, x); + }); +} + +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 (hasNode(v)) + handle(u, v, w, x); + }); +} + +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 (hasNode(v)) + handle(v, u, w, x); + }); +} + +double InducedSubgraphView::parallelSumForEdgesVirtualImpl( + bool directed, bool weighted, bool hasEdgeIds, + std::function handle) const { + return originalGraph.get().parallelSumForEdges([&](node u, node v, edgeweight w, edgeid 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 { + if (weighted) { + edgeweight sum = 0.0; + auto sumWeights = [&](node v, edgeweight w) { + sum += (countSelfLoopsTwice && u == v) ? 2. * w : w; + }; + if (inDegree) { + forInNeighborsOf(u, sumWeights); + } else { + forNeighborsOf(u, sumWeights); + } + return sum; + } + + count sum = inDegree ? degreeIn(u) : degreeOut(u); + auto countSelfLoops = [&](node v) { sum += (u == v); }; + + if (countSelfLoopsTwice && numberOfSelfLoops()) { + if (inDegree) { + forInNeighborsOf(u, countSelfLoops); + } else { + 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..a3f723840 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 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 new file mode 100644 index 000000000..c14b3cbb0 --- /dev/null +++ b/networkit/cpp/graph/test/InducedSubgraphViewGTest.cpp @@ -0,0 +1,1144 @@ +/* + * InducedSubgraphViewGTest.cpp + * + * Created on: 06.26.2026 + * Author: Ian Chen (ianchen3@illinois.edu) + */ + +#include +#include +#include +#include +#include + +#include "networkit/auxiliary/Random.hpp" +#include + +#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::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; + void expectGraphsEqual(const Graph &expected, const Graph &actual) const; +}; + +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::set InducedSubgraphViewGTest::allNodes(const Graph &G) const { + std::set subset; + G.forNodes([&](node u) { subset.insert(u); }); + return subset; +} + +std::set InducedSubgraphViewGTest::someNodes(const Graph &G, count k, bool random) const { + std::set subset; + index idx = 0; + auto cb = [&](node u) { + 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)}}; +} + +GraphW InducedSubgraphViewGTest::inducedSubgraph(const Graph &G, + const std::set &subset) const { + return GraphTools::subgraphFromNodes(G, subset.begin(), subset.end()); +} + +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() { + /* + * 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; + } + } +} + +/** 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); + + 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 **/ + +TEST_P(InducedSubgraphViewGTest, testAddNode) { + 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()); + + G.addNode(0); + ASSERT_TRUE(G.hasNode(0)); + ASSERT_FALSE(G.hasNode(1)); + ASSERT_EQ(1u, G.numberOfNodes()); + + InducedSubgraphView G2(H, someNodes(H, 2)); + 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) { + GraphW H = createGraphW(100); + + InducedSubgraphView G(H, someNodes(H, 5)); + ASSERT_EQ(G.numberOfNodes(), 5u); + + 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)); + + 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 = [&](const GraphW &base) { + InducedSubgraphView G(base, allNodes(base)); + + count n = G.numberOfNodes(); + count m = G.numberOfEdges(); + const count z = base.upperNodeIdBound(); + for (node u = 0; u < z; ++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); + base.forNodes([&](node v) { ASSERT_EQ(G.hasNode(v), v > u); }); + } + }; + + GraphW G1 = ErdosRenyiGenerator(200, 0.2, false).generate(); + GraphW G2 = ErdosRenyiGenerator(200, 0.2, true).generate(); + testInducedSubgraphView(G1); + testInducedSubgraphView(G2); +} + +TEST_P(InducedSubgraphViewGTest, testHasNode) { + GraphW H = createGraphW(7); + InducedSubgraphView G(H, someNodes(H, 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.removeNodes({0, 2}); + G.addNodes({5}); + + 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, testAddRemoveNodesOutsideOriginalGraph) { + GraphW H = createGraphW(5); + InducedSubgraphView G(H, allNodes(H)); + ASSERT_EQ(5u, G.numberOfNodes()); + + 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()); + + 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)); +} + +/** NODE PROPERTIES **/ + +TEST_P(InducedSubgraphViewGTest, testDegree) { + 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) { + 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) { + 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, testWeightedDegree) { + this->Ghouse.addEdge(2, 2, 0.75); + + 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) { + this->Ghouse.addEdge(2, 2, 0.75); + + 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)); + } +} + +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 base = ErdosRenyiGenerator(n, p, isDirected()).generate(); + if (isWeighted()) { + 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)); + } + } + } +} + +/** 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); + } +} + +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) { + 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); + } +} + +/** 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); + } + } +} + +/** 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 */