fix(gradient-checks): use relative error in all FD check functions

Finding-E from doc/reviewer/external-audit-2026-05-30.md.
Scan also uncovered the same issue in inversive_distance_functional.hpp.

Three functions used absolute error `|analytic - fd| > tol` while the
rest of the library (euclidean_functional, spherical_functional,
euclidean_hessian, hyper_ideal_functional) all use relative error
`|analytic - fd| / max(1, |analytic|) > tol`.

Absolute error is too strict for large gradients (false failures) and
too lenient for small gradients.

Fixed:
  cp_euclidean_functional.hpp  gradient_check_cp_euclidean()
  cp_euclidean_functional.hpp  hessian_check_cp_euclidean()
  inversive_distance_functional.hpp  gradient_check_inversive_distance()

All three now use the relative criterion and accumulate all failures
before returning (ok=false instead of early return on first mismatch).
Default tol updated from 1e-6/1e-5 to 1e-4, matching the Java
FunctionalTest convention used by all other checks in the library.
Error message updated to print rel-err instead of raw diff.

266/266 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-31 00:26:44 +02:00
parent cfbbc1b21f
commit 7534c62c3d
3 changed files with 40 additions and 25 deletions

View File

@@ -324,16 +324,19 @@ inline Eigen::SparseMatrix<double> cp_euclidean_hessian(const ConformalMesh&
return H; return H;
} }
/// FD gradient check for the CP-Euclidean functional. Mirrors the /// FD gradient check for the CP-Euclidean functional (central differences).
/// Java `FunctionalTest`; default `eps = 1e-5`, `tol = 1e-6`. /// Uses the same **relative** error criterion as every other gradient check in
/// this library: `|analytic fd| / max(1, |analytic|) < tol`.
/// Default `eps = 1e-5`, `tol = 1e-4` (matches Java `FunctionalTest`).
inline bool gradient_check_cp_euclidean(const ConformalMesh& mesh, inline bool gradient_check_cp_euclidean(const ConformalMesh& mesh,
const std::vector<double>& x, const std::vector<double>& x,
const CPEuclideanMaps& m, const CPEuclideanMaps& m,
double eps = 1e-5, double eps = 1e-5,
double tol = 1e-6) double tol = 1e-4)
{ {
auto G = cp_euclidean_gradient(mesh, x, m); auto G = cp_euclidean_gradient(mesh, x, m);
const std::size_t n = G.size(); const std::size_t n = G.size();
bool ok = true;
for (std::size_t i = 0; i < n; ++i) { for (std::size_t i = 0; i < n; ++i) {
std::vector<double> xp = x, xm = x; std::vector<double> xp = x, xm = x;
@@ -341,28 +344,33 @@ inline bool gradient_check_cp_euclidean(const ConformalMesh& mesh,
xm[i] -= eps; xm[i] -= eps;
const double Ep = cp_euclidean_energy(mesh, xp, m); const double Ep = cp_euclidean_energy(mesh, xp, m);
const double Em = cp_euclidean_energy(mesh, xm, m); const double Em = cp_euclidean_energy(mesh, xm, m);
const double fd = (Ep - Em) / (2.0 * eps); const double fd = (Ep - Em) / (2.0 * eps);
if (std::abs(G[i] - fd) > tol) { const double err = std::abs(G[i] - fd);
const double scale = std::max(1.0, std::abs(G[i]));
if (err / scale > tol) {
std::cerr << "[cp-euclidean] FD gradient mismatch at DOF " << i std::cerr << "[cp-euclidean] FD gradient mismatch at DOF " << i
<< ": analytic=" << G[i] << ": analytic=" << G[i]
<< " FD=" << fd << " FD=" << fd
<< " diff=" << (G[i] - fd) << "\n"; << " rel-err=" << (err / scale) << "\n";
return false; ok = false;
} }
} }
return true; return ok;
} }
/// FD Hessian check for the CP-Euclidean functional. Verifies analytic /// FD Hessian check for the CP-Euclidean functional. Verifies analytic
/// `H` column-by-column against `(G(x+εe_j) G(xεe_j)) / (2ε)`. /// `H` column-by-column against `(G(x+εe_j) G(xεe_j)) / (2ε)`.
/// Uses the same **relative** error criterion as `hessian_check_euclidean`:
/// `|analytic fd| / max(1, |analytic|) < tol`.
inline bool hessian_check_cp_euclidean(const ConformalMesh& mesh, inline bool hessian_check_cp_euclidean(const ConformalMesh& mesh,
const std::vector<double>& x, const std::vector<double>& x,
const CPEuclideanMaps& m, const CPEuclideanMaps& m,
double eps = 1e-5, double eps = 1e-5,
double tol = 1e-5) double tol = 1e-4)
{ {
const auto H = cp_euclidean_hessian(mesh, x, m); const auto H = cp_euclidean_hessian(mesh, x, m);
const int n = static_cast<int>(H.rows()); const int n = static_cast<int>(H.rows());
bool ok = true;
for (int j = 0; j < n; ++j) { for (int j = 0; j < n; ++j) {
std::vector<double> xp = x, xm = x; std::vector<double> xp = x, xm = x;
@@ -372,19 +380,21 @@ inline bool hessian_check_cp_euclidean(const ConformalMesh& mesh,
auto Gm = cp_euclidean_gradient(mesh, xm, m); auto Gm = cp_euclidean_gradient(mesh, xm, m);
for (int i = 0; i < n; ++i) { for (int i = 0; i < n; ++i) {
double fd = (Gp[static_cast<std::size_t>(i)] - Gm[static_cast<std::size_t>(i)]) double fd = (Gp[static_cast<std::size_t>(i)] - Gm[static_cast<std::size_t>(i)])
/ (2.0 * eps); / (2.0 * eps);
double an = H.coeff(i, j); double an = H.coeff(i, j);
if (std::abs(an - fd) > tol) { double err = std::abs(an - fd);
double scale = std::max(1.0, std::abs(an));
if (err / scale > tol) {
std::cerr << "[cp-euclidean] FD Hessian mismatch at (" std::cerr << "[cp-euclidean] FD Hessian mismatch at ("
<< i << "," << j << "): analytic=" << an << i << "," << j << "): analytic=" << an
<< " FD=" << fd << " FD=" << fd
<< " diff=" << (an - fd) << "\n"; << " rel-err=" << (err / scale) << "\n";
return false; ok = false;
} }
} }
} }
return true; return ok;
} }
} // namespace conformallab } // namespace conformallab

View File

@@ -354,32 +354,37 @@ inline double inversive_distance_energy(
} }
/// FD gradient check for the Inversive-Distance functional (central diff). /// FD gradient check for the Inversive-Distance functional (central diff).
/// Uses the same **relative** error criterion as every other gradient check:
/// `|analytic fd| / max(1, |analytic|) < tol`.
inline bool gradient_check_inversive_distance( inline bool gradient_check_inversive_distance(
const ConformalMesh& mesh, const ConformalMesh& mesh,
const std::vector<double>& x, const std::vector<double>& x,
const InversiveDistanceMaps& m, const InversiveDistanceMaps& m,
double eps = 1e-5, double eps = 1e-5,
double tol = 1e-6) double tol = 1e-4)
{ {
auto G = inversive_distance_gradient(mesh, x, m); auto G = inversive_distance_gradient(mesh, x, m);
const std::size_t n = G.size(); const std::size_t n = G.size();
bool ok = true;
for (std::size_t i = 0; i < n; ++i) { for (std::size_t i = 0; i < n; ++i) {
std::vector<double> xp = x, xm = x; std::vector<double> xp = x, xm = x;
xp[i] += eps; xp[i] += eps;
xm[i] -= eps; xm[i] -= eps;
double Ep = inversive_distance_energy(mesh, xp, m); double Ep = inversive_distance_energy(mesh, xp, m);
double Em = inversive_distance_energy(mesh, xm, m); double Em = inversive_distance_energy(mesh, xm, m);
double fd = (Ep - Em) / (2.0 * eps); double fd = (Ep - Em) / (2.0 * eps);
if (std::abs(G[i] - fd) > tol) { double err = std::abs(G[i] - fd);
double scale = std::max(1.0, std::abs(G[i]));
if (err / scale > tol) {
std::cerr << "[inversive-distance] FD gradient mismatch at DOF " << i std::cerr << "[inversive-distance] FD gradient mismatch at DOF " << i
<< ": analytic=" << G[i] << ": analytic=" << G[i]
<< " FD=" << fd << " FD=" << fd
<< " diff=" << (G[i] - fd) << "\n"; << " rel-err=" << (err / scale) << "\n";
return false; ok = false;
} }
} }
return true; return ok;
} }
/// Newton equilibrium check: returns `true` iff the gradient at `x` /// Newton equilibrium check: returns `true` iff the gradient at `x`

