NumericalDiff: Divide by the step actually applied to x libeigen/eigen!2987 Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com>
diff --git a/unsupported/Eigen/src/NumericalDiff/NumericalDiff.h b/unsupported/Eigen/src/NumericalDiff/NumericalDiff.h index f07d3cc..1944f98 100644 --- a/unsupported/Eigen/src/NumericalDiff/NumericalDiff.h +++ b/unsupported/Eigen/src/NumericalDiff/NumericalDiff.h
@@ -19,6 +19,21 @@ namespace Eigen { +namespace internal { + +// Keeps -ffast-math from folding the rounded evaluation points back into x and h, e.g. (x + h) - x +// into h. EIGEN_OPTIMIZATION_BARRIER is an asm operand constraint restricted to plain types (see +// Macros.h), so class-type scalars go unguarded. +template <typename Scalar, std::enable_if_t<std::is_floating_point<Scalar>::value, int> = 0> +EIGEN_STRONG_INLINE void numerical_diff_barrier(Scalar& x) { + EIGEN_UNUSED_VARIABLE(x); + EIGEN_OPTIMIZATION_BARRIER(x) +} +template <typename Scalar, std::enable_if_t<!std::is_floating_point<Scalar>::value, int> = 0> +EIGEN_STRONG_INLINE void numerical_diff_barrier(Scalar&) {} + +} // namespace internal + enum NumericalDiffMode { Forward, Central }; /** @@ -55,7 +70,11 @@ enum { InputsAtCompileTime = Functor::InputsAtCompileTime, ValuesAtCompileTime = Functor::ValuesAtCompileTime }; /** - * return the number of evaluations of the functor + * Computes the Jacobian of the functor at \a _x into \a jac and returns the number of functor evaluations. + * + * The step along coordinate \c j is <tt>h = eps * max(|x[j]|, 1)</tt> with <tt>eps = sqrt(max(epsfcn, epsilon))</tt> + * and \c epsilon the machine precision NumTraits<Scalar>::epsilon(); the difference quotient divides by the + * representable step <tt>fl(x[j] + h) - x[j]</tt> actually applied. */ int df(const InputType& _x, JacobianType& jac) const { using std::abs; @@ -89,24 +108,35 @@ for (int j = 0; j < n; ++j) { const Scalar x_abs = abs(x[j]); h = numext::maxi(x_abs, Scalar(1)) * eps; + // The functor is evaluated at fl(x[j] + h), so divide by that representable step: the rounding + // of x[j] + h perturbs h by up to ulp(x[j]) <= epsilon/eps * h <= sqrt(epsilon) * h, comparable + // to the error of the difference quotient itself. + Scalar x_plus = _x[j] + h; + internal::numerical_diff_barrier(x_plus); + h = x_plus - _x[j]; switch (mode) { case Forward: - x[j] += h; + x[j] = x_plus; Functor::operator()(x, val2); nfev++; x[j] = _x[j]; jac.col(j) = (val2 - val1) / h; break; - case Central: - x[j] += h; + case Central: { + x[j] = x_plus; Functor::operator()(x, val2); nfev++; - x[j] -= 2 * h; + // x[j] - h can round (a tie when x[j] < 0 has |x[j]| within h above a power of two), so + // divide by the separation of the two evaluation points rather than by 2*h. + Scalar x_minus = _x[j] - h; + internal::numerical_diff_barrier(x_minus); + x[j] = x_minus; Functor::operator()(x, val1); nfev++; x[j] = _x[j]; - jac.col(j) = (val2 - val1) / (2 * h); + jac.col(j) = (val2 - val1) / (x_plus - x_minus); break; + } default: eigen_assert(false); }
diff --git a/unsupported/test/NumericalDiff.cpp b/unsupported/test/NumericalDiff.cpp index bfde6b6..d2ebd6a 100644 --- a/unsupported/test/NumericalDiff.cpp +++ b/unsupported/test/NumericalDiff.cpp
@@ -59,6 +59,17 @@ } }; +// Scaling by a power of two is exact, so the difference quotients of this map equal the scales +// iff each divisor is the representable step actually applied to x. +struct pow2_scaling_functor : Functor<double> { + pow2_scaling_functor() : Functor<double>(7, 7), scale(7) { scale << 0.25, 1.0, 8.0, -2.0, 0.5, 4.0, -0.125; } + int operator()(const VectorXd &x, VectorXd &fvec) const { + fvec = scale.cwiseProduct(x); + return 0; + } + VectorXd scale; +}; + void test_forward() { VectorXd x(3); MatrixXd jac(15, 3); @@ -115,8 +126,29 @@ VERIFY_IS_APPROX(jac, actual_jac); } +template <NumericalDiffMode Mode> +void test_step_alignment(double epsfcn) { + pow2_scaling_functor functor; + NumericalDiff<pow2_scaling_functor, Mode> numDiff(functor, epsfcn); + + // Non-dyadic coordinates, so that x + h rounds; the first two take the absolute step eps, and the + // last two are negative with |x| at or just above a power of two, where x - h is a rounding tie. + VectorXd x(7); + x << 0.0, 0.082, 1.13, -2350.7, 7.7e8 + 0.3, -1.0 - std::ldexp(1.0, -27), -1.0; + MatrixXd jac(7, 7); + numDiff.df(x, jac); + + MatrixXd expected = functor.scale.asDiagonal(); + VERIFY_IS_EQUAL(jac, expected); +} + EIGEN_DECLARE_TEST(NumericalDiff) { CALL_SUBTEST(test_forward()); CALL_SUBTEST(test_central()); CALL_SUBTEST(test_small()); + CALL_SUBTEST(test_step_alignment<Forward>(0.0)); + CALL_SUBTEST(test_step_alignment<Central>(0.0)); + // A non-dyadic eps also misaligns the absolute step taken for |x| < 1. + CALL_SUBTEST(test_step_alignment<Forward>(1e-10)); + CALL_SUBTEST(test_step_alignment<Central>(1e-10)); }