Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@angriman
Copy link
Member

@angriman angriman commented Jun 2, 2025

Problem

The NetworKit::Graph class always uses 64-bit integers for node IDs, but NetworKit applications rarely use (if ever) graphs with more than 2^32 nodes. Using smaller node IDs (32-bit or less) significantly reduces the size of the graph and improves scalability.

Edge weights have a similar issue: NetworKit::Graph uses 64-bit floats for edge weights, while applications might only require 32-bit floats or integers.

Proposed solution

Adding a NodeType and EdgeWeightType template parameters to NetworKit's graph class enables users to select the most appropriate type for node IDs and edge weights. I considered two options for implementing this idea:

1. [not recommended] Add template parameters to NetworKit::Graph and to all algorithms using Graph.

With this option, we'll keep the original NetworKit::Graph class, but we'll have to add the same template parameters to all NetworKit algorithms using it:

// Graph.hpp

template <class NodeType, class EdgeWeightType>
class Graph { ... };

// MyAlgo.hpp
#include <networkit/graph/Graph.hpp>

template <class NodeType, class EdgeWeightType>
class MyAlgo {
public:
  MyAlgo(const Graph<NodeType, EdgeWeightType>& G);
};

Alternatively, we could add a GraphType template parameter to all algorithms and access the NodeType and EdgeWeightType in Graph:

// Graph.hpp

template <class NodeType, class EdgeWeightType>
class Graph {
public:
  typedef NodeType node_type;
  typedef EdgeWeightType edge_weight_type;
};

// MyAlgo.hpp
#include <networkit/graph/Graph.hpp>

template <class GraphType>
class MyAlgo {
  using NodeType = typename GraphType::node_type;
  using EdgeWeightType = typename GraphType::edge_weight_type;

public:
  MyAlgo(const GraphType& G);
};

I think that none of those solutions are viable because:

  1. they require significant changes to the code;
  2. they break all external C++ code using Graph (after the change, Graph will require template parameters).

2. [proposal] Rename Graph to DynamicGraph and define Graph as an alias of DynamicGraph<uint64_t, double>.

This is the option implemented in this PR. I also included an example of how we can migrate an existing algorithm (the NetworKit::FloydWarshall) to the template graph, demonstrating feasibility.

// Graph.hpp
template <class NodeType, class EdgeWeightType>
class DynamicGraph { ... };

using Graph = DynamicGraph<uint64_t, double>;

// MyAlgo.hpp
#include <networkit/graph/Graph.hpp>

template <class GraphType>
class MyAlgo {
  using NodeType = typename GraphType::node_type;
  using EdgeWeightType = typename GraphType::edge_weight_type;

public:
  MyAlgo(const GraphType& G);
};

If we accept renaming Graph, this option has two main advantages compared to the previous one:

  1. Graph and all algorithms will maintain the same behavior as before, no breakage of external code using Graph.
  2. Incremental migration: since Graph maintains the same behavior as before, we can migrate algorithms gradually instead of migrating all of them in one change (see the FloydWarshall example).

Further, the idea of adding a GraphType template parameter to algorithms will allow keeping a single implementation and generalize it to different graph types. This should be useful to efforts trying to introduce new graph types like #1231 or #1246.

Please let me know if you have any suggestions or if you have other ideas I haven't considered.

@angriman angriman force-pushed the feature/template-graph branch 3 times, most recently from 2f1c9ee to a5b053d Compare June 4, 2025 19:29
@angriman angriman force-pushed the feature/template-graph branch 4 times, most recently from e8b10c5 to 6c0d567 Compare June 4, 2025 20:26
@angriman angriman force-pushed the feature/template-graph branch from 6c0d567 to 5d610bc Compare June 4, 2025 20:28
@fabratu
Copy link
Member

fabratu commented Jun 23, 2025

We had also offline discussions about this topic. The rationale was basically the same as what is stated here, being more flexible with node ids and edge weights.

