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:
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -703,7 +703,7 @@ index. Add a comment:
|
|||||||
| B | `gauss_bonnet.hpp` | 87–88, 128–134 | API error | Medium | ✅ Fixed 2026-05-31 |
|
| B | `gauss_bonnet.hpp` | 87–88, 128–134 | API error | Medium | ✅ Fixed 2026-05-31 |
|
||||||
| C | `euclidean_hessian.hpp` | 26–27, 57–58 | Doc error | Medium | ✅ Fixed 2026-05-31 |
|
| C | `euclidean_hessian.hpp` | 26–27, 57–58 | Doc error | Medium | ✅ Fixed 2026-05-31 |
|
||||||
| D | `euclidean_functional.hpp` + 2 others | 97–107 | Doc error | Medium | ✅ Fixed 2026-05-31 |
|
| D | `euclidean_functional.hpp` + 2 others | 97–107 | Doc error | Medium | ✅ Fixed 2026-05-31 |
|
||||||
| E | `cp_euclidean_functional.hpp` | 338–349, 373–387 | Inconsistency | Medium | 🟡 Open |
|
| E | `cp_euclidean_functional.hpp` | 338–349, 373–387 | 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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user