View File

@@ -703,7 +703,7 @@ index. Add a comment:
| B | `gauss_bonnet.hpp` | 8788, 128134 | API error | Medium | ✅ Fixed 2026-05-31 | | B | `gauss_bonnet.hpp` | 8788, 128134 | API error | Medium | ✅ Fixed 2026-05-31 |
| C | `euclidean_hessian.hpp` | 2627, 5758 | Doc error | Medium | ✅ Fixed 2026-05-31 | | C | `euclidean_hessian.hpp` | 2627, 5758 | Doc error | Medium | ✅ Fixed 2026-05-31 |
| D | `euclidean_functional.hpp` + 2 others | 97107 | Doc error | Medium | ✅ Fixed 2026-05-31 | | D | `euclidean_functional.hpp` + 2 others | 97107 | Doc error | Medium | ✅ Fixed 2026-05-31 |
| E | `cp_euclidean_functional.hpp` | 338349, 373387 | Inconsistency | Medium | 🟡 Open | | E | `cp_euclidean_functional.hpp` | 338349, 373387 | Inconsistency | Medium | ✅ Fixed 2026-05-31 |
| F | test files | — | Test gap | Medium | 🟠 Open | | F | test files | — | Test gap | Medium | 🟠 Open |
| G | test files | — | Test gap | Medium | 🟠 Open | | G | test files | — | Test gap | Medium | 🟠 Open |
| H | test files | — | Test gap | Medium | 🟠 Open | | H | test files | — | Test gap | Medium | 🟠 Open |