Commit cd89dc71 authored by Numanoglu's avatar Numanoglu
Browse files

Add BVH performance metrics and exploration tests

parent b073d9bc
Pipeline #12396 passed with stage
in 2 minutes and 1 second
package de.hft.stuttgart.citydoctor2.checks.bht;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
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;
/**
* 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,
* synthetic fixtures, and parsed CityGML models.
*
* @author Numanoglu
*/
final class BvhInputMetricsCollector {
private static final double DEGENERATE_TOLERANCE = 1e-12;
private static final long MAX_PAIR_SAMPLES = 20_000L;
private BvhInputMetricsCollector() {
}
static Metrics forPolygons(String scenario, List<? extends Polygon> polygons) {
return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal()));
}
static Metrics forRings(String scenario, List<? extends LinearRing> rings) {
return collect(scenario, rings, AABB::of);
}
/**
* 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) {
List<AABB> boxes = new ArrayList<>(elements.size());
for (E element : elements) {
AABB aabb = aabbFunction.apply(element);
if (aabb != null) {
boxes.add(aabb);
}
}
return collectBoxes(scenario, boxes);
}
/**
* Collects metrics from precomputed AABBs. Pairwise overlap and containment
* are sampled when the full pairs set would be too large.
*/
static Metrics collectBoxes(String scenario, List<AABB> boxes) {
int count = boxes.size();
if (count == 0) {
return Metrics.empty(scenario);
}
Aggregate aggregate = aggregate(boxes);
PairSample pairSample = samplePairs(boxes);
int degenerateCount = 0;
double totalAspectRatio = 0.0;
double totalDx = 0.0;
double totalDy = 0.0;
double totalDz = 0.0;
double totalCx = 0.0;
double totalCy = 0.0;
double totalCz = 0.0;
for (AABB box : boxes) {
if (box.isDegenerate(DEGENERATE_TOLERANCE)) {
degenerateCount++;
}
double dx = extentX(box);
double dy = extentY(box);
double dz = extentZ(box);
totalDx += dx;
totalDy += dy;
totalDz += dz;
totalAspectRatio += aspectRatio(dx, dy, dz);
totalCx += box.getCenterX();
totalCy += box.getCenterY();
totalCz += box.getCenterZ();
}
double meanCx = totalCx / count;
double meanCy = totalCy / count;
double meanCz = totalCz / count;
double varianceX = 0.0;
double varianceY = 0.0;
double varianceZ = 0.0;
for (AABB box : boxes) {
varianceX += square(box.getCenterX() - meanCx);
varianceY += square(box.getCenterY() - meanCy);
varianceZ += square(box.getCenterZ() - meanCz);
}
return new Metrics(
scenario,
count,
countPairs(count),
pairSample.sampledPairs,
pairSample.overlapRate(),
pairSample.containmentRate(),
(double) degenerateCount / count,
totalAspectRatio / 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));
}
/**
* Emits one compact metrics line that can be read next to a performance table.
*/
static void print(Metrics metrics) {
System.out.println(metrics.toSummaryLine());
}
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;
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 Aggregate(maxX - minX, maxY - minY, maxZ - minZ);
}
private static PairSample samplePairs(List<AABB> boxes) {
long totalPairs = countPairs(boxes.size());
long sampleEvery = Math.max(1L, totalPairs / MAX_PAIR_SAMPLES);
long pairIndex = 0L;
long sampledPairs = 0L;
long overlappingPairs = 0L;
long containedPairs = 0L;
for (int i = 0; i < boxes.size() - 1; i++) {
AABB a = boxes.get(i);
for (int j = i + 1; j < boxes.size(); j++) {
if (pairIndex % sampleEvery == 0L) {
AABB b = boxes.get(j);
sampledPairs++;
if (a.overlaps(b)) {
overlappingPairs++;
}
if (a.contains(b) || b.contains(a)) {
containedPairs++;
}
}
pairIndex++;
}
}
return new PairSample(sampledPairs, overlappingPairs, containedPairs);
}
private static long countPairs(int count) {
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)));
if (min == Double.MAX_VALUE) {
return 1.0;
}
return max / min;
}
private static double positiveOrMax(double value) {
return value > DEGENERATE_TOLERANCE ? value : Double.MAX_VALUE;
}
private static double positiveOrOne(double value) {
return value > DEGENERATE_TOLERANCE ? value : 1.0;
}
private static double square(double value) {
return value * value;
}
private static final class Aggregate {
final double dx;
final double dy;
final double dz;
Aggregate(double dx, double dy, double dz) {
this.dx = dx;
this.dy = dy;
this.dz = dz;
}
}
private static final class PairSample {
final long sampledPairs;
final long overlappingPairs;
final long containedPairs;
PairSample(long sampledPairs, long overlappingPairs, long containedPairs) {
this.sampledPairs = sampledPairs;
this.overlappingPairs = overlappingPairs;
this.containedPairs = containedPairs;
}
double overlapRate() {
return sampledPairs == 0L ? 0.0 : (double) overlappingPairs / sampledPairs;
}
double containmentRate() {
return sampledPairs == 0L ? 0.0 : (double) containedPairs / sampledPairs;
}
}
static final class Metrics {
final String scenario;
final int elementCount;
final long totalPairs;
final long sampledPairs;
final double sampledOverlapRate;
final double sampledContainmentRate;
final double degenerateRate;
final double averageAspectRatio;
final double averageExtentX;
final double averageExtentY;
final double averageExtentZ;
final double normalizedCenterSpreadX;
final double normalizedCenterSpreadY;
final double normalizedCenterSpreadZ;
private Metrics(
String scenario,
int elementCount,
long totalPairs,
long sampledPairs,
double sampledOverlapRate,
double sampledContainmentRate,
double degenerateRate,
double averageAspectRatio,
double averageExtentX,
double averageExtentY,
double averageExtentZ,
double normalizedCenterSpreadX,
double normalizedCenterSpreadY,
double normalizedCenterSpreadZ) {
this.scenario = scenario;
this.elementCount = elementCount;
this.totalPairs = totalPairs;
this.sampledPairs = sampledPairs;
this.sampledOverlapRate = sampledOverlapRate;
this.sampledContainmentRate = sampledContainmentRate;
this.degenerateRate = degenerateRate;
this.averageAspectRatio = averageAspectRatio;
this.averageExtentX = averageExtentX;
this.averageExtentY = averageExtentY;
this.averageExtentZ = averageExtentZ;
this.normalizedCenterSpreadX = normalizedCenterSpreadX;
this.normalizedCenterSpreadY = normalizedCenterSpreadY;
this.normalizedCenterSpreadZ = normalizedCenterSpreadZ;
}
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);
}
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)",
scenario,
elementCount,
totalPairs,
sampledPairs,
sampledOverlapRate,
sampledContainmentRate,
degenerateRate,
averageAspectRatio,
averageExtentX,
averageExtentY,
averageExtentZ,
normalizedCenterSpreadX,
normalizedCenterSpreadY,
normalizedCenterSpreadZ);
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.List;
import java.util.function.Supplier;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.config.Configurator;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
/**
* Shared support for manual BVH performance probes.
*
* It performs a warmup, repeats the measured operation, validates stable
* result counts, suppresses noisy debug output during the measured operation,
* and prints one compact table per scenario. Timings are intended for local
* comparison of variants, not as deterministic CI assertions.
*
* @author Numanoglu
*/
final class BvhPerformanceTestSupport {
static final int DEFAULT_WARMUP_RUNS = 2;
static final int DEFAULT_MEASURE_RUNS = 5;
private static final boolean SUPPRESS_MEASURED_STDOUT = true;
private static final PrintStream SILENT_OUT = new PrintStream(OutputStream.nullOutputStream());
private BvhPerformanceTestSupport() {
}
static SplitStrategy[] concreteStrategies() {
return new SplitStrategy[] {
SplitStrategy.BINARY_OBJECT_MEDIAN,
SplitStrategy.BINARY_OBJECT_MEAN,
SplitStrategy.BINARY_SPATIAL_MEDIAN,
SplitStrategy.OCTONARY_OBJECT_MEDIAN,
SplitStrategy.OCTONARY_OBJECT_MEAN,
SplitStrategy.OCTONARY_SPATIAL_MEDIAN
};
}
static Measurement measure(String scenario, String variant, int inputSize, Supplier<Integer> measuredOperation) {
return measure(
scenario,
variant,
inputSize,
DEFAULT_WARMUP_RUNS,
DEFAULT_MEASURE_RUNS,
measuredOperation);
}
/**
* Measures one variant. The supplied operation must return a deterministic
* result count so correctness can still be checked while timing is collected.
*/
static Measurement measure(
String scenario,
String variant,
int inputSize,
int warmupRuns,
int measureRuns,
Supplier<Integer> measuredOperation) {
for (int i = 0; i < warmupRuns; i++) {
runMeasuredOperation(measuredOperation);
}
long totalNanos = 0L;
int resultCount = -1;
for (int i = 0; i < measureRuns; i++) {
long start = System.nanoTime();
int currentResultCount = runMeasuredOperation(measuredOperation);
totalNanos += System.nanoTime() - start;
if (resultCount < 0) {
resultCount = currentResultCount;
} else if (resultCount != currentResultCount) {
throw new AssertionError("Unstable result count for " + scenario + " / " + variant
+ ": expected " + resultCount + " but was " + currentResultCount);
}
}
Measurement measurement = new Measurement(
scenario,
variant,
inputSize,
resultCount,
totalNanos / measureRuns);
return measurement;
}
private static int runMeasuredOperation(Supplier<Integer> measuredOperation) {
Level originalRootLevel = LogManager.getRootLogger().getLevel();
Configurator.setRootLevel(Level.WARN);
if (!SUPPRESS_MEASURED_STDOUT) {
try {
return measuredOperation.get();
} finally {
Configurator.setRootLevel(originalRootLevel);
}
}
PrintStream originalOut = System.out;
try {
System.setOut(SILENT_OUT);
return measuredOperation.get();
} finally {
System.setOut(originalOut);
Configurator.setRootLevel(originalRootLevel);
}
}
/**
* Prints a scenario-level table and highlights the fastest measured variant.
* The first measurement is treated as the baseline for the speedup column.
*/
static void printScenarioSummary(String scenario, List<Measurement> measurements) {
if (measurements.isEmpty()) {
return;
}
Measurement baseline = measurements.get(0);
Measurement winner = baseline;
for (Measurement measurement : measurements) {
if (measurement.averageNanos < winner.averageNanos) {
winner = measurement;
}
}
System.out.println();
System.out.println("[BVH-PERFORMANCE] " + scenario);
System.out.println("--------------------------------------------------------------------------");
System.out.println(String.format(
"%-34s %10s %10s %12s %10s",
"variant", "input", "result", "avg ms", "vs first"));
System.out.println("--------------------------------------------------------------------------");
for (Measurement measurement : measurements) {
System.out.println(String.format(
"%-34s %10d %10d %12.3f %10.2fx",
measurement.variant,
measurement.inputSize,
measurement.resultCount,
measurement.averageMillis(),
speedup(baseline, measurement)));
}
System.out.println("--------------------------------------------------------------------------");
System.out.println(String.format(
"*** WINNER: %s avgMs=%.3f result=%d ***",
winner.variant,
winner.averageMillis(),
winner.resultCount));
System.out.println();
}
private static double speedup(Measurement baseline, Measurement measurement) {
if (measurement.averageNanos == 0L) {
return 0.0;
}
return (double) baseline.averageNanos / measurement.averageNanos;
}
static final class Measurement {
final String scenario;
final String variant;
final int inputSize;
final int resultCount;
final long averageNanos;
Measurement(String scenario, String variant, int inputSize, int resultCount, long averageNanos) {
this.scenario = scenario;
this.variant = variant;
this.inputSize = inputSize;
this.resultCount = resultCount;
this.averageNanos = averageNanos;
}
double averageMillis() {
return averageNanos / 1_000_000.0;
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
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.
*
* @author Numanoglu
*/
@Tag("performance")
public class BvhStrategyHeuristicExplorationTest {
private static final double DELTA = 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));
}
printObservationSummary(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(
scenario.name,
strategy.name(),
polygons.size(),
() -> calculateWithTree(scenario.geometry, strategy));
measurements.add(bvhMeasurement);
assertEquals("SSI result count differs for " + scenario.name + " / " + strategy,
bruteForce.resultCount, bvhMeasurement.resultCount);
}
BvhPerformanceTestSupport.printScenarioSummary(scenario.name, measurements);
return new Observation(metrics, fastest(measurements), fastestBvh(measurements), bruteForce);
}
private static int calculateWithTree(Geometry geometry, SplitStrategy strategy) {
BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newWithStrategy(
geometry.getPolygons(),
polygon -> AABB.of(polygon.getOriginal()),
strategy);
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;
}
}
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)) {
continue;
}
if (fastest == null || measurement.averageNanos < fastest.averageNanos) {
fastest = measurement;
}
}
return fastest;
}
private static void printObservationSummary(List<Observation> observations) {
System.out.println();
System.out.println("[BVH-HEURISTIC-EXPLORATION] SSI observations");
System.out.println("---------------------------------------------------------------------------------------------------------------");
System.out.println(String.format(
"%-24s %7s %9s %9s %9s %10s %12s %22s %22s",
"scenario",
"n",
"overlap",
"contain",
"degen",
"aspect",
"winner",
"fastest BVH",
"initial candidate rule"));
System.out.println("---------------------------------------------------------------------------------------------------------------");
for (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",
metrics.scenario,
metrics.elementCount,
metrics.sampledOverlapRate,
metrics.sampledContainmentRate,
metrics.degenerateRate,
metrics.averageAspectRatio,
observation.fastest.variant,
observation.fastestBvh.variant,
candidateRule(observation)));
}
System.out.println("---------------------------------------------------------------------------------------------------------------");
System.out.println("Candidate rules are prelim; check them against real CityGML models.");
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";
}
if (metrics.sampledOverlapRate < 0.01 && metrics.elementCount > 200) {
return "low overlap: BVH likely";
}
if (metrics.degenerateRate > 0.50 || metrics.averageAspectRatio > 50.0) {
return "flat/skinny: prefer measured stable BVH";
}
if (metrics.sampledOverlapRate > 0.10) {
return "high overlap: compare BVH vs brute";
}
return "citylike/mixed: prefer fastest BVH candidate";
}
private static final class Scenario {
final String name;
final Geometry geometry;
Scenario(String name, Geometry geometry) {
this.name = name;
this.geometry = geometry;
}
}
private static final class Observation {
final BvhInputMetricsCollector.Metrics metrics;
final BvhPerformanceTestSupport.Measurement fastest;
final BvhPerformanceTestSupport.Measurement fastestBvh;
final BvhPerformanceTestSupport.Measurement bruteForce;
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;
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
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.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
@Tag("performance")
public class NestedRingCheckBvhPerformanceTest {
@Test
public void compareOldAabbAndBvhVariantsOnSyntheticNestedRings() {
measureScenario("nested-disjoint", SyntheticNestedRingGeometryFactory.manyDisjointInnerRings(1_000));
measureScenario("nested-one-pair", SyntheticNestedRingGeometryFactory.oneNestedPairAmongMany(1_000));
measureScenario("nested-concentric", SyntheticNestedRingGeometryFactory.concentricNestedRings(300));
measureScenario("nested-overlap-no-error", SyntheticNestedRingGeometryFactory.overlappingAabbsButNotNested(1_000));
measureScenario("nested-clustered", SyntheticNestedRingGeometryFactory.clusteredInnerRings(16, 80));
measureScenario("nested-clustered-pair", SyntheticNestedRingGeometryFactory.clusteredInnerRingsWithNestedPair(16, 80));
}
private static void measureScenario(String scenario, ConcretePolygon polygon) {
int ringCount = polygon.getInnerRings().size();
BvhInputMetricsCollector.print(BvhInputMetricsCollector.forRings(scenario, polygon.getInnerRings()));
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
BvhPerformanceTestSupport.Measurement oldMeasurement = BvhPerformanceTestSupport.measure(
scenario,
NestedRingsCheck.Variant.OLD.name(),
ringCount,
() -> runCheck(polygon, NestedRingsCheck.Variant.OLD));
measurements.add(oldMeasurement);
BvhPerformanceTestSupport.Measurement aabbMeasurement = BvhPerformanceTestSupport.measure(
scenario,
NestedRingsCheck.Variant.AABB_FILTER.name(),
ringCount,
() -> runCheck(polygon, NestedRingsCheck.Variant.AABB_FILTER));
measurements.add(aabbMeasurement);
assertEquals("Nested result differs for " + scenario + " / AABB_FILTER",
oldMeasurement.resultCount, aabbMeasurement.resultCount);
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
NestedRingsCheck.Variant variant = variantFor(strategy);
BvhPerformanceTestSupport.Measurement bvhMeasurement = BvhPerformanceTestSupport.measure(
scenario,
variant.name(),
ringCount,
() -> runCheck(polygon, variant));
measurements.add(bvhMeasurement);
assertEquals("Nested result differs for " + scenario + " / " + variant,
oldMeasurement.resultCount, bvhMeasurement.resultCount);
}
BvhPerformanceTestSupport.printScenarioSummary(scenario, measurements);
}
private static int runCheck(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 NestedRingsCheck.Variant variantFor(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);
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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.RingSelfIntCheck;
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.bht.SplitStrategy;
@Tag("performance")
public class RingSelfIntCheckBvhPerformanceTest {
private static final double EPSILON = 0.001;
@Test
public void compareOldAndBvhVariantsOnSyntheticRings() {
measureScenario("rsi-small", SyntheticRingGeometryFactory.performanceRingGeometry(200, 100, EPSILON));
measureScenario("rsi-medium", SyntheticRingGeometryFactory.performanceRingGeometry(1_000, 500, EPSILON));
measureScenario("rsi-large", SyntheticRingGeometryFactory.performanceRingGeometry(5_000, 2_000, EPSILON));
}
private static void measureScenario(String scenario, Geometry geometry) {
int edgeCount = countEdges(geometry);
BvhInputMetricsCollector.print(BvhInputMetricsCollector.forRings(scenario, collectExteriorRings(geometry)));
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
BvhPerformanceTestSupport.Measurement oldMeasurement = BvhPerformanceTestSupport.measure(
scenario,
RingSelfIntCheck.Variant.OLD.name(),
edgeCount,
() -> runCheck(geometry, RingSelfIntCheck.Variant.OLD));
measurements.add(oldMeasurement);
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
RingSelfIntCheck.Variant variant = variantFor(strategy);
BvhPerformanceTestSupport.Measurement bvhMeasurement = BvhPerformanceTestSupport.measure(
scenario,
variant.name(),
edgeCount,
() -> runCheck(geometry, variant));
measurements.add(bvhMeasurement);
assertEquals("RSI result differs for " + scenario + " / " + variant,
oldMeasurement.resultCount, bvhMeasurement.resultCount);
}
BvhPerformanceTestSupport.printScenarioSummary(scenario, measurements);
}
private static int runCheck(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 errorCount;
}
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 RingSelfIntCheck.Variant variantFor(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);
}
}
}
...@@ -13,15 +13,9 @@ import de.hft.stuttgart.citydoctor2.check.CheckError; ...@@ -13,15 +13,9 @@ import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckResult; import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ResultStatus; import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck; import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck;
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.Geometry.Orientation;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
public class RingSelfIntCheckBvhVariantSyntheticTest { public class RingSelfIntCheckBvhVariantSyntheticTest {
...@@ -29,7 +23,7 @@ public class RingSelfIntCheckBvhVariantSyntheticTest { ...@@ -29,7 +23,7 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
@Test @Test
public void bvhVariantsMatchOriginalOnSyntheticRings() { public void bvhVariantsMatchOriginalOnSyntheticRings() {
Geometry oldGeometry = syntheticRingGeometry(); Geometry oldGeometry = SyntheticRingGeometryFactory.correctnessRingGeometry(EPSILON);
List<LinearRing> oldRings = collectExteriorRings(oldGeometry); List<LinearRing> oldRings = collectExteriorRings(oldGeometry);
List<CheckSummary> oldSummaries = new ArrayList<>(); List<CheckSummary> oldSummaries = new ArrayList<>();
...@@ -38,7 +32,7 @@ public class RingSelfIntCheckBvhVariantSyntheticTest { ...@@ -38,7 +32,7 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
} }
for (RingSelfIntCheck.Variant bvhVariant : bvhVariants()) { for (RingSelfIntCheck.Variant bvhVariant : bvhVariants()) {
Geometry bvhGeometry = syntheticRingGeometry(); Geometry bvhGeometry = SyntheticRingGeometryFactory.correctnessRingGeometry(EPSILON);
List<LinearRing> bvhRings = collectExteriorRings(bvhGeometry); List<LinearRing> bvhRings = collectExteriorRings(bvhGeometry);
assertEquals(oldRings.size(), bvhRings.size()); assertEquals(oldRings.size(), bvhRings.size());
...@@ -78,17 +72,6 @@ public class RingSelfIntCheckBvhVariantSyntheticTest { ...@@ -78,17 +72,6 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
return new CheckSummary(result.getResultStatus(), errorType); return new CheckSummary(result.getResultStatus(), errorType);
} }
private static Geometry syntheticRingGeometry() {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
addRingPolygon(geometry, rectangle());
addRingPolygon(geometry, bowTie());
addRingPolygon(geometry, pointNearEdge());
addRingPolygon(geometry, largeConvexRing(80, 20.0, 40.0, 0.0));
addRingPolygon(geometry, zigZagCorridor(30));
geometry.updateEdgesAndVertices();
return geometry;
}
private static List<LinearRing> collectExteriorRings(Geometry geometry) { private static List<LinearRing> collectExteriorRings(Geometry geometry) {
List<LinearRing> rings = new ArrayList<>(); List<LinearRing> rings = new ArrayList<>();
for (Polygon polygon : geometry.getPolygons()) { for (Polygon polygon : geometry.getPolygons()) {
...@@ -97,91 +80,6 @@ public class RingSelfIntCheckBvhVariantSyntheticTest { ...@@ -97,91 +80,6 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
return rings; return rings;
} }
private static void addRingPolygon(Geometry geometry, double[][] coordinates) {
ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
polygon.setExteriorRing(ring);
geometry.addPolygon(polygon);
Vertex firstVertex = null;
for (int i = 0; i < coordinates.length; i++) {
double[] coordinate = coordinates[i];
if (i == coordinates.length - 1 && sameCoordinate(coordinate, coordinates[0])) {
ring.addVertex(firstVertex);
continue;
}
Vertex vertex = new Vertex(coordinate[0], coordinate[1], coordinate[2]);
if (i == 0) {
firstVertex = vertex;
}
ring.addVertex(vertex);
}
}
private static boolean sameCoordinate(double[] a, double[] b) {
return Double.compare(a[0], b[0]) == 0
&& Double.compare(a[1], b[1]) == 0
&& Double.compare(a[2], b[2]) == 0;
}
private static double[][] rectangle() {
return new double[][] {
{0.0, 0.0, 0.0},
{10.0, 0.0, 0.0},
{10.0, 10.0, 0.0},
{0.0, 10.0, 0.0},
{0.0, 0.0, 0.0}
};
}
private static double[][] bowTie() {
return new double[][] {
{20.0, 0.0, 0.0},
{30.0, 10.0, 0.0},
{30.0, 0.0, 0.0},
{20.0, 10.0, 0.0},
{20.0, 0.0, 0.0}
};
}
private static double[][] pointNearEdge() {
return new double[][] {
{40.0, 0.0, 0.0},
{50.0, 0.0, 0.0},
{50.0, 10.0, 0.0},
{45.0, EPSILON * 0.5, 0.0},
{40.0, 10.0, 0.0},
{40.0, 0.0, 0.0}
};
}
private static double[][] largeConvexRing(int vertexCount, double centerX, double centerY, double z) {
double[][] coordinates = new double[vertexCount + 1][3];
for (int i = 0; i < vertexCount; i++) {
double angle = 2.0 * Math.PI * i / vertexCount;
coordinates[i][0] = centerX + Math.cos(angle) * 8.0;
coordinates[i][1] = centerY + Math.sin(angle) * 5.0;
coordinates[i][2] = z;
}
coordinates[vertexCount][0] = coordinates[0][0];
coordinates[vertexCount][1] = coordinates[0][1];
coordinates[vertexCount][2] = coordinates[0][2];
return coordinates;
}
private static double[][] zigZagCorridor(int segments) {
double[][] coordinates = new double[(segments * 2) + 3][3];
int index = 0;
for (int i = 0; i <= segments; i++) {
coordinates[index++] = new double[] {60.0 + i, i % 2 == 0 ? 0.0 : 1.0, 0.0};
}
for (int i = segments; i >= 0; i--) {
coordinates[index++] = new double[] {60.0 + i, i % 2 == 0 ? 4.0 : 5.0, 0.0};
}
coordinates[index] = new double[] {60.0, 0.0, 0.0};
return coordinates;
}
private static final class CheckSummary { private static final class CheckSummary {
final ResultStatus status; final ResultStatus status;
final Class<?> errorType; final Class<?> errorType;
......
...@@ -58,8 +58,8 @@ public class SolidSelfIntersectionBVHUtilTest { ...@@ -58,8 +58,8 @@ public class SolidSelfIntersectionBVHUtilTest {
); );
Building building = m.getBuildings().findFirst().orElseThrow(); Building building = m.getBuildings().findFirst().orElseThrow();
Geometry g = building.getGeometry(GeometryType.SOLID, Lod.LOD2); Geometry g = getSolidGeometry(building);
assertNotNull("Expected SOLID LOD2 geometry in test model", g); assertNotNull("Expected SOLID geometry in test model", g);
List<Polygon> polys = g.getPolygons(); List<Polygon> polys = g.getPolygons();
assertNotNull(polys); assertNotNull(polys);
...@@ -78,4 +78,12 @@ public class SolidSelfIntersectionBVHUtilTest { ...@@ -78,4 +78,12 @@ public class SolidSelfIntersectionBVHUtilTest {
assertTrue("No self-intersections expected for SolidSelfIntTest1.gml", assertTrue("No self-intersections expected for SolidSelfIntTest1.gml",
intersections.isEmpty()); intersections.isEmpty());
} }
private static Geometry getSolidGeometry(Building building) {
Geometry lod2 = building.getGeometry(GeometryType.SOLID, Lod.LOD2);
if (lod2 != null) {
return lod2;
}
return building.getGeometry(GeometryType.SOLID, Lod.LOD1);
}
} }
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
@Tag("performance")
public class SolidSelfIntersectionBvhPerformanceTest {
private static final double DELTA = 0.001;
@Test
public void compareBruteForceAndBvhVariantsOnSyntheticSolids() {
measureScenario("ssi-separated-grid", SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 12, 12));
measureScenario("ssi-overlapping-grid", SyntheticSolidGeometryFactory.overlappingBoxGrid(Lod.LOD2, 12, 12));
measureScenario("ssi-dense-clusters", SyntheticSolidGeometryFactory.denseBoxClusters(Lod.LOD2, 8, 20));
measureScenario("ssi-long-thin-slabs", SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 160));
measureScenario("ssi-flat-box-grid", SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 200));
measureScenario("ssi-citylike-mixed", SyntheticCityGmlLikeGeometryFactory.mixedUrbanDistrict(Lod.LOD2, 5, 4));
measureScenario("ssi-citylike-courtyard", SyntheticCityGmlLikeGeometryFactory.courtyardDistrict(Lod.LOD2, 16));
measureScenario("ssi-citylike-roofs", SyntheticCityGmlLikeGeometryFactory.variedRoofDistrict(Lod.LOD2, 180));
}
private static void measureScenario(String scenario, Geometry geometry) {
int polygonCount = geometry.getPolygons().size();
BvhInputMetricsCollector.print(BvhInputMetricsCollector.forPolygons(scenario, geometry.getPolygons()));
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
BvhPerformanceTestSupport.Measurement bruteForce = BvhPerformanceTestSupport.measure(
scenario,
"BRUTE_FORCE",
polygonCount,
() -> SelfIntersectionUtil.calculateSolidSelfIntersection0(geometry, DELTA).size());
measurements.add(bruteForce);
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
BvhPerformanceTestSupport.Measurement bvhMeasurement = BvhPerformanceTestSupport.measure(
scenario,
strategy.name(),
polygonCount,
() -> calculateWithTree(geometry, strategy));
measurements.add(bvhMeasurement);
assertEquals("SSI result count differs for " + scenario + " / " + strategy,
bruteForce.resultCount, bvhMeasurement.resultCount);
}
BvhPerformanceTestSupport.printScenarioSummary(scenario, measurements);
}
private static int calculateWithTree(Geometry geometry, SplitStrategy strategy) {
BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newWithStrategy(
geometry.getPolygons(),
polygon -> AABB.of(polygon.getOriginal()),
strategy);
return SelfIntersectionUtil.calculateSolidSelfIntersection(geometry, DELTA, tree).size();
}
}
...@@ -90,10 +90,6 @@ public class SolidSelfIntersectionOldVsNewTest { ...@@ -90,10 +90,6 @@ public class SolidSelfIntersectionOldVsNewTest {
System.out.println("oldTreeRes.size() = " + oldTreeRes.size()); System.out.println("oldTreeRes.size() = " + oldTreeRes.size());
System.out.println("newRes.size() = " + newRes.size()); System.out.println("newRes.size() = " + newRes.size());
assertFalse("oldRes is empty for: " + gmlPath, oldRes.isEmpty());
assertFalse("oldTreeRes is empty for: " + gmlPath, oldTreeRes.isEmpty());
assertFalse("newRes is empty for: " + gmlPath, newRes.isEmpty());
assertEquals("Old vs oldTree differs for: " + gmlPath, assertEquals("Old vs oldTree differs for: " + gmlPath,
oldRes.size(), oldTreeRes.size()); oldRes.size(), oldTreeRes.size());
......
package de.hft.stuttgart.citydoctor2.checks.bht;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
/**
* Synthetic but citygml-like geometry fixtures for BVH metric and performance
* experiments.
*
* The goal is not to serialize valid CityGML, but to mimic typical model
* structure more closely than pure grids: mixed building footprints, varying
* heights, roof-like tilted faces, block spacing, and courtyard arrangements.
*
* @author Numanoglu
*/
final class SyntheticCityGmlLikeGeometryFactory {
private SyntheticCityGmlLikeGeometryFactory() {
}
/**
* Creates several blocks with mixed compact, elongated, and roof-like buildings.
* Useful as a balanced city-district fixture with moderate spatial separation.
*/
static Geometry mixedUrbanDistrict(Lod lod, int blockColumns, int blockRows) {
Geometry geometry = newSolid(lod);
for (int blockX = 0; blockX < blockColumns; blockX++) {
for (int blockY = 0; blockY < blockRows; blockY++) {
double originX = blockX * 55.0;
double originY = blockY * 45.0;
addMixedBlock(geometry, originX, originY, blockX + blockY * blockColumns);
}
}
geometry.updateEdgesAndVertices();
return geometry;
}
/**
* Creates block-perimeter buildings around open courtyards. This produces many
* nearby but not necessarily intersecting AABBs, closer to dense urban blocks.
*/
static Geometry courtyardDistrict(Lod lod, int blockCount) {
Geometry geometry = newSolid(lod);
for (int block = 0; block < blockCount; block++) {
double originX = (block % 4) * 62.0;
double originY = (block / 4) * 58.0;
double height = 9.0 + (block % 3) * 2.5;
addBox(geometry, originX, originY, 0.0, 44.0, 7.0, height);
addBox(geometry, originX, originY + 30.0, 0.0, 44.0, 7.0, height + 1.5);
addBox(geometry, originX, originY + 7.0, 0.0, 7.0, 23.0, height - 1.0);
addBox(geometry, originX + 37.0, originY + 7.0, 0.0, 7.0, 23.0, height + 0.5);
}
geometry.updateEdgesAndVertices();
return geometry;
}
/**
* Creates many small buildings with alternating flat and gabled roof shapes.
* Useful for a larger polygon count with city-like height and footprint jitter.
*/
static Geometry variedRoofDistrict(Lod lod, int buildingCount) {
Geometry geometry = newSolid(lod);
for (int i = 0; i < buildingCount; i++) {
double x = (i % 12) * 13.0 + ((i / 12) % 2) * 4.0;
double y = (i / 12) * 11.0;
double width = 7.0 + (i % 4) * 1.3;
double depth = 6.0 + (i % 5) * 0.9;
double height = 5.0 + (i % 7) * 1.2;
if ((i & 1) == 0) {
addGabledBuilding(geometry, x, y, 0.0, width, depth, height, 2.2);
} else {
addBox(geometry, x, y, 0.0, width, depth, height);
}
}
geometry.updateEdgesAndVertices();
return geometry;
}
private static void addMixedBlock(Geometry geometry, double originX, double originY, int seed) {
addBox(geometry, originX, originY, 0.0, 9.0 + seed % 3, 11.0, 8.0 + seed % 4);
addGabledBuilding(geometry, originX + 13.0, originY + 2.0, 0.0, 12.0, 8.0, 7.0 + seed % 5, 2.5);
addBox(geometry, originX + 30.0, originY, 0.0, 7.0, 18.0, 10.0 + seed % 6);
addGabledBuilding(geometry, originX + 4.0, originY + 24.0, 0.0, 18.0, 9.0, 6.0 + seed % 4, 2.0);
addBox(geometry, originX + 28.0, originY + 24.0, 0.0, 14.0, 12.0, 5.0 + seed % 5);
}
private static Geometry newSolid(Lod lod) {
return new Geometry(GeometryType.SOLID, lod, Orientation.OUTWARD);
}
private static void addGabledBuilding(
Geometry geometry,
double x,
double y,
double z,
double width,
double depth,
double wallHeight,
double roofHeight) {
Vertex v000 = new Vertex(x, y, z);
Vertex v100 = new Vertex(x + width, y, z);
Vertex v110 = new Vertex(x + width, y + depth, z);
Vertex v010 = new Vertex(x, y + depth, z);
Vertex v001 = new Vertex(x, y, z + wallHeight);
Vertex v101 = new Vertex(x + width, y, z + wallHeight);
Vertex v111 = new Vertex(x + width, y + depth, z + wallHeight);
Vertex v011 = new Vertex(x, y + depth, z + wallHeight);
Vertex ridge0 = new Vertex(x + width * 0.5, y, z + wallHeight + roofHeight);
Vertex ridge1 = new Vertex(x + width * 0.5, y + depth, z + wallHeight + roofHeight);
addQuad(geometry, v000, v100, v110, v010);
addQuad(geometry, v000, v001, v101, v100);
addQuad(geometry, v100, v101, v111, v110);
addQuad(geometry, v110, v111, v011, v010);
addQuad(geometry, v010, v011, v001, v000);
addQuad(geometry, v001, ridge0, ridge1, v011);
addQuad(geometry, ridge0, v101, v111, ridge1);
addTriangle(geometry, v001, v101, ridge0);
addTriangle(geometry, v011, ridge1, v111);
}
private static void addBox(
Geometry geometry,
double x,
double y,
double z,
double width,
double depth,
double height) {
Vertex v000 = new Vertex(x, y, z);
Vertex v100 = new Vertex(x + width, y, z);
Vertex v110 = new Vertex(x + width, y + depth, z);
Vertex v010 = new Vertex(x, y + depth, z);
Vertex v001 = new Vertex(x, y, z + height);
Vertex v101 = new Vertex(x + width, y, z + height);
Vertex v111 = new Vertex(x + width, y + depth, z + height);
Vertex v011 = new Vertex(x, y + depth, z + height);
addQuad(geometry, v000, v100, v110, v010);
addQuad(geometry, v001, v011, v111, v101);
addQuad(geometry, v000, v001, v101, v100);
addQuad(geometry, v100, v101, v111, v110);
addQuad(geometry, v110, v111, v011, v010);
addQuad(geometry, v010, v011, v001, v000);
}
private static void addQuad(Geometry geometry, Vertex a, Vertex b, Vertex c, Vertex d) {
ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
polygon.setExteriorRing(ring);
geometry.addPolygon(polygon);
ring.addVertex(a);
ring.addVertex(b);
ring.addVertex(c);
ring.addVertex(d);
ring.addVertex(a);
}
private static void addTriangle(Geometry geometry, Vertex a, Vertex b, Vertex c) {
ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
polygon.setExteriorRing(ring);
geometry.addPolygon(polygon);
ring.addVertex(a);
ring.addVertex(b);
ring.addVertex(c);
ring.addVertex(a);
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
/**
* Test-fixture factory for RingSelfIntCheck BVH comparison tests.
*
* Scenarios:
* - rectangle:
* simple valid reference ring.
* - bowTie:
* direct segment crossing and therefore a clear self-intersection.
* - pointNearEdge:
* vertex close to a non-adjacent edge, useful for epsilon-sensitive checks.
* - largeConvexRing:
* many edges without self-intersection, useful for candidate-pruning cost.
* - zigZagCorridor:
* many nearby but non-crossing segments, useful as AABB broad-phase stress.
*
* @author Numanoglu
*/
final class SyntheticRingGeometryFactory {
private SyntheticRingGeometryFactory() {
}
static Geometry correctnessRingGeometry(double epsilon) {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
addRingPolygon(geometry, rectangle());
addRingPolygon(geometry, bowTie());
addRingPolygon(geometry, pointNearEdge(epsilon));
addRingPolygon(geometry, largeConvexRing(80, 20.0, 40.0, 0.0));
addRingPolygon(geometry, zigZagCorridor(30));
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry performanceRingGeometry(int convexVertexCount, int zigZagSegments, double epsilon) {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
addRingPolygon(geometry, largeConvexRing(convexVertexCount, 0.0, 0.0, 0.0));
addRingPolygon(geometry, zigZagCorridor(zigZagSegments));
addRingPolygon(geometry, pointNearEdge(epsilon));
addRingPolygon(geometry, bowTie());
geometry.updateEdgesAndVertices();
return geometry;
}
private static void addRingPolygon(Geometry geometry, double[][] coordinates) {
ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
polygon.setExteriorRing(ring);
geometry.addPolygon(polygon);
Vertex firstVertex = null;
for (int i = 0; i < coordinates.length; i++) {
double[] coordinate = coordinates[i];
if (i == coordinates.length - 1 && sameCoordinate(coordinate, coordinates[0])) {
ring.addVertex(firstVertex);
continue;
}
Vertex vertex = new Vertex(coordinate[0], coordinate[1], coordinate[2]);
if (i == 0) {
firstVertex = vertex;
}
ring.addVertex(vertex);
}
}
private static boolean sameCoordinate(double[] a, double[] b) {
return Double.compare(a[0], b[0]) == 0
&& Double.compare(a[1], b[1]) == 0
&& Double.compare(a[2], b[2]) == 0;
}
private static double[][] rectangle() {
return new double[][] {
{0.0, 0.0, 0.0},
{10.0, 0.0, 0.0},
{10.0, 10.0, 0.0},
{0.0, 10.0, 0.0},
{0.0, 0.0, 0.0}
};
}
private static double[][] bowTie() {
return new double[][] {
{20.0, 0.0, 0.0},
{30.0, 10.0, 0.0},
{30.0, 0.0, 0.0},
{20.0, 10.0, 0.0},
{20.0, 0.0, 0.0}
};
}
private static double[][] pointNearEdge(double epsilon) {
return new double[][] {
{40.0, 0.0, 0.0},
{50.0, 0.0, 0.0},
{50.0, 10.0, 0.0},
{45.0, epsilon * 0.5, 0.0},
{40.0, 10.0, 0.0},
{40.0, 0.0, 0.0}
};
}
private static double[][] largeConvexRing(int vertexCount, double centerX, double centerY, double z) {
double[][] coordinates = new double[vertexCount + 1][3];
for (int i = 0; i < vertexCount; i++) {
double angle = 2.0 * Math.PI * i / vertexCount;
coordinates[i][0] = centerX + Math.cos(angle) * 8.0;
coordinates[i][1] = centerY + Math.sin(angle) * 5.0;
coordinates[i][2] = z;
}
coordinates[vertexCount][0] = coordinates[0][0];
coordinates[vertexCount][1] = coordinates[0][1];
coordinates[vertexCount][2] = coordinates[0][2];
return coordinates;
}
private static double[][] zigZagCorridor(int segments) {
double[][] coordinates = new double[(segments * 2) + 3][3];
int index = 0;
for (int i = 0; i <= segments; i++) {
coordinates[index++] = new double[] {60.0 + i, i % 2 == 0 ? 0.0 : 1.0, 0.0};
}
for (int i = segments; i >= 0; i--) {
coordinates[index++] = new double[] {60.0 + i, i % 2 == 0 ? 4.0 : 5.0, 0.0};
}
coordinates[index] = new double[] {60.0, 0.0, 0.0};
return coordinates;
}
}
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