Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit d0ceaf2

Browse files
perf: pre-compute spatial distance matrix in GraphDrawingQualityAnalyzer
Previously computeStress, computeNeighbourhoodPreservation, and computeOverlapRatio each independently iterated all O(V^2) vertex pairs, performing HashMap lookups and Point2D.distance() calls per pair. computeNeighbourhoodPreservation also allocated O(V) Map.Entry objects per vertex for sorting. Now a single buildSpatialIndex() pass pre-computes: - Coordinate arrays (posX[], posY[]) for cache-friendly access - Vertex-to-index map for O(1) lookup - Flat upper-triangle Euclidean distance matrix (double[]) All three methods now use direct array indexing instead of repeated HashMap.get() + Point2D.distance(). For a graph with V positioned vertices, this eliminates ~2V^2 redundant sqrt computations and ~V^2 Map.Entry allocations.
1 parent f760097 commit d0ceaf2

1 file changed

Lines changed: 117 additions & 43 deletions

File tree

Gvisual/src/gvisual/GraphDrawingQualityAnalyzer.java

Lines changed: 117 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ public class GraphDrawingQualityAnalyzer {
6262
// ── BFS distance cache ──────────────────────────────────────────
6363
private Map<String, Map<String, Integer>> distCache;
6464

65+
// ── Pre-computed spatial data (shared across stress/overlap/neighbourhood) ──
66+
/** Positioned vertices in stable order. */
67+
private List<String> posVerts;
68+
/** posVerts.size(). */
69+
private int posN;
70+
/** x[i], y[i] coordinates indexed by posVerts order. */
71+
private double[] posX, posY;
72+
/** Flat upper-triangle Euclidean distance matrix: dist(i,j) for i<j
73+
* stored at index i*posN - i*(i+1)/2 + (j-i-1). */
74+
private double[] pairDist;
75+
/** Maps vertex ID → index in posVerts (for fast lookup). */
76+
private Map<String, Integer> posIdx;
77+
6578
public GraphDrawingQualityAnalyzer(Graph<String, Edge> graph,
6679
Map<String, Point2D> positions) {
6780
this.graph = Objects.requireNonNull(graph);
@@ -140,6 +153,7 @@ public String generateReport() {
140153

141154
private synchronized void ensureComputed() {
142155
if (computed) return;
156+
buildSpatialIndex();
143157
computeEdgeCrossings();
144158
computeEdgeLengths();
145159
computeAngularResolution();
@@ -151,6 +165,48 @@ private synchronized void ensureComputed() {
151165
computed = true;
152166
}
153167

168+
/**
169+
* Pre-computes positioned vertex list, coordinate arrays, index map,
170+
* and pairwise Euclidean distance matrix. This is done once and shared
171+
* by computeStress, computeNeighbourhoodPreservation, and
172+
* computeOverlapRatio — eliminating three independent O(V²) passes
173+
* that each performed HashMap lookups and Point2D.distance() calls.
174+
*/
175+
private void buildSpatialIndex() {
176+
posVerts = new ArrayList<>();
177+
for (String v : graph.getVertices()) {
178+
if (positions.containsKey(v)) posVerts.add(v);
179+
}
180+
posN = posVerts.size();
181+
posX = new double[posN];
182+
posY = new double[posN];
183+
posIdx = new HashMap<>(posN * 2);
184+
for (int i = 0; i < posN; i++) {
185+
Point2D p = positions.get(posVerts.get(i));
186+
posX[i] = p.getX();
187+
posY[i] = p.getY();
188+
posIdx.put(posVerts.get(i), i);
189+
}
190+
// Flat upper-triangle distance matrix
191+
long triSize = (long) posN * (posN - 1) / 2;
192+
pairDist = new double[(int) triSize];
193+
int idx = 0;
194+
for (int i = 0; i < posN; i++) {
195+
double xi = posX[i], yi = posY[i];
196+
for (int j = i + 1; j < posN; j++) {
197+
double dx = xi - posX[j];
198+
double dy = yi - posY[j];
199+
pairDist[idx++] = Math.sqrt(dx * dx + dy * dy);
200+
}
201+
}
202+
}
203+
204+
/** Returns the pre-computed Euclidean distance between posVerts[i] and posVerts[j] (i < j). */
205+
private double spatialDist(int i, int j) {
206+
if (i > j) { int t = i; i = j; j = t; }
207+
return pairDist[i * posN - i * (i + 1) / 2 + (j - i - 1)];
208+
}
209+
154210
// ── Edge crossings ──────────────────────────────────────────────
155211

156212
private void computeEdgeCrossings() {
@@ -253,27 +309,30 @@ private void computeAngularResolution() {
253309

254310
// ── Stress (Kamada-Kawai) ───────────────────────────────────────
255311

312+
/**
313+
* Kamada-Kawai stress using pre-computed spatial distance matrix.
314+
* Avoids per-pair HashMap lookups and Point2D.distance() calls.
315+
*/
256316
private void computeStress() {
257-
List<String> verts = new ArrayList<>();
258-
for (String v : graph.getVertices()) {
259-
if (positions.containsKey(v)) verts.add(v);
260-
}
261-
if (verts.size() < 2) { stress = 0; return; }
317+
if (posN < 2) { stress = 0; return; }
262318

263-
ensureDistCache(verts);
319+
ensureDistCache(posVerts);
264320

265321
double totalStress = 0;
266322
double normaliser = 0;
267-
for (int i = 0; i < verts.size(); i++) {
268-
for (int j = i + 1; j < verts.size(); j++) {
269-
String u = verts.get(i), v = verts.get(j);
270-
Integer dij = distCache.getOrDefault(u, Collections.emptyMap()).get(v);
323+
for (int i = 0; i < posN; i++) {
324+
String u = posVerts.get(i);
325+
Map<String, Integer> uDist = distCache.get(u);
326+
if (uDist == null) continue;
327+
for (int j = i + 1; j < posN; j++) {
328+
Integer dij = uDist.get(posVerts.get(j));
271329
if (dij == null || dij == 0) continue;
272330

273-
double drawDist = positions.get(u).distance(positions.get(v));
274-
double ideal = dij * edgeLengthMean; // scale graph distance by mean edge length
275-
double w = 1.0 / (dij * dij);
276-
totalStress += w * (drawDist - ideal) * (drawDist - ideal);
331+
double drawDist = spatialDist(i, j);
332+
double ideal = dij * edgeLengthMean;
333+
double w = 1.0 / ((double) dij * dij);
334+
double diff = drawDist - ideal;
335+
totalStress += w * diff * diff;
277336
normaliser += w * ideal * ideal;
278337
}
279338
}
@@ -282,43 +341,57 @@ private void computeStress() {
282341

283342
// ── Neighbourhood preservation ──────────────────────────────────
284343

344+
/**
345+
* Neighbourhood preservation using pre-computed spatial distances.
346+
*
347+
* <p>For each vertex, finds the k nearest vertices in the drawing and
348+
* checks overlap with graph neighbours. Uses array-indexed distances
349+
* from the pre-computed matrix instead of per-vertex Point2D.distance()
350+
* calls and Map.Entry allocations. Sorts an int[] of indices by distance
351+
* rather than allocating O(V) Map.Entry objects per vertex.</p>
352+
*/
285353
private void computeNeighbourhoodPreservation() {
286354
int totalNeighbours = 0;
287355
int preserved = 0;
288356

289-
List<String> verts = new ArrayList<>();
290-
for (String v : graph.getVertices()) {
291-
if (positions.containsKey(v)) verts.add(v);
292-
}
357+
// Reusable array for sorting neighbour indices by distance
358+
Integer[] sortIndices = new Integer[posN];
359+
for (int i = 0; i < posN; i++) sortIndices[i] = i;
293360

294-
for (String v : verts) {
361+
for (int vi = 0; vi < posN; vi++) {
362+
String v = posVerts.get(vi);
295363
Collection<String> nbrs = graph.getNeighbors(v);
296364
if (nbrs == null || nbrs.isEmpty()) continue;
297365

298366
int k = 0;
299367
for (String n : nbrs) {
300-
if (positions.containsKey(n)) k++;
368+
if (posIdx.containsKey(n)) k++;
301369
}
302370
if (k == 0) continue;
303371

304-
// find k nearest in drawing
305-
Point2D pv = positions.get(v);
306-
List<Map.Entry<String, Double>> dists = new ArrayList<>();
307-
for (String u : verts) {
308-
if (u.equals(v)) continue;
309-
dists.add(new AbstractMap.SimpleEntry<>(u, pv.distance(positions.get(u))));
310-
}
311-
dists.sort(Comparator.comparingDouble(Map.Entry::getValue));
312-
313-
Set<String> kNearest = new HashSet<>();
314-
for (int i = 0; i < Math.min(k, dists.size()); i++) {
315-
kNearest.add(dists.get(i).getKey());
372+
// Sort all other vertex indices by distance to vi
373+
final int src = vi;
374+
Arrays.sort(sortIndices, (a, b) -> {
375+
if (a == src) return 1; // push self to end
376+
if (b == src) return -1;
377+
return Double.compare(spatialDist(src, a), spatialDist(src, b));
378+
});
379+
380+
// Collect k nearest (skip self)
381+
Set<Integer> kNearest = new HashSet<>(k * 2);
382+
int found = 0;
383+
for (int i = 0; i < posN && found < k; i++) {
384+
int idx = sortIndices[i];
385+
if (idx == vi) continue;
386+
kNearest.add(idx);
387+
found++;
316388
}
317389

318390
for (String n : nbrs) {
319-
if (positions.containsKey(n)) {
391+
Integer ni = posIdx.get(n);
392+
if (ni != null) {
320393
totalNeighbours++;
321-
if (kNearest.contains(n)) preserved++;
394+
if (kNearest.contains(ni)) preserved++;
322395
}
323396
}
324397
}
@@ -329,22 +402,23 @@ private void computeNeighbourhoodPreservation() {
329402

330403
// ── Overlap ratio ───────────────────────────────────────────────
331404

405+
/**
406+
* Overlap ratio using pre-computed spatial distance matrix.
407+
* Direct array access replaces per-pair HashMap lookups and
408+
* Point2D.distance() calls.
409+
*/
332410
private void computeOverlapRatio() {
333-
List<String> verts = new ArrayList<>();
334-
for (String v : graph.getVertices()) {
335-
if (positions.containsKey(v)) verts.add(v);
336-
}
337-
if (verts.size() < 2) { overlapRatio = 0; return; }
411+
if (posN < 2) { overlapRatio = 0; return; }
338412

339-
// threshold: 5% of average edge length or 10 pixels, whichever is larger
340413
double threshold = Math.max(edgeLengthMean * 0.05, 10.0);
341414
int overlaps = 0;
342415
int pairs = 0;
416+
int idx = 0;
343417

344-
for (int i = 0; i < verts.size(); i++) {
345-
for (int j = i + 1; j < verts.size(); j++) {
418+
for (int i = 0; i < posN; i++) {
419+
for (int j = i + 1; j < posN; j++) {
346420
pairs++;
347-
if (positions.get(verts.get(i)).distance(positions.get(verts.get(j))) < threshold) {
421+
if (pairDist[idx++] < threshold) {
348422
overlaps++;
349423
}
350424
}

0 commit comments

Comments
 (0)