Commit a7ed4b64 authored by Numanoglu's avatar Numanoglu
Browse files

Prepare mergeable AABB BVH variant

parent 1d9c7810
Pipeline #12438 passed with stage
in 2 minutes and 11 seconds
package de.hft.stuttgart.citydoctor2.checks.aabb.performance;
import de.hft.stuttgart.citydoctor2.checks.aabb.fixtures.SyntheticNestedRingGeometryFactory;
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 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.aabb.SplitStrategy;
/**
* Manual performance probe for nested inner-ring detection.
*
* The old check, the simple AABB filter, and all BVH variants are run on the
* same synthetic polygon so correctness and candidate-search cost can be read
* from one table.
*
* @author Numanoglu
*/
@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) {
// Inner rings are the indexed elements; the exact nested-ring decision stays in NestedRingsCheck.
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) {
// The production check exposes concrete BVH strategies through its Variant enum.
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.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 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.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")
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) {
// Edge count is the natural input size because every segment can become a BVH element.
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) {
// Run each polygon ring independently, as the check normally receives one ring at a time.
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) {
// Bridge the shared BVH strategy enum to the check-specific mode enum.
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);
}
}
}
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 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.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.BoundingVolumeHierarchyTree;
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")
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) {
// Result counts must match the brute-force run; only the broad-phase cost should change.
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) {
// Build a fresh tree per measurement so construction cost stays part of the comparison.
BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newWithStrategy(
geometry.getPolygons(),
polygon -> AABB.of(polygon.getOriginal()),
strategy);
return SelfIntersectionUtil.calculateSolidSelfIntersection(geometry, DELTA, tree).size();
}
}
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",
"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(),
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.aabb.support;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
import de.hft.stuttgart.citydoctor2.checks.geometry.NestedRingsCheck;
import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.datastructure.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 {
private BvhHeuristicTimingSupport() {
}
public static Observation measureVariants(
String checkName,
String scenarioName,
BvhInputMetricsCollector.Metrics metrics,
int inputSize,
StrategyOperation strategyOperation) {
// Correctness against OLD or brute force is covered by the dedicated variant tests.
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
Integer expectedResultCount = null;
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
BvhPerformanceTestSupport.Measurement bvhMeasurement = BvhPerformanceTestSupport.measure(
scenarioName,
strategy.name(),
inputSize,
() -> strategyOperation.run(strategy));
measurements.add(bvhMeasurement);
if (expectedResultCount == null) {
expectedResultCount = bvhMeasurement.resultCount;
} else {
assertEquals(checkName + " result differs for " + scenarioName + " / " + strategy,
expectedResultCount.intValue(), bvhMeasurement.resultCount);
}
}
String currentPolicyPrediction = predictFromCurrentPolicy(checkName, metrics);
String candidateRulePrediction = predictFromCandidateRules(checkName, metrics);
return new Observation(
checkName,
metrics,
fastestBvh(measurements),
currentPolicyPrediction,
candidateRulePrediction,
measurementForPrediction(measurements, currentPolicyPrediction),
measurementForPrediction(measurements, candidateRulePrediction));
}
public static Observation measureBvhStrategies(
String checkName,
String scenarioName,
BvhInputMetricsCollector.Metrics metrics,
int inputSize,
StrategyOperation strategyOperation) {
// The first BVH result count is reused only to verify the remaining variants.
List<BvhPerformanceTestSupport.Measurement> measurements = new ArrayList<>();
Integer expectedResultCount = null;
for (SplitStrategy strategy : BvhPerformanceTestSupport.concreteStrategies()) {
BvhPerformanceTestSupport.Measurement bvhMeasurement = BvhPerformanceTestSupport.measure(
scenarioName,
strategy.name(),
inputSize,
() -> strategyOperation.run(strategy));
if (expectedResultCount == null) {
expectedResultCount = bvhMeasurement.resultCount;
} else {
assertEquals(checkName + " broad-phase count differs for " + scenarioName + " / " + strategy,
expectedResultCount.intValue(), bvhMeasurement.resultCount);
}
measurements.add(bvhMeasurement);
}
String currentPolicyPrediction = predictFromCurrentPolicy(checkName, metrics);
String candidateRulePrediction = predictFromCandidateRules(checkName, metrics);
return new Observation(
checkName,
metrics,
fastestBvh(measurements),
currentPolicyPrediction,
candidateRulePrediction,
measurementForPrediction(measurements, currentPolicyPrediction),
measurementForPrediction(measurements, candidateRulePrediction));
}
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);
if (!BvhUsagePolicy.shouldUseTree(checkType, metrics.summary)) {
return "OLD";
}
return BvhUsagePolicy.chooseSplitStrategy(checkType, metrics.summary).name();
}
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);
if (!BvhUsagePolicy.shouldUseTree(checkType, metrics.summary)) {
return "OLD";
}
if ("SSI".equals(checkName)
&& metrics.averageAspectRatio >= 20.0
&& metrics.averageAspectRatio < 80.0) {
return SplitStrategy.OCTONARY_SPATIAL_MEDIAN.name();
}
if ("NESTED".equals(checkName)
&& metrics.averageRelativeBoxVolume <= 0.0001) {
return SplitStrategy.OCTONARY_OBJECT_MEAN.name();
}
if ("RSI".equals(checkName)
&& metrics.thinBoxRate > 0.75) {
return SplitStrategy.OCTONARY_SPATIAL_MEDIAN.name();
}
if ("RSI".equals(checkName)
&& metrics.averageAspectRatio >= 80.0) {
return SplitStrategy.OCTONARY_SPATIAL_MEDIAN.name();
}
if ("RSI".equals(checkName)
&& metrics.averageRelativeBoxVolume <= 0.0001) {
return SplitStrategy.OCTONARY_SPATIAL_MEDIAN.name();
}
return BvhUsagePolicy.chooseSplitStrategy(checkType, metrics.summary).name();
}
public static NestedRingsCheck.Variant nestedVariantFor(SplitStrategy strategy) {
// NestedRingsCheck exposes variants as check modes, while the BVH builder uses SplitStrategy.
switch (strategy) {
case BINARY_OBJECT_MEDIAN:
return NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEDIAN;
case BINARY_OBJECT_MEAN:
return NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEAN;
case BINARY_SPATIAL_MEDIAN:
return NestedRingsCheck.Variant.BVH_BINARY_SPATIAL_MEDIAN;
case OCTONARY_OBJECT_MEDIAN:
return NestedRingsCheck.Variant.BVH_OCTONARY_OBJECT_MEDIAN;
case OCTONARY_OBJECT_MEAN:
return NestedRingsCheck.Variant.BVH_OCTONARY_OBJECT_MEAN;
case OCTONARY_SPATIAL_MEDIAN:
return NestedRingsCheck.Variant.BVH_OCTONARY_SPATIAL_MEDIAN;
case AUTO:
default:
throw new IllegalArgumentException("Unsupported nested-ring BVH strategy: " + strategy);
}
}
public static RingSelfIntCheck.Variant rsiVariantFor(SplitStrategy strategy) {
// RingSelfIntCheck has the same six BVH modes but owns its own enum.
switch (strategy) {
case BINARY_OBJECT_MEDIAN:
return RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEDIAN;
case BINARY_OBJECT_MEAN:
return RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEAN;
case BINARY_SPATIAL_MEDIAN:
return RingSelfIntCheck.Variant.BVH_BINARY_SPATIAL_MEDIAN;
case OCTONARY_OBJECT_MEDIAN:
return RingSelfIntCheck.Variant.BVH_OCTONARY_OBJECT_MEDIAN;
case OCTONARY_OBJECT_MEAN:
return RingSelfIntCheck.Variant.BVH_OCTONARY_OBJECT_MEAN;
case OCTONARY_SPATIAL_MEDIAN:
return RingSelfIntCheck.Variant.BVH_OCTONARY_SPATIAL_MEDIAN;
case AUTO:
default:
throw new IllegalArgumentException("Unsupported ring-self-intersection BVH strategy: " + strategy);
}
}
private static BvhUsagePolicy.BvhCheckType checkTypeFor(String checkName) {
if ("SSI".equals(checkName)) {
return BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION;
}
if ("NESTED".equals(checkName)) {
return BvhUsagePolicy.BvhCheckType.NESTED_RINGS;
}
if ("RSI".equals(checkName)) {
return BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION;
}
throw new IllegalArgumentException("Unsupported check name: " + checkName);
}
private static BvhPerformanceTestSupport.Measurement fastestBvh(
List<BvhPerformanceTestSupport.Measurement> measurements) {
BvhPerformanceTestSupport.Measurement fastest = null;
for (BvhPerformanceTestSupport.Measurement measurement : measurements) {
if (fastest == null || measurement.averageNanos < fastest.averageNanos) {
fastest = measurement;
}
}
return fastest;
}
private static BvhPerformanceTestSupport.Measurement measurementForPrediction(
List<BvhPerformanceTestSupport.Measurement> measurements,
String prediction) {
for (BvhPerformanceTestSupport.Measurement measurement : measurements) {
if (measurement.variant.equals(prediction)) {
return measurement;
}
}
return null;
}
/**
* Operation that can be executed with each concrete BVH split strategy.
*/
public interface StrategyOperation {
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 final String checkName;
public final BvhInputMetricsCollector.Metrics metrics;
public final BvhPerformanceTestSupport.Measurement fastestBvh;
public final BvhPerformanceTestSupport.Measurement currentPolicyMeasurement;
public final BvhPerformanceTestSupport.Measurement candidateRuleMeasurement;
public final String currentPolicyPrediction;
public final String candidateRulePrediction;
Observation(
String checkName,
BvhInputMetricsCollector.Metrics metrics,
BvhPerformanceTestSupport.Measurement fastestBvh,
String currentPolicyPrediction,
String candidateRulePrediction,
BvhPerformanceTestSupport.Measurement currentPolicyMeasurement,
BvhPerformanceTestSupport.Measurement candidateRuleMeasurement) {
this.checkName = checkName;
this.metrics = metrics;
this.fastestBvh = fastestBvh;
this.currentPolicyPrediction = currentPolicyPrediction;
this.candidateRulePrediction = candidateRulePrediction;
this.currentPolicyMeasurement = currentPolicyMeasurement;
this.candidateRuleMeasurement = candidateRuleMeasurement;
}
public String candidateRuleMatchLabel() {
if ("OLD".equals(candidateRulePrediction)) {
return "n/a";
}
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.aabb.support;
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.aabb.AABB;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
/**
* Collects cheap broad-phase shape metrics for BVH strategy experiments.
*
* These metrics are intentionally geometry-agnostic: the same collector can be
* used for solid polygons, nested-ring rings, ring-self-intersection edges,
* synthetic fixtures, and parsed CityGML models.
*
* @author Numanoglu
*/
public final class BvhInputMetricsCollector {
private static final double DEGENERATE_TOLERANCE = 1e-12;
private static final long MAX_PAIR_SAMPLES = 20_000L;
private static final double THIN_BOX_ASPECT_RATIO = 20.0;
private BvhInputMetricsCollector() {
}
public static Metrics forPolygons(String scenario, List<? extends Polygon> polygons) {
return collect(scenario, polygons, polygon -> AABB.of(polygon.getOriginal()));
}
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);
}
public static Metrics forRings(String scenario, List<? extends LinearRing> rings) {
return collect(scenario, rings, AABB::of);
}
public static Metrics forRingsCheap(String scenario, List<? extends LinearRing> rings) {
return collect(scenario, rings, AABB::of, false);
}
/**
* Collects metrics from any element type that can be converted to an AABB.
*/
public static <E> Metrics collect(String scenario, List<E> elements, Function<E, AABB> aabbFunction) {
return collect(scenario, elements, aabbFunction, true);
}
public static <E> Metrics collect(
String scenario,
List<E> elements,
Function<E, AABB> aabbFunction,
boolean samplePairs) {
List<AABB> boxes = new ArrayList<>(elements.size());
for (E element : elements) {
AABB aabb = aabbFunction.apply(element);
if (aabb != null) {
boxes.add(aabb);
}
}
return collectBoxes(scenario, boxes, samplePairs);
}
/**
* Collects metrics from precomputed AABBs. Pairwise overlap and containment
* are sampled when the full pairs set would be too large.
*/
public static Metrics collectBoxes(String scenario, List<AABB> boxes) {
return collectBoxes(scenario, boxes, true);
}
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);
}
private static Metrics collectBoxes(String scenario, List<AABB> boxes, boolean samplePairs) {
int count = boxes.size();
if (count == 0) {
return Metrics.empty(scenario);
}
Aggregate aggregate = aggregate(boxes);
PairSample pairSample = samplePairs ? samplePairs(boxes) : PairSample.empty();
BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofAabbs(boxes);
int degenerateCount = 0;
int thinBoxCount = 0;
double totalAspectRatio = 0.0;
double totalRelativeVolume = 0.0;
double totalDx = 0.0;
double totalDy = 0.0;
double totalDz = 0.0;
double totalCx = 0.0;
double totalCy = 0.0;
double totalCz = 0.0;
for (AABB box : boxes) {
if (box.isDegenerate(DEGENERATE_TOLERANCE)) {
degenerateCount++;
}
double dx = box.getExtentX();
double dy = box.getExtentY();
double dz = box.getExtentZ();
double currentAspectRatio = aspectRatio(dx, dy, dz);
if (currentAspectRatio >= THIN_BOX_ASPECT_RATIO) {
thinBoxCount++;
}
totalDx += dx;
totalDy += dy;
totalDz += dz;
totalAspectRatio += currentAspectRatio;
totalRelativeVolume += relativeVolume(dx, dy, dz, aggregate);
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,
(double) thinBoxCount / count,
totalAspectRatio / count,
totalRelativeVolume / count,
totalDx / count,
totalDy / count,
totalDz / count,
Math.sqrt(varianceX / count) / positiveOrOne(aggregate.dx),
Math.sqrt(varianceY / count) / positiveOrOne(aggregate.dy),
Math.sqrt(varianceZ / count) / positiveOrOne(aggregate.dz),
(Math.sqrt(varianceX / count) / positiveOrOne(aggregate.dx)
+ Math.sqrt(varianceY / count) / positiveOrOne(aggregate.dy)
+ Math.sqrt(varianceZ / count) / positiveOrOne(aggregate.dz)) / 3.0,
summary);
}
/**
* Emits one compact metrics line that can be read next to a performance table.
*/
public static void print(Metrics metrics) {
System.out.println(metrics.toSummaryLine());
}
private static Aggregate aggregate(List<AABB> boxes) {
AABB totalBox = AABB.enclosing(boxes);
return new Aggregate(totalBox.getExtentX(), totalBox.getExtentY(), totalBox.getExtentZ());
}
private static double relativeVolume(double dx, double dy, double dz, Aggregate aggregate) {
double localMeasure = 1.0;
double totalMeasure = 1.0;
boolean hasActiveExtent = false;
if (dx > DEGENERATE_TOLERANCE) {
localMeasure *= dx;
totalMeasure *= aggregate.dx;
hasActiveExtent = true;
}
if (dy > DEGENERATE_TOLERANCE) {
localMeasure *= dy;
totalMeasure *= aggregate.dy;
hasActiveExtent = true;
}
if (dz > DEGENERATE_TOLERANCE) {
localMeasure *= dz;
totalMeasure *= aggregate.dz;
hasActiveExtent = true;
}
if (!hasActiveExtent || totalMeasure <= DEGENERATE_TOLERANCE) {
return 0.0;
}
return localMeasure / totalMeasure;
}
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 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 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 {
public final double dx;
public final double dy;
public final double dz;
Aggregate(double dx, double dy, double dz) {
this.dx = dx;
this.dy = dy;
this.dz = dz;
}
}
private static final class PairSample {
public final long sampledPairs;
public final long overlappingPairs;
public 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 PairSample empty() {
return new PairSample(0L, 0L, 0L);
}
}
public static final class Metrics {
public final String scenario;
public final int elementCount;
public final long totalPairs;
public final long sampledPairs;
public final double sampledOverlapRate;
public final double sampledContainmentRate;
public final double degenerateRate;
public final double thinBoxRate;
public final double averageAspectRatio;
public final double averageRelativeBoxVolume;
public final double averageExtentX;
public final double averageExtentY;
public final double averageExtentZ;
public final double normalizedCenterSpreadX;
public final double normalizedCenterSpreadY;
public final double normalizedCenterSpreadZ;
public final double centerSpreadRatio;
public final BvhUsagePolicy.BvhInputSummary summary;
private Metrics(
String scenario,
int elementCount,
long totalPairs,
long sampledPairs,
double sampledOverlapRate,
double sampledContainmentRate,
double degenerateRate,
double thinBoxRate,
double averageAspectRatio,
double averageRelativeBoxVolume,
double averageExtentX,
double averageExtentY,
double averageExtentZ,
double normalizedCenterSpreadX,
double normalizedCenterSpreadY,
double normalizedCenterSpreadZ,
double centerSpreadRatio,
BvhUsagePolicy.BvhInputSummary summary) {
this.scenario = scenario;
this.elementCount = elementCount;
this.totalPairs = totalPairs;
this.sampledPairs = sampledPairs;
this.sampledOverlapRate = sampledOverlapRate;
this.sampledContainmentRate = sampledContainmentRate;
this.degenerateRate = degenerateRate;
this.thinBoxRate = thinBoxRate;
this.averageAspectRatio = averageAspectRatio;
this.averageRelativeBoxVolume = averageRelativeBoxVolume;
this.averageExtentX = averageExtentX;
this.averageExtentY = averageExtentY;
this.averageExtentZ = averageExtentZ;
this.normalizedCenterSpreadX = normalizedCenterSpreadX;
this.normalizedCenterSpreadY = normalizedCenterSpreadY;
this.normalizedCenterSpreadZ = normalizedCenterSpreadZ;
this.centerSpreadRatio = centerSpreadRatio;
this.summary = summary;
}
static Metrics empty(String scenario) {
return new Metrics(scenario, 0, 0L, 0L, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
BvhUsagePolicy.BvhInputSummary.countOnly(0));
}
String toSummaryLine() {
return String.format(Locale.ROOT,
"[BVH-METRICS] %s n=%d pairs=%d sampled=%d overlap=%.4f containment=%.4f"
+ " degenerate=%.4f thin=%.4f aspectAvg=%.2f relVol=%.6f"
+ " avgExtent=(%.2f, %.2f, %.2f) centerSpread=(%.3f, %.3f, %.3f) spread=%.3f",
scenario,
elementCount,
totalPairs,
sampledPairs,
sampledOverlapRate,
sampledContainmentRate,
degenerateRate,
thinBoxRate,
averageAspectRatio,
averageRelativeBoxVolume,
averageExtentX,
averageExtentY,
averageExtentZ,
normalizedCenterSpreadX,
normalizedCenterSpreadY,
normalizedCenterSpreadZ,
centerSpreadRatio);
}
}
}
package de.hft.stuttgart.citydoctor2.checks.aabb.support;
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.aabb.SplitStrategy;
/**
* Shared support for manual BVH performance probes.
*
* It performs a warm-up, 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
*/
public final class BvhPerformanceTestSupport {
public static final int DEFAULT_WARMUP_RUNS = 2;
public 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() {
}
public 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
};
}
public 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.
*/
public 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.
*/
public 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;
}
public static final class Measurement {
public final String scenario;
public final String variant;
public final int inputSize;
public final int resultCount;
public 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;
}
public double averageMillis() {
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()
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