Janus 2.0.0
High-performance C++20 dual-mode numerical framework
Loading...
Searching...
No Matches
Opti.hpp
Go to the documentation of this file.
1
5
6#pragma once
7
8#include "OptiOptions.hpp"
9#include "OptiSol.hpp"
10#include "OptiSweep.hpp"
11#include "Scaling.hpp"
16#include <algorithm>
17#include <casadi/casadi.hpp>
18#include <cmath>
19#include <limits>
20#include <map>
21#include <optional>
22#include <set>
23#include <string>
24#include <vector>
25
26namespace janus {
27
28namespace detail {
29
30inline double validate_positive_scale(double scale, const std::string &context) {
31 if (!(scale > 0.0) || !std::isfinite(scale)) {
32 throw InvalidArgument(context + ": scale must be positive and finite");
33 }
34 return scale;
35}
36
37inline double max_finite_abs(std::optional<double> lower_bound, std::optional<double> upper_bound) {
38 double bound_mag = 0.0;
39 if (lower_bound.has_value() && std::isfinite(lower_bound.value())) {
40 bound_mag = std::max(bound_mag, std::abs(lower_bound.value()));
41 }
42 if (upper_bound.has_value() && std::isfinite(upper_bound.value())) {
43 bound_mag = std::max(bound_mag, std::abs(upper_bound.value()));
44 }
45 return bound_mag;
46}
47
48inline double suggest_scalar_scale(double init_guess, std::optional<double> lower_bound,
49 std::optional<double> upper_bound) {
50 return std::max({std::abs(init_guess), max_finite_abs(lower_bound, upper_bound), 1.0});
51}
52
53inline double suggest_vector_scale(const NumericVector &init_guess,
54 std::optional<double> lower_bound,
55 std::optional<double> upper_bound) {
56 double init_mag = init_guess.size() > 0 ? init_guess.cwiseAbs().maxCoeff() : 0.0;
57 return std::max({init_mag, max_finite_abs(lower_bound, upper_bound), 1.0});
58}
59
60inline std::vector<double> dm_to_std_vector(const casadi::DM &value) {
61 return static_cast<std::vector<double>>(value);
62}
63
64inline double dm_to_scalar(const casadi::DM &value) {
65 std::vector<double> elements = dm_to_std_vector(value);
66 return elements.empty() ? 0.0 : elements.front();
67}
68
69inline std::vector<casadi::MX> current_assignments(const casadi::Opti &opti) {
70 std::vector<casadi::MX> assignments = opti.initial();
71 std::vector<casadi::MX> params = opti.value_parameters();
72 assignments.insert(assignments.end(), params.begin(), params.end());
73 return assignments;
74}
75
76inline double normalized_magnitude(double magnitude, double scale) {
77 return std::abs(magnitude) / validate_positive_scale(scale, "normalized_magnitude");
78}
79
80inline bool nearly_equal(double a, double b) {
81 double tol = 1e-12 * std::max({1.0, std::abs(a), std::abs(b)});
82 return std::abs(a - b) <= tol;
83}
84
85inline double constraint_violation(double value, double lower_bound, bool has_lower_bound,
86 double upper_bound, bool has_upper_bound) {
87 if (has_lower_bound && value < lower_bound) {
88 return lower_bound - value;
89 }
90 if (has_upper_bound && value > upper_bound) {
91 return value - upper_bound;
92 }
93 return 0.0;
94}
95
96inline double constraint_magnitude(double value, double lower_bound, bool has_lower_bound,
97 double upper_bound, bool has_upper_bound) {
98 double magnitude = std::abs(value);
99 if (has_lower_bound) {
100 magnitude = std::max(magnitude, std::abs(lower_bound));
101 }
102 if (has_upper_bound) {
103 magnitude = std::max(magnitude, std::abs(upper_bound));
104 }
105 return std::max(magnitude, 1.0);
106}
107
108inline ScalingIssueLevel issue_level(double normalized, const ScalingAnalysisOptions &opts) {
111}
112
113} // namespace detail
114
127 std::string category = "Uncategorized";
128 bool freeze = false;
129};
130
167class Opti {
168 public:
172 Opti() : opti_(), categories_to_freeze_(), categories_() {}
173
182 explicit Opti(const std::vector<std::string> &categories_to_freeze)
183 : opti_(), categories_to_freeze_(categories_to_freeze), categories_() {}
184
185 ~Opti() = default;
186
187 // =========================================================================
188 // Decision Variables
189 // =========================================================================
190
200 SymbolicScalar variable(double init_guess = 0.0, std::optional<double> scale = std::nullopt,
201 std::optional<double> lower_bound = std::nullopt,
202 std::optional<double> upper_bound = std::nullopt) {
203 return variable(init_guess, VariableOptions{}, scale, lower_bound, upper_bound);
204 }
205
216 SymbolicScalar variable(double init_guess, const VariableOptions &opts,
217 std::optional<double> scale = std::nullopt,
218 std::optional<double> lower_bound = std::nullopt,
219 std::optional<double> upper_bound = std::nullopt) {
220 // Check if variable should be frozen (explicit or via category)
221 bool should_freeze = opts.freeze || is_category_frozen(opts.category);
222
223 // Determine scale
224 double s = scale.has_value()
225 ? detail::validate_positive_scale(scale.value(), "Opti::variable")
226 : detail::suggest_scalar_scale(init_guess, lower_bound, upper_bound);
227
228 SymbolicScalar scaled_var;
229
230 if (should_freeze) {
231 // Create as parameter (not optimized)
232 SymbolicScalar param = opti_.parameter();
233 opti_.set_value(param, init_guess);
234 scaled_var = param;
235 } else {
236 // Create scaled variable
237 SymbolicScalar raw_var = opti_.variable();
238 scaled_var = s * raw_var;
239
240 // Set initial guess
241 opti_.set_initial(raw_var, init_guess / s);
242
243 // Apply bounds if specified
244 if (lower_bound.has_value()) {
245 opti_.subject_to(scaled_var >= lower_bound.value());
246 }
247 if (upper_bound.has_value()) {
248 opti_.subject_to(scaled_var <= upper_bound.value());
249 }
250
251 NumericVector init_vector(1);
252 init_vector(0) = init_guess;
253 register_variable_block(init_vector, opts.category, s, scale.has_value(), lower_bound,
254 upper_bound);
255 }
256
257 // Track in category
258 categories_[opts.category].push_back(scaled_var);
259
260 return scaled_var;
261 }
262
273 SymbolicVector variable(int n_vars, double init_guess = 0.0,
274 std::optional<double> scale = std::nullopt,
275 std::optional<double> lower_bound = std::nullopt,
276 std::optional<double> upper_bound = std::nullopt) {
277 // Determine scale
278 double s = scale.has_value()
279 ? detail::validate_positive_scale(scale.value(), "Opti::variable")
280 : detail::suggest_scalar_scale(init_guess, lower_bound, upper_bound);
281
282 // Create scaled variable
283 SymbolicScalar raw_var = opti_.variable(n_vars, 1);
284 SymbolicScalar scaled_var = s * raw_var;
285
286 // Set initial guess (constant vector)
287 opti_.set_initial(raw_var, init_guess / s);
288
289 // Apply bounds if specified
290 if (lower_bound.has_value()) {
291 opti_.subject_to(scaled_var >= lower_bound.value());
292 }
293 if (upper_bound.has_value()) {
294 opti_.subject_to(scaled_var <= upper_bound.value());
295 }
296
297 register_variable_block(NumericVector::Constant(n_vars, init_guess), "Uncategorized", s,
298 scale.has_value(), lower_bound, upper_bound);
299
300 return janus::to_eigen(scaled_var);
301 }
302
313 std::optional<double> scale = std::nullopt,
314 std::optional<double> lower_bound = std::nullopt,
315 std::optional<double> upper_bound = std::nullopt) {
316 int n_vars = static_cast<int>(init_guess.size());
317
318 // Determine scale from initial guess and finite bounds
319 double s = scale.has_value()
320 ? detail::validate_positive_scale(scale.value(), "Opti::variable")
321 : detail::suggest_vector_scale(init_guess, lower_bound, upper_bound);
322
323 // Create scaled variable
324 SymbolicScalar raw_var = opti_.variable(n_vars, 1);
325 SymbolicScalar scaled_var = s * raw_var;
326
327 // Set initial guess (convert Eigen to std::vector)
328 std::vector<double> init_vec(init_guess.data(), init_guess.data() + init_guess.size());
329 for (auto &v : init_vec) {
330 v /= s;
331 }
332 opti_.set_initial(raw_var, init_vec);
333
334 // Apply bounds if specified
335 if (lower_bound.has_value()) {
336 opti_.subject_to(scaled_var >= lower_bound.value());
337 }
338 if (upper_bound.has_value()) {
339 opti_.subject_to(scaled_var <= upper_bound.value());
340 }
341
342 register_variable_block(init_guess, "Uncategorized", s, scale.has_value(), lower_bound,
343 upper_bound);
344
345 return janus::to_eigen(scaled_var);
346 }
347
348 // =========================================================================
349 // Parameters (fixed values during optimization)
350 // =========================================================================
351
360 SymbolicScalar parameter(double value) {
361 SymbolicScalar param = opti_.parameter();
362 opti_.set_value(param, value);
363 return param;
364 }
365
373 int n = static_cast<int>(value.size());
374 SymbolicScalar param = opti_.parameter(n, 1);
375 std::vector<double> vals(value.data(), value.data() + value.size());
376 opti_.set_value(param, vals);
377 return janus::to_eigen(param);
378 }
379
380 // =========================================================================
381 // Constraints
382 // =========================================================================
383
396 void subject_to(const SymbolicScalar &constraint) { opti_.subject_to(constraint); }
397
407 void subject_to(const SymbolicScalar &constraint, double linear_scale) {
408 opti_.subject_to(constraint, casadi::DM(detail::validate_positive_scale(
409 linear_scale, "Opti::subject_to")));
410 }
411
417 void subject_to(const std::vector<SymbolicScalar> &constraints) {
418 for (const auto &c : constraints) {
419 opti_.subject_to(c);
420 }
421 }
422
428 void subject_to(const std::vector<SymbolicScalar> &constraints, double linear_scale) {
429 double s = detail::validate_positive_scale(linear_scale, "Opti::subject_to");
430 for (const auto &c : constraints) {
431 opti_.subject_to(c, casadi::DM(s));
432 }
433 }
434
447 void subject_to(std::initializer_list<SymbolicScalar> constraints) {
448 for (const auto &c : constraints) {
449 opti_.subject_to(c);
450 }
451 }
452
458 void subject_to(std::initializer_list<SymbolicScalar> constraints, double linear_scale) {
459 double s = detail::validate_positive_scale(linear_scale, "Opti::subject_to");
460 for (const auto &c : constraints) {
461 opti_.subject_to(c, casadi::DM(s));
462 }
463 }
464
465 // ---- Scalar Constraint Helpers ----
466
472 void subject_to_lower(const SymbolicScalar &scalar, double lower_bound) {
473 opti_.subject_to(scalar >= lower_bound);
474 }
475
481 void subject_to_upper(const SymbolicScalar &scalar, double upper_bound) {
482 opti_.subject_to(scalar <= upper_bound);
483 }
484
491 void subject_to_bounds(const SymbolicScalar &scalar, double lower_bound, double upper_bound) {
492 subject_to_lower(scalar, lower_bound);
493 subject_to_upper(scalar, upper_bound);
494 }
495
496 // ---- Vector Constraint Helpers ----
497
510 void subject_to_lower(const SymbolicVector &vec, double lower_bound) {
511 for (Eigen::Index i = 0; i < vec.size(); ++i) {
512 opti_.subject_to(vec(i) >= lower_bound);
513 }
514 }
515
522 void subject_to_upper(const SymbolicVector &vec, double upper_bound) {
523 for (Eigen::Index i = 0; i < vec.size(); ++i) {
524 opti_.subject_to(vec(i) <= upper_bound);
525 }
526 }
527
541 void subject_to_bounds(const SymbolicVector &vec, double lower_bound, double upper_bound) {
542 subject_to_lower(vec, lower_bound);
543 subject_to_upper(vec, upper_bound);
544 }
545
546 // =========================================================================
547 // Objective
548 // =========================================================================
549
555 void minimize(const SymbolicScalar &objective) { set_objective(objective, 1.0, false, false); }
556
565 void minimize(const SymbolicScalar &objective, double objective_scale) {
566 set_objective(objective, detail::validate_positive_scale(objective_scale, "Opti::minimize"),
567 true, false);
568 }
569
575 void maximize(const SymbolicScalar &objective) { set_objective(objective, 1.0, false, true); }
576
585 void maximize(const SymbolicScalar &objective, double objective_scale) {
586 set_objective(objective, detail::validate_positive_scale(objective_scale, "Opti::maximize"),
587 true, true);
588 }
589
590 // =========================================================================
591 // Solve
592 // =========================================================================
593
603 OptiSol solve(const OptiOptions &options = {}) {
604 // Verify solver availability
605 if (!solver_available(options.solver)) {
606 throw RuntimeError(std::string("Solver '") + solver_name(options.solver) +
607 "' is not available in this CasADi build");
608 }
609
610 casadi::Dict solver_opts;
611
612 // Configure solver-specific options
613 switch (options.solver) {
614 case Solver::Snopt:
615 configure_snopt_opts(solver_opts, options);
616 break;
617 case Solver::QpOases:
618 configure_qpoases_opts(solver_opts, options);
619 break;
620 case Solver::Ipopt:
621 default:
622 configure_ipopt_opts(solver_opts, options);
623 break;
624 }
625
626 // Common options
627 if (options.jit) {
628 solver_opts["jit"] = true;
629 solver_opts["jit_options"] = casadi::Dict{{"flags", std::vector<std::string>{"-O3"}}};
630 }
631
632 // Set solver and solve
633 opti_.solver(solver_name(options.solver), solver_opts);
634 return OptiSol(opti_.solve());
635 }
636
662 SweepResult solve_sweep(const SymbolicScalar &param, const std::vector<double> &values,
663 const OptiOptions &options = {}) {
664 // Verify solver availability
665 if (!solver_available(options.solver)) {
666 throw RuntimeError(std::string("Solver '") + solver_name(options.solver) +
667 "' is not available in this CasADi build");
668 }
669
670 SweepResult result;
671 result.param_values = values;
672 result.all_converged = true;
673
674 // Configure solver once
675 casadi::Dict solver_opts;
676
677 switch (options.solver) {
678 case Solver::Snopt:
679 configure_snopt_opts(solver_opts, options);
680 break;
681 case Solver::QpOases:
682 configure_qpoases_opts(solver_opts, options);
683 break;
684 case Solver::Ipopt:
685 default:
686 configure_ipopt_opts(solver_opts, options);
687 // Enable warm-start for parameter sweeps
688 solver_opts["ipopt.warm_start_init_point"] = "yes";
689 break;
690 }
691
692 opti_.solver(solver_name(options.solver), solver_opts);
693
694 for (size_t idx = 0; idx < values.size(); ++idx) {
695 double val = values[idx];
696 opti_.set_value(param, val);
697
698 try {
699 auto sol = opti_.solve();
700 result.iterations.push_back(sol.stats().count("iter_count")
701 ? static_cast<int>(sol.stats().at("iter_count"))
702 : -1);
703 result.solutions.emplace_back(sol);
704 result.converged.push_back(true);
705 result.errors.emplace_back();
706 } catch (const std::exception &e) {
707 result.all_converged = false;
708 result.solutions.emplace_back(std::nullopt);
709 result.converged.push_back(false);
710 result.errors.push_back(e.what());
711 result.iterations.push_back(-1);
712 }
713 }
714
715 return result;
716 }
717 // =========================================================================
718 // Derivative Helpers (for trajectory optimization)
719 // =========================================================================
720
742 SymbolicVector derivative_of(const SymbolicVector &var, const NumericVector &with_respect_to,
743 double derivative_init_guess,
744 const std::string &method = "trapezoidal") {
745 int n = static_cast<int>(var.size());
746 if (n != static_cast<int>(with_respect_to.size())) {
747 throw InvalidArgument(
748 "derivative_of: variable and with_respect_to must have same size");
749 }
750
751 // Create derivative variable
752 auto deriv = variable(n, derivative_init_guess);
753
754 // Add integration constraints
755 constrain_derivative(deriv, var, with_respect_to, method);
756
757 return deriv;
758 }
759
770 void constrain_derivative(const SymbolicVector &derivative, const SymbolicVector &var,
771 const NumericVector &with_respect_to,
772 const std::string &method = "trapezoidal") {
773 constrain_derivative(derivative, var, with_respect_to, parse_integration_method(method));
774 }
775
779 void constrain_derivative(const SymbolicVector &derivative, const SymbolicVector &var,
780 const NumericVector &with_respect_to, IntegrationMethod method) {
781 int n = static_cast<int>(var.size());
782
783 // Use janus::diff from Calculus.hpp
784 NumericVector dt = janus::diff(with_respect_to);
785
786 switch (method) {
789 // Second-order: x[i+1] - x[i] = 0.5 * (xdot[i] + xdot[i+1]) * dt[i]
790 for (int i = 0; i < n - 1; ++i) {
791 SymbolicScalar lhs = var(i + 1) - var(i);
792 SymbolicScalar rhs = 0.5 * (derivative(i) + derivative(i + 1)) * dt(i);
793 opti_.subject_to(lhs == rhs);
794 }
795 break;
796
798 // First-order forward: x[i+1] - x[i] = xdot[i] * dt[i]
799 for (int i = 0; i < n - 1; ++i) {
800 SymbolicScalar lhs = var(i + 1) - var(i);
801 SymbolicScalar rhs = derivative(i) * dt(i);
802 opti_.subject_to(lhs == rhs);
803 }
804 break;
805
807 // First-order backward: x[i+1] - x[i] = xdot[i+1] * dt[i]
808 for (int i = 0; i < n - 1; ++i) {
809 SymbolicScalar lhs = var(i + 1) - var(i);
810 SymbolicScalar rhs = derivative(i + 1) * dt(i);
811 opti_.subject_to(lhs == rhs);
812 }
813 break;
814 }
815 }
816
817 // =========================================================================
818 // Access
819 // =========================================================================
820
828 ScalingReport report;
829 report.summary.variable_blocks = static_cast<int>(variable_records_.size());
830 report.summary.scalar_constraints = static_cast<int>(opti_.ng());
831
832 if (!variable_records_.empty()) {
833 report.summary.min_variable_scale = variable_records_.front().scale;
834 report.summary.max_variable_scale = variable_records_.front().scale;
835 }
836
837 for (size_t i = 0; i < variable_records_.size(); ++i) {
838 const auto &record = variable_records_[i];
839 VariableScalingInfo info;
840 info.block_index = static_cast<int>(i);
841 info.size = record.size;
842 info.category = record.category;
843 info.frozen = false;
844 info.user_supplied_scale = record.user_supplied_scale;
845 info.scale = record.scale;
846 info.init_abs_mean = record.init_abs_mean;
847 info.init_abs_max = record.init_abs_max;
848 info.normalized_init_abs_mean = record.init_abs_mean / record.scale;
849 info.normalized_init_abs_max = record.init_abs_max / record.scale;
850 info.lower_bound = record.lower_bound;
851 info.upper_bound = record.upper_bound;
852 info.suggested_scale = record.suggested_scale;
853 report.variables.push_back(info);
854
856 std::min(report.summary.min_variable_scale, record.scale);
858 std::max(report.summary.max_variable_scale, record.scale);
859
860 if (record.init_abs_max > 0.0 &&
861 info.normalized_init_abs_max < opts.normalized_low_warn) {
862 report.issues.push_back(
864 "variable[" + std::to_string(i) + "]",
865 "initial guess is much smaller than the applied variable scale",
866 record.init_abs_max, record.scale, info.normalized_init_abs_max,
867 record.suggested_scale});
868 } else if (info.normalized_init_abs_max > opts.normalized_high_warn) {
869 report.issues.push_back(
870 {detail::issue_level(info.normalized_init_abs_max, opts),
871 ScalingIssueKind::Variable, static_cast<int>(i),
872 "variable[" + std::to_string(i) + "]",
873 "initial guess is much larger than the applied variable scale",
874 record.init_abs_max, record.scale, info.normalized_init_abs_max,
875 record.suggested_scale});
876 }
877 }
878
879 if (!variable_records_.empty()) {
882 if (report.summary.variable_scale_ratio > opts.variable_scale_ratio_warn) {
883 report.issues.push_back(
885 "decision-variable scales span many orders of magnitude",
888 }
889 }
890
891 std::vector<casadi::MX> assignments = detail::current_assignments(opti_);
892
893 if (objective_expr_.has_value()) {
894 report.objective.configured = true;
895 report.objective.maximize = objective_is_maximize_;
896 report.objective.user_supplied_scale = objective_scale_user_supplied_;
897 report.objective.scale = objective_scale_;
899 detail::dm_to_scalar(opti_.value(objective_expr_.value(), assignments));
901 std::abs(report.objective.value_at_initial) / objective_scale_;
903 std::max(std::abs(report.objective.value_at_initial), 1.0);
904
905 if (report.objective.normalized_value > opts.normalized_high_warn) {
906 report.issues.push_back(
908 ScalingIssueKind::Objective, 0, "objective",
909 "objective magnitude is large relative to the applied objective scale",
910 std::abs(report.objective.value_at_initial), objective_scale_,
912 }
913 }
914
915 if (opti_.ng() > 0) {
916 std::vector<double> g_values =
917 detail::dm_to_std_vector(opti_.value(opti_.g(), assignments));
918 std::vector<double> lbg_values =
919 detail::dm_to_std_vector(opti_.value(opti_.lbg(), assignments));
920 std::vector<double> ubg_values =
921 detail::dm_to_std_vector(opti_.value(opti_.ubg(), assignments));
922 std::vector<double> g_scales = detail::dm_to_std_vector(opti_.g_linear_scale());
923
924 for (int i = 0; i < static_cast<int>(g_values.size()); ++i) {
925 ConstraintScalingInfo info;
926 info.row = i;
927 info.value_at_initial = g_values.at(i);
928 info.scale = i < static_cast<int>(g_scales.size()) ? g_scales.at(i) : 1.0;
929 info.lower_bound = i < static_cast<int>(lbg_values.size()) ? lbg_values.at(i) : 0.0;
930 info.upper_bound = i < static_cast<int>(ubg_values.size()) ? ubg_values.at(i) : 0.0;
931 info.has_lower_bound =
932 i < static_cast<int>(lbg_values.size()) && std::isfinite(info.lower_bound);
933 info.has_upper_bound =
934 i < static_cast<int>(ubg_values.size()) && std::isfinite(info.upper_bound);
935 info.equality = info.has_lower_bound && info.has_upper_bound &&
936 detail::nearly_equal(info.lower_bound, info.upper_bound);
937 info.normalized_magnitude =
938 detail::constraint_magnitude(info.value_at_initial, info.lower_bound,
939 info.has_lower_bound, info.upper_bound,
940 info.has_upper_bound) /
941 info.scale;
942 info.normalized_violation =
943 detail::constraint_violation(info.value_at_initial, info.lower_bound,
944 info.has_lower_bound, info.upper_bound,
945 info.has_upper_bound) /
946 info.scale;
947 info.suggested_scale = detail::constraint_magnitude(
948 info.value_at_initial, info.lower_bound, info.has_lower_bound, info.upper_bound,
949 info.has_upper_bound);
950 report.constraints.push_back(info);
951
952 if (info.normalized_magnitude > opts.normalized_high_warn) {
953 report.issues.push_back(
954 {detail::issue_level(info.normalized_magnitude, opts),
955 ScalingIssueKind::Constraint, i, "constraint[" + std::to_string(i) + "]",
956 "constraint magnitude or bound magnitude is large relative to its scale",
957 info.suggested_scale, info.scale, info.normalized_magnitude,
958 info.suggested_scale});
959 }
960 }
961 }
962
963 if (opti_.nx() > 0 && opti_.ng() > 0) {
964 casadi::Sparsity jac_sp = casadi::MX::jacobian_sparsity(opti_.g(), opti_.x());
965 report.summary.jacobian_nnz = jac_sp.nnz();
967 static_cast<double>(jac_sp.nnz()) /
968 static_cast<double>(std::max<casadi_int>(1, opti_.nx() * opti_.ng()));
969 }
970
971 return report;
972 }
973
981 const casadi::Opti &casadi_opti() const { return opti_; }
982
992 void set_initial(const casadi::MX &x, const casadi::DM &v) { opti_.set_initial(x, v); }
993
994 // =========================================================================
995 // Category & Freezing
996 // =========================================================================
997
1004 std::vector<SymbolicScalar> get_category(const std::string &category) const {
1005 auto it = categories_.find(category);
1006 if (it != categories_.end()) {
1007 return it->second;
1008 }
1009 return {};
1010 }
1011
1016 std::vector<std::string> get_category_names() const {
1017 std::vector<std::string> names;
1018 names.reserve(categories_.size());
1019 for (const auto &[name, _] : categories_) {
1020 names.push_back(name);
1021 }
1022 return names;
1023 }
1024
1030 bool is_category_frozen(const std::string &category) const {
1031 return std::find(categories_to_freeze_.begin(), categories_to_freeze_.end(), category) !=
1032 categories_to_freeze_.end();
1033 }
1034
1035 private:
1036 struct VariableRecord {
1037 int size = 0;
1038 std::string category = "Uncategorized";
1039 bool user_supplied_scale = false;
1040 double scale = 1.0;
1041 double init_abs_mean = 0.0;
1042 double init_abs_max = 0.0;
1043 std::optional<double> lower_bound;
1044 std::optional<double> upper_bound;
1045 double suggested_scale = 1.0;
1046 };
1047
1048 casadi::Opti opti_;
1049 std::vector<std::string> categories_to_freeze_;
1050 std::map<std::string, std::vector<SymbolicScalar>> categories_;
1051 std::vector<VariableRecord> variable_records_;
1052 std::optional<SymbolicScalar> objective_expr_;
1053 double objective_scale_ = 1.0;
1054 bool objective_scale_user_supplied_ = false;
1055 bool objective_is_maximize_ = false;
1056
1057 // =========================================================================
1058 // Solver Configuration Helpers
1059 // =========================================================================
1060
1061 void configure_ipopt_opts(casadi::Dict &solver_opts, const OptiOptions &options) {
1062 solver_opts["ipopt.max_iter"] = options.max_iter;
1063 solver_opts["ipopt.max_cpu_time"] = options.max_cpu_time;
1064 solver_opts["ipopt.tol"] = options.tol;
1065 solver_opts["ipopt.mu_strategy"] = options.mu_strategy;
1066 solver_opts["ipopt.sb"] = "yes"; // Suppress banner
1067
1068 if (options.verbose) {
1069 solver_opts["ipopt.print_level"] = 5;
1070 } else {
1071 solver_opts["print_time"] = false;
1072 solver_opts["ipopt.print_level"] = 0;
1073 }
1074
1075 if (options.detect_simple_bounds) {
1076 solver_opts["detect_simple_bounds"] = true;
1077 }
1078 }
1079
1080 void configure_snopt_opts(casadi::Dict &solver_opts, const OptiOptions &options) {
1081 const auto &snopt = options.snopt_opts;
1082
1083 // SNOPT option names in CasADi use underscores
1084 solver_opts["snopt.Major_iterations_limit"] = snopt.major_iterations_limit;
1085 solver_opts["snopt.Minor_iterations_limit"] = snopt.minor_iterations_limit;
1086 solver_opts["snopt.Major_optimality_tolerance"] = snopt.major_optimality_tolerance;
1087 solver_opts["snopt.Major_feasibility_tolerance"] = snopt.major_feasibility_tolerance;
1088
1089 if (options.verbose) {
1090 solver_opts["snopt.Major_print_level"] = 1;
1091 solver_opts["snopt.Minor_print_level"] = 1;
1092 } else {
1093 solver_opts["print_time"] = false;
1094 solver_opts["snopt.Major_print_level"] = snopt.print_level;
1095 solver_opts["snopt.Minor_print_level"] = 0;
1096 }
1097 }
1098
1099 void configure_qpoases_opts(casadi::Dict &solver_opts, const OptiOptions &options) {
1100 solver_opts["qpoases.nWSR"] = options.max_iter;
1101 solver_opts["qpoases.CPUtime"] = options.max_cpu_time;
1102
1103 if (!options.verbose) {
1104 solver_opts["print_time"] = false;
1105 solver_opts["qpoases.printLevel"] = "none";
1106 }
1107 }
1108
1109 void register_variable_block(const NumericVector &init_guess, const std::string &category,
1110 double scale, bool user_supplied_scale,
1111 std::optional<double> lower_bound,
1112 std::optional<double> upper_bound) {
1113 VariableRecord record;
1114 record.size = static_cast<int>(init_guess.size());
1115 record.category = category;
1116 record.user_supplied_scale = user_supplied_scale;
1117 record.scale = detail::validate_positive_scale(scale, "Opti::variable");
1118 record.init_abs_mean = init_guess.size() > 0 ? init_guess.cwiseAbs().mean() : 0.0;
1119 record.init_abs_max = init_guess.size() > 0 ? init_guess.cwiseAbs().maxCoeff() : 0.0;
1120 record.lower_bound = lower_bound;
1121 record.upper_bound = upper_bound;
1122 record.suggested_scale =
1123 init_guess.size() == 1
1124 ? detail::suggest_scalar_scale(init_guess(0), lower_bound, upper_bound)
1125 : detail::suggest_vector_scale(init_guess, lower_bound, upper_bound);
1126 variable_records_.push_back(record);
1127 }
1128
1129 void set_objective(const SymbolicScalar &objective, double scale, bool user_supplied_scale,
1130 bool maximize) {
1131 objective_expr_ = objective;
1132 objective_scale_ = detail::validate_positive_scale(scale, "Opti objective");
1133 objective_scale_user_supplied_ = user_supplied_scale;
1134 objective_is_maximize_ = maximize;
1135 opti_.minimize((maximize ? -objective : objective) / objective_scale_);
1136 }
1137};
1138
1139} // namespace janus
Numerical differentiation and integration (gradient, trapz, cumtrapz, diff).
Finite difference weights, derivative approximations, and integration defects.
Custom exception hierarchy for Janus framework.
Core type aliases for numeric and symbolic Eigen/CasADi interop.
Solver selection and configuration for Opti.
Solution wrapper for extracting optimized values.
Parametric sweep results for optimization problems.
Scaling diagnostics and metadata for optimization problems.
Input validation failed (e.g., mismatched sizes, invalid parameters).
Definition JanusError.hpp:31
Solution wrapper for optimization results.
Definition OptiSol.hpp:32
void constrain_derivative(const SymbolicVector &derivative, const SymbolicVector &var, const NumericVector &with_respect_to, IntegrationMethod method)
Constrain an existing variable to be a derivative (enum version).
Definition Opti.hpp:779
SymbolicVector parameter(const NumericVector &value)
Create a vector parameter.
Definition Opti.hpp:372
SymbolicScalar variable(double init_guess, const VariableOptions &opts, std::optional< double > scale=std::nullopt, std::optional< double > lower_bound=std::nullopt, std::optional< double > upper_bound=std::nullopt)
Create a scalar decision variable with options.
Definition Opti.hpp:216
void subject_to(const std::vector< SymbolicScalar > &constraints)
Add multiple constraints.
Definition Opti.hpp:417
Opti(const std::vector< std::string > &categories_to_freeze)
Construct with frozen categories.
Definition Opti.hpp:182
void subject_to(const std::vector< SymbolicScalar > &constraints, double linear_scale)
Add multiple constraints with one shared linear scale.
Definition Opti.hpp:428
~Opti()=default
void minimize(const SymbolicScalar &objective, double objective_scale)
Set objective to minimize with explicit objective scaling.
Definition Opti.hpp:565
void subject_to_upper(const SymbolicVector &vec, double upper_bound)
Apply upper bound to all elements of a vector.
Definition Opti.hpp:522
SymbolicVector variable(int n_vars, double init_guess=0.0, std::optional< double > scale=std::nullopt, std::optional< double > lower_bound=std::nullopt, std::optional< double > upper_bound=std::nullopt)
Create a vector of decision variables with scalar init guess.
Definition Opti.hpp:273
std::vector< SymbolicScalar > get_category(const std::string &category) const
Get all variables in a category.
Definition Opti.hpp:1004
void minimize(const SymbolicScalar &objective)
Set objective to minimize.
Definition Opti.hpp:555
void maximize(const SymbolicScalar &objective, double objective_scale)
Set objective to maximize with explicit objective scaling.
Definition Opti.hpp:585
void subject_to_bounds(const SymbolicScalar &scalar, double lower_bound, double upper_bound)
Apply both lower and upper bounds to a scalar variable.
Definition Opti.hpp:491
OptiSol solve(const OptiOptions &options={})
Solve the optimization problem.
Definition Opti.hpp:603
Opti()
Construct a new optimization environment.
Definition Opti.hpp:172
const casadi::Opti & casadi_opti() const
Access the underlying CasADi Opti object.
Definition Opti.hpp:981
void subject_to_upper(const SymbolicScalar &scalar, double upper_bound)
Apply upper bound to a scalar variable.
Definition Opti.hpp:481
std::vector< std::string > get_category_names() const
Get all registered category names.
Definition Opti.hpp:1016
void subject_to_bounds(const SymbolicVector &vec, double lower_bound, double upper_bound)
Apply both lower and upper bounds to all elements.
Definition Opti.hpp:541
void maximize(const SymbolicScalar &objective)
Set objective to maximize.
Definition Opti.hpp:575
void subject_to(std::initializer_list< SymbolicScalar > constraints, double linear_scale)
Add an initializer-list of constraints with one shared linear scale.
Definition Opti.hpp:458
SymbolicScalar variable(double init_guess=0.0, std::optional< double > scale=std::nullopt, std::optional< double > lower_bound=std::nullopt, std::optional< double > upper_bound=std::nullopt)
Create a scalar decision variable.
Definition Opti.hpp:200
void subject_to(const SymbolicScalar &constraint, double linear_scale)
Add a scaled scalar constraint.
Definition Opti.hpp:407
void subject_to(std::initializer_list< SymbolicScalar > constraints)
Add constraints from initializer list.
Definition Opti.hpp:447
SymbolicVector derivative_of(const SymbolicVector &var, const NumericVector &with_respect_to, double derivative_init_guess, const std::string &method="trapezoidal")
Create a derivative variable constrained by integration.
Definition Opti.hpp:742
void constrain_derivative(const SymbolicVector &derivative, const SymbolicVector &var, const NumericVector &with_respect_to, const std::string &method="trapezoidal")
Constrain an existing variable to be a derivative.
Definition Opti.hpp:770
void subject_to_lower(const SymbolicVector &vec, double lower_bound)
Apply lower bound to all elements of a vector.
Definition Opti.hpp:510
bool is_category_frozen(const std::string &category) const
Check if a category is marked for freezing.
Definition Opti.hpp:1030
void set_initial(const casadi::MX &x, const casadi::DM &v)
Set initial guess for a symbolic expression.
Definition Opti.hpp:992
void subject_to_lower(const SymbolicScalar &scalar, double lower_bound)
Apply lower bound to a scalar variable.
Definition Opti.hpp:472
SymbolicVector variable(const NumericVector &init_guess, std::optional< double > scale=std::nullopt, std::optional< double > lower_bound=std::nullopt, std::optional< double > upper_bound=std::nullopt)
Create a vector of decision variables with per-element init guess.
Definition Opti.hpp:312
void subject_to(const SymbolicScalar &constraint)
Add a scalar constraint.
Definition Opti.hpp:396
SweepResult solve_sweep(const SymbolicScalar &param, const std::vector< double > &values, const OptiOptions &options={})
Perform parametric sweep over a parameter.
Definition Opti.hpp:662
SymbolicScalar parameter(double value)
Create a scalar parameter.
Definition Opti.hpp:360
ScalingReport analyze_scaling(const ScalingAnalysisOptions &opts={}) const
Diagnose variable, objective, and constraint scaling at the current initial point.
Definition Opti.hpp:827
Operation failed at runtime (e.g., CasADi eval with free variables).
Definition JanusError.hpp:41
Smooth approximation of ReLU function: softplus(x) = (1/beta) * log(1 + exp(beta * x)).
Definition Diagnostics.hpp:131
double validate_positive_scale(double scale, const std::string &context)
Definition Opti.hpp:30
bool nearly_equal(double a, double b)
Definition Opti.hpp:80
std::vector< casadi::MX > current_assignments(const casadi::Opti &opti)
Definition Opti.hpp:69
double dm_to_scalar(const casadi::DM &value)
Definition Opti.hpp:64
double max_finite_abs(std::optional< double > lower_bound, std::optional< double > upper_bound)
Definition Opti.hpp:37
ScalingIssueLevel issue_level(double normalized, const ScalingAnalysisOptions &opts)
Definition Opti.hpp:108
double constraint_magnitude(double value, double lower_bound, bool has_lower_bound, double upper_bound, bool has_upper_bound)
Definition Opti.hpp:96
double normalized_magnitude(double magnitude, double scale)
Definition Opti.hpp:76
double suggest_vector_scale(const NumericVector &init_guess, std::optional< double > lower_bound, std::optional< double > upper_bound)
Definition Opti.hpp:53
double constraint_violation(double value, double lower_bound, bool has_lower_bound, double upper_bound, bool has_upper_bound)
Definition Opti.hpp:85
std::vector< double > dm_to_std_vector(const casadi::DM &value)
Definition Opti.hpp:60
double suggest_scalar_scale(double init_guess, std::optional< double > lower_bound, std::optional< double > upper_bound)
Definition Opti.hpp:48
Definition Diagnostics.hpp:19
JanusVector< SymbolicScalar > SymbolicVector
Eigen vector of MX elements.
Definition JanusTypes.hpp:72
@ Ipopt
Interior Point OPTimizer (default, always available).
Definition OptiOptions.hpp:22
@ Snopt
Sparse Nonlinear OPTimizer (requires license).
Definition OptiOptions.hpp:23
@ QpOases
QP solver for QP subproblems.
Definition OptiOptions.hpp:24
JanusVector< NumericScalar > NumericVector
Eigen::VectorXd equivalent.
Definition JanusTypes.hpp:67
bool solver_available(Solver solver)
Check if a solver is available in the current CasADi build.
Definition OptiOptions.hpp:58
IntegrationMethod
Integration/differentiation method for trajectory optimization.
Definition FiniteDifference.hpp:24
@ BackwardEuler
First-order backward difference: x[i+1] - x[i] = xdot[i+1] * dt.
Definition FiniteDifference.hpp:26
@ ForwardEuler
First-order forward difference: x[i+1] - x[i] = xdot[i] * dt.
Definition FiniteDifference.hpp:25
@ Midpoint
Alias for Trapezoidal (same formula).
Definition FiniteDifference.hpp:28
@ Trapezoidal
Second-order trapezoidal: x[i+1] - x[i] = 0.5 * (xdot[i] + xdot[i+1]) * dt.
Definition FiniteDifference.hpp:27
auto diff(const Eigen::MatrixBase< Derived > &v)
Computes adjacent differences of a vector Returns a vector of size N-1 where out[i] = v[i+1] - v[i].
Definition Calculus.hpp:27
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
const char * solver_name(Solver solver)
Get the CasADi solver name string.
Definition OptiOptions.hpp:38
ScalingIssueLevel
Severity used by Opti scaling diagnostics.
Definition Scaling.hpp:20
@ Warning
Potential scaling concern.
Definition Scaling.hpp:21
@ Critical
Severe scaling issue likely to cause solver failure.
Definition Scaling.hpp:22
IntegrationMethod parse_integration_method(const std::string &method)
Parse integration method from string.
Definition FiniteDifference.hpp:47
@ Summary
Aggregate issue across the problem.
Definition Scaling.hpp:32
@ Objective
Issue with objective scaling.
Definition Scaling.hpp:31
@ Variable
Issue with variable scaling.
Definition Scaling.hpp:29
@ Constraint
Issue with constraint scaling.
Definition Scaling.hpp:30
casadi::MX SymbolicScalar
CasADi MX symbolic scalar.
Definition JanusTypes.hpp:70
double scale
Definition Scaling.hpp:106
double normalized_value
Definition Scaling.hpp:107
double suggested_scale
Definition Scaling.hpp:108
bool maximize
Definition Scaling.hpp:103
double value_at_initial
Definition Scaling.hpp:105
bool configured
Definition Scaling.hpp:102
bool user_supplied_scale
Definition Scaling.hpp:104
Options for solving optimization problems.
Definition OptiOptions.hpp:126
Thresholds controlling Opti scaling diagnostics.
Definition Scaling.hpp:38
double normalized_high_critical
Escalate to critical beyond this.
Definition Scaling.hpp:41
Aggregate result returned by Opti::analyze_scaling().
Definition Scaling.hpp:129
ScalingSummary summary
Top-level numeric summary.
Definition Scaling.hpp:130
std::vector< VariableScalingInfo > variables
Per-variable-block metadata.
Definition Scaling.hpp:132
ObjectiveScalingInfo objective
Objective scaling metadata.
Definition Scaling.hpp:131
std::vector< ConstraintScalingInfo > constraints
Per-constraint-row metadata.
Definition Scaling.hpp:133
std::vector< ScalingIssue > issues
Detected scaling issues.
Definition Scaling.hpp:134
int variable_blocks
Definition Scaling.hpp:115
double max_variable_scale
Definition Scaling.hpp:120
int scalar_constraints
Definition Scaling.hpp:116
int jacobian_nnz
Definition Scaling.hpp:117
double min_variable_scale
Definition Scaling.hpp:119
double jacobian_density
Definition Scaling.hpp:118
double variable_scale_ratio
Definition Scaling.hpp:121
Result of a parametric sweep.
Definition OptiSweep.hpp:32
Options for variable creation.
Definition Opti.hpp:126
bool freeze
If true, variable is fixed at init_guess.
Definition Opti.hpp:128
std::string category
Category for grouping variables.
Definition Opti.hpp:127