Commit 2c9eea60 authored by Numanoglu's avatar Numanoglu
Browse files

Refine AABB BVH heuristic exploration

parent 35a777b9
package de.hft.stuttgart.citydoctor2.checks.bht; package de.hft.stuttgart.citydoctor2.checks.aabb.performance;
import de.hft.stuttgart.citydoctor2.checks.aabb.fixtures.SyntheticRingGeometryFactory;
import de.hft.stuttgart.citydoctor2.checks.aabb.support.BvhInputMetricsCollector;
import de.hft.stuttgart.citydoctor2.checks.aabb.support.BvhPerformanceTestSupport;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -15,8 +19,16 @@ import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck; ...@@ -15,8 +19,16 @@ import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
/**
* Manual performance probe for ring self-intersection checks.
*
* The BVH indexes padded edge boxes. The exact ring-intersection logic remains
* in RingSelfIntCheck; this test only compares how cheaply candidates are found.
*
* @author Numanoglu
*/
@Tag("performance") @Tag("performance")
public class RingSelfIntCheckBvhPerformanceTest { public class RingSelfIntCheckBvhPerformanceTest {
...@@ -30,6 +42,7 @@ public class RingSelfIntCheckBvhPerformanceTest { ...@@ -30,6 +42,7 @@ public class RingSelfIntCheckBvhPerformanceTest {
} }
private static void measureScenario(String scenario, Geometry geometry) { private static void measureScenario(String scenario, Geometry geometry) {
// Edge count is the natural input size because every segment can become a BVH element.
int edgeCount = countEdges(geometry); int edgeCount = countEdges(geometry);
BvhInputMetricsCollector.print(BvhInputMetricsCollector.forRings(scenario, collectExteriorRings(geometry))); BvhInputMetricsCollector.print(BvhInputMetricsCollector.forRings(scenario, collectExteriorRings(geometry)));
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>(); List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
...@@ -55,6 +68,7 @@ public class RingSelfIntCheckBvhPerformanceTest { ...@@ -55,6 +68,7 @@ public class RingSelfIntCheckBvhPerformanceTest {
} }
private static int runCheck(Geometry geometry, RingSelfIntCheck.Variant variant) { private static int runCheck(Geometry geometry, RingSelfIntCheck.Variant variant) {
// Run each polygon ring independently, as the check normally receives one ring at a time.
int errorCount = 0; int errorCount = 0;
for (Polygon polygon : geometry.getPolygons()) { for (Polygon polygon : geometry.getPolygons()) {
RingSelfIntCheck check = new RingSelfIntCheck(variant); RingSelfIntCheck check = new RingSelfIntCheck(variant);
...@@ -87,6 +101,7 @@ public class RingSelfIntCheckBvhPerformanceTest { ...@@ -87,6 +101,7 @@ public class RingSelfIntCheckBvhPerformanceTest {
} }
private static RingSelfIntCheck.Variant variantFor(SplitStrategy strategy) { private static RingSelfIntCheck.Variant variantFor(SplitStrategy strategy) {
// Bridge the shared BVH strategy enum to the check-specific mode enum.
switch (strategy) { switch (strategy) {
case BINARY_OBJECT_MEDIAN: case BINARY_OBJECT_MEDIAN:
return RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEDIAN; return RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEDIAN;
......
package de.hft.stuttgart.citydoctor2.checks.bht; package de.hft.stuttgart.citydoctor2.checks.aabb.performance;
import de.hft.stuttgart.citydoctor2.checks.aabb.fixtures.SyntheticCityGmlLikeGeometryFactory;
import de.hft.stuttgart.citydoctor2.checks.aabb.fixtures.SyntheticSolidGeometryFactory;
import de.hft.stuttgart.citydoctor2.checks.aabb.support.BvhInputMetricsCollector;
import de.hft.stuttgart.citydoctor2.checks.aabb.support.BvhPerformanceTestSupport;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -12,10 +17,18 @@ import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil; ...@@ -12,10 +17,18 @@ import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Lod; 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.bht.AABB; import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree; import de.hft.stuttgart.citydoctor2.datastructure.aabb.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
/**
* Manual performance probe for solid self-intersection candidate search.
*
* It compares the brute-force reference with all six BVH split strategies on
* synthetic solids whose polygon distributions are deliberately different.
*
* @author Numanoglu
*/
@Tag("performance") @Tag("performance")
public class SolidSelfIntersectionBvhPerformanceTest { public class SolidSelfIntersectionBvhPerformanceTest {
...@@ -34,6 +47,7 @@ public class SolidSelfIntersectionBvhPerformanceTest { ...@@ -34,6 +47,7 @@ public class SolidSelfIntersectionBvhPerformanceTest {
} }
private static void measureScenario(String scenario, Geometry geometry) { private static void measureScenario(String scenario, Geometry geometry) {
// Result counts must match the brute-force run; only the broad-phase cost should change.
int polygonCount = geometry.getPolygons().size(); int polygonCount = geometry.getPolygons().size();
BvhInputMetricsCollector.print(BvhInputMetricsCollector.forPolygons(scenario, geometry.getPolygons())); BvhInputMetricsCollector.print(BvhInputMetricsCollector.forPolygons(scenario, geometry.getPolygons()));
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>(); List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
...@@ -58,6 +72,7 @@ public class SolidSelfIntersectionBvhPerformanceTest { ...@@ -58,6 +72,7 @@ public class SolidSelfIntersectionBvhPerformanceTest {
} }
private static int calculateWithTree(Geometry geometry, SplitStrategy strategy) { private static int calculateWithTree(Geometry geometry, SplitStrategy strategy) {
// Build a fresh tree per measurement so construction cost stays part of the comparison.
BoundingVolumeHierarchyTree<Polygon> tree = BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newWithStrategy( BoundingVolumeHierarchyTree.newWithStrategy(
geometry.getPolygons(), geometry.getPolygons(),
......
package de.hft.stuttgart.citydoctor2.checks.aabb.support;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
/**
* Writes structured BVH exploration data for reproducible downstream analysis.
*
* Console tables are useful while running the tests, but the CSV exports are
* the stable handoff to spreadsheet tools, pandas, and the written evaluation.
*
* @author Numanoglu
*/
public final class BvhExplorationCsvWriter {
public static final String CANDIDATE_POLICY_VERSION = "candidate-rules-v1";
private static final List<String> OBSERVATION_HEADER = List.of(
"source",
"dataSplit",
"dataset",
"datasetLabel",
"check",
"scenario",
"n",
"thin",
"relVol",
"spread",
"aspect",
"currentPolicy",
"currentPolicyMs",
"currentPolicyRegretRatio",
"candidatePolicy",
"winnerBvh",
"match",
"baselineVariant",
"baselineMs",
"bestBvhMs",
"candidatePolicyMs",
"regretMs",
"regretRatio",
"slowdownFactor",
"resultCount",
"policyVersion");
private static final List<String> BUCKET_HEADER = List.of(
"check",
"metric",
"bucket",
"cases",
"binaryObjectMedianWins",
"binaryObjectMeanWins",
"binarySpatialMedianWins",
"octonaryObjectMedianWins",
"octonaryObjectMeanWins",
"octonarySpatialMedianWins",
"mostCommonBvh",
"winnerCount",
"winnerShare",
"secondBestCount",
"winnerMargin",
"candidateRule");
private BvhExplorationCsvWriter() {
}
public static void writeObservations(Path outputFile, List<ObservationRecord> records) throws IOException {
// One row corresponds to one measured check/scenario combination.
List<List<String>> rows = new ArrayList<>(records.size());
for (ObservationRecord record : records) {
BvhHeuristicTimingSupport.Observation observation = record.observation;
BvhInputMetricsCollector.Metrics metrics = observation.metrics;
rows.add(List.of(
record.source,
record.dataSplit,
record.dataset,
record.datasetLabel,
observation.checkName,
metrics.scenario,
Integer.toString(metrics.elementCount),
decimal(metrics.thinBoxRate),
decimal(metrics.averageRelativeBoxVolume),
decimal(metrics.centerSpreadRatio),
decimal(metrics.averageAspectRatio),
observation.currentPolicyPrediction,
measurementMillis(observation.currentPolicyMeasurement),
decimalOrEmpty(observation.currentPolicyRegretRatio()),
observation.candidateRulePrediction,
observation.fastestBvh.variant,
observation.candidateRuleMatchLabel(),
observation.baseline == null ? "" : observation.baseline.variant,
measurementMillis(observation.baseline),
measurementMillis(observation.fastestBvh),
measurementMillis(observation.candidateRuleMeasurement),
decimalOrEmpty(observation.candidateRegretMillis()),
decimalOrEmpty(observation.candidateRegretRatio()),
decimalOrEmpty(observation.candidateSlowdownFactor()),
Integer.toString(observation.fastestBvh.resultCount),
record.policyVersion));
}
write(outputFile, OBSERVATION_HEADER, rows);
}
public static void writeBucketSummary(Path outputFile, List<BucketSummaryRecord> records) throws IOException {
// Bucket rows summarize how often each split strategy wins inside one metric interval.
List<List<String>> rows = new ArrayList<>(records.size());
for (BucketSummaryRecord record : records) {
rows.add(List.of(
record.checkName,
record.metricName,
record.bucketName,
Integer.toString(record.cases),
Integer.toString(record.winCount(SplitStrategy.BINARY_OBJECT_MEDIAN)),
Integer.toString(record.winCount(SplitStrategy.BINARY_OBJECT_MEAN)),
Integer.toString(record.winCount(SplitStrategy.BINARY_SPATIAL_MEDIAN)),
Integer.toString(record.winCount(SplitStrategy.OCTONARY_OBJECT_MEDIAN)),
Integer.toString(record.winCount(SplitStrategy.OCTONARY_OBJECT_MEAN)),
Integer.toString(record.winCount(SplitStrategy.OCTONARY_SPATIAL_MEDIAN)),
record.mostCommonBvh,
Integer.toString(record.winnerCount),
decimal(record.winnerShare),
Integer.toString(record.secondBestCount),
decimal(record.winnerMargin),
Boolean.toString(record.candidateRule)));
}
write(outputFile, BUCKET_HEADER, rows);
}
private static void write(Path outputFile, List<String> header, List<List<String>> rows) throws IOException {
Files.createDirectories(outputFile.getParent());
try (BufferedWriter writer = Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8)) {
writeRow(writer, header);
for (List<String> row : rows) {
writeRow(writer, row);
}
}
}
private static void writeRow(BufferedWriter writer, List<String> values) throws IOException {
for (int i = 0; i < values.size(); i++) {
if (i > 0) {
writer.write(',');
}
writer.write(escape(values.get(i)));
}
writer.newLine();
}
private static String escape(String value) {
if (value.indexOf(',') < 0 && value.indexOf('"') < 0 && value.indexOf('\n') < 0) {
return value;
}
return '"' + value.replace("\"", "\"\"") + '"';
}
private static String measurementMillis(BvhPerformanceTestSupport.Measurement measurement) {
return measurement == null ? "" : decimal(measurement.averageMillis());
}
private static String decimalOrEmpty(double value) {
return Double.isFinite(value) ? decimal(value) : "";
}
private static String decimal(double value) {
return String.format(Locale.ROOT, "%.9f", value);
}
public static final class ObservationRecord {
public final String source;
public final String dataSplit;
public final String dataset;
public final String datasetLabel;
public final String policyVersion;
public final BvhHeuristicTimingSupport.Observation observation;
/**
* Adds dataset labels around a measured observation before it is written.
*/
public ObservationRecord(
String source,
String dataSplit,
String dataset,
String datasetLabel,
String policyVersion,
BvhHeuristicTimingSupport.Observation observation) {
this.source = source;
this.dataSplit = dataSplit;
this.dataset = dataset;
this.datasetLabel = datasetLabel;
this.policyVersion = policyVersion;
this.observation = observation;
}
}
public static final class BucketSummaryRecord {
public final String checkName;
public final String metricName;
public final String bucketName;
public final int cases;
public final java.util.Map<String, Integer> winnerCounts;
public final String mostCommonBvh;
public final int winnerCount;
public final double winnerShare;
public final int secondBestCount;
public final double winnerMargin;
public final boolean candidateRule;
/**
* Stores the winner distribution for one check, metric, and bucket.
*/
public BucketSummaryRecord(
String checkName,
String metricName,
String bucketName,
int cases,
java.util.Map<String, Integer> winnerCounts,
String mostCommonBvh,
int winnerCount,
double winnerShare,
int secondBestCount,
double winnerMargin,
boolean candidateRule) {
this.checkName = checkName;
this.metricName = metricName;
this.bucketName = bucketName;
this.cases = cases;
this.winnerCounts = java.util.Map.copyOf(winnerCounts);
this.mostCommonBvh = mostCommonBvh;
this.winnerCount = winnerCount;
this.winnerShare = winnerShare;
this.secondBestCount = secondBestCount;
this.winnerMargin = winnerMargin;
this.candidateRule = candidateRule;
}
private int winCount(SplitStrategy strategy) {
return winnerCounts.getOrDefault(strategy.name(), 0);
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht; package de.hft.stuttgart.citydoctor2.checks.aabb.support;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
...@@ -8,8 +8,18 @@ import java.util.List; ...@@ -8,8 +8,18 @@ import java.util.List;
import de.hft.stuttgart.citydoctor2.checks.geometry.NestedRingsCheck; import de.hft.stuttgart.citydoctor2.checks.geometry.NestedRingsCheck;
import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck; import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy; import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
/**
* Centralizes timing and policy comparison code for the BVH heuristic tests.
*
* The class keeps the exploratory tests small: they provide the geometry and
* the operation to run, while this support class measures all split strategies,
* checks that result counts stay stable, and records both the current production
* policy and the candidate rules derived from the synthetic study.
*
* @author Numanoglu
*/
public final class BvhHeuristicTimingSupport { public final class BvhHeuristicTimingSupport {
private BvhHeuristicTimingSupport() { private BvhHeuristicTimingSupport() {
...@@ -23,6 +33,7 @@ public final class BvhHeuristicTimingSupport { ...@@ -23,6 +33,7 @@ public final class BvhHeuristicTimingSupport {
String baselineName, String baselineName,
MeasuredOperation baselineOperation, MeasuredOperation baselineOperation,
StrategyOperation strategyOperation) { StrategyOperation strategyOperation) {
// Full checks compare each BVH variant against the old or brute-force implementation.
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>(); List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
BvhPerformanceTestSupport.Measurement baseline = BvhPerformanceTestSupport.measure( BvhPerformanceTestSupport.Measurement baseline = BvhPerformanceTestSupport.measure(
scenarioName, scenarioName,
...@@ -42,17 +53,18 @@ public final class BvhHeuristicTimingSupport { ...@@ -42,17 +53,18 @@ public final class BvhHeuristicTimingSupport {
baseline.resultCount, bvhMeasurement.resultCount); baseline.resultCount, bvhMeasurement.resultCount);
} }
String syntheticPrediction = predictFromSyntheticPolicy(checkName, metrics); String currentPolicyPrediction = predictFromCurrentPolicy(checkName, metrics);
String candidatePrediction = predictCandidateStrategy(checkName, metrics); String candidateRulePrediction = predictFromCandidateRules(checkName, metrics);
return new Observation( return new Observation(
checkName, checkName,
metrics, metrics,
fastest(measurements), fastest(measurements),
fastestBvh(measurements), fastestBvh(measurements),
baseline, baseline,
syntheticPrediction, currentPolicyPrediction,
candidatePrediction, candidateRulePrediction,
measurementForPrediction(measurements, candidatePrediction)); measurementForPrediction(measurements, currentPolicyPrediction),
measurementForPrediction(measurements, candidateRulePrediction));
} }
public static Observation measureBvhStrategies( public static Observation measureBvhStrategies(
...@@ -61,6 +73,7 @@ public final class BvhHeuristicTimingSupport { ...@@ -61,6 +73,7 @@ public final class BvhHeuristicTimingSupport {
BvhInputMetricsCollector.Metrics metrics, BvhInputMetricsCollector.Metrics metrics,
int inputSize, int inputSize,
StrategyOperation strategyOperation) { StrategyOperation strategyOperation) {
// Real CityGML runs measure only broad-phase candidate search, so the first BVH variant is the count baseline.
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>(); List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
BvhPerformanceTestSupport.Measurement baseline = null; BvhPerformanceTestSupport.Measurement baseline = null;
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) { for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
...@@ -78,20 +91,22 @@ public final class BvhHeuristicTimingSupport { ...@@ -78,20 +91,22 @@ public final class BvhHeuristicTimingSupport {
measurements.add(bvhMeasurement); measurements.add(bvhMeasurement);
} }
String syntheticPrediction = predictFromSyntheticPolicy(checkName, metrics); String currentPolicyPrediction = predictFromCurrentPolicy(checkName, metrics);
String candidatePrediction = predictCandidateStrategy(checkName, metrics); String candidateRulePrediction = predictFromCandidateRules(checkName, metrics);
return new Observation( return new Observation(
checkName, checkName,
metrics, metrics,
fastest(measurements), fastest(measurements),
fastestBvh(measurements), fastestBvh(measurements),
baseline, baseline,
syntheticPrediction, currentPolicyPrediction,
candidatePrediction, candidateRulePrediction,
measurementForPrediction(measurements, candidatePrediction)); measurementForPrediction(measurements, currentPolicyPrediction),
measurementForPrediction(measurements, candidateRulePrediction));
} }
public static String predictFromSyntheticPolicy(String checkName, BvhInputMetricsCollector.Metrics metrics) { public static String predictFromCurrentPolicy(String checkName, BvhInputMetricsCollector.Metrics metrics) {
// This is the currently implemented production policy, not a learned rule.
BvhUsagePolicy.BvhCheckType checkType = checkTypeFor(checkName); BvhUsagePolicy.BvhCheckType checkType = checkTypeFor(checkName);
if (!BvhUsagePolicy.shouldUseTree(checkType, metrics.summary)) { if (!BvhUsagePolicy.shouldUseTree(checkType, metrics.summary)) {
return "OLD"; return "OLD";
...@@ -99,7 +114,8 @@ public final class BvhHeuristicTimingSupport { ...@@ -99,7 +114,8 @@ public final class BvhHeuristicTimingSupport {
return BvhUsagePolicy.chooseSplitStrategy(checkType, metrics.summary).name(); return BvhUsagePolicy.chooseSplitStrategy(checkType, metrics.summary).name();
} }
public static String predictCandidateStrategy(String checkName, BvhInputMetricsCollector.Metrics metrics) { public static String predictFromCandidateRules(String checkName, BvhInputMetricsCollector.Metrics metrics) {
// Candidate rules are intentionally explicit so they can be reviewed before being moved into production code.
BvhUsagePolicy.BvhCheckType checkType = checkTypeFor(checkName); BvhUsagePolicy.BvhCheckType checkType = checkTypeFor(checkName);
if (!BvhUsagePolicy.shouldUseTree(checkType, metrics.summary)) { if (!BvhUsagePolicy.shouldUseTree(checkType, metrics.summary)) {
return "OLD"; return "OLD";
...@@ -129,6 +145,7 @@ public final class BvhHeuristicTimingSupport { ...@@ -129,6 +145,7 @@ public final class BvhHeuristicTimingSupport {
} }
public static NestedRingsCheck.Variant nestedVariantFor(SplitStrategy strategy) { public static NestedRingsCheck.Variant nestedVariantFor(SplitStrategy strategy) {
// NestedRingsCheck exposes variants as check modes, while the BVH builder uses SplitStrategy.
switch (strategy) { switch (strategy) {
case BINARY_OBJECT_MEDIAN: case BINARY_OBJECT_MEDIAN:
return NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEDIAN; return NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEDIAN;
...@@ -149,6 +166,7 @@ public final class BvhHeuristicTimingSupport { ...@@ -149,6 +166,7 @@ public final class BvhHeuristicTimingSupport {
} }
public static RingSelfIntCheck.Variant rsiVariantFor(SplitStrategy strategy) { public static RingSelfIntCheck.Variant rsiVariantFor(SplitStrategy strategy) {
// RingSelfIntCheck has the same six BVH modes but owns its own enum.
switch (strategy) { switch (strategy) {
case BINARY_OBJECT_MEDIAN: case BINARY_OBJECT_MEDIAN:
return RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEDIAN; return RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEDIAN;
...@@ -212,7 +230,8 @@ public final class BvhHeuristicTimingSupport { ...@@ -212,7 +230,8 @@ public final class BvhHeuristicTimingSupport {
List<BvhPerformanceTestSupport.Measurement> measurements, List<BvhPerformanceTestSupport.Measurement> measurements,
String prediction) { String prediction) {
for (BvhPerformanceTestSupport.Measurement measurement : measurements) { for (BvhPerformanceTestSupport.Measurement measurement : measurements) {
if (measurement.variant.equals(prediction)) { if (measurement.variant.equals(prediction)
|| ("OLD".equals(prediction) && "BRUTE_FORCE".equals(measurement.variant))) {
return measurement; return measurement;
} }
} }
...@@ -223,19 +242,30 @@ public final class BvhHeuristicTimingSupport { ...@@ -223,19 +242,30 @@ public final class BvhHeuristicTimingSupport {
int run(); int run();
} }
/**
* Operation that can be executed with each concrete BVH split strategy.
*/
public interface StrategyOperation { public interface StrategyOperation {
int run(SplitStrategy strategy); int run(SplitStrategy strategy);
} }
/**
* One measured row in the heuristic tables.
*
* It stores the input metrics, the fastest measured BVH strategy, and the
* timings for the current and candidate policies so regret can be calculated
* without re-running the checks in the Python post-processing step.
*/
public static final class Observation { public static final class Observation {
public final String checkName; public final String checkName;
public final BvhInputMetricsCollector.Metrics metrics; public final BvhInputMetricsCollector.Metrics metrics;
public final BvhPerformanceTestSupport.Measurement fastest; public final BvhPerformanceTestSupport.Measurement fastest;
public final BvhPerformanceTestSupport.Measurement fastestBvh; public final BvhPerformanceTestSupport.Measurement fastestBvh;
public final BvhPerformanceTestSupport.Measurement baseline; public final BvhPerformanceTestSupport.Measurement baseline;
public final BvhPerformanceTestSupport.Measurement predictedBvh; public final BvhPerformanceTestSupport.Measurement currentPolicyMeasurement;
public final String syntheticPrediction; public final BvhPerformanceTestSupport.Measurement candidateRuleMeasurement;
public final String candidatePrediction; public final String currentPolicyPrediction;
public final String candidateRulePrediction;
Observation( Observation(
String checkName, String checkName,
...@@ -243,24 +273,56 @@ public final class BvhHeuristicTimingSupport { ...@@ -243,24 +273,56 @@ public final class BvhHeuristicTimingSupport {
BvhPerformanceTestSupport.Measurement fastest, BvhPerformanceTestSupport.Measurement fastest,
BvhPerformanceTestSupport.Measurement fastestBvh, BvhPerformanceTestSupport.Measurement fastestBvh,
BvhPerformanceTestSupport.Measurement baseline, BvhPerformanceTestSupport.Measurement baseline,
String syntheticPrediction, String currentPolicyPrediction,
String candidatePrediction, String candidateRulePrediction,
BvhPerformanceTestSupport.Measurement predictedBvh) { BvhPerformanceTestSupport.Measurement currentPolicyMeasurement,
BvhPerformanceTestSupport.Measurement candidateRuleMeasurement) {
this.checkName = checkName; this.checkName = checkName;
this.metrics = metrics; this.metrics = metrics;
this.fastest = fastest; this.fastest = fastest;
this.fastestBvh = fastestBvh; this.fastestBvh = fastestBvh;
this.baseline = baseline; this.baseline = baseline;
this.syntheticPrediction = syntheticPrediction; this.currentPolicyPrediction = currentPolicyPrediction;
this.candidatePrediction = candidatePrediction; this.candidateRulePrediction = candidateRulePrediction;
this.predictedBvh = predictedBvh; this.currentPolicyMeasurement = currentPolicyMeasurement;
this.candidateRuleMeasurement = candidateRuleMeasurement;
} }
public String candidatePredictionMatchLabel() { public String candidateRuleMatchLabel() {
if ("OLD".equals(candidatePrediction)) { if ("OLD".equals(candidateRulePrediction)) {
return "n/a"; return "n/a";
} }
return candidatePrediction.equals(fastestBvh.variant) ? "yes" : "no"; return candidateRulePrediction.equals(fastestBvh.variant) ? "yes" : "no";
}
public double candidateRegretMillis() {
if (candidateRuleMeasurement == null || fastestBvh == null) {
return Double.NaN;
}
return candidateRuleMeasurement.averageMillis() - fastestBvh.averageMillis();
}
public double candidateRegretRatio() {
if (candidateRuleMeasurement == null || fastestBvh == null || fastestBvh.averageNanos == 0L) {
return Double.NaN;
}
return (double) (candidateRuleMeasurement.averageNanos - fastestBvh.averageNanos)
/ fastestBvh.averageNanos;
}
public double candidateSlowdownFactor() {
if (candidateRuleMeasurement == null || fastestBvh == null || fastestBvh.averageNanos == 0L) {
return Double.NaN;
}
return (double) candidateRuleMeasurement.averageNanos / fastestBvh.averageNanos;
}
public double currentPolicyRegretRatio() {
if (currentPolicyMeasurement == null || fastestBvh == null || fastestBvh.averageNanos == 0L) {
return Double.NaN;
}
return (double) (currentPolicyMeasurement.averageNanos - fastestBvh.averageNanos)
/ fastestBvh.averageNanos;
} }
} }
} }
package de.hft.stuttgart.citydoctor2.checks.bht; package de.hft.stuttgart.citydoctor2.checks.aabb.support;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -7,7 +7,7 @@ import java.util.function.Function; ...@@ -7,7 +7,7 @@ import java.util.function.Function;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB; import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy; import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
/** /**
...@@ -19,7 +19,7 @@ import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy; ...@@ -19,7 +19,7 @@ import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
* *
* @author Numanoglu * @author Numanoglu
*/ */
final class BvhInputMetricsCollector { public final class BvhInputMetricsCollector {
private static final double DEGENERATE_TOLERANCE = 1e-12; private static final double DEGENERATE_TOLERANCE = 1e-12;
private static final long MAX_PAIR_SAMPLES = 20_000L; private static final long MAX_PAIR_SAMPLES = 20_000L;
...@@ -28,30 +28,31 @@ final class BvhInputMetricsCollector { ...@@ -28,30 +28,31 @@ final class BvhInputMetricsCollector {
private BvhInputMetricsCollector() { private BvhInputMetricsCollector() {
} }
static Metrics forPolygons(String scenario, List<? extends Polygon> polygons) { public static Metrics forPolygons(String scenario, List<? extends Polygon> polygons) {
return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal())); return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal()));
} }
static Metrics forPolygonsCheap(String scenario, List<? extends Polygon> polygons) { public static Metrics forPolygonsCheap(String scenario, List<? extends Polygon> polygons) {
// Used in heuristic runs where pair sampling would dominate the cost for large inputs.
return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal()), false); return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal()), false);
} }
static Metrics forRings(String scenario, List<? extends LinearRing> rings) { public static Metrics forRings(String scenario, List<? extends LinearRing> rings) {
return collect(scenario, rings, AABB::of); return collect(scenario, rings, AABB::of);
} }
static Metrics forRingsCheap(String scenario, List<? extends LinearRing> rings) { public static Metrics forRingsCheap(String scenario, List<? extends LinearRing> rings) {
return collect(scenario, rings, AABB::of, false); return collect(scenario, rings, AABB::of, false);
} }
/** /**
* Collects metrics from any element type that can be converted to an AABB. * Collects metrics from any element type that can be converted to an AABB.
*/ */
static <E> Metrics collect(String scenario, List<E> elements, Function<E, AABB> aabbFunction) { public static <E> Metrics collect(String scenario, List<E> elements, Function<E, AABB> aabbFunction) {
return collect(scenario, elements, aabbFunction, true); return collect(scenario, elements, aabbFunction, true);
} }
static <E> Metrics collect( public static <E> Metrics collect(
String scenario, String scenario,
List<E> elements, List<E> elements,
Function<E, AABB> aabbFunction, Function<E, AABB> aabbFunction,
...@@ -71,11 +72,12 @@ final class BvhInputMetricsCollector { ...@@ -71,11 +72,12 @@ final class BvhInputMetricsCollector {
* Collects metrics from precomputed AABBs. Pairwise overlap and containment * Collects metrics from precomputed AABBs. Pairwise overlap and containment
* are sampled when the full pairs set would be too large. * are sampled when the full pairs set would be too large.
*/ */
static Metrics collectBoxes(String scenario, List<AABB> boxes) { public static Metrics collectBoxes(String scenario, List<AABB> boxes) {
return collectBoxes(scenario, boxes, true); return collectBoxes(scenario, boxes, true);
} }
static Metrics collectBoxesCheap(String scenario, List<AABB> boxes) { public static Metrics collectBoxesCheap(String scenario, List<AABB> boxes) {
// Cheap mode keeps the metrics close to what an AUTO policy could compute during tree setup.
return collectBoxes(scenario, boxes, false); return collectBoxes(scenario, boxes, false);
} }
...@@ -160,7 +162,7 @@ final class BvhInputMetricsCollector { ...@@ -160,7 +162,7 @@ final class BvhInputMetricsCollector {
/** /**
* Emits one compact metrics line that can be read next to a performance table. * Emits one compact metrics line that can be read next to a performance table.
*/ */
static void print(Metrics metrics) { public static void print(Metrics metrics) {
System.out.println(metrics.toSummaryLine()); System.out.println(metrics.toSummaryLine());
} }
...@@ -195,6 +197,7 @@ final class BvhInputMetricsCollector { ...@@ -195,6 +197,7 @@ final class BvhInputMetricsCollector {
} }
private static PairSample samplePairs(List<AABB> boxes) { private static PairSample samplePairs(List<AABB> boxes) {
// Pair metrics are diagnostic only; they are capped so exploratory tests remain runnable.
long totalPairs = countPairs(boxes.size()); long totalPairs = countPairs(boxes.size());
long sampleEvery = Math.max(1L, totalPairs / MAX_PAIR_SAMPLES); long sampleEvery = Math.max(1L, totalPairs / MAX_PAIR_SAMPLES);
long pairIndex = 0L; long pairIndex = 0L;
...@@ -247,9 +250,9 @@ final class BvhInputMetricsCollector { ...@@ -247,9 +250,9 @@ final class BvhInputMetricsCollector {
} }
private static final class Aggregate { private static final class Aggregate {
final double dx; public final double dx;
final double dy; public final double dy;
final double dz; public final double dz;
Aggregate(double dx, double dy, double dz) { Aggregate(double dx, double dy, double dz) {
this.dx = dx; this.dx = dx;
...@@ -259,9 +262,9 @@ final class BvhInputMetricsCollector { ...@@ -259,9 +262,9 @@ final class BvhInputMetricsCollector {
} }
private static final class PairSample { private static final class PairSample {
final long sampledPairs; public final long sampledPairs;
final long overlappingPairs; public final long overlappingPairs;
final long containedPairs; public final long containedPairs;
PairSample(long sampledPairs, long overlappingPairs, long containedPairs) { PairSample(long sampledPairs, long overlappingPairs, long containedPairs) {
this.sampledPairs = sampledPairs; this.sampledPairs = sampledPairs;
...@@ -282,25 +285,25 @@ final class BvhInputMetricsCollector { ...@@ -282,25 +285,25 @@ final class BvhInputMetricsCollector {
} }
} }
static final class Metrics { public static final class Metrics {
final String scenario; public final String scenario;
final int elementCount; public final int elementCount;
final long totalPairs; public final long totalPairs;
final long sampledPairs; public final long sampledPairs;
final double sampledOverlapRate; public final double sampledOverlapRate;
final double sampledContainmentRate; public final double sampledContainmentRate;
final double degenerateRate; public final double degenerateRate;
final double thinBoxRate; public final double thinBoxRate;
final double averageAspectRatio; public final double averageAspectRatio;
final double averageRelativeBoxVolume; public final double averageRelativeBoxVolume;
final double averageExtentX; public final double averageExtentX;
final double averageExtentY; public final double averageExtentY;
final double averageExtentZ; public final double averageExtentZ;
final double normalizedCenterSpreadX; public final double normalizedCenterSpreadX;
final double normalizedCenterSpreadY; public final double normalizedCenterSpreadY;
final double normalizedCenterSpreadZ; public final double normalizedCenterSpreadZ;
final double centerSpreadRatio; public final double centerSpreadRatio;
final BvhUsagePolicy.BvhInputSummary summary; public final BvhUsagePolicy.BvhInputSummary summary;
private Metrics( private Metrics(
String scenario, String scenario,
......
package de.hft.stuttgart.citydoctor2.checks.bht; package de.hft.stuttgart.citydoctor2.checks.aabb.support;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintStream; import java.io.PrintStream;
...@@ -9,7 +9,7 @@ import org.apache.logging.log4j.Level; ...@@ -9,7 +9,7 @@ import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.config.Configurator; import org.apache.logging.log4j.core.config.Configurator;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
/** /**
* Shared support for manual BVH performance probes. * Shared support for manual BVH performance probes.
...@@ -21,17 +21,17 @@ import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy; ...@@ -21,17 +21,17 @@ import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
* *
* @author Numanoglu * @author Numanoglu
*/ */
final class BvhPerformanceTestSupport { public final class BvhPerformanceTestSupport {
static final int DEFAULT_WARMUP_RUNS = 2; public static final int DEFAULT_WARMUP_RUNS = 2;
static final int DEFAULT_MEASURE_RUNS = 5; public static final int DEFAULT_MEASURE_RUNS = 5;
private static final boolean SUPPRESS_MEASURED_STDOUT = true; private static final boolean SUPPRESS_MEASURED_STDOUT = true;
private static final PrintStream SILENT_OUT = new PrintStream(OutputStream.nullOutputStream()); private static final PrintStream SILENT_OUT = new PrintStream(OutputStream.nullOutputStream());
private BvhPerformanceTestSupport() { private BvhPerformanceTestSupport() {
} }
static SplitStrategy[] concreteStrategies() { public static SplitStrategy[] concreteStrategies() {
return new SplitStrategy[] { return new SplitStrategy[] {
SplitStrategy.BINARY_OBJECT_MEDIAN, SplitStrategy.BINARY_OBJECT_MEDIAN,
SplitStrategy.BINARY_OBJECT_MEAN, SplitStrategy.BINARY_OBJECT_MEAN,
...@@ -42,7 +42,7 @@ final class BvhPerformanceTestSupport { ...@@ -42,7 +42,7 @@ final class BvhPerformanceTestSupport {
}; };
} }
static Measurement measure(String scenario, String variant, int inputSize, Supplier<Integer> measuredOperation) { public static Measurement measure(String scenario, String variant, int inputSize, Supplier<Integer> measuredOperation) {
return measure( return measure(
scenario, scenario,
variant, variant,
...@@ -56,7 +56,7 @@ final class BvhPerformanceTestSupport { ...@@ -56,7 +56,7 @@ final class BvhPerformanceTestSupport {
* Measures one variant. The supplied operation must return a deterministic * Measures one variant. The supplied operation must return a deterministic
* result count so correctness can still be checked while timing is collected. * result count so correctness can still be checked while timing is collected.
*/ */
static Measurement measure( public static Measurement measure(
String scenario, String scenario,
String variant, String variant,
int inputSize, int inputSize,
...@@ -115,7 +115,7 @@ final class BvhPerformanceTestSupport { ...@@ -115,7 +115,7 @@ final class BvhPerformanceTestSupport {
* Prints a scenario-level table and highlights the fastest measured variant. * Prints a scenario-level table and highlights the fastest measured variant.
* The first measurement is treated as the baseline for the speedup column. * The first measurement is treated as the baseline for the speedup column.
*/ */
static void printScenarioSummary(String scenario, List<Measurement> measurements) { public static void printScenarioSummary(String scenario, List<Measurement> measurements) {
if (measurements.isEmpty()) { if (measurements.isEmpty()) {
return; return;
} }
...@@ -160,12 +160,12 @@ final class BvhPerformanceTestSupport { ...@@ -160,12 +160,12 @@ final class BvhPerformanceTestSupport {
return (double) baseline.averageNanos / measurement.averageNanos; return (double) baseline.averageNanos / measurement.averageNanos;
} }
static final class Measurement { public static final class Measurement {
final String scenario; public final String scenario;
final String variant; public final String variant;
final int inputSize; public final int inputSize;
final int resultCount; public final int resultCount;
final long averageNanos; public final long averageNanos;
Measurement(String scenario, String variant, int inputSize, int resultCount, long averageNanos) { Measurement(String scenario, String variant, int inputSize, int resultCount, long averageNanos) {
this.scenario = scenario; this.scenario = scenario;
...@@ -175,7 +175,7 @@ final class BvhPerformanceTestSupport { ...@@ -175,7 +175,7 @@ final class BvhPerformanceTestSupport {
this.averageNanos = averageNanos; this.averageNanos = averageNanos;
} }
double averageMillis() { public double averageMillis() {
return averageNanos / 1_000_000.0; return averageNanos / 1_000_000.0;
} }
} }
......
"""Create compact report tables from the BVH exploration CSV exports."""
from __future__ import annotations
import argparse
from pathlib import Path
import pandas as pd
OBSERVATION_COLUMNS = [
"dataSplit",
"dataset",
"datasetLabel",
"check",
"scenario",
"n",
"thin",
"relVol",
"spread",
"aspect",
"currentPolicy",
"currentPolicyMs",
"currentPolicyRegretRatio",
"candidatePolicy",
"winnerBvh",
"match",
"bestBvhMs",
"candidatePolicyMs",
"regretMs",
"regretRatio",
"slowdownFactor",
]
def parse_args() -> argparse.Namespace:
"""Read input and output directories from the command line."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--input-dir",
type=Path,
default=Path("target/bvh-exploration"),
help="Directory containing the Java CSV exports.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=Path("target/bvh-exploration/report"),
help="Directory for compact CSV and HTML tables.",
)
return parser.parse_args()
def read_csv(path: Path) -> pd.DataFrame:
"""Load one Java export and fail early when the expected file is missing."""
if not path.is_file():
raise FileNotFoundError(f"Missing BVH exploration export: {path}")
return pd.read_csv(path)
def compact_observations(frame: pd.DataFrame) -> pd.DataFrame:
"""Keep the columns that are useful for reading and reporting observations."""
columns = [column for column in OBSERVATION_COLUMNS if column in frame.columns]
return frame.loc[:, columns].copy()
def candidate_rules(bucket_summary: pd.DataFrame) -> pd.DataFrame:
"""Extract bucket rules that passed the Java-side support and share thresholds."""
candidate_mask = bucket_summary["candidateRule"].astype(str).str.lower().eq("true")
rules = bucket_summary.loc[candidate_mask].copy()
columns = [
"check",
"metric",
"bucket",
"cases",
"mostCommonBvh",
"winnerShare",
"winnerMargin",
]
return rules.loc[:, columns].sort_values(["check", "metric", "bucket"])
def real_dataset_summary(real: pd.DataFrame) -> pd.DataFrame:
"""Aggregate real CityGML observations by dataset split, dataset label, and check."""
evaluated = real.loc[real["regretRatio"].notna()].copy()
if evaluated.empty:
return pd.DataFrame(
columns=[
"dataSplit",
"dataset",
"datasetLabel",
"check",
"cases",
"matches",
"matchRate",
"medianRegretRatio",
"p90RegretRatio",
"medianSlowdownFactor",
"maxSlowdownFactor",
]
)
summary = (
evaluated.groupby(["dataSplit", "dataset", "datasetLabel", "check"], as_index=False)
.agg(
cases=("scenario", "size"),
matches=("match", lambda values: (values == "yes").sum()),
medianRegretRatio=("regretRatio", "median"),
p90RegretRatio=("regretRatio", lambda values: values.quantile(0.90)),
medianSlowdownFactor=("slowdownFactor", "median"),
maxSlowdownFactor=("slowdownFactor", "max"),
)
.sort_values(["dataSplit", "datasetLabel", "dataset", "check"])
)
summary.insert(6, "matchRate", summary["matches"] / summary["cases"])
return summary
def main() -> None:
"""Create compact CSV exports from the raw Java tables."""
args = parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
synthetic = read_csv(args.input_dir / "synthetic_observations.csv")
buckets = read_csv(args.input_dir / "synthetic_bucket_summary.csv")
real = read_csv(args.input_dir / "real_citygml_observations.csv")
synthetic_compact = compact_observations(synthetic)
rules = candidate_rules(buckets)
real_compact = compact_observations(real)
real_summary = real_dataset_summary(real)
synthetic_compact.to_csv(args.output_dir / "synthetic_strategy_comparison.csv", index=False)
rules.to_csv(args.output_dir / "synthetic_candidate_rules.csv", index=False)
real_compact.to_csv(args.output_dir / "real_strategy_comparison.csv", index=False)
real_summary.to_csv(args.output_dir / "real_dataset_summary.csv", index=False)
if __name__ == "__main__":
main()
package de.hft.stuttgart.citydoctor2.healing.bht; package de.hft.stuttgart.citydoctor2.healing.aabb;
import de.hft.stuttgart.citydoctor2.datastructure.bht.*; import de.hft.stuttgart.citydoctor2.datastructure.aabb.*;
import de.hft.stuttgart.citydoctor2.datastructure.*; import de.hft.stuttgart.citydoctor2.datastructure.*;
import de.hft.stuttgart.citydoctor2.parser.*; import de.hft.stuttgart.citydoctor2.parser.*;
import de.hft.stuttgart.citydoctor2.check.*; import de.hft.stuttgart.citydoctor2.check.*;
......
package de.hft.stuttgart.citydoctor2.healing.bht; package de.hft.stuttgart.citydoctor2.healing.aabb;
import java.io.IOException; import java.io.IOException;
//import java.io.InputStream; //new //import java.io.InputStream; //new
......
package de.hft.stuttgart.citydoctor2.healing.bht; package de.hft.stuttgart.citydoctor2.healing.aabb;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
......
package de.hft.stuttgart.citydoctor2.healing.bht; package de.hft.stuttgart.citydoctor2.healing.aabb;
/** /**
* Represents a CityGML test model with metadata for easier handling in tests. * Represents a CityGML test model with metadata for easier handling in tests.
......
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