From ae5db7216e4fa638868a84a424a5060043ffca11 Mon Sep 17 00:00:00 2001 From: Tarik Moussa Date: Mon, 11 May 2026 17:23:15 +0200 Subject: [PATCH] tests: port HyperIdealVisualizationPlugin circle-projection tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../hyper_ideal_visualization_utility.hpp | 129 ++++++++++++++++++ code/tests/CMakeLists.txt | 1 + ...test_hyper_ideal_visualization_utility.cpp | 50 +++++++ 3 files changed, 180 insertions(+) create mode 100644 code/include/hyper_ideal_visualization_utility.hpp create mode 100644 code/tests/test_hyper_ideal_visualization_utility.cpp diff --git a/code/include/hyper_ideal_visualization_utility.hpp b/code/include/hyper_ideal_visualization_utility.hpp new file mode 100644 index 0000000..f5ec00a --- /dev/null +++ b/code/include/hyper_ideal_visualization_utility.hpp @@ -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 +#include +#include + +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 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 diff --git a/code/tests/CMakeLists.txt b/code/tests/CMakeLists.txt index 1c09b91..dec6b8b 100644 --- a/code/tests/CMakeLists.txt +++ b/code/tests/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable(conformallab_tests test_surface_curve_utility.cpp test_discrete_elliptic_utility.cpp test_p2_utility.cpp + test_hyper_ideal_visualization_utility.cpp # ── Stubs: blocked until HDS port (Phase 4) ────────────────────────────── # All tests call GTEST_SKIP() with a clear explanation. diff --git a/code/tests/test_hyper_ideal_visualization_utility.cpp b/code/tests/test_hyper_ideal_visualization_utility.cpp new file mode 100644 index 0000000..8f73f6b --- /dev/null +++ b/code/tests/test_hyper_ideal_visualization_utility.cpp @@ -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 +#include + +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)"; +}