tinyopt 700
Tinyopt, a lightweight, header only optimization library
Loading...
Searching...
No Matches
optimize_autodiff.h
1// Copyright 2026 Julien Michot.
2// SPDX-License-Identifier: Apache-2.0
3
4#pragma once
5
6#include <algorithm>
7#include <cassert>
8#include <cstddef>
9#include <tuple>
10#include <type_traits>
11#include <utility>
12
13#include <tinyopt/cost.h>
14#include <tinyopt/math.h>
15#include <tinyopt/traits.h> // must be before jet.h
16
17#include <tinyopt/diff/jet.h> // Import Jet's Automatic Differentiation
18
19namespace tinyopt {
20
23template <bool IsNLLS, typename X_t, typename ResidualsFunc, typename OptimizeFunc,
24 typename OptionsType>
25inline auto OptimizeWithAutoDiff(X_t &x, const ResidualsFunc &residuals,
26 const OptimizeFunc &optimize, const OptionsType &options) {
27 using ptrait = traits::params_trait<X_t>;
28 using Scalar = typename ptrait::Scalar;
29 constexpr Index Dims = ptrait::Dims;
30 constexpr bool is_userdef_type =
31 !std::is_floating_point_v<X_t> && !traits::is_matrix_or_array_v<X_t>;
32
33 const Index dims = traits::DynDims(x);
34
35 // Construct the Jet
36 using Jet = diff::Jet<Scalar, Dims>;
37
38 // Note: XJetType and DXJetType are only instantiated to avoid having to set the 'v's at each
39 // iteration
40
41 // Construct the Jet for scalar or Matrix, so {Jet, Vector<Jet, N> ot nullptr_t}
42 using XJetType = std::conditional_t<
43 std::is_floating_point_v<X_t>, Jet,
44 std::conditional_t<is_userdef_type, std::nullptr_t, decltype(ptrait::template cast<Jet>(x))>>;
45 XJetType x_jet;
46
47 // DXJetType is either of {Vector<Jet, Dims>, Matrix<Jet, Rows, Cols>, nullptr}
48 using DXJetType = std::conditional_t<is_userdef_type, Vector<Jet, Dims>, std::nullptr_t>;
49
50 DXJetType dx_jet; // only for user defined X type
51 if constexpr (is_userdef_type) { // X is user defined object
52 dx_jet = DXJetType::Zero(dims);
53 for (Index i = 0; i < dims; ++i) {
54 // If X size at compile time is not known, we need to set the Jet.v
55 if constexpr (Dims == Dynamic) dx_jet[i].v = Vector<Scalar, Dynamic>::Zero(dims);
56 dx_jet[i].v[i] = 1;
57 }
58 // dx_jet is constant
59 } else if constexpr (std::is_floating_point_v<X_t>) { // X is scalar
60 x_jet = XJetType(x);
61 x_jet.v[0] = 1;
62 } else { // X is a Vector or Matrix
63 x_jet = ptrait::template cast<Jet>(x);
64 // Set Jet's v
65 for (int c = 0; c < x.cols(); ++c) {
66 for (int r = 0; r < x.rows(); ++r) {
67 const auto i = r + c * x.rows();
68 if constexpr (Dims == Dynamic) x_jet(r, c).v = Vector<Scalar, Dims>::Zero(dims);
69 x_jet(r, c).v[i] = 1;
70 }
71 }
72 }
73
74 // Update jet with latest 'x' values, either returning the 'x_jet' or a local copy for user
75 // defined types
76 auto update_x_jet = [&](const auto &x) {
77 if constexpr (is_userdef_type) {
78 auto x_jet = ptrait::template cast<Jet>(x); // Cast X to a Jet type // TODO reduce copy
79 using ptrait_jet = traits::params_trait<std::decay_t<decltype(x_jet)>>;
80 ptrait_jet::PlusEq(x_jet, dx_jet);
81 return std::move(x_jet);
82 } else if constexpr (std::is_floating_point_v<X_t>) { // X is scalar
83 x_jet.a = x;
84 return x_jet;
85 } else { // X is a Vector or Matrix
86 for (int c = 0; c < x.cols(); ++c) {
87 for (int r = 0; r < x.rows(); ++r) {
88 x_jet(r, c).a = x(r, c);
89 }
90 }
91 return x_jet;
92 }
93 };
94
95 auto acc = [&](const auto &x, auto &grad, auto &H) {
96 using H_t = decltype(H);
97 constexpr bool HasGrad = !traits::is_nullptr_v<decltype(grad)>;
98 constexpr bool HasH = !traits::is_nullptr_v<H_t>;
99
100 const auto &x_jet = update_x_jet(x);
101 const auto res = residuals(x_jet);
102 using ResType = typename std::decay_t<decltype(res)>;
103
104 static_assert(IsNLLS || traits::is_scalar_v<ResType> || traits::is_tuple_v<ResType>,
105 "General optimization cost function must return a scalar value");
106
107 auto process_tuple_item = [&](const auto &item) -> Cost {
108 using ItemType = std::decay_t<decltype(item)>;
109
110 if constexpr (traits::is_robust_residual_v<ItemType>) {
111 const auto &raw = item.residual;
112 using RawType = std::decay_t<decltype(raw)>;
113 static_assert(traits::is_jet_type_v<RawType> ||
114 (traits::is_matrix_or_array_v<RawType> &&
115 traits::is_jet_type_v<typename RawType::Scalar>));
116
117 constexpr int ResDims = traits::params_trait<RawType>::Dims;
118 const Index res_size = traits::DynDims(raw);
119 using J_t = Matrix<Scalar, ResDims, Dims>;
120
121 J_t J(res_size, dims);
122 Vector<Scalar, ResDims> res_f(res_size);
123 if constexpr (traits::is_matrix_or_array_v<RawType>) {
124 if constexpr (RawType::ColsAtCompileTime != 1) {
125 for (int c = 0; c < raw.cols(); ++c)
126 for (int r = 0; r < raw.rows(); ++r) {
127 const Index i = r + c * raw.rows();
128 J.row(i) = raw(r, c).v;
129 res_f[i] = raw(r, c).a;
130 }
131 } else {
132 for (Index i = 0; i < res_size; ++i) {
133 J.row(i) = raw[i].v;
134 res_f[i] = raw[i].a;
135 }
136 }
137 } else {
138 J.row(0) = raw.v;
139 res_f[0] = raw.a;
140 }
141
142 const double sq_norm = static_cast<double>(res_f.squaredNorm());
143 const double weight =
144 sq_norm <= item.delta2
145 ? 1.0
146 : item.delta / std::sqrt(std::max(sq_norm, std::numeric_limits<double>::min()));
147 const double robust_cost =
148 sq_norm <= item.delta2
149 ? sq_norm
150 : 2.0 * item.delta *
151 std::sqrt(std::max(sq_norm, std::numeric_limits<double>::min())) -
152 item.delta2;
153
154 if constexpr (HasGrad) grad.noalias() += weight * (J.transpose() * res_f);
155 if constexpr (HasH) {
156 if constexpr (traits::is_sparse_matrix_v<H_t>) {
157 H += (weight * (J.transpose() * J)).sparseView();
158 } else {
159 H.noalias() += weight * (J.transpose() * J);
160 }
161 }
162
163 return Cost(robust_cost, res_size, sq_norm <= item.delta2 ? 1.0f : 0.0f);
164 } else {
165 static_assert(
166 traits::is_jet_type_v<ItemType> || (traits::is_matrix_or_array_v<ItemType> &&
167 traits::is_jet_type_v<typename ItemType::Scalar>),
168 "Residual blocks must be Jet-valued vectors/matrices or robust wrappers");
169
170 if constexpr (traits::is_scalar_v<ItemType>) {
171 if constexpr (HasGrad) {
172 const auto &J = item.v;
173 if constexpr (std::is_floating_point_v<X_t>) {
174 grad[0] += J[0] * item.a;
175 if constexpr (HasH) H(0, 0) += J[0] * J[0];
176 } else {
177 grad.noalias() += J.transpose() * item.a;
178 if constexpr (HasH) {
179 if constexpr (traits::is_sparse_matrix_v<H_t>) {
180 H += (J * J.transpose()).sparseView();
181 } else {
182 H.noalias() += J * J.transpose();
183 }
184 }
185 }
186 }
187 return Cost(IsNLLS ? item.a * item.a : item.a, 1);
188 } else {
189 constexpr int ResDims = traits::params_trait<ItemType>::Dims;
190 const Index res_size = traits::DynDims(item);
191 using J_t = Matrix<Scalar, ResDims, Dims>;
192
193 J_t J(res_size, dims);
194 Vector<Scalar, ResDims> res_f(res_size);
195 if constexpr (traits::is_matrix_or_array_v<ItemType>) {
196 if constexpr (ItemType::ColsAtCompileTime != 1) {
197 for (int c = 0; c < item.cols(); ++c)
198 for (int r = 0; r < item.rows(); ++r) {
199 const Index i = r + c * item.rows();
200 J.row(i) = item(r, c).v;
201 res_f[i] = item(r, c).a;
202 }
203 } else {
204 for (Index i = 0; i < res_size; ++i) {
205 J.row(i) = item[i].v;
206 res_f[i] = item[i].a;
207 }
208 }
209 }
210
211 if constexpr (HasGrad) {
212 grad.noalias() += J.transpose() * res_f;
213 if constexpr (HasH) {
214 if constexpr (traits::is_sparse_matrix_v<H_t>) {
215 H += (J.transpose() * J).sparseView();
216 } else {
217 H.noalias() += J.transpose() * J;
218 }
219 }
220 }
221 return Cost(res_f.squaredNorm(), res_size);
222 }
223 }
224 };
225
226 if constexpr (traits::is_tuple_v<ResType>) {
227 Cost total;
228 std::apply([&](auto &&...items) { ((total += process_tuple_item(items)), ...); }, res);
229 return total;
230 } else {
231 static_assert(IsNLLS || traits::is_scalar_v<ResType>,
232 "General optimization cost function must return a scalar value");
233 static_assert(traits::is_jet_type_v<ResType> ||
234 (traits::is_matrix_or_array_v<ResType> &&
235 traits::is_jet_type_v<typename ResType::Scalar>));
236
237 if constexpr (traits::is_scalar_v<ResType>) { // One residual
238 if constexpr (HasGrad) {
239 const auto &J = res.v;
240 if constexpr (std::is_floating_point_v<X_t>) {
241 if constexpr (IsNLLS)
242 grad[0] = J[0] * res.a;
243 else
244 grad[0] = J[0];
245 if constexpr (HasH) H(0, 0) = J[0] * J[0];
246 } else {
247 if constexpr (IsNLLS)
248 grad.noalias() = J.transpose() * res.a;
249 else
250 grad = J;
251 if constexpr (HasH) {
252 if constexpr (traits::is_sparse_matrix_v<H_t>) {
253 H += (J * J.transpose()).sparseView();
254 } else {
255 H.noalias() += J * J.transpose();
256 }
257 }
258 }
259 }
260 return Cost{IsNLLS ? res.a * res.a : res.a}; // NLLS -> return ε², else ε
261 } else { // Extract jacobian (TODO speed this up)
262 constexpr int ResDims = traits::params_trait<ResType>::Dims;
263 const Index res_size = traits::DynDims(res);
264 using J_t = Matrix<Scalar, ResDims, Dims>;
265
266 J_t J(res_size, dims); // TODO make J sparse if H is.
267 Vector<Scalar, ResDims> res_f(res.size());
268 if constexpr (traits::is_matrix_or_array_v<ResType>) {
269 if constexpr (ResType::ColsAtCompileTime != 1) { // Matrix or Vector with dynamic size
270 for (int c = 0; c < res.cols(); ++c)
271 for (int r = 0; r < res.rows(); ++r) {
272 const Index i = r + c * res.rows();
273 if constexpr (HasGrad) J.row(i) = res(r, c).v;
274 res_f[i] = res(r, c).a;
275 }
276 } else { // Vector
277 for (Index i = 0; i < res_size; ++i) {
278 if constexpr (HasGrad) J.row(i) = res[i].v;
279 res_f[i] = res[i].a;
280 }
281 }
282 } else { // scalar
283 for (Index i = 0; i < res_size; ++i) {
284 if constexpr (HasGrad) J.row(i) = res.v;
285 res_f[i] = res.a;
286 }
287 }
288 if constexpr (HasGrad) {
289 grad = J.transpose() * res_f;
290 if constexpr (HasH) {
291 if constexpr (traits::is_sparse_matrix_v<H_t>) {
292 H = (J.transpose() * J).sparseView();
293 } else {
294 H = J.transpose() * J;
295 }
296 }
297 if (options.log.enable && options.log.print_J_jet)
298 TINYOPT_LOG("Jt:\n{}\n", J.transpose().eval());
299 }
300 return Cost(res_f.squaredNorm(), res_size);
301 }
302 }
303 };
304
305 return optimize(x, acc, options);
306}
307} // namespace tinyopt
Definition cost.h:18
Definition parameter_traits.h:15