-
Notifications
You must be signed in to change notification settings - Fork 4
Add bc attribute for nodes and edges #408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,14 @@ | ||||||||||||||||||||||||
| #pragma once | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| #include <cassert> | ||||||||||||||||||||||||
| #include <cmath> | ||||||||||||||||||||||||
| #include <limits> | ||||||||||||||||||||||||
| #include <queue> | ||||||||||||||||||||||||
| #include <stack> | ||||||||||||||||||||||||
| #include <type_traits> | ||||||||||||||||||||||||
| #include <unordered_map> | ||||||||||||||||||||||||
| #include <unordered_set> | ||||||||||||||||||||||||
| #include <vector> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| #include "Edge.hpp" | ||||||||||||||||||||||||
| #include "Node.hpp" | ||||||||||||||||||||||||
|
|
@@ -88,6 +95,32 @@ namespace dsf { | |||||||||||||||||||||||
| template <typename TEdge> | ||||||||||||||||||||||||
| requires(std::is_base_of_v<edge_t, TEdge>) | ||||||||||||||||||||||||
| TEdge& edge(Id edgeId); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// @brief Compute betweenness centralities for all nodes using Brandes' algorithm | ||||||||||||||||||||||||
| /// @tparam WeightFunc A callable type that takes a const reference to a unique_ptr<edge_t> and returns a double representing the edge weight | ||||||||||||||||||||||||
| /// @param getEdgeWeight A callable that takes a const reference to a unique_ptr<edge_t> and returns a double (must be positive) | ||||||||||||||||||||||||
| /// @details Implements Brandes' algorithm for directed weighted graphs. | ||||||||||||||||||||||||
| /// The computed centrality for each node v is: | ||||||||||||||||||||||||
| /// C_B(v) = sum_{s != v != t} sigma_st(v) / sigma_st | ||||||||||||||||||||||||
| /// where sigma_st is the number of shortest paths from s to t, | ||||||||||||||||||||||||
| /// and sigma_st(v) is the number of those paths passing through v. | ||||||||||||||||||||||||
| /// Results are stored via Node::setBetweennessCentrality. | ||||||||||||||||||||||||
| template <typename WeightFunc> | ||||||||||||||||||||||||
| requires(std::is_invocable_r_v<double, WeightFunc, std::unique_ptr<edge_t> const&>) | ||||||||||||||||||||||||
| void computeBetweennessCentralities(WeightFunc getEdgeWeight); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// @brief Compute edge betweenness centralities for all edges using Brandes' algorithm | ||||||||||||||||||||||||
| /// @tparam WeightFunc A callable type that takes a const reference to a unique_ptr<edge_t> and returns a double representing the edge weight | ||||||||||||||||||||||||
| /// @param getEdgeWeight A callable that takes a const reference to a unique_ptr<edge_t> and returns a double (must be positive) | ||||||||||||||||||||||||
| /// @details Implements Brandes' algorithm for directed weighted graphs. | ||||||||||||||||||||||||
| /// The computed centrality for each edge e is: | ||||||||||||||||||||||||
| /// C_B(e) = sum_{s != t} sigma_st(e) / sigma_st | ||||||||||||||||||||||||
| /// where sigma_st is the number of shortest paths from s to t, | ||||||||||||||||||||||||
| /// and sigma_st(e) is the number of those paths using edge e. | ||||||||||||||||||||||||
| /// Results are stored via Edge::setBetweennessCentrality. | ||||||||||||||||||||||||
|
Comment on lines
+112
to
+120
|
||||||||||||||||||||||||
| template <typename WeightFunc> | ||||||||||||||||||||||||
| requires(std::is_invocable_r_v<double, WeightFunc, std::unique_ptr<edge_t> const&>) | ||||||||||||||||||||||||
| void computeEdgeBetweennessCentralities(WeightFunc getEdgeWeight); | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| template <typename node_t, typename edge_t> | ||||||||||||||||||||||||
| requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>) | ||||||||||||||||||||||||
|
|
@@ -242,4 +275,176 @@ namespace dsf { | |||||||||||||||||||||||
| TEdge& Network<node_t, edge_t>::edge(Id edgeId) { | ||||||||||||||||||||||||
| return dynamic_cast<TEdge&>(*edge(edgeId)); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| template <typename node_t, typename edge_t> | ||||||||||||||||||||||||
| requires(std::is_base_of_v<Node, node_t> && std::is_base_of_v<Edge, edge_t>) | ||||||||||||||||||||||||
| template <typename WeightFunc> | ||||||||||||||||||||||||
| requires(std::is_invocable_r_v<double, WeightFunc, std::unique_ptr<edge_t> const&>) | ||||||||||||||||||||||||
| void Network<node_t, edge_t>::computeBetweennessCentralities(WeightFunc getEdgeWeight) { | ||||||||||||||||||||||||
| // Initialize all node betweenness centralities to 0 | ||||||||||||||||||||||||
| for (auto& [nodeId, pNode] : m_nodes) { | ||||||||||||||||||||||||
| pNode->setBetweennessCentrality(0.0); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Brandes' algorithm: run single-source Dijkstra from each node | ||||||||||||||||||||||||
| for (auto const& [sourceId, sourceNode] : m_nodes) { | ||||||||||||||||||||||||
| std::stack<Id> S; // nodes in order of non-increasing distance | ||||||||||||||||||||||||
| std::unordered_map<Id, std::vector<Id>> P; // predecessors on shortest paths | ||||||||||||||||||||||||
| std::unordered_map<Id, double> sigma; // number of shortest paths | ||||||||||||||||||||||||
| std::unordered_map<Id, double> dist; // distance from source | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for (auto const& [nId, _] : m_nodes) { | ||||||||||||||||||||||||
| P[nId] = {}; | ||||||||||||||||||||||||
| sigma[nId] = 0.0; | ||||||||||||||||||||||||
| dist[nId] = std::numeric_limits<double>::infinity(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| sigma[sourceId] = 1.0; | ||||||||||||||||||||||||
| dist[sourceId] = 0.0; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Min-heap priority queue: (distance, nodeId) | ||||||||||||||||||||||||
| std::priority_queue<std::pair<double, Id>, | ||||||||||||||||||||||||
| std::vector<std::pair<double, Id>>, | ||||||||||||||||||||||||
| std::greater<>> | ||||||||||||||||||||||||
| pq; | ||||||||||||||||||||||||
| pq.push({0.0, sourceId}); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| std::unordered_set<Id> visited; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| while (!pq.empty()) { | ||||||||||||||||||||||||
| auto [d, v] = pq.top(); | ||||||||||||||||||||||||
| pq.pop(); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (visited.contains(v)) { | ||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| visited.insert(v); | ||||||||||||||||||||||||
| S.push(v); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for (auto const& edgeId : m_nodes.at(v)->outgoingEdges()) { | ||||||||||||||||||||||||
| auto const& pEdge = m_edges.at(edgeId); | ||||||||||||||||||||||||
| Id w = pEdge->target(); | ||||||||||||||||||||||||
| if (visited.contains(w)) { | ||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| double edgeWeight = getEdgeWeight(pEdge); | ||||||||||||||||||||||||
| double newDist = dist[v] + edgeWeight; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (newDist < dist[w]) { | ||||||||||||||||||||||||
| dist[w] = newDist; | ||||||||||||||||||||||||
| sigma[w] = sigma[v]; | ||||||||||||||||||||||||
| P[w] = {v}; | ||||||||||||||||||||||||
| pq.push({newDist, w}); | ||||||||||||||||||||||||
| } else if (std::abs(newDist - dist[w]) < 1e-12 * std::max(1.0, dist[w])) { | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| } else if (std::abs(newDist - dist[w]) < 1e-12 * std::max(1.0, dist[w])) { | |
| } else if (std::abs(newDist - dist[w]) < 1e-12 * (1.0 + std::abs(dist[w]))) { |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code performs division by sigma[w] without checking if it's zero. While the algorithm design ensures sigma[w] should be positive for all nodes in stack S (since unreachable nodes aren't added to S), consider adding an assertion or defensive check (e.g., assert(sigma[w] > 0)) to catch potential logic errors during development and make the assumption explicit.
| S.pop(); | |
| S.pop(); | |
| assert(sigma[w] > 0.0); |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same floating-point comparison tolerance issue exists here as in computeBetweennessCentralities. When dist[w] is 0, the tolerance becomes 1e-12 which may be too strict. Consider using a more robust epsilon for consistency.
| } else if (std::abs(newDist - dist[w]) < 1e-12 * std::max(1.0, dist[w])) { | |
| sigma[w] += sigma[v]; | |
| P[w].push_back({v, eId}); | |
| } else { | |
| double diff = std::abs(newDist - dist[w]); | |
| double scale = std::max(std::abs(newDist), std::abs(dist[w])); | |
| double eps = 1e-9 * (1.0 + scale); | |
| if (diff <= eps) { | |
| sigma[w] += sigma[v]; | |
| P[w].push_back({v, eId}); | |
| } |
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same concern as in computeBetweennessCentralities: consider adding an assertion to verify sigma[w] > 0 to make the algorithm's assumptions explicit and catch potential logic errors.
| for (auto const& [v, eId] : P[w]) { | |
| for (auto const& [v, eId] : P[w]) { | |
| // sigma[w] should be > 0 whenever P[w] is non-empty (Brandes' invariant). | |
| // Assert this to make the assumption explicit and avoid division by zero. | |
| assert(sigma[w] > 0.0); | |
| if (sigma[w] == 0.0) { | |
| continue; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation mentions 'Brandes' algorithm' but doesn't include a reference or citation. Consider adding a citation to the original paper (U. Brandes, 'A faster algorithm for betweenness centrality', Journal of Mathematical Sociology, 2001) to help future maintainers understand the algorithm's theoretical foundation and verify its correctness.