11#include <casadi/casadi.hpp>
87 Eigen::Matrix<Scalar, Eigen::Dynamic, 1>
x;
103 return "trust-region Newton";
105 return "line-search Newton";
107 return "quasi-Newton Broyden";
109 return "pseudo-transient continuation";
131 static std::atomic<std::uint64_t> counter{0};
132 return prefix +
"_" + std::to_string(counter.fetch_add(1));
146 throw InvalidArgument(context +
": trust_region_initial_damping must be positive");
149 throw InvalidArgument(context +
": trust_region_damping_increase must exceed 1");
152 throw InvalidArgument(context +
": trust_region_damping_decrease must lie in (0, 1)");
155 throw InvalidArgument(context +
": line_search_contraction must lie in (0, 1)");
159 throw InvalidArgument(context +
": line_search_sufficient_decrease must lie in (0, 1)");
162 throw InvalidArgument(context +
": max_backtracking_steps must be positive");
165 throw InvalidArgument(context +
": broyden_jacobian_refresh cannot be negative");
168 throw InvalidArgument(context +
": pseudo_transient_dt0 must be positive");
171 throw InvalidArgument(context +
": pseudo_transient_dt_growth must exceed 1");
175 ": pseudo_transient_dt_max must be at least pseudo_transient_dt0");
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");
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");
189 if (!output_sparsity.is_dense() || !output_sparsity.is_column()) {
190 throw InvalidArgument(context +
": output must be a dense column vector residual");
192 if (f_casadi.nnz_in(0) != f_casadi.nnz_out(0)) {
193 throw InvalidArgument(context +
": input and output dimensions must match");
200 d[
"abstol"] = opts.
abstol;
209 d[
"print_in"] =
true;
210 d[
"print_out"] =
true;
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);
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()));
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());
235 return g_casadi.name() +
"_implicit";
239 const Eigen::VectorXd &x_guess,
241 if (g_casadi.n_in() == 0 || g_casadi.n_out() == 0) {
243 "create_implicit_function: G must have at least one input and one output");
248 throw InvalidArgument(
"create_implicit_function: implicit_input_index out of range");
252 throw InvalidArgument(
"create_implicit_function: implicit_output_index out of range");
256 if (!unknown_sparsity.is_dense() || !unknown_sparsity.is_column()) {
258 "create_implicit_function: implicit input must be a dense column vector");
262 if (!residual_sparsity.is_dense() || !residual_sparsity.is_column()) {
264 "create_implicit_function: implicit output must be a dense column vector residual");
267 const auto n_unknown =
269 const auto n_residual =
271 if (n_unknown != n_residual) {
273 "create_implicit_function: implicit input and output dimensions must match");
275 if (x_guess.size() != n_unknown) {
276 throw InvalidArgument(
"create_implicit_function: x_guess size must match the implicit "
282 return A.colPivHouseholderQr().solve(b);
285inline bool all_finite(
const Eigen::VectorXd &x) {
return x.array().isFinite().all(); }
287inline bool all_finite(
const Eigen::MatrixXd &x) {
return x.array().isFinite().all(); }
294 double merit = std::numeric_limits<double>::infinity();
307 const Eigen::VectorXd &x) {
308 const std::vector<casadi::DM> residual_dm =
310 if (residual_dm.size() != 1u) {
311 throw JanusError(
"rootfinder: residual function must return exactly one output");
317 const casadi::Function &jacobian_fn,
const Eigen::VectorXd &x,
318 const std::string &context) {
323 const std::vector<casadi::DM> jacobian_dm =
325 if (jacobian_dm.size() != 1u) {
326 throw JanusError(context +
": Jacobian function must return exactly one output");
334 throw JanusError(context +
": residual/Jacobian evaluation produced non-finite values");
341 std::cout <<
"[rootfinder] " << message <<
"\n";
350 const casadi::Function &jacobian_fn,
352 int max_iterations) {
359 :
"no trust-region iterations remaining";
365 constexpr double min_lambda = 1e-12;
366 constexpr double max_lambda = 1e12;
368 for (
int iter = 0; iter < max_iterations; ++iter) {
370 Eigen::MatrixXd normal_matrix = current.
jacobian.transpose() * current.
jacobian;
371 normal_matrix.diagonal().array() += lambda;
375 out.
step_norm = step.lpNorm<Eigen::Infinity>();
378 out.
message =
"trust-region Newton produced a non-finite step";
382 out.
message =
"trust-region Newton stalled on a tiny step";
386 const double predicted = -
gradient.dot(step) - 0.5 * step.dot(normal_matrix * step);
387 if (!(predicted > 0.0) || !std::isfinite(predicted)) {
389 out.
message =
"trust-region Newton could not predict a decrease";
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;
399 if (actual > 0.0 && rho > 1e-4) {
400 current = std::move(candidate);
402 out.
message =
"trust-region Newton accepted a step";
406 }
else if (rho < 0.25) {
412 out.
message =
"converged with trust-region Newton";
417 out.
message =
"trust-region Newton rejected a step";
419 }
catch (
const std::exception &) {
421 out.
message =
"trust-region Newton rejected an invalid trial step";
426 out.
message =
"trust-region Newton exhausted its iteration budget";
432 const casadi::Function &jacobian_fn,
434 int max_iterations) {
441 :
"no line-search iterations remaining";
446 for (
int iter = 0; iter < max_iterations; ++iter) {
455 out.
message =
"line-search Newton produced a non-finite step";
458 const double full_step_norm = step.lpNorm<Eigen::Infinity>();
461 out.
message =
"line-search Newton stalled on a tiny step";
466 bool accepted =
false;
467 const double directional_derivative =
gradient.dot(step);
471 current.
x + alpha * step,
"rootfinder");
473 alpha * directional_derivative) {
474 current = std::move(candidate);
476 out.
step_norm = (alpha * step).lpNorm<Eigen::Infinity>();
480 }
catch (
const std::exception &) {
486 out.
message =
"line-search Newton could not find an acceptable step";
492 out.
message =
"converged with line-search Newton";
498 out.
message =
"line-search Newton exhausted its iteration budget";
504 const casadi::Function &jacobian_fn,
const NumericState &start,
512 :
"no Broyden iterations remaining";
516 Eigen::VectorXd x = start.
x;
517 Eigen::VectorXd residual = start.
residual;
518 double merit = start.
merit;
520 int accepted_steps = 0;
522 for (
int iter = 0; iter < max_iterations; ++iter) {
525 out.
message =
"Broyden update produced a non-finite step";
529 const double full_step_norm = step.lpNorm<Eigen::Infinity>();
533 out.
message =
"Broyden update stalled on a tiny step";
538 bool accepted =
false;
539 Eigen::VectorXd candidate_x;
540 Eigen::VectorXd candidate_residual;
542 candidate_x = x + alpha * step;
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>();
552 }
catch (
const std::exception &) {
558 out.
message =
"Broyden update could not decrease the residual merit";
562 const Eigen::VectorXd s = candidate_x - x;
563 const Eigen::VectorXd y = candidate_residual - residual;
564 const double denom = s.squaredNorm();
567 residual = candidate_residual;
570 if (residual.lpNorm<Eigen::Infinity>() <= opts.
abstol) {
573 out.
message =
"converged with Broyden updates";
582 }
else if (denom > std::numeric_limits<double>::epsilon()) {
583 B.noalias() += ((y - B * s) / denom) * s.transpose();
585 out.
message =
"Broyden update encountered a degenerate secant step";
592 out.
message =
"Broyden update exhausted its iteration budget";
598 const casadi::Function &jacobian_fn,
600 int max_iterations) {
607 :
"no pseudo-transient iterations remaining";
614 for (
int iter = 0; iter < max_iterations; ++iter) {
616 bool accepted =
false;
619 Eigen::MatrixXd shifted = current.
jacobian;
620 shifted.diagonal().array() += 1.0 / dt;
623 out.
step_norm = step.lpNorm<Eigen::Infinity>();
625 out.
message =
"pseudo-transient continuation produced a non-finite step";
629 out.
message =
"pseudo-transient continuation stalled on a tiny step";
635 evaluate_state(residual_fn, jacobian_fn, current.
x + step,
"rootfinder");
637 current = std::move(candidate);
644 }
catch (
const std::exception &) {
651 out.
message =
"pseudo-transient continuation could not decrease the residual merit";
657 out.
message =
"converged with pseudo-transient continuation";
663 out.
message =
"pseudo-transient continuation exhausted its iteration budget";
691 residual_fn_ = f_casadi;
692 n_x_ = f_casadi.nnz_in(0);
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);
697 std::vector<casadi::MX>{x},
698 std::vector<casadi::MX>{casadi::MX::jacobian(residual, x)});
703 }
catch (
const std::exception &e) {
704 throw JanusError(std::string(
"NewtonSolver creation failed: ") + e.what());
711 template <
typename Scalar>
714 const int n_x =
static_cast<int>(x0.size());
716 throw InvalidArgument(
"NewtonSolver::solve: x0 size must match the residual dimension");
719 if constexpr (std::is_floating_point_v<Scalar>) {
720 const Eigen::VectorXd x0_numeric = x0.template cast<double>();
722 "NewtonSolver::solve");
724 std::to_string(initial.residual_norm));
729 result.
x = initial.x.template cast<Scalar>();
733 result.
message =
"Initial guess satisfies the residual tolerance";
737 int iterations_used = 0;
739 std::string last_message =
"No numeric solver stage was executed";
740 std::vector<RootSolveMethod> order;
743 if (opts_.line_search) {
753 const int remaining = opts_.max_iter - iterations_used;
754 if (remaining <= 0) {
782 current = stage.
state;
787 last_method = method;
792 ", iterations = " + std::to_string(iterations_used));
795 result.
x = current.
x.template cast<Scalar>();
805 result.
x = best.
x.template cast<Scalar>();
808 result.
method = last_method;
810 result.
message =
"Failed to converge: " + last_message;
814 std::vector<SymbolicScalar> args;
815 args.push_back(x0_mx);
816 if (symbolic_solver_.n_in() > 1) {
819 std::vector<SymbolicScalar> res = symbolic_solver_(args);
823 result.
message =
"Symbolic graph generated with CasADi newton rootfinder";
831 casadi::Function residual_fn_;
832 casadi::Function jacobian_fn_;
833 casadi::Function symbolic_solver_;
848template <
typename Scalar>
850 const Eigen::Matrix<Scalar, Eigen::Dynamic, 1> &x0,
852 NewtonSolver solver(F, opts);
853 return solver.solve(x0);
871 const Eigen::VectorXd &x_guess,
873 const ImplicitFunctionOptions &implicit_opts = {}) {
880 solver_opts[
"implicit_input"] = implicit_opts.implicit_input_index;
881 solver_opts[
"implicit_output"] = implicit_opts.implicit_output_index;
884 g_casadi, solver_opts);
886 std::vector<SymbolicArg> wrapper_inputs;
887 wrapper_inputs.reserve(
static_cast<std::size_t
>(g_casadi.n_in() - 1));
889 std::vector<casadi::MX> solver_args;
890 solver_args.reserve(
static_cast<std::size_t
>(g_casadi.n_in()));
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));
900 janus::sym(g_casadi.name_in(i), g_casadi.size1_in(i), g_casadi.size2_in(i));
902 solver_args.push_back(arg);
905 const auto solver_outputs = solver(solver_args);
906 const casadi::MX x_sol = solver_outputs.at(implicit_opts.implicit_output_index);
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