From 1d5f88c832c075f14b6c40f3356d23443212fec1 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Wed, 21 Jan 2026 11:49:51 +0100 Subject: [PATCH 1/3] Add counting on OD nodes [skip ci] --- src/dsf/bindings.cpp | 24 ++++++++++++++ src/dsf/mobility/RoadDynamics.hpp | 54 ++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index e63666be..d1bd7b26 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -603,6 +603,30 @@ PYBIND11_MODULE(dsf_cpp, m) { }, dsf::g_docstrings.at("dsf::mobility::RoadDynamics::normalizedTurnCounts") .c_str()) + .def( + "originCounts", + [](dsf::mobility::FirstOrderDynamics& self, bool reset) { + // Convert C++ unordered_map to Python dict + pybind11::dict py_result; + for (const auto& [node_id, count] : self.originCounts(reset)) { + py_result[pybind11::int_(node_id)] = pybind11::int_(count); + } + return py_result; + }, + pybind11::arg("reset") = true, + dsf::g_docstrings.at("dsf::mobility::RoadDynamics::originCounts").c_str()) + .def( + "destinationCounts", + [](dsf::mobility::FirstOrderDynamics& self, bool reset) { + // Convert C++ unordered_map to Python dict + pybind11::dict py_result; + for (const auto& [node_id, count] : self.destinationCounts(reset)) { + py_result[pybind11::int_(node_id)] = pybind11::int_(count); + } + return py_result; + }, + pybind11::arg("reset") = true, + dsf::g_docstrings.at("dsf::mobility::RoadDynamics::destinationCounts").c_str()) .def( "saveStreetDensities", &dsf::mobility::FirstOrderDynamics::saveStreetDensities, diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index 7e541fd5..4d5570fd 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -49,6 +49,8 @@ namespace dsf::mobility { std::unordered_map> m_itineraries; std::unordered_map m_originNodes; std::unordered_map m_destinationNodes; + tbb::concurrent_unordered_map m_originCounts; + tbb::concurrent_unordered_map m_destinationCounts; Size m_nAgents; protected: @@ -327,6 +329,15 @@ namespace dsf::mobility { return m_turnMapping; } + /// @brief Get the origin counts of the agents + /// @param bReset If true, the origin counts are cleared (default is true) + tbb::concurrent_unordered_map originCounts( + bool const bReset = true) noexcept; + /// @brief Get the destination counts of the agents + /// @param bReset If true, the destination counts are cleared (default is true) + tbb::concurrent_unordered_map destinationCounts( + bool const bReset = true) noexcept; + virtual double streetMeanSpeed(Id streetId) const; virtual Measurement streetMeanSpeed() const; virtual Measurement streetMeanSpeed(double, bool) const; @@ -453,6 +464,16 @@ namespace dsf::mobility { m_travelDTs.push_back({pAgent->distance(), static_cast(this->time_step() - pAgent->spawnTime())}); --m_nAgents; + auto const& streetId = pAgent->streetId(); + if (streetId.has_value()) { + auto const& pStreet{this->graph().edge(streetId.value())}; + auto const& pNode{this->graph().node(pStreet->target())}; + if (m_destinationCounts.contains(pNode->id())) [[likely]] { + ++m_destinationCounts[pNode->id()]; + } else [[unlikely]] { + m_destinationCounts[pNode->id()] = 1; + } + } return pAgent; } @@ -1513,7 +1534,15 @@ namespace dsf::mobility { void RoadDynamics::addAgent(std::unique_ptr pAgent) { m_agents.push_back(std::move(pAgent)); ++m_nAgents; - spdlog::debug("Added {}", *m_agents.back()); + spdlog::trace("Added {}", *m_agents.back()); + auto const& optNodeId{m_agents.back()->srcNodeId()}; + if (optNodeId.has_value()) { + if (m_originCounts.contains(*optNodeId)) [[likely]] { + ++m_originCounts[*optNodeId]; + } else [[unlikely]] { + m_originCounts[*optNodeId] = 1; + } + } } template @@ -2141,6 +2170,29 @@ namespace dsf::mobility { return normalizedTurnCounts; } + template + requires(is_numeric_v) + tbb::concurrent_unordered_map RoadDynamics::originCounts( + bool const bReset) noexcept { + if (!bReset) { + return m_originCounts; + } + auto const tempCounts{std::move(m_originCounts)}; + m_originCounts.clear(); + return tempCounts; + } + template + requires(is_numeric_v) + tbb::concurrent_unordered_map RoadDynamics::destinationCounts( + bool const bReset) noexcept { + if (!bReset) { + return m_destinationCounts; + } + auto const tempCounts{std::move(m_destinationCounts)}; + m_destinationCounts.clear(); + return tempCounts; + } + template requires(is_numeric_v) double RoadDynamics::streetMeanSpeed(Id streetId) const { From 81b35f357e623b48eb5b4d4b5d5180a95db559ad Mon Sep 17 00:00:00 2001 From: Grufoony Date: Wed, 21 Jan 2026 14:55:17 +0100 Subject: [PATCH 2/3] Add tests --- src/dsf/mobility/RoadDynamics.hpp | 14 +++--- test/mobility/Test_dynamics.cpp | 79 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index 4d5570fd..aea2bab3 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -468,10 +468,9 @@ namespace dsf::mobility { if (streetId.has_value()) { auto const& pStreet{this->graph().edge(streetId.value())}; auto const& pNode{this->graph().node(pStreet->target())}; - if (m_destinationCounts.contains(pNode->id())) [[likely]] { - ++m_destinationCounts[pNode->id()]; - } else [[unlikely]] { - m_destinationCounts[pNode->id()] = 1; + auto [it, bInserted] = m_destinationCounts.insert({pNode->id(), 1}); + if (!bInserted) { + ++it->second; } } return pAgent; @@ -1537,10 +1536,9 @@ namespace dsf::mobility { spdlog::trace("Added {}", *m_agents.back()); auto const& optNodeId{m_agents.back()->srcNodeId()}; if (optNodeId.has_value()) { - if (m_originCounts.contains(*optNodeId)) [[likely]] { - ++m_originCounts[*optNodeId]; - } else [[unlikely]] { - m_originCounts[*optNodeId] = 1; + auto [it, bInserted] = m_originCounts.insert({*optNodeId, 1}); + if (!bInserted) { + ++it->second; } } } diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index c72dd33d..9f14b8c7 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -1389,6 +1389,85 @@ TEST_CASE("RoadDynamics Configuration") { FirstOrderDynamics dynamics{defaultNetwork, false, 42}; + SUBCASE("originCounts and destinationCounts") { + GIVEN("A simple network with origin and destination nodes") { + Street s1{0, std::make_pair(0, 1), 13.8888888889}; + Street s2{1, std::make_pair(1, 2), 13.8888888889}; + RoadNetwork graph2; + graph2.addStreets(s1, s2); + FirstOrderDynamics dyn{graph2, false, 42, 0., dsf::PathWeight::LENGTH}; + dyn.addItinerary(2, 2); + dyn.updatePaths(); + + WHEN("We add agents with source nodes and evolve until they reach destination") { + dyn.addAgent(dyn.itineraries().at(2), 0); + dyn.addAgent(dyn.itineraries().at(2), 0); + dyn.addAgent(dyn.itineraries().at(2), 1); + + THEN("originCounts returns the correct counts") { + auto counts = dyn.originCounts(false); + CHECK_EQ(counts.at(0), 2); + CHECK_EQ(counts.at(1), 1); + } + + THEN("originCounts with bReset=true clears the counts") { + auto counts = dyn.originCounts(true); + CHECK_EQ(counts.at(0), 2); + CHECK_EQ(counts.at(1), 1); + + auto countsAfterReset = dyn.originCounts(false); + CHECK(countsAfterReset.empty()); + } + + // Evolve until agents reach destination + while (dyn.nAgents() > 0) { + dyn.evolve(false); + } + + THEN("destinationCounts returns the correct counts") { + auto destCounts = dyn.destinationCounts(false); + CHECK_EQ(destCounts.at(2), 3); + } + + THEN("destinationCounts with bReset=true clears the counts") { + auto destCounts = dyn.destinationCounts(true); + CHECK_EQ(destCounts.at(2), 3); + + auto destCountsAfterReset = dyn.destinationCounts(false); + CHECK(destCountsAfterReset.empty()); + } + } + } + + GIVEN("Multiple destinations") { + Street s0_1{0, std::make_pair(0, 1), 13.8888888889}; + Street s1_2{1, std::make_pair(1, 2), 13.8888888889}; + Street s1_3{2, std::make_pair(1, 3), 13.8888888889}; + RoadNetwork graph2; + graph2.addStreets(s0_1, s1_2, s1_3); + FirstOrderDynamics dyn{graph2, false, 42, 0., dsf::PathWeight::LENGTH}; + dyn.setDestinationNodes({2, 3}); + dyn.updatePaths(); + + WHEN("Agents travel to different destinations") { + dyn.addAgent(dyn.itineraries().at(2), 0); + dyn.addAgent(dyn.itineraries().at(3), 0); + dyn.addAgent(dyn.itineraries().at(3), 0); + + // Evolve until all agents reach destination + while (dyn.nAgents() > 0) { + dyn.evolve(false); + } + + THEN("destinationCounts tracks each destination separately") { + auto destCounts = dyn.destinationCounts(false); + CHECK_EQ(destCounts.at(2), 1); + CHECK_EQ(destCounts.at(3), 2); + } + } + } + } + SUBCASE("setMeanTravelDistance") { CHECK_THROWS_AS(dynamics.setMeanTravelDistance(-1.0), std::invalid_argument); CHECK_THROWS_AS(dynamics.setMeanTravelDistance(0.0), std::invalid_argument); From 889e3d877ae75b5ad1cb422f8d57059de5004ce0 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Wed, 21 Jan 2026 15:03:45 +0100 Subject: [PATCH 3/3] Bump version --- src/dsf/dsf.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index b3172ff9..0aa55faf 100644 --- a/src/dsf/dsf.hpp +++ b/src/dsf/dsf.hpp @@ -6,7 +6,7 @@ static constexpr uint8_t DSF_VERSION_MAJOR = 4; static constexpr uint8_t DSF_VERSION_MINOR = 7; -static constexpr uint8_t DSF_VERSION_PATCH = 1; +static constexpr uint8_t DSF_VERSION_PATCH = 2; static auto const DSF_VERSION = std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH);