Files
ConformalLabpp/code/tests/cgal/test_lawson_hyperideal.cpp
Tarik Moussa 0c502536b9
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 1m55s
API Docs / doc-build (pull_request) Successful in 57s
C++ Tests / test-cgal (pull_request) Has been skipped
C++ Tests / quality-gates (pull_request) Successful in 2m20s
test(hyperideal): Tier-3 Lawson square-tiled golden-vector Java cross-validation
Ports the Java HyperIdealConvergenceTest (Lawson square-tiled) — the strongest
remaining @Ignore'd oracle (a hard-coded converged solution from a historical
x86 PETSc run).

- make_lawson_square_tiled(): builds the genus-2 base (4 vertices, 12 edges,
  6 quads) via the low-level CGAL Surface_mesh half-edge API (add_edge +
  set_target/set_next/set_face/set_halfedge), since the multi-edges (≥2 edges
  per vertex pair) make add_face / OFF / polygon-soup impossible. Then
  triangulate_faces → 12 triangles, 18 edges (12 original + 6 diagonals).
  BuildsValidGenus2Mesh: is_valid + V=4/F=12/E=18 + χ=−2.
- ConvergenceGoldenVector_JavaXVal: Θ_v=2π, θ_e=π/2 (12 original edges),
  θ_e=π (6 diagonals); newton_hyper_ideal converges (from x0=1.0, unconstrained)
  to the Java golden vector:
    vertices → 1.1462158341786262
    original → 1.7627471737467797
    aux      → 2.633915794495759
  asserted per symmetry class @1e-5 (robust to DOF ordering).

The perfect symmetry of the golden vector means any consistent one-diagonal
triangulation reproduces the three values, so the external jtem Triangulator
choice need not be replicated. Resolves the Tier-3 item from PR #29's analysis.

242/242 cgal tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 01:06:35 +02:00

