Metis 2.0.0
High-performance C++20 dual-mode numerical framework
Loading...
Searching...
No Matches
DiffTestHarness.hpp
Go to the documentation of this file.
1#pragma once
15
16#include <Eigen/Dense>
17#include <algorithm>
18#include <cmath>
19#include <iomanip>
20#include <limits>
25#include <sstream>
26#include <stdexcept>
27#include <string>
28#include <type_traits>
29#include <vector>
30
32
33// ============================================================================
34// Configuration
35// ============================================================================
36
41 double fd_step = 1e-7;
42 double jac_rtol = 1e-5;
43 double jac_atol = 1e-8;
44 double value_tol = 1e-10;
45};
46
47// ============================================================================
48// Result Types
49// ============================================================================
50
55 bool symbolic_compiles = false;
56 bool values_match = false;
57 double max_value_error = 0.0;
58 std::string failure_detail;
59};
60
65 bool jacobian_matches = false;
66 double max_jacobian_error = 0.0;
67 Eigen::MatrixXd ad_jacobian;
68 Eigen::MatrixXd fd_jacobian;
69};
70
71// ============================================================================
72// Detail: Arity Detection and Invocation Helpers
73// ============================================================================
74
75namespace detail {
76
84template <typename F, typename T> constexpr int detect_arity() {
85 if constexpr (std::is_invocable_v<F, T>)
86 return 1;
87 else if constexpr (std::is_invocable_v<F, T, T>)
88 return 2;
89 else if constexpr (std::is_invocable_v<F, T, T, T>)
90 return 3;
91 else if constexpr (std::is_invocable_v<F, T, T, T, T>)
92 return 4;
93 else if constexpr (std::is_invocable_v<F, T, T, T, T, T>)
94 return 5;
95 else if constexpr (std::is_invocable_v<F, T, T, T, T, T, T>)
96 return 6;
97 else if constexpr (std::is_invocable_v<F, T, T, T, T, T, T, T>)
98 return 7;
99 else if constexpr (std::is_invocable_v<F, T, T, T, T, T, T, T, T>)
100 return 8;
101 else {
102 static_assert(std::is_invocable_v<F, T>,
103 "Lambda must accept 1-8 scalar arguments of the given type");
104 return 0;
105 }
106}
107
111template <typename Func, typename Scalar> constexpr int arity_of() {
112 return detect_arity<std::decay_t<Func>, Scalar>();
113}
114
121template <typename Func, typename Scalar> auto invoke(Func &&f, const std::vector<Scalar> &args) {
122 using F = std::decay_t<Func>;
123 constexpr int N = detect_arity<F, Scalar>();
124
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 " +
128 std::to_string(N) +
129 " arguments. Check your test_points dimensions.");
130 }
131
132 if constexpr (N == 1)
133 return f(args[0]);
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]);
148}
149
153template <typename T> Eigen::VectorXd to_numeric_vector(const T &val) {
154 if constexpr (std::is_arithmetic_v<std::decay_t<T>>) {
155 Eigen::VectorXd v(1);
156 v(0) = static_cast<double>(val);
157 return v;
158 } else {
159 // Assume Eigen vector/matrix — flatten to column
160 Eigen::VectorXd v(val.size());
161 for (Eigen::Index i = 0; i < val.size(); ++i) {
162 v(i) = static_cast<double>(val(i));
163 }
164 return v;
165 }
166}
167
171template <typename T> std::vector<SymbolicArg> to_symbolic_output_args(const T &val) {
172 if constexpr (std::is_same_v<std::decay_t<T>, SymbolicScalar>) {
173 return {val};
174 } else {
175 // Assume Eigen vector of MX elements
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));
180 }
181 return out;
182 }
183}
184
188inline std::string format_point(const std::vector<double> &point) {
189 std::ostringstream ss;
190 ss << "[";
191 for (size_t i = 0; i < point.size(); ++i) {
192 if (i > 0)
193 ss << ", ";
194 ss << point[i];
195 }
196 ss << "]";
197 return ss.str();
198}
199
203inline std::string format_matrix(const Eigen::MatrixXd &m) {
204 std::ostringstream ss;
205 ss << std::setprecision(8);
206 if (m.rows() == 1 && m.cols() == 1) {
207 ss << m(0, 0);
208 } else {
209 ss << "\n";
210 for (Eigen::Index i = 0; i < m.rows(); ++i) {
211 ss << " [";
212 for (Eigen::Index j = 0; j < m.cols(); ++j) {
213 if (j > 0)
214 ss << ", ";
215 ss << m(i, j);
216 }
217 ss << "]\n";
218 }
219 }
220 return ss.str();
221}
222
223} // namespace detail
224
225// ============================================================================
226// Core Verification Functions
227// ============================================================================
228
233template <typename Func>
234DualModeResult verify_dual_mode_at_point(Func &&f, const std::vector<double> &point,
235 const DiffTestOptions &opts = {}) {
236 DualModeResult result;
237 // Derive n from the lambda's arity, not from point.size().
238 // invoke() validates the two match and throws if they disagree.
239 constexpr int n = detail::arity_of<Func, double>();
240
241 // Step 1: Numeric evaluation
242 Eigen::VectorXd numeric_output;
243 try {
244 numeric_output = detail::to_numeric_vector(detail::invoke(f, point));
245 } catch (const std::exception &e) {
246 result.failure_detail =
247 "Numeric evaluation failed at point " + detail::format_point(point) + ": " + e.what();
248 return result;
249 }
250
251 // Step 2: Symbolic evaluation
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)));
256 }
257
258 try {
259 auto sym_result = detail::invoke(f, sym_vars);
260 result.symbolic_compiles = true;
261
262 // Build Function and evaluate at test point
263 std::vector<SymbolicArg> input_args(sym_vars.begin(), sym_vars.end());
264 std::vector<SymbolicArg> output_args = detail::to_symbolic_output_args(sym_result);
265
266 // Vertcat outputs into single MX for evaluation
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());
271 }
272 casadi::MX out_cat = casadi::MX::vertcat(mx_outputs);
273
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());
278
279 // Step 3: Compare
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 " +
286 return result;
287 }
288
289 result.max_value_error = (numeric_output - symbolic_output).cwiseAbs().maxCoeff();
290 result.values_match = result.max_value_error < opts.value_tol;
291
292 if (!result.values_match) {
293 std::ostringstream ss;
294 ss << "Value mismatch at point " << detail::format_point(point)
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();
299 }
300
301 } catch (const std::exception &e) {
302 result.symbolic_compiles = false;
303 result.failure_detail =
304 "Symbolic mode failed at point " + detail::format_point(point) + ": " + e.what();
305 }
306
307 return result;
308}
309
313template <typename Func>
314DualModeResult verify_dual_mode(Func &&f, const std::vector<std::vector<double>> &test_points,
315 const DiffTestOptions &opts = {}) {
316 DualModeResult overall;
317
318 if (test_points.empty()) {
319 overall.failure_detail = "No test points supplied";
320 return overall;
321 }
322
323 overall.symbolic_compiles = true;
324 overall.values_match = true;
325 overall.max_value_error = 0.0;
326
327 for (const auto &point : test_points) {
328 auto r = verify_dual_mode_at_point(f, point, opts);
329 overall.max_value_error = std::max(overall.max_value_error, r.max_value_error);
330
331 if (!r.symbolic_compiles) {
332 overall.symbolic_compiles = false;
333 overall.failure_detail = r.failure_detail;
334 return overall;
335 }
336 if (!r.values_match) {
337 overall.values_match = false;
338 overall.failure_detail = r.failure_detail;
339 return overall;
340 }
341 }
342
343 return overall;
344}
345
350template <typename Func>
351DiffTestResult verify_differentiable_at_point(Func &&f, const std::vector<double> &point,
352 const DiffTestOptions &opts = {}) {
353 DiffTestResult result;
354 constexpr int n = detail::arity_of<Func, double>();
355
356 // Step 1: Run dual-mode checks
357 auto dm = verify_dual_mode_at_point(f, point, opts);
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;
362
363 if (!result.symbolic_compiles || !result.values_match) {
364 return result;
365 }
366
367 // Step 2: Compute FD Jacobian (central differences)
368 Eigen::VectorXd f0;
369 try {
371 } catch (const std::exception &e) {
372 result.jacobian_matches = false;
373 result.failure_detail =
374 "Numeric evaluation failed at point " + detail::format_point(point) + ": " + e.what();
375 return result;
376 }
377 const int m = static_cast<int>(f0.size());
378
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;
385
386 try {
387 Eigen::VectorXd f_plus = detail::to_numeric_vector(detail::invoke(f, point_plus));
388 Eigen::VectorXd f_minus = detail::to_numeric_vector(detail::invoke(f, point_minus));
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;
392 result.failure_detail = "FD probe failed at point " + detail::format_point(point) +
393 " perturbing input " + std::to_string(j) + ": " + e.what();
394 return result;
395 }
396 }
397 result.fd_jacobian = fd_jac;
398
399 // Step 3: Compute AD Jacobian
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)));
404 }
405
406 try {
407 auto sym_result = detail::invoke(f, sym_vars);
408 std::vector<SymbolicArg> output_args = detail::to_symbolic_output_args(sym_result);
409 std::vector<SymbolicArg> input_args(sym_vars.begin(), sym_vars.end());
410
411 // Compute symbolic Jacobian
412 auto J_sym = metis::jacobian(output_args, input_args);
413
414 // Wrap in Function and evaluate
415 metis::Function J_func(input_args, {SymbolicArg(J_sym)});
416 auto J_results = J_func(point);
417 result.ad_jacobian = J_results[0];
418
419 } catch (const std::exception &e) {
420 result.jacobian_matches = false;
421 result.failure_detail = "AD Jacobian computation failed at point " +
422 detail::format_point(point) + ": " + e.what();
423 return result;
424 }
425
426 // Step 4: Compare AD vs FD Jacobian
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;
430 ss << "Jacobian dimension mismatch at point " << detail::format_point(point)
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();
434 return result;
435 }
436
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);
444
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();
448 worst_row = i;
449 worst_col = j;
450 break;
451 }
452
453 double error = std::abs(ad_val - fd_val);
454 if (error > result.max_jacobian_error) {
455 result.max_jacobian_error = error;
456 worst_row = i;
457 worst_col = j;
458 }
459
460 double tol = opts.jac_atol + opts.jac_rtol * std::abs(ad_val);
461 if (error > tol) {
462 result.jacobian_matches = false;
463 break;
464 }
465 }
466 }
467
468 if (!result.jacobian_matches) {
469 std::ostringstream ss;
470 ss << "Jacobian mismatch at point " << detail::format_point(point)
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)
476 << "\n AD Jacobian: " << detail::format_matrix(result.ad_jacobian)
477 << " FD Jacobian: " << detail::format_matrix(fd_jac);
478 result.failure_detail = ss.str();
479 }
480
481 return result;
482}
483
498template <typename Func>
499DiffTestResult verify_differentiable(Func &&f, const std::vector<std::vector<double>> &test_points,
500 const DiffTestOptions &opts = {}) {
501 DiffTestResult overall;
502
503 if (test_points.empty()) {
504 overall.failure_detail = "No test points supplied";
505 return overall;
506 }
507
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;
513
514 for (const auto &point : test_points) {
515 auto r = verify_differentiable_at_point(f, point, opts);
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);
518
519 if (!r.symbolic_compiles) {
520 overall.symbolic_compiles = false;
521 overall.failure_detail = r.failure_detail;
522 return overall;
523 }
524 if (!r.values_match) {
525 overall.values_match = false;
526 overall.failure_detail = r.failure_detail;
527 return overall;
528 }
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;
534 return overall;
535 }
536 }
537
538 return overall;
539}
540
541} // namespace metis::diff_test
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