From 06fcedb49c6cadfc16ee9808fd8987d0418c3707 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Wed, 18 Feb 2026 10:37:41 +0100 Subject: [PATCH 1/2] Add bc attribute for nodes and edges --- src/dsf/base/Edge.hpp | 11 +- src/dsf/base/Network.hpp | 205 ++++++++++++++++++++++++++++++ src/dsf/base/Node.hpp | 34 +++-- src/dsf/mobility/RoadDynamics.hpp | 12 +- src/dsf/mobility/Station.hpp | 2 +- test/base/Test_node.cpp | 1 - test/mobility/Test_graph.cpp | 191 ++++++++++++++++++++++++++++ 7 files changed, 439 insertions(+), 17 deletions(-) diff --git a/src/dsf/base/Edge.hpp b/src/dsf/base/Edge.hpp index 8e414459..f9f52549 100644 --- a/src/dsf/base/Edge.hpp +++ b/src/dsf/base/Edge.hpp @@ -15,7 +15,8 @@ namespace dsf { geometry::PolyLine m_geometry; Id m_id; std::pair m_nodePair; - std::optional m_weight; + std::optional m_betweennessCentrality{std::nullopt}; + std::optional m_weight{std::nullopt}; double m_angle; void m_setAngle(geometry::Point srcNodeCoordinates, @@ -35,6 +36,11 @@ namespace dsf { /// @brief Set the edge's geometry /// @param geometry dsf::geometry::PolyLine The edge's geometry, a vector of pairs of doubles representing the coordinates of the edge's geometry void setGeometry(geometry::PolyLine geometry); + /// @brief Set the edge's betweenness centrality + /// @param betweennessCentrality The edge's betweenness centrality + inline void setBetweennessCentrality(double const betweennessCentrality) { + m_betweennessCentrality = betweennessCentrality; + } /// @brief Set the edge's weight /// @param weight The edge's weight /// @throws std::invalid_argument if the weight is less or equal to 0 @@ -56,6 +62,9 @@ namespace dsf { /// @brief Get the edge's geometry /// @return dsf::geometry::PolyLine The edge's geometry, a vector of pairs of doubles representing the coordinates of the edge's geometry inline auto const& geometry() const { return m_geometry; } + /// @brief Get the edge's betweenness centrality + /// @return std::optional The edge's betweenness centrality + inline auto const& betweennessCentrality() const { return m_betweennessCentrality; } /// @brief Get the edge's angle, in radians, between the source and target nodes /// @return double The edge's angle, in radians diff --git a/src/dsf/base/Network.hpp b/src/dsf/base/Network.hpp index c163d0df..da6a8f5e 100644 --- a/src/dsf/base/Network.hpp +++ b/src/dsf/base/Network.hpp @@ -1,7 +1,14 @@ #pragma once #include +#include +#include +#include +#include +#include #include +#include +#include #include "Edge.hpp" #include "Node.hpp" @@ -88,6 +95,32 @@ namespace dsf { template requires(std::is_base_of_v) 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 and returns a double representing the edge weight + /// @param getEdgeWeight A callable that takes a const reference to a unique_ptr 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 + requires(std::is_invocable_r_v 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 and returns a double representing the edge weight + /// @param getEdgeWeight A callable that takes a const reference to a unique_ptr 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. + template + requires(std::is_invocable_r_v const&>) + void computeEdgeBetweennessCentralities(WeightFunc getEdgeWeight); }; template requires(std::is_base_of_v && std::is_base_of_v) @@ -242,4 +275,176 @@ namespace dsf { TEdge& Network::edge(Id edgeId) { return dynamic_cast(*edge(edgeId)); } + + template + requires(std::is_base_of_v && std::is_base_of_v) + template + requires(std::is_invocable_r_v const&>) + void Network::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 S; // nodes in order of non-increasing distance + std::unordered_map> P; // predecessors on shortest paths + std::unordered_map sigma; // number of shortest paths + std::unordered_map dist; // distance from source + + for (auto const& [nId, _] : m_nodes) { + P[nId] = {}; + sigma[nId] = 0.0; + dist[nId] = std::numeric_limits::infinity(); + } + sigma[sourceId] = 1.0; + dist[sourceId] = 0.0; + + // Min-heap priority queue: (distance, nodeId) + std::priority_queue, + std::vector>, + std::greater<>> + pq; + pq.push({0.0, sourceId}); + + std::unordered_set 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])) { + sigma[w] += sigma[v]; + P[w].push_back(v); + } + } + } + + // Dependency accumulation (backward pass) + std::unordered_map delta; + for (auto const& [nId, _] : m_nodes) { + delta[nId] = 0.0; + } + + while (!S.empty()) { + Id w = S.top(); + S.pop(); + for (Id v : P[w]) { + delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w]); + } + if (w != sourceId) { + auto currentBC = m_nodes.at(w)->betweennessCentrality(); + m_nodes.at(w)->setBetweennessCentrality(*currentBC + delta[w]); + } + } + } + } + + template + requires(std::is_base_of_v && std::is_base_of_v) + template + requires(std::is_invocable_r_v const&>) + void Network::computeEdgeBetweennessCentralities( + WeightFunc getEdgeWeight) { + // Initialize all edge betweenness centralities to 0 + for (auto& [edgeId, pEdge] : m_edges) { + pEdge->setBetweennessCentrality(0.0); + } + + // Brandes' algorithm: run single-source Dijkstra from each node + for (auto const& [sourceId, sourceNode] : m_nodes) { + std::stack S; // nodes in order of non-increasing distance + // predecessors: P[w] = list of (predecessor node id, edge id from pred to w) + std::unordered_map>> P; + std::unordered_map sigma; // number of shortest paths + std::unordered_map dist; // distance from source + + for (auto const& [nId, _] : m_nodes) { + P[nId] = {}; + sigma[nId] = 0.0; + dist[nId] = std::numeric_limits::infinity(); + } + sigma[sourceId] = 1.0; + dist[sourceId] = 0.0; + + // Min-heap priority queue: (distance, nodeId) + std::priority_queue, + std::vector>, + std::greater<>> + pq; + pq.push({0.0, sourceId}); + + std::unordered_set 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& eId : m_nodes.at(v)->outgoingEdges()) { + auto const& pEdge = m_edges.at(eId); + 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, eId}}; + pq.push({newDist, w}); + } 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}); + } + } + } + + // Dependency accumulation (backward pass) + std::unordered_map delta; + for (auto const& [nId, _] : m_nodes) { + delta[nId] = 0.0; + } + + while (!S.empty()) { + Id w = S.top(); + S.pop(); + for (auto const& [v, eId] : P[w]) { + double c = (sigma[v] / sigma[w]) * (1.0 + delta[w]); + delta[v] += c; + // Accumulate edge betweenness + auto currentBC = m_edges.at(eId)->betweennessCentrality(); + m_edges.at(eId)->setBetweennessCentrality(*currentBC + c); + } + } + } + } } // namespace dsf \ No newline at end of file diff --git a/src/dsf/base/Node.hpp b/src/dsf/base/Node.hpp index 43e9249f..6309d009 100644 --- a/src/dsf/base/Node.hpp +++ b/src/dsf/base/Node.hpp @@ -31,6 +31,7 @@ namespace dsf { std::string m_name; std::vector m_ingoingEdges; std::vector m_outgoingEdges; + std::optional m_betweennessCentrality{std::nullopt}; public: /// @brief Construct a new Node object with capacity 1 @@ -47,7 +48,8 @@ namespace dsf { m_geometry{other.m_geometry}, m_name{other.m_name}, m_ingoingEdges{other.m_ingoingEdges}, - m_outgoingEdges{other.m_outgoingEdges} {} + m_outgoingEdges{other.m_outgoingEdges}, + m_betweennessCentrality{other.m_betweennessCentrality} {} virtual ~Node() = default; Node& operator=(Node const& other) { @@ -57,6 +59,7 @@ namespace dsf { m_name = other.m_name; m_ingoingEdges = other.m_ingoingEdges; m_outgoingEdges = other.m_outgoingEdges; + m_betweennessCentrality = other.m_betweennessCentrality; } return *this; } @@ -98,24 +101,31 @@ namespace dsf { } m_outgoingEdges.push_back(edgeId); } + /// @brief Set the node's betweenness centrality + /// @param betweennessCentrality The node's betweenness centrality + inline void setBetweennessCentrality(double const betweennessCentrality) noexcept { + m_betweennessCentrality = betweennessCentrality; + } /// @brief Get the node's id /// @return Id The node's id - inline Id id() const { return m_id; } + inline auto id() const { return m_id; } /// @brief Get the node's geometry /// @return std::optional A geometry::Point - inline std::optional const& geometry() const noexcept { - return m_geometry; - } + inline auto const& geometry() const noexcept { return m_geometry; } /// @brief Get the node's name /// @return std::string The node's name - inline std::string const& name() const noexcept { return m_name; } - - inline std::vector const& ingoingEdges() const noexcept { return m_ingoingEdges; } - inline std::vector const& outgoingEdges() const noexcept { - return m_outgoingEdges; + inline auto const& name() const noexcept { return m_name; } + /// @brief Get the node's ingoing edges + /// @return std::vector A vector of the node's ingoing edge ids + inline auto const& ingoingEdges() const noexcept { return m_ingoingEdges; } + /// @brief Get the node's outgoing edges + /// @return std::vector A vector of the node's outgoing edge ids + inline auto const& outgoingEdges() const noexcept { return m_outgoingEdges; } + /// @brief Get the node's betweenness centrality + /// @return std::optional The node's betweenness centrality, or std::nullopt if not set + inline auto const& betweennessCentrality() const noexcept { + return m_betweennessCentrality; } - - virtual bool isStation() const noexcept { return false; } }; }; // namespace dsf diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index 4d38f034..31a8991a 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -554,14 +554,18 @@ namespace dsf::mobility { // Get current street information std::optional previousNodeId = std::nullopt; std::set forbiddenTurns; - double speedCurrent = 1.0; + double speedCurrent{1.0}; + double lengthCurrent{1.0}; double stationaryWeightCurrent = 1.0; + double bcCurrent{1.0}; if (pAgent->streetId().has_value()) { auto const& pStreetCurrent{this->graph().edge(pAgent->streetId().value())}; previousNodeId = pStreetCurrent->source(); forbiddenTurns = pStreetCurrent->forbiddenTurns(); speedCurrent = pStreetCurrent->maxSpeed(); + lengthCurrent = pStreetCurrent->length(); stationaryWeightCurrent = pStreetCurrent->stationaryWeight(); + bcCurrent = pStreetCurrent->betweennessCentrality().value_or(1.0); } // Get path targets for non-random agents @@ -597,10 +601,14 @@ namespace dsf::mobility { // Calculate base probability auto const speedNext{pStreetOut->maxSpeed()}; + auto const lengthNext{pStreetOut->length()}; + auto const bcNext{pStreetOut->betweennessCentrality().value_or(1.0)}; double const stationaryWeightNext = pStreetOut->stationaryWeight(); auto const weightRatio{stationaryWeightNext / stationaryWeightCurrent}; // SQRT (p_i / p_j) - double probability = speedCurrent * speedNext * std::sqrt(weightRatio); + double probability = + std::sqrt((bcCurrent * bcNext) * (speedCurrent / lengthCurrent) * + (speedNext / lengthNext) * weightRatio); // Apply error probability for non-random agents if (this->m_errorProbability.has_value() && !pathTargets.empty()) { diff --git a/src/dsf/mobility/Station.hpp b/src/dsf/mobility/Station.hpp index c75d13cc..afb7af35 100644 --- a/src/dsf/mobility/Station.hpp +++ b/src/dsf/mobility/Station.hpp @@ -51,6 +51,6 @@ namespace dsf::mobility { bool isFull() const; /// @brief Check if the node is a station /// @return True - constexpr bool isStation() const noexcept final { return true; } + constexpr bool isStation() const noexcept /*final*/ { return true; } }; } // namespace dsf::mobility \ No newline at end of file diff --git a/test/base/Test_node.cpp b/test/base/Test_node.cpp index 2dee6a40..c8818e52 100644 --- a/test/base/Test_node.cpp +++ b/test/base/Test_node.cpp @@ -23,7 +23,6 @@ TEST_CASE("Node basic") { CHECK_EQ(n1.name(), ""); CHECK(n1.ingoingEdges().empty()); CHECK(n1.outgoingEdges().empty()); - CHECK_FALSE(n1.isStation()); dsf::Node n2{6, dsf::geometry::Point{1.0, 2.0}}; CHECK_EQ(n2.id(), 6); diff --git a/test/mobility/Test_graph.cpp b/test/mobility/Test_graph.cpp index 3abb4f4f..b22abdf6 100644 --- a/test/mobility/Test_graph.cpp +++ b/test/mobility/Test_graph.cpp @@ -1668,3 +1668,194 @@ TEST_CASE("Change Street Lanes") { } } } + +TEST_CASE("BetweennessCentrality") { + Road::setMeanVehicleLength(5.); + auto unitWeight = []([[maybe_unused]] auto const& pEdge) { return 1.0; }; + + SUBCASE("Linear chain: 0 -> 1 -> 2 -> 3") { + // In a linear chain, all shortest paths between non-adjacent nodes pass + // through the intermediate nodes. Node 1 and 2 should have BC > 0, + // while endpoints 0 and 3 should have BC = 0. + RoadNetwork graph{}; + Street s01(0, std::make_pair(0, 1), 10.0); + Street s12(1, std::make_pair(1, 2), 10.0); + Street s23(2, std::make_pair(2, 3), 10.0); + graph.addStreets(s01, s12, s23); + + graph.computeBetweennessCentralities(unitWeight); + + // Node 0: source only, never an intermediate -> BC = 0 + REQUIRE(graph.node(0)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(0)->betweennessCentrality(), doctest::Approx(0.0)); + + // Node 1: on path 0->2 and 0->3 -> BC = 2 + REQUIRE(graph.node(1)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(1)->betweennessCentrality(), doctest::Approx(2.0)); + + // Node 2: on path 0->3 and 1->3 -> BC = 2 + REQUIRE(graph.node(2)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(2)->betweennessCentrality(), doctest::Approx(2.0)); + + // Node 3: sink only, never an intermediate -> BC = 0 + REQUIRE(graph.node(3)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(3)->betweennessCentrality(), doctest::Approx(0.0)); + } + + SUBCASE("Star topology: center node") { + // Star: 0 -> 1, 0 -> 2, 0 -> 3 + // No node is intermediate (all paths have length 1), so all BC = 0. + RoadNetwork graph{}; + Street s01(0, std::make_pair(0, 1), 10.0); + Street s02(1, std::make_pair(0, 2), 10.0); + Street s03(2, std::make_pair(0, 3), 10.0); + graph.addStreets(s01, s02, s03); + + graph.computeBetweennessCentralities(unitWeight); + + for (Id i = 0; i <= 3; ++i) { + REQUIRE(graph.node(i)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(i)->betweennessCentrality(), doctest::Approx(0.0)); + } + } + + SUBCASE("Diamond graph with different weights") { + /* 1 + / \ + 0 3 + \ / + 2 + 0->1 weight 1, 1->3 weight 1 (total 2) + 0->2 weight 1, 2->3 weight 3 (total 4) + Shortest path 0->3 goes through node 1 only */ + RoadNetwork graph{}; + Street s01(0, std::make_pair(0, 1), 10.0); + Street s02(1, std::make_pair(0, 2), 10.0); + Street s13(2, std::make_pair(1, 3), 10.0); + Street s23(3, std::make_pair(2, 3), 30.0); + graph.addStreets(s01, s02, s13, s23); + + graph.computeBetweennessCentralities( + [](auto const& pEdge) { return pEdge->length(); }); + + // Node 1 is on the only shortest path 0->3 -> BC = 1 + REQUIRE(graph.node(1)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(1)->betweennessCentrality(), doctest::Approx(1.0)); + + // Node 2 is not on any shortest path between other pairs -> BC = 0 + REQUIRE(graph.node(2)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(2)->betweennessCentrality(), doctest::Approx(0.0)); + } + + SUBCASE("Single edge") { + RoadNetwork graph{}; + Street s01(0, std::make_pair(0, 1), 10.0); + graph.addStreet(std::move(s01)); + + graph.computeBetweennessCentralities(unitWeight); + + REQUIRE(graph.node(0)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(0)->betweennessCentrality(), doctest::Approx(0.0)); + REQUIRE(graph.node(1)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(1)->betweennessCentrality(), doctest::Approx(0.0)); + } + + SUBCASE("Disconnected graph") { + // Two separate components: 0->1, 2->3 + // No intermediate nodes possible -> all BC = 0 + RoadNetwork graph{}; + Street s01(0, std::make_pair(0, 1), 10.0); + Street s23(1, std::make_pair(2, 3), 10.0); + graph.addStreets(s01, s23); + + graph.computeBetweennessCentralities(unitWeight); + + for (Id i = 0; i <= 3; ++i) { + REQUIRE(graph.node(i)->betweennessCentrality().has_value()); + CHECK_EQ(*graph.node(i)->betweennessCentrality(), doctest::Approx(0.0)); + } + } +} + +TEST_CASE("EdgeBetweennessCentrality") { + Road::setMeanVehicleLength(5.); + auto unitWeight = []([[maybe_unused]] auto const& pEdge) { return 1.0; }; + + SUBCASE("Linear chain: 0 -> 1 -> 2 -> 3") { + // Edge 0->1 (id=0): used by paths 0->1, 0->2, 0->3 => EBC = 3 + // Edge 1->2 (id=1): used by paths 0->2, 1->2, 0->3, 1->3 => EBC = 4 + // Edge 2->3 (id=2): used by paths 0->3, 1->3, 2->3 => EBC = 3 + RoadNetwork graph{}; + Street s01(0, std::make_pair(0, 1), 10.0); + Street s12(1, std::make_pair(1, 2), 10.0); + Street s23(2, std::make_pair(2, 3), 10.0); + graph.addStreets(s01, s12, s23); + + graph.computeEdgeBetweennessCentralities(unitWeight); + + REQUIRE(graph.edge(static_cast(0))->betweennessCentrality().has_value()); + CHECK_EQ(*graph.edge(static_cast(0))->betweennessCentrality(), + doctest::Approx(3.0)); + + REQUIRE(graph.edge(static_cast(1))->betweennessCentrality().has_value()); + CHECK_EQ(*graph.edge(static_cast(1))->betweennessCentrality(), + doctest::Approx(4.0)); + + REQUIRE(graph.edge(static_cast(2))->betweennessCentrality().has_value()); + CHECK_EQ(*graph.edge(static_cast(2))->betweennessCentrality(), + doctest::Approx(3.0)); + } + + SUBCASE("Diamond graph with different weights") { + /* 1 + / \ + 0 3 + \ / + 2 + 0->1 length 10, 1->3 length 10 (total 20) + 0->2 length 10, 2->3 length 30 (total 40) + Shortest path 0->3 goes 0->1->3 */ + RoadNetwork graph{}; + Street s01(0, std::make_pair(0, 1), 10.0); + Street s02(1, std::make_pair(0, 2), 10.0); + Street s13(2, std::make_pair(1, 3), 10.0); + Street s23(3, std::make_pair(2, 3), 30.0); + graph.addStreets(s01, s02, s13, s23); + + graph.computeEdgeBetweennessCentralities( + [](auto const& pEdge) { return pEdge->length(); }); + + // Edge 0->1: used by 0->1 and 0->3 (via 0->1->3) => EBC = 2 + REQUIRE(graph.edge(static_cast(0))->betweennessCentrality().has_value()); + CHECK_EQ(*graph.edge(static_cast(0))->betweennessCentrality(), + doctest::Approx(2.0)); + + // Edge 0->2: used only by 0->2 => EBC = 1 + REQUIRE(graph.edge(static_cast(1))->betweennessCentrality().has_value()); + CHECK_EQ(*graph.edge(static_cast(1))->betweennessCentrality(), + doctest::Approx(1.0)); + + // Edge 1->3: used by 1->3 and 0->3 => EBC = 2 + REQUIRE(graph.edge(static_cast(2))->betweennessCentrality().has_value()); + CHECK_EQ(*graph.edge(static_cast(2))->betweennessCentrality(), + doctest::Approx(2.0)); + + // Edge 2->3: used only by 2->3 => EBC = 1 + REQUIRE(graph.edge(static_cast(3))->betweennessCentrality().has_value()); + CHECK_EQ(*graph.edge(static_cast(3))->betweennessCentrality(), + doctest::Approx(1.0)); + } + + SUBCASE("Single edge") { + RoadNetwork graph{}; + Street s01(0, std::make_pair(0, 1), 10.0); + graph.addStreet(std::move(s01)); + + graph.computeEdgeBetweennessCentralities(unitWeight); + + // Only one path: 0->1, using edge 0 => EBC = 1 + REQUIRE(graph.edge(static_cast(0))->betweennessCentrality().has_value()); + CHECK_EQ(*graph.edge(static_cast(0))->betweennessCentrality(), + doctest::Approx(1.0)); + } +} From c194620f6d963f099071c34f1ba888e7facbd60f Mon Sep 17 00:00:00 2001 From: Grufoony Date: Wed, 18 Feb 2026 10:51:17 +0100 Subject: [PATCH 2/2] Add python bindings --- src/dsf/bindings.cpp | 83 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index 06fcff7f..2eebd083 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -265,7 +265,88 @@ PYBIND11_MODULE(dsf_cpp, m) { " threshold (float): A threshold value to consider alternative paths\n\n" "Returns:\n" " PathCollection: A map where each key is a node id and the value is a " - "vector of next hop node ids toward the target"); + "vector of next hop node ids toward the target") + .def( + "computeBetweennessCentralities", + [](dsf::mobility::RoadNetwork& self, const std::string& weight) { + auto weightFunc = + [&weight](const std::unique_ptr& street) { + if (weight == "length") { + return street->length(); + } else if (weight == "traveltime") { + return street->length() / street->maxSpeed(); + } else if (weight == "weight") { + return street->weight(); + } else { + throw std::invalid_argument( + "Invalid weight function: '" + weight + + "'. Valid options are: 'length', 'traveltime', 'weight'."); + } + }; + self.computeBetweennessCentralities(weightFunc); + }, + pybind11::arg("weight") = "length", + "Compute betweenness centralities for all nodes using Brandes' algorithm.\n\n" + "Args:\n" + " weight (str): The weight function to use. Options are:\n" + " - 'length': Use the street length as weight\n" + " - 'traveltime': Use length / max_speed as weight\n" + " - 'weight': Use the custom edge weight\n\n" + "The results are stored in each node's betweennessCentrality attribute.") + .def( + "computeEdgeBetweennessCentralities", + [](dsf::mobility::RoadNetwork& self, const std::string& weight) { + auto weightFunc = + [&weight](const std::unique_ptr& street) { + if (weight == "length") { + return street->length(); + } else if (weight == "traveltime") { + return street->length() / street->maxSpeed(); + } else if (weight == "weight") { + return street->weight(); + } else { + throw std::invalid_argument( + "Invalid weight function: '" + weight + + "'. Valid options are: 'length', 'traveltime', 'weight'."); + } + }; + self.computeEdgeBetweennessCentralities(weightFunc); + }, + pybind11::arg("weight") = "length", + "Compute edge betweenness centralities for all edges using Brandes' " + "algorithm.\n\n" + "Args:\n" + " weight (str): The weight function to use. Options are:\n" + " - 'length': Use the street length as weight\n" + " - 'traveltime': Use length / max_speed as weight\n" + " - 'weight': Use the custom edge weight\n\n" + "The results are stored in each edge's betweennessCentrality attribute.") + .def( + "nodeBetweennessCentralities", + [](const dsf::mobility::RoadNetwork& self) { + std::unordered_map> result; + for (auto const& [nodeId, pNode] : self.nodes()) { + result[nodeId] = pNode->betweennessCentrality(); + } + return result; + }, + "Get the betweenness centrality values for all nodes.\n\n" + "Returns:\n" + " dict[int, float | None]: A dictionary mapping node id to its " + "betweenness centrality value (None if not computed).") + .def( + "edgeBetweennessCentralities", + [](const dsf::mobility::RoadNetwork& self) { + std::unordered_map> result; + for (auto const& [edgeId, pEdge] : self.edges()) { + result[edgeId] = pEdge->betweennessCentrality(); + } + return result; + }, + "Get the betweenness centrality values for all edges.\n\n" + "Returns:\n" + " dict[int, float | None]: A dictionary mapping edge id to its " + "betweenness centrality value (None if not computed)."); pybind11::class_(mobility, "PathCollection") .def(pybind11::init<>(), "Create an empty PathCollection")