bug: Undirected graph with cost != reverse_cost: aStar, bdAstar and bdDijkstra functions return every node and edge twice
Maintainers usually reply within 3 days
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 30/100
Research direction
Start with the Path(graph, original, only_cost) constructor in include/cpp_common/path.hpp, around lines 196-202, which pushes a row for every out-edge whose id matches instead of one per step. Reproduce with the edges_undirected_asym table from the issue: pgr_dijkstra should return agg_cost 10 and pgr_bdDijkstra should match it. Done means the bidirectional and A* families return the same rows as pgr_dijkstra for asymmetric undirected graphs, with the existing tests still passing.
Written by the indexing model from the issue text.
Description
Summary
With directed => false, when an edge has both cost and reverse_cost non-negative and different, the aStar, bdAstar and bdDijkstra families return a path in which every node and every edge appears twice, and agg_cost adds up both weights. pgr_dijkstra, pgr_KSP and pgr_dijkstraVia return the expected result for the same input.
Reproduce
CREATE TEMP TABLE edges_undirected_asym AS
SELECT * FROM (VALUES
(1::BIGINT, 1::BIGINT, 2::BIGINT, 7::FLOAT, 2::FLOAT),
(2, 2, 3, 7, 2),
(3, 3, 4, 7, 2),
(4, 4, 5, 7, 2),
(5, 5, 6, 7, 2)
) AS t(id, source, target, cost, reverse_cost);
-- expected result
SELECT * FROM pgr_dijkstra(
'SELECT id, source, target, cost, reverse_cost FROM edges_undirected_asym',
1, 6, false);
-- wrong result
SELECT * FROM pgr_bdDijkstra(
'SELECT id, source, target, cost, reverse_cost FROM edges_undirected_asym',
1, 6, false);
Expected
6 rows and a final agg_cost of 10, using each edge once at cost 2.
doc/src/pgRouting-concepts.rst describes an undirected edge with two costs as two parallel edges between the same vertices, both with the same id, one weighing cost and one weighing reverse_cost. Using the cheaper one at every step gives 5 * 2 = 10. I also checked this without any pgRouting algorithm, by expanding every row into those arcs and enumerating the simple paths from 1 to 6: the minimum is 10.
Actual
pgr_dijkstra: 6 rows, agg_cost 10.
pgr_bdDijkstra: 11 rows, agg_cost 45. Every node and every edge appears twice, once with cost 7 and once with cost 2:
seq | node | edge | cost | agg_cost
1 | 1 | 1 | 7 | 0
2 | 1 | 1 | 2 | 7
3 | 2 | 2 | 7 | 9
4 | 2 | 2 | 2 | 16
...
11 | 6 | -1 | 0 | 45
45 = 5 * (7 + 2). It is not the cost of any walk in that graph.
Affected functions (tested with this graph and a second 4-edge graph)
Wrong: pgr_aStar, pgr_aStarCost, pgr_aStarCostMatrix, pgr_bdAstar, pgr_bdAstarCost, pgr_bdAstarCostMatrix, pgr_bdDijkstra, pgr_bdDijkstraCost, pgr_bdDijkstraCostMatrix.
Expected result: pgr_dijkstra, pgr_KSP (k = 1), pgr_dijkstraVia.
Directed graphs, and undirected graphs with cost = reverse_cost, give the expected result in all twelve functions.
Also reproduced on pgRouting 4.0.0 (pgr_dijkstra 10, pgr_bdDijkstra 45), so it is not recent. The bidirectional fix proposed in #3174 does not change this behaviour.
Where it seems to come from
From reading the code, not from instrumenting it:
- include/cpp_common/base_graph.hpp (graph_add_edge, around lines 774-781) stores such an undirected edge as two internal edges with the same id, which matches the documentation.
- include/cpp_common/path.hpp (the Path(graph, original, only_cost) constructor, around lines 196-202) looks up each step by id and pushes a row for every out-edge whose id matches, without a break. Both parallel edges match, so both are kept, and agg_cost is recomputed from all of them.
- That constructor is only called from include/astar/astar.hpp and include/cpp_common/bidirectional.hpp, which is the list of affected functions.
- pgr_dijkstra builds its result through get_edge_id(), which returns exactly one edge and uses the cost to choose, so it is not affected.
Why the tests do not catch it
The sample edges table only has rows with (1, 1), (-1, 1) or (1, -1) as cost and reverse_cost, so no row has two different non-negative weights. The compare_dijkstra tests start with
UPDATE edges SET cost = sign(cost) + 0.001 * id * id, reverse_cost = sign(reverse_cost) + 0.001 * id * id;
which keeps cost and reverse_cost equal.
Possible direction (not tested)
In the Path constructor, push one row per step instead of one per matching edge, choosing the one whose cost matches the step's recorded cost or the cheapest, as get_edge_id() does. I have not tried this.
- Dominant language
- C++
- Stars
- 1.4k
- Forks
- 397
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 8
Getting set up
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from pgRouting/pgrouting
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 3 days
-
Administrative Enhancement
Difficulty 1/5 Under an hour Newbie friendliness 80/100
Maintainers usually reply within 3 days
-
points_input Developers docPossibly taken @withaman69 claimed this 101 days ago. OpenC/C++ Developers Doc. good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
pgRouting/pgrouting#973 · 10 comments ·
Maintainers usually reply within 3 days
-
get_check_data developers docPossibly taken @withaman69 claimed this 103 days ago. OpenC/C++ Developers Doc. good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
pgRouting/pgrouting#972 · 7 comments ·
Maintainers usually reply within 3 days
All issues in pgRouting/pgrouting
Similar issues
-
std::fdim drops partial derivative w.r.t second argument in forward and reverse modePossibly taken @ADITYASINGH1206 claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 85/100
vgvassilev/clad#2534 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 61/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
HAP.cpp fixOpen
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
ardia-browser/ardia#1942 ·
Maintainers usually reply within 1 day