Also your proposal makes sense. A few (maybe not thorough thoughts):

  • The change shifts NetworKit substantially towards a header only library and large header files. This reduces readability, but I am not aware of a technique for avoidance.
  • From a design perspective we now have two namespaces/owners of node ids and edge weights. Code independent from the Graph-class currently uses fixed node and edgeweight via Globals.hpp. One way to deal with this would be to also make the datatypes in Globals also templates and provide alias names in order to not break existing code + convenience: node, node32, node16, edgeweight, edgeweight32, ... This would also provide a fallback, whenever a GraphType is not available.
  • Is there a way to avoid to add using NodeType = ... and using EdgeWeightType = ... at the beginning of the algorithms? It is just convenience, however this will likely lead to different parameter names in different classes, reducing readability further.

Likely, other modules/classes like GraphTools, generators or Aux need also be converted to templates in the end - however with the proposed solution this can be done in reasonable chunks.

@Schwarf
Copy link
Contributor

Schwarf commented Jun 24, 2025

Not an assigned reviewer, but I wanted to share a quick thought on the topic:
One way to keep the API clear while staying header-only is to split each templated class into:

  • A .hpp file containing only the declarations, docs and the using Graph = … alias.
  • A matching .inl (or .tpp) file with all of the inline/template implementations.
  • Then at the bottom of each X.hpp we simply include the corresponding #include "X.inl"

That way:

  • Readers see a concise interface first, without wading through implementation details.
  • Retains header-only compilation (no extra .cpp needed).
  • IDEs and doxygen can be pointed just at the .hpp for cleaner navigation.

@angriman
Copy link
Member Author

Thank you both for your feedback.

The change shifts NetworKit substantially towards a header only library and large header files. This reduces readability, but I am not aware of a technique for avoidance.

That's right. For readability, we could adopt the suggestions from @Schwarf, but we can't avoid other issues like longer compilation time.

Code independent from the Graph-class currently uses fixed node and edgeweight via Globals.hpp. One way to deal with this would be to also make the datatypes in Globals also templates and provide alias names in order to not break existing code.

I think that an easier solution is to template that code and add default template parameters to minimize changes in non-template algorithms using it. For example, BucketPQ.hpp defines an <int64_t, uint64_t> priority queue. We could replace it with:

// BucketPQ.hpp
template <class KeyType = int64_t, class ValueType = uint64_t>
class BucketPQ : public PrioQueue<KeyType, ValueType> {
    static_assert(std::is_signed<KeyType>() && std::is_integral<KeyType>());
   // ...
};

// SomeAlgo.hpp
// Instead of `BucketPQ prioQ;`
BucketPQ<> prioQ;

And then we gradually stop using the types declared in Globals.hpp as we template the classes using it.

Let me know if what you are proposing is simper than this, I might have misinterpreted it.

Is there a way to avoid to add using NodeType = ... and using EdgeWeightType = ... at the beginning of the algorithms?

Yes, we can do something similar as done with NodeIterators.hpp. The disadvantage is that doing so we have to specify the template arguments each time for GraphType as well:

template <template <class, class> class GraphType, class NodeType, class EdgeWeightType>
class FloydWarshall : public Algorithm {
public:
  // Before:
  // FloydWarshall(const GraphType &G);
  // Now it becomes:
  FloydWarshall(const GraphType<NodeType, EdgeWeightType> &G);
};

// Before:
// template <class GraphType>
// void FloydWarshall<GraphType>::run() {}
// Now it becomes:
template <template <class, class> class GraphType, class NodeType, class EdgeWeightType>
void FloydWarshall<GraphType<NodeType, EdgeWeightType>>::run() {}

To me this looks too cluttered. Adding the using NodeType = ... and using EdgeWeightType = ... at the beginning is redundant, but it significantly simplifies the code. I'm not aware of other solutions.

My question is how do we move from here? If we agree to proceed with this change, I think that we should clarify:

  • How we migrate the C++ code: how we name the template parameters (e.g., we could shorten them as NodeT and EdgeWeightT), whether we prefer having the using NodeType = ... and using EdgeWeightType = ... or the more verbose template <template <class, class> class GraphType, class NodeType, class EdgeWeightType> or others if available, etc.
  • Prepare a migration plan: after Graph, which algorithms/functions do we want to migrate first? Do we also want to migrate their Cython counterparts? For example, instead of a 64-bit graph, we could expose to the Cython code only a 32-bit graph.

Let me know what you think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants