| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #include <cmath> |
| #include <iostream> |
|
|
| #include <catch2/catch_approx.hpp> |
| #include <catch2/catch_test_macros.hpp> |
|
|
| #include <boost/math/quadrature/gauss_kronrod.hpp> |
|
|
| #include "lc_hyperbola.h" |
| #include "lc_hyperbolaspline.h" |
| #include "lc_quadratic.h" |
| #include "rs_debug.h" |
| #include "rs_math.h" |
| #include "rs_vector.h" |
|
|
| #ifndef M_PI_6 |
| #define M_PI_6 (M_PI / 6.) |
| #endif |
|
|
| using Catch::Approx; |
|
|
| namespace { |
| constexpr double TOL = 1e-6; |
| constexpr double ANGLE_TOL = 1e-6; |
|
|
| bool doublesApproxEqual(double x, double y, double tolerance = TOL) { |
| return std::abs(x - y) < TOL; |
| } |
| bool vectorsApproxEqual(const RS_Vector &v1, const RS_Vector &v2, |
| double tolerance = TOL) { |
| return doublesApproxEqual(v1.x, v2.x, tolerance) && |
| doublesApproxEqual(v1.y, v2.y, tolerance); |
| } |
| bool hyperbolaDataApproxEqual(const LC_HyperbolaData &a, |
| const LC_HyperbolaData &b) { |
| return vectorsApproxEqual(a.center, b.center) && |
| doublesApproxEqual(a.majorP.magnitude(), b.majorP.magnitude()) && |
| doublesApproxEqual(a.ratio, b.ratio) && a.reversed == b.reversed && |
| doublesApproxEqual( |
| RS_Math::getAngleDifference(a.majorP.angle(), b.majorP.angle()), |
| 0.0, ANGLE_TOL) && |
| doublesApproxEqual(a.angle1, b.angle1, ANGLE_TOL) && |
| doublesApproxEqual(a.angle2, b.angle2, ANGLE_TOL); |
| } |
| } |
|
|
| TEST_CASE("Hyperbola ↔ DRW_Spline round-trip validation", |
| "[hyperbola][spline][roundtrip]") { |
| SECTION("Right branch bounded arc: analytical shoulder validation") { |
| LC_HyperbolaData original; |
| original.center = RS_Vector(0.0, 0.0); |
| original.majorP = RS_Vector(2.0, 0.0); |
| original.ratio = 0.5; |
| original.reversed = false; |
|
|
| original.angle1 = -1.0; |
| original.angle2 = 1.5; |
|
|
| DRW_Spline spl; |
| REQUIRE(LC_HyperbolaSpline::hyperbolaToSpline(original, spl)); |
|
|
| |
| REQUIRE(spl.degree == 2); |
| REQUIRE(spl.flags == 8); |
| REQUIRE(spl.controllist.size() == 3); |
| REQUIRE(spl.weightlist.size() == 3); |
| REQUIRE(spl.knotslist.size() == 6); |
|
|
| |
| REQUIRE(doublesApproxEqual(spl.weightlist[0], 1.0)); |
| REQUIRE(doublesApproxEqual(spl.weightlist[2], 1.0)); |
| REQUIRE(spl.weightlist[1] > 1.0); |
|
|
| |
| auto recovered = LC_HyperbolaSpline::splineToHyperbola(spl, nullptr); |
| REQUIRE(recovered != nullptr); |
| REQUIRE(recovered->isValid()); |
|
|
| const LC_HyperbolaData &rec = recovered->getData(); |
| REQUIRE(hyperbolaDataApproxEqual(rec, original)); |
|
|
| |
| REQUIRE(doublesApproxEqual(rec.angle1, original.angle1, ANGLE_TOL)); |
| REQUIRE(doublesApproxEqual(rec.angle2, original.angle2, ANGLE_TOL)); |
| } |
|
|
| SECTION("Rotated and translated bounded arc") { |
| LC_HyperbolaData original; |
| original.center = RS_Vector(10.0, 20.0); |
| original.majorP = RS_Vector(4.0, 0.0).rotate(M_PI / 6.0); |
| original.ratio = 0.75; |
| original.reversed = false; |
| original.angle1 = -0.8; |
| original.angle2 = 1.2; |
|
|
| DRW_Spline spl; |
| REQUIRE(LC_HyperbolaSpline::hyperbolaToSpline(original, spl)); |
|
|
| auto recovered = LC_HyperbolaSpline::splineToHyperbola(spl, nullptr); |
| REQUIRE(recovered != nullptr); |
| REQUIRE(recovered->isValid()); |
|
|
| const LC_HyperbolaData &rec = recovered->getData(); |
| REQUIRE(hyperbolaDataApproxEqual(rec, original)); |
| } |
|
|
| SECTION("Very small arc near vertex") { |
| LC_HyperbolaData original; |
| original.center = RS_Vector(0.0, 0.0); |
| original.majorP = RS_Vector(1.0, 0.0); |
| original.ratio = 0.3; |
| original.reversed = false; |
| original.angle1 = -0.1; |
| original.angle2 = 0.1; |
|
|
| DRW_Spline spl; |
| REQUIRE(LC_HyperbolaSpline::hyperbolaToSpline(original, spl)); |
|
|
| |
| |
| REQUIRE(spl.weightlist[1] > 1.0); |
| REQUIRE(spl.weightlist[1] < 1.1); |
|
|
| auto recovered = LC_HyperbolaSpline::splineToHyperbola(spl, nullptr); |
| REQUIRE(recovered != nullptr); |
| REQUIRE(recovered->isValid()); |
|
|
| REQUIRE(hyperbolaDataApproxEqual(recovered->getData(), original)); |
| } |
|
|
| SECTION("Large parameter range (tests numerical stability)") { |
| LC_HyperbolaData original; |
| original.center = RS_Vector(0.0, 0.0); |
| original.majorP = RS_Vector(1.0, 0.0); |
| original.ratio = 0.6; |
| original.reversed = false; |
| original.angle1 = -3.0; |
| original.angle2 = 4.0; |
|
|
| DRW_Spline spl; |
| REQUIRE(LC_HyperbolaSpline::hyperbolaToSpline(original, spl)); |
|
|
| |
| REQUIRE(spl.weightlist[1] > 10.0); |
|
|
| auto recovered = LC_HyperbolaSpline::splineToHyperbola(spl, nullptr); |
| REQUIRE(recovered != nullptr); |
| REQUIRE(recovered->isValid()); |
|
|
| REQUIRE(hyperbolaDataApproxEqual(recovered->getData(), original)); |
| } |
|
|
| |
|
|
| SECTION("Limited arc hyperbola: analytical shoulder validation") { |
| LC_HyperbolaData original; |
| original.center = RS_Vector(0.0, 0.0); |
| original.majorP = RS_Vector(1.0, 0.0); |
| original.ratio = 0.25; |
| original.reversed = false; |
|
|
| double y_start = -1.0; |
| double y_end = 2.0; |
|
|
| double phi_start = std::asinh(y_start / 0.25); |
| double phi_end = std::asinh(y_end / 0.25); |
| original.angle1 = phi_start; |
| original.angle2 = phi_end; |
|
|
| DRW_Spline spl; |
| REQUIRE(LC_HyperbolaSpline::hyperbolaToSpline(original, spl)); |
|
|
| |
| REQUIRE(spl.degree == 2); |
| REQUIRE(spl.flags == 8); |
| REQUIRE(spl.controllist.size() == 3); |
| REQUIRE(spl.weightlist.size() == 3); |
|
|
| |
| REQUIRE(doublesApproxEqual(spl.weightlist[0], 1.0)); |
| REQUIRE(doublesApproxEqual(spl.weightlist[2], 1.0)); |
| double w_middle = spl.weightlist[1]; |
| REQUIRE(w_middle > 1.0); |
|
|
| |
| RS_Vector p0(spl.controllist[0]->x, spl.controllist[0]->y); |
| RS_Vector p1(spl.controllist[1]->x, spl.controllist[1]->y); |
| RS_Vector p2(spl.controllist[2]->x, spl.controllist[2]->y); |
|
|
| |
| REQUIRE(doublesApproxEqual(p0.y, y_start)); |
| REQUIRE(doublesApproxEqual(p2.y, y_end)); |
|
|
| |
| REQUIRE(doublesApproxEqual(p0.x * p0.x - 16.0 * p0.y * p0.y, 1.0)); |
| REQUIRE(doublesApproxEqual(p2.x * p2.x - 16.0 * p2.y * p2.y, 1.0)); |
|
|
| |
| |
| double a = 1.0; |
| double b = 0.25; |
| double phi_mid = (phi_start + phi_end) * 0.5; |
| double delta = (phi_end - phi_start) * 0.5; |
|
|
| |
| |
| double expected_shoulder_x = a * std::cosh(phi_mid) / std::cosh(delta); |
| double expected_shoulder_y = b * std::sinh(phi_mid) / std::cosh(delta); |
|
|
| |
| REQUIRE(doublesApproxEqual(p1.x, expected_shoulder_x, 1e-10)); |
| REQUIRE(doublesApproxEqual(p1.y, expected_shoulder_y, 1e-10)); |
|
|
| |
| auto recovered = LC_HyperbolaSpline::splineToHyperbola(spl, nullptr); |
| REQUIRE(recovered != nullptr); |
| REQUIRE(recovered->isValid()); |
|
|
| const LC_HyperbolaData &rec = recovered->getData(); |
| REQUIRE(hyperbolaDataApproxEqual(rec, original)); |
|
|
| |
| REQUIRE(doublesApproxEqual(rec.angle1, phi_start, 1e-6)); |
| REQUIRE(doublesApproxEqual(rec.angle2, phi_end, 1e-6)); |
| } |
|
|
| SECTION("Non-hyperbola splines return nullptr") { |
| |
| DRW_Spline parabola; |
| parabola.degree = 2; |
| parabola.flags = 8; |
| parabola.controllist.resize(3); |
| parabola.controllist[0] = std::make_shared<DRW_Coord>(0.0, 0.0); |
| parabola.controllist[1] = std::make_shared<DRW_Coord>(1.0, 1.0); |
| parabola.controllist[2] = std::make_shared<DRW_Coord>(2.0, 0.0); |
| parabola.weightlist = {1.0, 0.5, 1.0}; |
| parabola.knotslist = {0.0, 0.0, 0.0, 1.0, 1.0, 1.0}; |
|
|
| REQUIRE(LC_HyperbolaSpline::splineToHyperbola(parabola, nullptr) == |
| nullptr); |
|
|
| |
| DRW_Spline ellipse; |
| ellipse.degree = 2; |
| ellipse.flags = 8; |
| ellipse.controllist.resize(3); |
| ellipse.controllist[0] = std::make_shared<DRW_Coord>(1.0, 0.0); |
| ellipse.controllist[1] = std::make_shared<DRW_Coord>(1.0, 1.0); |
| ellipse.controllist[2] = std::make_shared<DRW_Coord>(0.0, 1.0); |
| double w_ell = 1.0 / std::sqrt(2.0); |
| ellipse.weightlist = {1.0, w_ell, 1.0}; |
| ellipse.knotslist = {0.0, 0.0, 0.0, 1.0, 1.0, 1.0}; |
|
|
| REQUIRE(LC_HyperbolaSpline::splineToHyperbola(ellipse, nullptr) == nullptr); |
| } |
| } |
|
|
| TEST_CASE("LC_Hyperbola dual curve methods", "[hyperbola][dual][quadratic]") { |
| SECTION("Standard right-opening hyperbola dual is a rotated/translated " |
| "hyperbola") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(3.0, 0.0); |
| data.ratio = 4.0 / 3.0; |
| data.reversed = false; |
| data.angle1 = -2.0; |
| data.angle2 = 2.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| LC_Quadratic q = hb.getQuadratic(); |
|
|
| LC_Quadratic dual = q.getDualCurve(); |
| REQUIRE(dual.isValid()); |
| REQUIRE(dual.isQuadratic()); |
|
|
| LC_Hyperbola dualHb(nullptr, dual.getCoefficients()); |
| REQUIRE(dualHb.isValid()); |
|
|
| |
| |
| |
| |
| |
| |
| double expectedMajor = 1.0 / 3.0; |
| double expectedRatio = 3.0 / 4.0; |
|
|
| std::cout << "a=" << dualHb.getMajorRadius() << std::endl; |
| std::cout << "b=" << dualHb.getMinorRadius() << std::endl; |
| std::cout << "b/a=" << dualHb.getMajorP().angle() << std::endl; |
| REQUIRE(doublesApproxEqual(dualHb.getMajorRadius(), expectedMajor, 1e-6)); |
| REQUIRE(doublesApproxEqual(dualHb.getRatio(), expectedRatio, 1e-6)); |
|
|
| |
| double angleDiff = |
| RS_Math::getAngleDifference(dualHb.getMajorP().angle(), 0.); |
| REQUIRE(doublesApproxEqual(angleDiff, 0.0, ANGLE_TOL)); |
|
|
| |
| REQUIRE(vectorsApproxEqual(dualHb.getCenter(), RS_Vector(0.0, 0.0))); |
| } |
|
|
| |
| |
|
|
| SECTION("Standard right-opening hyperbola dual is a rotated/translated " |
| "hyperbola") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(3.0, 0.0); |
| data.ratio = 4.0 / 3.0; |
| data.reversed = false; |
| data.angle1 = -2.0; |
| data.angle2 = 2.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| LC_Quadratic q = hb.getQuadratic(); |
|
|
| LC_Quadratic dual = q.getDualCurve(); |
| REQUIRE(dual.isValid()); |
| REQUIRE(dual.isQuadratic()); |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| std::vector<double> dualCoeffs = dual.getCoefficients(); |
|
|
| |
| |
| |
| REQUIRE(dualCoeffs.size() == 6); |
| |
| REQUIRE(std::abs(dualCoeffs[5]) > RS_TOLERANCE); |
| REQUIRE(!std::isnan(dualCoeffs[0])); |
| REQUIRE(!std::isnan(dualCoeffs[2])); |
|
|
| |
| |
| |
|
|
| |
| |
| |
| } |
|
|
| SECTION("Rotated hyperbola dual is correctly oriented") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(4.0, 0.0).rotate(M_PI_4); |
| data.ratio = 0.75; |
| data.reversed = false; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| LC_Quadratic q = hb.getQuadratic(); |
| LC_Quadratic dual = q.getDualCurve(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| double origAngle = data.majorP.angle(); |
|
|
| |
| double expectedPlus90 = RS_Math::correctAngle(origAngle); |
| double expectedMinus90 = RS_Math::correctAngle(origAngle + M_PI); |
|
|
| LC_Hyperbola dualhd{nullptr, dual}; |
| double dualAngle = dualhd.getMajorP().angle(); |
| dualAngle = RS_Math::correctAngle(dualAngle); |
|
|
| |
| double diffPlus = RS_Math::getAngleDifference(dualAngle, expectedPlus90); |
| double diffMinus = RS_Math::getAngleDifference(dualAngle, expectedMinus90); |
|
|
| bool isPerpendicular = doublesApproxEqual(diffPlus, 0.0, 2 * ANGLE_TOL) || |
| doublesApproxEqual(diffMinus, 0.0, 2 * ANGLE_TOL); |
|
|
| REQUIRE(isPerpendicular); |
| } |
|
|
| SECTION("Left branch hyperbola has same dual as right branch (up to sign)") { |
| LC_HyperbolaData right; |
| right.center = RS_Vector(0.0, 0.0); |
| right.majorP = RS_Vector(3.0, 0.0); |
| right.ratio = 4.0 / 3.0; |
| right.reversed = false; |
|
|
| LC_HyperbolaData left = right; |
| left.reversed = true; |
|
|
| LC_Hyperbola hbRight(nullptr, right); |
| LC_Hyperbola hbLeft(nullptr, left); |
|
|
| REQUIRE(hbRight.isValid()); |
| REQUIRE(hbLeft.isValid()); |
|
|
| LC_Quadratic qRight = hbRight.getQuadratic(); |
| LC_Quadratic qLeft = hbLeft.getQuadratic(); |
|
|
| auto coeffsRight = qRight.getCoefficients(); |
| auto coeffsLeft = qLeft.getCoefficients(); |
|
|
| for (size_t i = 0; i < coeffsRight.size(); ++i) { |
| REQUIRE(doublesApproxEqual(coeffsRight[i], coeffsLeft[i])); |
| } |
|
|
| LC_Quadratic dualRight = qRight.getDualCurve(); |
| LC_Quadratic dualLeft = qLeft.getDualCurve(); |
|
|
| auto dualCoeffsRight = dualRight.getCoefficients(); |
| auto dualCoeffsLeft = dualLeft.getCoefficients(); |
|
|
| for (size_t i = 0; i < dualCoeffsRight.size(); ++i) { |
| REQUIRE(doublesApproxEqual(dualCoeffsRight[i], dualCoeffsLeft[i])); |
| } |
| } |
| SECTION("dualLineTangentPoint() returns correct point for simple line") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(2.0, 0.0); |
| data.ratio = 0.5; |
| data.reversed = false; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| RS_Vector tangentPoint = hb.dualLineTangentPoint(RS_Vector(3.0, 0.0)); |
|
|
| REQUIRE(tangentPoint.valid); |
|
|
| double a = 2.0; |
| double b = 1.0; |
| double k = 3.0; |
|
|
| RS_Vector expectedPoint(a, 0.); |
|
|
| REQUIRE(std::abs(tangentPoint.x - expectedPoint.x) < 1e-8); |
| REQUIRE(std::abs(tangentPoint.y - expectedPoint.y) < 1e-8); |
| } |
| } |
| |
| |
| |
|
|
| TEST_CASE("LC_Hyperbola getLength() accuracy", "[hyperbola][length]") { |
| constexpr double TOL = 1e-8; |
|
|
| SECTION("Symmetric bounded arc around vertex") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(2.0, 0.0); |
| data.ratio = 0.5; |
| data.reversed = false; |
| data.angle1 = -1.0; |
| data.angle2 = 1.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double length = hb.getLength(); |
|
|
| |
| double expected = 3.3078924645266374; |
|
|
| REQUIRE(doublesApproxEqual(length, expected, TOL)); |
| } |
|
|
| SECTION("Asymmetric arc - rectangular hyperbola") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(1.0, 0.0); |
| data.ratio = 1.0; |
| data.reversed = false; |
| data.angle1 = 0.5; |
| data.angle2 = 2.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double length = hb.getLength(); |
|
|
| double expected = 4.084667883160526; |
|
|
| REQUIRE(doublesApproxEqual(length, expected, TOL)); |
| } |
|
|
| SECTION("Very small arc near vertex (near-parabolic behavior)") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(5.0, 0.0); |
| data.ratio = 0.1; |
| data.reversed = false; |
| data.angle1 = -0.1; |
| data.angle2 = 0.1; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double length = hb.getLength(); |
|
|
| |
| double expected_approx = 0.11493829774467469; |
|
|
| |
| REQUIRE(doublesApproxEqual(length, expected_approx, 1e-6)); |
| } |
|
|
| SECTION("Unbounded hyperbola returns infinite length") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(1.0, 0.0); |
| data.ratio = 0.5; |
| data.reversed = false; |
| data.angle1 = 0.0; |
| data.angle2 = 0.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double length = hb.getLength(); |
| REQUIRE(length == RS_MAXDOUBLE); |
| } |
|
|
| SECTION("Rotated and translated hyperbola (length invariant)") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(10.0, -5.0); |
| data.majorP = RS_Vector(3.0, 0.0).rotate(M_PI_6); |
| data.ratio = 2.0 / 3.0; |
| data.reversed = false; |
| data.angle1 = -1.5; |
| data.angle2 = 1.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double length_rot = hb.getLength(); |
|
|
| |
| double expected_ref = 8.966998793851278; |
|
|
| REQUIRE(doublesApproxEqual(length_rot, expected_ref, TOL)); |
| } |
| } |
| TEST_CASE("LC_Hyperbola getNearestDist() accuracy", |
| "[hyperbola][nearestdist]") { |
| constexpr double TOL = 1e-8; |
| constexpr double DIST_TOL = 1e-6; |
|
|
| SECTION("Symmetric bounded arc around vertex - distance from start") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(2.0, 0.0); |
| data.ratio = 0.5; |
| data.reversed = false; |
| data.angle1 = -1.0; |
| data.angle2 = 1.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double total_length = hb.getLength(); |
| |
| REQUIRE(std::abs(total_length - 3.3078924645) < 1e-6); |
|
|
| RS_Vector coord = hb.getStartpoint() + RS_Vector(0.1, 0.1); |
|
|
| double test_dist = total_length * 0.3; |
|
|
| RS_Vector point = hb.getNearestDist(test_dist, coord); |
| REQUIRE(point.valid); |
|
|
| |
| double phi_point = hb.getParamFromPoint(point); |
| double arc_to_point = hb.getArcLength(data.angle1, phi_point); |
|
|
| REQUIRE(std::abs(arc_to_point - test_dist) < DIST_TOL); |
| } |
|
|
| SECTION("Asymmetric arc - distance from end") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(1.0, 0.0); |
| data.ratio = 1.0; |
| data.reversed = false; |
| data.angle1 = 0.5; |
| data.angle2 = 2.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double total_length = hb.getLength(); |
| |
| REQUIRE(std::abs(total_length - 4.084667883) < 1e-8); |
|
|
| RS_Vector coord = hb.getEndpoint() + RS_Vector(0.05, -0.1); |
|
|
| double test_dist = |
| total_length * 0.4; |
| |
|
|
| RS_Vector point = hb.getNearestDist(test_dist, coord); |
| REQUIRE(point.valid); |
|
|
| double phi_point = hb.getParamFromPoint(point); |
| double arc_from_start = hb.getArcLength(data.angle1, phi_point); |
| double dist_from_end = total_length - arc_from_start; |
|
|
| |
| |
| REQUIRE(std::abs(dist_from_end - test_dist) < DIST_TOL); |
| } |
|
|
| SECTION("Small arc near vertex - near-parabolic behavior") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(5.0, 0.0); |
| data.ratio = 0.1; |
| data.reversed = false; |
| data.angle1 = -0.1; |
| data.angle2 = 0.1; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double total_length = hb.getLength(); |
| |
| REQUIRE(std::abs(total_length - 0.1149382977) < 1e-8); |
|
|
| RS_Vector coord = hb.getStartpoint(); |
|
|
| double test_dist = total_length * 0.5; |
|
|
| RS_Vector point = hb.getNearestDist(test_dist, coord); |
| REQUIRE(point.valid); |
|
|
| double phi_point = hb.getParamFromPoint(point); |
| double arc_to_point = hb.getArcLength(data.angle1, phi_point); |
|
|
| REQUIRE(std::abs(arc_to_point - test_dist) < DIST_TOL); |
| } |
|
|
| SECTION("Rotated hyperbola - invariance") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(10.0, -5.0); |
| data.majorP = RS_Vector(3.0, 0.0).rotate(M_PI_6); |
| data.ratio = 2.0 / 3.0; |
| data.reversed = false; |
| data.angle1 = -1.5; |
| data.angle2 = 1.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double length_rot = hb.getLength(); |
|
|
| |
| LC_HyperbolaData ref; |
| ref.center = RS_Vector(0.0, 0.0); |
| ref.majorP = RS_Vector(3.0, 0.0); |
| ref.ratio = 2.0 / 3.0; |
| ref.reversed = false; |
| ref.angle1 = -1.5; |
| ref.angle2 = 1.0; |
|
|
| LC_Hyperbola hb_ref(nullptr, ref); |
| double length_ref = hb_ref.getLength(); |
|
|
| REQUIRE(std::abs(length_rot - length_ref) < TOL); |
|
|
| RS_Vector coord = hb.getEndpoint(); |
|
|
| double test_dist = length_rot * 0.25; |
|
|
| RS_Vector point_rot = hb.getNearestDist(test_dist, coord); |
| REQUIRE(point_rot.valid); |
|
|
| |
| RS_Vector point_ref = hb_ref.getNearestDist( |
| test_dist, RS_Vector(0, 0)); |
| REQUIRE(point_ref.valid); |
|
|
| double phi_ref = hb_ref.getParamFromPoint(point_ref); |
| double arc_ref = hb_ref.getArcLength(ref.angle1, phi_ref); |
|
|
| double phi_rot = hb.getParamFromPoint(point_rot); |
| double arc_rot = hb.getArcLength(data.angle1, phi_rot); |
|
|
| REQUIRE(std::abs(arc_rot - arc_ref) < DIST_TOL); |
| } |
|
|
| SECTION("Invalid for unbounded hyperbola") { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(1.0, 0.0); |
| data.ratio = 0.5; |
| data.reversed = false; |
| data.angle1 = 0.0; |
| data.angle2 = 0.0; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| REQUIRE(hb.getLength() == RS_MAXDOUBLE); |
|
|
| RS_Vector point = hb.getNearestDist(10.0, RS_Vector(0, 0)); |
| REQUIRE(!point.valid); |
| } |
| } |
|
|
| TEST_CASE("LC_Hyperbola areaLineIntegral() analytical correctness", "[hyperbola][areaintegral]") |
| { |
| constexpr double TOL = 1e-10; |
|
|
| |
| auto numerical_area_integral = [](const LC_Hyperbola& hb) -> double { |
| if (!hb.isValid() || hb.isInfinite()) return 0.0; |
|
|
| double phi_min = std::min(hb.getData().angle1, hb.getData().angle2); |
| double phi_max = std::max(hb.getData().angle1, hb.getData().angle2); |
|
|
| double cx = hb.getData().center.x; |
| double cy = hb.getData().center.y; |
| double cos_th = std::cos(hb.getData().majorP.angle()); |
| double sin_th = std::sin(hb.getData().majorP.angle()); |
| double a = hb.getMajorRadius(); |
| double b = hb.getMinorRadius(); |
| int sign_x = hb.getData().reversed ? -1 : 1; |
|
|
| auto x_world = [cx, cos_th, sin_th, a, b, sign_x](double phi) { |
| double lx = sign_x * a * std::cosh(phi); |
| double ly = b * std::sinh(phi); |
| return cx + lx * cos_th - ly * sin_th; |
| }; |
|
|
| auto dy_dphi = [cos_th, sin_th, a, b, sign_x](double phi) { |
| double dlx_dphi = sign_x * a * std::sinh(phi); |
| double dly_dphi = b * std::cosh(phi); |
| return dlx_dphi * sin_th + dly_dphi * cos_th; |
| }; |
|
|
| auto integrand = [x_world, dy_dphi](double phi) { |
| return x_world(phi) * dy_dphi(phi); |
| }; |
|
|
| double result = 0.0; |
| double abs_error = 0.0; |
|
|
| if (phi_min < -RS_TOLERANCE && phi_max > RS_TOLERANCE) { |
| result = boost::math::quadrature::gauss_kronrod<double, 61>::integrate( |
| integrand, phi_min, 0.0, 0, 1e-12, &abs_error) + |
| boost::math::quadrature::gauss_kronrod<double, 61>::integrate( |
| integrand, 0.0, phi_max, 0, 1e-12, &abs_error); |
| } else { |
| result = boost::math::quadrature::gauss_kronrod<double, 61>::integrate( |
| integrand, phi_min, phi_max, 0, 1e-12, &abs_error); |
| } |
|
|
| return (hb.getData().angle2 >= hb.getData().angle1) ? result : -result; |
| }; |
|
|
| SECTION("Centered, no rotation, ratio 0.5") |
| { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(3.0, 0.0); |
| data.ratio = 0.5; |
| data.angle1 = -1.0; |
| data.angle2 = 1.5; |
| data.reversed = false; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double analytical = hb.areaLineIntegral(); |
| double numerical = numerical_area_integral(hb); |
|
|
| REQUIRE(analytical == Approx(numerical).epsilon(TOL)); |
| } |
|
|
| SECTION("Non-centered, no rotation") |
| { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(5.0, 2.0); |
| data.majorP = RS_Vector(3.0, 0.0); |
| data.ratio = 0.5; |
| data.angle1 = -1.0; |
| data.angle2 = 1.5; |
| data.reversed = false; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double analytical = hb.areaLineIntegral(); |
| double numerical = numerical_area_integral(hb); |
|
|
| REQUIRE(analytical == Approx(numerical).epsilon(TOL)); |
| } |
|
|
| SECTION("Rotated 30 degrees, centered") |
| { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(0.0, 0.0); |
| data.majorP = RS_Vector(3.0, 0.0).rotate(M_PI/6); |
| data.ratio = 0.5; |
| data.angle1 = -1.0; |
| data.angle2 = 1.5; |
| data.reversed = false; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double analytical = hb.areaLineIntegral(); |
| double numerical = numerical_area_integral(hb); |
|
|
| REQUIRE(analytical == Approx(numerical).epsilon(TOL)); |
| } |
|
|
| SECTION("Rotated 30 degrees, non-centered") |
| { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(5.0, 2.0); |
| data.majorP = RS_Vector(3.0, 0.0).rotate(M_PI/6); |
| data.ratio = 0.5; |
| data.angle1 = -1.0; |
| data.angle2 = 1.5; |
| data.reversed = false; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double analytical = hb.areaLineIntegral(); |
| double numerical = numerical_area_integral(hb); |
|
|
| REQUIRE(analytical == Approx(numerical).epsilon(TOL)); |
| } |
|
|
| SECTION("Rectangular hyperbola (ratio=1)") |
| { |
| LC_HyperbolaData data; |
| data.center = RS_Vector(5.0, 2.0); |
| data.majorP = RS_Vector(2.0, 0.0).rotate(M_PI/4); |
| data.ratio = 1.0; |
| data.angle1 = 0.5; |
| data.angle2 = 2.0; |
| data.reversed = false; |
|
|
| LC_Hyperbola hb(nullptr, data); |
| REQUIRE(hb.isValid()); |
|
|
| double analytical = hb.areaLineIntegral(); |
| double numerical = numerical_area_integral(hb); |
|
|
| REQUIRE(analytical == Approx(numerical).epsilon(TOL)); |
| } |
| } |
|
|