Commit 0c9af352 authored by NicolaiO's avatar NicolaiO 🐼 Committed by TIGERs GitLab
Browse files

Resolve "Improve HullCircle Creation speed"

Closes #1695

See merge request main/Sumatra!1462

sumatra-commit: e3911758ebf686f12a6ed6c5b999ee00aa5b949f
parent e91d36c2
Pipeline #17274 passed with stage
in 6 minutes and 9 seconds
......@@ -9,7 +9,6 @@ import edu.tigers.sumatra.math.SumatraMath;
import edu.tigers.sumatra.math.vector.IVector2;
import edu.tigers.sumatra.math.vector.Vector2;
import edu.tigers.sumatra.math.vector.Vector2f;
import edu.tigers.sumatra.stream.StreamUtil;
import org.apache.commons.math3.linear.Array2DRowRealMatrix;
import org.apache.commons.math3.linear.ArrayRealVector;
import org.apache.commons.math3.linear.DecompositionSolver;
......@@ -18,11 +17,11 @@ import org.apache.commons.math3.linear.RealMatrix;
import org.apache.commons.math3.linear.RealVector;
import org.apache.commons.math3.linear.SingularMatrixException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
/**
......@@ -150,20 +149,54 @@ public class Circle extends ACircle
/**
* Create a circle that encloses all points in the list.
* This method is not the most effective one and should not be used in match mode.
*
* @param points
* @return Smallest circle enclosing all points.
*/
public static Optional<ICircle> hullCircle(final List<IVector2> points)
{
return Stream
.concat(StreamUtil.nonRepeatingPermutation2Fold(points), StreamUtil.nonRepeatingPermutation3Fold(points))
.map(Circle::fromNPoints)
.filter(Optional::isPresent)
.map(Optional::get)
.filter(c -> points.stream().allMatch(p -> c.isPointInShape(p, 1e-10)))
.min(Comparator.comparing(ICircle::radius));
return hullCircleWelzl(points, Collections.emptyList());
}
/**
* Calculating the smallest circle problem using the Welzl's algorithm:
* https://en.wikipedia.org/wiki/Smallest-circle_problem#Welzl's_algorithm
* Which uses a randomized approach but has O(n) as an expected runtime
*
* @param points
* @param hullPoints
* @return
*/
private static Optional<ICircle> hullCircleWelzl(final List<IVector2> points, final List<IVector2> hullPoints)
{
if (points.isEmpty() || hullPoints.size() == 3)
{
switch (hullPoints.size())
{
case 1:
return Optional.of(Circle.createCircle(hullPoints.get(0), 0.0));
case 2:
return Optional.of(Circle.from2Points(hullPoints.get(0), hullPoints.get(1)));
case 3:
return Circle.from3Points(hullPoints.get(0), hullPoints.get(1), hullPoints.get(2));
default:
return Optional.empty();
}
} else
{
var shuffled = new ArrayList<>(points);
Collections.shuffle(shuffled);
final var p = shuffled.get(shuffled.size() - 1);
var circle = hullCircleWelzl(shuffled.subList(0, shuffled.size() - 1), hullPoints);
if (circle.isEmpty() || !circle.get().isPointInShape(p, 1e-10))
{
var hullPointsPlusP = new ArrayList<>(hullPoints);
hullPointsPlusP.add(p);
return hullCircleWelzl(shuffled.subList(0, shuffled.size() - 1), hullPointsPlusP);
}
return circle;
}
}
......
/*
* Copyright (c) 2009 - 2018, DHBW Mannheim - TIGERs Mannheim
* Copyright (c) 2009 - 2021, DHBW Mannheim - TIGERs Mannheim
*/
package edu.tigers.sumatra.math.circle;
import static org.assertj.core.api.Assertions.assertThat;
import edu.tigers.sumatra.math.line.Line;
import edu.tigers.sumatra.math.vector.IVector2;
import edu.tigers.sumatra.math.vector.Vector2;
import edu.tigers.sumatra.math.vector.Vector2f;
import edu.tigers.sumatra.stream.StreamUtil;
import org.junit.Assert;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Stream;
import org.junit.Assert;
import org.junit.Test;
import edu.tigers.sumatra.math.line.Line;
import edu.tigers.sumatra.math.vector.IVector2;
import edu.tigers.sumatra.math.vector.Vector2;
import edu.tigers.sumatra.math.vector.Vector2f;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test method for {@link Circle}.
*
*
* @author Malte
*/
public class CircleTest
{
private static final double ACCURACY = 1e-3;
/**
* Testmethod for Circle#isIntersectingWithLine.
*
*
* @author Dion
*/
@Test
......@@ -43,21 +45,21 @@ public class CircleTest
// Test true
ICircle circle = Circle.createCircle(Vector2.fromXY(1, 1), 1);
Line line = Line.fromDirection(Vector2.fromXY(0, 0), Vector2.fromXY(1, 1));
Assert.assertEquals(true, circle.isIntersectingWithLine(line));
Assert.assertTrue(circle.isIntersectingWithLine(line));
// Test true2
Line line3 = Line.fromDirection(Vector2.fromXY(-1, 1), Vector2.fromXY(1, 0));
Assert.assertEquals(true, circle.isIntersectingWithLine(line3));
Assert.assertTrue(circle.isIntersectingWithLine(line3));
// Test false
Line line2 = Line.fromDirection(Vector2f.ZERO_VECTOR, Vector2.fromXY(-1, 1));
Assert.assertEquals(false, circle.isIntersectingWithLine(line2));
Assert.assertFalse(circle.isIntersectingWithLine(line2));
}
/**
* Testmethod for Circle#LineIntersections.
*
*
* @author Dion
*/
@Test
......@@ -66,30 +68,20 @@ public class CircleTest
// Test 1
ICircle circle = Circle.createCircle(Vector2.fromXY(1, 1), 1);
Line line = Line.fromDirection(Vector2f.ZERO_VECTOR, Vector2.fromXY(-1, 1));
if (circle.lineIntersections(line).size() == 0)
{
Assert.assertTrue(true);
} else
{
Assert.assertTrue(false);
}
Assert.assertEquals(0, circle.lineIntersections(line).size());
// Test 2
Line line2 = Line.fromDirection(Vector2.fromXY(-1, 1), Vector2.fromXY(1, 0));
List<IVector2> result = circle.lineIntersections(line2);
if ((result.get(0).x() == 2) && (result.get(0).y() == 1) && (result.get(1).x() == 0) && (result.get(1).y() == 1))
{
Assert.assertTrue(true);
} else
{
Assert.assertTrue(false);
}
Assert.assertTrue(
(result.get(0).x() == 2) && (result.get(0).y() == 1) && (result.get(1).x() == 0) && (result.get(1).y()
== 1));
}
/**
* Testmethod for Circle#nearestPointOutsideCircle.
*
*
* @author Dion
*/
@Test
......@@ -99,13 +91,13 @@ public class CircleTest
ICircle circle = Circle.createCircle(Vector2.fromXY(-2, 4), 3);
Vector2 point = Vector2.fromXY(-1, 4);
Assert.assertEquals(circle.nearestPointOutside(point), Vector2.fromXY(1, 4));
// Test false
Vector2 point3 = Vector2.fromXY(1, 2);
Assert.assertEquals(circle.nearestPointOutside(point3), Vector2.fromXY(1, 2));
}
/**
* @author Nicolai Ommer <nicolai.ommer@gmail.com>
*/
......@@ -124,8 +116,8 @@ public class CircleTest
}
Assert.fail();
}
/**
* @author AndreR
*/
......@@ -135,15 +127,15 @@ public class CircleTest
IVector2 P1 = Vector2.fromXY(0, 1);
IVector2 P2 = Vector2.fromXY(1, 0);
IVector2 P3 = Vector2.fromXY(2, 1);
ICircle circle = Circle.from3Points(P1, P2, P3).orElseThrow(() -> new AssertionError("No circle found"));
Assert.assertEquals(1.0, circle.radius(), 1e-10);
Assert.assertEquals(1.0, circle.center().x(), 1e-10);
Assert.assertEquals(1.0, circle.center().y(), 1e-10);
}
/**
* @author AndreR
*/
......@@ -153,14 +145,14 @@ public class CircleTest
IVector2 P1 = Vector2.fromXY(0, 0);
IVector2 P2 = Vector2.fromXY(1, 0);
IVector2 P3 = Vector2.fromXY(2, 0);
if (Circle.from3Points(P1, P2, P3).isPresent())
{
Assert.fail();
}
}
/**
* @author AndreR
*/
......@@ -170,29 +162,29 @@ public class CircleTest
IVector2 P1 = Vector2.fromXY(0, 1e-9);
IVector2 P2 = Vector2.fromXY(1e-9, 0);
IVector2 P3 = Vector2.fromXY(2e-9, 1e-9);
ICircle circle = Circle.from3Points(P1, P2, P3).orElseThrow(() -> new AssertionError("No circle found"));
Assert.assertEquals(1e-9, circle.radius(), 1e-20);
Assert.assertEquals(1e-9, circle.center().x(), 1e-20);
Assert.assertEquals(1e-9, circle.center().y(), 1e-20);
}
@Test
public void testCircleFrom2Points()
{
IVector2 P1 = Vector2.fromXY(1, 0);
IVector2 P2 = Vector2.fromXY(-1, 0);
ICircle circle = Circle.from2Points(P1, P2);
Assert.assertEquals(1, circle.radius(), Double.MIN_VALUE);
Assert.assertEquals(0, circle.center().x(), Double.MIN_VALUE);
Assert.assertEquals(0, circle.center().y(), Double.MIN_VALUE);
}
@Test
public void testHullCircle()
{
......@@ -201,20 +193,20 @@ public class CircleTest
points.add(Vector2.fromXY(-1, 0));
points.add(Vector2.fromXY(0, 0.5));
points.add(Vector2.fromXY(0, -0.5));
ICircle circle = Circle.hullCircle(points).orElseThrow(AssertionError::new);
Assert.assertEquals(1.0, circle.radius(), Double.MIN_VALUE);
Assert.assertEquals(0.0, circle.center().x(), Double.MIN_VALUE);
Assert.assertEquals(0.0, circle.center().y(), Double.MIN_VALUE);
}
@Test
public void testHullCircleRandomPoints()
{
Random rnd = new Random(42);
long iterations = 1000;
for (int i = 0; i < iterations; i++)
{
......@@ -224,10 +216,23 @@ public class CircleTest
{
points.add(Vector2.fromXY((rnd.nextDouble() * 2000) - 1000, (rnd.nextDouble() + 2000) - 1000));
}
Optional<ICircle> hullCircleOpt = Circle.hullCircle(points);
ICircle hullCircle = hullCircleOpt.orElseThrow(AssertionError::new);
assertThat(points).allMatch(p -> hullCircle.isPointInShape(p, 1e-10));
var hullFast = Circle.hullCircle(points);
var hullSlow = Stream.concat(StreamUtil.nonRepeatingPermutation2Fold(points),
StreamUtil.nonRepeatingPermutation3Fold(points)).map(Circle::fromNPoints)
.filter(Optional::isPresent)
.map(Optional::get)
.filter(c -> points.stream().allMatch(p -> c.isPointInShape(p, 1e-10)))
.min(Comparator.comparing(ICircle::radius));
Assert.assertTrue(hullSlow.isPresent());
Assert.assertTrue(hullFast.isPresent());
Assert.assertEquals(hullSlow.get().center().x(), hullFast.get().center().x(), 1e-10);
Assert.assertEquals(hullSlow.get().center().y(), hullFast.get().center().y(), 1e-10);
Assert.assertEquals(hullSlow.get().radius(), hullFast.get().radius(), 1e-10);
ICircle hullFastNonOpt = hullFast.orElseThrow(AssertionError::new);
assertThat(points).allMatch(p -> hullFastNonOpt.isPointInShape(p, 1e-10));
}
}
}
Markdown is supported
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