Skip to content

Commit c512bd1

Browse files
steffenaxerdependabot[bot]samuelhoenleCopilot
committed
fix(router): Make CH router fully deterministic (#51)
* feat(router): Add time-dependent CATCHUp bidirectional CH router (SpeedyCH) (#48) * build(deps): bump the all-dependencies group across 1 directory with 3 updates (matsim-org#4845) Bumps the all-dependencies group with 3 updates in the / directory: [org.codehaus.woodstox:stax2-api](https://github.com/FasterXML/stax2-api), [net.bytebuddy:byte-buddy](https://github.com/raphw/byte-buddy) and software.amazon.awssdk:bom. Updates `org.codehaus.woodstox:stax2-api` from 4.2.2 to 4.3.0 - [Commits](FasterXML/stax2-api@stax2-api-4.2.2...stax2-api-4.3.0) Updates `net.bytebuddy:byte-buddy` from 1.18.7 to 1.18.8 - [Release notes](https://github.com/raphw/byte-buddy/releases) - [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md) - [Commits](raphw/byte-buddy@byte-buddy-1.18.7...byte-buddy-1.18.8) Updates `software.amazon.awssdk:bom` from 2.42.23 to 2.42.25 --- updated-dependencies: - dependency-name: org.codehaus.woodstox:stax2-api dependency-version: 4.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-dependencies - dependency-name: net.bytebuddy:byte-buddy dependency-version: 1.18.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.42.25 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(drt): Compute sharing metrics per DRT mode (matsim-org#4844) * feat(drt): Enable SpeedyCH time-dependent router for DRT insertion search (#49) * feat(router): add build statistics tracking for CH contraction process * feat(benchmark): enhance DRT benchmark with request inserter types and insertion search strategies * fix(dvrp): fix ArrayIndexOutOfBoundsException in CHRouter + OneToManyPathCalculator (#50) * feat(benchmark): update insertion search strategy to RepeatedSelective * fix test * fix: translate colored node indices through spatial reorder mapping and fix tests - Add SpeedyGraph.getInternalIndex(int) for raw external index translation - Fix LeastCostPathTree colored node index in forward/backward search - Fix SpeedyDijkstra, SpeedyALT, CHRouter, CHRouterTimeDep colored node indices - Fix SpeedyMultiSourceALT to use graph.getNodeIndex() instead of node.getId().index() - Fix LeastCostPathTreeTurnRestrictionsTest to use tree.getNodeIndex() - Update ReRoutingIT CH test: verify simulation succeeds instead of golden reference (CH construction is non-deterministic with spatial node ordering) Agent-Logs-Url: https://github.com/steffenaxer/matsim-libs/sessions/934c5dff-2a0c-4f13-9449-51662b958f4e Co-authored-by: steffenaxer <26229392+steffenaxer@users.noreply.github.com> * fix: deterministic Morton code tie-breaking and restore ReRoutingIT CH golden reference test Agent-Logs-Url: https://github.com/steffenaxer/matsim-libs/sessions/934c5dff-2a0c-4f13-9449-51662b958f4e Co-authored-by: steffenaxer <26229392+steffenaxer@users.noreply.github.com> * fix: make CH router fully deterministic and regenerate golden reference Two sources of non-determinism fixed: 1. InertialFlowCutter: Replace shared AtomicInteger levelCounter in parallel recursion with a two-phase approach: Phase 1 builds a deterministic NDCell tree (parallel), Phase 2 assigns levels via sequential depth-first walk. 2. CHBuilder: Disable parallel contraction (pool=null) because cells in the same round share adjacency structures — shortcuts from one cell can affect another cell's witness search, creating different shortcut sets per run. Additionally, sort each node's CSR edges by (neighborNode, origLink) in buildCHGraph() for deterministic edge iteration order. Verified deterministic across multiple runs: - ReRoutingIT: 4/4 pass (3 consecutive runs identical) - CH/Speedy tests: 81/81 pass - dvrp tests: 79/79 pass Agent-Logs-Url: https://github.com/steffenaxer/matsim-libs/sessions/ea70a0e9-8db0-4344-99a7-1895ad33d191 Co-authored-by: steffenaxer <26229392+steffenaxer@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: steffenaxer <26229392+steffenaxer@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Samuel Hönle <samuel.hoenle@ait.ac.at> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 9989e91 commit c512bd1

12 files changed

Lines changed: 260 additions & 102 deletions

File tree

contribs/dvrp/src/main/java/org/matsim/core/router/speedy/SpeedyMultiSourceALT.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,13 @@ public Path calcLeastCostPath(Collection<StartNode> startNodes, Node endNode, Pe
9595
this.currentIteration = Integer.MIN_VALUE;
9696
}
9797

98-
int endNodeIndex = endNode.getId().index();
98+
int endNodeIndex = graph.getNodeIndex(endNode);
9999
// TODO add support for dead ends
100100
// int endDeadend = this.astarData.getNodeDeadend(endNodeIndex);
101101

102102
this.pq.clear();
103103
for (StartNode startNode : startNodes) {
104-
int startNodeIndex = startNode.node.getId().index();
104+
int startNodeIndex = graph.getNodeIndex(startNode.node);
105105
// TODO add support for dead ends
106106
// int startDeadend = this.astarData.getNodeDeadend(startNodeIndex);
107107
double estimation = estimateMinTravelcostToDestination(startNodeIndex, endNodeIndex, backward);
@@ -247,11 +247,11 @@ private Path constructPath(int endNodeIndex, boolean backward) {
247247

248248
double startTime;
249249
if (backward) {
250-
startTime = getTimeRaw(nodes.get(nodes.size() - 1).getId().index());
250+
startTime = getTimeRaw(graph.getNodeIndex(nodes.get(nodes.size() - 1)));
251251
} else {
252252
Collections.reverse(nodes);
253253
Collections.reverse(links);
254-
startTime = getTimeRaw(nodes.get(0).getId().index());
254+
startTime = getTimeRaw(graph.getNodeIndex(nodes.get(0)));
255255
}
256256

257257
double travelTime = backward ? startTime - endTime : endTime - startTime;

contribs/dvrp/src/test/java/org/matsim/contrib/dvrp/path/LeastCostPathTreeTurnRestrictionsTest.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ void testNoTurnRestrictions() {
5757
tree.calculateBackwards(network.getLinks().get(Id.createLinkId("10")), 0, null, null);
5858

5959
for (Node node : network.getNodes().values()) {
60-
System.out.println(tree.getCost(node.getId().index()));
60+
System.out.println(tree.getCost(tree.getNodeIndex(node)));
6161
}
6262

63-
Assertions.assertEquals(1., tree.getCost(Id.createNodeId("0").index()));
64-
Assertions.assertEquals(0., tree.getCost(Id.createNodeId("1").index()));
65-
Assertions.assertEquals(1., tree.getCost(Id.createNodeId("2").index()));
63+
Assertions.assertEquals(1., tree.getCost(tree.getNodeIndex(network.getNodes().get(Id.createNodeId("0")))));
64+
Assertions.assertEquals(0., tree.getCost(tree.getNodeIndex(network.getNodes().get(Id.createNodeId("1")))));
65+
Assertions.assertEquals(1., tree.getCost(tree.getNodeIndex(network.getNodes().get(Id.createNodeId("2")))));
6666
}
6767

6868
@Test
@@ -83,8 +83,8 @@ void testWithOnlyColoredNodes() {
8383

8484
tree.calculateBackwards(network.getLinks().get(Id.createLinkId("10")), 0, null, null);
8585

86-
Assertions.assertEquals(1., tree.getCost(Id.createNodeId("0").index()));
87-
Assertions.assertEquals(0., tree.getCost(Id.createNodeId("1").index()));
88-
Assertions.assertEquals(1., tree.getCost(Id.createNodeId("2").index()));
86+
Assertions.assertEquals(1., tree.getCost(tree.getNodeIndex(network.getNodes().get(Id.createNodeId("0")))));
87+
Assertions.assertEquals(0., tree.getCost(tree.getNodeIndex(network.getNodes().get(Id.createNodeId("1")))));
88+
Assertions.assertEquals(1., tree.getCost(tree.getNodeIndex(network.getNodes().get(Id.createNodeId("2")))));
8989
}
9090
}

matsim/src/main/java/org/matsim/core/router/speedy/CHBuilder.java

Lines changed: 120 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,13 @@ private void contractNodesInOrder(int[] order) {
582582
* </ul>
583583
*/
584584
private void contractNodesParallel(int[] order, List<List<int[]>> rounds, int nThreads) {
585-
ForkJoinPool pool = (nThreads > 1) ? new ForkJoinPool(nThreads) : null;
585+
// Parallel contraction of independent ND cells is disabled because cells
586+
// share the same graph adjacency structures. Shortcuts added by one cell
587+
// can be visible to another cell's witness search, causing non-deterministic
588+
// shortcut creation. Sequential contraction preserves the same algorithmic
589+
// benefits (adaptive contraction, priority reordering, deferred nodes) while
590+
// guaranteeing deterministic results.
591+
ForkJoinPool pool = null;
586592

587593
// Reuse one WitnessContext per thread across all rounds & tasks.
588594
ThreadLocal<WitnessContext> tlCtx = ThreadLocal.withInitial(() -> new WitnessContext(nodeCount));
@@ -1958,42 +1964,85 @@ private CHGraph buildCHGraph() {
19581964
int[] oldToNew = new int[buildEdgeCount];
19591965
Arrays.fill(oldToNew, -1);
19601966

1961-
// 4. Fill CSR arrays (use upCount/dnCount as cursors, reset to 0 first)
1962-
int[] upCursor = new int[nodeCount];
1963-
int[] dnCursor = new int[nodeCount];
1967+
// 4. Sort build edges deterministically before filling CSR arrays.
1968+
// Parallel contraction may add shortcuts in non-deterministic order
1969+
// (different AtomicInteger increments per run). Sorting each node's
1970+
// edges by (neighborNode, origLink) ensures the CSR is always identical.
1971+
1972+
// Collect up-edge build indices per source node, dn-edge per target node.
1973+
int[][] upBiPerNode = new int[nodeCount][];
1974+
int[][] dnBiPerNode = new int[nodeCount][];
1975+
{ // scope for temporary arrays
1976+
int[] upCntTmp = new int[nodeCount];
1977+
int[] dnCntTmp = new int[nodeCount];
1978+
for (int bi = 0; bi < buildEdgeCount; bi++) {
1979+
int bBase = bi * BE_SIZE;
1980+
int fromNode = buildEdgeData[bBase + BE_FROM];
1981+
int toNode = buildEdgeData[bBase + BE_TO];
1982+
int lvFrom = nodeLevel[fromNode];
1983+
int lvTo = nodeLevel[toNode];
1984+
if (lvFrom < lvTo) upCntTmp[fromNode]++;
1985+
else if (lvFrom > lvTo) dnCntTmp[toNode]++;
1986+
}
1987+
for (int n = 0; n < nodeCount; n++) {
1988+
if (upCntTmp[n] > 0) upBiPerNode[n] = new int[upCntTmp[n]];
1989+
if (dnCntTmp[n] > 0) dnBiPerNode[n] = new int[dnCntTmp[n]];
1990+
upCntTmp[n] = 0;
1991+
dnCntTmp[n] = 0;
1992+
}
1993+
for (int bi = 0; bi < buildEdgeCount; bi++) {
1994+
int bBase = bi * BE_SIZE;
1995+
int fromNode = buildEdgeData[bBase + BE_FROM];
1996+
int toNode = buildEdgeData[bBase + BE_TO];
1997+
int lvFrom = nodeLevel[fromNode];
1998+
int lvTo = nodeLevel[toNode];
1999+
if (lvFrom < lvTo) upBiPerNode[fromNode][upCntTmp[fromNode]++] = bi;
2000+
else if (lvFrom > lvTo) dnBiPerNode[toNode][dnCntTmp[toNode]++] = bi;
2001+
}
2002+
}
19642003

1965-
for (int bi = 0; bi < buildEdgeCount; bi++) {
1966-
int bBase = bi * BE_SIZE;
1967-
int fromNode = buildEdgeData[bBase + BE_FROM];
1968-
int toNode = buildEdgeData[bBase + BE_TO];
1969-
int origLink = buildEdgeData[bBase + BE_ORIG];
1970-
int lvFrom = nodeLevel[fromNode];
1971-
int lvTo = nodeLevel[toNode];
2004+
// Sort each node's edges by (neighborNode, origLink) for determinism.
2005+
for (int n = 0; n < nodeCount; n++) {
2006+
if (upBiPerNode[n] != null && upBiPerNode[n].length > 1)
2007+
sortBuildEdgesBy(upBiPerNode[n], buildEdgeData, BE_SIZE, BE_TO, BE_ORIG);
2008+
if (dnBiPerNode[n] != null && dnBiPerNode[n].length > 1)
2009+
sortBuildEdgesBy(dnBiPerNode[n], buildEdgeData, BE_SIZE, BE_FROM, BE_ORIG);
2010+
}
19722011

1973-
if (lvFrom < lvTo) {
1974-
// Upward edge: gIdx = slot
1975-
int slot = upOff[fromNode] + upCursor[fromNode]++;
1976-
int eBase = slot * S;
1977-
upEdges[eBase + CHGraph.E_NODE] = toNode;
1978-
upEdges[eBase + CHGraph.E_GIDX] = slot; // contiguous gIdx
1979-
upWeights[slot] = buildEdgeWeights[bi];
1980-
edgeOrigLink[slot] = origLink;
1981-
// lower edges will be remapped after this loop
1982-
edgeLower1[slot] = buildEdgeData[bBase + BE_LOW1];
1983-
edgeLower2[slot] = buildEdgeData[bBase + BE_LOW2];
1984-
oldToNew[bi] = slot;
1985-
} else if (lvFrom > lvTo) {
1986-
// Downward edge: gIdx = totalUp + dnSlot
1987-
int dnSlot = dnOff[toNode] + dnCursor[toNode]++;
1988-
int gIdx = totalUp + dnSlot;
1989-
int eBase = dnSlot * S;
1990-
dnEdges[eBase + CHGraph.E_NODE] = fromNode;
1991-
dnEdges[eBase + CHGraph.E_GIDX] = gIdx; // contiguous gIdx
1992-
dnWeights[dnSlot] = buildEdgeWeights[bi];
1993-
edgeOrigLink[gIdx] = origLink;
1994-
edgeLower1[gIdx] = buildEdgeData[bBase + BE_LOW1];
1995-
edgeLower2[gIdx] = buildEdgeData[bBase + BE_LOW2];
1996-
oldToNew[bi] = gIdx;
2012+
// Fill CSR arrays in deterministic sorted order.
2013+
for (int n = 0; n < nodeCount; n++) {
2014+
if (upBiPerNode[n] != null) {
2015+
int off = upOff[n];
2016+
for (int k = 0; k < upBiPerNode[n].length; k++) {
2017+
int bi = upBiPerNode[n][k];
2018+
int bBase = bi * BE_SIZE;
2019+
int slot = off + k;
2020+
int eBase = slot * S;
2021+
upEdges[eBase + CHGraph.E_NODE] = buildEdgeData[bBase + BE_TO];
2022+
upEdges[eBase + CHGraph.E_GIDX] = slot;
2023+
upWeights[slot] = buildEdgeWeights[bi];
2024+
edgeOrigLink[slot] = buildEdgeData[bBase + BE_ORIG];
2025+
edgeLower1[slot] = buildEdgeData[bBase + BE_LOW1];
2026+
edgeLower2[slot] = buildEdgeData[bBase + BE_LOW2];
2027+
oldToNew[bi] = slot;
2028+
}
2029+
}
2030+
if (dnBiPerNode[n] != null) {
2031+
int off = dnOff[n];
2032+
for (int k = 0; k < dnBiPerNode[n].length; k++) {
2033+
int bi = dnBiPerNode[n][k];
2034+
int bBase = bi * BE_SIZE;
2035+
int dnSlot = off + k;
2036+
int gIdx = totalUp + dnSlot;
2037+
int eBase = dnSlot * S;
2038+
dnEdges[eBase + CHGraph.E_NODE] = buildEdgeData[bBase + BE_FROM];
2039+
dnEdges[eBase + CHGraph.E_GIDX] = gIdx;
2040+
dnWeights[dnSlot] = buildEdgeWeights[bi];
2041+
edgeOrigLink[gIdx] = buildEdgeData[bBase + BE_ORIG];
2042+
edgeLower1[gIdx] = buildEdgeData[bBase + BE_LOW1];
2043+
edgeLower2[gIdx] = buildEdgeData[bBase + BE_LOW2];
2044+
oldToNew[bi] = gIdx;
2045+
}
19972046
}
19982047
}
19992048

@@ -2008,8 +2057,9 @@ private CHGraph buildCHGraph() {
20082057
}
20092058

20102059
// 6. Build topological customization order:
2011-
// Process build edges in original order (which preserves dependency: lower edges
2012-
// are created before their parent shortcuts). Map to new gIdx.
2060+
// Process build edges in node-level order (lower levels first) to ensure
2061+
// dependencies (lower edges created before parent shortcuts) are preserved.
2062+
// Within the same level, edges are in deterministic sorted order.
20132063
int[] customizeOrder = new int[totalEdgeCount];
20142064
int orderIdx = 0;
20152065
for (int bi = 0; bi < buildEdgeCount; bi++) {
@@ -2029,6 +2079,39 @@ private CHGraph buildCHGraph() {
20292079
// Helpers
20302080
// -------------------------------------------------------------------------
20312081

2082+
/**
2083+
* Sorts an array of build-edge indices by (primaryField, secondaryField) of
2084+
* the build-edge data. This ensures deterministic CSR filling order
2085+
* regardless of parallel contraction thread scheduling.
2086+
*/
2087+
private static void sortBuildEdgesBy(int[] biArray, int[] buildEdgeData,
2088+
int beSize, int primaryField, int secondaryField) {
2089+
// Pack (primary, secondary, buildIndex) into a long for one-pass sort.
2090+
long[] keys = new long[biArray.length];
2091+
for (int i = 0; i < biArray.length; i++) {
2092+
int bi = biArray[i];
2093+
int bBase = bi * beSize;
2094+
int primary = buildEdgeData[bBase + primaryField];
2095+
int secondary = buildEdgeData[bBase + secondaryField];
2096+
// primary in upper 32 bits, secondary as unsigned in lower 32
2097+
keys[i] = ((long) primary << 32) | (secondary & 0xFFFFFFFFL);
2098+
}
2099+
// Co-sort keys and biArray
2100+
// Simple insertion sort is fine for typical edge counts per node (< 100)
2101+
for (int i = 1; i < keys.length; i++) {
2102+
long kv = keys[i];
2103+
int bv = biArray[i];
2104+
int j = i - 1;
2105+
while (j >= 0 && keys[j] > kv) {
2106+
keys[j + 1] = keys[j];
2107+
biArray[j + 1] = biArray[j];
2108+
j--;
2109+
}
2110+
keys[j + 1] = kv;
2111+
biArray[j + 1] = bv;
2112+
}
2113+
}
2114+
20322115
private int addBuildEdge(int from, int to, int origLink,
20332116
int middle, int lower1, int lower2, double weight) {
20342117
int idx = buildEdgeCounter.getAndIncrement();

matsim/src/main/java/org/matsim/core/router/speedy/CHRouter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public Path calcLeastCostPath(Link fromLink, Link toLink,
136136
if (turnRestrictions != null) {
137137
Map<Id<Link>, TurnRestrictionsContext.ColoredLink> replaced = turnRestrictions.replacedLinks;
138138
if (replaced.containsKey(fromLink.getId())) {
139-
startIdx = replaced.get(fromLink.getId()).toColoredNode.index();
139+
startIdx = baseGraph.getInternalIndex(replaced.get(fromLink.getId()).toColoredNode.index());
140140
}
141141
}
142142

matsim/src/main/java/org/matsim/core/router/speedy/CHRouterTimeDep.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public Path calcLeastCostPath(Link fromLink, Link toLink,
140140
if (turnRestrictions != null) {
141141
Map<Id<Link>, TurnRestrictionsContext.ColoredLink> replaced = turnRestrictions.replacedLinks;
142142
if (replaced.containsKey(fromLink.getId())) {
143-
startIdx = replaced.get(fromLink.getId()).toColoredNode.index();
143+
startIdx = baseGraph.getInternalIndex(replaced.get(fromLink.getId()).toColoredNode.index());
144144
}
145145
}
146146

0 commit comments

Comments
 (0)