| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | #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)); |
| | } |
| | } |
| |
|