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:
70
74 [[nodiscard]]
76 m_decision_variables.emplace_back();
77 return m_decision_variables.back();
78 }
79
85 [[nodiscard]]
86 VariableMatrix<Scalar> decision_variable(int rows, int cols = 1) {
87 m_decision_variables.reserve(m_decision_variables.size() + rows * cols);
88
89 VariableMatrix<Scalar> vars{detail::empty, rows, cols};
90
91 for (int row = 0; row < rows; ++row) {
92 for (int col = 0; col < cols; ++col) {
93 m_decision_variables.emplace_back();
94 vars[row, col] = m_decision_variables.back();
95 }
96 }
97
98 return vars;
99 }
100
110 [[nodiscard]]
112 // We only need to store the lower triangle of an n x n symmetric matrix;
113 // the other elements are duplicates. The lower triangle has (n² + n)/2
114 // elements.
115 //
116 // n
117 // Σ k = (n² + n)/2
118 // k=1
119 m_decision_variables.reserve(m_decision_variables.size() +
120 (rows * rows + rows) / 2);
121
122 VariableMatrix<Scalar> vars{detail::empty, rows, rows};
123
124 for (int row = 0; row < rows; ++row) {
125 for (int col = 0; col <= row; ++col) {
126 m_decision_variables.emplace_back();
127 vars[row, col] = m_decision_variables.back();
128 vars[col, row] = m_decision_variables.back();
129 }
130 }
131
132 return vars;
133 }
134
142 void minimize(const Variable<Scalar>& cost) { m_f = cost; }
143
151 void minimize(Variable<Scalar>&& cost) { m_f = std::move(cost); }
152
161 // Maximizing a cost function is the same as minimizing its negative
162 m_f = -objective;
163 }
164
173 // Maximizing a cost function is the same as minimizing its negative
174 m_f = -std::move(objective);
175 }
176
182 m_equality_constraints.reserve(m_equality_constraints.size() +
183 constraint.constraints.size());
184 std::ranges::copy(constraint.constraints,
185 std::back_inserter(m_equality_constraints));
186 }
187
193 m_equality_constraints.reserve(m_equality_constraints.size() +
194 constraint.constraints.size());
195 std::ranges::copy(constraint.constraints,
196 std::back_inserter(m_equality_constraints));
197 }
198
204 m_inequality_constraints.reserve(m_inequality_constraints.size() +
205 constraint.constraints.size());
206 std::ranges::copy(constraint.constraints,
207 std::back_inserter(m_inequality_constraints));
208 }
209
215 m_inequality_constraints.reserve(m_inequality_constraints.size() +
216 constraint.constraints.size());
217 std::ranges::copy(constraint.constraints,
218 std::back_inserter(m_inequality_constraints));
219 }
220
224 ExpressionType cost_function_type() const {
225 if (m_f) {
226 return m_f.value().type();
227 } else {
228 return ExpressionType::NONE;
229 }
230 }
231
235 ExpressionType equality_constraint_type() const {
236 if (!m_equality_constraints.empty()) {
237 return std::ranges::max(m_equality_constraints, {},
239 .type();
240 } else {
241 return ExpressionType::NONE;
242 }
243 }
244
248 ExpressionType inequality_constraint_type() const {
249 if (!m_inequality_constraints.empty()) {
250 return std::ranges::max(m_inequality_constraints, {},
252 .type();
253 } else {
254 return ExpressionType::NONE;
255 }
256 }
257
266 ExitStatus solve(const Options& options = Options{},
267 [[maybe_unused]] bool spy = false) {
268 using DenseVector = Eigen::Vector<Scalar, Eigen::Dynamic>;
269 using SparseMatrix = Eigen::SparseMatrix<Scalar>;
270 using SparseVector = Eigen::SparseVector<Scalar>;
271
272 // Create the initial value column vector
273 DenseVector x{m_decision_variables.size()};
274 for (size_t i = 0; i < m_decision_variables.size(); ++i) {
275 x[i] = m_decision_variables[i].value();
276 }
277
278 if (options.diagnostics) {
279 print_exit_conditions(options);
280 print_problem_analysis();
281 }
282
283 // Get the highest order constraint expression types
284 auto f_type = cost_function_type();
285 auto c_e_type = equality_constraint_type();
286 auto c_i_type = inequality_constraint_type();
287
288 // If the problem is empty or constant, there's nothing to do
289 if (f_type <= ExpressionType::CONSTANT &&
290 c_e_type <= ExpressionType::CONSTANT &&
291 c_i_type <= ExpressionType::CONSTANT) {
292#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
293 if (options.diagnostics) {
294 slp::println("\nInvoking no-op solver...\n");
295 }
296#endif
297 return ExitStatus::SUCCESS;
298 }
299
300 gch::small_vector<SetupProfiler> ad_setup_profilers;
301 ad_setup_profilers.emplace_back("setup").start();
302
303 VariableMatrix<Scalar> x_ad{m_decision_variables};
304
305 // Set up cost function
306 Variable f = m_f.value_or(Scalar(0));
307
308 // Set up gradient autodiff
309 ad_setup_profilers.emplace_back(" ↳ ∇f(x)").start();
310 Gradient g{f, x_ad};
311 ad_setup_profilers.back().stop();
312
313 [[maybe_unused]]
314 int num_decision_variables = m_decision_variables.size();
315 [[maybe_unused]]
316 int num_equality_constraints = m_equality_constraints.size();
317 [[maybe_unused]]
318 int num_inequality_constraints = m_inequality_constraints.size();
319
320 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
321 callbacks;
322 for (const auto& callback : m_iteration_callbacks) {
323 callbacks.emplace_back(callback);
324 }
325 for (const auto& callback : m_persistent_iteration_callbacks) {
326 callbacks.emplace_back(callback);
327 }
328
329 // Solve the optimization problem
330 ExitStatus status;
331 if (m_equality_constraints.empty() && m_inequality_constraints.empty()) {
332 if (options.diagnostics) {
333 slp::println("\nInvoking Newton solver...\n");
334 }
335
336 // Set up Lagrangian Hessian autodiff
337 ad_setup_profilers.emplace_back(" ↳ ∇²ₓₓL").start();
338 Hessian<Scalar, Eigen::Lower> H{f, x_ad};
339 ad_setup_profilers.back().stop();
340
341 ad_setup_profilers[0].stop();
342
343#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
344 // Sparsity pattern files written when spy flag is set
345 std::unique_ptr<Spy<Scalar>> H_spy;
346
347 if (spy) {
348 H_spy = std::make_unique<Spy<Scalar>>(
349 "H.spy", "Hessian", "Decision variables", "Decision variables",
350 num_decision_variables, num_decision_variables);
351 callbacks.push_back([&](const IterationInfo<Scalar>& info) -> bool {
352 H_spy->add(info.H);
353 return false;
354 });
355 }
356#endif
357
358 // Invoke Newton solver
359 status = newton<Scalar>(NewtonMatrixCallbacks<Scalar>{
360 [&](const DenseVector& x) -> Scalar {
361 x_ad.set_value(x);
362 return f.value();
363 },
364 [&](const DenseVector& x) -> SparseVector {
365 x_ad.set_value(x);
366 return g.value();
367 },
368 [&](const DenseVector& x) -> SparseMatrix {
369 x_ad.set_value(x);
370 return H.value();
371 }},
372 callbacks, options, x);
373 } else if (m_inequality_constraints.empty()) {
374 if (options.diagnostics) {
375 slp::println("\nInvoking SQP solver\n");
376 }
377
378 VariableMatrix<Scalar> c_e_ad{m_equality_constraints};
379
380 // Set up equality constraint Jacobian autodiff
381 ad_setup_profilers.emplace_back(" ↳ ∂cₑ/∂x").start();
382 Jacobian A_e{c_e_ad, x_ad};
383 ad_setup_profilers.back().stop();
384
385 // Set up Lagrangian
386 VariableMatrix<Scalar> y_ad(num_equality_constraints);
387 Variable L = f - y_ad.T() * c_e_ad;
388
389 // Set up Lagrangian Hessian autodiff
390 ad_setup_profilers.emplace_back(" ↳ ∇²ₓₓL").start();
391 Hessian<Scalar, Eigen::Lower> H{L, x_ad};
392 ad_setup_profilers.back().stop();
393
394 ad_setup_profilers[0].stop();
395
396#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
397 // Sparsity pattern files written when spy flag is set
398 std::unique_ptr<Spy<Scalar>> H_spy;
399 std::unique_ptr<Spy<Scalar>> A_e_spy;
400
401 if (spy) {
402 H_spy = std::make_unique<Spy<Scalar>>(
403 "H.spy", "Hessian", "Decision variables", "Decision variables",
404 num_decision_variables, num_decision_variables);
405 A_e_spy = std::make_unique<Spy<Scalar>>(
406 "A_e.spy", "Equality constraint Jacobian", "Constraints",
407 "Decision variables", num_equality_constraints,
408 num_decision_variables);
409 callbacks.push_back([&](const IterationInfo<Scalar>& info) -> bool {
410 H_spy->add(info.H);
411 A_e_spy->add(info.A_e);
412 return false;
413 });
414 }
415#endif
416
417 // Invoke SQP solver
418 status = sqp<Scalar>(
419 SQPMatrixCallbacks<Scalar>{
420 [&](const DenseVector& x) -> Scalar {
421 x_ad.set_value(x);
422 return f.value();
423 },
424 [&](const DenseVector& x) -> SparseVector {
425 x_ad.set_value(x);
426 return g.value();
427 },
428 [&](const DenseVector& x, const DenseVector& y) -> SparseMatrix {
429 x_ad.set_value(x);
430 y_ad.set_value(y);
431 return H.value();
432 },
433 [&](const DenseVector& x) -> DenseVector {
434 x_ad.set_value(x);
435 return c_e_ad.value();
436 },
437 [&](const DenseVector& x) -> SparseMatrix {
438 x_ad.set_value(x);
439 return A_e.value();
440 }},
441 callbacks, options, x);
442 } else {
443 if (options.diagnostics) {
444 slp::println("\nInvoking IPM solver...\n");
445 }
446
447 VariableMatrix<Scalar> c_e_ad{m_equality_constraints};
448 VariableMatrix<Scalar> c_i_ad{m_inequality_constraints};
449
450 // Set up equality constraint Jacobian autodiff
451 ad_setup_profilers.emplace_back(" ↳ ∂cₑ/∂x").start();
452 Jacobian A_e{c_e_ad, x_ad};
453 ad_setup_profilers.back().stop();
454
455 // Set up inequality constraint Jacobian autodiff
456 ad_setup_profilers.emplace_back(" ↳ ∂cᵢ/∂x").start();
457 Jacobian A_i{c_i_ad, x_ad};
458 ad_setup_profilers.back().stop();
459
460 // Set up Lagrangian
461 VariableMatrix<Scalar> y_ad(num_equality_constraints);
462 VariableMatrix<Scalar> z_ad(num_inequality_constraints);
463 Variable L = f - y_ad.T() * c_e_ad - z_ad.T() * c_i_ad;
464
465 // Set up Lagrangian Hessian autodiff
466 ad_setup_profilers.emplace_back(" ↳ ∇²ₓₓL").start();
467 Hessian<Scalar, Eigen::Lower> H{L, x_ad};
468 ad_setup_profilers.back().stop();
469
470 ad_setup_profilers[0].stop();
471
472#ifndef SLEIPNIR_DISABLE_DIAGNOSTICS
473 // Sparsity pattern files written when spy flag is set
474 std::unique_ptr<Spy<Scalar>> H_spy;
475 std::unique_ptr<Spy<Scalar>> A_e_spy;
476 std::unique_ptr<Spy<Scalar>> A_i_spy;
477
478 if (spy) {
479 H_spy = std::make_unique<Spy<Scalar>>(
480 "H.spy", "Hessian", "Decision variables", "Decision variables",
481 num_decision_variables, num_decision_variables);
482 A_e_spy = std::make_unique<Spy<Scalar>>(
483 "A_e.spy", "Equality constraint Jacobian", "Constraints",
484 "Decision variables", num_equality_constraints,
485 num_decision_variables);
486 A_i_spy = std::make_unique<Spy<Scalar>>(
487 "A_i.spy", "Inequality constraint Jacobian", "Constraints",
488 "Decision variables", num_inequality_constraints,
489 num_decision_variables);
490 callbacks.push_back([&](const IterationInfo<Scalar>& info) -> bool {
491 H_spy->add(info.H);
492 A_e_spy->add(info.A_e);
493 A_i_spy->add(info.A_i);
494 return false;
495 });
496 }
497#endif
498
499 const auto [bound_constraint_mask, bounds, conflicting_bound_indices] =
500 get_bounds<Scalar>(m_decision_variables, m_inequality_constraints,
501 A_i.value());
502 if (!conflicting_bound_indices.empty()) {
503 if (options.diagnostics) {
504 print_bound_constraint_global_infeasibility_error(
505 conflicting_bound_indices);
506 }
507 return ExitStatus::GLOBALLY_INFEASIBLE;
508 }
509
510#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
511 project_onto_bounds(x, bounds);
512#endif
513 // Invoke interior-point method solver
514 status = interior_point<Scalar>(
515 InteriorPointMatrixCallbacks<Scalar>{
516 [&](const DenseVector& x) -> Scalar {
517 x_ad.set_value(x);
518 return f.value();
519 },
520 [&](const DenseVector& x) -> SparseVector {
521 x_ad.set_value(x);
522 return g.value();
523 },
524 [&](const DenseVector& x, const DenseVector& y,
525 const DenseVector& z) -> SparseMatrix {
526 x_ad.set_value(x);
527 y_ad.set_value(y);
528 z_ad.set_value(z);
529 return H.value();
530 },
531 [&](const DenseVector& x) -> DenseVector {
532 x_ad.set_value(x);
533 return c_e_ad.value();
534 },
535 [&](const DenseVector& x) -> SparseMatrix {
536 x_ad.set_value(x);
537 return A_e.value();
538 },
539 [&](const DenseVector& x) -> DenseVector {
540 x_ad.set_value(x);
541 return c_i_ad.value();
542 },
543 [&](const DenseVector& x) -> SparseMatrix {
544 x_ad.set_value(x);
545 return A_i.value();
546 }},
547 callbacks, options,
548#ifdef SLEIPNIR_ENABLE_BOUND_PROJECTION
549 bound_constraint_mask,
550#endif
551 x);
552 }
553
554 if (options.diagnostics) {
555 print_autodiff_diagnostics(ad_setup_profilers);
556 slp::println("\nExit: {}", status);
557 }
558
559 // Assign the solution to the original Variable instances
560 VariableMatrix<Scalar>{m_decision_variables}.set_value(x);
561
562 return status;
563 }
564
570 template <typename F>
571 requires requires(F callback, const IterationInfo<Scalar>& info) {
572 { callback(info) } -> std::same_as<void>;
573 }
575 m_iteration_callbacks.emplace_back(
576 [=, callback =
577 std::forward<F>(callback)](const IterationInfo<Scalar>& info) {
578 callback(info);
579 return false;
580 });
581 }
582
589 template <typename F>
590 requires requires(F callback, const IterationInfo<Scalar>& info) {
591 { callback(info) } -> std::same_as<bool>;
592 }
594 m_iteration_callbacks.emplace_back(std::forward<F>(callback));
595 }
596
598 void clear_callbacks() { m_iteration_callbacks.clear(); }
599
608 template <typename F>
609 requires requires(F callback, const IterationInfo<Scalar>& info) {
610 { callback(info) } -> std::same_as<bool>;
611 }
613 m_persistent_iteration_callbacks.emplace_back(std::forward<F>(callback));
614 }
615
616 private:
617 // The list of decision variables, which are the root of the problem's
618 // expression tree
619 gch::small_vector<Variable<Scalar>> m_decision_variables;
620
621 // The cost function: f(x)
622 std::optional<Variable<Scalar>> m_f;
623
624 // The list of equality constraints: cₑ(x) = 0
625 gch::small_vector<Variable<Scalar>> m_equality_constraints;
626
627 // The list of inequality constraints: cᵢ(x) ≥ 0
628 gch::small_vector<Variable<Scalar>> m_inequality_constraints;
629
630 // The iteration callbacks
631 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
632 m_iteration_callbacks;
633 gch::small_vector<std::function<bool(const IterationInfo<Scalar>& info)>>
634 m_persistent_iteration_callbacks;
635
636 void print_exit_conditions([[maybe_unused]] const Options& options) {
637 // Print possible exit conditions
638 slp::println("User-configured exit conditions:");
639 slp::println(" ↳ error below {}", options.tolerance);
640 if (!m_iteration_callbacks.empty() ||
641 !m_persistent_iteration_callbacks.empty()) {
642 slp::println(" ↳ iteration callback requested stop");
643 }
644 if (std::isfinite(options.max_iterations)) {
645 slp::println(" ↳ executed {} iterations", options.max_iterations);
646 }
647 if (std::isfinite(options.timeout.count())) {
648 slp::println(" ↳ {} elapsed", options.timeout);
649 }
650 }
651
652 void print_problem_analysis() {
653 constexpr std::array types{"no", "constant", "linear", "quadratic",
654 "nonlinear"};
655
656 // Print problem structure
657 slp::println("\nProblem structure:");
658 slp::println(" ↳ {} cost function",
659 types[std::to_underlying(cost_function_type())]);
660 slp::println(" ↳ {} equality constraints",
661 types[std::to_underlying(equality_constraint_type())]);
662 slp::println(" ↳ {} inequality constraints",
663 types[std::to_underlying(inequality_constraint_type())]);
664
665 if (m_decision_variables.size() == 1) {
666 slp::print("\n1 decision variable\n");
667 } else {
668 slp::print("\n{} decision variables\n", m_decision_variables.size());
669 }
670
671 auto print_constraint_types =
672 [](const gch::small_vector<Variable<Scalar>>& constraints) {
673 std::array<size_t, 5> counts{};
674 for (const auto& constraint : constraints) {
675 ++counts[std::to_underlying(constraint.type())];
676 }
677 for (const auto& [count, name] :
678 std::views::zip(counts, std::array{"empty", "constant", "linear",
679 "quadratic", "nonlinear"})) {
680 if (count > 0) {
681 slp::println(" ↳ {} {}", count, name);
682 }
683 }
684 };
685
686 // Print constraint types
687 if (m_equality_constraints.size() == 1) {
688 slp::println("1 equality constraint");
689 } else {
690 slp::println("{} equality constraints", m_equality_constraints.size());
691 }
692 print_constraint_types(m_equality_constraints);
693 if (m_inequality_constraints.size() == 1) {
694 slp::println("1 inequality constraint");
695 } else {
696 slp::println("{} inequality constraints",
697 m_inequality_constraints.size());
698 }
699 print_constraint_types(m_inequality_constraints);
700 }
701};
702
703extern template class EXPORT_TEMPLATE_DECLARE(SLEIPNIR_DLLEXPORT)
704Problem<double>;
705
706} // namespace slp
Definition intrusive_shared_ptr.hpp:27
Definition problem.hpp:66
VariableMatrix< Scalar > symmetric_decision_variable(int rows)
Definition problem.hpp:111
void subject_to(InequalityConstraints< Scalar > &&constraint)
Definition problem.hpp:214
void add_callback(F &&callback)
Definition problem.hpp:593
void subject_to(EqualityConstraints< Scalar > &&constraint)
Definition problem.hpp:192
ExpressionType equality_constraint_type() const
Definition problem.hpp:235
void maximize(Variable< Scalar > &&objective)
Definition problem.hpp:172
void add_persistent_callback(F &&callback)
Definition problem.hpp:612
void subject_to(const InequalityConstraints< Scalar > &constraint)
Definition problem.hpp:203
ExpressionType inequality_constraint_type() const
Definition problem.hpp:248
void minimize(const Variable< Scalar > &cost)
Definition problem.hpp:142
Problem() noexcept=default
Construct the optimization problem.
VariableMatrix< Scalar > decision_variable(int rows, int cols=1)
Definition problem.hpp:86
void minimize(Variable< Scalar > &&cost)
Definition problem.hpp:151
void clear_callbacks()
Clears the registered callbacks.
Definition problem.hpp:598
void subject_to(const EqualityConstraints< Scalar > &constraint)
Definition problem.hpp:181
void maximize(const Variable< Scalar > &objective)
Definition problem.hpp:160
void add_callback(F &&callback)
Definition problem.hpp:574
ExpressionType cost_function_type() const
Definition problem.hpp:224
ExitStatus solve(const Options &options=Options{}, bool spy=false)
Definition problem.hpp:266
Variable< Scalar > decision_variable()
Definition problem.hpp:75
Definition variable.hpp:47
Solver options.
Definition options.hpp:13