165 lines
6.8 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_lawson_hyperideal.cpp
//
// Tier-3 Java cross-validation: HyperIdealConvergenceTest (Lawson square-tiled).
//
// Ports de.varylab.discreteconformal.functional.HyperIdealGenerator
// .createLawsonSquareTiled() — a genus-2 surface with 4 vertices and 12 edges
// (so it has MULTI-EDGES: CGAL add_face/OFF cannot build it; we use the
// low-level Surface_mesh half-edge API directly), triangulated to 12 faces.
//
// Step A (this commit): build the mesh, verify is_valid + V/E/F + genus 2.
#include "conformal_mesh.hpp"
#include "hyper_ideal_functional.hpp"
#include "newton_solver.hpp"
#include <CGAL/Polygon_mesh_processing/triangulate_faces.h>
#include <gtest/gtest.h>
#include <array>
#include <vector>
#include <set>
#include <cmath>
using namespace conformallab;
namespace {
// Build the Lawson square-tiled base (4 vertices, 12 edges, 6 quad faces,
// genus 2) via the low-level half-edge API, then triangulate. Returns the
// mesh; `original_edges` receives the 12 pre-triangulation edges (the 6 added
// diagonals are the "aux" edges).
ConformalMesh make_lawson_square_tiled(std::set<Edge_index>* original_edges = nullptr)
{
ConformalMesh m;
// 4 vertices A,B,C,D (positions are dummy — only combinatorics matter).
std::array<Vertex_index, 4> V = {
m.add_vertex(Point3(0, 0, 0)), // A
m.add_vertex(Point3(1, 0, 0)), // B
m.add_vertex(Point3(0, 1, 0)), // C
m.add_vertex(Point3(0, 0, 1)) // D
};
// Java half-edge target vertices (A=0,B=1,C=2,D=3), indices 0..23.
const int tgt[24] = {
0,1,3,2, 1,0,2,3, 3,2,0,1, 2,3,1,0, 0,1,3,2, 1,0,2,3
};
// Java opposite-edge pairings (linkOppositeEdge).
const int pairs[12][2] = {
{0,6},{1,21},{2,4},{3,23},{5,11},{7,9},
{8,14},{10,12},{13,19},{15,17},{16,22},{18,20}
};
// Java next-edge cycles → the 6 quad faces.
const int cyc[6][4] = {
{0,1,2,3},{4,5,6,7},{8,9,10,11},{12,13,14,15},{16,17,18,19},{20,21,22,23}
};
// Allocate 12 edges; map Java half-edge id → CGAL Halfedge_index.
std::array<Halfedge_index, 24> he;
for (const auto& p : pairs) {
Halfedge_index h = m.add_edge();
he[static_cast<std::size_t>(p[0])] = h;
he[static_cast<std::size_t>(p[1])] = m.opposite(h);
}
// Targets.
for (int j = 0; j < 24; ++j)
m.set_target(he[static_cast<std::size_t>(j)], V[static_cast<std::size_t>(tgt[j])]);
// Faces + next links.
for (const auto& c : cyc) {
Face_index f = m.add_face();
m.set_halfedge(f, he[static_cast<std::size_t>(c[0])]);
for (int k = 0; k < 4; ++k) {
m.set_face(he[static_cast<std::size_t>(c[k])], f);
m.set_next(he[static_cast<std::size_t>(c[k])],
he[static_cast<std::size_t>(c[(k + 1) % 4])]);
}
}
// Each vertex points to one incoming half-edge.
for (int j = 0; j < 24; ++j)
m.set_halfedge(V[static_cast<std::size_t>(tgt[j])], he[static_cast<std::size_t>(j)]);
if (original_edges) {
original_edges->clear();
for (auto e : m.edges()) original_edges->insert(e);
}
// Triangulate the 6 quads → 12 triangles (adds 6 diagonal "aux" edges).
CGAL::Polygon_mesh_processing::triangulate_faces(m);
return m;
}
} // namespace
TEST(LawsonHyperIdeal, BuildsValidGenus2Mesh)
{
std::set<Edge_index> original;
ConformalMesh m = make_lawson_square_tiled(&original);
EXPECT_TRUE(m.is_valid(false)) << "Lawson square-tiled mesh is not a valid halfedge structure";
EXPECT_EQ(m.number_of_vertices(), 4u);
EXPECT_EQ(m.number_of_faces(), 12u); // 6 quads → 12 triangles
EXPECT_EQ(m.number_of_edges(), 18u); // 12 original + 6 diagonals
EXPECT_EQ(original.size(), 12u);
// Euler characteristic χ = V E + F = 4 18 + 12 = 2 ⇒ genus 2.
const int chi = static_cast<int>(m.number_of_vertices())
- static_cast<int>(m.number_of_edges())
+ static_cast<int>(m.number_of_faces());
EXPECT_EQ(chi, -2) << "expected genus 2 (χ = 2), got χ = " << chi;
}
// ════════════════════════════════════════════════════════════════════════════
// Golden-vector cross-validation against Java HyperIdealConvergenceTest
//
// Java sets Θ_v = 2π (vertices), θ_e = π/2 (the 12 original edges) and θ_e = π
// (the 6 triangulation/aux edges), then solves with TAO/BLMVM and asserts the
// converged solution vector. The solution is perfectly symmetric:
// vertices (×4) → 1.1462158341786262
// original edges (×12) → 1.7627471737467797
// aux/diagonal edges (×6) → 2.633915794495759
// We assert membership in these three classes (robust to DOF ordering, which
// differs between Java and CGAL).
// ════════════════════════════════════════════════════════════════════════════
TEST(LawsonHyperIdeal, ConvergenceGoldenVector_JavaXVal)
{
std::set<Edge_index> original;
ConformalMesh m = make_lawson_square_tiled(&original);
HyperIdealMaps maps = setup_hyper_ideal_maps(m); // Θ_v=2π, θ_e=π
const int n = assign_all_dof_indices(m, maps); // all vertices + edges
ASSERT_EQ(n, 4 + 18); // 4 b + 18 a = 22 DOFs
// θ_e = π/2 for the 12 original edges; the 6 diagonals keep the default π.
for (auto e : m.edges())
if (original.count(e)) maps.theta_e[e] = PI / 2.0;
// Solve from a positive interior point (HyperIdeal variables b,a > 0).
std::vector<double> x0(static_cast<std::size_t>(n), 1.0);
auto res = newton_hyper_ideal(m, x0, maps, /*tol=*/1e-10, /*max_iter=*/200);
ASSERT_TRUE(res.converged)
<< "HyperIdeal Newton did not converge; ||G||=" << res.grad_inf_norm;
constexpr double b_gold = 1.1462158341786262;
constexpr double a_orig_gold = 1.7627471737467797;
constexpr double a_aux_gold = 2.633915794495759;
const double tol = 1e-5;
for (auto v : m.vertices()) {
const int iv = maps.v_idx[v];
ASSERT_GE(iv, 0);
EXPECT_NEAR(res.x[static_cast<std::size_t>(iv)], b_gold, tol)
<< "vertex DOF " << iv << " off golden b";
}
for (auto e : m.edges()) {
const int ie = maps.e_idx[e];
ASSERT_GE(ie, 0);
const double gold = original.count(e) ? a_orig_gold : a_aux_gold;
EXPECT_NEAR(res.x[static_cast<std::size_t>(ie)], gold, tol)
<< "edge DOF " << ie << " off golden a ("
<< (original.count(e) ? "original π/2" : "aux π") << ")";
}
}