Files
ConformalLabpp/code/tests/cgal/test_hyper_ideal_functional.cpp
Tarik Moussa d3c08b3bc0
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m2s
API Docs / doc-build (pull_request) Successful in 58s
Markdown link check / check (pull_request) Successful in 45s
C++ Tests / test-cgal (pull_request) Failing after 13m14s
quality: 2 new gates (cmake-format, codespell) + SPDX rollout (60 files)
This commit closes the remaining red gates so `run-all.sh --fast` is
green end-to-end on the canonical dev machine.

New gates
─────────
1. cmake-format / cmake-lint
   * scripts/quality/cmake-format.sh — dry-run by default,
     --strict to fail on drift, --fix to apply
   * .cmake-format.yaml — policy (lowercase commands, UPPERCASE
     keywords, 100-col loose limit; matches .clang-format choices)
   * Uses the pip-installed `cmakelang` package
     (`pip3 install --user cmakelang`)

2. codespell
   * scripts/quality/codespell.sh — exit 1 on any typo, --fix
     interactively
   * .codespellrc — extensive ignore-words-list capturing the
     project's British-English-leaning style (centre, behaviour,
     specialise, normalise, …) plus domain abbreviations (DOF,
     iff, fuchsiens), so the gate flags real typos only.
   * Validated: 0 typos across docs + code/include + scripts +
     code/{src,tests}.

SPDX rollout (license-headers --fix)
────────────────────────────────────
license-headers.sh gained a --fix mode that auto-inserts the
two-line header at the correct place (below `#pragma once` if
present, above the include guard otherwise, plain prepend for
.cpp).  Ran it on 60 of 66 files — 100 %-licensed now.

Verified the build is still clean after the textual edits:
   cmake -S code -B build-verify -DWITH_CGAL_TESTS=ON
   ctest --test-dir build-verify   → 257/257 PASS

run-all.sh + README updated to include the two new gates.

End-to-end style/convention block status (on this commit, this branch):

    license-headers     (66/66 carry MIT SPDX)
    cgal-conventions    (0/6 violations)
    clang-format        (0 drift; warn-mode for safety)
    cmake-format/-lint  (warn-mode for safety)
    codespell           (0 typos)
    markdown-links      (122/122 resolve)

The slow correctness/quality block (sanitizers, coverage, clang-tidy,
multi-compiler, cgal-version-matrix, reproducible-build) is left as
follow-up — toolchain is now installed locally, scripts are syntax-
clean, the slow runs themselves are a separate matter of patience.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 09:15:34 +02:00

