diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index 3236af88..f42025c1 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -399,17 +399,18 @@ PYBIND11_MODULE(dsf_cpp, m) { }, pybind11::arg("dataUpdatePeriod"), dsf::g_docstrings.at("dsf::mobility::RoadDynamics::setDataUpdatePeriod").c_str()) - .def("setMaxDistance", - &dsf::mobility::FirstOrderDynamics::setMaxDistance, - pybind11::arg("maxDistance"), - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::setMaxDistance").c_str()) - .def( - "setMaxTravelTime", - [](dsf::mobility::FirstOrderDynamics& self, uint64_t maxTravelTime) { - self.setMaxTravelTime(static_cast(maxTravelTime)); + .def("setMeanTravelDistance", + &dsf::mobility::FirstOrderDynamics::setMeanTravelDistance, + pybind11::arg("meanDistance"), + dsf::g_docstrings.at("dsf::mobility::RoadDynamics::setMeanTravelDistance") + .c_str()) + .def( + "setMeanTravelTime", + [](dsf::mobility::FirstOrderDynamics& self, uint64_t meanTravelTime) { + self.setMeanTravelTime(static_cast(meanTravelTime)); }, - pybind11::arg("maxTravelTime"), - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::setMaxTravelTime").c_str()) + pybind11::arg("meanTravelTime"), + dsf::g_docstrings.at("dsf::mobility::RoadDynamics::setMeanTravelTime").c_str()) .def( "setErrorProbability", &dsf::mobility::FirstOrderDynamics::setErrorProbability, diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index a214c825..b17cc1fa 100644 --- a/src/dsf/dsf.hpp +++ b/src/dsf/dsf.hpp @@ -5,8 +5,8 @@ #include static constexpr uint8_t DSF_VERSION_MAJOR = 4; -static constexpr uint8_t DSF_VERSION_MINOR = 4; -static constexpr uint8_t DSF_VERSION_PATCH = 10; +static constexpr uint8_t DSF_VERSION_MINOR = 5; +static constexpr uint8_t DSF_VERSION_PATCH = 0; static auto const DSF_VERSION = std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH); diff --git a/src/dsf/mobility/Agent.hpp b/src/dsf/mobility/Agent.hpp index ea746dbb..616120da 100644 --- a/src/dsf/mobility/Agent.hpp +++ b/src/dsf/mobility/Agent.hpp @@ -34,7 +34,9 @@ namespace dsf::mobility { std::optional m_nextStreetId; size_t m_itineraryIdx; double m_speed; - double m_distance; // Travelled distance + double m_distance; // Travelled distance + std::optional m_maxDistance; // Maximum distance for stochastic agents + std::optional m_maxTime; // Maximum time for stochastic agents public: /// @brief Construct a new Agent object @@ -73,6 +75,15 @@ namespace dsf::mobility { /// @brief Update the agent's itinerary /// @details If possible, the agent's itinerary is updated by removing the first element /// from the itinerary's vector. + inline void setMaxDistance(double const maxDistance) { + maxDistance > 0. ? m_maxDistance = maxDistance + : throw std::invalid_argument( + "Agent::setMaxDistance: maxDistance must be positive"); + }; + /// @brief Set the agent's maximum time + /// @param maxTime The agent's maximum time + inline void setMaxTime(std::time_t const maxTime) { m_maxTime = maxTime; } + void updateItinerary(); /// @brief Reset the agent /// @details Reset the following values: @@ -97,6 +108,20 @@ namespace dsf::mobility { /// @return The agent's itinerary /// @throw std::logic_error if the agent is a random agent Id itineraryId() const; + /// @brief Get the agent's maximum distance + /// @return The agent's maximum distance, or throw std::logic_error if not set + inline auto maxDistance() const { + return m_maxDistance.has_value() + ? m_maxDistance.value() + : throw std::logic_error("Agent::maxDistance: maxDistance is not set"); + }; + /// @brief Get the agent's maximum time + /// @return The agent's maximum time, or throw std::logic_error if not set + inline auto maxTime() const { + return m_maxTime.has_value() + ? m_maxTime.value() + : throw std::logic_error("Agent::maxTime: maxTime is not set"); + }; /// @brief Get the agent's trip /// @return The agent's trip inline std::vector const& trip() const noexcept { return m_trip; }; @@ -118,8 +143,23 @@ namespace dsf::mobility { /// @brief Return true if the agent is a random agent /// @return True if the agent is a random agent, false otherwise inline bool isRandom() const noexcept { return m_trip.empty(); }; + /// @brief Check if a random agent has arrived at its destination + /// @param currentTime The current simulation time + /// @return True if the agent has arrived (exceeded max distance or time), false otherwise + inline bool hasArrived(std::optional const& currentTime) const noexcept { + if (!isRandom()) { + return false; + } + if (currentTime.has_value() && m_maxTime.has_value()) { + return (currentTime.value() - m_spawnTime) >= m_maxTime.value(); + } + if (m_maxDistance.has_value()) { + return m_distance >= m_maxDistance.value(); + } + return false; + }; }; -}; // namespace dsf::mobility +} // namespace dsf::mobility // Specialization of std::formatter for dsf::mobility::Agent template <> diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index 29c71a82..4f95bc93 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -63,8 +63,8 @@ namespace dsf::mobility { std::function const&)> m_weightFunction; std::optional m_errorProbability; std::optional m_passageProbability; - double m_maxTravelDistance; - std::time_t m_maxTravelTime; + std::optional m_meanTravelDistance; + std::optional m_meanTravelTime; double m_weightTreshold; std::optional m_timeToleranceFactor; std::optional m_dataUpdatePeriod; @@ -151,20 +151,19 @@ namespace dsf::mobility { inline void setDataUpdatePeriod(delay_t dataUpdatePeriod) noexcept { m_dataUpdatePeriod = dataUpdatePeriod; } - /// @brief Set the maximum distance which a random agent can travel - /// @param maxDistance The maximum distance - /// @throw std::invalid_argument If the maximum distance is negative - inline void setMaxDistance(double const maxDistance) { - if (maxDistance < 0.) { - throw std::invalid_argument(std::format( - "Maximum travel distance ({}) must be non-negative", maxDistance)); - } - m_maxTravelDistance = maxDistance; + /// @brief Set the mean distance travelled by a random agent. The distance will be sampled from an exponential distribution with this mean. + /// @param meanTravelDistance The mean distance + /// @throw std::invalid_argument If the mean distance is negative + inline void setMeanTravelDistance(double const meanTravelDistance) { + meanTravelDistance > 0. ? m_meanTravelDistance = meanTravelDistance + : throw std::invalid_argument( + "RoadDynamics::setMeanTravelDistance: " + "meanTravelDistance must be positive"); }; - /// @brief Set the maximum travel time which a random agent can travel - /// @param maxTravelTime The maximum travel time - inline void setMaxTravelTime(std::time_t const maxTravelTime) noexcept { - m_maxTravelTime = maxTravelTime; + /// @brief Set the mean travel time for random agents. The travel time will be sampled from an exponential distribution with this mean. + /// @param meanTravelTime The mean travel time + inline void setMeanTravelTime(std::time_t const meanTravelTime) noexcept { + m_meanTravelTime = meanTravelTime; }; /// @brief Set the origin nodes /// @param originNodes The origin nodes @@ -398,8 +397,8 @@ namespace dsf::mobility { m_previousOptimizationTime{0}, m_errorProbability{std::nullopt}, m_passageProbability{std::nullopt}, - m_maxTravelDistance{std::numeric_limits::max()}, - m_maxTravelTime{std::numeric_limits::max()}, + m_meanTravelDistance{std::nullopt}, + m_meanTravelTime{std::nullopt}, m_timeToleranceFactor{std::nullopt}, m_bCacheEnabled{useCache}, m_forcePriorities{false} { @@ -890,10 +889,7 @@ namespace dsf::mobility { spdlog::debug("Random agent {} has arrived at destination node {}", pAgentTemp->id(), destinationNode->id()); - } else if (pAgentTemp->distance() >= m_maxTravelDistance) { - bArrived = true; - } else if (!bArrived && - (this->time_step() - pAgentTemp->spawnTime() >= m_maxTravelTime)) { + } else if (pAgentTemp->hasArrived(this->time_step())) { bArrived = true; } } @@ -1356,6 +1352,9 @@ namespace dsf::mobility { void RoadDynamics::addRandomAgents(std::size_t nAgents, TContainer const& spawnWeights) { std::uniform_real_distribution uniformDist{0., 1.}; + std::exponential_distribution distDist{1. / + m_meanTravelDistance.value_or(1.)}; + std::exponential_distribution timeDist{1. / m_meanTravelTime.value_or(1.)}; auto const bUniformSpawn{spawnWeights.empty()}; auto const bSingleSource{spawnWeights.size() == 1}; while (nAgents--) { @@ -1374,6 +1373,14 @@ namespace dsf::mobility { } } } + if (m_meanTravelDistance.has_value()) { + auto const& pAgent{this->m_agents.back()}; + pAgent->setMaxDistance(distDist(this->m_generator)); + } + if (m_meanTravelTime.has_value()) { + auto const& pAgent{this->m_agents.back()}; + pAgent->setMaxTime(timeDist(this->m_generator)); + } } } @@ -2456,4 +2463,4 @@ namespace dsf::mobility { file.close(); } -}; // namespace dsf::mobility +} // namespace dsf::mobility diff --git a/test/mobility/Test_agent.cpp b/test/mobility/Test_agent.cpp index da1377ee..59ff9bfd 100644 --- a/test/mobility/Test_agent.cpp +++ b/test/mobility/Test_agent.cpp @@ -164,4 +164,30 @@ TEST_CASE("Agent formatting") { CHECK(formatted.find("nextStreetId: N/A") != std::string::npos); CHECK(formatted.find("itineraryId: 99") != std::string::npos); } + SUBCASE("hasArrived") { + Agent agent{10}; // spawnTime = 10 + CHECK(agent.isRandom()); + + // Test Max Distance + agent.setMaxDistance(100.0); + CHECK_FALSE(agent.hasArrived(std::nullopt)); + + agent.incrementDistance(99.0); + CHECK_FALSE(agent.hasArrived(std::nullopt)); + + agent.incrementDistance(1.0); + CHECK(agent.hasArrived(std::nullopt)); + + // Test Max Time + // Reset agent + agent = Agent{10}; + agent.setMaxTime(50); // Max duration 50. Should expire at 10+50=60. + + // Before expiration + CHECK_FALSE(agent.hasArrived(59)); + // At expiration + CHECK(agent.hasArrived(60)); + // After expiration + CHECK(agent.hasArrived(61)); + } } diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index 9c8ba04f..1e8c091c 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -3,6 +3,7 @@ #include "dsf/mobility/Itinerary.hpp" #include "dsf/mobility/Street.hpp" #include "dsf/mobility/Intersection.hpp" +#include "dsf/mobility/Agent.hpp" #include #include @@ -1381,3 +1382,55 @@ TEST_CASE("Stationary Weights Impact on Random Navigation") { // Check total agents preserved (some might be in transit or node if something went wrong) CHECK_EQ(countStreet1 + countStreet2, numAgents); } + +TEST_CASE("RoadDynamics Configuration") { + auto defaultNetwork = RoadNetwork{}; + // We need at least some nodes to add agents + // Assuming the data files exist where Test_dynamics.cpp expects them + defaultNetwork.importEdges((DATA_FOLDER / "manhattan_edges.csv").string()); + defaultNetwork.importNodeProperties((DATA_FOLDER / "manhattan_nodes.csv").string()); + + FirstOrderDynamics dynamics{defaultNetwork, false, 42}; + + SUBCASE("setMeanTravelDistance") { + CHECK_THROWS_AS(dynamics.setMeanTravelDistance(-1.0), std::invalid_argument); + CHECK_THROWS_AS(dynamics.setMeanTravelDistance(0.0), std::invalid_argument); + + dynamics.setMeanTravelDistance(1000.0); + + // Set origin nodes + std::unordered_map origins; + for (const auto& [id, node] : defaultNetwork.nodes()) { + origins[id] = 1.0; + } + dynamics.setOriginNodes(origins); + + dynamics.addRandomAgents(10); + + const auto& agents = dynamics.agents(); + CHECK(agents.size() == 10); + for (const auto& agent : agents) { + CHECK_NOTHROW(agent->maxDistance()); + CHECK(agent->maxDistance() >= 0.0); + } + } + + SUBCASE("setMeanTravelTime") { + dynamics.setMeanTravelTime(3600); + + std::unordered_map origins; + for (const auto& [id, node] : defaultNetwork.nodes()) { + origins[id] = 1.0; + } + dynamics.setOriginNodes(origins); + + dynamics.addRandomAgents(10); + + const auto& agents = dynamics.agents(); + CHECK(agents.size() == 10); + for (const auto& agent : agents) { + CHECK_NOTHROW(agent->maxTime()); + CHECK(agent->maxTime() >= 0); + } + } +}