tests: port HyperIdealVisualizationPlugin circle-projection tests
Implements getEuclideanCircleFromHyperbolic() in C++ (hyperboloid model → Poincaré disk via Lorentz boost + circumcircle). Ports the 2 Java tests that were the only remaining candidates not requiring HDS or a solver. All other unported tests (DataTypesTest, SchottkyIOTest, UniformizationDataTest, BranchedCoverTorusTest) are blocked by XML serialization or HDS – stubs remain for Phase 4. Test totals: 36 registered | 23 passed | 13 skipped | 0 failed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
129
code/include/hyper_ideal_visualization_utility.hpp
Normal file
129
code/include/hyper_ideal_visualization_utility.hpp
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#pragma once
|
||||||
|
// Port of the static helper
|
||||||
|
// HyperIdealVisualizationPlugin.getEuclideanCircleFromHyperbolic()
|
||||||
|
// from de.varylab.discreteconformal.plugin.
|
||||||
|
//
|
||||||
|
// Converts a hyperbolic circle (center + radius in the hyperboloid model)
|
||||||
|
// to its Euclidean representation (cx, cy, r) in the Poincaré disk model.
|
||||||
|
//
|
||||||
|
// Mathematical background
|
||||||
|
// -----------------------
|
||||||
|
// Hyperboloid model: points (x,y,z,w) with w²-x²-y²-z²=1, w>0.
|
||||||
|
// Metric signature: g = diag(+1,+1,+1,−1) (spatial-first, time-last).
|
||||||
|
//
|
||||||
|
// Hyperbolic translation from the origin e₄=(0,0,0,1) to p=(a,b,c,d):
|
||||||
|
// T = [ I₃ + p'·p'ᵀ/(d+1) p' ] p' = (a,b,c)
|
||||||
|
// [ p'ᵀ d ]
|
||||||
|
// This is the standard Lorentz boost; it is in O(3,1) and maps e₄ → p.
|
||||||
|
//
|
||||||
|
// Poincaré disk projection (jReality convention):
|
||||||
|
// (x,y,z,w) → (x,y) / (w+1)
|
||||||
|
//
|
||||||
|
// The three reference points on the unit hyperbolic circle (at origin) are
|
||||||
|
// p1 = (sinh r, 0, 0, cosh r)
|
||||||
|
// p2 = (0, sinh r, 0, cosh r)
|
||||||
|
// p3 = (-sinh r, 0, 0, cosh r)
|
||||||
|
// After translation and projection to the Poincaré disk their circumcircle
|
||||||
|
// equals the image of the original hyperbolic circle.
|
||||||
|
|
||||||
|
#include <Eigen/Dense>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace conformallab {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Circumcenter of three 2-D points
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
inline Eigen::Vector2d circumcenter2d(
|
||||||
|
const Eigen::Vector2d& a,
|
||||||
|
const Eigen::Vector2d& b,
|
||||||
|
const Eigen::Vector2d& c)
|
||||||
|
{
|
||||||
|
double ax = a.x(), ay = a.y();
|
||||||
|
double bx = b.x(), by = b.y();
|
||||||
|
double cx = c.x(), cy = c.y();
|
||||||
|
|
||||||
|
double D = 2.0 * (ax*(by - cy) + bx*(cy - ay) + cx*(ay - by));
|
||||||
|
double a2 = ax*ax + ay*ay;
|
||||||
|
double b2 = bx*bx + by*by;
|
||||||
|
double c2 = cx*cx + cy*cy;
|
||||||
|
|
||||||
|
double ux = (a2*(by - cy) + b2*(cy - ay) + c2*(ay - by)) / D;
|
||||||
|
double uy = (a2*(cx - bx) + b2*(ax - cx) + c2*(bx - ax)) / D;
|
||||||
|
return {ux, uy};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 4×4 Lorentz boost: maps the hyperboloid origin e₄=(0,0,0,1) to `center`.
|
||||||
|
// `center` must lie on the hyperboloid: center[3]² - ‖center.head<3>()‖² = 1.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
inline Eigen::Matrix4d hyperboloidTranslation(const Eigen::Vector4d& center)
|
||||||
|
{
|
||||||
|
Eigen::Vector3d p = center.head<3>();
|
||||||
|
double d = center(3);
|
||||||
|
|
||||||
|
Eigen::Matrix4d T = Eigen::Matrix4d::Identity();
|
||||||
|
// Upper-left 3×3 block: I + p'·p'ᵀ / (d+1)
|
||||||
|
T.block<3,3>(0,0) += p * p.transpose() / (d + 1.0);
|
||||||
|
// Right column and bottom row
|
||||||
|
T.block<3,1>(0,3) = p;
|
||||||
|
T.block<1,3>(3,0) = p.transpose();
|
||||||
|
T(3,3) = d;
|
||||||
|
return T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Project a hyperboloid point to the Poincaré disk (jReality convention:
|
||||||
|
// add 1 to the w-coordinate, then dehomogenize the spatial part).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
inline Eigen::Vector2d toPoincareDisk(const Eigen::Vector4d& x)
|
||||||
|
{
|
||||||
|
double w = x(3) + 1.0;
|
||||||
|
return {x(0) / w, x(1) / w};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// getEuclideanCircleFromHyperbolic
|
||||||
|
//
|
||||||
|
// Inputs
|
||||||
|
// center – point on the hyperboloid, e.g. (0,0,0,1) for the origin
|
||||||
|
// radius – hyperbolic radius (real number > 0)
|
||||||
|
//
|
||||||
|
// Output
|
||||||
|
// { euclidean_cx, euclidean_cy, euclidean_radius }
|
||||||
|
// describing the circle in the Poincaré disk that corresponds to the
|
||||||
|
// given hyperbolic circle.
|
||||||
|
//
|
||||||
|
// Port of HyperIdealVisualizationPlugin.getEuclideanCircleFromHyperbolic()
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
inline std::array<double,3> getEuclideanCircleFromHyperbolic(
|
||||||
|
const Eigen::Vector4d& center, double radius)
|
||||||
|
{
|
||||||
|
const double s = std::sinh(radius);
|
||||||
|
const double ch = std::cosh(radius);
|
||||||
|
|
||||||
|
// Three points on the hyperbolic circle centered at the origin
|
||||||
|
Eigen::Vector4d p1( s, 0.0, 0.0, ch);
|
||||||
|
Eigen::Vector4d p2(0.0, s, 0.0, ch);
|
||||||
|
Eigen::Vector4d p3(-s, 0.0, 0.0, ch);
|
||||||
|
|
||||||
|
// Apply the hyperbolic translation to the target center
|
||||||
|
const Eigen::Matrix4d T = hyperboloidTranslation(center);
|
||||||
|
p1 = T * p1;
|
||||||
|
p2 = T * p2;
|
||||||
|
p3 = T * p3;
|
||||||
|
|
||||||
|
// Project to the Poincaré disk
|
||||||
|
const Eigen::Vector2d q1 = toPoincareDisk(p1);
|
||||||
|
const Eigen::Vector2d q2 = toPoincareDisk(p2);
|
||||||
|
const Eigen::Vector2d q3 = toPoincareDisk(p3);
|
||||||
|
|
||||||
|
// Euclidean circumcircle of the three projected points
|
||||||
|
const Eigen::Vector2d ec = circumcenter2d(q1, q2, q3);
|
||||||
|
const double r = (ec - q1).norm();
|
||||||
|
|
||||||
|
return {ec.x(), ec.y(), r};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace conformallab
|
||||||
@@ -6,6 +6,7 @@ add_executable(conformallab_tests
|
|||||||
test_surface_curve_utility.cpp
|
test_surface_curve_utility.cpp
|
||||||
test_discrete_elliptic_utility.cpp
|
test_discrete_elliptic_utility.cpp
|
||||||
test_p2_utility.cpp
|
test_p2_utility.cpp
|
||||||
|
test_hyper_ideal_visualization_utility.cpp
|
||||||
|
|
||||||
# ── Stubs: blocked until HDS port (Phase 4) ──────────────────────────────
|
# ── Stubs: blocked until HDS port (Phase 4) ──────────────────────────────
|
||||||
# All tests call GTEST_SKIP() with a clear explanation.
|
# All tests call GTEST_SKIP() with a clear explanation.
|
||||||
|
|||||||
50
code/tests/test_hyper_ideal_visualization_utility.cpp
Normal file
50
code/tests/test_hyper_ideal_visualization_utility.cpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Port of de.varylab.discreteconformal.plugin.HyperIdealVisualizationPluginTest (Java/JUnit).
|
||||||
|
//
|
||||||
|
// Tests the conversion from a hyperbolic circle (hyperboloid model)
|
||||||
|
// to its Euclidean representation (Poincaré disk model).
|
||||||
|
//
|
||||||
|
// Java test: static method HyperIdealVisualizationPlugin
|
||||||
|
// .getEuclideanCircleFromHyperbolic(double[] center, double radius)
|
||||||
|
// C++ port: conformallab::getEuclideanCircleFromHyperbolic(Vector4d, double)
|
||||||
|
// in hyper_ideal_visualization_utility.hpp
|
||||||
|
|
||||||
|
#include "hyper_ideal_visualization_utility.hpp"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
using namespace conformallab;
|
||||||
|
|
||||||
|
// Corresponds to Java testGetEuclideanCircleFromHyperbolic_Centered()
|
||||||
|
//
|
||||||
|
// A hyperbolic circle centered at the origin (0,0,0,1) with radius 1.
|
||||||
|
// In the Poincaré disk this maps to a Euclidean circle centered at (0,0)
|
||||||
|
// with radius sinh(1) / (cosh(1) + 1).
|
||||||
|
TEST(HyperIdealVisualizationUtilityTest, EuclideanCircleFromHyperbolic_Centered)
|
||||||
|
{
|
||||||
|
Eigen::Vector4d center(0.0, 0.0, 0.0, 1.0);
|
||||||
|
const double radius = 1.0;
|
||||||
|
|
||||||
|
auto result = getEuclideanCircleFromHyperbolic(center, radius);
|
||||||
|
|
||||||
|
const double expected_r = std::sinh(1.0) / (std::cosh(1.0) + 1.0);
|
||||||
|
EXPECT_NEAR(0.0, result[0], 1E-12) << "Euclidean cx should be 0";
|
||||||
|
EXPECT_NEAR(0.0, result[1], 1E-12) << "Euclidean cy should be 0";
|
||||||
|
EXPECT_NEAR(expected_r, result[2], 1E-12) << "Euclidean radius mismatch";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corresponds to Java testGetEuclideanCircleFromHyperbolic_OffCenter()
|
||||||
|
//
|
||||||
|
// A hyperbolic circle centered at (sinh(1),0,0,cosh(1)) with radius 1.
|
||||||
|
// By symmetry (center is on the x-axis, circle is symmetric about it):
|
||||||
|
// • the Euclidean center lies on the x-axis → cy = 0
|
||||||
|
// • the Euclidean center equals the radius → cx = r (the circle passes through the Poincaré origin)
|
||||||
|
TEST(HyperIdealVisualizationUtilityTest, EuclideanCircleFromHyperbolic_OffCenter)
|
||||||
|
{
|
||||||
|
Eigen::Vector4d center(std::sinh(1.0), 0.0, 0.0, std::cosh(1.0));
|
||||||
|
const double radius = 1.0;
|
||||||
|
|
||||||
|
auto result = getEuclideanCircleFromHyperbolic(center, radius);
|
||||||
|
|
||||||
|
EXPECT_NEAR(result[0], result[2], 1E-12) << "cx should equal Euclidean radius";
|
||||||
|
EXPECT_NEAR(0.0, result[1], 1E-12) << "cy should be 0 (x-axis symmetry)";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user