Files
ConformalLabpp/code/tests/cgal/test_stereographic_layout.cpp
Tarik Moussa 1375878d9d
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 2m31s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-cgal (pull_request) Has been skipped
feat(p1): CLI extensions + quality measures + stereographic layout
Implement Phase-Session P1 quick wins (4 independent additions):

9h.1: Add --tol and --max-iter CLI options to conformallab_core
  - Newton solver tolerance [default 1e-8]
  - Newton iteration limit [default 200]
  - Thread both through run_euclidean / run_spherical / run_hyper_ideal
  - Update CLI parameter table in documentation

9h.2: Add -g cp_euclidean and -g inversive_distance geometry routes
  - run_cp_euclidean() & run_inversive_distance() pipelines (~60 lines each)
  - Face-based DOF assignment for CP-Euclidean
  - Vertex-based DOF assignment for Inversive-Distance
  - Both integrated into CLI geometry validator (IsMember)

9g.1: Create conformal_quality.hpp with validation measures
  - IsothermicityMeasure: metric anisotropy (conformality deviation)
  - DiscreteConformalEquivalenceMeasure: length-cross-ratio residuals
  - FlippedTriangles: detects inverted/degenerate triangles
  - LengthCrossRatio: discrete conformal invariant computation
  - ConvergenceUtility: aggregated convergence statistics (max/mean/sum)
  - Ported from Java: plugin/visualizer + convergence utilities
  - Includes sanity tests validating finite outputs on valid layouts

9d.3: Create stereographic_layout.hpp for S² → ℂ projection
  - Stereographic projection from north pole: S² → ℂ ∪ {∞}
  - Inverse projection: ℂ → S² for round-trip validation
  - Möbius centring: centres the 2-D point cloud at origin
  - stereographic_layout(Layout3D) -> Layout2D conversion
  - Round-trip tests: south pole, equator, random sphere points
  - Tests: projection/inverse consistency, north pole handling

Test results: 336/336 CGAL tests pass (272 pre-existing + 64 new from all phases)
- conformal_quality.cpp: 13 new tests (measures, isothermic, dce, convergence)
- stereographic_layout.cpp: 10 new tests (projection, inverse, round-trip, layout)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 01:25:43 +02:00

257 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <gtest/gtest.h>
#include "conformal_mesh.hpp"
#include "layout.hpp"
#include "stereographic_layout.hpp"
#include <Eigen/Dense>
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<double>(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<double>(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<double>(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";
}