tinyopt 700
Tinyopt, a lightweight, header only optimization library
Loading...
Searching...
No Matches
num_diff.h
1// Copyright 2026 Julien Michot.
2// SPDX-License-Identifier: Apache-2.0
3
4#pragma once
5
6#include <tinyopt/cost.h>
7#include <tinyopt/math.h> // Defines Matrix and Vector
8#include <tinyopt/traits.h>
9#include <cstddef>
10
11namespace tinyopt::diff {
20enum Method {
29 kForward = 0,
30
40 kCentral,
41
51 kFastCentral
52};
53
56template <typename X_t, typename Func>
57auto NumEval(const X_t &x, const Func &f, const diff::Method &method = diff::Method::kCentral,
58 typename traits::params_trait<X_t>::Scalar h =
59 FloatEpsilon<typename traits::params_trait<X_t>::Scalar>()) {
60 using ptrait = traits::params_trait<X_t>;
61 using Scalar = typename ptrait::Scalar;
62
63 constexpr Index Dims = ptrait::Dims;
64 const Index dims = traits::DynDims(x);
65
66 // Support different function signatures
67 auto fg = [&](const auto &x) {
68 std::nullptr_t nul;
69 if constexpr (std::is_invocable_v<Func, const X_t &>)
70 return f(x);
71 else if constexpr (std::is_invocable_v<Func, const X_t &, std::nullptr_t &>)
72 return f(x, nul);
73 else if constexpr (std::is_invocable_v<Func, const X_t &, std::nullptr_t &, std::nullptr_t &>)
74 return f(x, nul, nul);
75 else { // likely a SparseMatrix<Scalar> hessian
76 SparseMatrix<Scalar> H;
77 H.resize(dims, dims);
78 return f(x, nul, H);
79 }
80 };
81
82 // Recover current residuals
83 const auto res = fg(x);
84 // Declare the jacobian matrix
85 using ResType = typename std::decay_t<decltype(res)>;
86 constexpr int ResDims = traits::params_trait<ResType>::Dims;
87 const Index res_dims = traits::DynDims(res);
88
89 using J_t = Matrix<Scalar, ResDims, Dims>;
90 J_t J(res_dims, dims);
91
92 // Estimate the jacobian using numerical differentiation
93 Vector<Scalar, Dims> dx = Vector<Scalar, Dims>::Zero(dims);
94 for (Index r = 0; r < dims; ++r) {
95 X_t y = x; // copy
96 if (r > 0) dx[r - 1] = 0;
97 dx[r] = h;
98 ptrait::PlusEq(y, dx);
99 const auto res_plus = fg(y);
100 using ResType2 = typename std::decay_t<decltype(res_plus)>;
101 if (method == Method::kCentral) {
102 y = x; // copy again
103 dx[r] = -h;
104 ptrait::PlusEq(y, dx);
105 const auto res_minus = fg(y);
106 if constexpr (std::is_scalar_v<ResType2>)
107 J[r] = (res_plus - res_minus) / (2 * h);
108 else
109 J.col(r) = (res_plus.reshaped() - res_minus.reshaped()) / (2 * h);
110 } else if (method == Method::kFastCentral) {
111 dx[r] = -2 * h; // given a small h, one can use this approximation, hopefully
112 ptrait::PlusEq(y, dx);
113 const auto res_minus = fg(y);
114 if constexpr (std::is_scalar_v<ResType2>)
115 J[r] = (res_plus - res_minus) / (2 * h);
116 else
117 J.col(r) = (res_plus.reshaped() - res_minus.reshaped()) / (2 * h);
118 } else {
119 if constexpr (std::is_scalar_v<ResType2>)
120 J[r] = (res_plus - res) / h;
121 else
122 J.col(r) = (res_plus.reshaped() - res) / h;
123 }
124 }
125 return std::make_pair(res, J);
126}
127
130template <typename X_t, typename Func>
131auto EstimateNumJac(const X_t &x, const Func &f,
132 const diff::Method &method = diff::Method::kCentral,
133 typename traits::params_trait<X_t>::Scalar h =
134 FloatEpsilon<typename traits::params_trait<X_t>::Scalar>()) {
135 const auto &[res, J] = NumEval(x, f, method, h);
136 return J;
137}
138
197template <typename X_t, typename ResidualsFunc>
198auto CreateNumDiffFunc1(X_t &, const ResidualsFunc &residuals,
199 const Method &method = Method::kCentral,
200 typename traits::params_trait<X_t>::Scalar h =
201 FloatEpsilon<typename traits::params_trait<X_t>::Scalar>()) {
202 auto loss = [&residuals, method, h](const auto &x, auto &grad) {
203 constexpr bool HasGrad = !traits::is_nullptr_v<decltype(grad)>;
204 // Recover current residuals
205 const auto res = residuals(x);
206
207 if constexpr (HasGrad) {
208 const auto J = EstimateNumJac(x, residuals, method, h);
209 grad = J.transpose() * res;
210 }
211
212 using ResType = typename std::decay_t<decltype(res)>;
213 if constexpr (std::is_scalar_v<ResType>) {
214 return std::abs(res);
215 } else {
216 // Returns the norm + number of residuals
217 return Cost(res.matrix().norm(), res.size());
218 }
219 };
220 return loss;
221}
222
283template <typename X_t, typename ResidualsFunc>
284auto CreateNumDiffFunc2(X_t &, const ResidualsFunc &residuals,
285 const Method &method = Method::kCentral,
286 typename traits::params_trait<X_t>::Scalar h =
287 FloatEpsilon<typename traits::params_trait<X_t>::Scalar>()) {
288 auto loss = [&residuals, method, h](const auto &x, auto &grad, auto &H) {
289 constexpr bool HasGrad = !traits::is_nullptr_v<decltype(grad)>;
290 constexpr bool HasH = !traits::is_nullptr_v<decltype(H)>;
291 // Recover current residuals
292 const auto res = residuals(x);
293
294 if constexpr (HasGrad) {
295 const auto J = EstimateNumJac(x, residuals, method, h);
296 grad = J.transpose() * res;
297 if constexpr (HasH) H = J.transpose() * J;
298 }
299
300 using ResType = typename std::decay_t<decltype(res)>;
301 if constexpr (std::is_scalar_v<ResType>) {
302 return std::abs(res);
303 } else {
304 // Returns the norm + number of residuals
305 return Cost(res.matrix().norm(), res.size());
306 }
307 };
308 return loss;
309}
310
311} // namespace tinyopt::diff