arXiv:math/0603097 is Springborn 2008 ("A variational principle for weighted
Delaunay triangulations and hyperideal polyhedra"), not a Kolpakov-Mednykh paper.
The author pair Kolpakov & Mednykh has no joint publication from 2006; their
earliest collaboration is arXiv:1008.0312 (2010, on torus knots, unrelated).
The wrong author name was introduced during the Java→C++ port — the Java source
correctly links to math/0603097 without naming the authors; whoever ported it
invented "Kolpakov-Mednykh". The S1 citation audit (2026-05-31) then cemented
the error by adding the incorrect row to references.md.
Files corrected (7):
- code/include/hyper_ideal_utility.hpp
- code/include/hyper_ideal_functional.hpp
- code/tests/cgal/test_hyper_ideal_functional.cpp
- doc/math/references.md
- doc/roadmap/research-track.md
- doc/architecture/project-structure.md
- doc/api/tests.md
Also:
- doc/reviewer/math-derivation-citation-audit-2026-05-31.md: M1 post-correction noted
- doc/reviewer/finding-orchestration.md: lesson-learned section added (AI citation
audits can introduce plausible-but-wrong attributions; human expert review required
before CGAL submission)
- papers/MANUAL-DOWNLOAD.md: overview of papers requiring manual download (paywalled
journals, TU Berlin theses, books)
- .gitignore: papers/*.pdf excluded (downloaded arXiv PDFs, not tracked)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
291 lines
14 KiB
C++
291 lines
14 KiB
C++
// 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_hyper_ideal_all_dof_indices(mesh, maps);
|
||
std::vector<double> x(static_cast<std::size_t>(n));
|
||
|
||
// Vertices first, then edges (matching assign_hyper_ideal_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_hyper_ideal_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_hyper_ideal(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_hyper_ideal(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_hyper_ideal(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_hyper_ideal_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_hyper_ideal(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_hyper_ideal(mesh, x, maps))
|
||
<< "Finite-difference gradient check failed on fan-6 mesh";
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
// Guard test: face_energy() must throw for 2+ ideal vertices in one face.
|
||
//
|
||
// This tests Finding-A from doc/reviewer/external-audit-2026-05-30.md.
|
||
//
|
||
// The Java reference (HyperIdealFunctional.java lines 222-231) silently applies
|
||
// the one-ideal-vertex volume formula to the first ideal vertex found, ignoring
|
||
// any additional ideal vertices in the same face. That is mathematically wrong
|
||
// for two-ideal / three-ideal faces. The C++ port detects this at runtime and
|
||
// throws std::logic_error instead of silently producing a wrong energy value.
|
||
//
|
||
// Tests cover:
|
||
// (a) Two ideal vertices in the same face (v1+v2 ideal, v3 hyper-ideal)
|
||
// (b) All three vertices ideal
|
||
// (c) Exactly one ideal vertex — must NOT throw (valid configuration)
|
||
// ════════════════════════════════════════════════════════════════════════════
|
||
|
||
TEST(HyperIdealFunctional, MultiIdealGuard_TwoIdealVertices_Throws)
|
||
{
|
||
// Triangle mesh: 3 vertices, 3 edges, 1 face (open mesh, single face).
|
||
auto mesh = make_triangle();
|
||
auto maps = setup_hyper_ideal_maps(mesh);
|
||
|
||
// Assign edge DOFs to all three edges.
|
||
int eidx = 0;
|
||
for (auto e : mesh.edges()) maps.e_idx[e] = eidx++;
|
||
|
||
// Pin v1 and v2 (ideal), make only v3 hyper-ideal.
|
||
auto vit = mesh.vertices().begin();
|
||
Vertex_index v1 = *vit++;
|
||
Vertex_index v2 = *vit++;
|
||
// v3 remains pinned (default v_idx = -1, i.e. ideal too — see below).
|
||
|
||
maps.v_idx[v1] = -1; // ideal
|
||
maps.v_idx[v2] = -1; // ideal
|
||
maps.v_idx[*vit] = 3; // hyper-ideal: DOF index 3 (after 3 edge DOFs)
|
||
|
||
// DOF vector: [a_e0, a_e1, a_e2, b_v3]
|
||
std::vector<double> x = {0.5, 0.5, 0.5, 1.0};
|
||
|
||
// evaluate_hyper_ideal calls face_energy() which must detect 2 ideal vertices
|
||
// and throw std::logic_error.
|
||
EXPECT_THROW(
|
||
evaluate_hyper_ideal(mesh, x, maps, /*energy=*/true, /*gradient=*/false),
|
||
std::logic_error)
|
||
<< "Expected std::logic_error for face with two ideal vertices";
|
||
}
|
||
|
||
TEST(HyperIdealFunctional, MultiIdealGuard_AllThreeIdealVertices_Throws)
|
||
{
|
||
auto mesh = make_triangle();
|
||
auto maps = setup_hyper_ideal_maps(mesh);
|
||
|
||
// Only edge DOFs — all vertices remain ideal (default v_idx = -1).
|
||
int eidx = 0;
|
||
for (auto e : mesh.edges()) maps.e_idx[e] = eidx++;
|
||
|
||
// DOF vector: [a_e0, a_e1, a_e2]
|
||
std::vector<double> x = {0.5, 0.5, 0.5};
|
||
|
||
EXPECT_THROW(
|
||
evaluate_hyper_ideal(mesh, x, maps, /*energy=*/true, /*gradient=*/false),
|
||
std::logic_error)
|
||
<< "Expected std::logic_error for face with all three ideal vertices";
|
||
}
|
||
|
||
TEST(HyperIdealFunctional, MultiIdealGuard_ExactlyOneIdeal_DoesNotThrow)
|
||
{
|
||
// Exactly one ideal vertex per face must NOT throw — it is the supported
|
||
// one-ideal-vertex configuration (Springborn 2008 formula).
|
||
auto mesh = make_triangle();
|
||
auto maps = setup_hyper_ideal_maps(mesh);
|
||
|
||
// All edges variable.
|
||
int eidx = 0;
|
||
for (auto e : mesh.edges()) maps.e_idx[e] = eidx++;
|
||
|
||
// Pin only v1 (ideal); v2 and v3 are hyper-ideal.
|
||
auto vit = mesh.vertices().begin();
|
||
maps.v_idx[*vit] = -1; ++vit; // v1: ideal
|
||
maps.v_idx[*vit] = 3; ++vit; // v2: hyper-ideal, DOF 3
|
||
maps.v_idx[*vit] = 4; // v3: hyper-ideal, DOF 4
|
||
|
||
// DOF vector: [a_e0, a_e1, a_e2, b_v2, b_v3]
|
||
std::vector<double> x = {0.5, 0.5, 0.5, 1.0, 1.0};
|
||
|
||
EXPECT_NO_THROW(
|
||
evaluate_hyper_ideal(mesh, x, maps, /*energy=*/true, /*gradient=*/false))
|
||
<< "Unexpected throw for valid one-ideal-vertex configuration";
|
||
}
|