Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/dsf/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Id, size_t> 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<Id, size_t> 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,
Expand Down
2 changes: 1 addition & 1 deletion src/dsf/dsf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
52 changes: 51 additions & 1 deletion src/dsf/mobility/RoadDynamics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ namespace dsf::mobility {
std::unordered_map<Id, std::shared_ptr<Itinerary>> m_itineraries;
std::unordered_map<Id, double> m_originNodes;
std::unordered_map<Id, double> m_destinationNodes;
tbb::concurrent_unordered_map<Id, std::size_t> m_originCounts;
tbb::concurrent_unordered_map<Id, std::size_t> m_destinationCounts;
Size m_nAgents;

protected:
Expand Down Expand Up @@ -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<Id, std::size_t> 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<Id, std::size_t> destinationCounts(
bool const bReset = true) noexcept;

virtual double streetMeanSpeed(Id streetId) const;
virtual Measurement<double> streetMeanSpeed() const;
virtual Measurement<double> streetMeanSpeed(double, bool) const;
Expand Down Expand Up @@ -453,6 +464,15 @@ namespace dsf::mobility {
m_travelDTs.push_back({pAgent->distance(),
static_cast<double>(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())};
auto [it, bInserted] = m_destinationCounts.insert({pNode->id(), 1});
if (!bInserted) {
++it->second;
}
}
return pAgent;
}

Expand Down Expand Up @@ -1513,7 +1533,14 @@ namespace dsf::mobility {
void RoadDynamics<delay_t>::addAgent(std::unique_ptr<Agent> 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()) {
auto [it, bInserted] = m_originCounts.insert({*optNodeId, 1});
if (!bInserted) {
++it->second;
}
}
}

template <typename delay_t>
Expand Down Expand Up @@ -2141,6 +2168,29 @@ namespace dsf::mobility {
return normalizedTurnCounts;
}

template <typename delay_t>
requires(is_numeric_v<delay_t>)
tbb::concurrent_unordered_map<Id, std::size_t> RoadDynamics<delay_t>::originCounts(
bool const bReset) noexcept {
if (!bReset) {
return m_originCounts;
}
auto const tempCounts{std::move(m_originCounts)};
m_originCounts.clear();
return tempCounts;
}
Comment on lines +2175 to +2181
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The move-and-clear pattern for reset is not thread-safe when the map can be accessed concurrently. If reset=true, another thread could be incrementing values during the move operation or immediately after. Consider using a mutex to protect the reset operation, or document that these methods should only be called when no other threads are actively modifying the maps (e.g., between simulation steps).

Copilot uses AI. Check for mistakes.
template <typename delay_t>
requires(is_numeric_v<delay_t>)
tbb::concurrent_unordered_map<Id, std::size_t> RoadDynamics<delay_t>::destinationCounts(
bool const bReset) noexcept {
if (!bReset) {
return m_destinationCounts;
}
auto const tempCounts{std::move(m_destinationCounts)};
m_destinationCounts.clear();
return tempCounts;
Comment on lines +2186 to +2191
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The move-and-clear pattern for reset is not thread-safe when the map can be accessed concurrently. If reset=true, another thread could be incrementing values during the move operation or immediately after. Consider using a mutex to protect the reset operation, or document that these methods should only be called when no other threads are actively modifying the maps (e.g., between simulation steps).

Copilot uses AI. Check for mistakes.
}
Comment on lines +2171 to +2192
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new originCounts and destinationCounts functionality lacks test coverage. Since the test suite in test/mobility/Test_dynamics.cpp contains comprehensive tests for other features, consider adding test cases that verify: 1) agents are correctly counted at their origin nodes when added, 2) agents are correctly counted at their destination nodes when they arrive, 3) the reset parameter works correctly, and 4) the counting works correctly under concurrent access during parallel evolve operations.

Copilot uses AI. Check for mistakes.

template <typename delay_t>
requires(is_numeric_v<delay_t>)
double RoadDynamics<delay_t>::streetMeanSpeed(Id streetId) const {
Expand Down
79 changes: 79 additions & 0 deletions test/mobility/Test_dynamics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,85 @@

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};

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

MISRA 12.3 rule
Street s2{1, std::make_pair(1, 2), 13.8888888889};

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

MISRA 12.3 rule
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};

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

MISRA 12.3 rule
Street s1_2{1, std::make_pair(1, 2), 13.8888888889};

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

MISRA 12.3 rule
Street s1_3{2, std::make_pair(1, 3), 13.8888888889};

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

MISRA 12.3 rule
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);
Expand Down
Loading