Commit e6f4979d authored by Numanoglu's avatar Numanoglu
Browse files

Add BVH variants and validation tests

parent 54c56f2d
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection;
public class SolidSelfIntersectionBvhVariantsSyntheticTest {
private static final double DELTA = 0.001;
@Test
public void allBvhVariantsMatchBruteForceOnSeparatedGrid() {
Geometry geometry = SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 6, 6);
assertAllVariantsMatchBruteForce(geometry, "separated grid");
}
@Test
public void allBvhVariantsMatchBruteForceOnOverlappingGrid() {
Geometry geometry = SyntheticSolidGeometryFactory.overlappingBoxGrid(Lod.LOD2, 6, 6);
assertAllVariantsMatchBruteForce(geometry, "overlapping grid");
}
@Test
public void allBvhVariantsMatchBruteForceOnDenseClusters() {
Geometry geometry = SyntheticSolidGeometryFactory.denseBoxClusters(Lod.LOD2, 4, 12);
assertAllVariantsMatchBruteForce(geometry, "dense clusters");
}
@Test
public void allBvhVariantsMatchBruteForceOnLongThinSlabs() {
Geometry geometry = SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 40);
assertAllVariantsMatchBruteForce(geometry, "long thin slabs");
}
@Test
public void allBvhVariantsMatchBruteForceOnFlatBoxGrid() {
Geometry geometry = SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 50);
assertAllVariantsMatchBruteForce(geometry, "flat box grid");
}
private static void assertAllVariantsMatchBruteForce(Geometry geometry, String scenario) {
assertNotNull("Expected geometry for " + scenario, geometry);
List<Polygon> polygons = geometry.getPolygons();
assertNotNull("Expected polygons for " + scenario, polygons);
List<PolygonIntersection> bruteForce =
SelfIntersectionUtil.calculateSolidSelfIntersection0(geometry, DELTA);
assertNotNull("Expected brute-force result for " + scenario, bruteForce);
Set<String> bruteForcePolygonPairs = polygonPairKeys(polygons, bruteForce);
for (SplitStrategy strategy : concreteStrategies()) {
BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newWithStrategy(
polygons,
p -> AABB.of(p.getOriginal()),
strategy);
List<PolygonIntersection> withTree =
SelfIntersectionUtil.calculateSolidSelfIntersection(geometry, DELTA, tree);
assertNotNull("Expected BVH result for " + scenario + " / " + strategy, withTree);
assertEquals(
"BVH result count differs from brute force for " + scenario + " / " + strategy,
bruteForce.size(),
withTree.size());
assertEquals(
"BVH polygon pairs differ from brute force for " + scenario + " / " + strategy,
bruteForcePolygonPairs,
polygonPairKeys(polygons, withTree));
}
}
private static Set<String> polygonPairKeys(List<Polygon> polygons, List<PolygonIntersection> intersections) {
Map<Polygon, Integer> polygonIds = new HashMap<>();
for (int i = 0; i < polygons.size(); i++) {
polygonIds.put(polygons.get(i), i);
polygonIds.put(polygons.get(i).getOriginal(), i);
}
Set<String> polygonPairs = new HashSet<>();
for (PolygonIntersection intersection : intersections) {
polygonPairs.add(polygonPairKey(polygonIds, intersection));
}
return polygonPairs;
}
private static String polygonPairKey(Map<Polygon, Integer> polygonIds, PolygonIntersection intersection) {
int p1 = polygonIndex(polygonIds, intersection.getP1());
int p2 = polygonIndex(polygonIds, intersection.getP2());
if (p1 <= p2) {
return p1 + "|" + p2;
}
return p2 + "|" + p1;
}
private static int polygonIndex(Map<Polygon, Integer> polygonIds, Polygon polygon) {
Integer id = polygonIds.get(polygon);
if (id == null) {
throw new AssertionError("Intersection references a polygon outside the tested geometry: " + polygon);
}
return id;
}
private 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
};
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
/*
* First comparison SolidSelfIntersection version with Bounding Volume Tree vs old version
*
* @ Baris Numanoglu
* */
public class SolidSelfIntersectionOldVsNewTest {
// @Test
// public void testOldVsNewSameResultCount() throws CityGmlParseException, InvalidGmlFileException {
// compareOnFile("src/test/resources/SolidSelfIntTest-known_false_positive_Big_Mesh2.gml", 0.001);
// }// dubious Test data: may be TP and not FP?
//
// @Test
// public void testOldVsNewSameResultCountFalsePositiveExample1() throws CityGmlParseException, InvalidGmlFileException {
// compareOnFile("src/test/resources/SolidSelfIntTest-known_false_positive1.gml", 0.001);
// }
@Test
public void testOldVsNewSameResultCountFalsePositiveExample2() throws CityGmlParseException, InvalidGmlFileException {
compareOnFile("src/test/resources/SolidSelfIntTest-known_false_positive2.gml", 0.001);
}
private void compareOnFile(String gmlPath, double delta) throws CityGmlParseException, InvalidGmlFileException {
ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig();
config.setSchematronFilePathInGlobalParameters(null);
CityDoctorModel m = CityGmlParser.parseCityGmlFile(gmlPath, config.getParserConfiguration());
Building building = m.getBuildings().findFirst().orElseThrow();
Geometry g = building.getGeometry(GeometryType.SOLID, Lod.LOD2);
assertNotNull("Expected SOLID LOD2 geometry in test model: " + gmlPath, g);
List<Polygon> polys = g.getPolygons();
assertNotNull(polys);
assertTrue("Expected at least 2 polygons in: " + gmlPath, polys.size() > 1);
/// Without Tree
long start = System.nanoTime();
List<PolygonIntersection> oldRes = SelfIntersectionUtil.calculateSolidSelfIntersection0(g, delta);
long dif = System.nanoTime() - start;
System.out.println("Alt: " + dif);
/// With IdentityHashMap
start = System.nanoTime();
BoundingVolumeHierarchyTree<Polygon> polygonTree =
BoundingVolumeHierarchyTree.newBinary(
g.getPolygons(),
p -> AABB.of(p.getOriginal())
);
List<PolygonIntersection> oldTreeRes =
SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta, polygonTree);
dif = System.nanoTime() - start;
System.out.println("Alt + external polygon tree: " + dif);
/// With Polygon Indices
start = System.nanoTime();
List<PolygonIntersection> newRes =
SelfIntersectionUtil.calculateSolidSelfIntersectionWithTree(g, delta);
dif = System.nanoTime() - start;
System.out.println("Neu: " + dif);
///
System.out.println("oldRes.size() = " + oldRes.size());
System.out.println("oldTreeRes.size() = " + oldTreeRes.size());
System.out.println("newRes.size() = " + newRes.size());
assertFalse("oldRes is empty for: " + gmlPath, oldRes.isEmpty());
assertFalse("oldTreeRes is empty for: " + gmlPath, oldTreeRes.isEmpty());
assertFalse("newRes is empty for: " + gmlPath, newRes.isEmpty());
assertEquals("Old vs oldTree differs for: " + gmlPath,
oldRes.size(), oldTreeRes.size());
assertEquals("Old vs new self-intersection result count differs for: " + gmlPath, oldRes.size(), newRes.size());
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import java.util.ArrayList;
import java.util.List;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
/**
* Test-fixture factory
* for polygons with many interior rings used by nested-ring BVH comparison tests.
*
*
* Scenarios:
* - manyDisjointInnerRings:
* many spatially separated holes;
* expected to be the best case for BVH candidate pruning.
*
* - oneNestedPairAmongMany:
* one true nested pair hidden among many unrelated rings;
* useful for correctness and selectivity checks.
*
* - concentricNestedRings:
* many rings around the same center;
* expected worst case because most AABBs overlap or contain each other.
*
* - overlappingAabbsButNotNested:
* rings whose boxes overlap while the rings are not contained;
* stresses the broad phase without creating true errors.
*
* - clusteredInnerRings:
* several local groups of rings;
* useful as a realistic middle ground between disjoint and fully overlapping data.
*
* - complexNestedRingGeometry:
* combines all scenarios in one Geometry so correctness tests can compare
* BVH variants across easy, selective, noisy, clustered, and worst-case inputs.
*
* @author Numanoglu
*/
final class SyntheticNestedRingGeometryFactory {
private SyntheticNestedRingGeometryFactory() {
}
static Geometry complexNestedRingGeometry() {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
for (ConcretePolygon polygon : complexNestedRingPolygons()) {
geometry.addPolygon(polygon);
}
geometry.updateEdgesAndVertices();
return geometry;
}
static List<ConcretePolygon> complexNestedRingPolygons() {
List<ConcretePolygon> polygons = new ArrayList<>();
polygons.add(manyDisjointInnerRings(100));
polygons.add(oneNestedPairAmongMany(96));
polygons.add(concentricNestedRings(32));
polygons.add(overlappingAabbsButNotNested(96));
polygons.add(clusteredInnerRings(9, 12));
polygons.add(clusteredInnerRingsWithNestedPair(9, 12));
return polygons;
}
static ConcretePolygon manyDisjointInnerRings(int count) {
ConcretePolygon polygon = basePolygon(count);
for (int i = 0; i < count; i++) {
double x = gridX(i, 10) * 8.0 + 4.0;
double y = gridY(i, 10) * 8.0 + 4.0;
polygon.addInteriorRing(squareRing(x, y, 1.0));
}
return polygon;
}
static ConcretePolygon oneNestedPairAmongMany(int count) {
ConcretePolygon polygon = manyDisjointInnerRings(Math.max(0, count - 2));
polygon.addInteriorRing(squareRing(5.0, 85.0, 5.0));
polygon.addInteriorRing(squareRing(5.0, 85.0, 1.0));
return polygon;
}
static ConcretePolygon concentricNestedRings(int count) {
ConcretePolygon polygon = basePolygon(count);
for (int i = 0; i < count; i++) {
double halfSize = Math.max(1.0, count - i);
polygon.addInteriorRing(squareRing(50.0, 50.0, halfSize));
}
return polygon;
}
static ConcretePolygon overlappingAabbsButNotNested(int count) {
ConcretePolygon polygon = basePolygon(count);
for (int i = 0; i < count; i++) {
double x = 6.0 + (i % 12) * 6.0;
double y = 6.0 + (i / 12) * 6.0;
double skew = i % 2 == 0 ? 1.5 : -1.5;
polygon.addInteriorRing(diamondRing(x + skew, y, 2.4, 1.0));
}
return polygon;
}
static ConcretePolygon clusteredInnerRings(int clusterCount, int ringsPerCluster) {
ConcretePolygon polygon = basePolygon(clusterCount * ringsPerCluster);
for (int cluster = 0; cluster < clusterCount; cluster++) {
double clusterX = 8.0 + (cluster % 4) * 22.0;
double clusterY = 8.0 + (cluster / 4) * 22.0;
for (int i = 0; i < ringsPerCluster; i++) {
double offsetX = (i % 5) * 2.5;
double offsetY = (i / 5) * 2.5;
polygon.addInteriorRing(squareRing(clusterX + offsetX, clusterY + offsetY, 0.8));
}
}
return polygon;
}
static ConcretePolygon clusteredInnerRingsWithNestedPair(int clusterCount, int ringsPerCluster) {
ConcretePolygon polygon = clusteredInnerRings(clusterCount, ringsPerCluster);
polygon.addInteriorRing(squareRing(88.0, 88.0, 5.0));
polygon.addInteriorRing(squareRing(88.0, 88.0, 1.0));
return polygon;
}
private static ConcretePolygon basePolygon(int expectedInnerRingCount) {
double extent = Math.max(120.0, Math.ceil(Math.sqrt(Math.max(1, expectedInnerRingCount))) * 10.0 + 20.0);
ConcretePolygon polygon = new ConcretePolygon();
polygon.setExteriorRing(rectangleRing(0.0, 0.0, extent, extent, LinearRingType.EXTERIOR));
return polygon;
}
private static double gridX(int index, int columns) {
return index % columns;
}
private static double gridY(int index, int columns) {
return index / columns;
}
private static LinearRing squareRing(double centerX, double centerY, double halfSize) {
return rectangleRing(
centerX - halfSize,
centerY - halfSize,
centerX + halfSize,
centerY + halfSize,
LinearRingType.INTERIOR);
}
private static LinearRing rectangleRing(
double minX,
double minY,
double maxX,
double maxY,
LinearRingType type) {
LinearRing ring = new LinearRing(type);
Vertex v0 = new Vertex(minX, minY, 0.0);
Vertex v1 = new Vertex(maxX, minY, 0.0);
Vertex v2 = new Vertex(maxX, maxY, 0.0);
Vertex v3 = new Vertex(minX, maxY, 0.0);
ring.addVertex(v0);
ring.addVertex(v1);
ring.addVertex(v2);
ring.addVertex(v3);
ring.addVertex(v0);
return ring;
}
private static LinearRing diamondRing(double centerX, double centerY, double radiusX, double radiusY) {
LinearRing ring = new LinearRing(LinearRingType.INTERIOR);
Vertex v0 = new Vertex(centerX, centerY - radiusY, 0.0);
Vertex v1 = new Vertex(centerX + radiusX, centerY, 0.0);
Vertex v2 = new Vertex(centerX, centerY + radiusY, 0.0);
Vertex v3 = new Vertex(centerX - radiusX, centerY, 0.0);
ring.addVertex(v0);
ring.addVertex(v1);
ring.addVertex(v2);
ring.addVertex(v3);
ring.addVertex(v0);
return ring;
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
/**
* Test-fixture-factory for Synthetic Solid Geometries used by BVH comparisons.
*
* Provides separated grids, intentionally overlapping grids,
* dense local clusters, long thin slabs and almost-flat boxes
* to cover different broad-phase selectivity and split-strategy stress cases.
*
* @author Numanoglu
*/
final class SyntheticSolidGeometryFactory {
private SyntheticSolidGeometryFactory() {
}
static Geometry separatedBoxGrid(Lod lod, int xCount, int yCount) {
Geometry geometry = newSolid(lod);
double spacing = 6.0;
for (int ix = 0; ix < xCount; ix++) {
for (int iy = 0; iy < yCount; iy++) {
addBox(geometry, ix * spacing, iy * spacing, 0.0, 4.0, 4.0, 4.0);
}
}
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry overlappingBoxGrid(Lod lod, int xCount, int yCount) {
Geometry geometry = separatedBoxGrid(lod, xCount, yCount);
addBox(geometry, 6.0, 6.0, 0.5, 6.0, 2.8, 3.5);
addBox(geometry, 8.0, 5.5, 0.0, 2.8, 6.0, 4.2);
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry denseBoxClusters(Lod lod, int clusterCount, int boxesPerCluster) {
Geometry geometry = newSolid(lod);
double clusterSpacing = 30.0;
for (int cluster = 0; cluster < clusterCount; cluster++) {
double baseX = cluster * clusterSpacing;
double baseY = (cluster % 2) * clusterSpacing;
for (int i = 0; i < boxesPerCluster; i++) {
double offset = i * 1.15;
addBox(geometry, baseX + offset, baseY + offset * 0.5, 0.0, 3.0, 3.0, 3.0);
}
}
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry longThinSlabs(Lod lod, int count) {
Geometry geometry = newSolid(lod);
for (int i = 0; i < count; i++) {
double y = i * 0.75;
addBox(geometry, 0.0, y, 0.0, 40.0, 0.25, 1.0);
}
geometry.updateEdgesAndVertices();
return geometry;
}
static Geometry flatBoxGrid(Lod lod, int count) {
Geometry geometry = newSolid(lod);
for (int i = 0; i < count; i++) {
double x = (i % 10) * 5.0;
double y = (i / 10) * 5.0;
addBox(geometry, x, y, 0.0, 4.0, 4.0, 0.02);
}
geometry.updateEdgesAndVertices();
return geometry;
}
private static Geometry newSolid(Lod lod) {
return new Geometry(GeometryType.SOLID, lod, Orientation.OUTWARD);
}
private static void addBox(
Geometry geometry,
double x,
double y,
double z,
double width,
double depth,
double height) {
Vertex v000 = new Vertex(x, y, z);
Vertex v100 = new Vertex(x + width, y, z);
Vertex v110 = new Vertex(x + width, y + depth, z);
Vertex v010 = new Vertex(x, y + depth, z);
Vertex v001 = new Vertex(x, y, z + height);
Vertex v101 = new Vertex(x + width, y, z + height);
Vertex v111 = new Vertex(x + width, y + depth, z + height);
Vertex v011 = new Vertex(x, y + depth, z + height);
addQuad(geometry, v000, v100, v110, v010);
addQuad(geometry, v001, v011, v111, v101);
addQuad(geometry, v000, v001, v101, v100);
addQuad(geometry, v100, v101, v111, v110);
addQuad(geometry, v110, v111, v011, v010);
addQuad(geometry, v010, v011, v001, v000);
}
private static void addQuad(Geometry geometry, Vertex a, Vertex b, Vertex c, Vertex d) {
ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
polygon.setExteriorRing(ring);
geometry.addPolygon(polygon);
ring.addVertex(a);
ring.addVertex(b);
ring.addVertex(c);
ring.addVertex(d);
ring.addVertex(a);
}
}
...@@ -84,10 +84,8 @@ public class TestCityGmlAABB { ...@@ -84,10 +84,8 @@ public class TestCityGmlAABB {
System.out.println("\n> Polygon-level AABB (broad-phase)"); System.out.println("\n> Polygon-level AABB (broad-phase)");
Set<ConcretePolygon> polys = collectAllPolygons(buildings); Set<ConcretePolygon> polys = collectAllPolygons(buildings);
BoundingVolumeHierarchyTree<ConcretePolygon> bvh = new BoundingVolumeHierarchyTree<>( BoundingVolumeHierarchyTree<ConcretePolygon> bvh =
new ArrayList<>(polys), BoundingVolumeHierarchyTree.newBinary(new ArrayList<>(polys), AABB::of);
AABB::of
);
System.out.println("Polygons: " + polys.size()); System.out.println("Polygons: " + polys.size());
if (bvh.getRoot() != null) { if (bvh.getRoot() != null) {
System.out.print("Root AABB: "); System.out.print("Root AABB: ");
...@@ -103,10 +101,8 @@ public class TestCityGmlAABB { ...@@ -103,10 +101,8 @@ public class TestCityGmlAABB {
? collectExteriorRings(buildings) ? collectExteriorRings(buildings)
: collectInteriorRings(buildings); : collectInteriorRings(buildings);
BoundingVolumeHierarchyTree<LinearRing> bvh = new BoundingVolumeHierarchyTree<>( BoundingVolumeHierarchyTree<LinearRing> bvh =
new ArrayList<>(rings), BoundingVolumeHierarchyTree.newBinary(new ArrayList<>(rings), AABB::of);
AABB::of
);
System.out.println(label + " rings: " + rings.size()); System.out.println(label + " rings: " + rings.size());
if (bvh.getRoot() != null) { if (bvh.getRoot() != null) {
...@@ -119,10 +115,8 @@ public class TestCityGmlAABB { ...@@ -119,10 +115,8 @@ public class TestCityGmlAABB {
System.out.println("\n> Vertex-level AABB (fine-phase)"); System.out.println("\n> Vertex-level AABB (fine-phase)");
Set<Vertex> verts = collectAllVertices(buildings); Set<Vertex> verts = collectAllVertices(buildings);
BoundingVolumeHierarchyTree<Vertex> bvh = new BoundingVolumeHierarchyTree<>( BoundingVolumeHierarchyTree<Vertex> bvh =
new ArrayList<>(verts), BoundingVolumeHierarchyTree.newBinary(new ArrayList<>(verts), AABB::of);
AABB::of
);
System.out.println("Vertices: " + verts.size()); System.out.println("Vertices: " + verts.size());
if (bvh.getRoot() != null) { if (bvh.getRoot() != null) {
......
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