Sleipnir C++ API
Loading...
Searching...
No Matches
problem.hpp
1// Copyright (c) Sleipnir contributors
2
3#pragma once
4
5#include <algorithm>
6#include <array>
7#include <cmath>
8#include <concepts>
9#include <functional>
10#include <iterator>
11#include <memory>
12#include <optional>
13#include <ranges>
14#include <utility>
15
16#include <Eigen/Core>
17#include <Eigen/SparseCore>
18#include <gch/small_vector.hpp>
19
20#include "sleipnir/autodiff/expression_type.hpp"
21#include "sleipnir/autodiff/gradient.hpp"
22#include "sleipnir/autodiff/hessian.hpp"
23#include "sleipnir/autodiff/jacobian.hpp"
24#include "sleipnir/autodiff/variable.hpp"
25#include "sleipnir/autodiff/variable_matrix.hpp"
26#include "sleipnir/optimization/solver/exit_status.hpp"
27#include "sleipnir/optimization/solver/interior_point.hpp"
28#include "sleipnir/optimization/solver/iteration_info.hpp"
29#include "sleipnir/optimization/solver/newton.hpp"
30#include "sleipnir/optimization/solver/options.hpp"
31#include "sleipnir/optimization/solver/sqp.hpp"
32#include "sleipnir/optimization/solver/util/bounds.hpp"
33#include "sleipnir/util/empty.hpp"
34#include "sleipnir/util/print.hpp"
35#include "sleipnir/util/print_diagnostics.hpp"
36#include "sleipnir/util/setup_profiler.hpp"
37#include "sleipnir/util/spy.hpp"
38#include "sleipnir/util/symbol_exports.hpp"
39
40namespace slp {
41
65template <typename Scalar>
66class Problem {
67 public:
72
78 [[nodiscard]]
80 m_decision_variables.emplace_back();
81 return m_decision_variables.back();
82 }
83
91 [[nodiscard]]
92 VariableMatrix<Scalar> decision_variable(int rows, int cols = 1) {
93 m_decision_variables.reserve(m_decision_variables.size() + rows * cols);
94
95 VariableMatrix<Scalar> vars{detail::empty, rows, cols};
96
97 for (int row = 0; row < rows; ++row) {
98 for (int col = 0; col < cols; ++col) {
99 m_decision_variables.emplace_back();
100 vars[row, col] = m_decision_variables.back();
101 }
102 }
103
104 return vars;
105 }
106
118 [[nodiscard]]
120 // We only need to store the lower triangle of an n x n symmetric matrix;
121 // the other elements are duplicates. The lower triangle has (n² + n)/2
122 // elements.
123 //
124 // n
125 // Σ k = (n² + n)/2
126 // k=1
127 m_decision_variables.reserve(m_decision_variables.size() +
128 (rows * rows + rows) / 2);
129
130 VariableMatrix<Scalar> vars{detail::empty, rows, rows};
131
132 for (int row = 0; row < rows; ++row) {
133 for (int col = 0; col <= row; ++col) {
134 m_decision_variables.emplace_back();
135 vars[row, col] = m_decision_variables.back();
136 vars[col, row] = m_decision_variables.back();
137 }
138 }
139
140 return vars;
141 }
142
152 void minimize(const Variable<Scalar>& cost) { m_f = cost; }
153
163 void minimize(Variable<Scalar>&& cost) { m_f = std::move(cost); }
164
175 // Maximizing a cost function is the same as minimizing its negative
176 m_f = -objective;
177 }
178
189 // Maximizing a cost function is the same as minimizing its negative
190 m_f = -std::move(objective);
191 }
192
200 m_equality_constraints.reserve(m_equality_constraints.size() +
201 constraint.constraints.size());
202 std::ranges::copy(constraint.constraints,
203 std::back_inserter(m_equality_constraints));
204 }
205
213 m_equality_constraints.reserve(m_equality_constraints.size() +
214 constraint.constraints.size());
215 std::ranges::copy(constraint.constraints,
216 std::back_inserter(m_equality_constraints));
217 }
218
226 m_inequality_constraints.reserve(m_inequality_constraints.size() +
227 constraint.constraints.size());
228 std::ranges::copy(constraint.constraints,
229 std::back_inserter(m_inequality_constraints));
230 }
231
239 m_inequality_constraints.reserve(m_inequality_constraints.size() +
240 constraint.constraints.size());
241 std::ranges::copy(constraint.constraints,
242 std::back_inserter(m_inequality_constraints));
243 }
244
250 ExpressionType cost_function_type() const {
251 if (m_f) {
252 return m_f.value().type();
253 } else {
254 return ExpressionType::NONE;
255 }
256 }
257
263 ExpressionType equality_constraint_type() const {
264 if (!m_equality_constraints.empty()) {
265 return std::ranges::max(m_equality_constraints, {},
267 .type();
268 } else {
269 return ExpressionType::NONE;
270 }
271 }
272
278 ExpressionType inequality_constraint_type() const {
279 if (!m_inequality_constraints.empty()) {
280 return std::ranges::max(m_inequality_constraints, {},
282 .type();
283 } else {
284 return ExpressionType::NONE;
285 }
286 }
287
298 ExitStatus solve(const Options& options = Options{},
299 [[maybe_unused]] bool spy = false) {
300 // Create the initial value column vector
301 Eigen::Vector<Scalar, Eigen::Dynamic> x{m_decision_variables.size()};
302 for (size_t i = 0; i < m_decision_variables.size(); ++i) {
303 x[i] = m_decision_variables[i].value();
304 }
305
306 if (options.diagnostics) {
307 print_exit_conditions(options);
308 print_problem_analysis();
309 }
310
311 // Get the highest order constraint expression types
312 auto f_type = cost_function_type();
313 auto c_e_type = equality_constraint_type();
314 auto c_i_type = inequality_constraint_type();
315
316 // If the problem is empty or constant, there's nothing to do
317 if (f_type <= ExpressionType::CONSTANT &&
318 c_e_type <= ExpressionType::CONSTANT &&
319 c_i_type <= ExpressionType::CONSTANT) {
320#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
321 if (options.diagnostics) {
322 slp::println("\nInvoking no-op solver...\n");
323 }
324#endif
325 return ExitStatus::SUCCESS;
326 }
327
328 gch::small_vector<SetupProfiler> ad_setup_profilers;
329 ad_setup_profilers.emplace_back("setup").start();
330
331 VariableMatrix<Scalar> x_ad{m_decision_variables};
332
333 // Set up cost function
334 Variable f = m_f.value_or(Scalar(0));
335
336 // Set up gradient autodiff
337 ad_setup_profilers.emplace_back(" ↳ ∇f(x)").start();
338 Gradient g{f, x_ad};
339 ad_setup_profilers.back().stop();
340
341 [[maybe_unused]]
342 int num_decision_variables = m_decision_variables.size();
343 [[maybe_unused]]
344 int num_equality_constraints = m_equality_constraints.size();
345 [[maybe_unused]]
346 int num_inequality_constraints = m_inequality_constraints.size();
347
348 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
349 callbacks;
350 for (const auto& callback : m_iteration_callbacks) {
351 callbacks.emplace_back(callback);
352 }
353 for (const auto& callback : m_persistent_iteration_callbacks) {
354 callbacks.emplace_back(callback);
355 }
356
357 // Solve the optimization problem
358 ExitStatus status;
359 if (m_equality_constraints.empty() && m_inequality_constraints.empty()) {
360 if (options.diagnostics) {
361 slp::println("\nInvoking Newton solver...\n");
362 }
363
364 // Set up Lagrangian Hessian autodiff
365 ad_setup_profilers.emplace_back(" ↳ ∇²ₓₓL").start();
366 Hessian<Scalar, Eigen::Lower> H{f, x_ad};
367 ad_setup_profilers.back().stop();
368
369 ad_setup_profilers[0].stop();
370
371#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
372 // Sparsity pattern files written when spy flag is set
373 std::unique_ptr<Spy<Scalar>> H_spy;
374
375 if (spy) {
376 H_spy = std::make_unique<Spy<Scalar>>(
377 "H.spy", "Hessian", "Decision variables", "Decision variables",
378 num_decision_variables, num_decision_variables);
379 callbacks.push_back([&](const IterationInfo<Scalar>& info) -> bool {
380 H_spy->add(info.H);
381 return false;
382 });
383 }
384#endif
385
386 // Invoke Newton solver
387 status = newton<Scalar>(
388 NewtonMatrixCallbacks<Scalar>{
389 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x) -> Scalar {
390 x_ad.set_value(x);
391 return f.value();
392 },
393 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
394 -> Eigen::SparseVector<Scalar> {
395 x_ad.set_value(x);
396 return g.value();
397 },
398 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
399 -> Eigen::SparseMatrix<Scalar> {
400 x_ad.set_value(x);
401 return H.value();
402 }},
403 callbacks, options, x);
404 } else if (m_inequality_constraints.empty()) {
405 if (options.diagnostics) {
406 slp::println("\nInvoking SQP solver\n");
407 }
408
409 VariableMatrix<Scalar> c_e_ad{m_equality_constraints};
410
411 // Set up equality constraint Jacobian autodiff
412 ad_setup_profilers.emplace_back(" ↳ ∂cₑ/∂x").start();
413 Jacobian A_e{c_e_ad, x_ad};
414 ad_setup_profilers.back().stop();
415
416 // Set up Lagrangian
417 VariableMatrix<Scalar> y_ad(num_equality_constraints);
418 Variable L = f - y_ad.T() * c_e_ad;
419
420 // Set up Lagrangian Hessian autodiff
421 ad_setup_profilers.emplace_back(" ↳ ∇²ₓₓL").start();
422 Hessian<Scalar, Eigen::Lower> H{L, x_ad};
423 ad_setup_profilers.back().stop();
424
425 ad_setup_profilers[0].stop();
426
427#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
428 // Sparsity pattern files written when spy flag is set
429 std::unique_ptr<Spy<Scalar>> H_spy;
430 std::unique_ptr<Spy<Scalar>> A_e_spy;
431
432 if (spy) {
433 H_spy = std::make_unique<Spy<Scalar>>(
434 "H.spy", "Hessian", "Decision variables", "Decision variables",
435 num_decision_variables, num_decision_variables);
436 A_e_spy = std::make_unique<Spy<Scalar>>(
437 "A_e.spy", "Equality constraint Jacobian", "Constraints",
438 "Decision variables", num_equality_constraints,
439 num_decision_variables);
440 callbacks.push_back([&](const IterationInfo<Scalar>& info) -> bool {
441 H_spy->add(info.H);
442 A_e_spy->add(info.A_e);
443 return false;
444 });
445 }
446#endif
447
448 // Invoke SQP solver
449 status = sqp<Scalar>(
450 SQPMatrixCallbacks<Scalar>{
451 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x) -> Scalar {
452 x_ad.set_value(x);
453 return f.value();
454 },
455 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
456 -> Eigen::SparseVector<Scalar> {
457 x_ad.set_value(x);
458 return g.value();
459 },
460 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x,
461 const Eigen::Vector<Scalar, Eigen::Dynamic>& y)
462 -> Eigen::SparseMatrix<Scalar> {
463 x_ad.set_value(x);
464 y_ad.set_value(y);
465 return H.value();
466 },
467 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
468 -> Eigen::Vector<Scalar, Eigen::Dynamic> {
469 x_ad.set_value(x);
470 return c_e_ad.value();
471 },
472 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
473 -> Eigen::SparseMatrix<Scalar> {
474 x_ad.set_value(x);
475 return A_e.value();
476 }},
477 callbacks, options, x);
478 } else {
479 if (options.diagnostics) {
480 slp::println("\nInvoking IPM solver...\n");
481 }
482
483 VariableMatrix<Scalar> c_e_ad{m_equality_constraints};
484 VariableMatrix<Scalar> c_i_ad{m_inequality_constraints};
485
486 // Set up equality constraint Jacobian autodiff
487 ad_setup_profilers.emplace_back(" ↳ ∂cₑ/∂x").start();
488 Jacobian A_e{c_e_ad, x_ad};
489 ad_setup_profilers.back().stop();
490
491 // Set up inequality constraint Jacobian autodiff
492 ad_setup_profilers.emplace_back(" ↳ ∂cᵢ/∂x").start();
493 Jacobian A_i{c_i_ad, x_ad};
494 ad_setup_profilers.back().stop();
495
496 // Set up Lagrangian
497 VariableMatrix<Scalar> y_ad(num_equality_constraints);
498 VariableMatrix<Scalar> z_ad(num_inequality_constraints);
499 Variable L = f - y_ad.T() * c_e_ad - z_ad.T() * c_i_ad;
500
501 // Set up Lagrangian Hessian autodiff
502 ad_setup_profilers.emplace_back(" ↳ ∇²ₓₓL").start();
503 Hessian<Scalar, Eigen::Lower> H{L, x_ad};
504 ad_setup_profilers.back().stop();
505
506 ad_setup_profilers[0].stop();
507
508#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
509 // Sparsity pattern files written when spy flag is set
510 std::unique_ptr<Spy<Scalar>> H_spy;
511 std::unique_ptr<Spy<Scalar>> A_e_spy;
512 std::unique_ptr<Spy<Scalar>> A_i_spy;
513
514 if (spy) {
515 H_spy = std::make_unique<Spy<Scalar>>(
516 "H.spy", "Hessian", "Decision variables", "Decision variables",
517 num_decision_variables, num_decision_variables);
518 A_e_spy = std::make_unique<Spy<Scalar>>(
519 "A_e.spy", "Equality constraint Jacobian", "Constraints",
520 "Decision variables", num_equality_constraints,
521 num_decision_variables);
522 A_i_spy = std::make_unique<Spy<Scalar>>(
523 "A_i.spy", "Inequality constraint Jacobian", "Constraints",
524 "Decision variables", num_inequality_constraints,
525 num_decision_variables);
526 callbacks.push_back([&](const IterationInfo<Scalar>& info) -> bool {
527 H_spy->add(info.H);
528 A_e_spy->add(info.A_e);
529 A_i_spy->add(info.A_i);
530 return false;
531 });
532 }
533#endif
534
535 const auto [bound_constraint_mask, bounds, conflicting_bound_indices] =
536 get_bounds<Scalar>(m_decision_variables, m_inequality_constraints,
537 A_i.value());
538 if (!conflicting_bound_indices.empty()) {
539 if (options.diagnostics) {
540 print_bound_constraint_global_infeasibility_error(
541 conflicting_bound_indices);
542 }
543 return ExitStatus::GLOBALLY_INFEASIBLE;
544 }
545
546#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
547 project_onto_bounds(x, bounds);
548#endif
549 // Invoke interior-point method solver
550 status = interior_point<Scalar>(
551 InteriorPointMatrixCallbacks<Scalar>{
552 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x) -> Scalar {
553 x_ad.set_value(x);
554 return f.value();
555 },
556 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
557 -> Eigen::SparseVector<Scalar> {
558 x_ad.set_value(x);
559 return g.value();
560 },
561 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x,
562 const Eigen::Vector<Scalar, Eigen::Dynamic>& y,
563 const Eigen::Vector<Scalar, Eigen::Dynamic>& z)
564 -> Eigen::SparseMatrix<Scalar> {
565 x_ad.set_value(x);
566 y_ad.set_value(y);
567 z_ad.set_value(z);
568 return H.value();
569 },
570 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
571 -> Eigen::Vector<Scalar, Eigen::Dynamic> {
572 x_ad.set_value(x);
573 return c_e_ad.value();
574 },
575 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
576 -> Eigen::SparseMatrix<Scalar> {
577 x_ad.set_value(x);
578 return A_e.value();
579 },
580 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
581 -> Eigen::Vector<Scalar, Eigen::Dynamic> {
582 x_ad.set_value(x);
583 return c_i_ad.value();
584 },
585 [&](const Eigen::Vector<Scalar, Eigen::Dynamic>& x)
586 -> Eigen::SparseMatrix<Scalar> {
587 x_ad.set_value(x);
588 return A_i.value();
589 }},
590 callbacks, options,
591#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
592 bound_constraint_mask,
593#endif
594 x);
595 }
596
597 if (options.diagnostics) {
598 print_autodiff_diagnostics(ad_setup_profilers);
599 slp::println("\nExit: {}", to_message(status));
600 }
601
602 // Assign the solution to the original Variable instances
603 VariableMatrix<Scalar>{m_decision_variables}.set_value(x);
604
605 return status;
606 }
607
615 template <typename F>
616 requires requires(F callback, const IterationInfo<Scalar>& info) {
617 { callback(info) } -> std::same_as<void>;
618 }
620 m_iteration_callbacks.emplace_back(
621 [=, callback =
622 std::forward<F>(callback)](const IterationInfo<Scalar>& info) {
623 callback(info);
624 return false;
625 });
626 }
627
636 template <typename F>
637 requires requires(F callback, const IterationInfo<Scalar>& info) {
638 { callback(info) } -> std::same_as<bool>;
639 }
641 m_iteration_callbacks.emplace_back(std::forward<F>(callback));
642 }
643
647 void clear_callbacks() { m_iteration_callbacks.clear(); }
648
659 template <typename F>
660 requires requires(F callback, const IterationInfo<Scalar>& info) {
661 { callback(info) } -> std::same_as<bool>;
662 }
664 m_persistent_iteration_callbacks.emplace_back(std::forward<F>(callback));
665 }
666
667 private:
668 // The list of decision variables, which are the root of the problem's
669 // expression tree
670 gch::small_vector<Variable<Scalar>> m_decision_variables;
671
672 // The cost function: f(x)
673 std::optional<Variable<Scalar>> m_f;
674
675 // The list of equality constraints: cₑ(x) = 0
676 gch::small_vector<Variable<Scalar>> m_equality_constraints;
677
678 // The list of inequality constraints: cᵢ(x) ≥ 0
679 gch::small_vector<Variable<Scalar>> m_inequality_constraints;
680
681 // The iteration callbacks
682 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
683 m_iteration_callbacks;
684 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
685 m_persistent_iteration_callbacks;
686
687 void print_exit_conditions([[maybe_unused]] const Options& options) {
688 // Print possible exit conditions
689 slp::println("User-configured exit conditions:");
690 slp::println(" ↳ error below {}", options.tolerance);
691 if (!m_iteration_callbacks.empty() ||
692 !m_persistent_iteration_callbacks.empty()) {
693 slp::println(" ↳ iteration callback requested stop");
694 }
695 if (std::isfinite(options.max_iterations)) {
696 slp::println(" ↳ executed {} iterations", options.max_iterations);
697 }
698 if (std::isfinite(options.timeout.count())) {
699 slp::println(" ↳ {} elapsed", options.timeout);
700 }
701 }
702
703 void print_problem_analysis() {
704 constexpr std::array types{"no", "constant", "linear", "quadratic",
705 "nonlinear"};
706
707 // Print problem structure
708 slp::println("\nProblem structure:");
709 slp::println(" ↳ {} cost function",
710 types[std::to_underlying(cost_function_type())]);
711 slp::println(" ↳ {} equality constraints",
712 types[std::to_underlying(equality_constraint_type())]);
713 slp::println(" ↳ {} inequality constraints",
714 types[std::to_underlying(inequality_constraint_type())]);
715
716 if (m_decision_variables.size() == 1) {
717 slp::print("\n1 decision variable\n");
718 } else {
719 slp::print("\n{} decision variables\n", m_decision_variables.size());
720 }
721
722 auto print_constraint_types =
723 [](const gch::small_vector<Variable<Scalar>>& constraints) {
724 std::array<size_t, 5> counts{};
725 for (const auto& constraint : constraints) {
726 ++counts[std::to_underlying(constraint.type())];
727 }
728 for (const auto& [count, name] :
729 std::views::zip(counts, std::array{"empty", "constant", "linear",
730 "quadratic", "nonlinear"})) {
731 if (count > 0) {
732 slp::println(" ↳ {} {}", count, name);
733 }
734 }
735 };
736
737 // Print constraint types
738 if (m_equality_constraints.size() == 1) {
739 slp::println("1 equality constraint");
740 } else {
741 slp::println("{} equality constraints", m_equality_constraints.size());
742 }
743 print_constraint_types(m_equality_constraints);
744 if (m_inequality_constraints.size() == 1) {
745 slp::println("1 inequality constraint");
746 } else {
747 slp::println("{} inequality constraints",
748 m_inequality_constraints.size());
749 }
750 print_constraint_types(m_inequality_constraints);
751 }
752};
753
754extern template class EXPORT_TEMPLATE_DECLARE(SLEIPNIR_DLLEXPORT)
755Problem<double>;
756
757} // namespace slp
Definition intrusive_shared_ptr.hpp:29
Definition problem.hpp:66
VariableMatrix< Scalar > symmetric_decision_variable(int rows)
Definition problem.hpp:119
void subject_to(InequalityConstraints< Scalar > &&constraint)
Definition problem.hpp:238
void add_callback(F &&callback)
Definition problem.hpp:640
void subject_to(EqualityConstraints< Scalar > &&constraint)
Definition problem.hpp:212
ExpressionType equality_constraint_type() const
Definition problem.hpp:263
void maximize(Variable< Scalar > &&objective)
Definition problem.hpp:188
void add_persistent_callback(F &&callback)
Definition problem.hpp:663
void subject_to(const InequalityConstraints< Scalar > &constraint)
Definition problem.hpp:225
ExpressionType inequality_constraint_type() const
Definition problem.hpp:278
void minimize(const Variable< Scalar > &cost)
Definition problem.hpp:152
Problem() noexcept=default
VariableMatrix< Scalar > decision_variable(int rows, int cols=1)
Definition problem.hpp:92
void minimize(Variable< Scalar > &&cost)
Definition problem.hpp:163
void clear_callbacks()
Definition problem.hpp:647
void subject_to(const EqualityConstraints< Scalar > &constraint)
Definition problem.hpp:199
void maximize(const Variable< Scalar > &objective)
Definition problem.hpp:174
void add_callback(F &&callback)
Definition problem.hpp:619
ExpressionType cost_function_type() const
Definition problem.hpp:250
ExitStatus solve(const Options &options=Options{}, bool spy=false)
Definition problem.hpp:298
Variable< Scalar > decision_variable()
Definition problem.hpp:79
Definition variable.hpp:49
Definition options.hpp:15