Summary
calcPathBased() has two bugs that together make precompute unusable:
- O(R·N·K·N) complexity —
comparePathDistance() runs an uncached BFS for every entity × center × iteration, causing precompute to exceed the time limit on standard maps.
ArithmeticException: / by zero — when a cluster receives no entities during iteration, clusterEntitiesList.get(index).size() returns 0 and the subsequent division crashes the thread.
Both bugs were confirmed on the RSL 2025 kobe1 map with the default configuration.
Bug 1: Excessive BFS calls in the assignment step
Inside the K-means iteration loop, each entity is assigned to its nearest center via:
// calcPathBased(), line ~336
StandardEntity tmp = this.getNearEntity(this.worldInfo, this.centerList, entity);
This calls comparePathDistance() for each candidate center, which in turn calls shortestPath() (BFS, O(N)) twice per comparison with no caching:
private StandardEntity comparePathDistance(...) {
double firstDistance = getPathDistance(worldInfo, shortestPath(target.getID(), first.getID()));
double secondDistance = getPathDistance(worldInfo, shortestPath(target.getID(), second.getID()));
return (firstDistance < secondDistance ? first : second);
}
Total complexity: O(R·N·K·N)
| Symbol |
Value (RSL 2025 kobe1, default config) |
| N |
2,359 (road + building entities) |
| K |
30 (agents per type) |
| R |
7 (repeatPrecompute default) |
With these values: approximately 2.3 × 10⁹ BFS node accesses per precompute call.
Measured impact: precompute did not complete within 6 minutes and had to be killed.
Bug 2: Division by zero when a cluster becomes empty
In the center update step, if a cluster receives no entities, the code divides by zero:
// calcPathBased(), line ~347
int centerX = sumX / clusterEntitiesList.get(index).size(); // size() == 0 → crash
int centerY = sumY / clusterEntitiesList.get(index).size();
This throws ArithmeticException and kills the agent thread before precompute data is written.
Observed stack trace:
Exception in thread "Thread-29" java.lang.ArithmeticException: / by zero
at adf.impl.module.algorithm.KMeansClustering.calcPathBased(KMeansClustering.java:347)
at adf.impl.module.algorithm.KMeansClustering.precompute(KMeansClustering.java:107)
...
Note: the same isEmpty() guard is also missing in calcStandard() at the corresponding location.
Proposed Fix
For Bug 1: Replace the per-entity BFS with multi-source Dijkstra, which expands from all K centers simultaneously and assigns each entity to the nearest center in a single O(N log N) pass. This also eliminates the empty cluster problem (Bug 2) for connected graphs, since every reachable entity is guaranteed to be assigned.
This requires two changes:
-
Replace initShortestPath (unweighted adjacency graph) with initWeightedGraph (edge-weighted adjacency graph). Edge weight between adjacent areas A and B is defined to match getPathDistance():
cost(A→B) = dist(A.center, midpoint of A-B edge)
+ dist(B.center, midpoint of B-A edge)
-
Replace the per-entity getNearEntity() call with assignByMultiSourceDijkstra():
private Map<EntityID, Integer> assignByMultiSourceDijkstra(List<StandardEntity> centers) {
PriorityQueue<double[]> pq = new PriorityQueue<>(Comparator.comparingDouble(a -> a[0]));
// seed all K centers at distance 0
for (int i = 0; i < centers.size(); i++) { ... }
// Dijkstra expansion
while (!pq.isEmpty()) { ... }
return assignment; // EntityID → clusterIndex
}
For Bug 2 (defensive fix): Add an isEmpty() guard before the division in both calcPathBased() and calcStandard() to handle any remaining edge cases:
List<StandardEntity> clusterEntities = this.clusterEntitiesList.get(index);
if (clusterEntities.isEmpty()) continue;
int centerX = sumX / clusterEntities.size();
int centerY = sumY / clusterEntities.size();
Measured Results (RSL 2025 kobe1 map, K=30)
- Before: some agents threw
ArithmeticException immediately; the remaining agents did not complete precompute and were manually killed after 6 minutes.
- After: precompute completed successfully — BUILD SUCCESSFUL in 49 seconds.
Complexity reduces from O(R·N·K·N) to O(R·N·log N).
Additional Notes
- The two
getNearEntity() overloads used exclusively in calcPathBased() (getNearEntity(list, entity) using comparePathDistance, and getNearEntity(list, x, y) using compareLineDistance) become dead code after the fix and can be removed.
initShortestPath() and shortestPathGraph are no longer needed. shortestPath() (used by getNearAgent()) can be updated to use weightedGraph.keySet() as the adjacency list.
LazyMap, HashSet, and Set imports can be removed.
Environment
Summary
calcPathBased()has two bugs that together make precompute unusable:comparePathDistance()runs an uncached BFS for every entity × center × iteration, causing precompute to exceed the time limit on standard maps.ArithmeticException: / by zero— when a cluster receives no entities during iteration,clusterEntitiesList.get(index).size()returns 0 and the subsequent division crashes the thread.Both bugs were confirmed on the RSL 2025 kobe1 map with the default configuration.
Bug 1: Excessive BFS calls in the assignment step
Inside the K-means iteration loop, each entity is assigned to its nearest center via:
This calls
comparePathDistance()for each candidate center, which in turn callsshortestPath()(BFS, O(N)) twice per comparison with no caching:Total complexity: O(R·N·K·N)
repeatPrecomputedefault)With these values: approximately 2.3 × 10⁹ BFS node accesses per precompute call.
Measured impact: precompute did not complete within 6 minutes and had to be killed.
Bug 2: Division by zero when a cluster becomes empty
In the center update step, if a cluster receives no entities, the code divides by zero:
This throws
ArithmeticExceptionand kills the agent thread before precompute data is written.Observed stack trace:
Note: the same
isEmpty()guard is also missing incalcStandard()at the corresponding location.Proposed Fix
For Bug 1: Replace the per-entity BFS with multi-source Dijkstra, which expands from all K centers simultaneously and assigns each entity to the nearest center in a single O(N log N) pass. This also eliminates the empty cluster problem (Bug 2) for connected graphs, since every reachable entity is guaranteed to be assigned.
This requires two changes:
Replace
initShortestPath(unweighted adjacency graph) withinitWeightedGraph(edge-weighted adjacency graph). Edge weight between adjacent areas A and B is defined to matchgetPathDistance():Replace the per-entity
getNearEntity()call withassignByMultiSourceDijkstra():For Bug 2 (defensive fix): Add an
isEmpty()guard before the division in bothcalcPathBased()andcalcStandard()to handle any remaining edge cases:Measured Results (RSL 2025 kobe1 map, K=30)
ArithmeticExceptionimmediately; the remaining agents did not complete precompute and were manually killed after 6 minutes.Complexity reduces from O(R·N·K·N) to O(R·N·log N).
Additional Notes
getNearEntity()overloads used exclusively incalcPathBased()(getNearEntity(list, entity)usingcomparePathDistance, andgetNearEntity(list, x, y)usingcompareLineDistance) become dead code after the fix and can be removed.initShortestPath()andshortestPathGraphare no longer needed.shortestPath()(used bygetNearAgent()) can be updated to useweightedGraph.keySet()as the adjacency list.LazyMap,HashSet, andSetimports can be removed.Environment
adf-core-javasrc/main/java/adf/impl/module/algorithm/KMeansClustering.java