Commit 35a777b9 authored by Numanoglu's avatar Numanoglu
Browse files

Add BVH split strategy heuristics

parent fb72860f
Pipeline #12403 passed with stage
in 2 minutes and 10 seconds
...@@ -309,3 +309,6 @@ gradle-app.setting ...@@ -309,3 +309,6 @@ gradle-app.setting
/.idea/codeStyles/codeStyleConfig.xml /.idea/codeStyles/codeStyleConfig.xml
/GUISettings.properties /GUISettings.properties
/.idea/inspectionProfiles/Project_Default.xml /.idea/inspectionProfiles/Project_Default.xml
# Local empirical data for BVH exploration
/CityDoctorValidation/src/test/resources/real-citygml/
...@@ -47,6 +47,30 @@ public class AABB { ...@@ -47,6 +47,30 @@ public class AABB {
return new AABB(minX, minY, minZ, maxX, maxY, maxZ); return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
} }
public static AABB enclosing(List<AABB> boxes) {
Objects.requireNonNull(boxes, "boxes");
if (boxes.isEmpty()) {
throw new IllegalArgumentException("boxes must not be empty");
}
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double minZ = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double maxZ = Double.NEGATIVE_INFINITY;
for (AABB box : boxes) {
minX = Math.min(minX, box.getMinX());
minY = Math.min(minY, box.getMinY());
minZ = Math.min(minZ, box.getMinZ());
maxX = Math.max(maxX, box.getMaxX());
maxY = Math.max(maxY, box.getMaxY());
maxZ = Math.max(maxZ, box.getMaxZ());
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
/** /**
* Computes an AABB for a single vertex degenerate AABB with min=max=vertex * Computes an AABB for a single vertex degenerate AABB with min=max=vertex
* coordinates * coordinates
...@@ -190,6 +214,26 @@ public class AABB { ...@@ -190,6 +214,26 @@ public class AABB {
return maxZ; return maxZ;
} }
public double getExtentX() {
return maxX - minX;
}
public double getExtentY() {
return maxY - minY;
}
public double getExtentZ() {
return maxZ - minZ;
}
public double getLongestExtent() {
return Math.max(getExtentX(), Math.max(getExtentY(), getExtentZ()));
}
public double getShortestExtent() {
return Math.min(getExtentX(), Math.min(getExtentY(), getExtentZ()));
}
public double getCenterX() { public double getCenterX() {
return (minX + maxX) / 2.0; return (minX + maxX) / 2.0;
} }
...@@ -234,9 +278,9 @@ public class AABB { ...@@ -234,9 +278,9 @@ public class AABB {
* Returns the index of the longest axis in the AABB * Returns the index of the longest axis in the AABB
*/ */
public int findLongestAxis() { public int findLongestAxis() {
double x = getMaxX() - getMinX(); double x = getExtentX();
double y = getMaxY() - getMinY(); double y = getExtentY();
double z = getMaxZ() - getMinZ(); double z = getExtentZ();
if (x > y && x > z) if (x > y && x > z)
return 0; return 0;
...@@ -247,9 +291,9 @@ public class AABB { ...@@ -247,9 +291,9 @@ public class AABB {
/** Returns true if the AABB is (nearly) flat in at least two axes. */ /** Returns true if the AABB is (nearly) flat in at least two axes. */
public boolean isDegenerate(double tol) { public boolean isDegenerate(double tol) {
double dx = getMaxX() - getMinX(); double dx = getExtentX();
double dy = getMaxY() - getMinY(); double dy = getExtentY();
double dz = getMaxZ() - getMinZ(); double dz = getExtentZ();
int flatAxes = 0; int flatAxes = 0;
if (dx <= tol) if (dx <= tol)
flatAxes++; flatAxes++;
......
...@@ -35,7 +35,6 @@ import de.hft.stuttgart.citydoctor2.check.Requirement; ...@@ -35,7 +35,6 @@ import de.hft.stuttgart.citydoctor2.check.Requirement;
import de.hft.stuttgart.citydoctor2.check.ResultStatus; import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.NestedRingError; import de.hft.stuttgart.citydoctor2.check.error.NestedRingError;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy; import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy.BvhCheckType;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils; import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
...@@ -136,11 +135,32 @@ public class NestedRingsCheck extends Check { ...@@ -136,11 +135,32 @@ public class NestedRingsCheck extends Check {
checkWithBoundingBoxFilter(p); checkWithBoundingBoxFilter(p);
} else if (variant.isBvh()) { } else if (variant.isBvh()) {
checkWithBvhFilter(p, variant.getSplitStrategy()); checkWithBvhFilter(p, variant.getSplitStrategy());
} else if (BvhUsagePolicy.shouldUseTree(BvhCheckType.NESTED_RINGS, p.getInnerRings().size())) { } else {
checkAuto(p);
}
}
private void checkAuto(Polygon p) {
List<LinearRing> innerRings = p.getInnerRings();
if (innerRings.size() < 2) {
checkOriginal(p);
return;
}
Map<LinearRing, AABB> ringBoxes = new HashMap<>(innerRings.size());
List<AABB> boxes = new ArrayList<>(innerRings.size());
for (LinearRing ring : innerRings) {
AABB box = AABB.of(ring);
ringBoxes.put(ring, box);
boxes.add(box);
}
BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofAabbs(boxes);
if (BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.NESTED_RINGS, summary)) {
SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy( SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy(
BvhCheckType.NESTED_RINGS, p.getInnerRings().size()); BvhUsagePolicy.BvhCheckType.NESTED_RINGS, summary);
checkWithBvhFilter(p, splitStrategy); checkWithBvhFilter(p, splitStrategy, ringBoxes);
} else if (p.getInnerRings().size() > 3) { } else if (innerRings.size() > 3) {
checkWithBoundingBoxFilter(p); checkWithBoundingBoxFilter(p);
} else { } else {
checkOriginal(p); checkOriginal(p);
...@@ -183,6 +203,11 @@ public class NestedRingsCheck extends Check { ...@@ -183,6 +203,11 @@ public class NestedRingsCheck extends Check {
ringBoxes.put(ring, AABB.of(ring)); ringBoxes.put(ring, AABB.of(ring));
} }
checkWithBvhFilter(p, splitStrategy, ringBoxes);
}
private void checkWithBvhFilter(Polygon p, SplitStrategy splitStrategy, Map<LinearRing, AABB> ringBoxes) {
List<LinearRing> innerRings = p.getInnerRings();
BoundingVolumeHierarchyTree<LinearRing> ringTree = BoundingVolumeHierarchyTree<LinearRing> ringTree =
BoundingVolumeHierarchyTree.newWithStrategy(innerRings, ringBoxes::get, splitStrategy); BoundingVolumeHierarchyTree.newWithStrategy(innerRings, ringBoxes::get, splitStrategy);
......
...@@ -37,7 +37,6 @@ import de.hft.stuttgart.citydoctor2.check.error.PointTouchesEdgeError; ...@@ -37,7 +37,6 @@ import de.hft.stuttgart.citydoctor2.check.error.PointTouchesEdgeError;
import de.hft.stuttgart.citydoctor2.check.error.RingEdgeIntersectionError; import de.hft.stuttgart.citydoctor2.check.error.RingEdgeIntersectionError;
import de.hft.stuttgart.citydoctor2.checks.Checks; import de.hft.stuttgart.citydoctor2.checks.Checks;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy; import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy.BvhCheckType;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils; import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.datastructure.Edge; import de.hft.stuttgart.citydoctor2.datastructure.Edge;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
...@@ -158,9 +157,9 @@ public class RingSelfIntCheck extends Check { ...@@ -158,9 +157,9 @@ public class RingSelfIntCheck extends Check {
} }
if (variant == Variant.AUTO) { if (variant == Variant.AUTO) {
int edgeCount = Math.max(0, lr.getVertices().size() - 1); int edgeCount = Math.max(0, lr.getVertices().size() - 1);
if (BvhUsagePolicy.shouldUseTree(BvhCheckType.RING_SELF_INTERSECTION, edgeCount)) { if (BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, edgeCount)) {
SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy( SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy(
BvhCheckType.RING_SELF_INTERSECTION, edgeCount); BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, edgeCount);
checkRingBvh(lr, splitStrategy); checkRingBvh(lr, splitStrategy);
} else { } else {
checkRingOld(lr); checkRingOld(lr);
......
...@@ -33,11 +33,11 @@ import de.hft.stuttgart.citydoctor2.check.RequirementType; ...@@ -33,11 +33,11 @@ import de.hft.stuttgart.citydoctor2.check.RequirementType;
import de.hft.stuttgart.citydoctor2.check.ResultStatus; import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy; import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy.BvhCheckType;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils; import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil; import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType; import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree; import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
...@@ -155,12 +155,12 @@ public class SolidSelfIntCheck extends Check { ...@@ -155,12 +155,12 @@ public class SolidSelfIntCheck extends Check {
return calculateIntersectionsWithTree(g, variant.getSplitStrategy()); return calculateIntersectionsWithTree(g, variant.getSplitStrategy());
} }
int polygonCount = g.getPolygons().size(); BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofElements(g.getPolygons(), AABB::of);
if (!BvhUsagePolicy.shouldUseTree(BvhCheckType.SOLID_SELF_INTERSECTION, polygonCount)) { if (!BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary)) {
return SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta); return SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta);
} }
SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy( SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy(
BvhCheckType.SOLID_SELF_INTERSECTION, polygonCount); BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary);
return calculateIntersectionsWithTree(g, splitStrategy); return calculateIntersectionsWithTree(g, splitStrategy);
} }
......
package de.hft.stuttgart.citydoctor2.checks.util; package de.hft.stuttgart.citydoctor2.checks.util;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
/** /**
...@@ -10,6 +15,8 @@ public final class BvhUsagePolicy { ...@@ -10,6 +15,8 @@ public final class BvhUsagePolicy {
private static final int SOLID_SELF_INTERSECTION_TREE_THRESHOLD = 16; private static final int SOLID_SELF_INTERSECTION_TREE_THRESHOLD = 16;
private static final int NESTED_RINGS_TREE_THRESHOLD = 8; private static final int NESTED_RINGS_TREE_THRESHOLD = 8;
private static final int RING_SELF_INTERSECTION_TREE_THRESHOLD = 32; private static final int RING_SELF_INTERSECTION_TREE_THRESHOLD = 32;
private static final double THIN_BOX_ASPECT_RATIO = 20.0;
private static final double DEGENERATE_TOLERANCE = 1e-12;
private BvhUsagePolicy() { private BvhUsagePolicy() {
} }
...@@ -20,18 +27,253 @@ public final class BvhUsagePolicy { ...@@ -20,18 +27,253 @@ public final class BvhUsagePolicy {
RING_SELF_INTERSECTION RING_SELF_INTERSECTION
} }
public static final class BvhInputSummary {
private final int elementCount;
private final double extentX;
private final double extentY;
private final double extentZ;
private final double flatnessRatio;
private final double averageRelativeBoxExtent;
private final double thinBoxRatio;
private final double averageRelativeBoxVolume;
private final double centerSpreadRatio;
private BvhInputSummary(
int elementCount,
double extentX,
double extentY,
double extentZ,
double flatnessRatio,
double averageRelativeBoxExtent,
double thinBoxRatio,
double averageRelativeBoxVolume,
double centerSpreadRatio) {
this.elementCount = elementCount;
this.extentX = extentX;
this.extentY = extentY;
this.extentZ = extentZ;
this.flatnessRatio = flatnessRatio;
this.averageRelativeBoxExtent = averageRelativeBoxExtent;
this.thinBoxRatio = thinBoxRatio;
this.averageRelativeBoxVolume = averageRelativeBoxVolume;
this.centerSpreadRatio = centerSpreadRatio;
}
public static BvhInputSummary ofAabbs(List<AABB> boxes) {
Objects.requireNonNull(boxes, "boxes");
if (boxes.isEmpty()) {
return empty();
}
AABB totalBox = AABB.enclosing(boxes);
double extentX = totalBox.getExtentX();
double extentY = totalBox.getExtentY();
double extentZ = totalBox.getExtentZ();
double relativeExtentSum = 0.0;
double relativeVolumeSum = 0.0;
double centerXSum = 0.0;
double centerYSum = 0.0;
double centerZSum = 0.0;
int thinBoxCount = 0;
for (AABB box : boxes) {
relativeExtentSum += relativeExtent(box.getExtentX(), extentX);
relativeExtentSum += relativeExtent(box.getExtentY(), extentY);
relativeExtentSum += relativeExtent(box.getExtentZ(), extentZ);
relativeVolumeSum += relativeVolume(box, totalBox);
if (aspectRatio(box) >= THIN_BOX_ASPECT_RATIO) {
thinBoxCount++;
}
centerXSum += box.getCenterX();
centerYSum += box.getCenterY();
centerZSum += box.getCenterZ();
}
double maxExtent = totalBox.getLongestExtent();
double minExtent = totalBox.getShortestExtent();
double flatnessRatio = maxExtent == 0.0 ? 0.0 : minExtent / maxExtent;
double averageRelativeBoxExtent = relativeExtentSum / (boxes.size() * 3.0);
double thinBoxRatio = (double) thinBoxCount / boxes.size();
double averageRelativeBoxVolume = relativeVolumeSum / boxes.size();
double centerSpreadRatio = centerSpreadRatio(
boxes,
centerXSum / boxes.size(),
centerYSum / boxes.size(),
centerZSum / boxes.size(),
totalBox);
return new BvhInputSummary(
boxes.size(),
extentX,
extentY,
extentZ,
flatnessRatio,
averageRelativeBoxExtent,
thinBoxRatio,
averageRelativeBoxVolume,
centerSpreadRatio);
}
public static <E> BvhInputSummary ofElements(List<E> elements, Function<E, AABB> aabbFunction) {
Objects.requireNonNull(elements, "elements");
Objects.requireNonNull(aabbFunction, "aabbFunction");
return ofAabbs(elements.stream().map(aabbFunction).toList());
}
public static BvhInputSummary countOnly(int elementCount) {
return new BvhInputSummary(elementCount, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0);
}
private static BvhInputSummary empty() {
return countOnly(0);
}
private static double relativeExtent(double localExtent, double totalExtent) {
return totalExtent == 0.0 ? 0.0 : localExtent / totalExtent;
}
private static double relativeVolume(AABB box, AABB totalBox) {
double localMeasure = 1.0;
double totalMeasure = 1.0;
boolean hasActiveExtent = false;
if (box.getExtentX() > DEGENERATE_TOLERANCE) {
localMeasure *= box.getExtentX();
totalMeasure *= totalBox.getExtentX();
hasActiveExtent = true;
}
if (box.getExtentY() > DEGENERATE_TOLERANCE) {
localMeasure *= box.getExtentY();
totalMeasure *= totalBox.getExtentY();
hasActiveExtent = true;
}
if (box.getExtentZ() > DEGENERATE_TOLERANCE) {
localMeasure *= box.getExtentZ();
totalMeasure *= totalBox.getExtentZ();
hasActiveExtent = true;
}
if (!hasActiveExtent || totalMeasure <= DEGENERATE_TOLERANCE) {
return 0.0;
}
return localMeasure / totalMeasure;
}
private static double aspectRatio(AABB box) {
double longest = box.getLongestExtent();
double shortest = shortestPositiveExtent(box);
return shortest == 0.0 ? Double.POSITIVE_INFINITY : longest / shortest;
}
private static double shortestPositiveExtent(AABB box) {
double shortest = Double.POSITIVE_INFINITY;
if (box.getExtentX() > 0.0) {
shortest = Math.min(shortest, box.getExtentX());
}
if (box.getExtentY() > 0.0) {
shortest = Math.min(shortest, box.getExtentY());
}
if (box.getExtentZ() > 0.0) {
shortest = Math.min(shortest, box.getExtentZ());
}
return shortest == Double.POSITIVE_INFINITY ? 0.0 : shortest;
}
private static double centerSpreadRatio(
List<AABB> boxes,
double meanCenterX,
double meanCenterY,
double meanCenterZ,
AABB totalBox) {
double varianceX = 0.0;
double varianceY = 0.0;
double varianceZ = 0.0;
for (AABB box : boxes) {
varianceX += square(box.getCenterX() - meanCenterX);
varianceY += square(box.getCenterY() - meanCenterY);
varianceZ += square(box.getCenterZ() - meanCenterZ);
}
double spreadX = normalizedStdDev(varianceX, boxes.size(), totalBox.getExtentX());
double spreadY = normalizedStdDev(varianceY, boxes.size(), totalBox.getExtentY());
double spreadZ = normalizedStdDev(varianceZ, boxes.size(), totalBox.getExtentZ());
return (spreadX + spreadY + spreadZ) / 3.0;
}
private static double normalizedStdDev(double varianceSum, int count, double extent) {
if (extent == 0.0) {
return 0.0;
}
return Math.sqrt(varianceSum / count) / extent;
}
private static double square(double value) {
return value * value;
}
public int getElementCount() {
return elementCount;
}
public double getExtentX() {
return extentX;
}
public double getExtentY() {
return extentY;
}
public double getExtentZ() {
return extentZ;
}
public double getFlatnessRatio() {
return flatnessRatio;
}
public double getAverageRelativeBoxExtent() {
return averageRelativeBoxExtent;
}
public double getThinBoxRatio() {
return thinBoxRatio;
}
public double getAverageRelativeBoxVolume() {
return averageRelativeBoxVolume;
}
public double getCenterSpreadRatio() {
return centerSpreadRatio;
}
}
public static boolean shouldUseTree(BvhCheckType checkType, int elementCount) { public static boolean shouldUseTree(BvhCheckType checkType, int elementCount) {
return shouldUseTree(checkType, BvhInputSummary.countOnly(elementCount));
}
public static boolean shouldUseTree(BvhCheckType checkType, BvhInputSummary summary) {
int elementCount = summary.getElementCount();
if (elementCount < 2) { if (elementCount < 2) {
return false; return false;
} }
return elementCount >= thresholdFor(checkType); if (elementCount < thresholdFor(checkType)) {
return false;
}
if (summary.getAverageRelativeBoxExtent() > 0.65 && elementCount < 128) {
return false;
}
return true;
} }
public static SplitStrategy chooseSplitStrategy(BvhCheckType checkType, int elementCount) { public static SplitStrategy chooseSplitStrategy(BvhCheckType checkType, int elementCount) {
return chooseSplitStrategy(checkType, BvhInputSummary.countOnly(elementCount));
}
public static SplitStrategy chooseSplitStrategy(BvhCheckType checkType, BvhInputSummary summary) {
if (checkType == BvhCheckType.RING_SELF_INTERSECTION) { if (checkType == BvhCheckType.RING_SELF_INTERSECTION) {
return SplitStrategy.BINARY_SPATIAL_MEDIAN; return SplitStrategy.BINARY_SPATIAL_MEDIAN;
} }
if (elementCount >= 128) { if (summary.getFlatnessRatio() < 0.02) {
return SplitStrategy.BINARY_SPATIAL_MEDIAN;
}
if (summary.getElementCount() >= 128) {
return SplitStrategy.OCTONARY_OBJECT_MEAN; return SplitStrategy.OCTONARY_OBJECT_MEAN;
} }
return SplitStrategy.BINARY_SPATIAL_MEDIAN; return SplitStrategy.BINARY_SPATIAL_MEDIAN;
......
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
import de.hft.stuttgart.citydoctor2.checks.geometry.NestedRingsCheck;
import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
public final class BvhHeuristicTimingSupport {
private BvhHeuristicTimingSupport() {
}
public static Observation measureVariants(
String checkName,
String scenarioName,
BvhInputMetricsCollector.Metrics metrics,
int inputSize,
String baselineName,
MeasuredOperation baselineOperation,
StrategyOperation strategyOperation) {
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
BvhPerformanceTestSupport.Measurement baseline = BvhPerformanceTestSupport.measure(
scenarioName,
baselineName,
inputSize,
baselineOperation::run);
measurements.add(baseline);
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
BvhPerformanceTestSupport.Measurement bvhMeasurement = BvhPerformanceTestSupport.measure(
scenarioName,
strategy.name(),
inputSize,
() -> strategyOperation.run(strategy));
measurements.add(bvhMeasurement);
assertEquals(checkName + " result differs for " + scenarioName + " / " + strategy,
baseline.resultCount, bvhMeasurement.resultCount);
}
String syntheticPrediction = predictFromSyntheticPolicy(checkName, metrics);
String candidatePrediction = predictCandidateStrategy(checkName, metrics);
return new Observation(
checkName,
metrics,
fastest(measurements),
fastestBvh(measurements),
baseline,
syntheticPrediction,
candidatePrediction,
measurementForPrediction(measurements, candidatePrediction));
}
public static Observation measureBvhStrategies(
String checkName,
String scenarioName,
BvhInputMetricsCollector.Metrics metrics,
int inputSize,
StrategyOperation strategyOperation) {
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
BvhPerformanceTestSupport.Measurement baseline = null;
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
BvhPerformanceTestSupport.Measurement bvhMeasurement = BvhPerformanceTestSupport.measure(
scenarioName,
strategy.name(),
inputSize,
() -> strategyOperation.run(strategy));
if (baseline == null) {
baseline = bvhMeasurement;
} else {
assertEquals(checkName + " broad-phase count differs for " + scenarioName + " / " + strategy,
baseline.resultCount, bvhMeasurement.resultCount);
}
measurements.add(bvhMeasurement);
}
String syntheticPrediction = predictFromSyntheticPolicy(checkName, metrics);
String candidatePrediction = predictCandidateStrategy(checkName, metrics);
return new Observation(
checkName,
metrics,
fastest(measurements),
fastestBvh(measurements),
baseline,
syntheticPrediction,
candidatePrediction,
measurementForPrediction(measurements, candidatePrediction));
}
public static String predictFromSyntheticPolicy(String checkName, BvhInputMetricsCollector.Metrics metrics) {
BvhUsagePolicy.BvhCheckType checkType = checkTypeFor(checkName);
if (!BvhUsagePolicy.shouldUseTree(checkType, metrics.summary)) {
return "OLD";
}
return BvhUsagePolicy.chooseSplitStrategy(checkType, metrics.summary).name();
}
public static String predictCandidateStrategy(String checkName, BvhInputMetricsCollector.Metrics metrics) {
BvhUsagePolicy.BvhCheckType checkType = checkTypeFor(checkName);
if (!BvhUsagePolicy.shouldUseTree(checkType, metrics.summary)) {
return "OLD";
}
if ("SSI".equals(checkName)
&& metrics.averageAspectRatio >= 20.0
&& metrics.averageAspectRatio < 80.0) {
return SplitStrategy.OCTONARY_SPATIAL_MEDIAN.name();
}
if ("NESTED".equals(checkName)
&& metrics.averageRelativeBoxVolume <= 0.0001) {
return SplitStrategy.OCTONARY_OBJECT_MEAN.name();
}
if ("RSI".equals(checkName)
&& metrics.thinBoxRate > 0.75) {
return SplitStrategy.OCTONARY_SPATIAL_MEDIAN.name();
}
if ("RSI".equals(checkName)
&& metrics.averageAspectRatio >= 80.0) {
return SplitStrategy.OCTONARY_SPATIAL_MEDIAN.name();
}
if ("RSI".equals(checkName)
&& metrics.averageRelativeBoxVolume <= 0.0001) {
return SplitStrategy.OCTONARY_SPATIAL_MEDIAN.name();
}
return BvhUsagePolicy.chooseSplitStrategy(checkType, metrics.summary).name();
}
public static NestedRingsCheck.Variant nestedVariantFor(SplitStrategy strategy) {
switch (strategy) {
case BINARY_OBJECT_MEDIAN:
return NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEDIAN;
case BINARY_OBJECT_MEAN:
return NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEAN;
case BINARY_SPATIAL_MEDIAN:
return NestedRingsCheck.Variant.BVH_BINARY_SPATIAL_MEDIAN;
case OCTONARY_OBJECT_MEDIAN:
return NestedRingsCheck.Variant.BVH_OCTONARY_OBJECT_MEDIAN;
case OCTONARY_OBJECT_MEAN:
return NestedRingsCheck.Variant.BVH_OCTONARY_OBJECT_MEAN;
case OCTONARY_SPATIAL_MEDIAN:
return NestedRingsCheck.Variant.BVH_OCTONARY_SPATIAL_MEDIAN;
case AUTO:
default:
throw new IllegalArgumentException("Unsupported nested-ring BVH strategy: " + strategy);
}
}
public static RingSelfIntCheck.Variant rsiVariantFor(SplitStrategy strategy) {
switch (strategy) {
case BINARY_OBJECT_MEDIAN:
return RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEDIAN;
case BINARY_OBJECT_MEAN:
return RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEAN;
case BINARY_SPATIAL_MEDIAN:
return RingSelfIntCheck.Variant.BVH_BINARY_SPATIAL_MEDIAN;
case OCTONARY_OBJECT_MEDIAN:
return RingSelfIntCheck.Variant.BVH_OCTONARY_OBJECT_MEDIAN;
case OCTONARY_OBJECT_MEAN:
return RingSelfIntCheck.Variant.BVH_OCTONARY_OBJECT_MEAN;
case OCTONARY_SPATIAL_MEDIAN:
return RingSelfIntCheck.Variant.BVH_OCTONARY_SPATIAL_MEDIAN;
case AUTO:
default:
throw new IllegalArgumentException("Unsupported ring-self-intersection BVH strategy: " + strategy);
}
}
private static BvhUsagePolicy.BvhCheckType checkTypeFor(String checkName) {
if ("SSI".equals(checkName)) {
return BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION;
}
if ("NESTED".equals(checkName)) {
return BvhUsagePolicy.BvhCheckType.NESTED_RINGS;
}
if ("RSI".equals(checkName)) {
return BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION;
}
throw new IllegalArgumentException("Unsupported check name: " + checkName);
}
private static BvhPerformanceTestSupport.Measurement fastest(
List<BvhPerformanceTestSupport.Measurement> measurements) {
BvhPerformanceTestSupport.Measurement fastest = measurements.get(0);
for (BvhPerformanceTestSupport.Measurement measurement : measurements) {
if (measurement.averageNanos < fastest.averageNanos) {
fastest = measurement;
}
}
return fastest;
}
private static BvhPerformanceTestSupport.Measurement fastestBvh(
List<BvhPerformanceTestSupport.Measurement> measurements) {
BvhPerformanceTestSupport.Measurement fastest = null;
for (BvhPerformanceTestSupport.Measurement measurement : measurements) {
if ("BRUTE_FORCE".equals(measurement.variant)
|| NestedRingsCheck.Variant.OLD.name().equals(measurement.variant)
|| RingSelfIntCheck.Variant.OLD.name().equals(measurement.variant)) {
continue;
}
if (fastest == null || measurement.averageNanos < fastest.averageNanos) {
fastest = measurement;
}
}
return fastest;
}
private static BvhPerformanceTestSupport.Measurement measurementForPrediction(
List<BvhPerformanceTestSupport.Measurement> measurements,
String prediction) {
for (BvhPerformanceTestSupport.Measurement measurement : measurements) {
if (measurement.variant.equals(prediction)) {
return measurement;
}
}
return null;
}
public interface MeasuredOperation {
int run();
}
public interface StrategyOperation {
int run(SplitStrategy strategy);
}
public static final class Observation {
public final String checkName;
public final BvhInputMetricsCollector.Metrics metrics;
public final BvhPerformanceTestSupport.Measurement fastest;
public final BvhPerformanceTestSupport.Measurement fastestBvh;
public final BvhPerformanceTestSupport.Measurement baseline;
public final BvhPerformanceTestSupport.Measurement predictedBvh;
public final String syntheticPrediction;
public final String candidatePrediction;
Observation(
String checkName,
BvhInputMetricsCollector.Metrics metrics,
BvhPerformanceTestSupport.Measurement fastest,
BvhPerformanceTestSupport.Measurement fastestBvh,
BvhPerformanceTestSupport.Measurement baseline,
String syntheticPrediction,
String candidatePrediction,
BvhPerformanceTestSupport.Measurement predictedBvh) {
this.checkName = checkName;
this.metrics = metrics;
this.fastest = fastest;
this.fastestBvh = fastestBvh;
this.baseline = baseline;
this.syntheticPrediction = syntheticPrediction;
this.candidatePrediction = candidatePrediction;
this.predictedBvh = predictedBvh;
}
public String candidatePredictionMatchLabel() {
if ("OLD".equals(candidatePrediction)) {
return "n/a";
}
return candidatePrediction.equals(fastestBvh.variant) ? "yes" : "no";
}
}
}
...@@ -8,12 +8,13 @@ import java.util.function.Function; ...@@ -8,12 +8,13 @@ import java.util.function.Function;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB; import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
/** /**
* Collects cheap broad-phase shape metrics for BVH strategy experiments. * Collects cheap broad-phase shape metrics for BVH strategy experiments.
* *
* These metrics are intentionally geometry-agnostic: the same collector can be * These metrics are intentionally geometry-agnostic: the same collector can be
* used for solid polygons, nested-ring rings, ring-self-intersection edges later, * used for solid polygons, nested-ring rings, ring-self-intersection edges,
* synthetic fixtures, and parsed CityGML models. * synthetic fixtures, and parsed CityGML models.
* *
* @author Numanoglu * @author Numanoglu
...@@ -22,6 +23,7 @@ final class BvhInputMetricsCollector { ...@@ -22,6 +23,7 @@ final class BvhInputMetricsCollector {
private static final double DEGENERATE_TOLERANCE = 1e-12; private static final double DEGENERATE_TOLERANCE = 1e-12;
private static final long MAX_PAIR_SAMPLES = 20_000L; private static final long MAX_PAIR_SAMPLES = 20_000L;
private static final double THIN_BOX_ASPECT_RATIO = 20.0;
private BvhInputMetricsCollector() { private BvhInputMetricsCollector() {
} }
...@@ -30,14 +32,30 @@ final class BvhInputMetricsCollector { ...@@ -30,14 +32,30 @@ final class BvhInputMetricsCollector {
return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal())); return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal()));
} }
static Metrics forPolygonsCheap(String scenario, List<? extends Polygon> polygons) {
return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal()), false);
}
static Metrics forRings(String scenario, List<? extends LinearRing> rings) { static Metrics forRings(String scenario, List<? extends LinearRing> rings) {
return collect(scenario, rings, AABB::of); return collect(scenario, rings, AABB::of);
} }
static Metrics forRingsCheap(String scenario, List<? extends LinearRing> rings) {
return collect(scenario, rings, AABB::of, false);
}
/** /**
* Collects metrics from any element type that can be converted to an AABB. * Collects metrics from any element type that can be converted to an AABB.
*/ */
static <E> Metrics collect(String scenario, List<E> elements, Function<E, AABB> aabbFunction) { static <E> Metrics collect(String scenario, List<E> elements, Function<E, AABB> aabbFunction) {
return collect(scenario, elements, aabbFunction, true);
}
static <E> Metrics collect(
String scenario,
List<E> elements,
Function<E, AABB> aabbFunction,
boolean samplePairs) {
List<AABB> boxes = new ArrayList<>(elements.size()); List<AABB> boxes = new ArrayList<>(elements.size());
for (E element : elements) { for (E element : elements) {
AABB aabb = aabbFunction.apply(element); AABB aabb = aabbFunction.apply(element);
...@@ -46,7 +64,7 @@ final class BvhInputMetricsCollector { ...@@ -46,7 +64,7 @@ final class BvhInputMetricsCollector {
} }
} }
return collectBoxes(scenario, boxes); return collectBoxes(scenario, boxes, samplePairs);
} }
/** /**
...@@ -54,16 +72,27 @@ final class BvhInputMetricsCollector { ...@@ -54,16 +72,27 @@ final class BvhInputMetricsCollector {
* are sampled when the full pairs set would be too large. * are sampled when the full pairs set would be too large.
*/ */
static Metrics collectBoxes(String scenario, List<AABB> boxes) { static Metrics collectBoxes(String scenario, List<AABB> boxes) {
return collectBoxes(scenario, boxes, true);
}
static Metrics collectBoxesCheap(String scenario, List<AABB> boxes) {
return collectBoxes(scenario, boxes, false);
}
private static Metrics collectBoxes(String scenario, List<AABB> boxes, boolean samplePairs) {
int count = boxes.size(); int count = boxes.size();
if (count == 0) { if (count == 0) {
return Metrics.empty(scenario); return Metrics.empty(scenario);
} }
Aggregate aggregate = aggregate(boxes); Aggregate aggregate = aggregate(boxes);
PairSample pairSample = samplePairs(boxes); PairSample pairSample = samplePairs ? samplePairs(boxes) : PairSample.empty();
BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofAabbs(boxes);
int degenerateCount = 0; int degenerateCount = 0;
int thinBoxCount = 0;
double totalAspectRatio = 0.0; double totalAspectRatio = 0.0;
double totalRelativeVolume = 0.0;
double totalDx = 0.0; double totalDx = 0.0;
double totalDy = 0.0; double totalDy = 0.0;
double totalDz = 0.0; double totalDz = 0.0;
...@@ -76,13 +105,18 @@ final class BvhInputMetricsCollector { ...@@ -76,13 +105,18 @@ final class BvhInputMetricsCollector {
degenerateCount++; degenerateCount++;
} }
double dx = extentX(box); double dx = box.getExtentX();
double dy = extentY(box); double dy = box.getExtentY();
double dz = extentZ(box); double dz = box.getExtentZ();
double currentAspectRatio = aspectRatio(dx, dy, dz);
if (currentAspectRatio >= THIN_BOX_ASPECT_RATIO) {
thinBoxCount++;
}
totalDx += dx; totalDx += dx;
totalDy += dy; totalDy += dy;
totalDz += dz; totalDz += dz;
totalAspectRatio += aspectRatio(dx, dy, dz); totalAspectRatio += currentAspectRatio;
totalRelativeVolume += relativeVolume(dx, dy, dz, aggregate);
totalCx += box.getCenterX(); totalCx += box.getCenterX();
totalCy += box.getCenterY(); totalCy += box.getCenterY();
totalCz += box.getCenterZ(); totalCz += box.getCenterZ();
...@@ -108,13 +142,19 @@ final class BvhInputMetricsCollector { ...@@ -108,13 +142,19 @@ final class BvhInputMetricsCollector {
pairSample.overlapRate(), pairSample.overlapRate(),
pairSample.containmentRate(), pairSample.containmentRate(),
(double) degenerateCount / count, (double) degenerateCount / count,
(double) thinBoxCount / count,
totalAspectRatio / count, totalAspectRatio / count,
totalRelativeVolume / count,
totalDx / count, totalDx / count,
totalDy / count, totalDy / count,
totalDz / count, totalDz / count,
Math.sqrt(varianceX / count) / positiveOrOne(aggregate.dx), Math.sqrt(varianceX / count) / positiveOrOne(aggregate.dx),
Math.sqrt(varianceY / count) / positiveOrOne(aggregate.dy), Math.sqrt(varianceY / count) / positiveOrOne(aggregate.dy),
Math.sqrt(varianceZ / count) / positiveOrOne(aggregate.dz)); Math.sqrt(varianceZ / count) / positiveOrOne(aggregate.dz),
(Math.sqrt(varianceX / count) / positiveOrOne(aggregate.dx)
+ Math.sqrt(varianceY / count) / positiveOrOne(aggregate.dy)
+ Math.sqrt(varianceZ / count) / positiveOrOne(aggregate.dz)) / 3.0,
summary);
} }
/** /**
...@@ -125,22 +165,33 @@ final class BvhInputMetricsCollector { ...@@ -125,22 +165,33 @@ final class BvhInputMetricsCollector {
} }
private static Aggregate aggregate(List<AABB> boxes) { private static Aggregate aggregate(List<AABB> boxes) {
double minX = Double.POSITIVE_INFINITY; AABB totalBox = AABB.enclosing(boxes);
double minY = Double.POSITIVE_INFINITY; return new Aggregate(totalBox.getExtentX(), totalBox.getExtentY(), totalBox.getExtentZ());
double minZ = Double.POSITIVE_INFINITY; }
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double maxZ = Double.NEGATIVE_INFINITY;
for (AABB box : boxes) { private static double relativeVolume(double dx, double dy, double dz, Aggregate aggregate) {
minX = Math.min(minX, box.getMinX()); double localMeasure = 1.0;
minY = Math.min(minY, box.getMinY()); double totalMeasure = 1.0;
minZ = Math.min(minZ, box.getMinZ()); boolean hasActiveExtent = false;
maxX = Math.max(maxX, box.getMaxX()); if (dx > DEGENERATE_TOLERANCE) {
maxY = Math.max(maxY, box.getMaxY()); localMeasure *= dx;
maxZ = Math.max(maxZ, box.getMaxZ()); totalMeasure *= aggregate.dx;
hasActiveExtent = true;
}
if (dy > DEGENERATE_TOLERANCE) {
localMeasure *= dy;
totalMeasure *= aggregate.dy;
hasActiveExtent = true;
}
if (dz > DEGENERATE_TOLERANCE) {
localMeasure *= dz;
totalMeasure *= aggregate.dz;
hasActiveExtent = true;
} }
return new Aggregate(maxX - minX, maxY - minY, maxZ - minZ); if (!hasActiveExtent || totalMeasure <= DEGENERATE_TOLERANCE) {
return 0.0;
}
return localMeasure / totalMeasure;
} }
private static PairSample samplePairs(List<AABB> boxes) { private static PairSample samplePairs(List<AABB> boxes) {
...@@ -174,18 +225,6 @@ final class BvhInputMetricsCollector { ...@@ -174,18 +225,6 @@ final class BvhInputMetricsCollector {
return (long) count * (count - 1) / 2L; return (long) count * (count - 1) / 2L;
} }
private static double extentX(AABB box) {
return box.getMaxX() - box.getMinX();
}
private static double extentY(AABB box) {
return box.getMaxY() - box.getMinY();
}
private static double extentZ(AABB box) {
return box.getMaxZ() - box.getMinZ();
}
private static double aspectRatio(double dx, double dy, double dz) { private static double aspectRatio(double dx, double dy, double dz) {
double max = Math.max(dx, Math.max(dy, dz)); double max = Math.max(dx, Math.max(dy, dz));
double min = Math.min(positiveOrMax(dx), Math.min(positiveOrMax(dy), positiveOrMax(dz))); double min = Math.min(positiveOrMax(dx), Math.min(positiveOrMax(dy), positiveOrMax(dz)));
...@@ -237,6 +276,10 @@ final class BvhInputMetricsCollector { ...@@ -237,6 +276,10 @@ final class BvhInputMetricsCollector {
double containmentRate() { double containmentRate() {
return sampledPairs == 0L ? 0.0 : (double) containedPairs / sampledPairs; return sampledPairs == 0L ? 0.0 : (double) containedPairs / sampledPairs;
} }
static PairSample empty() {
return new PairSample(0L, 0L, 0L);
}
} }
static final class Metrics { static final class Metrics {
...@@ -247,13 +290,17 @@ final class BvhInputMetricsCollector { ...@@ -247,13 +290,17 @@ final class BvhInputMetricsCollector {
final double sampledOverlapRate; final double sampledOverlapRate;
final double sampledContainmentRate; final double sampledContainmentRate;
final double degenerateRate; final double degenerateRate;
final double thinBoxRate;
final double averageAspectRatio; final double averageAspectRatio;
final double averageRelativeBoxVolume;
final double averageExtentX; final double averageExtentX;
final double averageExtentY; final double averageExtentY;
final double averageExtentZ; final double averageExtentZ;
final double normalizedCenterSpreadX; final double normalizedCenterSpreadX;
final double normalizedCenterSpreadY; final double normalizedCenterSpreadY;
final double normalizedCenterSpreadZ; final double normalizedCenterSpreadZ;
final double centerSpreadRatio;
final BvhUsagePolicy.BvhInputSummary summary;
private Metrics( private Metrics(
String scenario, String scenario,
...@@ -263,13 +310,17 @@ final class BvhInputMetricsCollector { ...@@ -263,13 +310,17 @@ final class BvhInputMetricsCollector {
double sampledOverlapRate, double sampledOverlapRate,
double sampledContainmentRate, double sampledContainmentRate,
double degenerateRate, double degenerateRate,
double thinBoxRate,
double averageAspectRatio, double averageAspectRatio,
double averageRelativeBoxVolume,
double averageExtentX, double averageExtentX,
double averageExtentY, double averageExtentY,
double averageExtentZ, double averageExtentZ,
double normalizedCenterSpreadX, double normalizedCenterSpreadX,
double normalizedCenterSpreadY, double normalizedCenterSpreadY,
double normalizedCenterSpreadZ) { double normalizedCenterSpreadZ,
double centerSpreadRatio,
BvhUsagePolicy.BvhInputSummary summary) {
this.scenario = scenario; this.scenario = scenario;
this.elementCount = elementCount; this.elementCount = elementCount;
this.totalPairs = totalPairs; this.totalPairs = totalPairs;
...@@ -277,25 +328,30 @@ final class BvhInputMetricsCollector { ...@@ -277,25 +328,30 @@ final class BvhInputMetricsCollector {
this.sampledOverlapRate = sampledOverlapRate; this.sampledOverlapRate = sampledOverlapRate;
this.sampledContainmentRate = sampledContainmentRate; this.sampledContainmentRate = sampledContainmentRate;
this.degenerateRate = degenerateRate; this.degenerateRate = degenerateRate;
this.thinBoxRate = thinBoxRate;
this.averageAspectRatio = averageAspectRatio; this.averageAspectRatio = averageAspectRatio;
this.averageRelativeBoxVolume = averageRelativeBoxVolume;
this.averageExtentX = averageExtentX; this.averageExtentX = averageExtentX;
this.averageExtentY = averageExtentY; this.averageExtentY = averageExtentY;
this.averageExtentZ = averageExtentZ; this.averageExtentZ = averageExtentZ;
this.normalizedCenterSpreadX = normalizedCenterSpreadX; this.normalizedCenterSpreadX = normalizedCenterSpreadX;
this.normalizedCenterSpreadY = normalizedCenterSpreadY; this.normalizedCenterSpreadY = normalizedCenterSpreadY;
this.normalizedCenterSpreadZ = normalizedCenterSpreadZ; this.normalizedCenterSpreadZ = normalizedCenterSpreadZ;
this.centerSpreadRatio = centerSpreadRatio;
this.summary = summary;
} }
static Metrics empty(String scenario) { static Metrics empty(String scenario) {
return new Metrics(scenario, 0, 0L, 0L, 0.0, 0.0, 0.0, 0.0, return new Metrics(scenario, 0, 0L, 0L, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0); 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
BvhUsagePolicy.BvhInputSummary.countOnly(0));
} }
String toSummaryLine() { String toSummaryLine() {
return String.format(Locale.ROOT, return String.format(Locale.ROOT,
"[BVH-METRICS] %s n=%d pairs=%d sampled=%d overlap=%.4f containment=%.4f" "[BVH-METRICS] %s n=%d pairs=%d sampled=%d overlap=%.4f containment=%.4f"
+ " degenerate=%.4f aspectAvg=%.2f avgExtent=(%.2f, %.2f, %.2f)" + " degenerate=%.4f thin=%.4f aspectAvg=%.2f relVol=%.6f"
+ " centerSpread=(%.3f, %.3f, %.3f)", + " avgExtent=(%.2f, %.2f, %.2f) centerSpread=(%.3f, %.3f, %.3f) spread=%.3f",
scenario, scenario,
elementCount, elementCount,
totalPairs, totalPairs,
...@@ -303,13 +359,16 @@ final class BvhInputMetricsCollector { ...@@ -303,13 +359,16 @@ final class BvhInputMetricsCollector {
sampledOverlapRate, sampledOverlapRate,
sampledContainmentRate, sampledContainmentRate,
degenerateRate, degenerateRate,
thinBoxRate,
averageAspectRatio, averageAspectRatio,
averageRelativeBoxVolume,
averageExtentX, averageExtentX,
averageExtentY, averageExtentY,
averageExtentZ, averageExtentZ,
normalizedCenterSpreadX, normalizedCenterSpreadX,
normalizedCenterSpreadY, normalizedCenterSpreadY,
normalizedCenterSpreadZ); normalizedCenterSpreadZ,
centerSpreadRatio);
} }
} }
} }
package de.hft.stuttgart.citydoctor2.checks.bht; package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.checks.geometry.NestedRingsCheck;
import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil; import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Lod; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB; import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree; import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
/** /**
* Collects SSI timing observations together with BVH input metrics. * Collects compact timing observations and broad-phase metrics for BVH strategy
* * selection from synthetic fixtures.
* The summary is meant as a working note for deriving conservative strategy
* rules after the fixture set changes.
* *
* @author Numanoglu * @author Numanoglu
*/ */
...@@ -28,64 +33,67 @@ import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; ...@@ -28,64 +33,67 @@ import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
public class BvhStrategyHeuristicExplorationTest { public class BvhStrategyHeuristicExplorationTest {
private static final double DELTA = 0.001; private static final double DELTA = 0.001;
private static final double EPSILON = 0.001;
@Test @Test
public void exploreSsiStrategyCandidatesFromMetricsAndTiming() { public void exploreSsiNestedAndRsiStrategyCandidates() {
List<Scenario> scenarios = new ArrayList<>(); List<BvhHeuristicTimingSupport.Observation> observations = new ArrayList<>();
scenarios.add(new Scenario("ssi-separated-grid", addSsiObservations(observations);
SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 12, 12))); addNestedObservations(observations);
scenarios.add(new Scenario("ssi-overlapping-grid", addRsiObservations(observations);
SyntheticSolidGeometryFactory.overlappingBoxGrid(Lod.LOD2, 12, 12)));
scenarios.add(new Scenario("ssi-dense-clusters",
SyntheticSolidGeometryFactory.denseBoxClusters(Lod.LOD2, 8, 20)));
scenarios.add(new Scenario("ssi-long-thin-slabs",
SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 160)));
scenarios.add(new Scenario("ssi-flat-box-grid",
SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 200)));
scenarios.add(new Scenario("ssi-citylike-mixed",
SyntheticCityGmlLikeGeometryFactory.mixedUrbanDistrict(Lod.LOD2, 5, 4)));
scenarios.add(new Scenario("ssi-citylike-courtyard",
SyntheticCityGmlLikeGeometryFactory.courtyardDistrict(Lod.LOD2, 16)));
scenarios.add(new Scenario("ssi-citylike-roofs",
SyntheticCityGmlLikeGeometryFactory.variedRoofDistrict(Lod.LOD2, 180)));
List<Observation> observations = new ArrayList<>();
for (Scenario scenario : scenarios) {
observations.add(measureScenario(scenario));
}
printObservationSummary(observations); printObservationSummary(observations);
printBucketSummary(observations);
} }
private static Observation measureScenario(Scenario scenario) { private static void addSsiObservations(List<BvhHeuristicTimingSupport.Observation> observations) {
List<Polygon> polygons = scenario.geometry.getPolygons(); for (BvhSyntheticScenarioCatalog.SsiScenario scenario : BvhSyntheticScenarioCatalog.ssiScenarios()) {
BvhInputMetricsCollector.Metrics metrics = List<Polygon> polygons = scenario.geometry.getPolygons();
BvhInputMetricsCollector.forPolygons(scenario.name, polygons); BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.forPolygonsCheap(scenario.name, polygons);
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>(); observations.add(BvhHeuristicTimingSupport.measureVariants(
BvhPerformanceTestSupport.Measurement bruteForce = BvhPerformanceTestSupport.measure( "SSI",
scenario.name,
"BRUTE_FORCE",
polygons.size(),
() -> SelfIntersectionUtil.calculateSolidSelfIntersection0(scenario.geometry, DELTA).size());
measurements.add(bruteForce);
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
BvhPerformanceTestSupport.Measurement bvhMeasurement = BvhPerformanceTestSupport.measure(
scenario.name, scenario.name,
strategy.name(), metrics,
polygons.size(), polygons.size(),
() -> calculateWithTree(scenario.geometry, strategy)); "BRUTE_FORCE",
measurements.add(bvhMeasurement); () -> SelfIntersectionUtil.calculateSolidSelfIntersection0(scenario.geometry, DELTA).size(),
assertEquals("SSI result count differs for " + scenario.name + " / " + strategy, strategy -> calculateSsiWithTree(scenario.geometry, strategy)));
bruteForce.resultCount, bvhMeasurement.resultCount);
} }
}
BvhPerformanceTestSupport.printScenarioSummary(scenario.name, measurements); private static void addNestedObservations(List<BvhHeuristicTimingSupport.Observation> observations) {
return new Observation(metrics, fastest(measurements), fastestBvh(measurements), bruteForce); for (BvhSyntheticScenarioCatalog.NestedScenario scenario : BvhSyntheticScenarioCatalog.nestedScenarios()) {
List<LinearRing> rings = scenario.polygon.getInnerRings();
BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.forRingsCheap(scenario.name, rings);
observations.add(BvhHeuristicTimingSupport.measureVariants(
"NESTED",
scenario.name,
metrics,
rings.size(),
NestedRingsCheck.Variant.OLD.name(),
() -> runNestedCheck(scenario.polygon, NestedRingsCheck.Variant.OLD),
strategy -> runNestedCheck(scenario.polygon, BvhHeuristicTimingSupport.nestedVariantFor(strategy))));
}
} }
private static int calculateWithTree(Geometry geometry, SplitStrategy strategy) { private static void addRsiObservations(List<BvhHeuristicTimingSupport.Observation> observations) {
for (BvhSyntheticScenarioCatalog.RsiScenario scenario : BvhSyntheticScenarioCatalog.rsiScenarios()) {
BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.collectBoxesCheap(scenario.name, collectEdgeBoxes(scenario.geometry));
observations.add(BvhHeuristicTimingSupport.measureVariants(
"RSI",
scenario.name,
metrics,
countEdges(scenario.geometry),
RingSelfIntCheck.Variant.OLD.name(),
() -> runRsiCheck(scenario.geometry, RingSelfIntCheck.Variant.OLD),
strategy -> runRsiCheck(scenario.geometry, BvhHeuristicTimingSupport.rsiVariantFor(strategy))));
}
}
private static int calculateSsiWithTree(Geometry geometry, SplitStrategy strategy) {
BoundingVolumeHierarchyTree<Polygon> tree = BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newWithStrategy( BoundingVolumeHierarchyTree.newWithStrategy(
geometry.getPolygons(), geometry.getPolygons(),
...@@ -94,109 +102,245 @@ public class BvhStrategyHeuristicExplorationTest { ...@@ -94,109 +102,245 @@ public class BvhStrategyHeuristicExplorationTest {
return SelfIntersectionUtil.calculateSolidSelfIntersection(geometry, DELTA, tree).size(); return SelfIntersectionUtil.calculateSolidSelfIntersection(geometry, DELTA, tree).size();
} }
private static BvhPerformanceTestSupport.Measurement fastest( private static int runNestedCheck(ConcretePolygon polygon, NestedRingsCheck.Variant variant) {
List<BvhPerformanceTestSupport.Measurement> measurements) { NestedRingsCheck check = new NestedRingsCheck(variant);
BvhPerformanceTestSupport.Measurement fastest = measurements.get(0); check.check(polygon);
for (BvhPerformanceTestSupport.Measurement measurement : measurements) { CheckResult result = polygon.getCheckResult(check);
if (measurement.averageNanos < fastest.averageNanos) { return result.getResultStatus() == ResultStatus.ERROR ? 1 : 0;
fastest = measurement; }
private static int runRsiCheck(Geometry geometry, RingSelfIntCheck.Variant variant) {
int errorCount = 0;
for (Polygon polygon : geometry.getPolygons()) {
RingSelfIntCheck check = new RingSelfIntCheck(variant);
check.init(Collections.singletonMap("minVertexDistance", String.valueOf(EPSILON)), null);
check.check(polygon.getExteriorRing());
CheckResult result = polygon.getExteriorRing().getCheckResult(check);
if (result.getResultStatus() == ResultStatus.ERROR) {
errorCount++;
} }
} }
return fastest; return errorCount;
} }
private static BvhPerformanceTestSupport.Measurement fastestBvh( private static int countEdges(Geometry geometry) {
List<BvhPerformanceTestSupport.Measurement> measurements) { int edgeCount = 0;
BvhPerformanceTestSupport.Measurement fastest = null; for (Polygon polygon : geometry.getPolygons()) {
for (BvhPerformanceTestSupport.Measurement measurement : measurements) { LinearRing ring = polygon.getExteriorRing();
if ("BRUTE_FORCE".equals(measurement.variant)) { edgeCount += Math.max(0, ring.getVertices().size() - 1);
continue; }
} return edgeCount;
if (fastest == null || measurement.averageNanos < fastest.averageNanos) { }
fastest = measurement;
private static List<LinearRing> collectExteriorRings(Geometry geometry) {
List<LinearRing> rings = new ArrayList<>();
for (Polygon polygon : geometry.getPolygons()) {
rings.add(polygon.getExteriorRing());
}
return rings;
}
private static List<AABB> collectEdgeBoxes(Geometry geometry) {
List<AABB> boxes = new ArrayList<>();
for (LinearRing ring : collectExteriorRings(geometry)) {
List<Vertex> vertices = ring.getVertices();
for (int i = 0; i < vertices.size() - 1; i++) {
boxes.add(AABB.of(vertices.get(i), vertices.get(i + 1), EPSILON));
} }
} }
return fastest; return boxes;
} }
private static void printObservationSummary(List<Observation> observations) { private static void printObservationSummary(List<BvhHeuristicTimingSupport.Observation> observations) {
System.out.println(); System.out.println();
System.out.println("[BVH-HEURISTIC-EXPLORATION] SSI observations"); System.out.println("[BVH-HEURISTIC-EXPLORATION]");
System.out.println("---------------------------------------------------------------------------------------------------------------"); System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
System.out.println(String.format( System.out.println(String.format(
"%-24s %7s %9s %9s %9s %10s %12s %22s %22s", "%-7s %-24s %7s %7s %8s %7s %8s %24s %24s %24s %10s",
"check",
"scenario", "scenario",
"n", "n",
"overlap", "thin",
"contain", "relVol",
"degen", "spread",
"aspect", "aspect",
"winner", "policyPred",
"candidatePred",
"fastest BVH", "fastest BVH",
"initial candidate rule")); "bvh ms"));
System.out.println("---------------------------------------------------------------------------------------------------------------"); System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
for (Observation observation : observations) { for (BvhHeuristicTimingSupport.Observation observation : observations) {
BvhInputMetricsCollector.Metrics metrics = observation.metrics; BvhInputMetricsCollector.Metrics metrics = observation.metrics;
System.out.println(String.format( System.out.println(String.format(
"%-24s %7d %9.4f %9.4f %9.4f %10.2f %12s %28s %28s", "%-7s %-24s %7d %7.3f %8.5f %7.3f %8.1f %24s %24s %24s %10.3f",
observation.checkName,
metrics.scenario, metrics.scenario,
metrics.elementCount, metrics.elementCount,
metrics.sampledOverlapRate, metrics.thinBoxRate,
metrics.sampledContainmentRate, metrics.averageRelativeBoxVolume,
metrics.degenerateRate, metrics.centerSpreadRatio,
metrics.averageAspectRatio, metrics.averageAspectRatio,
observation.fastest.variant, observation.syntheticPrediction,
observation.candidatePrediction,
observation.fastestBvh.variant, observation.fastestBvh.variant,
candidateRule(observation))); observation.fastestBvh.averageMillis()));
} }
System.out.println("---------------------------------------------------------------------------------------------------------------"); System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
System.out.println("Candidate rules are prelim; check them against real CityGML models."); System.out.println("Fastest BVH is the target for candidate strategy rules.");
System.out.println(); System.out.println();
} }
private static String candidateRule(Observation observation) { private static void printBucketSummary(List<BvhHeuristicTimingSupport.Observation> observations) {
BvhInputMetricsCollector.Metrics metrics = observation.metrics; Map<String, BucketStats> buckets = new LinkedHashMap<>();
if ("BRUTE_FORCE".equals(observation.fastest.variant)) { for (BvhHeuristicTimingSupport.Observation observation : observations) {
return "consider brute below/near this n"; addBucket(buckets, observation, "thin", thinBucket(observation.metrics.thinBoxRate));
addBucket(buckets, observation, "relVol", relVolBucket(observation.metrics.averageRelativeBoxVolume));
addBucket(buckets, observation, "spread", spreadBucket(observation.metrics.centerSpreadRatio));
addBucket(buckets, observation, "aspect", aspectBucket(observation.metrics.averageAspectRatio));
} }
if (metrics.sampledOverlapRate < 0.01 && metrics.elementCount > 200) {
return "low overlap: BVH likely"; System.out.println("[BVH-WINNER-BY-METRIC-BUCKET]");
System.out.println("-------------------------------------------------------------------------------");
System.out.println(String.format(
"%-7s %-7s %-14s %7s %24s %8s",
"check", "metric", "bucket", "cases", "mostCommonBvh", "share"));
System.out.println("-------------------------------------------------------------------------------");
for (BucketStats bucket : buckets.values()) {
System.out.println(String.format(
"%-7s %-7s %-14s %7d %24s %7.1f%%",
bucket.checkName,
bucket.metricName,
bucket.bucketName,
bucket.total,
bucket.mostCommonWinner(),
bucket.mostCommonShare() * 100.0));
} }
if (metrics.degenerateRate > 0.50 || metrics.averageAspectRatio > 50.0) {
return "flat/skinny: prefer measured stable BVH"; System.out.println();
System.out.println("[BVH-CANDIDATE-RULES]");
System.out.println("Minimum support: 3 cases; minimum winner share: 65%.");
for (BucketStats bucket : buckets.values()) {
if (bucket.total >= 3 && bucket.mostCommonShare() >= 0.65) {
System.out.println(String.format(
"if check=%s and %s in %s -> %s (%d cases, %.1f%%)",
bucket.checkName,
bucket.metricName,
bucket.bucketName,
bucket.mostCommonWinner(),
bucket.total,
bucket.mostCommonShare() * 100.0));
}
} }
if (metrics.sampledOverlapRate > 0.10) { System.out.println();
return "high overlap: compare BVH vs brute"; }
private static void addBucket(
Map<String, BucketStats> buckets,
BvhHeuristicTimingSupport.Observation observation,
String metricName,
String bucketName) {
String key = observation.checkName + "|" + metricName + "|" + bucketName;
BucketStats bucket = buckets.computeIfAbsent(key,
ignored -> new BucketStats(observation.checkName, metricName, bucketName));
bucket.add(observation.fastestBvh.variant);
}
private static String thinBucket(double value) {
if (value == 0.0) {
return "0";
}
if (value <= 0.25) {
return "(0,0.25]";
}
if (value <= 0.50) {
return "(0.25,0.50]";
}
if (value <= 0.75) {
return "(0.50,0.75]";
} }
return "citylike/mixed: prefer fastest BVH candidate"; return "(0.75,1]";
} }
private static final class Scenario { private static String relVolBucket(double value) {
final String name; if (value <= 0.00001) {
final Geometry geometry; return "<=1e-5";
}
if (value <= 0.0001) {
return "<=1e-4";
}
if (value <= 0.001) {
return "<=1e-3";
}
if (value <= 0.01) {
return "<=1e-2";
}
return ">1e-2";
}
private static String spreadBucket(double value) {
if (value < 0.15) {
return "<0.15";
}
if (value < 0.25) {
return "0.15..0.25";
}
if (value < 0.35) {
return "0.25..0.35";
}
return ">=0.35";
}
Scenario(String name, Geometry geometry) { private static String aspectBucket(double value) {
this.name = name; if (value < 5.0) {
this.geometry = geometry; return "<5";
}
if (value < 20.0) {
return "5..20";
} }
if (value < 80.0) {
return "20..80";
}
return ">=80";
} }
private static final class Observation { private static final class BucketStats {
final BvhInputMetricsCollector.Metrics metrics; final String checkName;
final BvhPerformanceTestSupport.Measurement fastest; final String metricName;
final BvhPerformanceTestSupport.Measurement fastestBvh; final String bucketName;
final BvhPerformanceTestSupport.Measurement bruteForce; final Map<String, Integer> winners = new LinkedHashMap<>();
int total;
BucketStats(String checkName, String metricName, String bucketName) {
this.checkName = checkName;
this.metricName = metricName;
this.bucketName = bucketName;
}
void add(String winner) {
total++;
winners.merge(winner, 1, Integer::sum);
}
Observation( String mostCommonWinner() {
BvhInputMetricsCollector.Metrics metrics, String bestWinner = "";
BvhPerformanceTestSupport.Measurement fastest, int bestCount = -1;
BvhPerformanceTestSupport.Measurement fastestBvh, for (Map.Entry<String, Integer> entry : winners.entrySet()) {
BvhPerformanceTestSupport.Measurement bruteForce) { if (entry.getValue() > bestCount) {
this.metrics = metrics; bestWinner = entry.getKey();
this.fastest = fastest; bestCount = entry.getValue();
this.fastestBvh = fastestBvh; }
this.bruteForce = bruteForce; }
return bestWinner;
}
double mostCommonShare() {
if (total == 0) {
return 0.0;
}
return (double) winners.get(mostCommonWinner()) / total;
} }
} }
} }
package de.hft.stuttgart.citydoctor2.checks.bht;
import java.util.List;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
/**
* Central catalog of synthetic BVH exploration scenarios.
*
* The scenarios intentionally vary count, thinness, relative box volume, center
* spread, and city-like clustering so heuristic experiments can compare metric
* ranges without duplicating fixture lists in several tests.
*
* @author Numanoglu
*/
final class BvhSyntheticScenarioCatalog {
private static final double EPSILON = 0.001;
private BvhSyntheticScenarioCatalog() {
}
static List<SsiScenario> ssiScenarios() {
return List.of(
new SsiScenario("sep-grid-small",
SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 6, 6)),
new SsiScenario("sep-grid-medium",
SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 12, 12)),
new SsiScenario("overlap-grid",
SyntheticSolidGeometryFactory.overlappingBoxGrid(Lod.LOD2, 12, 12)),
new SsiScenario("dense-clusters-small",
SyntheticSolidGeometryFactory.denseBoxClusters(Lod.LOD2, 4, 20)),
new SsiScenario("dense-clusters-large",
SyntheticSolidGeometryFactory.denseBoxClusters(Lod.LOD2, 10, 24)),
new SsiScenario("long-thin-slabs-small",
SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 80)),
new SsiScenario("long-thin-slabs-large",
SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 240)),
new SsiScenario("flat-box-grid-small",
SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 80)),
new SsiScenario("flat-box-grid-large",
SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 240)),
new SsiScenario("thin-ratio-025",
SyntheticSolidGeometryFactory.thinRatioSweepBoxes(Lod.LOD2, 120, 0.25)),
new SsiScenario("thin-ratio-050",
SyntheticSolidGeometryFactory.thinRatioSweepBoxes(Lod.LOD2, 120, 0.50)),
new SsiScenario("thin-ratio-075",
SyntheticSolidGeometryFactory.thinRatioSweepBoxes(Lod.LOD2, 120, 0.75)),
new SsiScenario("aspect-005",
SyntheticSolidGeometryFactory.aspectSweepBoxes(Lod.LOD2, 120, 5.0)),
new SsiScenario("aspect-020",
SyntheticSolidGeometryFactory.aspectSweepBoxes(Lod.LOD2, 120, 20.0)),
new SsiScenario("aspect-080",
SyntheticSolidGeometryFactory.aspectSweepBoxes(Lod.LOD2, 120, 80.0)),
new SsiScenario("spread-tight",
SyntheticSolidGeometryFactory.spreadSweepBoxes(Lod.LOD2, 120, 3.5)),
new SsiScenario("spread-wide",
SyntheticSolidGeometryFactory.spreadSweepBoxes(Lod.LOD2, 120, 20.0)),
new SsiScenario("relvol-compact",
SyntheticSolidGeometryFactory.relativeVolumeSweepBoxes(Lod.LOD2, 80, 5.0, 1.1)),
new SsiScenario("relvol-sparse",
SyntheticSolidGeometryFactory.relativeVolumeSweepBoxes(Lod.LOD2, 80, 5.0, 5.0)),
new SsiScenario("citylike-mixed-small",
SyntheticCityGmlLikeGeometryFactory.mixedUrbanDistrict(Lod.LOD2, 3, 3)),
new SsiScenario("citylike-mixed-large",
SyntheticCityGmlLikeGeometryFactory.mixedUrbanDistrict(Lod.LOD2, 6, 5)),
new SsiScenario("citylike-courtyard",
SyntheticCityGmlLikeGeometryFactory.courtyardDistrict(Lod.LOD2, 16)),
new SsiScenario("citylike-roofs-small",
SyntheticCityGmlLikeGeometryFactory.variedRoofDistrict(Lod.LOD2, 80)),
new SsiScenario("citylike-roofs-large",
SyntheticCityGmlLikeGeometryFactory.variedRoofDistrict(Lod.LOD2, 240)));
}
static List<NestedScenario> nestedScenarios() {
return List.of(
new NestedScenario("disjoint-small",
SyntheticNestedRingGeometryFactory.manyDisjointInnerRings(120)),
new NestedScenario("disjoint-large",
SyntheticNestedRingGeometryFactory.manyDisjointInnerRings(1_000)),
new NestedScenario("one-pair-small",
SyntheticNestedRingGeometryFactory.oneNestedPairAmongMany(120)),
new NestedScenario("one-pair-large",
SyntheticNestedRingGeometryFactory.oneNestedPairAmongMany(1_000)),
new NestedScenario("concentric-small",
SyntheticNestedRingGeometryFactory.concentricNestedRings(80)),
new NestedScenario("concentric-large",
SyntheticNestedRingGeometryFactory.concentricNestedRings(300)),
new NestedScenario("overlap-no-error",
SyntheticNestedRingGeometryFactory.overlappingAabbsButNotNested(1_000)),
new NestedScenario("clustered-small",
SyntheticNestedRingGeometryFactory.clusteredInnerRings(8, 40)),
new NestedScenario("clustered-large",
SyntheticNestedRingGeometryFactory.clusteredInnerRings(20, 80)),
new NestedScenario("clustered-pair",
SyntheticNestedRingGeometryFactory.clusteredInnerRingsWithNestedPair(16, 80)),
new NestedScenario("aspect-005",
SyntheticNestedRingGeometryFactory.aspectSweepInnerRings(180, 5.0)),
new NestedScenario("aspect-020",
SyntheticNestedRingGeometryFactory.aspectSweepInnerRings(180, 20.0)),
new NestedScenario("aspect-080",
SyntheticNestedRingGeometryFactory.aspectSweepInnerRings(180, 80.0)),
new NestedScenario("spread-tight",
SyntheticNestedRingGeometryFactory.spreadSweepInnerRings(240, 2.0)),
new NestedScenario("spread-wide",
SyntheticNestedRingGeometryFactory.spreadSweepInnerRings(240, 20.0)));
}
static List<RsiScenario> rsiScenarios() {
return List.of(
new RsiScenario("rsi-small",
SyntheticRingGeometryFactory.performanceRingGeometry(200, 100, EPSILON)),
new RsiScenario("rsi-medium",
SyntheticRingGeometryFactory.performanceRingGeometry(1_000, 500, EPSILON)),
new RsiScenario("rsi-large",
SyntheticRingGeometryFactory.performanceRingGeometry(5_000, 2_000, EPSILON)),
new RsiScenario("rsi-convex-heavy",
SyntheticRingGeometryFactory.performanceRingGeometry(6_000, 200, EPSILON)),
new RsiScenario("rsi-zigzag-heavy",
SyntheticRingGeometryFactory.performanceRingGeometry(400, 4_000, EPSILON)),
new RsiScenario("rsi-aspect-005",
SyntheticRingGeometryFactory.aspectSweepGeometry(240, 5.0)),
new RsiScenario("rsi-aspect-020",
SyntheticRingGeometryFactory.aspectSweepGeometry(240, 20.0)),
new RsiScenario("rsi-aspect-080",
SyntheticRingGeometryFactory.aspectSweepGeometry(240, 80.0)),
new RsiScenario("rsi-spread-tight",
SyntheticRingGeometryFactory.spreadSweepGeometry(240, 4.0)),
new RsiScenario("rsi-spread-wide",
SyntheticRingGeometryFactory.spreadSweepGeometry(240, 30.0)));
}
static final class SsiScenario {
final String name;
final Geometry geometry;
SsiScenario(String name, Geometry geometry) {
this.name = name;
this.geometry = geometry;
}
}
static final class NestedScenario {
final String name;
final ConcretePolygon polygon;
NestedScenario(String name, ConcretePolygon polygon) {
this.name = name;
this.polygon = polygon;
}
}
static final class RsiScenario {
final String name;
final Geometry geometry;
RsiScenario(String name, Geometry geometry) {
this.name = name;
this.geometry = geometry;
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
public class BvhUsagePolicyTest {
@Test
public void smallInputsStayWithoutTree() {
Geometry smallSolid = SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 1, 1);
BvhUsagePolicy.BvhInputSummary summary = summaryForPolygons(smallSolid);
assertFalse(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
assertFalse(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.NESTED_RINGS, 4));
assertFalse(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, 16));
}
@Test
public void largeDistributedInputsUseOctonaryObjectMean() {
Geometry cityLikeGeometry = SyntheticCityGmlLikeGeometryFactory.variedRoofDistrict(Lod.LOD2, 180);
BvhUsagePolicy.BvhInputSummary summary = summaryForPolygons(cityLikeGeometry);
assertTrue(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
assertEquals(SplitStrategy.OCTONARY_OBJECT_MEAN,
BvhUsagePolicy.chooseSplitStrategy(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
}
@Test
public void flatInputsUseBinarySpatialMedian() {
Geometry flatGeometry = SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 200);
BvhUsagePolicy.BvhInputSummary summary = summaryForPolygons(flatGeometry);
assertTrue(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
assertEquals(SplitStrategy.BINARY_SPATIAL_MEDIAN,
BvhUsagePolicy.chooseSplitStrategy(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
}
@Test
public void ringSelfIntersectionUsesBinarySpatialMedian() {
Geometry ringGeometry = SyntheticRingGeometryFactory.performanceRingGeometry(200, 100, 0.001);
BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofAabbs(edgeBoxes(ringGeometry));
assertTrue(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, summary));
assertEquals(SplitStrategy.BINARY_SPATIAL_MEDIAN,
BvhUsagePolicy.chooseSplitStrategy(BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, summary));
}
@Test
public void summaryExposesThinVolumeAndSpreadMetrics() {
Geometry slabGeometry = SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 160);
BvhUsagePolicy.BvhInputSummary summary = summaryForPolygons(slabGeometry);
assertTrue("Expected long-thin fixture to contain thin boxes", summary.getThinBoxRatio() > 0.0);
assertTrue("Relative box volume must be non-negative", summary.getAverageRelativeBoxVolume() >= 0.0);
assertTrue("Expected distributed slab centers", summary.getCenterSpreadRatio() > 0.0);
}
private static BvhUsagePolicy.BvhInputSummary summaryForPolygons(Geometry geometry) {
return BvhUsagePolicy.BvhInputSummary.ofElements(geometry.getPolygons(), polygon -> AABB.of(polygon.getOriginal()));
}
private static List<AABB> edgeBoxes(Geometry geometry) {
List<AABB> boxes = new ArrayList<>();
for (Polygon polygon : geometry.getPolygons()) {
LinearRing ring = polygon.getExteriorRing();
List<Vertex> vertices = ring.getVertices();
for (int i = 0; i < vertices.size() - 1; i++) {
boxes.add(AABB.of(vertices.get(i), vertices.get(i + 1), 0.001));
}
}
return boxes;
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.config.Configurator;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.utils.visitors.CityObjectCollector;
/**
* Extracts broad-phase BVH input metrics and timing observations from real CityGML
* files.
*
* The test measures broad-phase costs and compares candidate strategy rules with
* the fastest measured BVH split strategy.
*
* @author Numanoglu
*/
@Tag("performance")
public class RealCityGmlBvhHeuristicExplorationTest {
private static final Path REAL_CITYGML_ROOT =
Path.of("src", "test", "resources", "real-citygml");
private static final double EDGE_BOX_PADDING = 0.001;
private static final int FILE_COLUMN_WIDTH = 28;
private static final String TABLE_FORMAT =
"%-28s | %-6s | %7s | %5s | %7s | %6s | %7s | %23s | %23s | %5s | %9s | %9s | %9s";
private static final PrintStream SILENT_OUT = new PrintStream(OutputStream.nullOutputStream());
@Test
public void printRealCityGmlMetricsAndBvhTiming() throws Exception {
assumeTrue(Files.isDirectory(REAL_CITYGML_ROOT),
"Real CityGML test data directory is not available: " + REAL_CITYGML_ROOT);
List<Path> datasetDirectories = listDatasetDirectories(REAL_CITYGML_ROOT);
assumeTrue(!datasetDirectories.isEmpty(),
"Expected real CityGML dataset directories in " + REAL_CITYGML_ROOT);
assertFalse("No real CityGML dataset directories found in " + REAL_CITYGML_ROOT,
datasetDirectories.isEmpty());
List<RealObservation> allObservations = new ArrayList<>();
for (Path datasetDirectory : datasetDirectories) {
List<Path> files = listGmlFiles(datasetDirectory);
if (files.isEmpty()) {
continue;
}
printHeader(datasetDirectory.getFileName().toString());
for (Path file : files) {
ModelElements elements = runQuietly(() -> {
CityDoctorModel model = CityGmlParser.parseCityGmlFile(
file.toString(),
new ParserConfiguration(8, false));
return collectElements(model);
});
printAndCollect(allObservations, datasetDirectory, file, "SSI",
measureBroadPhase("SSI", file.getFileName() + ":polygons",
elements.polygonBoxes, QueryMode.INTERSECTING));
printAndCollect(allObservations, datasetDirectory, file, "NESTED",
measureBroadPhase("NESTED", file.getFileName() + ":innerRings",
elements.innerRingBoxes, QueryMode.CONTAINED));
printAndCollect(allObservations, datasetDirectory, file, "RSI",
measureBroadPhase("RSI", file.getFileName() + ":edges",
elements.edgeBoxes, QueryMode.INTERSECTING));
printSeparator();
}
}
printAggregateSummary(allObservations);
printFooter();
}
private static void printAndCollect(
List<RealObservation> allObservations,
Path datasetDirectory,
Path file,
String checkName,
BvhHeuristicTimingSupport.Observation observation) {
printObservation(file.getFileName().toString(), observation);
allObservations.add(new RealObservation(
datasetDirectory.getFileName().toString(),
file.getFileName().toString(),
checkName,
observation));
}
private static BvhHeuristicTimingSupport.Observation measureBroadPhase(
String checkName,
String scenarioName,
List<AABB> boxes,
QueryMode queryMode) {
BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.collectBoxesCheap(scenarioName, boxes);
return BvhHeuristicTimingSupport.measureBvhStrategies(
checkName,
scenarioName,
metrics,
boxes.size(),
strategy -> runBroadPhase(boxes, queryMode, strategy));
}
private static int runBroadPhase(List<AABB> boxes, QueryMode queryMode, SplitStrategy strategy) {
List<Integer> indices = new ArrayList<>(boxes.size());
for (int i = 0; i < boxes.size(); i++) {
indices.add(i);
}
BoundingVolumeHierarchyTree<Integer> tree =
BoundingVolumeHierarchyTree.newWithStrategy(indices, boxes::get, strategy);
int candidateCount = 0;
for (int i = 0; i < boxes.size(); i++) {
AABB box = boxes.get(i);
if (queryMode == QueryMode.CONTAINED) {
candidateCount += tree.getAllElementsContainedIn(box).size();
} else {
candidateCount += tree.getAllIntersectingElements(box).size();
}
}
return candidateCount;
}
private static <T> T runQuietly(ThrowingSupplier<T> operation) throws Exception {
Level originalRootLevel = LogManager.getRootLogger().getLevel();
PrintStream originalOut = System.out;
PrintStream originalErr = System.err;
try {
Configurator.setRootLevel(Level.WARN);
Configurator.setAllLevels("com.zaxxer.hikari", Level.WARN);
System.setOut(SILENT_OUT);
System.setErr(SILENT_OUT);
return operation.get();
} finally {
System.setOut(originalOut);
System.setErr(originalErr);
Configurator.setRootLevel(originalRootLevel);
}
}
private interface ThrowingSupplier<T> {
T get() throws Exception;
}
private static List<Path> listGmlFiles(Path directory) throws IOException {
try (var stream = Files.list(directory)) {
return stream
.filter(path -> path.getFileName().toString().toLowerCase().endsWith(".gml"))
.sorted(Comparator.comparing(path -> path.getFileName().toString()))
.toList();
}
}
private static List<Path> listDatasetDirectories(Path root) throws IOException {
try (var stream = Files.list(root)) {
return stream
.filter(Files::isDirectory)
.sorted(Comparator.comparing(path -> path.getFileName().toString()))
.toList();
}
}
private static ModelElements collectElements(CityDoctorModel model) {
ModelElements elements = new ModelElements();
CityObjectCollector collector = new CityObjectCollector();
model.createFeatureStream()
.filter(cityObject -> cityObject != null)
.forEach(cityObject -> cityObject.accept(collector));
for (CityObject cityObject : collector.getCityObjects()) {
collectFromCityObject(cityObject, elements);
}
return elements;
}
private static void collectFromCityObject(CityObject cityObject, ModelElements elements) {
for (Geometry geometry : cityObject.getGeometries()) {
if (geometry == null) {
continue;
}
for (Polygon polygon : geometry.getPolygons()) {
if (polygon == null) {
continue;
}
if (!elements.seenPolygons.add(polygon)) {
continue;
}
elements.polygons.add(polygon);
elements.polygonBoxes.add(AABB.of(polygon.getOriginal()));
elements.innerRings.addAll(polygon.getInnerRings());
for (LinearRing innerRing : polygon.getInnerRings()) {
elements.innerRingBoxes.add(AABB.of(innerRing));
}
collectEdgeBoxes(polygon.getExteriorRing(), elements.edgeBoxes);
for (LinearRing innerRing : polygon.getInnerRings()) {
collectEdgeBoxes(innerRing, elements.edgeBoxes);
}
}
}
}
private static void collectEdgeBoxes(LinearRing ring, List<AABB> edgeBoxes) {
if (ring == null) {
return;
}
List<Vertex> vertices = ring.getVertices();
for (int i = 0; i < vertices.size() - 1; i++) {
edgeBoxes.add(AABB.of(vertices.get(i), vertices.get(i + 1), EDGE_BOX_PADDING));
}
}
private static void printHeader(String datasetName) {
System.out.println();
System.out.println("[REAL-CITYGML-BVH-HEURISTIC-EXPLORATION] " + datasetName);
String header = String.format(
TABLE_FORMAT,
"file", "check", "n", "thin", "relVol", "spread", "aspect",
"candidatePred", "winner", "match", "best ms", "pred ms", "loss ms");
printSeparator(header.length());
System.out.println(header);
printSeparator(header.length());
}
private static void printObservation(
String fileName,
BvhHeuristicTimingSupport.Observation observation) {
BvhInputMetricsCollector.Metrics metrics = observation.metrics;
System.out.println(String.format(
TABLE_FORMAT,
shortName(fileName),
observation.checkName,
metrics.elementCount,
String.format("%.3f", metrics.thinBoxRate),
String.format("%.5f", metrics.averageRelativeBoxVolume),
String.format("%.3f", metrics.centerSpreadRatio),
String.format("%.1f", metrics.averageAspectRatio),
observation.candidatePrediction,
winnerLabel(observation),
observation.candidatePredictionMatchLabel(),
bestMillisLabel(observation),
predictedMillisLabel(observation),
lossMillisLabel(observation)));
}
private static String winnerLabel(BvhHeuristicTimingSupport.Observation observation) {
if ("OLD".equals(observation.candidatePrediction)) {
return "OLD";
}
return observation.fastestBvh.variant;
}
private static String bestMillisLabel(BvhHeuristicTimingSupport.Observation observation) {
if ("OLD".equals(observation.candidatePrediction)) {
return "-";
}
return String.format("%.3f", observation.fastestBvh.averageMillis());
}
private static String predictedMillisLabel(BvhHeuristicTimingSupport.Observation observation) {
if (observation.predictedBvh == null) {
return "-";
}
return String.format("%.3f", observation.predictedBvh.averageMillis());
}
private static String lossMillisLabel(BvhHeuristicTimingSupport.Observation observation) {
if (observation.predictedBvh == null) {
return "-";
}
double lossMillis = observation.predictedBvh.averageMillis() - observation.fastestBvh.averageMillis();
return String.format("%.3f", lossMillis);
}
private static void printAggregateSummary(List<RealObservation> observations) {
System.out.println();
System.out.println("[REAL-CITYGML-BVH-HEURISTIC-SUMMARY]");
System.out.println(String.format(
"%-7s | %5s | %7s | %7s | %8s | %10s | %10s | %10s | %10s",
"check", "cases", "tested", "match", "accuracy", "thin med", "relVol med", "spread med", "aspect med"));
System.out.println("-----------------------------------------------------------------------------------------------");
for (String checkName : List.of("SSI", "NESTED", "RSI")) {
List<RealObservation> checkObservations = observations.stream()
.filter(observation -> checkName.equals(observation.checkName))
.collect(Collectors.toList());
printCheckSummary(checkName, checkObservations);
}
System.out.println();
System.out.println("[REAL-CITYGML-BVH-METRIC-QUANTILES]");
System.out.println(String.format(
"%-7s | %-7s | %10s | %10s | %10s | %10s | %10s",
"check", "metric", "min", "p25", "median", "p75", "max"));
System.out.println("--------------------------------------------------------------------------------");
for (String checkName : List.of("SSI", "NESTED", "RSI")) {
List<RealObservation> checkObservations = observations.stream()
.filter(observation -> checkName.equals(observation.checkName))
.collect(Collectors.toList());
printMetricQuantiles(checkName, "thin", checkObservations,
observation -> observation.observation.metrics.thinBoxRate);
printMetricQuantiles(checkName, "relVol", checkObservations,
observation -> observation.observation.metrics.averageRelativeBoxVolume);
printMetricQuantiles(checkName, "spread", checkObservations,
observation -> observation.observation.metrics.centerSpreadRatio);
printMetricQuantiles(checkName, "aspect", checkObservations,
observation -> observation.observation.metrics.averageAspectRatio);
}
}
private static void printCheckSummary(String checkName, List<RealObservation> observations) {
long tested = observations.stream()
.filter(observation -> !"OLD".equals(observation.observation.candidatePrediction))
.count();
long matches = observations.stream()
.filter(observation -> "yes".equals(observation.observation.candidatePredictionMatchLabel()))
.count();
String accuracy = tested == 0L ? "n/a" : String.format("%.1f%%", 100.0 * matches / tested);
System.out.println(String.format(
"%-7s | %5d | %7d | %7d | %8s | %10.3f | %10.5f | %10.3f | %10.1f",
checkName,
observations.size(),
tested,
matches,
accuracy,
median(observations, observation -> observation.observation.metrics.thinBoxRate),
median(observations, observation -> observation.observation.metrics.averageRelativeBoxVolume),
median(observations, observation -> observation.observation.metrics.centerSpreadRatio),
median(observations, observation -> observation.observation.metrics.averageAspectRatio)));
}
private static void printMetricQuantiles(
String checkName,
String metricName,
List<RealObservation> observations,
MetricValue metricValue) {
double[] values = sortedValues(observations, metricValue);
System.out.println(String.format(Locale.ROOT,
"%-7s | %-7s | %10.5f | %10.5f | %10.5f | %10.5f | %10.5f",
checkName,
metricName,
quantile(values, 0.0),
quantile(values, 0.25),
quantile(values, 0.5),
quantile(values, 0.75),
quantile(values, 1.0)));
}
private static double median(List<RealObservation> observations, MetricValue metricValue) {
return quantile(sortedValues(observations, metricValue), 0.5);
}
private static double[] sortedValues(List<RealObservation> observations, MetricValue metricValue) {
return observations.stream()
.mapToDouble(metricValue::value)
.sorted()
.toArray();
}
private static double quantile(double[] sortedValues, double probability) {
if (sortedValues.length == 0) {
return 0.0;
}
double position = probability * (sortedValues.length - 1);
int lower = (int) Math.floor(position);
int upper = (int) Math.ceil(position);
if (lower == upper) {
return sortedValues[lower];
}
double fraction = position - lower;
return sortedValues[lower] * (1.0 - fraction) + sortedValues[upper] * fraction;
}
private static void printFooter() {
System.out.println("Broad-phase timing only; full validation checks and AUTO thresholds are not changed here.");
System.out.println();
}
private static void printSeparator() {
printSeparator(String.format(
TABLE_FORMAT,
"", "", "", "", "", "", "", "", "", "", "", "", "").length());
}
private static void printSeparator(int width) {
System.out.println("-".repeat(width));
}
private static String shortName(String fileName) {
String name = fileName.replace(".gml", "");
if (name.length() <= FILE_COLUMN_WIDTH) {
return name;
}
return name.substring(0, FILE_COLUMN_WIDTH - 1) + ".";
}
private enum QueryMode {
INTERSECTING,
CONTAINED
}
private static final class ModelElements {
final Set<Polygon> seenPolygons = Collections.newSetFromMap(new IdentityHashMap<>());
final List<Polygon> polygons = new ArrayList<>();
final List<AABB> polygonBoxes = new ArrayList<>();
final List<LinearRing> innerRings = new ArrayList<>();
final List<AABB> innerRingBoxes = new ArrayList<>();
final List<AABB> edgeBoxes = new ArrayList<>();
}
private static final class RealObservation {
final String datasetName;
final String fileName;
final String checkName;
final BvhHeuristicTimingSupport.Observation observation;
RealObservation(
String datasetName,
String fileName,
String checkName,
BvhHeuristicTimingSupport.Observation observation) {
this.datasetName = datasetName;
this.fileName = fileName;
this.checkName = checkName;
this.observation = observation;
}
}
private interface MetricValue {
double value(RealObservation observation);
}
}
...@@ -139,8 +139,37 @@ final class SyntheticNestedRingGeometryFactory { ...@@ -139,8 +139,37 @@ final class SyntheticNestedRingGeometryFactory {
return polygon; return polygon;
} }
static ConcretePolygon aspectSweepInnerRings(int count, double aspectRatio) {
int columns = (int) Math.ceil(Math.sqrt(count));
double width = Math.max(1.0, aspectRatio);
double extent = columns * (width + 4.0) + 20.0;
ConcretePolygon polygon = basePolygonWithExtent(extent);
for (int i = 0; i < count; i++) {
double x = 5.0 + (i % columns) * (width + 4.0);
double y = 5.0 + (i / columns) * 5.0;
polygon.addInteriorRing(rectangleRing(x, y, x + width, y + 1.0, LinearRingType.INTERIOR));
}
return polygon;
}
static ConcretePolygon spreadSweepInnerRings(int count, double spacing) {
int columns = (int) Math.ceil(Math.sqrt(count));
double extent = columns * spacing + 20.0;
ConcretePolygon polygon = basePolygonWithExtent(extent);
for (int i = 0; i < count; i++) {
double centerX = 5.0 + (i % columns) * spacing;
double centerY = 5.0 + (i / columns) * spacing;
polygon.addInteriorRing(squareRing(centerX, centerY, 0.8));
}
return polygon;
}
private static ConcretePolygon basePolygon(int expectedInnerRingCount) { private static ConcretePolygon basePolygon(int expectedInnerRingCount) {
double extent = Math.max(120.0, Math.ceil(Math.sqrt(Math.max(1, expectedInnerRingCount))) * 10.0 + 20.0); double extent = Math.max(120.0, Math.ceil(Math.sqrt(Math.max(1, expectedInnerRingCount))) * 10.0 + 20.0);
return basePolygonWithExtent(extent);
}
private static ConcretePolygon basePolygonWithExtent(double extent) {
ConcretePolygon polygon = new ConcretePolygon(); ConcretePolygon polygon = new ConcretePolygon();
polygon.setExteriorRing(rectangleRing(0.0, 0.0, extent, extent, LinearRingType.EXTERIOR)); polygon.setExteriorRing(rectangleRing(0.0, 0.0, extent, extent, LinearRingType.EXTERIOR));
return polygon; return polygon;
......
...@@ -68,6 +68,29 @@ final class SyntheticRingGeometryFactory { ...@@ -68,6 +68,29 @@ final class SyntheticRingGeometryFactory {
return geometry; return geometry;
} }
static Geometry aspectSweepGeometry(int ringCount, double aspectRatio) {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
double width = Math.max(1.0, aspectRatio);
for (int i = 0; i < ringCount; i++) {
double x = (i % 20) * (width + 4.0);
double y = (i / 20) * 5.0;
addRingPolygon(geometry, rectangle(x, y, width, 1.0));
}
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry spreadSweepGeometry(int ringCount, double spacing) {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
for (int i = 0; i < ringCount; i++) {
double x = (i % 20) * spacing;
double y = (i / 20) * spacing;
addRingPolygon(geometry, rectangle(x, y, 3.0, 3.0));
}
geometry.updateEdgesAndVertices();
return geometry;
}
private static void addRingPolygon(Geometry geometry, double[][] coordinates) { private static void addRingPolygon(Geometry geometry, double[][] coordinates) {
ConcretePolygon polygon = new ConcretePolygon(); ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR); LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
...@@ -105,6 +128,16 @@ final class SyntheticRingGeometryFactory { ...@@ -105,6 +128,16 @@ final class SyntheticRingGeometryFactory {
}; };
} }
private static double[][] rectangle(double x, double y, double width, double height) {
return new double[][] {
{x, y, 0.0},
{x + width, y, 0.0},
{x + width, y + height, 0.0},
{x, y + height, 0.0},
{x, y, 0.0}
};
}
private static double[][] bowTie() { private static double[][] bowTie() {
return new double[][] { return new double[][] {
{20.0, 0.0, 0.0}, {20.0, 0.0, 0.0},
......
...@@ -91,6 +91,62 @@ final class SyntheticSolidGeometryFactory { ...@@ -91,6 +91,62 @@ final class SyntheticSolidGeometryFactory {
return geometry; return geometry;
} }
static Geometry thinRatioSweepBoxes(Lod lod, int count, double thinRatio) {
Geometry geometry = newSolid(lod);
int columns = (int) Math.ceil(Math.sqrt(count));
int thinCount = (int) Math.round(count * thinRatio);
for (int i = 0; i < count; i++) {
double x = (i % columns) * 50.0;
double y = (i / columns) * 8.0;
if (i < thinCount) {
addBox(geometry, x, y, 0.0, 40.0, 0.35, 1.0);
} else {
addBox(geometry, x, y, 0.0, 4.0, 4.0, 4.0);
}
}
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry aspectSweepBoxes(Lod lod, int count, double aspectRatio) {
Geometry geometry = newSolid(lod);
int columns = (int) Math.ceil(Math.sqrt(count));
double width = Math.max(1.0, aspectRatio);
double spacingX = width + 3.0;
for (int i = 0; i < count; i++) {
double x = (i % columns) * spacingX;
double y = (i / columns) * 4.0;
addBox(geometry, x, y, 0.0, width, 1.0, 1.0);
}
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry spreadSweepBoxes(Lod lod, int count, double spacing) {
Geometry geometry = newSolid(lod);
int columns = (int) Math.ceil(Math.sqrt(count));
for (int i = 0; i < count; i++) {
double x = (i % columns) * spacing;
double y = (i / columns) * spacing;
addBox(geometry, x, y, 0.0, 3.0, 3.0, 3.0);
}
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry relativeVolumeSweepBoxes(Lod lod, int count, double boxSize, double spacingFactor) {
Geometry geometry = newSolid(lod);
int columns = (int) Math.ceil(Math.sqrt(count));
double spacing = boxSize * spacingFactor;
for (int i = 0; i < count; i++) {
double x = (i % columns) * spacing;
double y = (i / columns) * spacing;
addBox(geometry, x, y, 0.0, boxSize, boxSize, boxSize);
}
geometry.updateEdgesAndVertices();
return geometry;
}
private static Geometry newSolid(Lod lod) { private static Geometry newSolid(Lod lod) {
return new Geometry(GeometryType.SOLID, lod, Orientation.OUTWARD); return new Geometry(GeometryType.SOLID, lod, Orientation.OUTWARD);
} }
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment