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
...@@ -10,6 +10,8 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -10,6 +10,8 @@ public class BoundingVolumeHierarchyTree<E> {
private final Node<E> root; private final Node<E> root;
static final int BINARY_TREE_DEGREE = 2;
static final int OCTONARY_TREE_DEGREE = 8;
static final int DEFAULT_MIN_DEPTH = 4; static final int DEFAULT_MIN_DEPTH = 4;
static final int DEFAULT_MAX_DEPTH = 32; static final int DEFAULT_MAX_DEPTH = 32;
static final int DEFAULT_BINARY_LEAF_SIZE = 1; static final int DEFAULT_BINARY_LEAF_SIZE = 1;
...@@ -88,11 +90,11 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -88,11 +90,11 @@ public class BoundingVolumeHierarchyTree<E> {
case BINARY_OBJECT_MEDIAN: case BINARY_OBJECT_MEDIAN:
case BINARY_OBJECT_MEAN: case BINARY_OBJECT_MEAN:
case BINARY_SPATIAL_MEDIAN: case BINARY_SPATIAL_MEDIAN:
return 2; return BINARY_TREE_DEGREE;
case OCTONARY_OBJECT_MEDIAN: case OCTONARY_OBJECT_MEDIAN:
case OCTONARY_OBJECT_MEAN: case OCTONARY_OBJECT_MEAN:
case OCTONARY_SPATIAL_MEDIAN: case OCTONARY_SPATIAL_MEDIAN:
return 8; return OCTONARY_TREE_DEGREE;
case AUTO: case AUTO:
default: default:
throw new IllegalArgumentException("AUTO does not define a concrete BVH variant."); throw new IllegalArgumentException("AUTO does not define a concrete BVH variant.");
...@@ -100,7 +102,7 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -100,7 +102,7 @@ public class BoundingVolumeHierarchyTree<E> {
} }
private static int defaultMaxLeafSizeFor(SplitStrategy splitStrategy) { private static int defaultMaxLeafSizeFor(SplitStrategy splitStrategy) {
return degreeFor(splitStrategy) == 2 return degreeFor(splitStrategy) == BINARY_TREE_DEGREE
? DEFAULT_BINARY_LEAF_SIZE ? DEFAULT_BINARY_LEAF_SIZE
: DEFAULT_OCTONARY_LEAF_SIZE; : DEFAULT_OCTONARY_LEAF_SIZE;
} }
...@@ -200,7 +202,7 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -200,7 +202,7 @@ public class BoundingVolumeHierarchyTree<E> {
} }
public static final class Builder<E> { public static final class Builder<E> {
private int degree = 2; private int degree = BINARY_TREE_DEGREE;
private SplitStrategy splitStrategy = SplitStrategy.AUTO; private SplitStrategy splitStrategy = SplitStrategy.AUTO;
private int maxLeafSize = DEFAULT_BINARY_LEAF_SIZE; private int maxLeafSize = DEFAULT_BINARY_LEAF_SIZE;
private int maxDepth = DEFAULT_MAX_DEPTH; private int maxDepth = DEFAULT_MAX_DEPTH;
...@@ -251,8 +253,8 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -251,8 +253,8 @@ public class BoundingVolumeHierarchyTree<E> {
Objects.requireNonNull(elements, "elements"); Objects.requireNonNull(elements, "elements");
Objects.requireNonNull(aabbFunction, "aabbFunction"); Objects.requireNonNull(aabbFunction, "aabbFunction");
if (degree != 2 && degree != 8) { if (degree != BINARY_TREE_DEGREE && degree != OCTONARY_TREE_DEGREE) {
throw new IllegalArgumentException("Only degree 2 and 8 are supported."); throw new IllegalArgumentException("Only binary and octonary tree degrees are supported.");
} }
if (maxLeafSize < 1) { if (maxLeafSize < 1) {
throw new IllegalArgumentException("maxLeafSize must be >= 1."); throw new IllegalArgumentException("maxLeafSize must be >= 1.");
...@@ -274,7 +276,7 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -274,7 +276,7 @@ public class BoundingVolumeHierarchyTree<E> {
List<BvhBuildItem<E>> items = toBuildItems(elements); List<BvhBuildItem<E>> items = toBuildItems(elements);
SplitStrategy resolvedStrategy = resolveSplitStrategy(); SplitStrategy resolvedStrategy = resolveSplitStrategy();
return degree == 2 return degree == BINARY_TREE_DEGREE
? buildBinaryRecursive(items, 0, resolvedStrategy) ? buildBinaryRecursive(items, 0, resolvedStrategy)
: buildOctonaryRecursive(items, 0, resolvedStrategy); : buildOctonaryRecursive(items, 0, resolvedStrategy);
} }
...@@ -293,7 +295,7 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -293,7 +295,7 @@ public class BoundingVolumeHierarchyTree<E> {
validateStrategyMatchesDegree(degree, splitStrategy); validateStrategyMatchesDegree(degree, splitStrategy);
return splitStrategy; return splitStrategy;
} }
return degree == 2 return degree == BINARY_TREE_DEGREE
? SplitStrategy.BINARY_SPATIAL_MEDIAN ? SplitStrategy.BINARY_SPATIAL_MEDIAN
: SplitStrategy.OCTONARY_OBJECT_MEAN; : SplitStrategy.OCTONARY_OBJECT_MEAN;
} }
...@@ -307,11 +309,11 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -307,11 +309,11 @@ public class BoundingVolumeHierarchyTree<E> {
|| strategy == SplitStrategy.OCTONARY_OBJECT_MEAN || strategy == SplitStrategy.OCTONARY_OBJECT_MEAN
|| strategy == SplitStrategy.OCTONARY_SPATIAL_MEDIAN; || strategy == SplitStrategy.OCTONARY_SPATIAL_MEDIAN;
if (treeDegree == 2 && !binary) { if (treeDegree == BINARY_TREE_DEGREE && !binary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 2."); throw new IllegalArgumentException("Strategy " + strategy + " does not match binary tree degree.");
} }
if (treeDegree == 8 && !octonary) { if (treeDegree == OCTONARY_TREE_DEGREE && !octonary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 8."); throw new IllegalArgumentException("Strategy " + strategy + " does not match octonary tree degree.");
} }
} }
...@@ -384,7 +386,7 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -384,7 +386,7 @@ public class BoundingVolumeHierarchyTree<E> {
} }
Node<E> node = new Node<>(null, totalAabb); Node<E> node = new Node<>(null, totalAabb);
for (int i = 0; i < 8; i++) { for (int i = 0; i < OCTONARY_TREE_DEGREE; i++) {
List<BvhBuildItem<E>> bucket = split.buckets[i]; List<BvhBuildItem<E>> bucket = split.buckets[i];
if (bucket.isEmpty()) { if (bucket.isEmpty()) {
continue; continue;
...@@ -440,7 +442,7 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -440,7 +442,7 @@ public class BoundingVolumeHierarchyTree<E> {
static <E> Builder<E> binaryDefault() { static <E> Builder<E> binaryDefault() {
return new Builder<E>() return new Builder<E>()
.degree(2) .degree(BINARY_TREE_DEGREE)
.splitStrategy(SplitStrategy.AUTO) .splitStrategy(SplitStrategy.AUTO)
.maxLeafSize(DEFAULT_BINARY_LEAF_SIZE) .maxLeafSize(DEFAULT_BINARY_LEAF_SIZE)
.maxDepth(DEFAULT_MAX_DEPTH) .maxDepth(DEFAULT_MAX_DEPTH)
...@@ -449,7 +451,7 @@ public class BoundingVolumeHierarchyTree<E> { ...@@ -449,7 +451,7 @@ public class BoundingVolumeHierarchyTree<E> {
static <E> Builder<E> octonaryDefault() { static <E> Builder<E> octonaryDefault() {
return new Builder<E>() return new Builder<E>()
.degree(8) .degree(OCTONARY_TREE_DEGREE)
.splitStrategy(SplitStrategy.AUTO) .splitStrategy(SplitStrategy.AUTO)
.maxLeafSize(DEFAULT_OCTONARY_LEAF_SIZE) .maxLeafSize(DEFAULT_OCTONARY_LEAF_SIZE)
.maxDepth(DEFAULT_MAX_DEPTH) .maxDepth(DEFAULT_MAX_DEPTH)
......
...@@ -68,13 +68,13 @@ final class OctonarySplitters { ...@@ -68,13 +68,13 @@ final class OctonarySplitters {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static <E> OctonarySplitResult<E> bucketize(List<BvhBuildItem<E>> items, double sx, double sy, double sz) { private static <E> OctonarySplitResult<E> bucketize(List<BvhBuildItem<E>> items, double sx, double sy, double sz) {
List<BvhBuildItem<E>>[] buckets = new List[8]; List<BvhBuildItem<E>>[] buckets = new List[BoundingVolumeHierarchyTree.OCTONARY_TREE_DEGREE];
for (int i = 0; i < 8; i++) { for (int i = 0; i < BoundingVolumeHierarchyTree.OCTONARY_TREE_DEGREE; i++) {
buckets[i] = new ArrayList<>(); buckets[i] = new ArrayList<>();
} }
int nonEmptyCount = 0; int nonEmptyCount = 0;
boolean[] seen = new boolean[8]; boolean[] seen = new boolean[BoundingVolumeHierarchyTree.OCTONARY_TREE_DEGREE];
for (BvhBuildItem<E> item : items) { for (BvhBuildItem<E> item : items) {
int idx = octantIndex(item.centerX, item.centerY, item.centerZ, sx, sy, sz); int idx = octantIndex(item.centerX, item.centerY, item.centerZ, sx, sy, sz);
......
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.checks.aabb;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.HashMap;
import java.util.Map;
import de.hft.stuttgart.citydoctor2.check.Check;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.RequirementType;
import de.hft.stuttgart.citydoctor2.check.Requirement;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.NestedRingError;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABBUtils;
/**
* Checks whether a inner ring is completely contained in another inner ring
* within the same polygon
*
* @author Baris Numanoglu
*
*/
// TODO extend check with tree
public class NestedRingsCheckAABB extends Check {
private static final List<CheckId> dependencies;
static {
ArrayList<CheckId> deps = new ArrayList<>();
deps.add(CheckId.C_GE_R_TOO_FEW_POINTS);
deps.add(CheckId.C_GE_R_NOT_CLOSED);
deps.add(CheckId.C_GE_R_DUPLICATE_POINT);
deps.add(CheckId.C_GE_R_SELF_INTERSECTION);
deps.add(CheckId.C_GE_P_NON_PLANAR);
deps.add(CheckId.C_GE_P_ORIENTATION_RINGS_SAME);
dependencies = Collections.unmodifiableList(deps);
}
@Override
public void check(Polygon p) {
List<LinearRing> inner = p.getInnerRings();
if (inner == null || inner.isEmpty()) {
p.addCheckResult(new CheckResult(this, ResultStatus.OK, null));
return;
}
// --- NEW: cache AABBs of inner rings for broad-phase ---
Map<LinearRing, AABB> ringBoxes = new HashMap<>(inner.size());
for (LinearRing r : inner) {
ringBoxes.put(r, AABB.of(r));
}
for (LinearRing interiorRing : inner) {
AABB interiorBox = ringBoxes.get(interiorRing);
for (LinearRing checkRing : inner) {
if (checkRing == interiorRing) {
// do not compare with itself
continue;
}
// --- NEW: Broad-phase rejection ---
AABB checkBox = ringBoxes.get(checkRing);
// If the inner AABB is not fully inside the other ring's AABB,
// "nested" is impossible -> skip narrow phase.
if (! interiorBox.contains(checkBox)) {
continue;
}
// are all points from checkRing inside interiorRing?
if (areAllPointsInside(interiorRing, checkRing)) {
// found all vertices inside another interior ring
CheckError err = new NestedRingError(p, interiorRing, checkRing);
CheckResult cr = new CheckResult(this, ResultStatus.ERROR, err);
p.addCheckResult(cr);
return;
}
}
}
CheckResult cr = new CheckResult(this, ResultStatus.OK, null);
p.addCheckResult(cr);
}
private boolean areAllPointsInside(LinearRing ring, LinearRing checkRing) {
boolean isInside = true;
for (Vertex v : checkRing.getVertices()) {
if (!ring.isPointInside(v)) {
isInside = false;
break;
}
}
return isInside;
}
@Override
public List<CheckId> getDependencies() {
return dependencies;
}
@Override
public Set<Requirement> appliesToRequirements() {
return CollectionUtils.singletonSet(Requirement.R_GE_P_INNER_RINGS_NESTED);
}
@Override
public RequirementType getType() {
return RequirementType.GEOMETRY;
}
@Override
public Check createNewInstance() {
return new NestedRingsCheckAABB();
}
@Override
public CheckId getCheckId() {
return CheckId.C_GE_P_INNER_RINGS_NESTED;
}
}
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.checks.aabb;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import Jama.EigenvalueDecomposition;
import de.hft.stuttgart.citydoctor2.check.Check;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.Requirement;
import de.hft.stuttgart.citydoctor2.check.RequirementType;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.DegeneratedRingError;
import de.hft.stuttgart.citydoctor2.check.error.PointTouchesEdgeError;
import de.hft.stuttgart.citydoctor2.check.error.RingEdgeIntersectionError;
import de.hft.stuttgart.citydoctor2.checks.Checks;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.datastructure.BoundingBox;
import de.hft.stuttgart.citydoctor2.datastructure.Edge;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.math.CovarianceMatrix;
import de.hft.stuttgart.citydoctor2.math.DistanceResult;
import de.hft.stuttgart.citydoctor2.math.Matrix3x3d;
import de.hft.stuttgart.citydoctor2.math.OrthogonalRegressionPlane;
import de.hft.stuttgart.citydoctor2.math.Segment3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABBUtils;
/**
* Checks whether a ring self intersects. Also checks if a point is too close to
* an edge of the ring.
*
* Axis Aligned Boundin Box Early-Out-Version of Matthias Betz's Check
* @author Baris Numanoglu
*/
// TODO extend check with tree
public class RingSelfIntCheckAABB extends Check {
private static final String EPSILON_NAME = "minVertexDistance";
// check requirement class for default parameters
private double degeneratedRingTolerance = 0.01;
private double epsilon = Checks.MIN_VERTEX_DISTANCE_DEFAULT;
private static final List<CheckId> dependencies;
static {
ArrayList<CheckId> deps = new ArrayList<>();
deps.add(CheckId.C_GE_R_TOO_FEW_POINTS);
deps.add(CheckId.C_GE_R_NOT_CLOSED);
deps.add(CheckId.C_GE_R_DUPLICATE_POINT);
dependencies = Collections.unmodifiableList(deps);
}
@Override
public void init(Map<String, String> parameters, ParserConfiguration config) {
String epsilonString = parameters.get(EPSILON_NAME);
if (epsilonString != null) {
epsilon = Double.parseDouble(epsilonString);
}
if (parameters.containsKey(Requirement.DEGENERATED_RING_TOLERANCE)) {
degeneratedRingTolerance = Double.parseDouble(parameters.get(Requirement.DEGENERATED_RING_TOLERANCE));
}
}
@Override
public void check(LinearRing lr) {
checkRingJava(lr);
}
private void checkRingJava(LinearRing lr) {
// keep original behavior (tiny/degenerate detection first)
List<Vertex> vertices = lr.getVertices();
Vector3d centroid = CovarianceMatrix.getCentroid(vertices);
EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid);
if (checkEigenvalues(lr, vertices, ed)) {
return; // error added, stop here
}
List<Edge> edges = getEdgesForRing(lr);
// --- NEW: Broad-phase AABB for "point touches edge" test ---
// Precompute padded AABBs for all edges (padding = epsilon)
List<EdgeBox> edgeBoxes = new ArrayList<>(edges.size());
for (Edge e : edges) {
edgeBoxes.add(new EdgeBox(e, AABB.of(e.getFrom(), e.getTo(), epsilon)));
}
// Optimized "point touches edge": first filter by padded edge AABB
for (EdgeBox eb : edgeBoxes) {
if (checkForPointsTouchingEdgeBroadPhase(lr, eb)) {
return;
}
}
// --- Original pairwise edge test, now with AABB early-out ---
for (int i = 0; i < edges.size() - 1; i++) {
Edge e1 = edges.get(i);
EdgeBox eb1 = edgeBoxes.get(i);
for (int j = i + 1; j < edges.size(); j++) {
Edge e2 = edges.get(j);
// share a vertex? skip
if (e1.getConnectionPoint(e2) != null) {
continue;
}
//skip if AABBs don't overlap
EdgeBox eb2 = edgeBoxes.get(j);
if (!eb1.box.overlaps(eb2.box)) {
continue;
}
// Narrow-phase distance test (unchanged)
Segment3d s1 = new Segment3d(e1.getFrom(), e1.getTo());
Segment3d s2 = new Segment3d(e2.getFrom(), e2.getTo());
DistanceResult dr = s1.getDistanceResult(s2);
if (dr.distance() < epsilon) {
CheckError err = new RingEdgeIntersectionError(lr, e1, e2, dr.point1());
CheckResult cr = new CheckResult(this, ResultStatus.ERROR, err);
lr.addCheckResult(cr);
return;
}
}
}
// no errors detected
CheckResult cr = new CheckResult(this, ResultStatus.OK, null);
lr.addCheckResult(cr);
}
private boolean checkEigenvalues(LinearRing lr, List<Vertex> points, EigenvalueDecomposition ed) {
Matrix3x3d mat = new Matrix3x3d(ed.getV().getArray());
List<Vector3d> rotatedVertices = new ArrayList<>();
for (Vertex v : points) {
rotatedVertices.add(mat.mult(v));
}
// --- AABB replacement for BoundingBox.ofPoints(rotatedVertices) ---
// Computes the axis-aligned extents of the rotated point cloud.
AABB aabb = AABB.ofPoints(rotatedVertices);
double dx = aabb.getMaxX() - aabb.getMinX();
double dy = aabb.getMaxY() - aabb.getMinY();
double dz = aabb.getMaxZ() - aabb.getMinZ();
int nrOfEigenvaluesBelowTolerance = 0;
if (dx < degeneratedRingTolerance) {
nrOfEigenvaluesBelowTolerance++;
}
if (dy < degeneratedRingTolerance) {
nrOfEigenvaluesBelowTolerance++;
}
if (dz < degeneratedRingTolerance) {
nrOfEigenvaluesBelowTolerance++;
}
if (nrOfEigenvaluesBelowTolerance >= 2) {
CheckError err = new DegeneratedRingError(lr);
lr.addCheckResult(new CheckResult(this, ResultStatus.ERROR, err));
return true;
}
return false;
}
/**
* Original narrow-phase "point near edge" with a broad-phase AABB filter.
*/
private boolean checkForPointsTouchingEdgeBroadPhase(LinearRing lr, EdgeBox eb) {
Segment3d seg = new Segment3d(eb.edge.getFrom(), eb.edge.getTo());
for (Vertex v : lr.getVertices()) {
// skip endpoints
if (v == eb.edge.getFrom() || v == eb.edge.getTo()) {
continue;
}
// Broad-phase: reject if vertex is outside padded edge AABB
if (!eb.box.contains(v.getX(), v.getY(), v.getZ())) {
continue;
}
// Narrow-phase: exact segment-point distance
if (seg.getDistance(v) < epsilon) {
CheckError err = new PointTouchesEdgeError(lr, eb.edge, v);
CheckResult cr = new CheckResult(this, ResultStatus.ERROR, err);
lr.addCheckResult(cr);
return true;
}
}
return false;
}
private List<Edge> getEdgesForRing(LinearRing lr) {
List<Edge> edges = new ArrayList<>();
Geometry geom = lr.getParent().getParent();
for (int i = 0; i < lr.getVertices().size() - 1; i++) {
Vertex v1 = lr.getVertices().get(i);
Vertex v2 = lr.getVertices().get(i + 1);
Edge e = geom.getEdge(v1, v2);
if (e == null) {
throw new IllegalStateException("Edge between v1=" + v1 + " to v2=" + v2 + " is missing");
}
edges.add(e);
}
return edges;
}
private static final class EdgeBox {
final Edge edge;
final AABB box;
EdgeBox(Edge edge, AABB box) {
this.edge = edge;
this.box = box;
}
}
@Override
public List<CheckId> getDependencies() {
return dependencies;
}
@Override
public Set<Requirement> appliesToRequirements() {
return CollectionUtils.singletonSet(Requirement.R_GE_R_SELF_INTERSECTION);
}
@Override
public RequirementType getType() {
return RequirementType.GEOMETRY;
}
@Override
public Check createNewInstance() {
return new RingSelfIntCheckAABB();
}
@Override
public CheckId getCheckId() {
return CheckId.C_GE_R_SELF_INTERSECTION;
}
}
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CityDoctor2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.checks.aabb;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import de.hft.stuttgart.citydoctor2.check.Check;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.Requirement;
import de.hft.stuttgart.citydoctor2.check.RequirementType;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.BoundingVolumeHierarchyTree;
/**
* Check for self intersecting solids
*
* @author Baris Numanoglu
*/
public class SolidSelfIntCheckAABB extends Check {
private static final List<CheckId> dependencies;
static {
ArrayList<CheckId> deps = new ArrayList<>();
dependencies = Collections.unmodifiableList(deps);
deps.add(CheckId.C_GE_R_TOO_FEW_POINTS);
deps.add(CheckId.C_GE_R_NOT_CLOSED);
deps.add(CheckId.C_GE_R_DUPLICATE_POINT);
deps.add(CheckId.C_GE_R_SELF_INTERSECTION);
deps.add(CheckId.C_GE_P_HOLE_OUTSIDE);
deps.add(CheckId.C_GE_P_INNER_RINGS_NESTED);
deps.add(CheckId.C_GE_P_INTERIOR_DISCONNECTED);
deps.add(CheckId.C_GE_P_INTERSECTING_RINGS);
deps.add(CheckId.C_GE_P_ORIENTATION_RINGS_SAME);
deps.add(CheckId.C_GE_P_NON_PLANAR);
deps.add(CheckId.C_GE_S_TOO_FEW_POLYGONS);
deps.add(CheckId.C_GE_S_MULTIPLE_CONNECTED_COMPONENTS);
deps.add(CheckId.C_GE_S_NON_MANIFOLD_EDGE);
deps.add(CheckId.C_GE_S_NON_MANIFOLD_VERTEX);
deps.add(CheckId.C_GE_S_POLYGON_WRONG_ORIENTATION);
}
@Override
public void check(Geometry g) {
if (g.getType() != GeometryType.SOLID && g.getType() != GeometryType.COMPOSITE_SURFACE) {
return;
}
// --- broad-phase via BVH (AABB tree) ---
List<Polygon> polys = g.getPolygons();
if (polys == null || polys.size() <= 1) {
g.addCheckResult(new CheckResult(this, ResultStatus.OK, null));
return;
}
CheckResult cr;
// Build BVH on polygons, but compute AABBs from the *original* polygons
BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.<Polygon>binaryBuilder()
.elements(polys)
.aabbFunction(p -> AABB.of(p.getOriginal()))
.build();
// TODO: comparison with older version without tree
List<PolygonIntersection> intersections =
SelfIntersectionUtil.calculateSolidSelfIntersection(g, 0.001, tree);
if (intersections.isEmpty()) {
cr = new CheckResult(this, ResultStatus.OK, null);
} else {
CheckError e = new SolidSelfIntError(g, intersections);
cr = new CheckResult(this, ResultStatus.ERROR, e);
}
g.addCheckResult(cr);
}
@Override
public List<CheckId> getDependencies() {
return dependencies;
}
@Override
public Set<Requirement> appliesToRequirements() {
return CollectionUtils.singletonSet(Requirement.R_GE_S_SELF_INTERSECTION);
}
@Override
public RequirementType getType() {
return RequirementType.GEOMETRY;
}
@Override
public Check createNewInstance() {
return new SolidSelfIntCheckAABB();
}
@Override
public CheckId getCheckId() {
return CheckId.C_GE_S_SELF_INTERSECTION;
}
}
...@@ -34,7 +34,6 @@ import de.hft.stuttgart.citydoctor2.check.RequirementType; ...@@ -34,7 +34,6 @@ import de.hft.stuttgart.citydoctor2.check.RequirementType;
import de.hft.stuttgart.citydoctor2.check.Requirement; import de.hft.stuttgart.citydoctor2.check.Requirement;
import de.hft.stuttgart.citydoctor2.check.ResultStatus; import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.NestedRingError; import de.hft.stuttgart.citydoctor2.check.error.NestedRingError;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils; import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
...@@ -66,7 +65,7 @@ public class NestedRingsCheck extends Check { ...@@ -66,7 +65,7 @@ public class NestedRingsCheck extends Check {
} }
public enum Variant { public enum Variant {
AUTO, AUTO(SplitStrategy.OCTONARY_OBJECT_MEAN),
OLD, OLD,
AABB_FILTER, AABB_FILTER,
BVH_BINARY_OBJECT_MEDIAN(SplitStrategy.BINARY_OBJECT_MEDIAN), BVH_BINARY_OBJECT_MEDIAN(SplitStrategy.BINARY_OBJECT_MEDIAN),
...@@ -136,34 +135,7 @@ public class NestedRingsCheck extends Check { ...@@ -136,34 +135,7 @@ public class NestedRingsCheck extends Check {
} else if (variant.isBvh()) { } else if (variant.isBvh()) {
checkWithBvhFilter(p, variant.getSplitStrategy()); checkWithBvhFilter(p, variant.getSplitStrategy());
} else { } else {
checkAuto(p); throw new IllegalStateException("Unsupported nested rings variant: " + variant);
}
}
private void checkAuto(Polygon p) {
List<LinearRing> innerRings = p.getInnerRings();
if (innerRings.size() < 2) {
checkOriginal(p);
return;
}
Map<LinearRing, AABB> ringBoxes = new HashMap<>(innerRings.size());
List<AABB> boxes = new ArrayList<>(innerRings.size());
for (LinearRing ring : innerRings) {
AABB box = AABB.of(ring);
ringBoxes.put(ring, box);
boxes.add(box);
}
BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofAabbs(boxes);
if (BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.NESTED_RINGS, summary)) {
SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy(
BvhUsagePolicy.BvhCheckType.NESTED_RINGS, summary);
checkWithBvhFilter(p, splitStrategy, ringBoxes);
} else if (innerRings.size() > 3) {
checkWithBoundingBoxFilter(p);
} else {
checkOriginal(p);
} }
} }
......
...@@ -37,7 +37,6 @@ import de.hft.stuttgart.citydoctor2.check.error.DegeneratedRingError; ...@@ -37,7 +37,6 @@ import de.hft.stuttgart.citydoctor2.check.error.DegeneratedRingError;
import de.hft.stuttgart.citydoctor2.check.error.PointTouchesEdgeError; import de.hft.stuttgart.citydoctor2.check.error.PointTouchesEdgeError;
import de.hft.stuttgart.citydoctor2.check.error.RingEdgeIntersectionError; import de.hft.stuttgart.citydoctor2.check.error.RingEdgeIntersectionError;
import de.hft.stuttgart.citydoctor2.checks.Checks; import de.hft.stuttgart.citydoctor2.checks.Checks;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils; import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.datastructure.Edge; import de.hft.stuttgart.citydoctor2.datastructure.Edge;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
...@@ -91,7 +90,7 @@ public class RingSelfIntCheck extends Check { ...@@ -91,7 +90,7 @@ public class RingSelfIntCheck extends Check {
} }
public enum Variant { public enum Variant {
AUTO, AUTO(SplitStrategy.OCTONARY_OBJECT_MEAN),
OLD, OLD,
BVH_BINARY_OBJECT_MEDIAN(SplitStrategy.BINARY_OBJECT_MEDIAN), BVH_BINARY_OBJECT_MEDIAN(SplitStrategy.BINARY_OBJECT_MEDIAN),
BVH_BINARY_OBJECT_MEAN(SplitStrategy.BINARY_OBJECT_MEAN), BVH_BINARY_OBJECT_MEAN(SplitStrategy.BINARY_OBJECT_MEAN),
...@@ -156,18 +155,11 @@ public class RingSelfIntCheck extends Check { ...@@ -156,18 +155,11 @@ public class RingSelfIntCheck extends Check {
checkRingOld(lr); checkRingOld(lr);
return; return;
} }
if (variant == Variant.AUTO) { if (variant.isBvh()) {
int edgeCount = Math.max(0, lr.getVertices().size() - 1); checkRingBvh(lr, variant.getSplitStrategy());
if (BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, edgeCount)) {
SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy(
BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, edgeCount);
checkRingBvh(lr, splitStrategy);
} else {
checkRingOld(lr);
}
return; return;
} }
checkRingBvh(lr, variant.getSplitStrategy()); throw new IllegalStateException("Unsupported ring self-intersection variant: " + variant);
} }
/** /**
......
...@@ -32,12 +32,10 @@ import de.hft.stuttgart.citydoctor2.check.Requirement; ...@@ -32,12 +32,10 @@ import de.hft.stuttgart.citydoctor2.check.Requirement;
import de.hft.stuttgart.citydoctor2.check.RequirementType; import de.hft.stuttgart.citydoctor2.check.RequirementType;
import de.hft.stuttgart.citydoctor2.check.ResultStatus; import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError; import de.hft.stuttgart.citydoctor2.check.error.SolidSelfIntError;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils; import de.hft.stuttgart.citydoctor2.checks.util.CollectionUtils;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil; import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType; import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.BoundingVolumeHierarchyTree; import de.hft.stuttgart.citydoctor2.datastructure.aabb.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy; import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
...@@ -57,7 +55,7 @@ public class SolidSelfIntCheck extends Check { ...@@ -57,7 +55,7 @@ public class SolidSelfIntCheck extends Check {
private Variant variant = Variant.OLD; private Variant variant = Variant.OLD;
public enum Variant { public enum Variant {
AUTO, AUTO(SplitStrategy.OCTONARY_OBJECT_MEAN),
OLD, OLD,
BVH_BINARY_OBJECT_MEDIAN(SplitStrategy.BINARY_OBJECT_MEDIAN), BVH_BINARY_OBJECT_MEDIAN(SplitStrategy.BINARY_OBJECT_MEDIAN),
BVH_BINARY_OBJECT_MEAN(SplitStrategy.BINARY_OBJECT_MEAN), BVH_BINARY_OBJECT_MEAN(SplitStrategy.BINARY_OBJECT_MEAN),
...@@ -154,20 +152,14 @@ public class SolidSelfIntCheck extends Check { ...@@ -154,20 +152,14 @@ public class SolidSelfIntCheck extends Check {
if (variant.isBvh()) { if (variant.isBvh()) {
return calculateIntersectionsWithTree(g, variant.getSplitStrategy()); return calculateIntersectionsWithTree(g, variant.getSplitStrategy());
} }
throw new IllegalStateException("Unsupported solid self-intersection variant: " + variant);
BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofElements(g.getPolygons(), AABB::of);
if (!BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary)) {
return SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta);
}
SplitStrategy splitStrategy = BvhUsagePolicy.chooseSplitStrategy(
BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary);
return calculateIntersectionsWithTree(g, splitStrategy);
} }
private List<PolygonIntersection> calculateIntersectionsWithTree(Geometry g, SplitStrategy splitStrategy) { private List<PolygonIntersection> calculateIntersectionsWithTree(Geometry g, SplitStrategy splitStrategy) {
BoundingVolumeHierarchyTree.Builder<Integer> treeConfig = BoundingVolumeHierarchyTree.<Integer>builder() BoundingVolumeHierarchyTree.Builder<Integer> treeConfig = isBinary(splitStrategy)
.degree(isBinary(splitStrategy) ? 2 : 8) ? BoundingVolumeHierarchyTree.<Integer>binaryBuilder()
.splitStrategy(splitStrategy); : BoundingVolumeHierarchyTree.<Integer>octonaryBuilder();
treeConfig.splitStrategy(splitStrategy);
return SelfIntersectionUtil.calculateSolidSelfIntersectionWithTree(g, delta, treeConfig); return SelfIntersectionUtil.calculateSolidSelfIntersectionWithTree(g, delta, treeConfig);
} }
......
package de.hft.stuttgart.citydoctor2.checks.util;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
/**
* Central policy for cheap BVH decisions in geometry checks.
*/
public final class BvhUsagePolicy {
private static final int SOLID_SELF_INTERSECTION_TREE_THRESHOLD = 16;
private static final int NESTED_RINGS_TREE_THRESHOLD = 8;
private static final int RING_SELF_INTERSECTION_TREE_THRESHOLD = 32;
private static final double THIN_BOX_ASPECT_RATIO = 20.0;
private static final double DEGENERATE_TOLERANCE = 1e-12;
private BvhUsagePolicy() {
}
public enum BvhCheckType {
SOLID_SELF_INTERSECTION,
NESTED_RINGS,
RING_SELF_INTERSECTION
}
public static final class BvhInputSummary {
private final int elementCount;
private final double extentX;
private final double extentY;
private final double extentZ;
private final double flatnessRatio;
private final double averageRelativeBoxExtent;
private final double thinBoxRatio;
private final double averageRelativeBoxVolume;
private final double centerSpreadRatio;
private BvhInputSummary(
int elementCount,
double extentX,
double extentY,
double extentZ,
double flatnessRatio,
double averageRelativeBoxExtent,
double thinBoxRatio,
double averageRelativeBoxVolume,
double centerSpreadRatio) {
this.elementCount = elementCount;
this.extentX = extentX;
this.extentY = extentY;
this.extentZ = extentZ;
this.flatnessRatio = flatnessRatio;
this.averageRelativeBoxExtent = averageRelativeBoxExtent;
this.thinBoxRatio = thinBoxRatio;
this.averageRelativeBoxVolume = averageRelativeBoxVolume;
this.centerSpreadRatio = centerSpreadRatio;
}
public static BvhInputSummary ofAabbs(List<AABB> boxes) {
Objects.requireNonNull(boxes, "boxes");
if (boxes.isEmpty()) {
return empty();
}
AABB totalBox = AABB.enclosing(boxes);
double extentX = totalBox.getExtentX();
double extentY = totalBox.getExtentY();
double extentZ = totalBox.getExtentZ();
double relativeExtentSum = 0.0;
double relativeVolumeSum = 0.0;
double centerXSum = 0.0;
double centerYSum = 0.0;
double centerZSum = 0.0;
int thinBoxCount = 0;
for (AABB box : boxes) {
relativeExtentSum += relativeExtent(box.getExtentX(), extentX);
relativeExtentSum += relativeExtent(box.getExtentY(), extentY);
relativeExtentSum += relativeExtent(box.getExtentZ(), extentZ);
relativeVolumeSum += relativeVolume(box, totalBox);
if (aspectRatio(box) >= THIN_BOX_ASPECT_RATIO) {
thinBoxCount++;
}
centerXSum += box.getCenterX();
centerYSum += box.getCenterY();
centerZSum += box.getCenterZ();
}
double maxExtent = totalBox.getLongestExtent();
double minExtent = totalBox.getShortestExtent();
double flatnessRatio = maxExtent == 0.0 ? 0.0 : minExtent / maxExtent;
double averageRelativeBoxExtent = relativeExtentSum / (boxes.size() * 3.0);
double thinBoxRatio = (double) thinBoxCount / boxes.size();
double averageRelativeBoxVolume = relativeVolumeSum / boxes.size();
double centerSpreadRatio = centerSpreadRatio(
boxes,
centerXSum / boxes.size(),
centerYSum / boxes.size(),
centerZSum / boxes.size(),
totalBox);
return new BvhInputSummary(
boxes.size(),
extentX,
extentY,
extentZ,
flatnessRatio,
averageRelativeBoxExtent,
thinBoxRatio,
averageRelativeBoxVolume,
centerSpreadRatio);
}
public static <E> BvhInputSummary ofElements(List<E> elements, Function<E, AABB> aabbFunction) {
Objects.requireNonNull(elements, "elements");
Objects.requireNonNull(aabbFunction, "aabbFunction");
return ofAabbs(elements.stream().map(aabbFunction).toList());
}
public static BvhInputSummary countOnly(int elementCount) {
return new BvhInputSummary(elementCount, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0);
}
private static BvhInputSummary empty() {
return countOnly(0);
}
private static double relativeExtent(double localExtent, double totalExtent) {
return totalExtent == 0.0 ? 0.0 : localExtent / totalExtent;
}
private static double relativeVolume(AABB box, AABB totalBox) {
double localMeasure = 1.0;
double totalMeasure = 1.0;
boolean hasActiveExtent = false;
if (box.getExtentX() > DEGENERATE_TOLERANCE) {
localMeasure *= box.getExtentX();
totalMeasure *= totalBox.getExtentX();
hasActiveExtent = true;
}
if (box.getExtentY() > DEGENERATE_TOLERANCE) {
localMeasure *= box.getExtentY();
totalMeasure *= totalBox.getExtentY();
hasActiveExtent = true;
}
if (box.getExtentZ() > DEGENERATE_TOLERANCE) {
localMeasure *= box.getExtentZ();
totalMeasure *= totalBox.getExtentZ();
hasActiveExtent = true;
}
if (!hasActiveExtent || totalMeasure <= DEGENERATE_TOLERANCE) {
return 0.0;
}
return localMeasure / totalMeasure;
}
private static double aspectRatio(AABB box) {
double longest = box.getLongestExtent();
double shortest = shortestPositiveExtent(box);
return shortest == 0.0 ? Double.POSITIVE_INFINITY : longest / shortest;
}
private static double shortestPositiveExtent(AABB box) {
double shortest = Double.POSITIVE_INFINITY;
if (box.getExtentX() > 0.0) {
shortest = Math.min(shortest, box.getExtentX());
}
if (box.getExtentY() > 0.0) {
shortest = Math.min(shortest, box.getExtentY());
}
if (box.getExtentZ() > 0.0) {
shortest = Math.min(shortest, box.getExtentZ());
}
return shortest == Double.POSITIVE_INFINITY ? 0.0 : shortest;
}
private static double centerSpreadRatio(
List<AABB> boxes,
double meanCenterX,
double meanCenterY,
double meanCenterZ,
AABB totalBox) {
double varianceX = 0.0;
double varianceY = 0.0;
double varianceZ = 0.0;
for (AABB box : boxes) {
varianceX += square(box.getCenterX() - meanCenterX);
varianceY += square(box.getCenterY() - meanCenterY);
varianceZ += square(box.getCenterZ() - meanCenterZ);
}
double spreadX = normalizedStdDev(varianceX, boxes.size(), totalBox.getExtentX());
double spreadY = normalizedStdDev(varianceY, boxes.size(), totalBox.getExtentY());
double spreadZ = normalizedStdDev(varianceZ, boxes.size(), totalBox.getExtentZ());
return (spreadX + spreadY + spreadZ) / 3.0;
}
private static double normalizedStdDev(double varianceSum, int count, double extent) {
if (extent == 0.0) {
return 0.0;
}
return Math.sqrt(varianceSum / count) / extent;
}
private static double square(double value) {
return value * value;
}
public int getElementCount() {
return elementCount;
}
public double getExtentX() {
return extentX;
}
public double getExtentY() {
return extentY;
}
public double getExtentZ() {
return extentZ;
}
public double getFlatnessRatio() {
return flatnessRatio;
}
public double getAverageRelativeBoxExtent() {
return averageRelativeBoxExtent;
}
public double getThinBoxRatio() {
return thinBoxRatio;
}
public double getAverageRelativeBoxVolume() {
return averageRelativeBoxVolume;
}
public double getCenterSpreadRatio() {
return centerSpreadRatio;
}
}
public static boolean shouldUseTree(BvhCheckType checkType, int elementCount) {
return shouldUseTree(checkType, BvhInputSummary.countOnly(elementCount));
}
public static boolean shouldUseTree(BvhCheckType checkType, BvhInputSummary summary) {
int elementCount = summary.getElementCount();
if (elementCount < 20) {
return false;
}
if (elementCount < thresholdFor(checkType)) {
return false;
}
if (summary.getAverageRelativeBoxExtent() > 0.65 && elementCount < 128) {
return false;
}
return true;
}
public static SplitStrategy chooseSplitStrategy(BvhCheckType checkType, int elementCount) {
return chooseSplitStrategy(checkType, BvhInputSummary.countOnly(elementCount));
}
public static SplitStrategy chooseSplitStrategy(BvhCheckType checkType, BvhInputSummary summary) {
if (checkType == BvhCheckType.RING_SELF_INTERSECTION) {
return SplitStrategy.BINARY_SPATIAL_MEDIAN;
}
if (summary.getFlatnessRatio() < 0.02) {
return SplitStrategy.BINARY_SPATIAL_MEDIAN;
}
if (summary.getElementCount() >= 128) {
return SplitStrategy.OCTONARY_OBJECT_MEAN;
}
return SplitStrategy.BINARY_SPATIAL_MEDIAN;
}
private static int thresholdFor(BvhCheckType checkType) {
switch (checkType) {
case SOLID_SELF_INTERSECTION:
return SOLID_SELF_INTERSECTION_TREE_THRESHOLD;
case NESTED_RINGS:
return NESTED_RINGS_TREE_THRESHOLD;
case RING_SELF_INTERSECTION:
return RING_SELF_INTERSECTION_TREE_THRESHOLD;
default:
throw new IllegalArgumentException("Unsupported BVH check type: " + checkType);
}
}
}
...@@ -184,7 +184,7 @@ public class SelfIntersectionUtil { ...@@ -184,7 +184,7 @@ public class SelfIntersectionUtil {
return calculateSolidSelfIntersectionWithTree( return calculateSolidSelfIntersectionWithTree(
g, g,
delta, delta,
BoundingVolumeHierarchyTree.<Integer>binaryBuilder() BoundingVolumeHierarchyTree.<Integer>octonaryBuilder()
); );
} }
......
package de.hft.stuttgart.citydoctor2.checks.aabb.core;
import de.hft.stuttgart.citydoctor2.checks.aabb.fixtures.SyntheticCityGmlLikeGeometryFactory;
import de.hft.stuttgart.citydoctor2.checks.aabb.fixtures.SyntheticRingGeometryFactory;
import de.hft.stuttgart.citydoctor2.checks.aabb.fixtures.SyntheticSolidGeometryFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.checks.util.BvhUsagePolicy;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
/**
* Covers the current tree-usage policy with small, readable synthetic inputs.
*
* These tests are not meant to prove an optimal heuristic. They guard the
* conservative decisions that are currently wired into BvhUsagePolicy.
*
* @author Numanoglu
*/
public class BvhUsagePolicyTest {
@Test
public void smallInputsStayWithoutTree() {
Geometry smallSolid = SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 1, 1);
BvhUsagePolicy.BvhInputSummary summary = summaryForPolygons(smallSolid);
assertFalse(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
assertFalse(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.NESTED_RINGS, 4));
assertFalse(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, 16));
}
@Test
public void largeDistributedInputsUseOctonaryObjectMean() {
Geometry cityLikeGeometry = SyntheticCityGmlLikeGeometryFactory.variedRoofDistrict(Lod.LOD2, 180);
BvhUsagePolicy.BvhInputSummary summary = summaryForPolygons(cityLikeGeometry);
assertTrue(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
assertEquals(SplitStrategy.OCTONARY_OBJECT_MEAN,
BvhUsagePolicy.chooseSplitStrategy(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
}
@Test
public void flatInputsUseBinarySpatialMedian() {
Geometry flatGeometry = SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 200);
BvhUsagePolicy.BvhInputSummary summary = summaryForPolygons(flatGeometry);
assertTrue(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
assertEquals(SplitStrategy.BINARY_SPATIAL_MEDIAN,
BvhUsagePolicy.chooseSplitStrategy(BvhUsagePolicy.BvhCheckType.SOLID_SELF_INTERSECTION, summary));
}
@Test
public void ringSelfIntersectionUsesBinarySpatialMedian() {
Geometry ringGeometry = SyntheticRingGeometryFactory.performanceRingGeometry(200, 100, 0.001);
BvhUsagePolicy.BvhInputSummary summary = BvhUsagePolicy.BvhInputSummary.ofAabbs(edgeBoxes(ringGeometry));
assertTrue(BvhUsagePolicy.shouldUseTree(BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, summary));
assertEquals(SplitStrategy.BINARY_SPATIAL_MEDIAN,
BvhUsagePolicy.chooseSplitStrategy(BvhUsagePolicy.BvhCheckType.RING_SELF_INTERSECTION, summary));
}
@Test
public void summaryExposesThinVolumeAndSpreadMetrics() {
Geometry slabGeometry = SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 160);
BvhUsagePolicy.BvhInputSummary summary = summaryForPolygons(slabGeometry);
assertTrue("Expected long-thin fixture to contain thin boxes", summary.getThinBoxRatio() > 0.0);
assertTrue("Relative box volume must be non-negative", summary.getAverageRelativeBoxVolume() >= 0.0);
assertTrue("Expected distributed slab centers", summary.getCenterSpreadRatio() > 0.0);
}
private static BvhUsagePolicy.BvhInputSummary summaryForPolygons(Geometry geometry) {
// Production code computes the same cheap summary from each element's AABB.
return BvhUsagePolicy.BvhInputSummary.ofElements(geometry.getPolygons(), polygon -> AABB.of(polygon.getOriginal()));
}
private static List<AABB> edgeBoxes(Geometry geometry) {
// Ring self-intersection uses segment boxes rather than whole polygon boxes.
List<AABB> boxes = new ArrayList<>();
for (Polygon polygon : geometry.getPolygons()) {
LinearRing ring = polygon.getExteriorRing();
List<Vertex> vertices = ring.getVertices();
for (int i = 0; i < vertices.size() - 1; i++) {
boxes.add(AABB.of(vertices.get(i), vertices.get(i + 1), 0.001));
}
}
return boxes;
}
}
package de.hft.stuttgart.citydoctor2.checks.aabb.core;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import java.util.Arrays;
import java.util.List;
import de.hft.stuttgart.citydoctor2.checks.aabb.NestedRingsCheckAABB;
import de.hft.stuttgart.citydoctor2.checks.aabb.RingSelfIntCheckAABB;
import de.hft.stuttgart.citydoctor2.checks.aabb.SolidSelfIntCheckAABB;
/**
* Manual smoke runner for the older AABB check classes.
*
* The class is intentionally simple and prints its result to the console. It is
* useful when the checks are inspected locally, while systematic assertions live
* in the dedicated correctness tests.
*
* @author Numanoglu
*/
public class GeometryChecksWithAABBTest {
public static void main(String[] args) {
System.out.println("=== Geometry Checks with AABB - Manual Test Runner ===");
//testTooFewPoints();
testNestedRings();
testRingSelfIntersection();
testSolidSelfIntersection();
System.out.println("\n=== Test run finished ===");
}
// --------------------------------------------------------------
// NestedRingsCheck
// --------------------------------------------------------------
private static void testNestedRings() {
System.out.println("\n[TEST] NestedRingsCheck");
// outer ring
LinearRing outer = new LinearRing(LinearRing.LinearRingType.EXTERIOR);
outer.addAllVertices(Arrays.asList(
new Vertex(0,0,0),
new Vertex(1,0,0),
new Vertex(1,1,0),
new Vertex(0,1,0),
new Vertex(0,0,0)
));
ConcretePolygon p = new ConcretePolygon();
p.setExteriorRing(outer);
// inner ring 1
LinearRing inner1 = new LinearRing(LinearRing.LinearRingType.INTERIOR);
inner1.addAllVertices(Arrays.asList(
new Vertex(0.2,0.2,0),
new Vertex(0.8,0.2,0),
new Vertex(0.8,0.8,0),
new Vertex(0.2,0.8,0),
new Vertex(0.2,0.2,0)
));
// inner ring 2 inside inner ring 1
LinearRing inner2 = new LinearRing(LinearRing.LinearRingType.INTERIOR);
inner2.addAllVertices(Arrays.asList(
new Vertex(0.4,0.4,0),
new Vertex(0.6,0.4,0),
new Vertex(0.6,0.6,0),
new Vertex(0.4,0.6,0),
new Vertex(0.4,0.4,0)
));
p.addInteriorRing(inner1);
p.addInteriorRing(inner2);
NestedRingsCheckAABB check = new NestedRingsCheckAABB();
check.check(p);
System.out.println(" - Should detect nested rings: " + p.containsAnyError());
}
// --------------------------------------------------------------
// RingSelfIntCheck
// --------------------------------------------------------------
private static void testRingSelfIntersection() {
System.out.println("\n[TEST] RingSelfIntCheck");
LinearRing ring = new LinearRing(LinearRing.LinearRingType.EXTERIOR);
ring.addAllVertices(Arrays.asList(
new Vertex(0,0,0),
new Vertex(1,1,0),
new Vertex(1,0,0),
new Vertex(0,1,0),
new Vertex(0,0,0)
));
ConcretePolygon p = new ConcretePolygon();
p.setExteriorRing(ring);
// Orientation is not the focus here; the geometry mainly provides parent links.
Geometry g = new Geometry(GeometryType.SOLID, Lod.LOD2, Geometry.Orientation.OUTWARD);
g.addPolygon(p); // ensures parent links exist
g.updateEdgesAndVertices();
RingSelfIntCheckAABB check = new RingSelfIntCheckAABB();
check.check(ring);
System.out.println(" - Should detect self-intersection: " + ring.containsAnyError());
}
// --------------------------------------------------------------
// SolidSelfIntCheck
// --------------------------------------------------------------
private static void testSolidSelfIntersection() {
System.out.println("\n[TEST] SolidSelfIntCheck");
// first triangle
ConcretePolygon p1 = new ConcretePolygon();
LinearRing r1 = new LinearRing(LinearRing.LinearRingType.EXTERIOR);
r1.addAllVertices(Arrays.asList(
new Vertex(0,0,0),
new Vertex(1,0,0),
new Vertex(1,1,0),
new Vertex(0,0,0)
));
p1.setExteriorRing(r1);
// second triangle overlapping the first
ConcretePolygon p2 = new ConcretePolygon();
LinearRing r2 = new LinearRing(LinearRing.LinearRingType.EXTERIOR);
r2.addAllVertices(Arrays.asList(
new Vertex(0,0,0),
new Vertex(1,1,0),
new Vertex(0,1,0),
new Vertex(0,0,0)
));
p2.setExteriorRing(r2);
Geometry solid = new Geometry(GeometryType.SOLID, Lod.LOD2, Geometry.Orientation.OUTWARD);
solid.getPolygons().addAll(List.of(p1, p2));
SolidSelfIntCheckAABB check = new SolidSelfIntCheckAABB();
check.check(solid);
System.out.println(" - Should detect solid self-intersection: " + solid.containsAnyError());
}
}
...@@ -44,17 +44,11 @@ public class RingSelfIntCheckBvhVariantCityGmlTest { ...@@ -44,17 +44,11 @@ public class RingSelfIntCheckBvhVariantCityGmlTest {
throws CityGmlParseException, InvalidGmlFileException { throws CityGmlParseException, InvalidGmlFileException {
Geometry geometryOld = parseGeometry(TEST_GML); Geometry geometryOld = parseGeometry(TEST_GML);
long start = System.nanoTime();
int oldCount = runCheckAndCountErrors(geometryOld, RingSelfIntCheck.Variant.OLD); int oldCount = runCheckAndCountErrors(geometryOld, RingSelfIntCheck.Variant.OLD);
long oldTime = System.nanoTime() - start;
System.out.println("RingSelfIntCheck OLD count=" + oldCount + " time(ns)=" + oldTime);
for (RingSelfIntCheck.Variant bvhVariant : bvhVariants()) { for (RingSelfIntCheck.Variant bvhVariant : bvhVariants()) {
Geometry geometryBvh = parseGeometry(TEST_GML); Geometry geometryBvh = parseGeometry(TEST_GML);
start = System.nanoTime();
int bvhCount = runCheckAndCountErrors(geometryBvh, bvhVariant); int bvhCount = runCheckAndCountErrors(geometryBvh, bvhVariant);
long bvhTime = System.nanoTime() - start;
System.out.println("RingSelfIntCheck " + bvhVariant + " count=" + bvhCount + " time(ns)=" + bvhTime);
assertEquals("OLD vs " + bvhVariant + " differs", oldCount, bvhCount); assertEquals("OLD vs " + bvhVariant + " differs", oldCount, bvhCount);
} }
} }
......
...@@ -57,8 +57,6 @@ public class SolidSelfIntersectionBuildingTest { ...@@ -57,8 +57,6 @@ public class SolidSelfIntersectionBuildingTest {
List<Polygon> candidates = tree.getAllIntersectingElements(probeAabb); List<Polygon> candidates = tree.getAllIntersectingElements(probeAabb);
int nonEmptyQueries = 0; int nonEmptyQueries = 0;
int totalCandidates = 0;
int maxCandidates = 0;
for (Polygon polygon : polygons) { for (Polygon polygon : polygons) {
AABB query = AABB.of(polygon.getOriginal()); AABB query = AABB.of(polygon.getOriginal());
assertNotNull("Query AABB must not be null for " + lodLabel, query); assertNotNull("Query AABB must not be null for " + lodLabel, query);
...@@ -66,20 +64,9 @@ public class SolidSelfIntersectionBuildingTest { ...@@ -66,20 +64,9 @@ public class SolidSelfIntersectionBuildingTest {
if (!perPolygonCandidates.isEmpty()) { if (!perPolygonCandidates.isEmpty()) {
nonEmptyQueries++; nonEmptyQueries++;
} }
int currentSize = perPolygonCandidates.size();
totalCandidates += currentSize;
if (currentSize > maxCandidates) {
maxCandidates = currentSize;
}
} }
printBvhStats( assertFalse("Expected the probe polygon to return BVH candidates on " + lodLabel, candidates.isEmpty());
lodLabel,
polygons.size(),
candidates.size(),
nonEmptyQueries,
totalCandidates,
maxCandidates);
assertTrue( assertTrue(
"Expected at least one non-empty BVH query on " + lodLabel, "Expected at least one non-empty BVH query on " + lodLabel,
nonEmptyQueries > 0); nonEmptyQueries > 0);
...@@ -102,7 +89,6 @@ public class SolidSelfIntersectionBuildingTest { ...@@ -102,7 +89,6 @@ public class SolidSelfIntersectionBuildingTest {
List<PolygonIntersection> newRes = SelfIntersectionUtil.calculateSolidSelfIntersectionWithTree(geometry, DELTA); List<PolygonIntersection> newRes = SelfIntersectionUtil.calculateSolidSelfIntersectionWithTree(geometry, DELTA);
assertNotNull("New result list must not be null for " + lodLabel, newRes); assertNotNull("New result list must not be null for " + lodLabel, newRes);
printComparisonStats(lodLabel, polygons.size(), oldRes.size(), oldTreeRes.size(), newRes.size());
assertEquals("Old vs external-tree differs for " + lodLabel, oldRes.size(), oldTreeRes.size()); assertEquals("Old vs external-tree differs for " + lodLabel, oldRes.size(), oldTreeRes.size());
assertEquals("Old vs new-tree differs for " + lodLabel, oldRes.size(), newRes.size()); assertEquals("Old vs new-tree differs for " + lodLabel, oldRes.size(), newRes.size());
} }
...@@ -118,39 +104,6 @@ public class SolidSelfIntersectionBuildingTest { ...@@ -118,39 +104,6 @@ public class SolidSelfIntersectionBuildingTest {
return polygons; return polygons;
} }
private void printBvhStats(
String lodLabel,
int polygonCount,
int probeCandidateCount,
int nonEmptyQueries,
int totalCandidates,
int maxCandidates) {
System.out.printf(
"[BVH][%s] polygons=%d, probeCandidates=%d, nonEmptyQueries=%d/%d, totalCandidates=%d, maxPerQuery=%d%n",
lodLabel,
polygonCount,
probeCandidateCount,
nonEmptyQueries,
polygonCount,
totalCandidates,
maxCandidates);
}
private void printComparisonStats(
String lodLabel,
int polygonCount,
int oldCount,
int oldTreeCount,
int newCount) {
System.out.printf(
"[SelfInt][%s] polygons=%d, old=%d, old+tree=%d, new=%d%n",
lodLabel,
polygonCount,
oldCount,
oldTreeCount,
newCount);
}
private Building createDenseBuildingWithoutIntersections() { private Building createDenseBuildingWithoutIntersections() {
Building b = new Building(); Building b = new Building();
b.addGeometry(createGridGeometry(Lod.LOD2, 4, 4, false)); b.addGeometry(createGridGeometry(Lod.LOD2, 4, 4, false));
......
...@@ -58,14 +58,9 @@ public class SolidSelfIntersectionOldVsNewTest { ...@@ -58,14 +58,9 @@ public class SolidSelfIntersectionOldVsNewTest {
assertTrue("Expected at least 2 polygons in: " + gmlPath, polys.size() > 1); assertTrue("Expected at least 2 polygons in: " + gmlPath, polys.size() > 1);
/// Without Tree /// Without Tree
long start = System.nanoTime();
List<PolygonIntersection> oldRes = SelfIntersectionUtil.calculateSolidSelfIntersection0(g, delta); List<PolygonIntersection> oldRes = SelfIntersectionUtil.calculateSolidSelfIntersection0(g, delta);
long dif = System.nanoTime() - start;
System.out.println("Alt: " + dif);
/// With IdentityHashMap /// With IdentityHashMap
start = System.nanoTime();
BoundingVolumeHierarchyTree<Polygon> polygonTree = BoundingVolumeHierarchyTree<Polygon> polygonTree =
BoundingVolumeHierarchyTree.newBinary( BoundingVolumeHierarchyTree.newBinary(
g.getPolygons(), g.getPolygons(),
...@@ -75,20 +70,10 @@ public class SolidSelfIntersectionOldVsNewTest { ...@@ -75,20 +70,10 @@ public class SolidSelfIntersectionOldVsNewTest {
List<PolygonIntersection> oldTreeRes = List<PolygonIntersection> oldTreeRes =
SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta, polygonTree); SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta, polygonTree);
dif = System.nanoTime() - start;
System.out.println("Alt + external polygon tree: " + dif);
/// With Polygon Indices /// With Polygon Indices
start = System.nanoTime();
List<PolygonIntersection> newRes = List<PolygonIntersection> newRes =
SelfIntersectionUtil.calculateSolidSelfIntersectionWithTree(g, delta); 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());
assertEquals("Old vs oldTree differs for: " + gmlPath, assertEquals("Old vs oldTree differs for: " + gmlPath,
oldRes.size(), oldTreeRes.size()); oldRes.size(), oldTreeRes.size());
......
package de.hft.stuttgart.citydoctor2.checks.aabb.fixtures;
import java.util.List;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
/**
* Central catalog of synthetic BVH exploration scenarios.
*
* The scenarios intentionally vary count, thinness, relative box volume, center
* spread, and city-like clustering so heuristic experiments can compare metric
* ranges without duplicating fixture lists in several tests.
*
* @author Numanoglu
*/
public final class BvhSyntheticScenarioCatalog {
private static final double EPSILON = 0.001;
private BvhSyntheticScenarioCatalog() {
}
public static List<SsiScenario> ssiScenarios() {
return List.of(
new SsiScenario("sep-grid-small",
SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 6, 6)),
new SsiScenario("sep-grid-medium",
SyntheticSolidGeometryFactory.separatedBoxGrid(Lod.LOD2, 12, 12)),
new SsiScenario("overlap-grid",
SyntheticSolidGeometryFactory.overlappingBoxGrid(Lod.LOD2, 12, 12)),
new SsiScenario("dense-clusters-small",
SyntheticSolidGeometryFactory.denseBoxClusters(Lod.LOD2, 4, 20)),
new SsiScenario("dense-clusters-large",
SyntheticSolidGeometryFactory.denseBoxClusters(Lod.LOD2, 10, 24)),
new SsiScenario("long-thin-slabs-small",
SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 80)),
new SsiScenario("long-thin-slabs-large",
SyntheticSolidGeometryFactory.longThinSlabs(Lod.LOD2, 240)),
new SsiScenario("flat-box-grid-small",
SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 80)),
new SsiScenario("flat-box-grid-large",
SyntheticSolidGeometryFactory.flatBoxGrid(Lod.LOD2, 240)),
new SsiScenario("thin-ratio-025",
SyntheticSolidGeometryFactory.thinRatioVariationBoxes(Lod.LOD2, 120, 0.25)),
new SsiScenario("thin-ratio-050",
SyntheticSolidGeometryFactory.thinRatioVariationBoxes(Lod.LOD2, 120, 0.50)),
new SsiScenario("thin-ratio-075",
SyntheticSolidGeometryFactory.thinRatioVariationBoxes(Lod.LOD2, 120, 0.75)),
new SsiScenario("aspect-005",
SyntheticSolidGeometryFactory.aspectRatioVariationBoxes(Lod.LOD2, 120, 5.0)),
new SsiScenario("aspect-020",
SyntheticSolidGeometryFactory.aspectRatioVariationBoxes(Lod.LOD2, 120, 20.0)),
new SsiScenario("aspect-080",
SyntheticSolidGeometryFactory.aspectRatioVariationBoxes(Lod.LOD2, 120, 80.0)),
new SsiScenario("spread-tight",
SyntheticSolidGeometryFactory.centerSpreadVariationBoxes(Lod.LOD2, 120, 3.5)),
new SsiScenario("spread-wide",
SyntheticSolidGeometryFactory.centerSpreadVariationBoxes(Lod.LOD2, 120, 20.0)),
new SsiScenario("relvol-compact",
SyntheticSolidGeometryFactory.relativeVolumeVariationBoxes(Lod.LOD2, 80, 5.0, 1.1)),
new SsiScenario("relvol-sparse",
SyntheticSolidGeometryFactory.relativeVolumeVariationBoxes(Lod.LOD2, 80, 5.0, 5.0)),
new SsiScenario("citylike-mixed-small",
SyntheticCityGmlLikeGeometryFactory.mixedUrbanDistrict(Lod.LOD2, 3, 3)),
new SsiScenario("citylike-mixed-large",
SyntheticCityGmlLikeGeometryFactory.mixedUrbanDistrict(Lod.LOD2, 6, 5)),
new SsiScenario("citylike-courtyard",
SyntheticCityGmlLikeGeometryFactory.courtyardDistrict(Lod.LOD2, 16)),
new SsiScenario("citylike-roofs-small",
SyntheticCityGmlLikeGeometryFactory.variedRoofDistrict(Lod.LOD2, 80)),
new SsiScenario("citylike-roofs-large",
SyntheticCityGmlLikeGeometryFactory.variedRoofDistrict(Lod.LOD2, 240)));
}
public static List<NestedScenario> nestedScenarios() {
return List.of(
new NestedScenario("disjoint-small",
SyntheticNestedRingGeometryFactory.manyDisjointInnerRings(120)),
new NestedScenario("disjoint-large",
SyntheticNestedRingGeometryFactory.manyDisjointInnerRings(1_000)),
new NestedScenario("one-pair-small",
SyntheticNestedRingGeometryFactory.oneNestedPairAmongMany(120)),
new NestedScenario("one-pair-large",
SyntheticNestedRingGeometryFactory.oneNestedPairAmongMany(1_000)),
new NestedScenario("concentric-small",
SyntheticNestedRingGeometryFactory.concentricNestedRings(80)),
new NestedScenario("concentric-large",
SyntheticNestedRingGeometryFactory.concentricNestedRings(300)),
new NestedScenario("overlap-no-error",
SyntheticNestedRingGeometryFactory.overlappingAabbsButNotNested(1_000)),
new NestedScenario("clustered-small",
SyntheticNestedRingGeometryFactory.clusteredInnerRings(8, 40)),
new NestedScenario("clustered-large",
SyntheticNestedRingGeometryFactory.clusteredInnerRings(20, 80)),
new NestedScenario("clustered-pair",
SyntheticNestedRingGeometryFactory.clusteredInnerRingsWithNestedPair(16, 80)),
new NestedScenario("aspect-005",
SyntheticNestedRingGeometryFactory.aspectRatioVariationInnerRings(180, 5.0)),
new NestedScenario("aspect-020",
SyntheticNestedRingGeometryFactory.aspectRatioVariationInnerRings(180, 20.0)),
new NestedScenario("aspect-080",
SyntheticNestedRingGeometryFactory.aspectRatioVariationInnerRings(180, 80.0)),
new NestedScenario("spread-tight",
SyntheticNestedRingGeometryFactory.centerSpreadVariationInnerRings(240, 2.0)),
new NestedScenario("spread-wide",
SyntheticNestedRingGeometryFactory.centerSpreadVariationInnerRings(240, 20.0)));
}
public static List<RsiScenario> rsiScenarios() {
return List.of(
new RsiScenario("rsi-small",
SyntheticRingGeometryFactory.performanceRingGeometry(200, 100, EPSILON)),
new RsiScenario("rsi-medium",
SyntheticRingGeometryFactory.performanceRingGeometry(1_000, 500, EPSILON)),
new RsiScenario("rsi-large",
SyntheticRingGeometryFactory.performanceRingGeometry(5_000, 2_000, EPSILON)),
new RsiScenario("rsi-convex-heavy",
SyntheticRingGeometryFactory.performanceRingGeometry(6_000, 200, EPSILON)),
new RsiScenario("rsi-zigzag-heavy",
SyntheticRingGeometryFactory.performanceRingGeometry(400, 4_000, EPSILON)),
new RsiScenario("rsi-aspect-005",
SyntheticRingGeometryFactory.aspectRatioVariationGeometry(240, 5.0)),
new RsiScenario("rsi-aspect-020",
SyntheticRingGeometryFactory.aspectRatioVariationGeometry(240, 20.0)),
new RsiScenario("rsi-aspect-080",
SyntheticRingGeometryFactory.aspectRatioVariationGeometry(240, 80.0)),
new RsiScenario("rsi-spread-tight",
SyntheticRingGeometryFactory.centerSpreadVariationGeometry(240, 4.0)),
new RsiScenario("rsi-spread-wide",
SyntheticRingGeometryFactory.centerSpreadVariationGeometry(240, 30.0)));
}
public static final class SsiScenario {
public final String name;
public final Geometry geometry;
SsiScenario(String name, Geometry geometry) {
this.name = name;
this.geometry = geometry;
}
}
public static final class NestedScenario {
public final String name;
public final ConcretePolygon polygon;
NestedScenario(String name, ConcretePolygon polygon) {
this.name = name;
this.polygon = polygon;
}
}
public static final class RsiScenario {
public final String name;
public final Geometry geometry;
RsiScenario(String name, Geometry geometry) {
this.name = name;
this.geometry = geometry;
}
}
}
...@@ -10,8 +10,7 @@ import de.hft.stuttgart.citydoctor2.datastructure.Lod; ...@@ -10,8 +10,7 @@ import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex; import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
/** /**
* Synthetic but citygml-like geometry fixtures for BVH metric and performance * Synthetic but citygml-like geometry fixtures for BVH comparison tests.
* experiments.
* *
* The goal is not to serialize valid CityGML, but to mimic typical model * The goal is not to serialize valid CityGML, but to mimic typical model
* structure more closely than pure grids: mixed building footprints, varying * structure more closely than pure grids: mixed building footprints, varying
......
package de.hft.stuttgart.citydoctor2.checks.aabb.fixtures; package de.hft.stuttgart.citydoctor2.checks.aabb.fixtures;
import java.io.File;
import org.citygml4j.core.model.CityGMLVersion;
import org.citygml4j.core.model.core.CityModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.database.UnconnectedCache;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon; import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation; import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType; import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType; import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.datastructure.Lod; import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex; import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
...@@ -35,10 +23,10 @@ import de.hft.stuttgart.citydoctor2.datastructure.Vertex; ...@@ -35,10 +23,10 @@ import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
* vertex close to a non-adjacent edge, useful for epsilon-sensitive checks. * vertex close to a non-adjacent edge, useful for epsilon-sensitive checks.
* - largeConvexRing: * - largeConvexRing:
* many edges without self-intersection, useful for candidate-pruning cost. * many edges without self-intersection, useful for larger ring inputs.
* - zigZagCorridor: * - zigZagCorridor:
* many nearby but non-crossing segments, useful as AABB broad-phase stress. * many nearby but non-crossing segments, useful for AABB candidate filtering.
* *
* @author Numanoglu * @author Numanoglu
*/ */
...@@ -58,7 +46,7 @@ public final class SyntheticRingGeometryFactory { ...@@ -58,7 +46,7 @@ public final class SyntheticRingGeometryFactory {
return geometry; return geometry;
} }
public static Geometry performanceRingGeometry(int convexVertexCount, int zigZagSegments, double epsilon) { public static Geometry largeRingGeometry(int convexVertexCount, int zigZagSegments, double epsilon) {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD); Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
addRingPolygon(geometry, largeConvexRing(convexVertexCount, 0.0, 0.0, 0.0)); addRingPolygon(geometry, largeConvexRing(convexVertexCount, 0.0, 0.0, 0.0));
addRingPolygon(geometry, zigZagCorridor(zigZagSegments)); addRingPolygon(geometry, zigZagCorridor(zigZagSegments));
...@@ -68,29 +56,6 @@ public final class SyntheticRingGeometryFactory { ...@@ -68,29 +56,6 @@ public final class SyntheticRingGeometryFactory {
return geometry; return geometry;
} }
public static Geometry aspectRatioVariationGeometry(int ringCount, double aspectRatio) {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
double width = Math.max(1.0, aspectRatio);
for (int i = 0; i < ringCount; i++) {
double x = (i % 20) * (width + 4.0);
double y = (i / 20) * 5.0;
addRingPolygon(geometry, rectangle(x, y, width, 1.0));
}
geometry.updateEdgesAndVertices();
return geometry;
}
public static Geometry centerSpreadVariationGeometry(int ringCount, double spacing) {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
for (int i = 0; i < ringCount; i++) {
double x = (i % 20) * spacing;
double y = (i / 20) * spacing;
addRingPolygon(geometry, rectangle(x, y, 3.0, 3.0));
}
geometry.updateEdgesAndVertices();
return geometry;
}
private static void addRingPolygon(Geometry geometry, double[][] coordinates) { private static void addRingPolygon(Geometry geometry, double[][] coordinates) {
ConcretePolygon polygon = new ConcretePolygon(); ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR); LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
...@@ -185,21 +150,4 @@ public final class SyntheticRingGeometryFactory { ...@@ -185,21 +150,4 @@ public final class SyntheticRingGeometryFactory {
coordinates[index] = new double[] {60.0, 0.0, 0.0}; coordinates[index] = new double[] {60.0, 0.0, 0.0};
return coordinates; return coordinates;
} }
@Test
@Disabled
public void createCityGML() throws CityDoctorWriteException {
ParserConfiguration config = new ParserConfiguration(8, false);
CityDoctorModel model = new CityDoctorModel(config, new File(""), new UnconnectedCache());
model.setCityModel(new CityModel());
model.setParsedCityGMLVersion(CityGMLVersion.v2_0);
Building b = new Building();
b.setGmlObject(new org.citygml4j.core.model.building.Building());
b.getGeometries().add(performanceRingGeometry(1_000, 500, 0.0001));
model.addBuilding(b);
model.saveAs("output.gml", false);
}
} }
package de.hft.stuttgart.citydoctor2.checks.aabb.heuristics;
import de.hft.stuttgart.citydoctor2.checks.aabb.fixtures.BvhSyntheticScenarioCatalog;
import de.hft.stuttgart.citydoctor2.checks.aabb.support.BvhExplorationCsvWriter;
import de.hft.stuttgart.citydoctor2.checks.aabb.support.BvhHeuristicTimingSupport;
import de.hft.stuttgart.citydoctor2.checks.aabb.support.BvhInputMetricsCollector;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Disabled;
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.checks.geometry.RingSelfIntCheck;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
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.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.datastructure.aabb.SplitStrategy;
/**
* Collects compact timing observations and broad-phase metrics for BVH strategy
* selection from synthetic fixtures.
*
* The synthetic scenarios are the calibration side of the heuristic workflow:
* they deliberately vary shape and distribution properties so candidate rules
* can be proposed before they are checked against real CityGML data.
*
* @author Numanoglu
*/
@Tag("performance")
public class BvhStrategyHeuristicExplorationTest {
private static final double DELTA = 0.001;
private static final double EPSILON = 0.001;
private static final Path OUTPUT_DIRECTORY = Path.of("target", "bvh-exploration");
@Test
@Disabled
public void exploreSsiNestedAndRsiStrategyCandidates() throws IOException {
// Keep all three checks in one table so metric trends can be compared side by side.
List<BvhHeuristicTimingSupport.Observation> observations = new ArrayList<>();
addSsiObservations(observations);
addNestedObservations(observations);
addRsiObservations(observations);
printObservationSummary(observations);
List<BvhExplorationCsvWriter.BucketSummaryRecord> bucketSummary = printBucketSummary(observations);
BvhExplorationCsvWriter.writeObservations(
OUTPUT_DIRECTORY.resolve("synthetic_observations.csv"),
toSyntheticRecords(observations));
BvhExplorationCsvWriter.writeBucketSummary(
OUTPUT_DIRECTORY.resolve("synthetic_bucket_summary.csv"),
bucketSummary);
}
private static List<BvhExplorationCsvWriter.ObservationRecord> toSyntheticRecords(
List<BvhHeuristicTimingSupport.Observation> observations) {
// Synthetic observations are labelled as calibration data, not as final evaluation data.
List<BvhExplorationCsvWriter.ObservationRecord> records = new ArrayList<>(observations.size());
for (BvhHeuristicTimingSupport.Observation observation : observations) {
records.add(new BvhExplorationCsvWriter.ObservationRecord(
"synthetic",
"calibration",
"synthetic-scenario-catalog",
"controlled-synthetic-scenarios",
BvhExplorationCsvWriter.CANDIDATE_POLICY_VERSION,
observation));
}
return records;
}
private static void addSsiObservations(List<BvhHeuristicTimingSupport.Observation> observations) {
// SSI uses polygons as BVH elements.
for (BvhSyntheticScenarioCatalog.SsiScenario scenario : BvhSyntheticScenarioCatalog.ssiScenarios()) {
List<Polygon> polygons = scenario.geometry.getPolygons();
BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.forPolygonsCheap(scenario.name, polygons);
observations.add(BvhHeuristicTimingSupport.measureVariants(
"SSI",
scenario.name,
metrics,
polygons.size(),
strategy -> calculateSsiWithTree(scenario.geometry, strategy)));
}
}
private static void addNestedObservations(List<BvhHeuristicTimingSupport.Observation> observations) {
// Nested rings use inner rings as searchable elements; the exact containment check stays unchanged.
for (BvhSyntheticScenarioCatalog.NestedScenario scenario : BvhSyntheticScenarioCatalog.nestedScenarios()) {
List<LinearRing> rings = scenario.polygon.getInnerRings();
BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.forRingsCheap(scenario.name, rings);
observations.add(BvhHeuristicTimingSupport.measureVariants(
"NESTED",
scenario.name,
metrics,
rings.size(),
strategy -> runNestedCheck(scenario.polygon, BvhHeuristicTimingSupport.nestedVariantFor(strategy))));
}
}
private static void addRsiObservations(List<BvhHeuristicTimingSupport.Observation> observations) {
// RSI broad-phase behavior is driven by edge boxes rather than whole polygon boxes.
for (BvhSyntheticScenarioCatalog.RsiScenario scenario : BvhSyntheticScenarioCatalog.rsiScenarios()) {
BvhInputMetricsCollector.Metrics metrics =
BvhInputMetricsCollector.collectBoxesCheap(scenario.name, collectEdgeBoxes(scenario.geometry));
observations.add(BvhHeuristicTimingSupport.measureVariants(
"RSI",
scenario.name,
metrics,
countEdges(scenario.geometry),
strategy -> runRsiCheck(scenario.geometry, BvhHeuristicTimingSupport.rsiVariantFor(strategy))));
}
}
private static int calculateSsiWithTree(Geometry geometry, SplitStrategy strategy) {
// The tree indexes original polygons; SelfIntersectionUtil still performs the exact intersection checks.
BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newWithStrategy(
geometry.getPolygons(),
polygon -> AABB.of(polygon.getOriginal()),
strategy);
return SelfIntersectionUtil.calculateSolidSelfIntersection(geometry, DELTA, tree).size();
}
private static int runNestedCheck(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 int runRsiCheck(Geometry geometry, RingSelfIntCheck.Variant variant) {
int errorCount = 0;
for (Polygon polygon : geometry.getPolygons()) {
RingSelfIntCheck check = new RingSelfIntCheck(variant);
check.init(Collections.singletonMap("minVertexDistance", String.valueOf(EPSILON)), null);
check.check(polygon.getExteriorRing());
CheckResult result = polygon.getExteriorRing().getCheckResult(check);
if (result.getResultStatus() == ResultStatus.ERROR) {
errorCount++;
}
}
return errorCount;
}
private static int countEdges(Geometry geometry) {
int edgeCount = 0;
for (Polygon polygon : geometry.getPolygons()) {
LinearRing ring = polygon.getExteriorRing();
edgeCount += Math.max(0, ring.getVertices().size() - 1);
}
return edgeCount;
}
private static List<LinearRing> collectExteriorRings(Geometry geometry) {
List<LinearRing> rings = new ArrayList<>();
for (Polygon polygon : geometry.getPolygons()) {
rings.add(polygon.getExteriorRing());
}
return rings;
}
private static List<AABB> collectEdgeBoxes(Geometry geometry) {
List<AABB> boxes = new ArrayList<>();
for (LinearRing ring : collectExteriorRings(geometry)) {
List<Vertex> vertices = ring.getVertices();
for (int i = 0; i < vertices.size() - 1; i++) {
boxes.add(AABB.of(vertices.get(i), vertices.get(i + 1), EPSILON));
}
}
return boxes;
}
private static void printObservationSummary(List<BvhHeuristicTimingSupport.Observation> observations) {
System.out.println();
System.out.println("[BVH-HEURISTIC-EXPLORATION]");
System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
System.out.println(String.format(
"%-7s %-24s %7s %7s %8s %7s %8s %24s %24s %24s %10s",
"check",
"scenario",
"n",
"thin",
"relVol",
"spread",
"aspect",
"policyPred",
"candidatePred",
"fastest BVH",
"bvh ms"));
System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
for (BvhHeuristicTimingSupport.Observation observation : observations) {
BvhInputMetricsCollector.Metrics metrics = observation.metrics;
System.out.println(String.format(
"%-7s %-24s %7d %7.3f %8.5f %7.3f %8.1f %24s %24s %24s %10.3f",
observation.checkName,
metrics.scenario,
metrics.elementCount,
metrics.thinBoxRate,
metrics.averageRelativeBoxVolume,
metrics.centerSpreadRatio,
metrics.averageAspectRatio,
observation.currentPolicyPrediction,
observation.candidateRulePrediction,
observation.fastestBvh.variant,
observation.fastestBvh.averageMillis()));
}
System.out.println("-------------------------------------------------------------------------------------------------------------------------------");
System.out.println("Fastest BVH is the target for candidate strategy rules.");
System.out.println();
}
private static List<BvhExplorationCsvWriter.BucketSummaryRecord> printBucketSummary(
List<BvhHeuristicTimingSupport.Observation> observations) {
// Buckets reduce continuous metrics to readable intervals for first-pass rule discovery.
Map<String, BucketStats> buckets = new LinkedHashMap<>();
for (BvhHeuristicTimingSupport.Observation observation : observations) {
addBucket(buckets, observation, "thin", thinBucket(observation.metrics.thinBoxRate));
addBucket(buckets, observation, "relVol", relVolBucket(observation.metrics.averageRelativeBoxVolume));
addBucket(buckets, observation, "spread", spreadBucket(observation.metrics.centerSpreadRatio));
addBucket(buckets, observation, "aspect", aspectBucket(observation.metrics.averageAspectRatio));
}
System.out.println("[BVH-WINNER-BY-METRIC-BUCKET]");
System.out.println("-------------------------------------------------------------------------------");
System.out.println(String.format(
"%-7s %-7s %-14s %7s %24s %8s",
"check", "metric", "bucket", "cases", "mostCommonBvh", "share"));
System.out.println("-------------------------------------------------------------------------------");
for (BucketStats bucket : buckets.values()) {
System.out.println(String.format(
"%-7s %-7s %-14s %7d %24s %7.1f%%",
bucket.checkName,
bucket.metricName,
bucket.bucketName,
bucket.total,
bucket.mostCommonWinner(),
bucket.mostCommonShare() * 100.0));
}
System.out.println();
System.out.println("[BVH-CANDIDATE-RULES]");
System.out.println("Minimum support: 3 cases; minimum winner share: 65%.");
for (BucketStats bucket : buckets.values()) {
if (bucket.total >= 3 && bucket.mostCommonShare() >= 0.65) {
System.out.println(String.format(
"if check=%s and %s in %s -> %s (%d cases, %.1f%%)",
bucket.checkName,
bucket.metricName,
bucket.bucketName,
bucket.mostCommonWinner(),
bucket.total,
bucket.mostCommonShare() * 100.0));
}
}
System.out.println();
List<BvhExplorationCsvWriter.BucketSummaryRecord> summary = new ArrayList<>(buckets.size());
for (BucketStats bucket : buckets.values()) {
summary.add(bucket.toRecord());
}
return summary;
}
private static void addBucket(
Map<String, BucketStats> buckets,
BvhHeuristicTimingSupport.Observation observation,
String metricName,
String bucketName) {
String key = observation.checkName + "|" + metricName + "|" + bucketName;
BucketStats bucket = buckets.computeIfAbsent(key,
ignored -> new BucketStats(observation.checkName, metricName, bucketName));
bucket.add(observation.fastestBvh.variant);
}
private static String thinBucket(double value) {
if (value == 0.0) {
return "0";
}
if (value <= 0.25) {
return "(0,0.25]";
}
if (value <= 0.50) {
return "(0.25,0.50]";
}
if (value <= 0.75) {
return "(0.50,0.75]";
}
return "(0.75,1]";
}
private static String relVolBucket(double value) {
if (value <= 0.00001) {
return "<=1e-5";
}
if (value <= 0.0001) {
return "<=1e-4";
}
if (value <= 0.001) {
return "<=1e-3";
}
if (value <= 0.01) {
return "<=1e-2";
}
return ">1e-2";
}
private static String spreadBucket(double value) {
if (value < 0.15) {
return "<0.15";
}
if (value < 0.25) {
return "0.15..0.25";
}
if (value < 0.35) {
return "0.25..0.35";
}
return ">=0.35";
}
private static String aspectBucket(double value) {
if (value < 5.0) {
return "<5";
}
if (value < 20.0) {
return "5..20";
}
if (value < 80.0) {
return "20..80";
}
return ">=80";
}
private static final class BucketStats {
final String checkName;
final String metricName;
final String bucketName;
final Map<String, Integer> winners = new LinkedHashMap<>();
int total;
/**
* Counts which split strategy wins inside one metric bucket.
*/
BucketStats(String checkName, String metricName, String bucketName) {
this.checkName = checkName;
this.metricName = metricName;
this.bucketName = bucketName;
}
void add(String winner) {
total++;
winners.merge(winner, 1, Integer::sum);
}
String mostCommonWinner() {
String bestWinner = "";
int bestCount = -1;
for (Map.Entry<String, Integer> entry : winners.entrySet()) {
if (entry.getValue() > bestCount) {
bestWinner = entry.getKey();
bestCount = entry.getValue();
}
}
return bestWinner;
}
double mostCommonShare() {
if (total == 0) {
return 0.0;
}
return (double) winners.get(mostCommonWinner()) / total;
}
int winnerCount() {
return winners.getOrDefault(mostCommonWinner(), 0);
}
int secondBestCount() {
int best = 0;
int second = 0;
for (int count : winners.values()) {
if (count > best) {
second = best;
best = count;
} else if (count > second) {
second = count;
}
}
return second;
}
double winnerMargin() {
if (total == 0) {
return 0.0;
}
return (double) (winnerCount() - secondBestCount()) / total;
}
BvhExplorationCsvWriter.BucketSummaryRecord toRecord() {
return new BvhExplorationCsvWriter.BucketSummaryRecord(
checkName,
metricName,
bucketName,
total,
winners,
mostCommonWinner(),
winnerCount(),
mostCommonShare(),
secondBestCount(),
winnerMargin(),
total >= 3 && mostCommonShare() >= 0.65);
}
}
}
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