Skip to content

KMeansClustering.calcPathBased() causes precompute to time out and throws ArithmeticException #25

@ToB213

Description

@ToB213

Summary

calcPathBased() has two bugs that together make precompute unusable:

  1. O(R·N·K·N) complexitycomparePathDistance() runs an uncached BFS for every entity × center × iteration, causing precompute to exceed the time limit on standard maps.
  2. 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:

  1. 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)
    
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions