Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
CityDoctor
CityDoctor2
Commits
a7ed4b64
Commit
a7ed4b64
authored
Sep 07, 2026
by
Numanoglu
Browse files
Prepare mergeable AABB BVH variant
parent
1d9c7810
Pipeline
#12438
passed with stage
in 2 minutes and 11 seconds
Changes
28
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/aabb/performance/NestedRingCheckBvhPerformanceTest.java
deleted
100644 → 0
View file @
1d9c7810
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
);
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/aabb/performance/RingSelfIntCheckBvhPerformanceTest.java
deleted
100644 → 0
View file @
1d9c7810
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
);
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/aabb/performance/SolidSelfIntersectionBvhPerformanceTest.java
deleted
100644 → 0
View file @
1d9c7810
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
();
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/aabb/support/BvhExplorationCsvWriter.java
deleted
100644 → 0
View file @
1d9c7810
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
);
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/aabb/support/BvhHeuristicTimingSupport.java
deleted
100644 → 0
View file @
1d9c7810
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
;
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/aabb/support/BvhInputMetricsCollector.java
deleted
100644 → 0
View file @
1d9c7810
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
=
1
e
-
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
);
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/aabb/support/BvhPerformanceTestSupport.java
deleted
100644 → 0
View file @
1d9c7810
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
;
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/python/bvh_exploration_tables.py
deleted
100644 → 0
View file @
1d9c7810
"""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
()
Prev
1
2
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment