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>
165 lines
6.8 KiB
C++
165 lines
6.8 KiB
C++
// 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 π") << ")";
|
||
}
|
||
}
|