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
21 changes: 11 additions & 10 deletions src/dsf/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::time_t>(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<std::time_t>(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,
Expand Down
4 changes: 2 additions & 2 deletions src/dsf/dsf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
#include <format>

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);
Expand Down
44 changes: 42 additions & 2 deletions src/dsf/mobility/Agent.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
std::optional<Id> m_nextStreetId;
size_t m_itineraryIdx;
double m_speed;
double m_distance; // Travelled distance
double m_distance; // Travelled distance
std::optional<double> m_maxDistance; // Maximum distance for stochastic agents
std::optional<std::time_t> m_maxTime; // Maximum time for stochastic agents

public:
/// @brief Construct a new Agent object
Expand Down Expand Up @@ -73,6 +75,15 @@
/// @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

Check warning

Code scanning / Cppcheck (reported by Codacy)

AST broken, ternary operator missing operand(s) Warning

AST broken, ternary operator missing operand(s)
: throw std::invalid_argument(
"Agent::setMaxDistance: maxDistance must be positive");
};
Comment on lines +78 to +82
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The setMaxDistance method is missing documentation. A doc comment should be added to describe what this method does, its parameter, and the exception it throws, following the pattern of other methods in this class.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +82
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The new setMaxDistance() method lacks test coverage. Consider adding tests to verify:

  1. Setting a valid positive max distance
  2. Attempting to set a non-positive max distance throws std::invalid_argument
  3. The value can be retrieved via maxDistance()

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +82
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The validation logic uses a ternary operator to throw an exception, which is unconventional and reduces readability. Consider using a traditional if-statement for consistency with other validation methods in the codebase (see setSpeed() in Agent.cpp as an example).

Suggested change
maxDistance > 0. ? m_maxDistance = maxDistance
: throw std::invalid_argument(
"Agent::setMaxDistance: maxDistance must be positive");
};
if (maxDistance > 0.) {
m_maxDistance = maxDistance;
} else {
throw std::invalid_argument("Agent::setMaxDistance: maxDistance must be positive");
}
}

Copilot uses AI. Check for mistakes.
/// @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:
Expand All @@ -97,6 +108,20 @@
/// @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");
Comment on lines +114 to +116
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The maxDistance() getter method documentation says it returns the agent's maximum distance "or throw std::logic_error if not set", but the actual implementation throws the error inline within the function using a ternary operator. This is unconventional. It would be clearer to use a normal if-else statement for better readability and to follow the pattern of other error-throwing methods in this codebase (see itineraryId() as an example).

Suggested change
return m_maxDistance.has_value()
? m_maxDistance.value()
: throw std::logic_error("Agent::maxDistance: maxDistance is not set");
if (m_maxDistance.has_value()) {
return m_maxDistance.value();
} else {
throw std::logic_error("Agent::maxDistance: maxDistance is not set");
}

Copilot uses AI. Check for mistakes.
};
Comment on lines +113 to +117
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The new maxDistance() getter method lacks test coverage. Consider adding tests to verify:

  1. Retrieving a valid max distance that was previously set
  2. Attempting to retrieve max distance when not set throws std::logic_error with the appropriate message

