fix(hyper-ideal): guard face_energy() against unsupported multi-ideal faces
Finding-A from doc/reviewer/external-audit-2026-05-30.md. Root cause: both the Java reference (HyperIdealFunctional.java:222-231) and the C++ port silently applied the one-ideal-vertex volume formula to the first ideal vertex found in a face, ignoring any additional ideal vertices. For two or three ideal vertices this produces a wrong energy value with no diagnostic. Fix: add an ideal_count guard at the top of face_energy() that throws std::logic_error for ideal_count >= 2. The one-ideal (Kolpakov-Mednykh) and zero-ideal (Meyerhoff/Ushijima) paths are unchanged and correct. Three new GTests cover the three guard cases: MultiIdealGuard_TwoIdealVertices_Throws (two ideal → throw) MultiIdealGuard_AllThreeIdealVertices_Throws (all ideal → throw) MultiIdealGuard_ExactlyOneIdeal_DoesNotThrow (one ideal → no throw) 262/262 CGAL tests pass, 0 failed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Tarik Moussa
parent
7f1a077269
commit
9afefcbb7b
@@ -197,3 +197,94 @@ TEST(HyperIdealFunctional, GradientCheck_Fan6AllVariable)
|
||||
EXPECT_TRUE(gradient_check(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 (Kolpakov-Mednykh 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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user