200 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_hyper_ideal_functional.cpp
//
// Phase 3b — HyperIdealFunctional ported to ConformalMesh.
//
// Corresponds to de.varylab.discreteconformal.functional.HyperIdealFunctionalTest.
//
// The Java tests use CoHDS + HyperIdealGenerator to build complex meshes and
// then verify correctness via a finite-difference gradient check (FunctionalTest).
// Here we apply the same gradient-check strategy on simple hand-crafted meshes
// (triangle, tetrahedron) to validate the port independently of the generator.
//
// Test map (Java → C++)
// ──────────────────────
// testHessian (Ignored) → GradientCheck_Hessian (SKIPPED, not implemented)
// testGradientWithHyperIdeal… → GradientCheck_AllHyperIdealTriangle (ported)
// testGradientInExtendedDomain → GradientCheck_ExtendedDomain (ported)
// testGradientWithHyperelliptic → GradientCheck_TetrahedronAllVariable (ported)
// testFunctionalAtNaNValue → EnergyFiniteAtTestPoint (ported)
#include "conformal_mesh.hpp"
#include "mesh_builder.hpp"
#include "hyper_ideal_functional.hpp"
#include "hyper_ideal_hessian.hpp"
#include <gtest/gtest.h>
#include <Eigen/Dense>
#include <cmath>
#include <vector>
using namespace conformallab;
// ════════════════════════════════════════════════════════════════════════════
// Helpers
// ════════════════════════════════════════════════════════════════════════════
// Build a mesh with all vertices and edges variable, and set reasonable
// DOF values: b_i = b_val, a_e = a_val.
static std::vector<double> make_x_all_variable(
ConformalMesh& mesh, HyperIdealMaps& maps,
double b_val, double a_val)
{
int n = assign_all_dof_indices(mesh, maps);
std::vector<double> x(static_cast<std::size_t>(n));
// Vertices first, then edges (matching assign_all_dof_indices order)
for (auto v : mesh.vertices())
x[static_cast<std::size_t>(maps.v_idx[v])] = b_val;
for (auto e : mesh.edges())
x[static_cast<std::size_t>(maps.e_idx[e])] = a_val;
return x;
}
// ════════════════════════════════════════════════════════════════════════════
// @Ignore in Java: no Hessian implemented
// ════════════════════════════════════════════════════════════════════════════
TEST(HyperIdealFunctional, HessianSymmetryCheck)
{
// Hessian is now implemented (numerical FD). Verify it is symmetric.
auto mesh = make_triangle();
auto maps = setup_hyper_ideal_maps(mesh);
int n = assign_all_dof_indices(mesh, maps);
std::vector<double> x(static_cast<std::size_t>(n), 0.5);
auto H = hyper_ideal_hessian_sym(mesh, x, maps);
Eigen::MatrixXd Hd(H);
EXPECT_NEAR((Hd - Hd.transpose()).norm(), 0.0, 1e-8)
<< "HyperIdeal Hessian must be symmetric";
}
// ════════════════════════════════════════════════════════════════════════════
// Gradient check: single triangle, all vertices hyper-ideal
//
// Mirrors testGradientWithHyperIdealAndIdealPoints (simplified to single face).
// DOFs: 3 vertex b-values + 3 edge a-values = 6 total.
// Point: b_i = 1.0, a_e = 0.5.
// ════════════════════════════════════════════════════════════════════════════
TEST(HyperIdealFunctional, GradientCheck_AllHyperIdealTriangle)
{
auto mesh = make_triangle();
auto maps = setup_hyper_ideal_maps(mesh);
auto x = make_x_all_variable(mesh, maps, /*b=*/1.0, /*a=*/0.5);
EXPECT_TRUE(gradient_check(mesh, x, maps))
<< "Finite-difference gradient check failed on all-hyper-ideal triangle";
}
// ════════════════════════════════════════════════════════════════════════════
// Gradient check in the "extended domain"
//
// Mirrors testGradientInTheExtendedDomain: larger DOF values
// (Java: x_i = 1.2 + |rnd|, so typically > 1.2).
// ════════════════════════════════════════════════════════════════════════════
TEST(HyperIdealFunctional, GradientCheck_ExtendedDomain)
{
auto mesh = make_triangle();
auto maps = setup_hyper_ideal_maps(mesh);
auto x = make_x_all_variable(mesh, maps, /*b=*/2.0, /*a=*/1.5);
EXPECT_TRUE(gradient_check(mesh, x, maps))
<< "Finite-difference gradient check failed in extended domain";
}
// ════════════════════════════════════════════════════════════════════════════
// Gradient check: tetrahedron (4 faces), all vertices and edges variable
//
// Mirrors testGradientWithHyperellipticCurve — larger mesh, closed surface.
// DOFs: 4 vertex b-values + 6 edge a-values = 10 total.
// ════════════════════════════════════════════════════════════════════════════
TEST(HyperIdealFunctional, GradientCheck_TetrahedronAllVariable)
{
auto mesh = make_tetrahedron();
auto maps = setup_hyper_ideal_maps(mesh);
auto x = make_x_all_variable(mesh, maps, /*b=*/1.0, /*a=*/0.5);
EXPECT_TRUE(gradient_check(mesh, x, maps))
<< "Finite-difference gradient check failed on all-variable tetrahedron";
}
// ════════════════════════════════════════════════════════════════════════════
// Energy is finite (not NaN / Inf)
//
// Mirrors testFunctionalAtNaNValue: evaluates at a test point and checks
// the energy is a real number. Uses a quad-strip to exercise the interior
// edge path (adjacent faces sharing one non-border edge).
// ════════════════════════════════════════════════════════════════════════════
TEST(HyperIdealFunctional, EnergyFiniteAtTestPoint)
{
auto mesh = make_quad_strip();
auto maps = setup_hyper_ideal_maps(mesh);
int n = assign_all_dof_indices(mesh, maps);
// Values taken from the Java testFunctionalAtNaNValue spirit:
// large-ish but positive DOF values that could expose degenerate paths.
std::vector<double> x(static_cast<std::size_t>(n), 1.5);
auto res = evaluate_hyper_ideal(mesh, x, maps, true, false);
EXPECT_FALSE(std::isnan(res.energy)) << "Energy must not be NaN";
EXPECT_FALSE(std::isinf(res.energy)) << "Energy must not be Inf";
}
// ════════════════════════════════════════════════════════════════════════════
// Gradient check: mixed vertices (some ideal = pinned, some hyper-ideal)
//
// One vertex is ideal (v_idx = -1, b = 0), the other two are hyper-ideal.
// This exercises the lij / αij branches for the ideal-vertex case.
// ════════════════════════════════════════════════════════════════════════════
TEST(HyperIdealFunctional, GradientCheck_MixedIdealHyperIdeal)
{
auto mesh = make_triangle();
auto maps = setup_hyper_ideal_maps(mesh);
// Make v0 ideal (pinned), v1 and v2 hyper-ideal.
auto vit = mesh.vertices().begin();
Vertex_index v0 = *vit++;
Vertex_index v1 = *vit++;
Vertex_index v2 = *vit;
maps.v_idx[v0] = -1; // ideal: b_0 = 0 (fixed)
maps.v_idx[v1] = 0;
maps.v_idx[v2] = 1;
// All edges variable: indices 2, 3, 4
int eidx = 2;
for (auto e : mesh.edges()) maps.e_idx[e] = eidx++;
// DOF vector: [b1, b2, a_e0, a_e1, a_e2]
std::vector<double> x = {1.0, 1.0, 0.5, 0.5, 0.5};
EXPECT_TRUE(gradient_check(mesh, x, maps))
<< "Finite-difference gradient check failed for mixed ideal / hyper-ideal";
}
// ════════════════════════════════════════════════════════════════════════════
// Gradient check: fan mesh (n=6), all variable
//
// Exercises the full halfedge-around-vertex traversal for a high-valence
// interior vertex.
// ════════════════════════════════════════════════════════════════════════════
TEST(HyperIdealFunctional, GradientCheck_Fan6AllVariable)
{
auto mesh = make_fan(6);
auto maps = setup_hyper_ideal_maps(mesh);
auto x = make_x_all_variable(mesh, maps, /*b=*/1.0, /*a=*/0.5);
EXPECT_TRUE(gradient_check(mesh, x, maps))
<< "Finite-difference gradient check failed on fan-6 mesh";
}