diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index d1bd7b26..0a636e5b 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -173,6 +173,12 @@ PYBIND11_MODULE(dsf_cpp, m) { .def("autoMapStreetLanes", &dsf::mobility::RoadNetwork::autoMapStreetLanes, dsf::g_docstrings.at("dsf::mobility::RoadNetwork::autoMapStreetLanes").c_str()) + .def( + "describe", + [](dsf::mobility::RoadNetwork& self) { + self.describe(); // Uses default std::cout + }, + dsf::g_docstrings.at("dsf::mobility::RoadNetwork::describe").c_str()) .def("autoAssignRoadPriorities", &dsf::mobility::RoadNetwork::autoAssignRoadPriorities, dsf::g_docstrings.at("dsf::mobility::RoadNetwork::autoAssignRoadPriorities") @@ -650,7 +656,13 @@ PYBIND11_MODULE(dsf_cpp, m) { pybind11::arg("filename"), pybind11::arg("separator") = ';', dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveMacroscopicObservables") - .c_str()); + .c_str()) + .def( + "summary", + [](dsf::mobility::FirstOrderDynamics& self) { + self.summary(); // Uses default std::cout + }, + dsf::g_docstrings.at("dsf::mobility::RoadDynamics::summary").c_str()); // Bind TrajectoryCollection class to mdt submodule pybind11::class_(mdt, "TrajectoryCollection") diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index c8a9ca49..a7154348 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 = 4; +static constexpr uint8_t DSF_VERSION_PATCH = 5; static auto const DSF_VERSION = std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH); diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index aea2bab3..c769928e 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +53,8 @@ namespace dsf::mobility { std::unordered_map m_destinationNodes; tbb::concurrent_unordered_map m_originCounts; tbb::concurrent_unordered_map m_destinationCounts; - Size m_nAgents; + std::atomic m_nAgents{0}, m_nAddedAgents{0}, m_nInsertedAgents{0}, + m_nKilledAgents{0}, m_nArrivedAgents{0}; protected: std::unordered_map> m_turnCounts; @@ -59,19 +62,19 @@ namespace dsf::mobility { tbb::concurrent_unordered_map> m_queuesAtTrafficLights; tbb::concurrent_vector> m_travelDTs; - std::time_t m_previousOptimizationTime; + std::time_t m_previousOptimizationTime{0}; private: std::function const&)> m_weightFunction; - std::optional m_errorProbability; - std::optional m_passageProbability; - std::optional m_meanTravelDistance; - std::optional m_meanTravelTime; + std::optional m_errorProbability{std::nullopt}; + std::optional m_passageProbability{std::nullopt}; + std::optional m_meanTravelDistance{std::nullopt}; + std::optional m_meanTravelTime{std::nullopt}; double m_weightTreshold; - std::optional m_timeToleranceFactor; + std::optional m_timeToleranceFactor{std::nullopt}; std::optional m_dataUpdatePeriod; bool m_bCacheEnabled; - bool m_forcePriorities; + bool m_forcePriorities{false}; private: /// @brief Kill an agent @@ -394,6 +397,17 @@ namespace dsf::mobility { /// NOTE: the mean density is normalized in [0, 1] and reset is true for all observables which have such parameter void saveMacroscopicObservables(std::string filename = std::string(), char const separator = ';'); + + /// @brief Print a summary of the dynamics to an output stream + /// @param os The output stream to write to (default is std::cout) + /// @details The summary includes: + /// - The RoadNetwork description (nodes, edges, capacity, intersections, traffic lights, roundabouts, coil sensors) + /// - Number of inserted agents + /// - Number of added agents + /// - Number of arrived agents + /// - Number of killed agents + /// - Current number of agents in the simulation + void summary(std::ostream& os = std::cout) const; }; template @@ -403,16 +417,7 @@ namespace dsf::mobility { std::optional seed, PathWeight const weightFunction, std::optional weightTreshold) - : Dynamics(graph, seed), - m_nAgents{0}, - m_previousOptimizationTime{0}, - m_errorProbability{std::nullopt}, - m_passageProbability{std::nullopt}, - m_meanTravelDistance{std::nullopt}, - m_meanTravelTime{std::nullopt}, - m_timeToleranceFactor{std::nullopt}, - m_bCacheEnabled{useCache}, - m_forcePriorities{false} { + : Dynamics(graph, seed), m_bCacheEnabled{useCache} { this->setWeightFunction(weightFunction, weightTreshold); if (m_bCacheEnabled) { if (!std::filesystem::exists(CACHE_FOLDER)) { @@ -738,6 +743,7 @@ namespace dsf::mobility { timeDiff); // Kill the agent this->m_killAgent(pStreet->dequeue(queueIndex)); + ++m_nKilledAgents; continue; } } @@ -893,6 +899,7 @@ namespace dsf::mobility { } if (bArrived) { auto pAgent = this->m_killAgent(pStreet->dequeue(queueIndex)); + ++m_nArrivedAgents; if (reinsert_agents) { // reset Agent's values pAgent->reset(this->time_step()); @@ -1292,6 +1299,7 @@ namespace dsf::mobility { requires(is_numeric_v) void RoadDynamics::addAgentsUniformly(Size nAgents, std::optional optItineraryId) { + m_nAddedAgents += nAgents; if (m_timeToleranceFactor.has_value() && !m_agents.empty()) { auto const nStagnantAgents{m_agents.size()}; spdlog::warn( @@ -1356,6 +1364,7 @@ namespace dsf::mobility { std::is_same_v>) void RoadDynamics::addRandomAgents(std::size_t nAgents, TContainer const& spawnWeights) { + m_nAddedAgents += nAgents; std::uniform_real_distribution uniformDist{0., 1.}; std::exponential_distribution distDist{1. / m_meanTravelDistance.value_or(1.)}; @@ -1402,6 +1411,7 @@ namespace dsf::mobility { void RoadDynamics::addAgentsRandomly(Size nAgents, const TContainer& src_weights, const TContainer& dst_weights) { + m_nAddedAgents += nAgents; if (m_timeToleranceFactor.has_value() && !m_agents.empty()) { auto const nStagnantAgents{m_agents.size()}; spdlog::warn( @@ -1533,6 +1543,7 @@ namespace dsf::mobility { void RoadDynamics::addAgent(std::unique_ptr pAgent) { m_agents.push_back(std::move(pAgent)); ++m_nAgents; + ++m_nInsertedAgents; spdlog::trace("Added {}", *m_agents.back()); auto const& optNodeId{m_agents.back()->srcNodeId()}; if (optNodeId.has_value()) { @@ -2498,4 +2509,16 @@ namespace dsf::mobility { file.close(); } + + template + requires(is_numeric_v) + void RoadDynamics::summary(std::ostream& os) const { + os << "RoadDynamics Summary:\n"; + this->graph().describe(os); + os << "\nNumber of added agents: " << m_nAddedAgents << '\n' + << "Number of inserted agents: " << m_nInsertedAgents << '\n' + << "Number of arrived agents: " << m_nArrivedAgents << '\n' + << "Number of killed agents: " << m_nKilledAgents << '\n' + << "Current number of agents: " << this->nAgents() << '\n'; + } } // namespace dsf::mobility diff --git a/src/dsf/mobility/RoadNetwork.cpp b/src/dsf/mobility/RoadNetwork.cpp index a2e39301..213933d7 100644 --- a/src/dsf/mobility/RoadNetwork.cpp +++ b/src/dsf/mobility/RoadNetwork.cpp @@ -336,23 +336,23 @@ namespace dsf::mobility { RoadNetwork::RoadNetwork(AdjacencyMatrix const& adj) : Network{adj}, m_capacity{0} {} - Size RoadNetwork::nCoils() const { + std::size_t RoadNetwork::nCoils() const { return std::count_if(m_edges.cbegin(), m_edges.cend(), [](auto const& pair) { return pair.second->hasCoil(); }); } - Size RoadNetwork::nIntersections() const { + std::size_t RoadNetwork::nIntersections() const { return std::count_if(m_nodes.cbegin(), m_nodes.cend(), [](auto const& pair) { return pair.second->isIntersection(); }); } - Size RoadNetwork::nRoundabouts() const { + std::size_t RoadNetwork::nRoundabouts() const { return std::count_if(m_nodes.cbegin(), m_nodes.cend(), [](auto const& pair) { return pair.second->isRoundabout(); }); } - Size RoadNetwork::nTrafficLights() const { + std::size_t RoadNetwork::nTrafficLights() const { return std::count_if(m_nodes.cbegin(), m_nodes.cend(), [](auto const& pair) { return pair.second->isTrafficLight(); }); @@ -815,6 +815,14 @@ namespace dsf::mobility { spdlog::debug("Done auto-assigning road priorities."); } + void RoadNetwork::describe(std::ostream& os) const { + os << "RoadNetwork with " << nNodes() << " nodes and " << nEdges() + << " edges. Total capacity: " << m_capacity << " vehicles.\n" + << "There are\n- " << nIntersections() << " intersections,\n- " << nTrafficLights() + << " traffic lights,\n- " << nRoundabouts() << " roundabouts,\n- " << nCoils() + << " coil sensors.\n"; + } + void RoadNetwork::adjustNodeCapacities() { double value; for (auto const& [_, pNode] : nodes()) { diff --git a/src/dsf/mobility/RoadNetwork.hpp b/src/dsf/mobility/RoadNetwork.hpp index e1eebaaa..42fb845d 100644 --- a/src/dsf/mobility/RoadNetwork.hpp +++ b/src/dsf/mobility/RoadNetwork.hpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -73,18 +74,18 @@ namespace dsf::mobility { RoadNetwork& operator=(RoadNetwork&&) = default; /// @brief Get the graph's number of coil streets - /// @return The number of coil streets - Size nCoils() const; + /// @return std::size_t The number of coil streets + std::size_t nCoils() const; /// @brief Get the graph's number of intersections - /// @return The number of intersections - Size nIntersections() const; + /// @return std::size_t The number of intersections + std::size_t nIntersections() const; /// @brief Get the graph's number of roundabouts - /// @return The number of roundabouts - Size nRoundabouts() const; + /// @return std::size_t The number of roundabouts + std::size_t nRoundabouts() const; /// @brief Get the graph's number of traffic lights - /// @return The number of traffic lights - Size nTrafficLights() const; + /// @return std::size_t The number of traffic lights + std::size_t nTrafficLights() const; /// @brief Adjust the nodes' transport capacity /// @details The nodes' capacity is adjusted using the graph's streets transport capacity, which may vary basing on the number of lanes. The node capacity will be set to the sum of the incoming streets' transport capacity. @@ -101,6 +102,10 @@ namespace dsf::mobility { /// @brief Automatically assigns road priorities at intersections, basing on road types void autoAssignRoadPriorities(); + /// @brief Describe the RoadNetwork + /// @param os The output stream to write the description to (default is std::cout) + void describe(std::ostream& os = std::cout) const; + /// @brief Import the graph's streets from a file /// @param fileName The name of the file to import the streets from. /// @details Supports csv, json and geojson file formats. diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index 9f14b8c7..c78f97ec 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "doctest.h" @@ -93,6 +94,21 @@ TEST_CASE("FirstOrderDynamics") { FirstOrderDynamics dynamics{defaultNetwork, false, 69}; THEN("The street has a coil") { CHECK(dynamics.graph().edge(8)->hasCoil()); } } + WHEN("We call summary") { + FirstOrderDynamics dynamics{defaultNetwork, false, 69}; + std::ostringstream oss; + dynamics.summary(oss); + std::string summaryStr = oss.str(); + THEN("The summary contains expected information") { + CHECK(summaryStr.find("RoadDynamics Summary") != std::string::npos); + CHECK(summaryStr.find("RoadNetwork with 120 nodes and 436 edges") != + std::string::npos); + CHECK(summaryStr.find("Number of inserted agents") != std::string::npos); + CHECK(summaryStr.find("Number of added agents") != std::string::npos); + CHECK(summaryStr.find("Number of killed agents") != std::string::npos); + CHECK(summaryStr.find("Current number of agents: 0") != std::string::npos); + } + } } } SUBCASE("setDestinationNodes") { diff --git a/test/mobility/Test_graph.cpp b/test/mobility/Test_graph.cpp index be71e140..4127a16d 100644 --- a/test/mobility/Test_graph.cpp +++ b/test/mobility/Test_graph.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "doctest.h" @@ -87,6 +88,15 @@ TEST_CASE("RoadNetwork") { CHECK(graph.edge(0, 2)); CHECK(graph.edge(0, 3)); CHECK(graph.edge(2, 3)); + // Test describe method + std::ostringstream oss; + graph.describe(oss); + std::string description = oss.str(); + CHECK(description.find("RoadNetwork with 4 nodes and 5 edges") != std::string::npos); + CHECK(description.find("intersections") != std::string::npos); + CHECK(description.find("traffic lights") != std::string::npos); + CHECK(description.find("roundabouts") != std::string::npos); + CHECK(description.find("coil sensors") != std::string::npos); } SUBCASE("automatically do things") {