/*
* Smallest enclosing circle - Test suite (C++)
*
* Copyright (c) 2022 Project Nayuki
* https://www.nayuki.io/page/smallest-enclosing-circle
*
* This program 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.
*
* This program 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 this program (see COPYING.txt and COPYING.LESSER.txt).
* If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include "SmallestEnclosingCircle.hpp"
using std::size_t;
using std::vector;
// Forward declarations
static void testMatchingNaiveAlgorithm();
static void testTranslation();
static void testScaling();
static vector makeRandomPoints(size_t n);
static Circle smallestEnclosingCircleNaive(const vector &points);
static void assertApproxEqual(double expect, double actual, double epsilon);
// Random number generation global variables
static std::default_random_engine randGen((std::random_device())());
static std::normal_distribution normalDist;
static const double EPSILON = 1e-12;
int main() {
try {
testMatchingNaiveAlgorithm();
testTranslation();
testScaling();
std::cerr << "Test passed" << std::endl;
return EXIT_SUCCESS;
} catch (std::exception &e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
}
static void testMatchingNaiveAlgorithm() {
const long trials = 10000;
std::uniform_int_distribution numPointsDist(1, 30);
for (long i = 0; i < trials; i++) {
const vector points = makeRandomPoints(numPointsDist(randGen));
Circle reference = smallestEnclosingCircleNaive(points);
Circle actual = makeSmallestEnclosingCircle(std::move(points));
assertApproxEqual(reference.c.x, actual.c.x, EPSILON);
assertApproxEqual(reference.c.y, actual.c.y, EPSILON);
assertApproxEqual(reference.r , actual.r , EPSILON);
}
}
static void testTranslation() {
const long trials = 1000;
const long checks = 10;
std::uniform_int_distribution numPointsDist(1, 300);
for (long i = 0; i < trials; i++) {
const vector points = makeRandomPoints(numPointsDist(randGen));
Circle reference = makeSmallestEnclosingCircle(points);
for (long j = 0; j < checks; j++) {
double dx = normalDist(randGen);
double dy = normalDist(randGen);
vector newPoints;
for (const Point &p : points)
newPoints.push_back(Point{p.x + dx, p.y + dy});
Circle translated = makeSmallestEnclosingCircle(std::move(newPoints));
assertApproxEqual(reference.c.x + dx, translated.c.x, EPSILON);
assertApproxEqual(reference.c.y + dy, translated.c.y, EPSILON);
assertApproxEqual(reference.r , translated.r , EPSILON);
}
}
}
static void testScaling() {
const long trials = 1000;
const long checks = 10;
std::uniform_int_distribution numPointsDist(1, 300);
for (long i = 0; i < trials; i++) {
const vector points = makeRandomPoints(numPointsDist(randGen));
Circle reference = makeSmallestEnclosingCircle(points);
for (long j = 0; j < checks; j++) {
double scale = normalDist(randGen);
vector newPoints;
for (const Point &p : points)
newPoints.push_back(Point{p.x * scale, p.y * scale});
Circle scaled = makeSmallestEnclosingCircle(std::move(newPoints));
assertApproxEqual(reference.c.x * scale, scaled.c.x, EPSILON);
assertApproxEqual(reference.c.y * scale, scaled.c.y, EPSILON);
assertApproxEqual(reference.r * std::fabs(scale), scaled.r, EPSILON);
}
}
}
static vector makeRandomPoints(size_t n) {
vector result;
if (std::uniform_real_distribution()(randGen) < 0.2) { // Discrete lattice (to have a chance of duplicated points)
std::uniform_int_distribution dist(0, 9);
for (size_t i = 0; i < n; i++)
result.push_back(Point{static_cast(dist(randGen)), static_cast(dist(randGen))});
} else { // Gaussian distribution
for (size_t i = 0; i < n; i++)
result.push_back(Point{normalDist(randGen), normalDist(randGen)});
}
return result;
}
// Returns the smallest enclosing circle in O(n^4) time using the naive algorithm.
static Circle smallestEnclosingCircleNaive(const vector &points) {
// Degenerate cases
if (points.empty())
return Circle::INVALID;
else if (points.size() == 1)
return Circle{points.at(0), 0};
// Try all unique pairs
Circle result(Circle::INVALID);
for (size_t i = 0; i < points.size(); i++) {
for (size_t j = i + 1; j < points.size(); j++) {
Circle c = makeDiameter(points.at(i), points.at(j));
if ((result.r < 0 || c.r < result.r) && c.contains(points))
result = c;
}
}
if (result.r >= 0)
return result; // This optimization is not mathematically proven
// Try all unique triples
for (size_t i = 0; i < points.size(); i++) {
for (size_t j = i + 1; j < points.size(); j++) {
for (size_t k = j + 1; k < points.size(); k++) {
Circle c = makeCircumcircle(points.at(i), points.at(j), points.at(k));
if (c.r >= 0 && (result.r < 0 || c.r < result.r) && c.contains(points))
result = c;
}
}
}
if (result.r < 0)
throw std::runtime_error("Assertion error");
return result;
}
static void assertApproxEqual(double expect, double actual, double epsilon) {
if (std::fabs(expect - actual) > epsilon)
throw std::runtime_error("Value mismatch");
}