85 if constexpr (std::is_invocable_v<F, T>)
87 else if constexpr (std::is_invocable_v<F, T, T>)
89 else if constexpr (std::is_invocable_v<F, T, T, T>)
91 else if constexpr (std::is_invocable_v<F, T, T, T, T>)
93 else if constexpr (std::is_invocable_v<F, T, T, T, T, T>)
95 else if constexpr (std::is_invocable_v<F, T, T, T, T, T, T>)
97 else if constexpr (std::is_invocable_v<F, T, T, T, T, T, T, T>)
99 else if constexpr (std::is_invocable_v<F, T, T, T, T, T, T, T, T>)
102 static_assert(std::is_invocable_v<F, T>,
103 "Lambda must accept 1-8 scalar arguments of the given type");
111template <
typename Func,
typename Scalar>
constexpr int arity_of() {
121template <
typename Func,
typename Scalar>
auto invoke(Func &&f,
const std::vector<Scalar> &args) {
122 using F = std::decay_t<Func>;
125 if (
static_cast<int>(args.size()) != N) {
126 throw std::invalid_argument(
"DiffTestHarness: test point has " +
127 std::to_string(args.size()) +
" values but lambda accepts " +
129 " arguments. Check your test_points dimensions.");
132 if constexpr (N == 1)
134 else if constexpr (N == 2)
135 return f(args[0], args[1]);
136 else if constexpr (N == 3)
137 return f(args[0], args[1], args[2]);
138 else if constexpr (N == 4)
139 return f(args[0], args[1], args[2], args[3]);
140 else if constexpr (N == 5)
141 return f(args[0], args[1], args[2], args[3], args[4]);
142 else if constexpr (N == 6)
143 return f(args[0], args[1], args[2], args[3], args[4], args[5]);
144 else if constexpr (N == 7)
145 return f(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
146 else if constexpr (N == 8)
147 return f(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
154 if constexpr (std::is_arithmetic_v<std::decay_t<T>>) {
155 Eigen::VectorXd v(1);
156 v(0) =
static_cast<double>(val);
160 Eigen::VectorXd v(val.size());
161 for (Eigen::Index i = 0; i < val.size(); ++i) {
162 v(i) =
static_cast<double>(val(i));
176 std::vector<SymbolicArg> out;
177 out.reserve(
static_cast<size_t>(val.size()));
178 for (Eigen::Index i = 0; i < val.size(); ++i) {
179 out.emplace_back(val(i));
189 std::ostringstream ss;
191 for (
size_t i = 0; i < point.size(); ++i) {
204 std::ostringstream ss;
205 ss << std::setprecision(8);
206 if (m.rows() == 1 && m.cols() == 1) {
210 for (Eigen::Index i = 0; i < m.rows(); ++i) {
212 for (Eigen::Index j = 0; j < m.cols(); ++j) {
233template <
typename Func>
236 DualModeResult result;
242 Eigen::VectorXd numeric_output;
245 }
catch (
const std::exception &e) {
246 result.failure_detail =
252 std::vector<SymbolicScalar> sym_vars;
253 sym_vars.reserve(
static_cast<size_t>(n));
254 for (
int i = 0; i < n; ++i) {
255 sym_vars.push_back(
metis::sym(
"x" + std::to_string(i)));
260 result.symbolic_compiles =
true;
263 std::vector<SymbolicArg> input_args(sym_vars.begin(), sym_vars.end());
267 std::vector<casadi::MX> mx_outputs;
268 mx_outputs.reserve(output_args.size());
269 for (
const auto &o : output_args) {
270 mx_outputs.push_back(o.get());
272 casadi::MX out_cat = casadi::MX::vertcat(mx_outputs);
274 metis::Function func(input_args, {SymbolicArg(out_cat)});
275 auto results = func(point);
276 Eigen::VectorXd symbolic_output =
277 Eigen::Map<Eigen::VectorXd>(results[0].data(), results[0].rows() * results[0].cols());
280 if (numeric_output.size() != symbolic_output.size()) {
281 result.values_match =
false;
282 result.failure_detail =
283 "Output dimension mismatch: numeric=" + std::to_string(numeric_output.size()) +
284 " symbolic=" + std::to_string(symbolic_output.size()) +
" at point " +
289 result.max_value_error = (numeric_output - symbolic_output).cwiseAbs().maxCoeff();
290 result.values_match = result.max_value_error < opts.value_tol;
292 if (!result.values_match) {
293 std::ostringstream ss;
295 <<
": max_error=" << result.max_value_error <<
" (tol=" << opts.value_tol <<
")"
296 <<
"\n Numeric: " << numeric_output.transpose()
297 <<
"\n Symbolic: " << symbolic_output.transpose();
298 result.failure_detail = ss.str();
301 }
catch (
const std::exception &e) {
302 result.symbolic_compiles =
false;
303 result.failure_detail =
313template <
typename Func>
316 DualModeResult overall;
318 if (test_points.empty()) {
319 overall.failure_detail =
"No test points supplied";
323 overall.symbolic_compiles =
true;
324 overall.values_match =
true;
325 overall.max_value_error = 0.0;
327 for (
const auto &point : test_points) {
329 overall.max_value_error = std::max(overall.max_value_error, r.max_value_error);
331 if (!r.symbolic_compiles) {
332 overall.symbolic_compiles =
false;
333 overall.failure_detail = r.failure_detail;
336 if (!r.values_match) {
337 overall.values_match =
false;
338 overall.failure_detail = r.failure_detail;
350template <
typename Func>
353 DiffTestResult result;
358 result.symbolic_compiles = dm.symbolic_compiles;
359 result.values_match = dm.values_match;
360 result.max_value_error = dm.max_value_error;
361 result.failure_detail = dm.failure_detail;
363 if (!result.symbolic_compiles || !result.values_match) {
371 }
catch (
const std::exception &e) {
372 result.jacobian_matches =
false;
373 result.failure_detail =
377 const int m =
static_cast<int>(f0.size());
379 Eigen::MatrixXd fd_jac(m, n);
380 for (
int j = 0; j < n; ++j) {
381 std::vector<double> point_plus = point;
382 std::vector<double> point_minus = point;
383 point_plus[
static_cast<size_t>(j)] += opts.fd_step;
384 point_minus[
static_cast<size_t>(j)] -= opts.fd_step;
389 fd_jac.col(j) = (f_plus - f_minus) / (2.0 * opts.fd_step);
390 }
catch (
const std::exception &e) {
391 result.jacobian_matches =
false;
393 " perturbing input " + std::to_string(j) +
": " + e.what();
397 result.fd_jacobian = fd_jac;
400 std::vector<SymbolicScalar> sym_vars;
401 sym_vars.reserve(
static_cast<size_t>(n));
402 for (
int i = 0; i < n; ++i) {
403 sym_vars.push_back(
metis::sym(
"x" + std::to_string(i)));
409 std::vector<SymbolicArg> input_args(sym_vars.begin(), sym_vars.end());
415 metis::Function J_func(input_args, {SymbolicArg(J_sym)});
416 auto J_results = J_func(point);
417 result.ad_jacobian = J_results[0];
419 }
catch (
const std::exception &e) {
420 result.jacobian_matches =
false;
421 result.failure_detail =
"AD Jacobian computation failed at point " +
427 if (result.ad_jacobian.rows() != fd_jac.rows() || result.ad_jacobian.cols() != fd_jac.cols()) {
428 result.jacobian_matches =
false;
429 std::ostringstream ss;
431 <<
": AD=" << result.ad_jacobian.rows() <<
"x" << result.ad_jacobian.cols()
432 <<
" FD=" << fd_jac.rows() <<
"x" << fd_jac.cols();
433 result.failure_detail = ss.str();
437 result.max_jacobian_error = 0.0;
438 result.jacobian_matches =
true;
439 int worst_row = 0, worst_col = 0;
440 for (
int i = 0; i < result.ad_jacobian.rows() && result.jacobian_matches; ++i) {
441 for (
int j = 0; j < result.ad_jacobian.cols(); ++j) {
442 double ad_val = result.ad_jacobian(i, j);
443 double fd_val = fd_jac(i, j);
445 if (!std::isfinite(ad_val) || !std::isfinite(fd_val)) {
446 result.jacobian_matches =
false;
447 result.max_jacobian_error = std::numeric_limits<double>::infinity();
453 double error = std::abs(ad_val - fd_val);
454 if (error > result.max_jacobian_error) {
455 result.max_jacobian_error = error;
460 double tol = opts.jac_atol + opts.jac_rtol * std::abs(ad_val);
462 result.jacobian_matches =
false;
468 if (!result.jacobian_matches) {
469 std::ostringstream ss;
471 <<
":\n Max error: " << result.max_jacobian_error <<
" (rtol=" << opts.jac_rtol
472 <<
", atol=" << opts.jac_atol <<
")"
473 <<
"\n Worst element: (" << worst_row <<
", " << worst_col
474 <<
"): AD=" << result.ad_jacobian(worst_row, worst_col)
475 <<
", FD=" << fd_jac(worst_row, worst_col)
478 result.failure_detail = ss.str();
498template <
typename Func>
501 DiffTestResult overall;
503 if (test_points.empty()) {
504 overall.failure_detail =
"No test points supplied";
508 overall.symbolic_compiles =
true;
509 overall.values_match =
true;
510 overall.jacobian_matches =
true;
511 overall.max_value_error = 0.0;
512 overall.max_jacobian_error = 0.0;
514 for (
const auto &point : test_points) {
516 overall.max_value_error = std::max(overall.max_value_error, r.max_value_error);
517 overall.max_jacobian_error = std::max(overall.max_jacobian_error, r.max_jacobian_error);
519 if (!r.symbolic_compiles) {
520 overall.symbolic_compiles =
false;
521 overall.failure_detail = r.failure_detail;
524 if (!r.values_match) {
525 overall.values_match =
false;
526 overall.failure_detail = r.failure_detail;
529 if (!r.jacobian_matches) {
530 overall.jacobian_matches =
false;
531 overall.failure_detail = r.failure_detail;
532 overall.ad_jacobian = r.ad_jacobian;
533 overall.fd_jacobian = r.fd_jacobian;
Symbolic automatic differentiation (Jacobian, gradient, Hessian, sensitivity analysis).
Symbolic function wrapper around CasADi with Eigen-native IO.
IO Utilities and Traits for Metis.
Core type aliases for numeric and symbolic Eigen/CasADi interop.
Definition DiffTestHarness.hpp:75
std::vector< SymbolicArg > to_symbolic_output_args(const T &val)
Normalize a symbolic scalar or vector return to a vector of SymbolicArg.
Definition DiffTestHarness.hpp:171
std::string format_matrix(const Eigen::MatrixXd &m)
Format an Eigen matrix as a string for diagnostics.
Definition DiffTestHarness.hpp:203
std::string format_point(const std::vector< double > &point)
Format a test point as a string for diagnostics.
Definition DiffTestHarness.hpp:188
constexpr int arity_of()
Get the detected arity of a callable for a given scalar type.
Definition DiffTestHarness.hpp:111
Eigen::VectorXd to_numeric_vector(const T &val)
Normalize a scalar or vector return to Eigen::VectorXd.
Definition DiffTestHarness.hpp:153
constexpr int detect_arity()
Detect the arity of a callable when invoked with arguments of type T.
Definition DiffTestHarness.hpp:84
auto invoke(Func &&f, const std::vector< Scalar > &args)
Invoke a callable with N arguments unpacked from a vector.
Definition DiffTestHarness.hpp:121
Definition DiffTestHarness.hpp:31
DiffTestResult verify_differentiable_at_point(Func &&f, const std::vector< double > &point, const DiffTestOptions &opts={})
Verify differentiability at a single test point: dual-mode checks + AD Jacobian vs FD Jacobian.
Definition DiffTestHarness.hpp:351
DualModeResult verify_dual_mode_at_point(Func &&f, const std::vector< double > &point, const DiffTestOptions &opts={})
Verify that a dual-mode function compiles symbolically and that symbolic evaluation matches numeric e...
Definition DiffTestHarness.hpp:234
DiffTestResult verify_differentiable(Func &&f, const std::vector< std::vector< double > > &test_points, const DiffTestOptions &opts={})
Verify differentiability across multiple test points. Returns on first failure.
Definition DiffTestHarness.hpp:499
DualModeResult verify_dual_mode(Func &&f, const std::vector< std::vector< double > > &test_points, const DiffTestOptions &opts={})
Verify dual-mode across multiple test points. Returns on first failure.
Definition DiffTestHarness.hpp:314
auto jacobian(const Expr &expression, const Vars &...variables)
Computes Jacobian of an expression with respect to variables.
Definition AutoDiff.hpp:109
casadi::MX SymbolicScalar
CasADi MX symbolic scalar.
Definition MetisTypes.hpp:70
SymbolicScalar sym(const std::string &name)
Create a named symbolic scalar variable.
Definition MetisTypes.hpp:107
Options controlling differentiability test behavior.
Definition DiffTestHarness.hpp:40
double fd_step
Finite difference perturbation size.
Definition DiffTestHarness.hpp:41
double value_tol
Tolerance for numeric-vs-symbolic value comparison.
Definition DiffTestHarness.hpp:44
double jac_rtol
Relative tolerance for AD-vs-FD Jacobian comparison.
Definition DiffTestHarness.hpp:42
double jac_atol
Absolute tolerance for AD-vs-FD Jacobian comparison.
Definition DiffTestHarness.hpp:43
Result of a full differentiability check (dual-mode + Jacobian).
Definition DiffTestHarness.hpp:64
double max_jacobian_error
Largest element-wise |AD - FD|.
Definition DiffTestHarness.hpp:66
Eigen::MatrixXd fd_jacobian
FD Jacobian for diagnostics.
Definition DiffTestHarness.hpp:68
Eigen::MatrixXd ad_jacobian
AD Jacobian for diagnostics.
Definition DiffTestHarness.hpp:67
bool jacobian_matches
Does AD Jacobian ≈ FD Jacobian?
Definition DiffTestHarness.hpp:65
Result of a dual-mode (symbolic compilation + value match) check.
Definition DiffTestHarness.hpp:54
bool values_match
Does eval(symbolic_output) == numeric_output?
Definition DiffTestHarness.hpp:56
std::string failure_detail
Human-readable on failure, empty on success.
Definition DiffTestHarness.hpp:58
bool symbolic_compiles
Did the lambda execute in symbolic mode?
Definition DiffTestHarness.hpp:55
double max_value_error
Largest |symbolic - numeric| across outputs.
Definition DiffTestHarness.hpp:57