Skip to content

Commit 790da1b

Browse files
committed
Add node-to-node shortestPath function
1 parent 30a595d commit 790da1b

File tree

3 files changed

+394
-28
lines changed

3 files changed

+394
-28
lines changed

benchmark/Bench_Network.cpp

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ static void BM_RoadNetwork_EdgesLooping(benchmark::State& state) {
5757
}
5858
}
5959
}
60-
static void BM_RoadNetwork_ShortestPath(benchmark::State& state) {
60+
static void BM_RoadNetwork_AllPathsTo(benchmark::State& state) {
6161
dsf::mobility::RoadNetwork network;
6262
network.importEdges((DATA_FOLDER / "forlì_edges.csv").string());
6363
network.importNodeProperties((DATA_FOLDER / "forlì_nodes.csv").string());
@@ -68,12 +68,31 @@ static void BM_RoadNetwork_ShortestPath(benchmark::State& state) {
6868
++itNode;
6969
}
7070
}
71+
static void BM_RoadNetwork_ShortestPath(benchmark::State& state) {
72+
dsf::mobility::RoadNetwork network;
73+
network.importEdges((DATA_FOLDER / "forlì_edges.csv").string());
74+
network.importNodeProperties((DATA_FOLDER / "forlì_nodes.csv").string());
75+
auto itSource = network.nodes().cbegin();
76+
auto itTarget = std::next(network.nodes().cbegin(), network.nodes().size() / 2);
77+
for (auto _ : state) {
78+
auto path = network.shortestPath(itSource->first,
79+
itTarget->first,
80+
[](auto const& pEdge) { return pEdge->length(); });
81+
benchmark::DoNotOptimize(path);
82+
++itSource;
83+
++itTarget;
84+
if (itTarget == network.nodes().cend()) {
85+
itTarget = network.nodes().cbegin();
86+
}
87+
}
88+
}
7189
BENCHMARK(BM_RoadNetwork_AddNode);
7290
BENCHMARK(BM_RoadNetwork_AddEdge);
7391
BENCHMARK(BM_RoadNetwork_CSVImport);
7492
BENCHMARK(BM_RoadNetwork_GeoJSONImport);
7593
BENCHMARK(BM_RoadNetwork_NodesLooping);
7694
BENCHMARK(BM_RoadNetwork_EdgesLooping);
7795
BENCHMARK(BM_RoadNetwork_ShortestPath);
96+
BENCHMARK(BM_RoadNetwork_AllPathsTo);
7897

7998
BENCHMARK_MAIN();

src/dsf/mobility/RoadNetwork.hpp

Lines changed: 170 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)