Copilot uses AI. Check for mistakes.
/// @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<Id> const& trip() const noexcept { return m_trip; };
Expand All @@ -118,8 +143,23 @@
/// @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<std::time_t> 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 <>
Expand Down
51 changes: 29 additions & 22 deletions src/dsf/mobility/RoadDynamics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ namespace dsf::mobility {
std::function<double(std::unique_ptr<Street> const&)> m_weightFunction;
std::optional<double> m_errorProbability;
std::optional<double> m_passageProbability;
double m_maxTravelDistance;
std::time_t m_maxTravelTime;
std::optional<double> m_meanTravelDistance;
std::optional<std::time_t> m_meanTravelTime;
double m_weightTreshold;
std::optional<double> m_timeToleranceFactor;
std::optional<delay_t> m_dataUpdatePeriod;
Expand Down Expand Up @@ -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
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The documentation states "If the mean distance is negative" but the validation checks if it's not positive (meanTravelDistance > 0.). This is inconsistent - the comment should say "If the mean distance is not positive" or the validation should check meanTravelDistance < 0. to allow zero values.

Suggested change
/// @throw std::invalid_argument If the mean distance is negative
/// @throw std::invalid_argument If the mean distance is not positive

Copilot uses AI. Check for mistakes.
inline void setMeanTravelDistance(double const meanTravelDistance) {
meanTravelDistance > 0. ? m_meanTravelDistance = meanTravelDistance
: throw std::invalid_argument(
"RoadDynamics::setMeanTravelDistance: "
"meanTravelDistance must be positive");
Comment on lines +158 to +161
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The validation logic uses a ternary operator to throw an exception, which is unconventional and reduces readability. Consider using a traditional if-statement for consistency with other validation methods in the codebase (see setSpeed() in Agent.cpp as an example).

Suggested change
meanTravelDistance > 0. ? m_meanTravelDistance = meanTravelDistance
: throw std::invalid_argument(
"RoadDynamics::setMeanTravelDistance: "
"meanTravelDistance must be positive");
if (meanTravelDistance > 0.) {
m_meanTravelDistance = meanTravelDistance;
} else {
throw std::invalid_argument(
"RoadDynamics::setMeanTravelDistance: meanTravelDistance must be positive");
}

Copilot uses AI. Check for mistakes.
};
Comment on lines +157 to 162
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The new setMeanTravelDistance() method lacks test coverage. Consider adding tests to verify:

  1. Setting a valid positive mean travel distance
  2. Attempting to set a non-positive value throws std::invalid_argument with the appropriate message
  3. The mean distance affects agent creation in addRandomAgents()

Copilot uses AI. Check for mistakes.
/// @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
Expand Down Expand Up @@ -398,8 +397,8 @@ namespace dsf::mobility {
m_previousOptimizationTime{0},
m_errorProbability{std::nullopt},
m_passageProbability{std::nullopt},
m_maxTravelDistance{std::numeric_limits<double>::max()},
m_maxTravelTime{std::numeric_limits<std::time_t>::max()},
m_meanTravelDistance{std::nullopt},
m_meanTravelTime{std::nullopt},
m_timeToleranceFactor{std::nullopt},
m_bCacheEnabled{useCache},
m_forcePriorities{false} {
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -1356,6 +1352,9 @@ namespace dsf::mobility {
void RoadDynamics<delay_t>::addRandomAgents(std::size_t nAgents,
TContainer const& spawnWeights) {
std::uniform_real_distribution<double> uniformDist{0., 1.};
std::exponential_distribution<double> distDist{1. /
m_meanTravelDistance.value_or(1.)};
Comment on lines +1355 to +1356
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The exponential distribution is created with the rate parameter 1. / m_meanTravelDistance.value_or(1.). If m_meanTravelDistance is not set, this creates a distribution with mean 1.0, and then the distance is sampled and assigned via setMaxDistance() at lines 1378-1381. However, the condition at line 1378 checks if (m_meanTravelDistance.has_value()), which means when m_meanTravelDistance is not set, no max distance is assigned to agents. This makes the value_or(1.) fallback in line 1359 unnecessary and misleading, since the distribution is only used when m_meanTravelDistance has a value. The distribution initialization should be moved inside the if-block at line 1378.

Copilot uses AI. Check for mistakes.
Comment on lines +1355 to +1356
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The variable name distDist (short for "distance distribution") is ambiguous and unclear. Consider a more descriptive name like travelDistanceDist or exponentialTravelDist to better indicate what this distribution represents.

Copilot uses AI. Check for mistakes.
std::exponential_distribution<double> timeDist{1. / m_meanTravelTime.value_or(1.)};
auto const bUniformSpawn{spawnWeights.empty()};
auto const bSingleSource{spawnWeights.size() == 1};
while (nAgents--) {
Expand All @@ -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));
}
}
}

Expand Down Expand Up @@ -2456,4 +2463,4 @@ namespace dsf::mobility {

file.close();
}
}; // namespace dsf::mobility
} // namespace dsf::mobility
26 changes: 26 additions & 0 deletions test/mobility/Test_agent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
53 changes: 53 additions & 0 deletions test/mobility/Test_dynamics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <chrono>
#include <cstdint>
Expand Down Expand Up @@ -1381,3 +1382,55 @@
// 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<Id, double> origins;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

MISRA 12.3 rule
for (const auto& [id, node] : defaultNetwork.nodes()) {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

MISRA 12.3 rule
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<Id, double> origins;

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

MISRA 12.3 rule
for (const auto& [id, node] : defaultNetwork.nodes()) {

Check notice

Code scanning / Cppcheck (reported by Codacy)

MISRA 12.3 rule Note test

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