@@ -208,6 +208,24 @@ namespace dsf::mobility {
208208 Id const targetId,
209209 DynamicsFunc getEdgeWeight,
210210 double const threshold = 1e-9) const;
211+
212+ /// @brief Find the shortest path between two nodes using Dijkstra's algorithm
213+ /// @tparam DynamicsFunc A callable type that takes a const reference to a Street and returns a double representing the edge weight
214+ /// @param sourceId The id of the source node
215+ /// @param targetId The id of the target node
216+ /// @param getEdgeWeight A callable that takes a const reference to a Street and returns a double representing the edge weight
217+ /// @param threshold A threshold value to consider alternative paths
218+ /// @return A map where each key is a node id and the value is a vector of next hop node ids toward the target. Returns an empty map if no path exists
219+ /// @throws std::out_of_range if the source or target node does not exist
220+ /// @details Uses Dijkstra's algorithm to find shortest paths from source to target.
221+ /// Like allPathsTo, this method tracks all equivalent paths within the threshold, allowing for multiple next hops per node.
222+ template <typename DynamicsFunc>
223+ requires(std::is_invocable_r_v<double, DynamicsFunc, std::unique_ptr<Street> const&>)
224+ std::unordered_map<Id, std::vector<Id>> shortestPath(
225+ Id const sourceId,
226+ Id const targetId,
227+ DynamicsFunc getEdgeWeight,
228+ double const threshold = 1e-9) const;
211229 };
212230
213231 template <typename... TArgs>
@@ -295,19 +313,15 @@ namespace dsf::mobility {
295313 template <typename DynamicsFunc>
296314 requires(std::is_invocable_r_v<double, DynamicsFunc, std::unique_ptr<Street> const&>)
297315 std::unordered_map<Id, std::vector<Id>> RoadNetwork::allPathsTo(
298- Id const sourceId , DynamicsFunc f, double const threshold) const {
316+ Id const targetId , DynamicsFunc f, double const threshold) const {
299317 // Check if source node exists
300318 auto const& nodes = this->nodes();
301- if (!nodes.contains(sourceId)) {
302- throw std::out_of_range(
303- std::format("Source node with id {} does not exist.", sourceId));
304- }
305319
306320 // Distance from each node to the source (going backward)
307- std::unordered_map<Id, double> distToSource ;
308- distToSource .reserve(nNodes());
321+ std::unordered_map<Id, double> distToTarget ;
322+ distToTarget .reserve(nNodes());
309323 // Next hop from each node toward the source
310- std::unordered_map<Id, std::vector<Id>> nextHopsToSource ;
324+ std::unordered_map<Id, std::vector<Id>> nextHopsToTarget ;
311325
312326 // Priority queue: pair<distance, nodeId> (min-heap)
313327 std::priority_queue<std::pair<double, Id>,
@@ -317,20 +331,20 @@ namespace dsf::mobility {
317331
318332 // Initialize all nodes with infinite distance
319333 std::for_each(nodes.cbegin(), nodes.cend(), [&](auto const& pair) {
320- distToSource [pair.first] = std::numeric_limits<double>::infinity();
321- nextHopsToSource [pair.first] = std::vector<Id>();
334+ distToTarget [pair.first] = std::numeric_limits<double>::infinity();
335+ nextHopsToTarget [pair.first] = std::vector<Id>();
322336 });
323337
324- // Source has distance 0 to itself
325- distToSource[sourceId ] = 0.0;
326- pq.push({0.0, sourceId });
338+ // Target has distance 0 to itself
339+ distToTarget[targetId ] = 0.0;
340+ pq.push({0.0, targetId });
327341
328342 while (!pq.empty()) {
329343 auto [currentDist, currentNode] = pq.top();
330344 pq.pop();
331345
332346 // Skip if we've already found a better path to this node
333- if (currentDist > distToSource [currentNode]) {
347+ if (currentDist > distToTarget [currentNode]) {
334348 continue;
335349 }
336350
@@ -341,26 +355,26 @@ namespace dsf::mobility {
341355
342356 // Calculate the weight of the edge from neighbor to currentNode using the dynamics function
343357 double edgeWeight = f(this->edge(inEdgeId));
344- double newDistToSource = distToSource [currentNode] + edgeWeight;
358+ double newDistToTarget = distToTarget [currentNode] + edgeWeight;
345359
346360 // If we found a shorter path from neighborId to source
347- if (newDistToSource < distToSource [neighborId]) {
348- distToSource [neighborId] = newDistToSource ;
349- nextHopsToSource [neighborId].clear();
350- nextHopsToSource [neighborId].push_back(currentNode);
351- pq.push({newDistToSource , neighborId});
361+ if (newDistToTarget < distToTarget [neighborId]) {
362+ distToTarget [neighborId] = newDistToTarget ;
363+ nextHopsToTarget [neighborId].clear();
364+ nextHopsToTarget [neighborId].push_back(currentNode);
365+ pq.push({newDistToTarget , neighborId});
352366 }
353367 // If we found an equally good path, add it as alternative
354- else if (newDistToSource < (1. + threshold) * distToSource [neighborId]) {
368+ else if (newDistToTarget < (1. + threshold) * distToTarget [neighborId]) {
355369 spdlog::debug(
356370 "Found alternative path to node {} with distance {:.6f} (existing: {:.6f}) "
357371 "for threshold {:.6f}",
358372 neighborId,
359- newDistToSource ,
360- distToSource [neighborId],
373+ newDistToTarget ,
374+ distToTarget [neighborId],
361375 threshold);
362376 // Check if currentNode is not already in the nextHops
363- auto& hops = nextHopsToSource [neighborId];
377+ auto& hops = nextHopsToTarget [neighborId];
364378 if (std::find(hops.begin(), hops.end(), currentNode) == hops.end()) {
365379 hops.push_back(currentNode);
366380 }
@@ -370,14 +384,143 @@ namespace dsf::mobility {
370384
371385 // Build result: only include reachable nodes (excluding source)
372386 std::unordered_map<Id, std::vector<Id>> result;
373- for (auto const& [nodeId, hops] : nextHopsToSource ) {
374- if (nodeId != sourceId &&
375- distToSource [nodeId] != std::numeric_limits<double>::infinity() &&
387+ for (auto const& [nodeId, hops] : nextHopsToTarget ) {
388+ if (nodeId != targetId &&
389+ distToTarget [nodeId] != std::numeric_limits<double>::infinity() &&
376390 !hops.empty()) {
377391 result[nodeId] = hops;
378392 }
379393 }
380394
381395 return result;
382396 }
397+
398+ template <typename DynamicsFunc>
399+ requires(std::is_invocable_r_v<double, DynamicsFunc, std::unique_ptr<Street> const&>)
400+ std::unordered_map<Id, std::vector<Id>> RoadNetwork::shortestPath(
401+ Id const sourceId, Id const targetId, DynamicsFunc f, double const threshold) const {
402+ // If source equals target, return empty map (no intermediate hops needed)
403+ if (sourceId == targetId) {
404+ return std::unordered_map<Id, std::vector<Id>>{};
405+ }
406+ // Check if source node exists
407+ if (!this->nodes().contains(sourceId)) {
408+ throw std::out_of_range(
409+ std::format("Source node with id {} does not exist in the graph", sourceId));
410+ }
411+ // Check if target node exists
412+ if (!this->nodes().contains(targetId)) {
413+ throw std::out_of_range(
414+ std::format("Target node with id {} does not exist in the graph", targetId));
415+ }
416+ auto const& nodes = this->nodes();
417+
418+ // Distance from each node to the target (going backward)
419+ std::unordered_map<Id, double> distToTarget;
420+ distToTarget.reserve(nNodes());
421+ // Next hop from each node toward the target
422+ std::unordered_map<Id, std::vector<Id>> nextHopsToTarget;
423+
424+ // Priority queue: pair<distance, nodeId> (min-heap)
425+ std::priority_queue<std::pair<double, Id>,
426+ std::vector<std::pair<double, Id>>,
427+ std::greater<>>
428+ pq;
429+
430+ // Initialize all nodes with infinite distance
431+ std::for_each(nodes.cbegin(), nodes.cend(), [&](auto const& pair) {
432+ distToTarget[pair.first] = std::numeric_limits<double>::infinity();
433+ nextHopsToTarget[pair.first] = std::vector<Id>();
434+ });
435+
436+ // Target has distance 0 to itself
437+ distToTarget[targetId] = 0.0;
438+ pq.push({0.0, targetId});
439+
440+ while (!pq.empty()) {
441+ auto [currentDist, currentNode] = pq.top();
442+ pq.pop();
443+
444+ // Skip if we've already found a better path to this node
445+ if (currentDist > distToTarget[currentNode]) {
446+ continue;
447+ }
448+
449+ // If we've reached the source, we can stop early
450+ if (currentNode == sourceId) {
451+ break;
452+ }
453+
454+ // Explore all incoming edges (nodes that can reach currentNode)
455+ auto const& inEdges = node(currentNode)->ingoingEdges();
456+ for (auto const& inEdgeId : inEdges) {
457+ Id neighborId = edge(inEdgeId)->source();
458+
459+ // Calculate the weight of the edge from neighbor to currentNode using the dynamics function
460+ double edgeWeight = f(this->edge(inEdgeId));
461+ double newDistToTarget = distToTarget[currentNode] + edgeWeight;
462+
463+ // If we found a shorter path from neighborId to target
464+ if (newDistToTarget < distToTarget[neighborId]) {
465+ distToTarget[neighborId] = newDistToTarget;
466+ nextHopsToTarget[neighborId].clear();
467+ nextHopsToTarget[neighborId].push_back(currentNode);
468+ pq.push({newDistToTarget, neighborId});
469+ }
470+ // If we found an equally good path, add it as alternative
471+ else if (newDistToTarget < (1. + threshold) * distToTarget[neighborId]) {
472+ spdlog::debug(
473+ "Found alternative path to node {} with distance {:.6f} (existing: {:.6f}) "
474+ "for threshold {:.6f}",
475+ neighborId,
476+ newDistToTarget,
477+ distToTarget[neighborId],
478+ threshold);
479+ // Check if currentNode is not already in the nextHops
480+ auto& hops = nextHopsToTarget[neighborId];
481+ if (std::find(hops.begin(), hops.end(), currentNode) == hops.end()) {
482+ hops.push_back(currentNode);
483+ }
484+ }
485+ }
486+ }
487+
488+ // Check if target is reachable from source
489+ if (distToTarget[sourceId] == std::numeric_limits<double>::infinity()) {
490+ return std::unordered_map<Id, std::vector<Id>>{};
491+ }
492+
493+ // Build result: only include nodes on the path from source to target
494+ std::unordered_map<Id, std::vector<Id>> result;
495+ std::unordered_set<Id> nodesOnPath;
496+
497+ // Start from source and traverse to target using BFS to find all nodes on valid paths
498+ std::queue<Id> queue;
499+ queue.push(sourceId);
500+ nodesOnPath.insert(sourceId);
501+
502+ while (!queue.empty()) {
503+ Id current = queue.front();
504+ queue.pop();
505+
506+ if (current == targetId) {
507+ continue;
508+ }
509+
510+ // Add this node's next hops to the result if they exist
511+ if (nextHopsToTarget.contains(current) && !nextHopsToTarget[current].empty()) {
512+ result[current] = nextHopsToTarget[current];
513+
514+ // Add next hops to the queue if not already visited
515+ for (Id nextHop : nextHopsToTarget[current]) {
516+ if (!nodesOnPath.contains(nextHop)) {
517+ nodesOnPath.insert(nextHop);
518+ queue.push(nextHop);
519+ }
520+ }
521+ }
522+ }
523+
524+ return result;
525+ }
383526}; // namespace dsf::mobility
0 commit comments