diff --git a/CITATION.cff b/CITATION.cff index 4f2759d4..5571751d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,5 +12,5 @@ authors: repository-code: 'https://github.com/Grufoony/DynamicalSystemFramework' url: 'https://grufoony.github.io/DynamicalSystemFramework/' license: AGPL-3.0-only -version: 7.0.4 -date-released: '2026-09-15' +version: 7.1.0 +date-released: '2026-09-16' diff --git a/src/dsf/base/Node.hpp b/src/dsf/base/Node.hpp index 77d7e1ab..992738af 100644 --- a/src/dsf/base/Node.hpp +++ b/src/dsf/base/Node.hpp @@ -10,6 +10,7 @@ #include "../utility/queue.hpp" #include "../utility/Typedef.hpp" +#include #include #include #include diff --git a/src/dsf/base/PathCollection.cpp b/src/dsf/base/PathCollection.cpp index 1967fd4c..dfc53bcc 100644 --- a/src/dsf/base/PathCollection.cpp +++ b/src/dsf/base/PathCollection.cpp @@ -1,42 +1,73 @@ #include "PathCollection.hpp" #include +#include + +namespace { + std::list> explodeImpl(dsf::PathCollection const& collection, + dsf::Id const sourceId, + dsf::Id const targetId, + std::unordered_set& onStack); +} // namespace std::list> dsf::PathCollection::explode(Id const sourceId, Id const targetId) const { - std::list> paths; + std::unordered_set onStack; + return explodeImpl(*this, sourceId, targetId, onStack); +} - // Base case: if source equals target, return a path with just the source - if (sourceId == targetId) { - paths.push_back({sourceId}); - return paths; - } +namespace { + using dsf::Id; - // Check if sourceId exists in the map - auto it = this->find(sourceId); - if (it == this->end()) { - throw std::runtime_error( - std::format("Source {} not found in PathCollection", sourceId)); - } + std::list> explodeImpl(dsf::PathCollection const& collection, + Id const sourceId, + Id const targetId, + std::unordered_set& onStack) { + std::list> paths; + + // Base case: if source equals target, return a path with just the source + if (sourceId == targetId) { + paths.push_back({sourceId}); + return paths; + } + + // Check if sourceId exists in the map + auto it = collection.find(sourceId); + if (it == collection.end()) { + throw std::runtime_error( + std::format("Source {} not found in PathCollection", sourceId)); + } - auto const& nextHops = it->second; - - // For each possible next hop from sourceId - for (auto const& hop : nextHops) { - if (hop == targetId) { - // Direct path found - paths.push_back({sourceId, targetId}); - } else { - // Recursively find paths from hop to target - auto subPaths = explode(hop, targetId); - - // Prepend sourceId to each sub-path - for (auto& subPath : subPaths) { - subPath.insert(subPath.begin(), sourceId); - paths.push_back(std::move(subPath)); + // The hop graph is built to be acyclic, but a hand-assembled PathCollection need + // not be; without this guard a cycle recurses until the stack runs out. + if (!onStack.insert(sourceId).second) { + return paths; + } + struct StackGuard { + std::unordered_set& set; + Id id; + ~StackGuard() { set.erase(id); } + } guard{onStack, sourceId}; + + auto const& nextHops = it->second; + + // For each possible next hop from sourceId + for (auto const& hop : nextHops) { + if (hop == targetId) { + // Direct path found + paths.push_back({sourceId, targetId}); + } else { + // Recursively find paths from hop to target + auto subPaths = explodeImpl(collection, hop, targetId, onStack); + + // Prepend sourceId to each sub-path + for (auto& subPath : subPaths) { + subPath.insert(subPath.begin(), sourceId); + paths.push_back(std::move(subPath)); + } } } - } - return paths; -} \ No newline at end of file + return paths; + } +} // namespace \ No newline at end of file diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index 42b8d1f5..5bad9b68 100644 --- a/src/dsf/dsf.hpp +++ b/src/dsf/dsf.hpp @@ -8,8 +8,8 @@ #include static constexpr uint8_t DSF_VERSION_MAJOR = 7; -static constexpr uint8_t DSF_VERSION_MINOR = 0; -static constexpr uint8_t DSF_VERSION_PATCH = 4; +static constexpr uint8_t DSF_VERSION_MINOR = 1; +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/mdt/PointsCluster.cpp b/src/dsf/mdt/PointsCluster.cpp index 94aa393a..cf4db79f 100644 --- a/src/dsf/mdt/PointsCluster.cpp +++ b/src/dsf/mdt/PointsCluster.cpp @@ -46,16 +46,15 @@ namespace dsf::mdt { m_centroid = dsf::geometry::Point(compute_median(xs), compute_median(ys)); } - void PointsCluster::addActivityPoint(ActivityPoint const& activityPoint) noexcept { + void PointsCluster::addActivityPoint(ActivityPoint const& activityPoint) { m_points.emplace_back(activityPoint); m_bSorted = false; m_centroid.reset(); } - void PointsCluster::addPoint(std::time_t timestamp, - dsf::geometry::Point const& point) noexcept { + void PointsCluster::addPoint(std::time_t timestamp, dsf::geometry::Point const& point) { this->addActivityPoint(ActivityPoint{timestamp, point}); } - void PointsCluster::sort() const noexcept { + void PointsCluster::sort() const { if (m_bSorted) { return; } diff --git a/src/dsf/mdt/PointsCluster.hpp b/src/dsf/mdt/PointsCluster.hpp index 6899f5d0..f7b5f641 100644 --- a/src/dsf/mdt/PointsCluster.hpp +++ b/src/dsf/mdt/PointsCluster.hpp @@ -16,7 +16,7 @@ namespace dsf::mdt { private: mutable std::vector m_points; mutable std::optional m_centroid; - mutable bool m_bSorted; + mutable bool m_bSorted{true}; /// @brief Update the centroid of the cluster based on current activity points. /// The centroid is computed as the median of the x and y coordinates of the points. /// @throws std::runtime_error if the cluster is empty. @@ -34,13 +34,13 @@ namespace dsf::mdt { PointsCluster& operator=(PointsCluster const& other) = default; /// @brief Add an activity point to the cluster. /// @param activityPoint The activity point to add. - void addActivityPoint(ActivityPoint const& activityPoint) noexcept; + void addActivityPoint(ActivityPoint const& activityPoint); /// @brief Add a point with timestamp to the cluster. /// @param timestamp The timestamp of the activity point. /// @param point The geometric point of the activity point. - void addPoint(std::time_t timestamp, dsf::geometry::Point const& point) noexcept; + void addPoint(std::time_t timestamp, dsf::geometry::Point const& point); /// @brief Sort the activity points in the cluster by timestamp. - void sort() const noexcept; + void sort() const; /// @brief Compute and return the centroid of the cluster. /// @return The centroid point of the cluster. dsf::geometry::Point centroid() const; diff --git a/src/dsf/mdt/Trajectory.cpp b/src/dsf/mdt/Trajectory.cpp index 50039a6f..a097383a 100644 --- a/src/dsf/mdt/Trajectory.cpp +++ b/src/dsf/mdt/Trajectory.cpp @@ -97,7 +97,7 @@ namespace dsf::mdt { } } - void Trajectory::sort() noexcept { + void Trajectory::sort() { if (m_bSorted) { return; } diff --git a/src/dsf/mdt/Trajectory.hpp b/src/dsf/mdt/Trajectory.hpp index 71a0f5ff..6a81f226 100644 --- a/src/dsf/mdt/Trajectory.hpp +++ b/src/dsf/mdt/Trajectory.hpp @@ -14,7 +14,7 @@ namespace dsf::mdt { class Trajectory { private: std::vector m_points; - bool m_bSorted; + bool m_bSorted{true}; public: Trajectory() = default; @@ -34,7 +34,7 @@ namespace dsf::mdt { /// @param max_speed_kph The max allowed speed (in km/h) to consider a cluster as a stop point. void filter(double const cluster_radius_km, double const max_speed_kph); /// @brief Sort the trajectory points by timestamp. - void sort() noexcept; + void sort(); /// @brief Get the number of points in the trajectory. /// @return The size of the trajectory. inline std::size_t size() const noexcept { return m_points.size(); } diff --git a/src/dsf/mdt/TrajectoryCollection.cpp b/src/dsf/mdt/TrajectoryCollection.cpp index 785863c1..c4040df9 100644 --- a/src/dsf/mdt/TrajectoryCollection.cpp +++ b/src/dsf/mdt/TrajectoryCollection.cpp @@ -112,14 +112,10 @@ namespace dsf::mdt { (currentCluster.lastTimestamp() + currentCluster.firstTimestamp()) * 0.5; auto const previous_time = (previousCluster.lastTimestamp() + previousCluster.firstTimestamp()) * 0.5; - if (current_time < previous_time) { - // Should never happen if data is clean - throw std::runtime_error( - "Timestamps are not in increasing order within the trajectory."); - } - if (current_time == previous_time) { + if (current_time <= previous_time) { spdlog::debug( - "Non-increasing timestamps detected. Skipping speed check for these points."); + "Non-increasing cluster midpoints detected. Skipping the speed check for " + "these clusters."); return true; } auto const speed_kph = @@ -217,13 +213,14 @@ namespace dsf::mdt { if (!bShouldSplit) { bShouldSplit = !check_min_duration(currentCluster); } - // If constraint violated (max speed or min duration) - finalize current trajectory and start a new one + // If a constraint is violated, finalise the current trajectory and start a + // fresh one. The next iteration adds points[i]; re-adding currentCluster here + // would duplicate it across the two segments. if (bShouldSplit && !newTrajectory.empty()) { if (newTrajectory.size() >= min_points_per_trajectory) { trajectories.emplace_back(std::move(newTrajectory)); } newTrajectory = Trajectory(); - newTrajectory.addCluster(currentCluster); } } if (newTrajectory.size() >= min_points_per_trajectory) { diff --git a/src/dsf/mobility/Agent.cpp b/src/dsf/mobility/Agent.cpp index be16e8b1..3663110b 100644 --- a/src/dsf/mobility/Agent.cpp +++ b/src/dsf/mobility/Agent.cpp @@ -67,7 +67,7 @@ namespace dsf::mobility { m_distance += distance; } void Agent::updateItinerary() { - if (m_itineraryIdx < m_trip.size() - 1) { + if (!m_trip.empty() && m_itineraryIdx < m_trip.size() - 1) { ++m_itineraryIdx; } } @@ -75,6 +75,9 @@ namespace dsf::mobility { m_spawnTime = spawnTime; m_freeTime = 0; m_streetId = std::nullopt; + // NOTE: m_nextStreetId is deliberately preserved. FirstOrderDynamics' + // reinsertion path (m_reinsertAgents) relies on it to know where to put the + // agent back; clearing it here makes m_evolveAgents kill the agent instead. m_speed = 0.; m_distance = 0.; m_itineraryIdx = 0; diff --git a/src/dsf/mobility/FirstOrderDynamics.cpp b/src/dsf/mobility/FirstOrderDynamics.cpp index 0e029a9d..a8a2146d 100644 --- a/src/dsf/mobility/FirstOrderDynamics.cpp +++ b/src/dsf/mobility/FirstOrderDynamics.cpp @@ -432,18 +432,19 @@ namespace dsf::mobility { std::format("The only source node {} is also the only destination node.", std::get(m_origins.at(0)))); } - std::uniform_int_distribution nodeDist{ - 0, static_cast(this->graph().nNodes() - 1)}; std::uniform_real_distribution uniformDist{0., 1.}; spdlog::debug("Adding {} agents at time {}.", nAgents, this->time_step()); while (nAgents--) { std::optional srcId{std::nullopt}, dstId{std::nullopt}; - // Select source using weighted random selection + // Select source using weighted random selection. Origins and destinations are + // street ids, so the fallback must stay within them: picking a random *node* + // here would hand a node id to addAgent's source-street parameter. if (nSources == 1) { srcId = std::get(m_origins.at(0)); } else { auto randValue = uniformDist(this->m_generator); + srcId = std::get(m_origins.back()); for (const auto& [id, weight] : m_origins) { if (randValue < weight) { srcId = id; @@ -458,6 +459,7 @@ namespace dsf::mobility { dstId = std::get(m_destinations.at(0)); } else { auto randValue = uniformDist(this->m_generator); + dstId = std::get(m_destinations.back()); for (const auto& [id, weight] : m_destinations) { if (randValue < weight) { dstId = id; @@ -467,18 +469,6 @@ namespace dsf::mobility { } } - // Fallback to random nodes if selection failed - if (!srcId.has_value()) { - auto nodeIt{this->graph().nodes().begin()}; - std::advance(nodeIt, nodeDist(this->m_generator)); - srcId = nodeIt->first; - } - if (!dstId.has_value()) { - auto nodeIt{this->graph().nodes().begin()}; - std::advance(nodeIt, nodeDist(this->m_generator)); - dstId = nodeIt->first; - } - // Find the itinerary with the given destination auto itineraryIt{std::find_if(this->itineraries().cbegin(), this->itineraries().cend(), @@ -826,6 +816,19 @@ namespace dsf::mobility { weights.push_back(0.); } } + if (validLanes.empty()) { + // No lane of this street serves the required direction: fall back to a + // uniform choice rather than normalising an all-zero weight vector. + spdlog::debug( + "No lane on street {} maps direction {}; falling back to a uniform lane " + "choice.", + pStreet->id(), + directionToString.at(static_cast(direction))); + std::uniform_int_distribution fallbackDist{ + 0, static_cast(nLanes - 1)}; + pStreet->enqueue(fallbackDist(this->m_generator)); + continue; + } // If all weights are the same, make the last 0 if (std::all_of(weights.begin(), weights.end(), [&](double w) { return std::abs(w - weights.front()) < std::numeric_limits::epsilon(); @@ -837,6 +840,12 @@ namespace dsf::mobility { } // Normalize the weights auto const sum = std::accumulate(weights.begin(), weights.end(), 0.); + if (!(sum > 0.)) { + std::uniform_int_distribution fallbackDist{ + 0, static_cast(nLanes - 1)}; + pStreet->enqueue(fallbackDist(this->m_generator)); + continue; + } for (auto& w : weights) { w /= sum; } @@ -849,11 +858,14 @@ namespace dsf::mobility { if (pStreet->queue(queueIndex).empty()) { continue; } - if (uniformDist(this->m_generator) > transportCapacity) { - spdlog::trace("Skipping due to transport capacity {} < random {}", - transportCapacity, - uniformDist(this->m_generator)); - continue; + { + auto const rndValue{uniformDist(this->m_generator)}; + if (rndValue > transportCapacity) { + spdlog::trace("Skipping due to transport capacity {} < random {}", + transportCapacity, + rndValue); + continue; + } } // Logger::debug("Taking temp agent"); auto const& pAgentTemp{pStreet->queue(queueIndex).front()}; @@ -1079,6 +1091,13 @@ namespace dsf::mobility { } else if (destinationNode->isRoundabout()) { auto& roundabout = dynamic_cast(*destinationNode); roundabout.enqueue(std::move(pAgent)); + } else { + spdlog::warn( + "{} cannot host agents (it is neither an intersection nor a roundabout). " + "Killing the agent coming from street {}.", + *destinationNode, + pStreet->id()); + this->m_removeAgent(std::move(pAgent)); } } } @@ -1233,6 +1252,13 @@ namespace dsf::mobility { } else if (pSourceNode->isRoundabout()) { auto& roundabout = dynamic_cast(*pSourceNode); roundabout.enqueue(std::move(pAgent)); + } else { + spdlog::warn( + "{} cannot host agents (it is neither an intersection nor a roundabout). " + "Killing agent {}.", + *pSourceNode, + pAgent->id()); + this->m_removeAgent(std::move(pAgent)); } itAgent = m_agents.erase(itAgent); } @@ -1283,6 +1309,11 @@ namespace dsf::mobility { m_origins.reserve(origins.size()); if (origins.empty()) { // If no origin nodes are provided, try to set origin nodes basing on streets' stationary weights + if (this->graph().nEdges() == 0) { + throw std::runtime_error( + "FirstOrderDynamics::setOrigins: no origins given and the graph has no " + "edges to fall back on."); + } auto const UNIFORM_WEIGHT{1. / this->graph().nEdges()}; for (auto const& edgePair : this->graph().edges()) { m_origins.push_back({edgePair.first, UNIFORM_WEIGHT}); @@ -1395,6 +1426,9 @@ namespace dsf::mobility { m_itineraries.clear(); auto const N{destinations.size()}; m_destinations.clear(); + if (N == 0) { + return; + } m_destinations.reserve(N); double const UNIFORM_WEIGHT{1. / N}; std::for_each(destinations.begin(), @@ -1552,6 +1586,9 @@ namespace dsf::mobility { bool const bRandomItinerary{!optItineraryId.has_value() && !this->itineraries().empty()}; std::shared_ptr pItinerary; + if (optItineraryId.has_value()) { + pItinerary = this->itineraries().at(*optItineraryId); + } std::uniform_int_distribution itineraryDist{ 0, this->itineraries().size() - 1}; std::uniform_int_distribution streetDist{0, this->graph().nEdges() - 1}; @@ -1711,8 +1748,17 @@ namespace dsf::mobility { } } } - for (auto& [direction, value] : m_queuesAtTrafficLights.at(inEdgeId)) { - value += pStreet->nExitingAgents(direction, true); + auto const itQueues{m_queuesAtTrafficLights.find(inEdgeId)}; + if (itQueues == m_queuesAtTrafficLights.end()) { + spdlog::debug( + "Street {} is green in no phase of traffic light {}: skipping " + "queue-data collection for it.", + inEdgeId, + pNode->id()); + } else { + for (auto& [direction, value] : itQueues->second) { + value += pStreet->nExitingAgents(direction, true); + } } } m_evolveStreet(pStreet); @@ -1806,12 +1852,14 @@ namespace dsf::mobility { std::sqrt(std::max(0.0, std_speed.load() / averageStats.nValidEdges - averageStats.meanSpeed * averageStats.meanSpeed)); + averageStats.meanTravelTime = mean_traveltime.load() / averageStats.nValidEdges; + } + if (edgeCount > 0.) { averageStats.meanDensity = mean_density.load() / edgeCount; averageStats.stdDensity = std::sqrt( std::max(0.0, std_density.load() / edgeCount - averageStats.meanDensity * averageStats.meanDensity)); - averageStats.meanTravelTime = mean_traveltime.load() / averageStats.nValidEdges; averageStats.meanQueueLength = mean_queue_length.load() / edgeCount; } stepData.averageStats = averageStats; @@ -2002,6 +2050,9 @@ namespace dsf::mobility { density += pStreet->density(); ++n; } + if (n == 0.) { + continue; + } density /= n; densities[nodeId] = density; } diff --git a/src/dsf/mobility/FirstOrderDynamics.hpp b/src/dsf/mobility/FirstOrderDynamics.hpp index e0eb4156..b2a746a1 100644 --- a/src/dsf/mobility/FirstOrderDynamics.hpp +++ b/src/dsf/mobility/FirstOrderDynamics.hpp @@ -579,13 +579,16 @@ namespace dsf::mobility { } while (nAgents--) { auto randValue{uniformDist(this->m_generator)}; + // Fall back to the last origin + Id selectedOrigin{std::get<0>(m_origins.back())}; for (auto const& [origin, weight] : m_origins) { if (randValue < weight) { - this->addAgent(nullptr, origin); + selectedOrigin = origin; break; } randValue -= weight; } + this->addAgent(nullptr, selectedOrigin); if (m_meanTravelDistance.has_value()) { this->m_agents.back()->setMaxDistance(distDist(this->m_generator)); } @@ -602,6 +605,9 @@ namespace dsf::mobility { m_itineraries.clear(); auto const N{destinations.size()}; m_destinations.clear(); + if (N == 0) { + return; + } m_destinations.reserve(N); double const UNIFORM_WEIGHT{1. / N}; std::for_each(destinations.begin(), diff --git a/src/dsf/mobility/Intersection.cpp b/src/dsf/mobility/Intersection.cpp index fe8fa091..730fa3ca 100644 --- a/src/dsf/mobility/Intersection.cpp +++ b/src/dsf/mobility/Intersection.cpp @@ -1,5 +1,6 @@ #include "Intersection.hpp" +#include #include namespace dsf::mobility { @@ -22,10 +23,18 @@ namespace dsf::mobility { } void Intersection::addAgent(std::unique_ptr pAgent) { - int lastKey{0}; + if (isFull()) { + throw std::runtime_error(std::format("{} is full.", *this)); + } + int16_t lastKey{0}; if (!m_agents.empty()) { - lastKey = m_agents.rbegin()->first + 1; + lastKey = m_agents.rbegin()->first; + if (lastKey == std::numeric_limits::max()) { + throw std::runtime_error(std::format( + "{} cannot order any further agent: the queue key saturated.", *this)); + } + ++lastKey; } - addAgent(static_cast(lastKey), std::move(pAgent)); + m_agents.emplace(lastKey, std::move(pAgent)); } } // namespace dsf::mobility \ No newline at end of file diff --git a/src/dsf/mobility/Intersection.hpp b/src/dsf/mobility/Intersection.hpp index 78e48e67..9bc0cee9 100644 --- a/src/dsf/mobility/Intersection.hpp +++ b/src/dsf/mobility/Intersection.hpp @@ -89,7 +89,7 @@ namespace dsf::mobility { } /// @brief Returns true if the node is full /// @return bool True if the node is full - inline bool isFull() const override { return this->nAgents() == this->capacity(); } + inline bool isFull() const override { return this->nAgents() >= this->capacity(); } /// @brief Get the node's street priorities /// @details This function returns a std::set containing the node's street priorities. diff --git a/src/dsf/mobility/Road.hpp b/src/dsf/mobility/Road.hpp index 4b2f014a..127bb47c 100644 --- a/src/dsf/mobility/Road.hpp +++ b/src/dsf/mobility/Road.hpp @@ -106,7 +106,7 @@ namespace dsf::mobility { double density() const noexcept; /// @brief Check if the road is full /// @return bool, True if the road is full, false otherwise - inline bool isFull() const final { return this->nAgents() == this->capacity(); } + inline bool isFull() const final { return this->nAgents() >= this->capacity(); } /// @brief Check if the road is active (i.e., open) /// @return bool, True if the road is active, false otherwise inline bool isActive() const final { return m_roadStatus == RoadStatus::OPEN; } diff --git a/src/dsf/mobility/RoadNetwork.cpp b/src/dsf/mobility/RoadNetwork.cpp index e1884037..746305f3 100644 --- a/src/dsf/mobility/RoadNetwork.cpp +++ b/src/dsf/mobility/RoadNetwork.cpp @@ -674,7 +674,7 @@ namespace dsf::mobility { return; } auto const& inNeighbours = pNode->ingoingEdges(); - std::map> capacities; + std::map> capacities; std::unordered_map streetAngles; std::unordered_map maxSpeeds; std::unordered_map nLanes; @@ -724,8 +724,8 @@ namespace dsf::mobility { std::unordered_map counts; for (auto const& [streetId, name] : streetNames) { if (name.empty()) { - // Ignore empty names - return; + // Ignore empty names, but keep initialising the other streets. + continue; } if (!counts.contains(name)) { counts[name] = 1; @@ -776,14 +776,12 @@ namespace dsf::mobility { std::sort(sortedAngles.begin(), sortedAngles.end(), [](auto const& a, auto const& b) { return a.second < b.second; }); - streetAngles.clear(); - for (auto const& [streetId, angle] : sortedAngles) { - streetAngles.emplace(streetId, angle); - } - auto const& streetId = streetAngles.begin()->first; - auto const& angle = streetAngles.begin()->second; - for (auto const& [streetId2, angle2] : streetAngles) { + // Iterate the *sorted* sequence: streetAngles is unordered, so its + // begin() would hand back an arbitrary street. + auto const& streetId = sortedAngles.front().first; + auto const& angle = sortedAngles.front().second; + for (auto const& [streetId2, angle2] : sortedAngles) { if (std::abs(angle - angle2) > 0.75 * std::numbers::pi) { tl.addStreetPriority(streetId); tl.addStreetPriority(streetId2); @@ -791,17 +789,32 @@ namespace dsf::mobility { } } } - if (tl.streetPriorities().empty() || tl.streetPriorities().size() != 2) { + if (tl.streetPriorities().size() != 2) { spdlog::warn("Failed to auto-init Traffic Light {} - going random", pNode->id()); - // Assign first and third keys of capacity map - auto it = capacities.begin(); - auto const& firstKey = it->first; - ++it; - ++it; - auto const& thirdKey = it->first; - tl.addStreetPriority(firstKey); - tl.addStreetPriority(thirdKey); + // Fall back to the first and third ingoing streets by capacity. + if (capacities.size() < 3) { + spdlog::warn( + "Traffic Light {} has only {} distinct ingoing streets - downgrading " + "to a plain intersection.", + pNode->id(), + capacities.size()); + pNode = std::make_unique(*pNode); + return; + } + std::vector byCapacity; + byCapacity.reserve(capacities.size()); + for (auto const& [streetId, capacity] : capacities) { + byCapacity.push_back(streetId); + } + std::sort(byCapacity.begin(), + byCapacity.end(), + [&capacities](Id const a, Id const b) { + return capacities.at(a) > capacities.at(b); + }); + tl.setStreetPriorities({}); + tl.addStreetPriority(byCapacity[0]); + tl.addStreetPriority(byCapacity[2]); } // Build two phases: priority streets (phase 0) and non-priority (phase 1). @@ -1041,16 +1054,25 @@ namespace dsf::mobility { // pInStreet->target(), // nLanes, // allowedTurns.size())); - assert(allowedTurns.size() == static_cast(nLanes)); + if (allowedTurns.size() != static_cast(nLanes)) { + spdlog::warn( + "Street {} -> {} has {} lanes but {} allowed turns: truncating the " + "lane mapping to the lane count.", + pInStreet->source(), + pInStreet->target(), + nLanes, + allowedTurns.size()); + } // Logger::info( // std::format("Street {}->{} with {} lanes and {} allowed turns", // pInStreet->source(), // pInStreet->target(), // nLanes, // allowedTurns.size())); - std::vector newMapping(nLanes); + std::vector newMapping(nLanes, Direction::ANY); auto it{allowedTurns.cbegin()}; - for (size_t i{0}; i < allowedTurns.size(); ++i, ++it) { + for (size_t i{0}; i < newMapping.size() && it != allowedTurns.cend(); + ++i, ++it) { newMapping[i] = *it; } // If the last one is RIGHTANDSTRAIGHT, move it in front @@ -1181,7 +1203,7 @@ namespace dsf::mobility { auto* pStreet{&this->edge(edgeId)}; value += pStreet->nLanes() * pStreet->transportCapacity(); } - pNode->setCapacity(value); + pNode->setCapacity(static_cast(value)); value = 0.; for (auto const& edgeId : pNode->outgoingEdges()) { auto* pStreet{&this->edge(edgeId)}; @@ -1189,7 +1211,11 @@ namespace dsf::mobility { } pNode->setTransportCapacity(value == 0. ? 1. : value); if (pNode->capacity() == 0) { - pNode->setCapacity(value); + // Falling back to the outgoing sum, and ultimately to 1: a zero capacity makes + // isFull() permanently true (the node would never accept an agent) and turns + // density() into a division by zero. + pNode->setCapacity(value > 0. ? static_cast(value) + : static_cast(1)); } } } @@ -1280,6 +1306,17 @@ namespace dsf::mobility { } } + if (secondaryGreenTime == 0) { + // Every street shares the full cycle: a zero-duration second phase would be + // rejected by setPhases(), so emit the single phase instead. + spdlog::debug( + "importTrafficLights: node {} has a single {}-tick phase covering the whole " + "cycle.", + nodeId, + firstGreenTime); + tl.setPhases({phase0}); + continue; + } tl.setPhases({phase0, phase1}); spdlog::debug( "importTrafficLights: node {} → phase0 ({} ticks, {} streets) + " @@ -1412,6 +1449,7 @@ namespace dsf::mobility { } catch (const std::out_of_range&) { throw std::out_of_range(std::format("Street with id {} not found", streetId)); } + m_updateMaxAgentCapacity(); } void RoadNetwork::changeStreetNLanesByName(std::string const& streetName, int const nLanes, @@ -1427,6 +1465,7 @@ namespace dsf::mobility { ++nAffectedRoads; } }); + m_updateMaxAgentCapacity(); spdlog::info( "Changed number of lanes to {} for {} streets with name containing " "\"{}\"", @@ -1442,6 +1481,7 @@ namespace dsf::mobility { } catch (const std::out_of_range&) { throw std::out_of_range(std::format("Street with id {} not found", streetId)); } + m_updateMaxAgentCapacity(); } void RoadNetwork::changeStreetCapacityByName(std::string const& streetName, double const factor) { @@ -1456,6 +1496,7 @@ namespace dsf::mobility { ++nAffectedRoads; } }); + m_updateMaxAgentCapacity(); spdlog::info( "Changed capacity by factor {} to {} streets with name containing \"{}\"", factor, diff --git a/src/dsf/mobility/RoadNetwork.hpp b/src/dsf/mobility/RoadNetwork.hpp index f244e888..727ffb21 100644 --- a/src/dsf/mobility/RoadNetwork.hpp +++ b/src/dsf/mobility/RoadNetwork.hpp @@ -111,7 +111,7 @@ namespace dsf::mobility { /// @brief Automatically assigns road priorities at intersections, basing on road types void autoAssignRoadPriorities(); /// @brief Set the edge weight function based on a string identifier - /// @param strv_weight The string identifier of the weight function. Supported values are "travelTime", "length" and any custom attribute name. + /// @param strv_weight The string identifier of the weight function. Supported values are "traveltime", "length" and any custom attribute name. /// @param threshold An optional threshold to apply to the weight function. The effective weight will be weight * (1 + threshold). This can be used to increase the weight of certain paths and thus make them less likely to be chosen by agents when using a weight-based path update strategy. void setEdgeWeight(std::string_view const strv_weight, std::optional const threshold = std::nullopt) final; diff --git a/src/dsf/mobility/Roundabout.hpp b/src/dsf/mobility/Roundabout.hpp index 3120e9ad..7ff7ca0c 100644 --- a/src/dsf/mobility/Roundabout.hpp +++ b/src/dsf/mobility/Roundabout.hpp @@ -50,7 +50,7 @@ namespace dsf::mobility { } /// @brief Returns true if the node is full /// @return bool True if the node is full - bool isFull() const override { return m_agents.size() == this->capacity(); } + bool isFull() const override { return m_agents.size() >= this->capacity(); } /// @brief Returns true if the node is a roundabout /// @return bool True if the node is a roundabout constexpr bool isRoundabout() const noexcept final { return true; } diff --git a/src/dsf/mobility/Street.cpp b/src/dsf/mobility/Street.cpp index 7fa43884..144cee58 100644 --- a/src/dsf/mobility/Street.cpp +++ b/src/dsf/mobility/Street.cpp @@ -105,8 +105,19 @@ namespace dsf::mobility { "Changing number of lanes for {} from {} to {}", *this, m_nLanes, nLanes); m_capacity = static_cast(m_capacity * static_cast(nLanes) / m_nLanes); m_transportCapacity = m_transportCapacity * nLanes / m_nLanes; + auto const previousNLanes = m_nLanes; m_nLanes = nLanes; m_maxSpeed = m_maxSpeed * speedFactor.value_or(1.0); + if (nLanes < previousNLanes) { + // Resizing down would destroy the agents queued in the removed lanes without + // anyone noticing. Move them to the last surviving lane instead. + auto& survivingQueue{m_exitQueues[static_cast(nLanes) - 1]}; + for (auto i{static_cast(nLanes)}; i < m_exitQueues.size(); ++i) { + while (!m_exitQueues[i].empty()) { + survivingQueue.push(m_exitQueues[i].extract_front()); + } + } + } m_exitQueues.resize(m_nLanes); m_updateLaneMapping(m_nLanes); } @@ -156,9 +167,16 @@ namespace dsf::mobility { assert(!m_exitQueues[index].empty()); auto pAgent{m_exitQueues[index].extract_front()}; // Keep track of average speed - auto const insertionTime{m_agentsInsertionTimes[pAgent->id()]}; - m_avgSpeeds.push_back(m_length / (currentTime - insertionTime)); - m_agentsInsertionTimes.erase(pAgent->id()); + auto const itInsertion{m_agentsInsertionTimes.find(pAgent->id())}; + auto const insertionTime{ + itInsertion == m_agentsInsertionTimes.cend() ? currentTime : itInsertion->second}; + // An agent leaving in the same time-step it entered would divide by zero. + if (currentTime > insertionTime) { + m_avgSpeeds.push_back(m_length / (currentTime - insertionTime)); + } + if (itInsertion != m_agentsInsertionTimes.cend()) { + m_agentsInsertionTimes.erase(itInsertion); + } if (m_agentData.has_value()) { m_agentData->operator[](m_id).emplace_back( pAgent->id(), insertionTime, currentTime); @@ -188,6 +206,7 @@ namespace dsf::mobility { ++n; } else if (m_laneMapping[i] == direction) { nAgents += m_exitQueues[i].size(); + ++n; } else if (m_laneMapping[i] == Direction::RIGHTANDSTRAIGHT && (direction == Direction::RIGHT || direction == Direction::STRAIGHT)) { nAgents += m_exitQueues[i].size(); @@ -208,8 +227,8 @@ namespace dsf::mobility { ++n; } } - if (normalizeOnNLanes) { - n > 1 ? nAgents /= n : nAgents; + if (normalizeOnNLanes && n > 1) { + nAgents /= n; } return nAgents; } diff --git a/src/dsf/mobility/TrafficSimulator.cpp b/src/dsf/mobility/TrafficSimulator.cpp index 6e154c13..434a8a21 100644 --- a/src/dsf/mobility/TrafficSimulator.cpp +++ b/src/dsf/mobility/TrafficSimulator.cpp @@ -12,6 +12,46 @@ #include namespace dsf::mobility { + namespace { + /// @brief Percentage of @p value over @p total, or 0 when @p total is 0. + double percentOf(std::size_t const value, std::size_t const total) { + return total == 0 ? 0. + : static_cast(value) * 100. / static_cast(total); + } + } // namespace + + void TrafficSimulator::m_logRunSummary(std::size_t const nAdded, + std::size_t const nInserted, + std::size_t const nArrived, + std::size_t const nKilled, + std::size_t const nRemaining) const { + // These counters need not add up: the ghost-clearing paths in FirstOrderDynamics + // decrement the live count without bumping the killed count. Do the subtraction in + // a signed type so an imbalance shows up as a negative number instead of wrapping + // around to ~1.8e19. + auto const accounted = static_cast(nArrived) + + static_cast(nKilled) + + static_cast(nRemaining); + auto const nGhosts = static_cast(nInserted) - accounted; + spdlog::info( + "Simulation completed. Total agents added: {}\n\tInserted: {} " + "({:.2f}%)\n\tArrived: {} ({:.2f}%)\n\tKilled: {} ({:.2f}%)\n\tGhosts: {} " + "({:.2f}%)\n\tRemaining: {} ({:.2f}%).", + nAdded, + nInserted, + percentOf(nInserted, nAdded), + nArrived, + percentOf(nArrived, nInserted), + nKilled, + percentOf(nKilled, nInserted), + nGhosts, + nInserted == 0 + ? 0. + : static_cast(nGhosts) * 100. / static_cast(nInserted), + nRemaining, + percentOf(nRemaining, nInserted)); + } + void TrafficSimulator::m_createId() { // Take the current time and set id as YYYYMMDDHHMMSS auto const now = std::chrono::system_clock::now(); @@ -339,13 +379,22 @@ namespace dsf::mobility { std::optional const deltaT, double const percentRandomAgents) { if (deltaT.has_value()) { - if (m_endTime == 0) { + if (deltaT.value() <= 0) { + throw std::invalid_argument(std::format( + "Agent insertion delta time ({}) must be positive.", deltaT.value())); + } + auto const scheduleEndTime = static_cast( + m_initTime + nAgentsPerTimeStep.size() * deltaT.value()); + if (m_endTime != 0 && m_endTime != scheduleEndTime) { spdlog::warn( - "Delta time for agent insertion is set to {} seconds, but no end time is " - "currently set. The end time will be ignored for agent insertion timing.", - deltaT.value()); + "The configured end time ({}) does not match the agent insertion schedule " + "({} insertions every {} seconds). Using the schedule's end time ({}).", + m_timeToStr(m_endTime), + nAgentsPerTimeStep.size(), + deltaT.value(), + m_timeToStr(scheduleEndTime)); } - m_endTime = m_initTime + nAgentsPerTimeStep.size() * deltaT.value(); + m_endTime = scheduleEndTime; } std::time_t agentInsertionDeltaT = deltaT.value_or(0); @@ -354,13 +403,19 @@ namespace dsf::mobility { "Cannot run the simulation without an agent insertion schedule."); } + if (m_endTime != 0 && m_endTime < m_initTime) { + throw std::runtime_error(std::format( + "End time ({}) precedes the initial time ({}); the simulation would not run.", + m_timeToStr(m_endTime), + m_timeToStr(m_initTime))); + } auto totalTimeSteps = static_cast(m_endTime - m_initTime); auto const nInsertions{nAgentsPerTimeStep.size()}; if (agentInsertionDeltaT == 0) { if (m_endTime > m_initTime) { agentInsertionDeltaT = totalTimeSteps / static_cast(nInsertions); - if (totalTimeSteps % nInsertions != 0) { + if (totalTimeSteps % static_cast(nInsertions) != 0) { spdlog::warn( "Total simulation time ({} seconds) is not perfectly divisible by the " "number of agent insertion steps ({}). The last agent insertion step " @@ -460,21 +515,7 @@ namespace dsf::mobility { } auto const [nAdded, nInserted, nArrived, nKilled, nRemaining] = m_dynamics->agentStats(); - spdlog::info( - "Simulation completed. Total agents added: {}\n\tInserted: {} " - "({:.2f}%)\n\tArrived: {} ({:.2f}%)\n\tKilled: {} ({:.2f}%)\n\tGhosts: {} " - "({:.2f}%)\n\tRemaining: {} ({:.2f}%).", - nAdded, - nInserted, - nInserted * 100.0f / nAdded, - nArrived, - nArrived * 100.0f / nInserted, - nKilled, - nKilled * 100.0f / nInserted, - nRemaining, - nRemaining * 100.0f / nInserted, - nInserted - (nArrived + nKilled + nRemaining), - (nInserted - (nArrived + nKilled + nRemaining)) * 100.0f / nInserted); + m_logRunSummary(nAdded, nInserted, nArrived, nKilled, nRemaining); } void TrafficSimulator::m_runSlowCharge(std::size_t const nInitialAgents, std::time_t const agentInsertionDeltaT, @@ -484,6 +525,14 @@ namespace dsf::mobility { throw std::runtime_error( "End time must be greater than or equal to initial time for the simulation."); } + if (agentInsertionDeltaT <= 0) { + throw std::invalid_argument(std::format( + "Agent insertion delta time ({}) must be positive.", agentInsertionDeltaT)); + } + if (checkDeltaT <= 0) { + throw std::invalid_argument( + std::format("Check delta time ({}) must be positive.", checkDeltaT)); + } auto const totalTimeSteps = static_cast(m_endTime - m_initTime); m_preparePersistence(); @@ -556,21 +605,7 @@ namespace dsf::mobility { } auto const [nAdded, nInserted, nArrived, nKilled, nRemaining] = m_dynamics->agentStats(); - spdlog::info( - "Simulation completed. Total agents added: {}\n\tInserted: {} " - "({:.2f}%)\n\tArrived: {} ({:.2f}%)\n\tKilled: {} ({:.2f}%)\n\tGhosts: {} " - "({:.2f}%)\n\tRemaining: {} ({:.2f}%).", - nAdded, - nInserted, - nInserted * 100.0f / nAdded, - nArrived, - nArrived * 100.0f / nInserted, - nKilled, - nKilled * 100.0f / nInserted, - nRemaining, - nRemaining * 100.0f / nInserted, - nInserted - (nArrived + nKilled + nRemaining), - (nInserted - (nArrived + nKilled + nRemaining)) * 100.0f / nInserted); + m_logRunSummary(nAdded, nInserted, nArrived, nKilled, nRemaining); } void TrafficSimulator::connectDataBase(std::string_view const dbPath, @@ -805,7 +840,7 @@ namespace dsf::mobility { auto avgSpeed = record.avgSpeed.has_value() ? std::format("{}", record.avgSpeed.value()) : ""; auto stdSpeed = - record.avgSpeed.has_value() ? std::format("{}", record.stdSpeed.value()) : ""; + record.stdSpeed.has_value() ? std::format("{}", record.stdSpeed.value()) : ""; writer.write_row( datetime, time_step, @@ -1152,6 +1187,9 @@ namespace dsf::mobility { } for (auto const& [edgeId, turnCounts] : turnCountsRecords) { for (auto const& [nextEdgeId, count] : turnCounts) { + if (count == 0) { + continue; // Match the SQL writer, which also skips empty turns. + } writer.write_row(datetime, time_step, edgeId, nextEdgeId, count); } } @@ -1174,9 +1212,9 @@ namespace dsf::mobility { "SELECT name FROM sqlite_master WHERE type='table' AND name='nodes';"); bool edgesTableExists = edgesQuery.executeStep(); bool nodesTableExists = nodesQuery.executeStep(); - if (edgesTableExists && nodesTableExists) { + if (edgesTableExists || nodesTableExists) { spdlog::debug( - "Edges and nodes tables already exist in the database. Skipping network " + "Edges and/or nodes tables already exist in the database. Skipping network " "dump."); return; } diff --git a/src/dsf/mobility/TrafficSimulator.hpp b/src/dsf/mobility/TrafficSimulator.hpp index dd98c90b..7992f2f7 100644 --- a/src/dsf/mobility/TrafficSimulator.hpp +++ b/src/dsf/mobility/TrafficSimulator.hpp @@ -212,6 +212,11 @@ namespace dsf::mobility { void m_saveTurnCountsCSV(const std::string& datetime, const std::int64_t time_step, TurnCountsDict turnCounts) const; + void m_logRunSummary(std::size_t const nAdded, + std::size_t const nInserted, + std::size_t const nArrived, + std::size_t const nKilled, + std::size_t const nRemaining) const; void m_dumpNetwork() const; void m_preparePersistence(); void m_flushStepData(StepDataResult stepData); diff --git a/src/dsf/utility/Measurement.hpp b/src/dsf/utility/Measurement.hpp index 6d274bdb..f10ea7d8 100644 --- a/src/dsf/utility/Measurement.hpp +++ b/src/dsf/utility/Measurement.hpp @@ -34,7 +34,8 @@ namespace dsf { x2_mean += value * value; }); mean = x_mean / n; - std = std::sqrt(x2_mean / n - mean * mean); + auto const variance = x2_mean / n - mean * mean; + std = variance > static_cast(0) ? std::sqrt(variance) : static_cast(0); } }; } // namespace dsf \ No newline at end of file diff --git a/test/mobility/Test_traffic_simulator.cpp b/test/mobility/Test_traffic_simulator.cpp index 8bb3f51e..c81a5ac9 100644 --- a/test/mobility/Test_traffic_simulator.cpp +++ b/test/mobility/Test_traffic_simulator.cpp @@ -468,7 +468,10 @@ TEST_CASE("TrafficSimulator CSV turn counts persistence") { simulator.importRoadNetwork(edgesPath.string()); REQUIRE(simulator.dynamics() != nullptr); simulator.dynamics()->setSpeedFunction(SpeedFunction::LINEAR, 0.8); - simulator.dynamics()->setODs(std::vector>{{0, 1, 1.0}}); + // Route agents from edge 0 to edge 3 so that they actually turn (0 -> 2). With + // destination edge 1 the agent arrives at that edge's source node and is removed + // without ever turning, so no turn event would be recorded at all. + simulator.dynamics()->setODs(std::vector>{{0, 3, 1.0}}); simulator.dynamics()->updatePaths(); // 4th flag = save turn counts