akreon-convex
Typed convex modeling, conic canonicalization, and primal-dual optimization in C++26.
Loading...
Searching...
No Matches
pd_ipm.hpp
Go to the documentation of this file.
1/*
2 * Copyright © 2025-2026 Akreon LLC
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6#pragma once
7
10#include "equality_coupling.hpp"
11#include "newton_step.hpp"
12#include "pd_info.hpp"
13#include "pd_ipm_result.hpp"
14#include "pred_corr.hpp"
15#include "residual_state.hpp"
16
17namespace convex {
18
30template <linalg::VectorContainer ConeVar>
31constexpr auto mu(const ConeVar& x, const ConeVar& y,
32 const linalg::vector_element_t<ConeVar>& nu) {
33 return linalg::inner_product(x, y) / nu;
34}
35
49template <linalg::SquareMatrix Mat, linalg::MatrixContainer EqMat>
50constexpr auto regularize(
51 Mat M, const EqMat& A,
52 const config::RegularizationConfig<linalg::matrix_scalar_t<EqMat>>& reg) {
53 using T = linalg::matrix_scalar_t<Mat>;
54 constexpr std::size_t N = linalg::mat_cols_v<Mat>;
55 constexpr std::size_t EqRows = linalg::mat_rows_v<EqMat>;
56 constexpr std::size_t EqCols = linalg::mat_cols_v<EqMat>;
57
58 const auto d = get_diag(M);
59 T s{0};
60 for (const auto& x : d) s = std::max(s, linalg::constexpr_abs(x));
61
62 s = std::max(s, T(1));
63 const auto delta = reg.global_eps * s;
64 const auto eq_delta = reg.eq_eps * s;
65
66 for (std::size_t j = 0; j < N; ++j) {
67 T bump = delta;
68
69 if constexpr (EqRows != 0 && EqCols != 0) {
70 bool eq_active = false;
71 if (j < EqCols) {
72 for (std::size_t i = 0; i < EqRows; ++i) {
73 if (linalg::constexpr_abs(A(i, j)) > T(0)) {
74 eq_active = true;
75 break;
76 }
77 }
78 }
79
80 if (eq_active && linalg::constexpr_abs(d(j)) < eq_delta) bump = eq_delta;
81 }
82
83 M(j, j) += bump;
84 }
85
86 return M;
87}
88
92template <ProblemLike Problem>
97 using ConeMap = decltype(std::declval<const Problem&>().constraint_map());
98 using ConeMat = affine::affine_mat_t<ConeMap>;
99 using EqMap = decltype(std::declval<const Problem&>().equality_map());
100 using EqMat = affine::affine_mat_t<EqMap>;
102 decltype(std::declval<const Problem&>().canonical_trace());
103 using T = linalg::space_type_t<DecVar>;
104
106 static constexpr std::size_t NumCones = std::tuple_size_v<
107 decltype(std::declval<const Problem&>().constraint_cones())>;
108 static constexpr std::size_t M = linalg::mat_rows_v<ConeMat>;
109 static constexpr std::size_t N = linalg::mat_cols_v<ConeMat>;
110
111 static constexpr std::size_t EqRows = linalg::mat_rows_v<EqMat>;
112 using NuVar = linalg::Vector<T, EqRows>;
113
115 using BlockMat = linalg::Matrix<T, N, N>;
116 using Var = linalg::Vector<T, N>;
117 using ConeVar = linalg::Vector<T, M>;
120#ifdef AKREON_REPORT_OUTPUT
122#else
124#endif
125
126 static_assert(is_problem_set_v<Problem>,
127 "Problem is not set therefore cannot be solved");
128
130 static constexpr T rho{linalg::ln2_v<T>};
131
133 static constexpr T beta{1 - linalg::ln2_v<T>};
134
140 constexpr explicit PrimalDualIPM(const Problem& prob) { solve(prob); }
141
148 constexpr explicit PrimalDualIPM(
150 : config(config_in) {
151 solve(prob);
152 }
153
160 constexpr explicit PrimalDualIPM(
161 const Problem& prob,
163 res.iterate = iterate;
164 solve(prob, true);
165 }
166
174 constexpr explicit PrimalDualIPM(
175 const Problem& prob,
178 : config(config_in) {
179 res.iterate = iterate;
180 solve(prob, true);
181 }
182
186#ifdef AKREON_REPORT_OUTPUT
187 SolverTrace trace{};
188#endif
189 bool dual_feas{false};
190 bool primal_feas{false};
191 bool gap_min{false};
192
193private:
202 constexpr void solve(const Problem& prob, bool warm_start = false) {
203 const auto c = prob.linear_objective();
204 const auto g = prob.constraint_map().translation();
205 const auto F = prob.constraint_map().linear_part();
206 const auto A = prob.equality_map().linear_part();
207 const auto b = prob.equality_map().translation();
208 const auto K = prob.constraint_cones();
209 res.canonical_trace = prob.canonical_trace();
210
211 AK_ASSERT(prob.validate() && "submitted problem is not valid!");
212 if (!prob.validate()) return;
213
214 auto& z = res.iterate.z;
215 auto& x = res.iterate.x;
216 auto& y = res.iterate.y;
217 auto& nu = res.iterate.nu;
218 auto& rp = res.rp;
219 auto& rd = res.rd;
220 auto& req = res.req;
221 auto& tag = res.tag;
222 auto& i = res.num_iter;
223
224 PredictorCorrector<NumCones, decltype(F), decltype(A), Var, ConeVar> pc;
225
226 const auto barrier_param = seed_prob(K, x, y, warm_start);
227
228 for (i = 0; i < config.steps && tag != config::SolverResult::OPTIMAL &&
229 !res.stalled;
230 ++i) {
231#ifdef AKREON_REPORT_OUTPUT
232 auto& iter_trace = trace.current();
233 iter_trace.reset();
234#endif
235
237 const auto [M_blk, block_event] =
238 accumulate_block(BlockMat{}, F, K, x, y);
239 if (linalg::is_fatal_event(block_event)) {
240 res.event = block_event;
241 res.stalled = true;
242 return;
243 }
244 const auto M_ldl =
245 linalg::LDL(regularize(M_blk, A, config.reg), config.schur);
246 const auto [eq_opt, coupling_event] = make_equality_coupling(M_ldl, A);
247 if (linalg::is_fatal_event(coupling_event) || !eq_opt.has_value()) {
248 res.event = coupling_event;
249 res.stalled = true;
250 return;
251 }
252 const auto& eq = *eq_opt;
253
255 auto residuals = compute_residuals(F, A, g, c, b, z, nu, x, y);
256 rd = residuals.rd;
257 rp = residuals.rp;
258 req = residuals.req;
259 auto rd_norm = residuals.rd_norm;
260 auto rp_norm = residuals.rp_norm;
261 auto req_norm = residuals.req_norm;
262
264 const auto [pred_rhs, pred_step, pred_event] =
265 pc.predictor(Var{}, K, M_ldl, eq, F, rp, rd, req, x, y
266#ifdef AKREON_REPORT_OUTPUT
267 ,
269#endif
270 );
271 static_cast<void>(pred_rhs);
272 if (linalg::is_fatal_event(pred_event)) {
273 res.event = pred_event;
274 res.stalled = true;
275 return;
276 }
277
279 const auto [corr_rhs, corr_step, corr_event] = pc.corrector(
280 Var{}, pred_step.px, pred_step.py, K, M_ldl, eq, F, rp, rd, req, x, y
281#ifdef AKREON_REPORT_OUTPUT
282 ,
284#endif
285 );
286 static_cast<void>(corr_rhs);
287 if (linalg::is_fatal_event(corr_event)) {
288 res.event = corr_event;
289 res.stalled = true;
290 return;
291 }
292
294 const auto step = blend_steps(pred_step, corr_step);
295
297 auto alpha_xy = alpha_max(T(1), T(1), K, x, y, step.px, step.py);
298
300 const auto accepted =
301 backtrack_iterates(alpha_xy, K, F, A, g, c, b, step.pz, step.pnu,
302 step.px, step.py, z, nu, x, y);
303 res.stalled = accepted ? 0 : 1;
304
305 res.mu = mu(x, y, barrier_param);
306#ifdef AKREON_REPORT_OUTPUT
307 iter_trace.mu = res.mu;
308 iter_trace.rd_norm = rd_norm;
309 iter_trace.rp_norm = rp_norm;
310 iter_trace.req_norm = req_norm;
311 iter_trace.alpha_x = std::get<0>(alpha_xy);
312 iter_trace.alpha_y = std::get<1>(alpha_xy);
313 iter_trace.pz_norm = linalg::L2<decltype(step.pz)>()(step.pz);
314 iter_trace.pnu_norm = linalg::L2<decltype(step.pnu)>()(step.pnu);
315 iter_trace.px_norm = linalg::L2<decltype(step.px)>()(step.px);
316 iter_trace.py_norm = linalg::L2<decltype(step.py)>()(step.py);
317#endif
319 residuals = compute_residuals(F, A, g, c, b, z, nu, x, y);
320 rd = residuals.rd;
321 rp = residuals.rp;
322 req = residuals.req;
323 rd_norm = residuals.rd_norm;
324 rp_norm = residuals.rp_norm;
325 req_norm = residuals.req_norm;
326
327 dual_feas = rd_norm <= config.dual_feas_tol;
328 primal_feas = rp_norm <= config.primal_feas_tol;
329 gap_min = res.mu >= T(0) && res.mu <= config.duality_gap_tol;
330
331 tag = get_tag();
332#ifdef AKREON_REPORT_OUTPUT
333 iter_trace.tag = tag;
334 iter_trace.event = res.event;
335 iter_trace.stalled = res.stalled;
336 trace.commit_iter();
337 res.solver_trace = trace;
338#endif
339 }
340
341 res.convex_obj = linalg::inner_product(c, z);
342 res.dual_obj = -linalg::inner_product(g, y);
343 }
344
367 template <std::size_t idx = 0, std::size_t Start = 0>
368 constexpr auto accumulate_block(BlockMat&& M_blk, const ConeMat& F,
369 const auto& K, const ConeVar& x,
370 const ConeVar& y) const {
371 if constexpr (idx >= NumCones)
372 return std::tuple(std::move(M_blk), linalg::NumericalEvent::NONE);
373 else {
374 const auto Ki = std::get<idx>(K);
375 using Slack = conic::cone_slack_t<decltype(Ki)>;
376
377 constexpr std::size_t KiRows = linalg::vector_dim_v<Slack>;
378 constexpr std::size_t End = Start + KiRows;
379
380 using ConePoint = linalg::Vector<T, End - Start>;
381 ConePoint xk{}, yk{};
382
383 for (std::size_t i = Start, j = 0; i < End; ++i, ++j)
384 xk(j) = x(i), yk(j) = y(i);
385
386 const auto [P, event] = Ki.H(xk, yk);
387 if (linalg::is_fatal_event(event))
388 return std::tuple(std::move(M_blk), event);
389
390 const auto F_blk = linalg::get_block<Start, End, 0, N>(F);
391 const auto Mi = linalg::transpose(F_blk) * P.apply(F_blk);
392 M_blk = M_blk + Mi;
393
394 return accumulate_block<idx + 1, End>(std::move(M_blk), F, K, x, y);
395 }
396 }
397
411 template <std::size_t idx = 0, std::size_t Start = 0>
412 constexpr T seed_prob(const auto& K, ConeVar& x, ConeVar& y,
413 bool warm_start) const {
414 if constexpr (idx >= NumCones) return T(0);
415 else {
416 const auto Ki = std::get<idx>(K);
417 using Slack = conic::cone_slack_t<decltype(Ki)>;
418
419 constexpr std::size_t KiRows = linalg::vector_dim_v<Slack>;
420 constexpr std::size_t End = Start + KiRows;
421
422 if (warm_start) {
423 using ConePoint = linalg::Vector<T, End - Start>;
424 ConePoint xk{}, yk{};
425 for (std::size_t i = Start, j = 0; i < End; ++i, ++j)
426 xk(j) = x(i), yk(j) = y(i);
427
428 if (Ki.in_cone(xk) && Ki.in_dual_cone(yk))
429 return Ki.nu() + seed_prob<idx + 1, End>(K, x, y, warm_start);
430 }
431
432 const auto x0 = Ki.x0(), y0 = Ki.y0();
433 for (std::size_t i = Start, j = 0; i < End; ++i, ++j)
434 x(i) = x0(j), y(i) = y0(j);
435
436 return Ki.nu() + seed_prob<idx + 1, End>(K, x, y, warm_start);
437 }
438 }
439
455 template <std::size_t idx = 0, std::size_t Start = 0>
456 constexpr auto alpha_max(T&& alpha_x, T&& alpha_y, const auto& K,
457 const ConeVar& x, const ConeVar& y,
458 const ConeVar& px, const ConeVar& py) const {
459 if constexpr (idx >= NumCones) {
460 const auto ax = std::max(T(0), std::min(T(1), alpha_x));
461 const auto ay = std::max(T(0), std::min(T(1), alpha_y));
462 return std::tuple(ax, ay);
463 } else {
464 const auto Ki = std::get<idx>(K);
465 using Slack = conic::cone_slack_t<decltype(Ki)>;
466 constexpr std::size_t KiRows = linalg::vector_dim_v<Slack>;
467 constexpr std::size_t End = Start + KiRows;
468
469 using ConePoint = linalg::Vector<T, End - Start>;
470 ConePoint xk{}, yk{}, pxk{}, pyk{};
471
472 for (std::size_t i = Start, j = 0; i < End; ++i, ++j)
473 xk(j) = x(i), yk(j) = y(i), pxk(j) = px(i), pyk(j) = py(i);
474
475 alpha_x = std::min(Ki.alpha_max(xk, pxk), alpha_x);
476 alpha_y = std::min(Ki.alpha_max_dual(yk, pyk), alpha_y);
477
478 return alpha_max<idx + 1, End>(std::move(alpha_x), std::move(alpha_y), K,
479 x, y, px, py);
480 }
481 }
482
494 template <std::size_t idx = 0, std::size_t Start = 0>
495 constexpr bool check_functional_proximity(const auto& K, const ConeVar& x,
496 const ConeVar& y) const {
497 if constexpr (idx >= NumCones) {
498 return true;
499 } else {
500 const auto Ki = std::get<idx>(K);
501 using Slack = conic::cone_slack_t<decltype(Ki)>;
502 constexpr std::size_t KiRows = linalg::vector_dim_v<Slack>;
503 constexpr std::size_t End = Start + KiRows;
504
505 using ConePoint = linalg::Vector<T, End - Start>;
506 ConePoint xk{}, yk{};
507 for (std::size_t i = Start, j = 0; i < End; ++i, ++j)
508 xk(j) = x(i), yk(j) = y(i);
509
510 const auto beta_limit = beta + config.Delta;
511 const auto [gamma_F, event] = Ki.gamma_F(xk, yk);
512 bool rv = Ki.in_cone(xk) && Ki.in_dual_cone(yk) &&
513 !linalg::is_fatal_event(event) && gamma_F < beta_limit;
515 }
516 }
517
539 constexpr bool backtrack_iterates(std::tuple<T, T>& alpha_xy, const auto& K,
540 const ConeMat& F, const EqMat& A,
541 const ConeVar& g, const Var& c,
542 const NuVar& b, const Var& pz,
543 const NuVar& pnu, const ConeVar& px,
544 const ConeVar& py, Var& z, NuVar& nu,
545 ConeVar& x, ConeVar& y) {
546 T& ax = std::get<0>(alpha_xy);
547 T& ay = std::get<1>(alpha_xy);
548 ax = std::max(T(0), ax);
549 ay = std::max(T(0), ay);
550
551 const auto curr = compute_residuals(F, A, g, c, b, z, nu, x, y);
552
553 constexpr auto zeta{1.05};
554
555 for (std::size_t i = 0; i < config.backtrack_steps; ++i) {
556 const auto z_tmp = z + ax * pz;
557 const auto nu_tmp = nu + ax * pnu;
558 const auto x_tmp = x + ax * px;
559 const auto y_tmp = y + ay * py;
560
561 const auto tmp =
562 compute_residuals(F, A, g, c, b, z_tmp, nu_tmp, x_tmp, y_tmp);
563
564 const bool inflation_check = tmp.rd_norm <= zeta * curr.rd_norm &&
565 tmp.rp_norm <= zeta * curr.rp_norm &&
566 tmp.req_norm <= zeta * curr.req_norm;
567
568 if (check_functional_proximity(K, x_tmp, y_tmp) && inflation_check) {
569 x = x_tmp;
570 y = y_tmp;
571 z = z_tmp;
572 nu = nu_tmp;
573 return true;
574 }
575
576 ax *= rho;
577 ay *= rho;
578 }
579 return false;
580 }
581
595 constexpr auto dual_residual(const ConeMat& F, const EqMat& A, const Var& c,
596 const NuVar& nu, const ConeVar& y) const {
597 if constexpr (EqRows == 0) return linalg::transpose(F) * y - c;
598 else return linalg::transpose(F) * y - linalg::transpose(A) * nu - c;
599 }
600
613 constexpr auto primal_residual(const ConeMat& F, const ConeVar& g,
614 const Var& z, const ConeVar& x) const {
615 return F * z + g - x;
616 }
617
629 constexpr auto equality_residual(const EqMat& A, const NuVar& b,
630 const Var& z) const {
631 if constexpr (EqRows == 0) return NuVar{};
632 else return b - A * z;
633 }
634
648 constexpr Residuals compute_residuals(const ConeMat& F, const EqMat& A,
649 const ConeVar& g, const Var& c,
650 const NuVar& b, const Var& z,
651 const NuVar& nu, const ConeVar& x,
652 const ConeVar& y) const {
654 residuals.rd = dual_residual(F, A, c, nu, y);
655 residuals.rp = primal_residual(F, g, z, x);
656 residuals.req = equality_residual(A, b, z);
657 residuals.rd_norm = linalg::L2<Var>()(residuals.rd);
658 residuals.rp_norm = linalg::L2<ConeVar>()(residuals.rp);
659 residuals.req_norm = linalg::L2<NuVar>()(residuals.req);
660 return residuals;
661 }
662
671 constexpr auto make_equality_coupling(const auto& M_ldl,
672 const EqMat& A) const {
673 const auto& schur_config = config.schur;
674
675 if constexpr (EqRows == 0) {
677 return std::tuple(std::optional<Coupling>{Coupling{}},
678 linalg::NumericalEvent::NONE);
679 } else if constexpr (EqRows == 1) {
680 const auto [M_inv_AT, event] = M_ldl.apply(linalg::transpose(A));
682 if (linalg::is_fatal_event(event))
683 return std::tuple(std::optional<Coupling>{}, event);
684
685 const auto S = A * M_inv_AT;
686
687 if (S(0, 0) < schur_config.tol)
688 return std::tuple(std::optional<Coupling>{},
689 linalg::NumericalEvent::FACTORIZATION_FAILED);
690
691 return std::tuple(std::optional<Coupling>{Coupling(A, S(0, 0))},
692 linalg::NumericalEvent::NONE);
693 } else {
694 const auto [M_inv_AT, event] = M_ldl.apply(linalg::transpose(A));
695 using SchurFactor = decltype(linalg::LDL(A * M_inv_AT));
697 if (linalg::is_fatal_event(event))
698 return std::tuple(std::optional<Coupling>{}, event);
699
700 const auto S = A * M_inv_AT;
701 const auto S_ldl = linalg::LDL(S, schur_config);
702 if (!S_ldl.is_valid())
703 return std::tuple(std::optional<Coupling>{},
704 linalg::NumericalEvent::FACTORIZATION_FAILED);
705
706 return std::tuple(std::optional<Coupling>{Coupling(A, S_ldl)},
707 linalg::NumericalEvent::NONE);
708 }
709 }
710
719 constexpr Step blend_steps(const Step& pred_step,
720 const Step& corr_step) const {
721 Step blended{};
722 blended.px = rho * pred_step.px + (T(1) - rho) * corr_step.px;
723 blended.py = rho * pred_step.py + (T(1) - rho) * corr_step.py;
724 blended.pz = rho * pred_step.pz + (T(1) - rho) * corr_step.pz;
725 blended.pnu = rho * pred_step.pnu + (T(1) - rho) * corr_step.pnu;
726 return blended;
727 }
728
734 constexpr auto get_tag() const {
736 if (primal_feas && dual_feas && gap_min)
738 else if (primal_feas && dual_feas)
746 return tag;
747 }
748
751};
752
753} // namespace convex
SolverResult
SolverResult.
typename cone_traits< linalg::remove_cvref_t< Cone > >::SlackT cone_slack_t
alias for slack type in cone
typename builder_traits< linalg::remove_cvref_t< Builder > >::Dom builder_dom_t
builder domain
constexpr std::size_t dom_size_v
constexpr auto regularize(Mat M, const EqMat &A, const config::RegularizationConfig< linalg::matrix_scalar_t< EqMat > > &reg)
regularize
Definition pd_ipm.hpp:50
constexpr auto mu(const ConeVar &x, const ConeVar &y, const linalg::vector_element_t< ConeVar > &nu)
mu - normalized duality gap
Definition pd_ipm.hpp:31
typename prob_traits< linalg::remove_cvref_t< Prob > >::BuilderT prob_builder_t
builder type from prob
RegularizationConfig.
Empty solver trace for users/builds that only want the final result.
Newton step bundle for the primal-dual interior-point iterate.
Fixed-capacity PD-IPM trace.
Predictor Corrector.
Definition pred_corr.hpp:25
Primal Dual Interior Point Method Solver.
Definition pd_ipm.hpp:93
static constexpr std::size_t EqRows
Definition pd_ipm.hpp:111
static constexpr std::size_t NumCones
helpers for size of cones & F mat size & A mat rows
Definition pd_ipm.hpp:106
bool dual_feas
status flag for dual feasibility
Definition pd_ipm.hpp:189
static constexpr T beta
beta F(beta) neighborhood of central path (within feasibility)
Definition pd_ipm.hpp:133
constexpr PrimalDualIPM(const Problem &prob)
CTOR.
Definition pd_ipm.hpp:140
constexpr PrimalDualIPM(const Problem &prob, const config::PrimalDualIPMConfig< T > &config_in)
CTOR.
Definition pd_ipm.hpp:148
NewtonStep< Var, ConeVar, NuVar > Step
Definition pd_ipm.hpp:118
static constexpr std::size_t N
Definition pd_ipm.hpp:109
decltype(std::declval< const Problem & >().equality_map()) EqMap
Definition pd_ipm.hpp:99
PrimalDualResult< Var, ConeVar, NuVar, CanonicalTrace, SolverTrace > res
member vars
Definition pd_ipm.hpp:185
constexpr PrimalDualIPM(const Problem &prob, const PrimalDualIterate< Var, ConeVar, NuVar > &iterate, const config::PrimalDualIPMConfig< T > &config_in)
CTOR (warm start)
Definition pd_ipm.hpp:174
static constexpr std::size_t M
Definition pd_ipm.hpp:108
affine::affine_mat_t< EqMap > EqMat
Definition pd_ipm.hpp:100
bool primal_feas
status flag for primal feasibility
Definition pd_ipm.hpp:190
prob_builder_t< Problem > Builder
alias and helpers to get types
Definition pd_ipm.hpp:95
decltype(std::declval< const Problem & >().constraint_map()) ConeMap
Definition pd_ipm.hpp:97
static constexpr T rho
damping param using for alpha steps in predictor corrector
Definition pd_ipm.hpp:130
linalg::Vector< T, M > ConeVar
Definition pd_ipm.hpp:117
linalg::Vector< T, N > Var
Definition pd_ipm.hpp:116
linalg::space_type_t< DecVar > T
Definition pd_ipm.hpp:103
constexpr PrimalDualIPM(const Problem &prob, const PrimalDualIterate< Var, ConeVar, NuVar > &iterate)
CTOR (warm start)
Definition pd_ipm.hpp:160
affine::affine_mat_t< ConeMap > ConeMat
Definition pd_ipm.hpp:98
conic::builder_dom_t< Builder > DecVar
Definition pd_ipm.hpp:96
linalg::Vector< T, EqRows > NuVar
Definition pd_ipm.hpp:112
bool gap_min
status flag for duality gap minimized
Definition pd_ipm.hpp:191
ResidualState< Var, ConeVar, NuVar > Residuals
Definition pd_ipm.hpp:119
decltype(std::declval< const Problem & >().canonical_trace()) CanonicalTrace
Definition pd_ipm.hpp:102
linalg::Matrix< T, N, N > BlockMat
useful types for PD-IPM
Definition pd_ipm.hpp:115
PrimalDualIterate.
Definition pd_info.hpp:17
Final primal-dual solve result plus optional structural and iteration traces.
Residual state bundle for a PD-IPM iterate.