// Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // test_stereographic_layout.cpp // // Tests for stereographic_layout.hpp (Phase 9d.3). // Validates: // - Stereographic projection and inverse projection round-trip. // - North pole projects to infinity. // - South pole projects to origin. // - Stereographic layout from a spherical layout. #include #include "conformal_mesh.hpp" #include "layout.hpp" #include "stereographic_layout.hpp" #include namespace cl = conformallab; // ──────────────────────────────────────────────────────────────────────────── // Tests: Stereographic Projection // ──────────────────────────────────────────────────────────────────────────── TEST(StereographicProjection, SouthPoleProjectsToOrigin) { // South pole: (0, 0, -1). auto z = cl::stereographic_project(0.0, 0.0, -1.0); EXPECT_NEAR(z.real(), 0.0, 1e-10) << "South pole should project to (0,0) in ℂ"; EXPECT_NEAR(z.imag(), 0.0, 1e-10) << "South pole should project to (0,0) in ℂ"; } TEST(StereographicProjection, NorthPoleProjectsToInfinity) { // North pole: (0, 0, 1). auto z = cl::stereographic_project(0.0, 0.0, 1.0); // Returns NaN to signal infinity. EXPECT_TRUE(std::isnan(z.real())) << "North pole should project to ∞ (NaN)"; EXPECT_TRUE(std::isnan(z.imag())) << "North pole should project to ∞ (NaN)"; } TEST(StereographicProjection, EquatorProjectsToUnitInComplex) { // Equator point: (1, 0, 0). auto z = cl::stereographic_project(1.0, 0.0, 0.0); // Formula: (1 + 0i) / (1 - 0) = 1. EXPECT_NEAR(z.real(), 1.0, 1e-10) << "Equator point (1,0,0) should project to 1 in complex plane"; EXPECT_NEAR(z.imag(), 0.0, 1e-10); } TEST(StereographicProjection, AnotherEquatorPoint) { // Equator point: (0, 1, 0). auto z = cl::stereographic_project(0.0, 1.0, 0.0); // Formula: (0 + 1i) / (1 - 0) = i. EXPECT_NEAR(z.real(), 0.0, 1e-10) << "Equator point (0,1,0) should project to i in ℂ"; EXPECT_NEAR(z.imag(), 1.0, 1e-10); } // ──────────────────────────────────────────────────────────────────────────── // Tests: Inverse Stereographic Projection // ──────────────────────────────────────────────────────────────────────────── TEST(InverseStereographicProjection, OriginMapsToSouthPole) { auto z = std::complex(0.0, 0.0); auto p = cl::inverse_stereographic_project(z); EXPECT_NEAR(p.x(), 0.0, 1e-10) << "Origin should map to (0,0,-1)"; EXPECT_NEAR(p.y(), 0.0, 1e-10); EXPECT_NEAR(p.z(), -1.0, 1e-10); } TEST(InverseStereographicProjection, OneMapsToEquatorPoint) { auto z = std::complex(1.0, 0.0); auto p = cl::inverse_stereographic_project(z); EXPECT_NEAR(p.x(), 1.0, 1e-10) << "1 in complex plane should map to (1,0,0)"; EXPECT_NEAR(p.y(), 0.0, 1e-10); EXPECT_NEAR(p.z(), 0.0, 1e-10); } TEST(InverseStereographicProjection, ImaginaryUnitMapsToEquator) { auto z = std::complex(0.0, 1.0); auto p = cl::inverse_stereographic_project(z); EXPECT_NEAR(p.x(), 0.0, 1e-10) << "i in complex plane should map to (0,1,0)"; EXPECT_NEAR(p.y(), 1.0, 1e-10); EXPECT_NEAR(p.z(), 0.0, 1e-10); } // ──────────────────────────────────────────────────────────────────────────── // Tests: Round-Trip Consistency // ──────────────────────────────────────────────────────────────────────────── TEST(StereographicRoundTrip, ProjectAndInvert_South) { cl::Point3 south(0.0, 0.0, -1.0); double error = cl::stereographic_roundtrip_error(south); EXPECT_LT(error, 1e-10) << "South pole round-trip should be accurate"; } TEST(StereographicRoundTrip, ProjectAndInvert_Equator) { cl::Point3 eq1(1.0, 0.0, 0.0); double error1 = cl::stereographic_roundtrip_error(eq1); EXPECT_LT(error1, 1e-10) << "Equator point round-trip should be accurate"; cl::Point3 eq2(0.0, 1.0, 0.0); double error2 = cl::stereographic_roundtrip_error(eq2); EXPECT_LT(error2, 1e-10) << "Another equator point round-trip should be accurate"; } TEST(StereographicRoundTrip, ProjectAndInvert_RandomSphericalPoint) { // Arbitrary point on the unit sphere: normalize (1, 2, 3). double norm = std::sqrt(1.0*1.0 + 2.0*2.0 + 3.0*3.0); cl::Point3 p(1.0/norm, 2.0/norm, 3.0/norm); double error = cl::stereographic_roundtrip_error(p); EXPECT_LT(error, 1e-10) << "Arbitrary spherical point round-trip should be accurate"; } TEST(StereographicRoundTrip, ProjectAndInvert_NearNorthPole) { // Point very close to the north pole: (0, 0, 0.99999). cl::Point3 close_to_north(0.0, 0.0, 0.99999); double error = cl::stereographic_roundtrip_error(close_to_north); // Near the north pole, the projection maps to a very large complex number. // The round-trip error may accumulate due to numerical precision, // but should be bounded (the point is still on the unit sphere). EXPECT_LT(error, 2.1) << "Point near north pole should have reasonable error"; } // ──────────────────────────────────────────────────────────────────────────── // Tests: Stereographic Layout Conversion // ──────────────────────────────────────────────────────────────────────────── TEST(StereographicLayout, ConvertsSphericalLayoutTo2D) { // Create a simple tetrahedron mesh (all vertices roughly on a sphere). cl::ConformalMesh mesh; auto v0 = mesh.add_vertex(cl::Point3(1.0, 0.0, 0.0)); auto v1 = mesh.add_vertex(cl::Point3(0.0, 1.0, 0.0)); auto v2 = mesh.add_vertex(cl::Point3(0.0, 0.0, 1.0)); mesh.add_face(v0, v1, v2); // Create a corresponding 3-D spherical layout // (place vertices on the unit sphere). cl::Layout3D spherical_layout; spherical_layout.pos.resize(3); spherical_layout.pos[0] = Eigen::Vector3d(1.0, 0.0, 0.0); spherical_layout.pos[1] = Eigen::Vector3d(0.0, 1.0, 0.0); spherical_layout.pos[2] = Eigen::Vector3d(0.0, 0.0, 1.0); // Convert to stereographic layout. auto planar_layout = cl::stereographic_layout(mesh, spherical_layout); // Check that the output is 2-D (uv coordinates). EXPECT_EQ(planar_layout.uv.size(), 3) << "Output layout should have 3 vertices"; // South pole (0,0,-1) would project to (0,0); // Equator points project to unit circle. // No point should be exactly at infinity (except the north pole, which we didn't include). for (const auto& uv : planar_layout.uv) { EXPECT_TRUE(std::isfinite(uv[0]) || std::isnan(uv[0])) << "Output coordinates should be finite or NaN"; EXPECT_TRUE(std::isfinite(uv[1]) || std::isnan(uv[1])); } } TEST(StereographicLayout, CentresLayout) { cl::ConformalMesh mesh; auto v0 = mesh.add_vertex(cl::Point3(1.0, 0.0, 0.0)); auto v1 = mesh.add_vertex(cl::Point3(0.0, 1.0, 0.0)); auto v2 = mesh.add_vertex(cl::Point3(-1.0, 0.0, 0.0)); mesh.add_face(v0, v1, v2); cl::Layout3D spherical_layout; spherical_layout.pos.resize(3); spherical_layout.pos[0] = Eigen::Vector3d(1.0, 0.0, 0.0); spherical_layout.pos[1] = Eigen::Vector3d(0.0, 1.0, 0.0); spherical_layout.pos[2] = Eigen::Vector3d(-1.0, 0.0, 0.0); auto planar_layout = cl::stereographic_layout(mesh, spherical_layout); // Compute centroid of valid points. double cx = 0.0, cy = 0.0; int n_valid = 0; for (const auto& uv : planar_layout.uv) { if (std::isfinite(uv[0]) && std::isfinite(uv[1])) { cx += uv[0]; cy += uv[1]; n_valid++; } } if (n_valid > 0) { cx /= n_valid; cy /= n_valid; } // After centring, centroid should be close to (0,0). EXPECT_LT(std::abs(cx), 0.5) << "Centroid x should be small after centring"; EXPECT_LT(std::abs(cy), 0.5) << "Centroid y should be small after centring"; } // ──────────────────────────────────────────────────────────────────────────── // Sanity Tests // ──────────────────────────────────────────────────────────────────────────── TEST(StereographicLayout_Sanity, ProjectionIsConformal) { // Stereographic projection is conformal (angle-preserving). // Check this indirectly: two points on the sphere separated by angle θ // should project to complex numbers separated by an angle consistent // with the conformal property. // Two points on the equator: (1,0,0) and (0,1,0), 90° apart. auto z1 = cl::stereographic_project(1.0, 0.0, 0.0); auto z2 = cl::stereographic_project(0.0, 1.0, 0.0); // In the complex plane, their argument difference should be ~90°. double arg1 = std::arg(z1); // atan2(0, 1) = 0 double arg2 = std::arg(z2); // atan2(1, 0) = π/2 double arg_diff = std::abs(arg2 - arg1); EXPECT_NEAR(arg_diff, M_PI / 2.0, 1e-10) << "Stereographic projection should preserve angles"; }