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
/.idea/codeStyles/codeStyleConfig.xml
/GUISettings.properties
/.idea/inspectionProfiles/Project_Default.xml
# Local empirical data for BVH exploration
/CityDoctorValidation/src/test/resources/real-citygml/
......@@ -47,6 +47,30 @@ public class AABB {
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
* coordinates
......@@ -190,6 +214,26 @@ public class AABB {
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() {
return (minX + maxX) / 2.0;
}
......@@ -234,9 +278,9 @@ public class AABB {
* Returns the index of the longest axis in the AABB
*/
public int findLongestAxis() {
double x = getMaxX() - getMinX();
double y = getMaxY() - getMinY();
double z = getMaxZ() - getMinZ();
double x = getExtentX();
double y = getExtentY();
double z = getExtentZ();
if (x > y && x > z)
return 0;
......@@ -247,9 +291,9 @@ public class AABB {
/** Returns true if the AABB is (nearly) flat in at least two axes. */
public boolean isDegenerate(double tol) {
double dx = getMaxX() - getMinX();
double dy = getMaxY() - getMinY();
double dz = getMaxZ() - getMinZ();
double dx = getExtentX();
double dy = getExtentY();
double dz = getExtentZ();
int flatAxes = 0;
if (dx <= tol)
flatAxes++;
......
......@@ -35,7 +35,6 @@ import de.hft.stuttgart.citydoctor2.check.Requirement;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.NestedRingError;
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.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
......@@ -136,11 +135,32 @@ public class NestedRingsCheck extends Check {
checkWithBoundingBoxFilter(p);
} else if (variant.isBvh()) {
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(
BvhCheckType.NESTED_RINGS, p.getInnerRings().size());
checkWithBvhFilter(p, splitStrategy);
} else if (p.getInnerRings().size() > 3) {
BvhUsagePolicy.BvhCheckType.NESTED_RINGS, summary);
checkWithBvhFilter(p, splitStrategy, ringBoxes);
} else if (innerRings.size() > 3) {
checkWithBoundingBoxFilter(p);
} else {
checkOriginal(p);
......@@ -183,6 +203,11 @@ public class NestedRingsCheck extends Check {
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.newWithStrategy(innerRings, ringBoxes::get, splitStrategy);
......
......@@ -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.checks.Checks;
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.datastructure.Edge;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
......@@ -158,9 +157,9 @@ public class RingSelfIntCheck extends Check {
}
if (variant == Variant.AUTO) {
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(
BvhCheckType.RING_SELF_INTERSECTION, edgeCount);
BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, edgeCount);
checkRingBvh(lr, splitStrategy);
} else {
checkRingOld(lr);
......
......@@ -33,11 +33,11 @@ import de.hft.stuttgart.citydoctor2.check.RequirementType;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError;
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.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
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.SplitStrategy;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
......@@ -155,12 +155,12 @@ public class SolidSelfIntCheck extends Check {
return calculateIntersectionsWithTree(g, variant.getSplitStrategy());
}
int polygonCount = g.getPolygons().size();
if (!BvhUsagePolicy.shouldUseTree(BvhCheckType.SOLID_SELF_INTERSECTION, polygonCount)) {
BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofElements(g.getPolygons(), AABB::of);
if (!BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary)) {
return SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta);
}
SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy(
BvhCheckType.SOLID_SELF_INTERSECTION, polygonCount);
BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary);
return calculateIntersectionsWithTree(g, splitStrategy);
}
......
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;
/**
......@@ -10,6 +15,8 @@ public final class BvhUsagePolicy {
private static final int SOLID_SELF_INTERSECTION_TREE_THRESHOLD = 16;
private static final int NESTED_RINGS_TREE_THRESHOLD = 8;
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() {
}
......@@ -20,18 +27,253 @@ public final class BvhUsagePolicy {
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) {
return shouldUseTree(checkType, BvhInputSummary.countOnly(elementCount));
}
public static boolean shouldUseTree(BvhCheckType checkType, BvhInputSummary summary) {
int elementCount = summary.getElementCount();
if (elementCount < 2) {
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) {
return chooseSplitStrategy(checkType, BvhInputSummary.countOnly(elementCount));
}
public static SplitStrategy chooseSplitStrategy(BvhCheckType checkType, BvhInputSummary summary) {
if (checkType == BvhCheckType.RING_SELF_INTERSECTION) {
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.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;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
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.
*
* 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.
*
* @author Numanoglu
......@@ -22,6 +23,7 @@ final class BvhInputMetricsCollector {
private static final double DEGENERATE_TOLERANCE = 1e-12;
private static final long MAX_PAIR_SAMPLES = 20_000L;
private static final double THIN_BOX_ASPECT_RATIO = 20.0;
private BvhInputMetricsCollector() {
}
......@@ -30,14 +32,30 @@ final class BvhInputMetricsCollector {
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) {
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.
*/
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());
for (E element : elements) {
AABB aabb = aabbFunction.apply(element);
......@@ -46,7 +64,7 @@ final class BvhInputMetricsCollector {
}
}
return collectBoxes(scenario, boxes);
return collectBoxes(scenario, boxes, samplePairs);
}
/**
......@@ -54,16 +72,27 @@ final class BvhInputMetricsCollector {
* are sampled when the full pairs set would be too large.
*/
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();
if (count == 0) {
return Metrics.empty(scenario);
}
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 thinBoxCount = 0;
double totalAspectRatio = 0.0;
double totalRelativeVolume = 0.0;
double totalDx = 0.0;
double totalDy = 0.0;
double totalDz = 0.0;
......@@ -76,13 +105,18 @@ final class BvhInputMetricsCollector {
degenerateCount++;
}
double dx = extentX(box);
double dy = extentY(box);
double dz = extentZ(box);
double dx = box.getExtentX();
double dy = box.getExtentY();
double dz = box.getExtentZ();
double currentAspectRatio = aspectRatio(dx, dy, dz);
if (currentAspectRatio >= THIN_BOX_ASPECT_RATIO) {
thinBoxCount++;
}
totalDx += dx;
totalDy += dy;
totalDz += dz;
totalAspectRatio += aspectRatio(dx, dy, dz);
totalAspectRatio += currentAspectRatio;
totalRelativeVolume += relativeVolume(dx, dy, dz, aggregate);
totalCx += box.getCenterX();
totalCy += box.getCenterY();
totalCz += box.getCenterZ();
......@@ -108,13 +142,19 @@ final class BvhInputMetricsCollector {
pairSample.overlapRate(),
pairSample.containmentRate(),
(double) degenerateCount / count,
(double) thinBoxCount / count,
totalAspectRatio / count,
totalRelativeVolume / count,
totalDx / count,
totalDy / count,
totalDz / count,
Math.sqrt(varianceX / count) / positiveOrOne(aggregate.dx),
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 {
}
private static Aggregate aggregate(List<AABB> boxes) {
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;
AABB totalBox = AABB.enclosing(boxes);
return new Aggregate(totalBox.getExtentX(), totalBox.getExtentY(), totalBox.getExtentZ());
}
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());
private static double relativeVolume(double dx, double dy, double dz, Aggregate aggregate) {
double localMeasure = 1.0;
double totalMeasure = 1.0;
boolean hasActiveExtent = false;
if (dx > DEGENERATE_TOLERANCE) {
localMeasure *= dx;
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) {
......@@ -174,18 +225,6 @@ final class BvhInputMetricsCollector {
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) {
double max = Math.max(dx, Math.max(dy, dz));
double min = Math.min(positiveOrMax(dx), Math.min(positiveOrMax(dy), positiveOrMax(dz)));
......@@ -237,6 +276,10 @@ final class BvhInputMetricsCollector {
double containmentRate() {
return sampledPairs == 0L ? 0.0 : (double) containedPairs / sampledPairs;
}
static PairSample empty() {
return new PairSample(0L, 0L, 0L);
}
}
static final class Metrics {
......@@ -247,13 +290,17 @@ final class BvhInputMetricsCollector {
final double sampledOverlapRate;
final double sampledContainmentRate;
final double degenerateRate;
final double thinBoxRate;
final double averageAspectRatio;
final double averageRelativeBoxVolume;
final double averageExtentX;
final double averageExtentY;
final double averageExtentZ;
final double normalizedCenterSpreadX;
final double normalizedCenterSpreadY;
final double normalizedCenterSpreadZ;
final double centerSpreadRatio;
final BvhUsagePolicy.BvhInputSummary summary;
private Metrics(
String scenario,
......@@ -263,13 +310,17 @@ final class BvhInputMetricsCollector {
double sampledOverlapRate,
double sampledContainmentRate,
double degenerateRate,
double thinBoxRate,
double averageAspectRatio,
double averageRelativeBoxVolume,
double averageExtentX,
double averageExtentY,
double averageExtentZ,
double normalizedCenterSpreadX,
double normalizedCenterSpreadY,
double normalizedCenterSpreadZ) {
double normalizedCenterSpreadZ,
double centerSpreadRatio,
BvhUsagePolicy.BvhInputSummary summary) {
this.scenario = scenario;
this.elementCount = elementCount;
this.totalPairs = totalPairs;
......@@ -277,25 +328,30 @@ final class BvhInputMetricsCollector {
this.sampledOverlapRate = sampledOverlapRate;
this.sampledContainmentRate = sampledContainmentRate;
this.degenerateRate = degenerateRate;
this.thinBoxRate = thinBoxRate;
this.averageAspectRatio = averageAspectRatio;
this.averageRelativeBoxVolume = averageRelativeBoxVolume;
this.averageExtentX = averageExtentX;
this.averageExtentY = averageExtentY;
this.averageExtentZ = averageExtentZ;
this.normalizedCenterSpreadX = normalizedCenterSpreadX;
this.normalizedCenterSpreadY = normalizedCenterSpreadY;
this.normalizedCenterSpreadZ = normalizedCenterSpreadZ;
this.centerSpreadRatio = centerSpreadRatio;
this.summary = summary;
}
static Metrics empty(String scenario) {
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() {
return String.format(Locale.ROOT,
"[BVH-METRICS] %s n=%d pairs=%d sampled=%d overlap=%.4f containment=%.4f"
+ " degenerate=%.4f aspectAvg=%.2f avgExtent=(%.2f, %.2f, %.2f)"
+ " centerSpread=(%.3f, %.3f, %.3f)",
+ " degenerate=%.4f thin=%.4f aspectAvg=%.2f relVol=%.6f"
+ " avgExtent=(%.2f, %.2f, %.2f) centerSpread=(%.3f, %.3f, %.3f) spread=%.3f",
scenario,
elementCount,
totalPairs,
......@@ -303,13 +359,16 @@ final class BvhInputMetricsCollector {
sampledOverlapRate,
sampledContainmentRate,
degenerateRate,
thinBoxRate,
averageAspectRatio,
averageRelativeBoxVolume,
averageExtentX,
averageExtentY,
averageExtentZ,
normalizedCenterSpreadX,
normalizedCenterSpreadY,
normalizedCenterSpreadZ);
normalizedCenterSpreadZ,
centerSpreadRatio);
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Tag;
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.datastructure.ConcretePolygon;
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.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;
/**
* Collects SSI timing observations together with BVH input metrics.
*
* The summary is meant as a working note for deriving conservative strategy
* rules after the fixture set changes.
* Collects compact timing observations and broad-phase metrics for BVH strategy
* selection from synthetic fixtures.
*
* @author Numanoglu
*/
......@@ -28,64 +33,67 @@ import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
public class BvhStrategyHeuristicExplorationTest {
private static final double DELTA = 0.001;
private static final double EPSILON = 0.001;
@Test
public void exploreSsiStrategyCandidatesFromMetricsAndTiming() {
List<Scenario> scenarios = new ArrayList<>();
scenarios.add(new Scenario("ssi-separated-grid",
SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 12, 12)));
scenarios.add(new Scenario("ssi-overlapping-grid",
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));
}
public void exploreSsiNestedAndRsiStrategyCandidates() {
List<BvhHeuristicTimingSupport.Observation> observations = new ArrayList<>();
addSsiObservations(observations);
addNestedObservations(observations);
addRsiObservations(observations);
printObservationSummary(observations);
printBucketSummary(observations);
}
private static Observation measureScenario(Scenario scenario) {
List<Polygon> polygons = scenario.geometry.getPolygons();
BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.forPolygons(scenario.name, polygons);
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
BvhPerformanceTestSupport.Measurement bruteForce = BvhPerformanceTestSupport.measure(
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(
private static void addSsiObservations(List<BvhHeuristicTimingSupport.Observation> observations) {
for (BvhSyntheticScenarioCatalog.SsiScenario scenario : BvhSyntheticScenarioCatalog.ssiScenarios()) {
List<Polygon> polygons = scenario.geometry.getPolygons();
BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.forPolygonsCheap(scenario.name, polygons);
observations.add(BvhHeuristicTimingSupport.measureVariants(
"SSI",
scenario.name,
strategy.name(),
metrics,
polygons.size(),
() -> calculateWithTree(scenario.geometry, strategy));
measurements.add(bvhMeasurement);
assertEquals("SSI result count differs for " + scenario.name + " / " + strategy,
bruteForce.resultCount, bvhMeasurement.resultCount);
"BRUTE_FORCE",
() -> SelfIntersectionUtil.calculateSolidSelfIntersection0(scenario.geometry, DELTA).size(),
strategy -> calculateSsiWithTree(scenario.geometry, strategy)));
}
}
BvhPerformanceTestSupport.printScenarioSummary(scenario.name, measurements);
return new Observation(metrics, fastest(measurements), fastestBvh(measurements), bruteForce);
private static void addNestedObservations(List<BvhHeuristicTimingSupport.Observation> observations) {
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.newWithStrategy(
geometry.getPolygons(),
......@@ -94,109 +102,245 @@ public class BvhStrategyHeuristicExplorationTest {
return SelfIntersectionUtil.calculateSolidSelfIntersection(geometry, DELTA, tree).size();
}
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;
private static int runNestedCheck(ConcretePolygon polygon, NestedRingsCheck.Variant variant) {
NestedRingsCheck check = new NestedRingsCheck(variant);
check.check(polygon);
CheckResult result = polygon.getCheckResult(check);
return result.getResultStatus() == ResultStatus.ERROR ? 1 : 0;
}
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(
List<BvhPerformanceTestSupport.Measurement> measurements) {
BvhPerformanceTestSupport.Measurement fastest = null;
for (BvhPerformanceTestSupport.Measurement measurement : measurements) {
if ("BRUTE_FORCE".equals(measurement.variant)) {
continue;
}
if (fastest == null || measurement.averageNanos < fastest.averageNanos) {
fastest = measurement;
private static int countEdges(Geometry geometry) {
int edgeCount = 0;
for (Polygon polygon : geometry.getPolygons()) {
LinearRing ring = polygon.getExteriorRing();
edgeCount += Math.max(0, ring.getVertices().size() - 1);
}
return edgeCount;
}
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("[BVH-HEURISTIC-EXPLORATION] SSI observations");
System.out.println("---------------------------------------------------------------------------------------------------------------");
System.out.println("[BVH-HEURISTIC-EXPLORATION]");
System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
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",
"n",
"overlap",
"contain",
"degen",
"thin",
"relVol",
"spread",
"aspect",
"winner",
"policyPred",
"candidatePred",
"fastest BVH",
"initial candidate rule"));
System.out.println("---------------------------------------------------------------------------------------------------------------");
"bvh ms"));
System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
for (Observation observation : observations) {
for (BvhHeuristicTimingSupport.Observation observation : observations) {
BvhInputMetricsCollector.Metrics metrics = observation.metrics;
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.elementCount,
metrics.sampledOverlapRate,
metrics.sampledContainmentRate,
metrics.degenerateRate,
metrics.thinBoxRate,
metrics.averageRelativeBoxVolume,
metrics.centerSpreadRatio,
metrics.averageAspectRatio,
observation.fastest.variant,
observation.syntheticPrediction,
observation.candidatePrediction,
observation.fastestBvh.variant,
candidateRule(observation)));
observation.fastestBvh.averageMillis()));
}
System.out.println("---------------------------------------------------------------------------------------------------------------");
System.out.println("Candidate rules are prelim; check them against real CityGML models.");
System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
System.out.println("Fastest BVH is the target for candidate strategy rules.");
System.out.println();
}
private static String candidateRule(Observation observation) {
BvhInputMetricsCollector.Metrics metrics = observation.metrics;
if ("BRUTE_FORCE".equals(observation.fastest.variant)) {
return "consider brute below/near this n";
private static void printBucketSummary(List<BvhHeuristicTimingSupport.Observation> observations) {
Map<String, BucketStats> buckets = new LinkedHashMap<>();
for (BvhHeuristicTimingSupport.Observation observation : observations) {
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) {
return "high overlap: compare BVH vs brute";
System.out.println();
}
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 {
final String name;
final Geometry geometry;
private static String relVolBucket(double value) {
if (value <= 0.00001) {
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) {
this.name = name;
this.geometry = geometry;
private static String aspectBucket(double value) {
if (value < 5.0) {
return "<5";
}
if (value < 20.0) {
return "5..20";
}
if (value < 80.0) {
return "20..80";
}
return ">=80";
}
private static final class Observation {
final BvhInputMetricsCollector.Metrics metrics;
final BvhPerformanceTestSupport.Measurement fastest;
final BvhPerformanceTestSupport.Measurement fastestBvh;
final BvhPerformanceTestSupport.Measurement bruteForce;
private static final class BucketStats {
final String checkName;
final String metricName;
final String bucketName;
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(
BvhInputMetricsCollector.Metrics metrics,
BvhPerformanceTestSupport.Measurement fastest,
BvhPerformanceTestSupport.Measurement fastestBvh,
BvhPerformanceTestSupport.Measurement bruteForce) {
this.metrics = metrics;
this.fastest = fastest;
this.fastestBvh = fastestBvh;
this.bruteForce = bruteForce;
String mostCommonWinner() {
String bestWinner = "";
int bestCount = -1;
for (Map.Entry<String, Integer> entry : winners.entrySet()) {
if (entry.getValue() > bestCount) {
bestWinner = entry.getKey();
bestCount = entry.getValue();
}
}
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 {
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) {
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();
polygon.setExteriorRing(rectangleRing(0.0, 0.0, extent, extent, LinearRingType.EXTERIOR));
return polygon;
......
......@@ -68,6 +68,29 @@ final class SyntheticRingGeometryFactory {
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) {
ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
......@@ -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() {
return new double[][] {
{20.0, 0.0, 0.0},
......
......@@ -91,6 +91,62 @@ final class SyntheticSolidGeometryFactory {
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) {
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