Janus 2.0.0
High-performance C++20 dual-mode numerical framework
Loading...
Searching...
No Matches
RootFinding.hpp
Go to the documentation of this file.
1#pragma once
7
11#include <casadi/casadi.hpp>
12
13#include <algorithm>
14#include <atomic>
15#include <cmath>
16#include <cstdint>
17#include <iostream>
18#include <limits>
19#include <string>
20#include <vector>
21
22namespace janus {
23
34
45
55 double abstol = 1e-10; // Absolute tolerance on residual infinity norm
56 double abstolStep = 1e-10; // Stall threshold on step infinity norm
57 int max_iter = 50; // Total numeric iteration budget across all stages
58 bool line_search = true; // Include line-search Newton in Auto fallback stack
59 bool verbose = false; // Print numeric solver stage progress
60 std::string linear_solver = "qr"; // Linear solver used by CasADi rootfinder
61 casadi::Dict linear_solver_options; // Options forwarded to the linear solver
69 int broyden_jacobian_refresh = 8; // 0 disables exact Jacobian refreshes
70 double pseudo_transient_dt0 = 1e-2;
73};
74
79 int implicit_input_index = 0; // Input slot containing the unknown state
80 int implicit_output_index = 0; // Output slot containing the residual equation
81};
82
86template <typename Scalar> struct RootResult {
87 Eigen::Matrix<Scalar, Eigen::Dynamic, 1> x; // Solution
88 int iterations = 0; // Total numeric iterations used
89 bool converged = false; // Whether solution converged
91 double residual_norm = std::numeric_limits<double>::infinity();
92 double step_norm = 0.0;
93 std::string message = "";
94};
95
96namespace detail {
97
98inline std::string method_name(RootSolveMethod method) {
99 switch (method) {
101 return "none";
103 return "trust-region Newton";
105 return "line-search Newton";
107 return "quasi-Newton Broyden";
109 return "pseudo-transient continuation";
110 }
111 throw InvalidArgument("rootfinder: unsupported solve method");
112}
113
129
130inline std::string unique_name(const std::string &prefix) {
131 static std::atomic<std::uint64_t> counter{0};
132 return prefix + "_" + std::to_string(counter.fetch_add(1));
133}
134
135inline void validate_root_options(const RootFinderOptions &opts, const std::string &context) {
136 if (opts.abstol <= 0.0) {
137 throw InvalidArgument(context + ": abstol must be positive");
138 }
139 if (opts.abstolStep <= 0.0) {
140 throw InvalidArgument(context + ": abstolStep must be positive");
141 }
142 if (opts.max_iter <= 0) {
143 throw InvalidArgument(context + ": max_iter must be positive");
144 }
145 if (opts.trust_region_initial_damping <= 0.0) {
146 throw InvalidArgument(context + ": trust_region_initial_damping must be positive");
147 }
148 if (opts.trust_region_damping_increase <= 1.0) {
149 throw InvalidArgument(context + ": trust_region_damping_increase must exceed 1");
150 }
151 if (opts.trust_region_damping_decrease <= 0.0 || opts.trust_region_damping_decrease >= 1.0) {
152 throw InvalidArgument(context + ": trust_region_damping_decrease must lie in (0, 1)");
153 }
154 if (opts.line_search_contraction <= 0.0 || opts.line_search_contraction >= 1.0) {
155 throw InvalidArgument(context + ": line_search_contraction must lie in (0, 1)");
156 }
157 if (opts.line_search_sufficient_decrease <= 0.0 ||
159 throw InvalidArgument(context + ": line_search_sufficient_decrease must lie in (0, 1)");
160 }
161 if (opts.max_backtracking_steps <= 0) {
162 throw InvalidArgument(context + ": max_backtracking_steps must be positive");
163 }
164 if (opts.broyden_jacobian_refresh < 0) {
165 throw InvalidArgument(context + ": broyden_jacobian_refresh cannot be negative");
166 }
167 if (opts.pseudo_transient_dt0 <= 0.0) {
168 throw InvalidArgument(context + ": pseudo_transient_dt0 must be positive");
169 }
170 if (opts.pseudo_transient_dt_growth <= 1.0) {
171 throw InvalidArgument(context + ": pseudo_transient_dt_growth must exceed 1");
172 }
174 throw InvalidArgument(context +
175 ": pseudo_transient_dt_max must be at least pseudo_transient_dt0");
176 }
177}
178
179inline void validate_root_problem(const casadi::Function &f_casadi, const std::string &context) {
180 if (f_casadi.n_in() != 1 || f_casadi.n_out() != 1) {
181 throw InvalidArgument(context + ": Function F must have exactly 1 input and 1 output");
182 }
183
184 const auto input_sparsity = f_casadi.sparsity_in(0);
185 const auto output_sparsity = f_casadi.sparsity_out(0);
186 if (!input_sparsity.is_dense() || !input_sparsity.is_column()) {
187 throw InvalidArgument(context + ": input must be a dense column vector");
188 }
189 if (!output_sparsity.is_dense() || !output_sparsity.is_column()) {
190 throw InvalidArgument(context + ": output must be a dense column vector residual");
191 }
192 if (f_casadi.nnz_in(0) != f_casadi.nnz_out(0)) {
193 throw InvalidArgument(context + ": input and output dimensions must match");
194 }
195}
196
197// Helper to convert options to CasADi dictionary
198inline casadi::Dict opts_to_dict(const RootFinderOptions &opts) {
199 casadi::Dict d;
200 d["abstol"] = opts.abstol;
201 d["abstolStep"] = opts.abstolStep;
202 d["max_iter"] = opts.max_iter;
203 d["linear_solver"] = opts.linear_solver;
204 if (!opts.linear_solver_options.empty()) {
205 d["linear_solver_options"] = opts.linear_solver_options;
206 }
207 if (opts.verbose) {
208 d["verbose"] = true;
209 d["print_in"] = true;
210 d["print_out"] = true;
211 }
212 return d;
213}
214
215inline casadi::DM vector_to_dm(const Eigen::VectorXd &x) {
216 casadi::DM out(x.size(), 1);
217 for (Eigen::Index i = 0; i < x.size(); ++i) {
218 out(static_cast<int>(i), 0) = x(i);
219 }
220 return out;
221}
222
223inline Eigen::VectorXd dm_to_vector(const casadi::DM &x) {
224 const std::vector<double> elements = static_cast<std::vector<double>>(x);
225 return Eigen::Map<const Eigen::VectorXd>(elements.data(),
226 static_cast<Eigen::Index>(elements.size()));
227}
228
229inline Eigen::MatrixXd dm_to_matrix(const casadi::DM &x) {
230 const std::vector<double> elements = static_cast<std::vector<double>>(x);
231 return Eigen::Map<const Eigen::MatrixXd>(elements.data(), x.size1(), x.size2());
232}
233
234inline std::string implicit_function_name(const casadi::Function &g_casadi) {
235 return g_casadi.name() + "_implicit";
236}
237
238inline void validate_implicit_problem(const casadi::Function &g_casadi,
239 const Eigen::VectorXd &x_guess,
240 const ImplicitFunctionOptions &implicit_opts) {
241 if (g_casadi.n_in() == 0 || g_casadi.n_out() == 0) {
242 throw InvalidArgument(
243 "create_implicit_function: G must have at least one input and one output");
244 }
245
246 if (implicit_opts.implicit_input_index < 0 ||
247 implicit_opts.implicit_input_index >= g_casadi.n_in()) {
248 throw InvalidArgument("create_implicit_function: implicit_input_index out of range");
249 }
250 if (implicit_opts.implicit_output_index < 0 ||
251 implicit_opts.implicit_output_index >= g_casadi.n_out()) {
252 throw InvalidArgument("create_implicit_function: implicit_output_index out of range");
253 }
254
255 const auto unknown_sparsity = g_casadi.sparsity_in(implicit_opts.implicit_input_index);
256 if (!unknown_sparsity.is_dense() || !unknown_sparsity.is_column()) {
257 throw InvalidArgument(
258 "create_implicit_function: implicit input must be a dense column vector");
259 }
260
261 const auto residual_sparsity = g_casadi.sparsity_out(implicit_opts.implicit_output_index);
262 if (!residual_sparsity.is_dense() || !residual_sparsity.is_column()) {
263 throw InvalidArgument(
264 "create_implicit_function: implicit output must be a dense column vector residual");
265 }
266
267 const auto n_unknown =
268 static_cast<Eigen::Index>(g_casadi.nnz_in(implicit_opts.implicit_input_index));
269 const auto n_residual =
270 static_cast<Eigen::Index>(g_casadi.nnz_out(implicit_opts.implicit_output_index));
271 if (n_unknown != n_residual) {
272 throw InvalidArgument(
273 "create_implicit_function: implicit input and output dimensions must match");
274 }
275 if (x_guess.size() != n_unknown) {
276 throw InvalidArgument("create_implicit_function: x_guess size must match the implicit "
277 "input dimension");
278 }
279}
280
281inline Eigen::VectorXd solve_linear_system(const Eigen::MatrixXd &A, const Eigen::VectorXd &b) {
282 return A.colPivHouseholderQr().solve(b);
283}
284
285inline bool all_finite(const Eigen::VectorXd &x) { return x.array().isFinite().all(); }
286
287inline bool all_finite(const Eigen::MatrixXd &x) { return x.array().isFinite().all(); }
288
290 Eigen::VectorXd x;
291 Eigen::VectorXd residual;
292 Eigen::MatrixXd jacobian;
293 double residual_norm = std::numeric_limits<double>::infinity();
294 double merit = std::numeric_limits<double>::infinity();
295};
296
305
306inline Eigen::VectorXd evaluate_residual_only(const casadi::Function &residual_fn,
307 const Eigen::VectorXd &x) {
308 const std::vector<casadi::DM> residual_dm =
309 residual_fn(std::vector<casadi::DM>{vector_to_dm(x)});
310 if (residual_dm.size() != 1u) {
311 throw JanusError("rootfinder: residual function must return exactly one output");
312 }
313 return dm_to_vector(residual_dm.front());
314}
315
316inline NumericState evaluate_state(const casadi::Function &residual_fn,
317 const casadi::Function &jacobian_fn, const Eigen::VectorXd &x,
318 const std::string &context) {
319 NumericState state;
320 state.x = x;
321 state.residual = evaluate_residual_only(residual_fn, x);
322
323 const std::vector<casadi::DM> jacobian_dm =
324 jacobian_fn(std::vector<casadi::DM>{vector_to_dm(x)});
325 if (jacobian_dm.size() != 1u) {
326 throw JanusError(context + ": Jacobian function must return exactly one output");
327 }
328 state.jacobian = dm_to_matrix(jacobian_dm.front());
329 state.residual_norm = state.residual.lpNorm<Eigen::Infinity>();
330 state.merit = 0.5 * state.residual.squaredNorm();
331
332 if (!all_finite(state.residual) || !all_finite(state.jacobian) ||
333 !std::isfinite(state.residual_norm) || !std::isfinite(state.merit)) {
334 throw JanusError(context + ": residual/Jacobian evaluation produced non-finite values");
335 }
336 return state;
337}
338
339inline void maybe_log(const RootFinderOptions &opts, const std::string &message) {
340 if (opts.verbose) {
341 std::cout << "[rootfinder] " << message << "\n";
342 }
343}
344
345inline bool is_converged(const NumericState &state, const RootFinderOptions &opts) {
346 return state.residual_norm <= opts.abstol;
347}
348
349inline StageOutcome solve_trust_region(const casadi::Function &residual_fn,
350 const casadi::Function &jacobian_fn,
351 const NumericState &start, const RootFinderOptions &opts,
352 int max_iterations) {
353 StageOutcome out;
355 out.state = start;
356 if (is_converged(start, opts) || max_iterations <= 0) {
357 out.converged = is_converged(start, opts);
358 out.message = out.converged ? "initial iterate satisfies tolerance"
359 : "no trust-region iterations remaining";
360 return out;
361 }
362
363 NumericState current = start;
364 double lambda = opts.trust_region_initial_damping;
365 constexpr double min_lambda = 1e-12;
366 constexpr double max_lambda = 1e12;
367
368 for (int iter = 0; iter < max_iterations; ++iter) {
369 const Eigen::VectorXd gradient = current.jacobian.transpose() * current.residual;
370 Eigen::MatrixXd normal_matrix = current.jacobian.transpose() * current.jacobian;
371 normal_matrix.diagonal().array() += lambda;
372
373 const Eigen::VectorXd step = solve_linear_system(normal_matrix, -gradient);
374 out.iterations += 1;
375 out.step_norm = step.lpNorm<Eigen::Infinity>();
376
377 if (!all_finite(step)) {
378 out.message = "trust-region Newton produced a non-finite step";
379 break;
380 }
381 if (out.step_norm <= opts.abstolStep) {
382 out.message = "trust-region Newton stalled on a tiny step";
383 break;
384 }
385
386 const double predicted = -gradient.dot(step) - 0.5 * step.dot(normal_matrix * step);
387 if (!(predicted > 0.0) || !std::isfinite(predicted)) {
388 lambda = std::min(max_lambda, lambda * opts.trust_region_damping_increase);
389 out.message = "trust-region Newton could not predict a decrease";
390 continue;
391 }
392
393 try {
394 NumericState candidate =
395 evaluate_state(residual_fn, jacobian_fn, current.x + step, "rootfinder");
396 const double actual = current.merit - candidate.merit;
397 const double rho = actual / predicted;
398
399 if (actual > 0.0 && rho > 1e-4) {
400 current = std::move(candidate);
401 out.state = current;
402 out.message = "trust-region Newton accepted a step";
403
404 if (rho > 0.75) {
405 lambda = std::max(min_lambda, lambda * opts.trust_region_damping_decrease);
406 } else if (rho < 0.25) {
407 lambda = std::min(max_lambda, lambda * opts.trust_region_damping_increase);
408 }
409
410 if (is_converged(current, opts)) {
411 out.converged = true;
412 out.message = "converged with trust-region Newton";
413 return out;
414 }
415 } else {
416 lambda = std::min(max_lambda, lambda * opts.trust_region_damping_increase);
417 out.message = "trust-region Newton rejected a step";
418 }
419 } catch (const std::exception &) {
420 lambda = std::min(max_lambda, lambda * opts.trust_region_damping_increase);
421 out.message = "trust-region Newton rejected an invalid trial step";
422 }
423 }
424
425 if (out.message.empty()) {
426 out.message = "trust-region Newton exhausted its iteration budget";
427 }
428 return out;
429}
430
431inline StageOutcome solve_line_search(const casadi::Function &residual_fn,
432 const casadi::Function &jacobian_fn,
433 const NumericState &start, const RootFinderOptions &opts,
434 int max_iterations) {
435 StageOutcome out;
437 out.state = start;
438 if (is_converged(start, opts) || max_iterations <= 0) {
439 out.converged = is_converged(start, opts);
440 out.message = out.converged ? "initial iterate satisfies tolerance"
441 : "no line-search iterations remaining";
442 return out;
443 }
444
445 NumericState current = start;
446 for (int iter = 0; iter < max_iterations; ++iter) {
447 const Eigen::VectorXd gradient = current.jacobian.transpose() * current.residual;
448 Eigen::VectorXd step = solve_linear_system(current.jacobian, -current.residual);
449 if (gradient.dot(step) >= 0.0) {
450 step = -gradient;
451 }
452
453 out.iterations += 1;
454 if (!all_finite(step)) {
455 out.message = "line-search Newton produced a non-finite step";
456 break;
457 }
458 const double full_step_norm = step.lpNorm<Eigen::Infinity>();
459 if (full_step_norm <= opts.abstolStep) {
460 out.step_norm = full_step_norm;
461 out.message = "line-search Newton stalled on a tiny step";
462 break;
463 }
464
465 double alpha = 1.0;
466 bool accepted = false;
467 const double directional_derivative = gradient.dot(step);
468 for (int bt = 0; bt < opts.max_backtracking_steps; ++bt) {
469 try {
470 NumericState candidate = evaluate_state(residual_fn, jacobian_fn,
471 current.x + alpha * step, "rootfinder");
472 if (candidate.merit <= current.merit + opts.line_search_sufficient_decrease *
473 alpha * directional_derivative) {
474 current = std::move(candidate);
475 out.state = current;
476 out.step_norm = (alpha * step).lpNorm<Eigen::Infinity>();
477 accepted = true;
478 break;
479 }
480 } catch (const std::exception &) {
481 }
482 alpha *= opts.line_search_contraction;
483 }
484
485 if (!accepted) {
486 out.message = "line-search Newton could not find an acceptable step";
487 break;
488 }
489
490 if (is_converged(current, opts)) {
491 out.converged = true;
492 out.message = "converged with line-search Newton";
493 return out;
494 }
495 }
496
497 if (out.message.empty()) {
498 out.message = "line-search Newton exhausted its iteration budget";
499 }
500 return out;
501}
502
503inline StageOutcome solve_broyden(const casadi::Function &residual_fn,
504 const casadi::Function &jacobian_fn, const NumericState &start,
505 const RootFinderOptions &opts, int max_iterations) {
506 StageOutcome out;
508 out.state = start;
509 if (is_converged(start, opts) || max_iterations <= 0) {
510 out.converged = is_converged(start, opts);
511 out.message = out.converged ? "initial iterate satisfies tolerance"
512 : "no Broyden iterations remaining";
513 return out;
514 }
515
516 Eigen::VectorXd x = start.x;
517 Eigen::VectorXd residual = start.residual;
518 double merit = start.merit;
519 Eigen::MatrixXd B = start.jacobian;
520 int accepted_steps = 0;
521
522 for (int iter = 0; iter < max_iterations; ++iter) {
523 Eigen::VectorXd step = solve_linear_system(B, -residual);
524 if (!all_finite(step)) {
525 out.message = "Broyden update produced a non-finite step";
526 break;
527 }
528
529 const double full_step_norm = step.lpNorm<Eigen::Infinity>();
530 out.iterations += 1;
531 if (full_step_norm <= opts.abstolStep) {
532 out.step_norm = full_step_norm;
533 out.message = "Broyden update stalled on a tiny step";
534 break;
535 }
536
537 double alpha = 1.0;
538 bool accepted = false;
539 Eigen::VectorXd candidate_x;
540 Eigen::VectorXd candidate_residual;
541 for (int bt = 0; bt < opts.max_backtracking_steps; ++bt) {
542 candidate_x = x + alpha * step;
543 try {
544 candidate_residual = evaluate_residual_only(residual_fn, candidate_x);
545 const double candidate_merit = 0.5 * candidate_residual.squaredNorm();
546 if (candidate_merit < merit) {
547 merit = candidate_merit;
548 out.step_norm = (alpha * step).lpNorm<Eigen::Infinity>();
549 accepted = true;
550 break;
551 }
552 } catch (const std::exception &) {
553 }
554 alpha *= opts.line_search_contraction;
555 }
556
557 if (!accepted) {
558 out.message = "Broyden update could not decrease the residual merit";
559 break;
560 }
561
562 const Eigen::VectorXd s = candidate_x - x;
563 const Eigen::VectorXd y = candidate_residual - residual;
564 const double denom = s.squaredNorm();
565
566 x = candidate_x;
567 residual = candidate_residual;
568 accepted_steps += 1;
569
570 if (residual.lpNorm<Eigen::Infinity>() <= opts.abstol) {
571 out.state = evaluate_state(residual_fn, jacobian_fn, x, "rootfinder");
572 out.converged = true;
573 out.message = "converged with Broyden updates";
574 return out;
575 }
576
577 const bool refresh_exact = opts.broyden_jacobian_refresh > 0 &&
578 accepted_steps % opts.broyden_jacobian_refresh == 0;
579 if (refresh_exact) {
580 out.state = evaluate_state(residual_fn, jacobian_fn, x, "rootfinder");
581 B = out.state.jacobian;
582 } else if (denom > std::numeric_limits<double>::epsilon()) {
583 B.noalias() += ((y - B * s) / denom) * s.transpose();
584 } else {
585 out.message = "Broyden update encountered a degenerate secant step";
586 break;
587 }
588 }
589
590 out.state = evaluate_state(residual_fn, jacobian_fn, x, "rootfinder");
591 if (out.message.empty()) {
592 out.message = "Broyden update exhausted its iteration budget";
593 }
594 return out;
595}
596
597inline StageOutcome solve_pseudo_transient(const casadi::Function &residual_fn,
598 const casadi::Function &jacobian_fn,
599 const NumericState &start, const RootFinderOptions &opts,
600 int max_iterations) {
601 StageOutcome out;
603 out.state = start;
604 if (is_converged(start, opts) || max_iterations <= 0) {
605 out.converged = is_converged(start, opts);
606 out.message = out.converged ? "initial iterate satisfies tolerance"
607 : "no pseudo-transient iterations remaining";
608 return out;
609 }
610
611 NumericState current = start;
612 double dt = opts.pseudo_transient_dt0;
613
614 for (int iter = 0; iter < max_iterations; ++iter) {
615 out.iterations += 1;
616 bool accepted = false;
617
618 for (int bt = 0; bt < opts.max_backtracking_steps; ++bt) {
619 Eigen::MatrixXd shifted = current.jacobian;
620 shifted.diagonal().array() += 1.0 / dt;
621
622 const Eigen::VectorXd step = solve_linear_system(shifted, -current.residual);
623 out.step_norm = step.lpNorm<Eigen::Infinity>();
624 if (!all_finite(step)) {
625 out.message = "pseudo-transient continuation produced a non-finite step";
626 return out;
627 }
628 if (out.step_norm <= opts.abstolStep) {
629 out.message = "pseudo-transient continuation stalled on a tiny step";
630 return out;
631 }
632
633 try {
634 NumericState candidate =
635 evaluate_state(residual_fn, jacobian_fn, current.x + step, "rootfinder");
636 if (candidate.merit < current.merit) {
637 current = std::move(candidate);
638 out.state = current;
639 dt = std::min(opts.pseudo_transient_dt_max,
641 accepted = true;
642 break;
643 }
644 } catch (const std::exception &) {
645 }
646
647 dt *= opts.line_search_contraction;
648 }
649
650 if (!accepted) {
651 out.message = "pseudo-transient continuation could not decrease the residual merit";
652 break;
653 }
654
655 if (is_converged(current, opts)) {
656 out.converged = true;
657 out.message = "converged with pseudo-transient continuation";
658 return out;
659 }
660 }
661
662 if (out.message.empty()) {
663 out.message = "pseudo-transient continuation exhausted its iteration budget";
664 }
665 return out;
666}
667
668} // namespace detail
669
678 public:
685 NewtonSolver(const janus::Function &F, const RootFinderOptions &opts = {}) : opts_(opts) {
686 detail::validate_root_options(opts_, "NewtonSolver");
687
688 casadi::Function f_casadi = F.casadi_function();
689 detail::validate_root_problem(f_casadi, "NewtonSolver");
690
691 residual_fn_ = f_casadi;
692 n_x_ = f_casadi.nnz_in(0);
693
694 casadi::MX x = janus::sym(f_casadi.name_in(0), f_casadi.size1_in(0), f_casadi.size2_in(0));
695 casadi::MX residual = f_casadi(std::vector<casadi::MX>{x}).at(0);
696 jacobian_fn_ = casadi::Function(detail::unique_name(f_casadi.name() + "_jacobian"),
697 std::vector<casadi::MX>{x},
698 std::vector<casadi::MX>{casadi::MX::jacobian(residual, x)});
699
700 try {
701 symbolic_solver_ = casadi::rootfinder(detail::unique_name("rf_solver"), "newton",
702 f_casadi, detail::opts_to_dict(opts_));
703 } catch (const std::exception &e) {
704 throw JanusError(std::string("NewtonSolver creation failed: ") + e.what());
705 }
706 }
707
711 template <typename Scalar>
712 RootResult<Scalar> solve(const Eigen::Matrix<Scalar, Eigen::Dynamic, 1> &x0) const {
713 RootResult<Scalar> result;
714 const int n_x = static_cast<int>(x0.size());
715 if (n_x != n_x_) {
716 throw InvalidArgument("NewtonSolver::solve: x0 size must match the residual dimension");
717 }
718
719 if constexpr (std::is_floating_point_v<Scalar>) {
720 const Eigen::VectorXd x0_numeric = x0.template cast<double>();
721 const auto initial = detail::evaluate_state(residual_fn_, jacobian_fn_, x0_numeric,
722 "NewtonSolver::solve");
723 detail::maybe_log(opts_, "initial residual inf-norm = " +
724 std::to_string(initial.residual_norm));
725
726 detail::NumericState current = initial;
727 detail::NumericState best = initial;
728
729 result.x = initial.x.template cast<Scalar>();
730 result.residual_norm = initial.residual_norm;
731 if (detail::is_converged(initial, opts_)) {
732 result.converged = true;
733 result.message = "Initial guess satisfies the residual tolerance";
734 return result;
735 }
736
737 int iterations_used = 0;
739 std::string last_message = "No numeric solver stage was executed";
740 std::vector<RootSolveMethod> order;
741 if (opts_.strategy == RootSolveStrategy::Auto) {
742 order.push_back(RootSolveMethod::TrustRegionNewton);
743 if (opts_.line_search) {
744 order.push_back(RootSolveMethod::LineSearchNewton);
745 }
748 } else {
749 order.push_back(detail::strategy_to_method(opts_.strategy));
750 }
751
752 for (RootSolveMethod method : order) {
753 const int remaining = opts_.max_iter - iterations_used;
754 if (remaining <= 0) {
755 break;
756 }
757
758 detail::maybe_log(opts_, "starting " + detail::method_name(method));
760 switch (method) {
762 stage = detail::solve_trust_region(residual_fn_, jacobian_fn_, current, opts_,
763 remaining);
764 break;
766 stage = detail::solve_line_search(residual_fn_, jacobian_fn_, current, opts_,
767 remaining);
768 break;
770 stage = detail::solve_broyden(residual_fn_, jacobian_fn_, current, opts_,
771 remaining);
772 break;
774 stage = detail::solve_pseudo_transient(residual_fn_, jacobian_fn_, current,
775 opts_, remaining);
776 break;
778 continue;
779 }
780
781 iterations_used += stage.iterations;
782 current = stage.state;
783 if (current.merit < best.merit) {
784 best = current;
785 }
786
787 last_method = method;
788 last_message = stage.message;
789 result.step_norm = stage.step_norm;
790 detail::maybe_log(opts_, detail::method_name(method) + ": residual inf-norm = " +
791 std::to_string(current.residual_norm) +
792 ", iterations = " + std::to_string(iterations_used));
793
794 if (stage.converged) {
795 result.x = current.x.template cast<Scalar>();
796 result.iterations = iterations_used;
797 result.converged = true;
798 result.method = method;
799 result.residual_norm = current.residual_norm;
800 result.message = stage.message;
801 return result;
802 }
803 }
804
805 result.x = best.x.template cast<Scalar>();
806 result.iterations = iterations_used;
807 result.converged = false;
808 result.method = last_method;
809 result.residual_norm = best.residual_norm;
810 result.message = "Failed to converge: " + last_message;
811 } else {
812 SymbolicScalar x0_mx = janus::to_mx(x0);
813
814 std::vector<SymbolicScalar> args;
815 args.push_back(x0_mx);
816 if (symbolic_solver_.n_in() > 1) {
817 args.push_back(SymbolicScalar(0, 0));
818 }
819 std::vector<SymbolicScalar> res = symbolic_solver_(args);
820
821 result.x = janus::to_eigen(res[0]);
822 result.converged = true;
823 result.message = "Symbolic graph generated with CasADi newton rootfinder";
824 }
825 return result;
826 }
827
828 private:
829 RootFinderOptions opts_;
830 int n_x_ = 0;
831 casadi::Function residual_fn_;
832 casadi::Function jacobian_fn_;
833 casadi::Function symbolic_solver_;
834};
835
848template <typename Scalar>
850 const Eigen::Matrix<Scalar, Eigen::Dynamic, 1> &x0,
851 const RootFinderOptions &opts = {}) {
852 NewtonSolver solver(F, opts);
853 return solver.solve(x0);
854}
855
871 const Eigen::VectorXd &x_guess,
872 const RootFinderOptions &opts = {},
873 const ImplicitFunctionOptions &implicit_opts = {}) {
874 detail::validate_root_options(opts, "create_implicit_function");
875
876 casadi::Function g_casadi = G.casadi_function();
877 detail::validate_implicit_problem(g_casadi, x_guess, implicit_opts);
878
879 casadi::Dict solver_opts = detail::opts_to_dict(opts);
880 solver_opts["implicit_input"] = implicit_opts.implicit_input_index;
881 solver_opts["implicit_output"] = implicit_opts.implicit_output_index;
882
883 casadi::Function solver = casadi::rootfinder(detail::implicit_function_name(g_casadi), "newton",
884 g_casadi, solver_opts);
885
886 std::vector<SymbolicArg> wrapper_inputs;
887 wrapper_inputs.reserve(static_cast<std::size_t>(g_casadi.n_in() - 1));
888
889 std::vector<casadi::MX> solver_args;
890 solver_args.reserve(static_cast<std::size_t>(g_casadi.n_in()));
891
892 const casadi::DM x0 = detail::vector_to_dm(x_guess);
893 for (int i = 0; i < g_casadi.n_in(); ++i) {
894 if (i == implicit_opts.implicit_input_index) {
895 solver_args.push_back(casadi::MX(x0));
896 continue;
897 }
898
899 casadi::MX arg =
900 janus::sym(g_casadi.name_in(i), g_casadi.size1_in(i), g_casadi.size2_in(i));
901 wrapper_inputs.push_back(SymbolicArg(arg));
902 solver_args.push_back(arg);
903 }
904
905 const auto solver_outputs = solver(solver_args);
906 const casadi::MX x_sol = solver_outputs.at(implicit_opts.implicit_output_index);
907
908 return janus::Function(detail::implicit_function_name(g_casadi), wrapper_inputs,
909 std::vector<SymbolicArg>{SymbolicArg(x_sol)});
910}
911
912} // namespace janus
Symbolic function wrapper around CasADi with Eigen-native IO.
C++20 concepts constraining valid Janus scalar types.
Custom exception hierarchy for Janus framework.
Wrapper around casadi::Function providing Eigen-native IO.
Definition Function.hpp:46
const casadi::Function & casadi_function() const
Access the underlying CasADi function.
Definition Function.hpp:186
Input validation failed (e.g., mismatched sizes, invalid parameters).
Definition JanusError.hpp:31
Base exception for all Janus errors.
Definition JanusError.hpp:19
NewtonSolver(const janus::Function &F, const RootFinderOptions &opts={})
Construct a new persistent nonlinear solver.
Definition RootFinding.hpp:685
RootResult< Scalar > solve(const Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > &x0) const
Solve F(x) = 0 from initial guess x0.
Definition RootFinding.hpp:712
Universal symbolic argument wrapper for Function inputs/outputs.
Definition JanusTypes.hpp:250
Smooth approximation of ReLU function: softplus(x) = (1/beta) * log(1 + exp(beta * x)).
Definition Diagnostics.hpp:131
StageOutcome solve_pseudo_transient(const casadi::Function &residual_fn, const casadi::Function &jacobian_fn, const NumericState &start, const RootFinderOptions &opts, int max_iterations)
Definition RootFinding.hpp:597
StageOutcome solve_broyden(const casadi::Function &residual_fn, const casadi::Function &jacobian_fn, const NumericState &start, const RootFinderOptions &opts, int max_iterations)
Definition RootFinding.hpp:503
Eigen::VectorXd solve_linear_system(const Eigen::MatrixXd &A, const Eigen::VectorXd &b)
Definition RootFinding.hpp:281
void validate_implicit_problem(const casadi::Function &g_casadi, const Eigen::VectorXd &x_guess, const ImplicitFunctionOptions &implicit_opts)
Definition RootFinding.hpp:238
void validate_root_options(const RootFinderOptions &opts, const std::string &context)
Definition RootFinding.hpp:135
StageOutcome solve_trust_region(const casadi::Function &residual_fn, const casadi::Function &jacobian_fn, const NumericState &start, const RootFinderOptions &opts, int max_iterations)
Definition RootFinding.hpp:349
NumericState evaluate_state(const casadi::Function &residual_fn, const casadi::Function &jacobian_fn, const Eigen::VectorXd &x, const std::string &context)
Definition RootFinding.hpp:316
RootSolveMethod strategy_to_method(RootSolveStrategy strategy)
Definition RootFinding.hpp:114
bool all_finite(const Eigen::VectorXd &x)
Definition RootFinding.hpp:285
void maybe_log(const RootFinderOptions &opts, const std::string &message)
Definition RootFinding.hpp:339
Eigen::VectorXd dm_to_vector(const casadi::DM &x)
Definition RootFinding.hpp:223
const char * method_name(SecondOrderIntegratorMethod method)
Definition Integrate.hpp:152
bool is_converged(const NumericState &state, const RootFinderOptions &opts)
Definition RootFinding.hpp:345
StageOutcome solve_line_search(const casadi::Function &residual_fn, const casadi::Function &jacobian_fn, const NumericState &start, const RootFinderOptions &opts, int max_iterations)
Definition RootFinding.hpp:431
casadi::Dict opts_to_dict(const RootFinderOptions &opts)
Definition RootFinding.hpp:198
std::string unique_name(const std::string &prefix)
Definition RootFinding.hpp:130
void validate_root_problem(const casadi::Function &f_casadi, const std::string &context)
Definition RootFinding.hpp:179
Eigen::VectorXd evaluate_residual_only(const casadi::Function &residual_fn, const Eigen::VectorXd &x)
Definition RootFinding.hpp:306
std::string implicit_function_name(const casadi::Function &g_casadi)
Definition RootFinding.hpp:234
casadi::DM vector_to_dm(const Eigen::VectorXd &x)
Definition RootFinding.hpp:215
Eigen::MatrixXd dm_to_matrix(const casadi::DM &x)
Definition RootFinding.hpp:229
Definition Diagnostics.hpp:19
auto gradient(const Eigen::MatrixBase< DerivedF > &f, const Spacing &dx=1.0, int edge_order=1, int n=1)
Computes gradient using second-order accurate central differences.
Definition Calculus.hpp:178
janus::Function create_implicit_function(const janus::Function &G, const Eigen::VectorXd &x_guess, const RootFinderOptions &opts={}, const ImplicitFunctionOptions &implicit_opts={})
Create a differentiable implicit solve wrapper for G(...) = 0.
Definition RootFinding.hpp:870
RootSolveMethod
Numeric nonlinear solver method actually used.
Definition RootFinding.hpp:38
@ LineSearchNewton
Exact-Jacobian Newton with backtracking line search.
Definition RootFinding.hpp:41
@ QuasiNewtonBroyden
Broyden updates after one exact Jacobian evaluation.
Definition RootFinding.hpp:42
@ None
No method selected yet.
Definition RootFinding.hpp:39
@ PseudoTransientContinuation
Backward-Euler pseudo-transient continuation.
Definition RootFinding.hpp:43
@ TrustRegionNewton
Levenberg-Marquardt style trust-region Newton.
Definition RootFinding.hpp:40
casadi::MX to_mx(const Eigen::MatrixBase< Derived > &e)
Convert Eigen matrix of MX (or numeric) to CasADi MX.
Definition JanusTypes.hpp:189
@ None
Definition AutoDiff.hpp:30
RootSolveStrategy
Numeric nonlinear solver strategy selection.
Definition RootFinding.hpp:27
@ Auto
Trust-region LM, then line-search Newton, Broyden, pseudo-transient.
Definition RootFinding.hpp:28
@ LineSearchNewton
Exact-Jacobian Newton with backtracking line search.
Definition RootFinding.hpp:30
@ QuasiNewtonBroyden
Broyden updates after one exact Jacobian evaluation.
Definition RootFinding.hpp:31
@ PseudoTransientContinuation
Backward-Euler pseudo-transient continuation.
Definition RootFinding.hpp:32
@ TrustRegionNewton
Levenberg-Marquardt style trust-region Newton.
Definition RootFinding.hpp:29
SymbolicScalar sym(const std::string &name)
Create a named symbolic scalar variable.
Definition JanusTypes.hpp:90
Eigen::Matrix< casadi::MX, Eigen::Dynamic, Eigen::Dynamic > to_eigen(const casadi::MX &m)
Convert CasADi MX to Eigen matrix of MX.
Definition JanusTypes.hpp:213
RootResult< Scalar > rootfinder(const janus::Function &F, const Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > &x0, const RootFinderOptions &opts={})
Solve F(x) = 0 for x given an initial guess.
Definition RootFinding.hpp:849
casadi::MX SymbolicScalar
CasADi MX symbolic scalar.
Definition JanusTypes.hpp:70
Options for building differentiable implicit solve wrappers.
Definition RootFinding.hpp:78
int implicit_output_index
Definition RootFinding.hpp:80
int implicit_input_index
Definition RootFinding.hpp:79
Options for root finding algorithms.
Definition RootFinding.hpp:54
int max_iter
Definition RootFinding.hpp:57
double pseudo_transient_dt_max
Definition RootFinding.hpp:72
double abstol
Definition RootFinding.hpp:55
double pseudo_transient_dt_growth
Definition RootFinding.hpp:71
double line_search_sufficient_decrease
Definition RootFinding.hpp:67
double trust_region_initial_damping
Definition RootFinding.hpp:63
double line_search_contraction
Definition RootFinding.hpp:66
RootSolveStrategy strategy
Definition RootFinding.hpp:62
double trust_region_damping_decrease
Definition RootFinding.hpp:65
bool line_search
Definition RootFinding.hpp:58
double trust_region_damping_increase
Definition RootFinding.hpp:64
int max_backtracking_steps
Definition RootFinding.hpp:68
casadi::Dict linear_solver_options
Definition RootFinding.hpp:61
int broyden_jacobian_refresh
Definition RootFinding.hpp:69
double pseudo_transient_dt0
Definition RootFinding.hpp:70
double abstolStep
Definition RootFinding.hpp:56
bool verbose
Definition RootFinding.hpp:59
std::string linear_solver
Definition RootFinding.hpp:60
Result of a root finding operation.
Definition RootFinding.hpp:86
double residual_norm
Definition RootFinding.hpp:91
bool converged
Definition RootFinding.hpp:89
double step_norm
Definition RootFinding.hpp:92
int iterations
Definition RootFinding.hpp:88
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > x
Definition RootFinding.hpp:87
std::string message
Definition RootFinding.hpp:93
RootSolveMethod method
Definition RootFinding.hpp:90
Definition RootFinding.hpp:289
Eigen::VectorXd residual
Definition RootFinding.hpp:291
double merit
Definition RootFinding.hpp:294
double residual_norm
Definition RootFinding.hpp:293
Eigen::MatrixXd jacobian
Definition RootFinding.hpp:292
Eigen::VectorXd x
Definition RootFinding.hpp:290
Definition RootFinding.hpp:297
int iterations
Definition RootFinding.hpp:300
RootSolveMethod method
Definition RootFinding.hpp:298
bool converged
Definition RootFinding.hpp:301
std::string message
Definition RootFinding.hpp:303
double step_norm
Definition RootFinding.hpp:302
NumericState state
Definition RootFinding.hpp:299