akreon-convex
Typed convex modeling, conic canonicalization, and primal-dual optimization in C++26.
Loading...
Searching...
No Matches
canonical_form_builder.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
8#include <algorithm>
9#include "cone_block.hpp"
10
11#include "akreon/linalg/affine/affine_map.hpp"
12#include "akreon/linalg/assert.hpp"
16
17namespace conic {
18
26template <linalg::RnSpace DecVarDom, affine::AffineMapLike CoordMap,
27 conic::ConeBlockLike CB, affine::AffineMapLike EqMap,
28 typename CanonicalTrace = convex::EmptyCanonicalTrace>
31 using CoordMapT = affine::affine_mat_t<CoordMap>;
33 using T = linalg::matrix_scalar_t<CoordMapT>;
34
36 static constexpr std::size_t N = linalg::space_dim_v<DecVarDom>;
37
45 constexpr explicit CanonicalFormBuilder(const CoordMap& coord_in,
46 const CB& cb_in, const EqMap& eq_in,
47 const CanonicalTrace& trace_in = {})
48 : coord_map(coord_in), cb(cb_in), eq_map(eq_in),
49 canonical_trace(trace_in) {}
50
64 template <std::size_t SharedCols, affine::AffineMapLike LiftedMap,
65 typename... Cs>
66 constexpr auto make(int idx, const LiftedMap& lift_map,
67 const std::tuple<Cs...>& cones) const {
68 const auto new_map = this->template update_map<SharedCols>();
69 const auto new_eq_map = this->template update_eq_map<SharedCols>();
70 static constexpr std::size_t FullDom =
71 linalg::mat_cols_v<decltype(new_map.linear_part())>;
72 constexpr std::size_t FreshCols =
73 linalg::mat_cols_v<CoordMapT> - SharedCols;
74 static_assert(FreshCols > 0, "Lifted emission must append an auxiliary");
75
76 linalg::Vector<T, FullDom> sel{};
77 linalg::Vector<T, 1> bias{lift_map.translation()(0)};
78
79 AK_ASSERT(idx < 0 && static_cast<std::size_t>(-idx) <= FreshCols &&
80 "Aux variable selector must select a fresh column in "
81 "CanonicalFormBuilder::make");
82
83 const std::size_t selector_idx = FullDom - static_cast<std::size_t>(-idx);
84 sel(selector_idx) = lift_map.linear_part()(0, 0);
85 const linalg::RowVector<T, FullDom> sel_row = linalg::as_row_vector(sel);
86 const auto new_coord_map =
87 affine::AffineMap<decltype(sel_row)>(sel_row, bias);
88 const auto new_cb =
89 conic::ConeBlock(new_map, std::tuple_cat(cb.cones, cones));
90 return make_builder<DecVarDom>(new_coord_map, new_cb, new_eq_map,
92 }
93
101 template <std::size_t SharedCols, typename... Cs>
102 constexpr auto make(const std::tuple<Cs...>& cones) const {
103 const auto new_map = this->template update_map<SharedCols>();
104 const auto new_eq_map = this->template update_eq_map<SharedCols>();
105 const auto new_cb =
106 conic::ConeBlock(new_map, std::tuple_cat(cb.cones, cones));
107 return make_builder<DecVarDom>(new_map, new_cb, new_eq_map,
109 }
110
122 template <convex::AtomType Type, affine::AffineMapLike LiftedMap,
123 LocalEmissionLike Emission>
124 constexpr auto realize(int idx, const LiftedMap& lift_map,
125 const Emission& emission) const {
126 // The child selector defines the shared prefix. AtomType affects tracing,
127 // never column ownership or placement.
128 constexpr std::size_t ChildCols = linalg::mat_cols_v<CoordMapT>;
129 const auto traced = add_cone_trace<Type, Emission>(emission);
130 const auto local_builder = make_builder<DecVarDom>(
131 emission.local_coord_map, cb, emission.local_eq_map, traced);
132 return local_builder.template make<ChildCols>(idx, lift_map,
133 emission.local_cones);
134 }
135
144 template <convex::AtomType Type, LocalEmissionLike Emission>
145 constexpr auto realize(const Emission& emission) const {
146 const auto traced = add_cone_trace<Type, Emission>(emission);
147 const auto local_builder = make_builder<DecVarDom>(
148 emission.local_coord_map, cb, emission.local_eq_map, traced);
149 constexpr std::size_t SharedCols =
150 std::max(N, linalg::mat_cols_v<CoordMapT>);
151 return local_builder.template make<SharedCols>(emission.local_cones);
152 }
153
165 template <LocalEqualityEmissionLike Emission>
166 constexpr auto realize(const Emission& emission) const {
167 using CurrentEqMat = affine::affine_mat_t<EqMap>;
168 using LocalEqMap = emission_local_eq_map_t<Emission>;
169 using LocalEqMat = affine::affine_mat_t<LocalEqMap>;
170
171 static_assert(std::same_as<T, linalg::matrix_scalar_t<LocalEqMat>>,
172 "type mismatch between equality emission and builder");
173
174 static constexpr std::size_t CurrentRows = linalg::mat_rows_v<CurrentEqMat>;
175 static constexpr std::size_t CurrentCols = linalg::mat_cols_v<CurrentEqMat>;
176 static constexpr std::size_t LocalRows = linalg::mat_rows_v<LocalEqMat>;
177 static constexpr std::size_t LocalCols = linalg::mat_cols_v<LocalEqMat>;
178 static constexpr std::size_t FullRows = CurrentRows + LocalRows;
179 static constexpr std::size_t FullCols =
180 std::max({N, CurrentCols, LocalCols, cone_block_map_cols_v<CB>,
181 linalg::mat_cols_v<CoordMapT>});
182
183 linalg::Matrix<T, FullRows, FullCols> A{};
184 linalg::Vector<T, FullRows> b{};
185
186 if constexpr (CurrentRows != 0 && CurrentCols != 0) {
187 A = linalg::set_block<0, CurrentRows, 0, CurrentCols>(
188 A, eq_map.linear_part());
189 for (std::size_t i = 0; i < CurrentRows; ++i)
190 b(i) = eq_map.translation()(i);
191 }
192
193 if constexpr (LocalRows != 0 && LocalCols != 0) {
194 A = linalg::set_block<CurrentRows, FullRows, 0, LocalCols>(
195 A, emission.local_eq_map.linear_part());
196 for (std::size_t i = 0; i < LocalRows; ++i)
197 b(i + CurrentRows) = emission.local_eq_map.translation()(i);
198 }
199
200 const auto new_eq_map = affine::AffineMap<decltype(A)>(A, b);
201 const auto traced = add_equality_trace(emission);
202 return make_builder<DecVarDom>(coord_map, cb, new_eq_map, traced);
203 }
204
206 CoordMap coord_map;
208 CB cb;
210 EqMap eq_map;
211 CanonicalTrace canonical_trace;
212
213private:
221 template <convex::AtomType Type, LocalEmissionLike Emission>
222 constexpr auto add_cone_trace(const Emission& /*emission*/) const {
223 using ConeTuple = emission_cones_t<Emission>;
224 using EqMapT = affine::affine_mat_t<EqMap>;
225
226 // The builder owns global offsets: atom emissions only describe local
227 // cones/equalities, while the builder knows how many rows were realized
228 // before this emission.
229 constexpr std::size_t NodeId =
231 constexpr std::size_t GlobalConeStart = conic::cone_block_num_cones_v<CB>;
232 constexpr std::size_t ConeOffsetStart = conic::cone_block_map_rows_v<CB>;
233 constexpr std::size_t LocalEqRows = emission_local_eq_rows_v<Emission>;
234 constexpr std::size_t EqOffsetStart = linalg::mat_rows_v<EqMapT>;
235
236 const auto nodes = std::make_tuple(convex::CanonicalNodeRecord{
237 NodeId, convex::canonical_no_parent, Type, 0});
239 NodeId, GlobalConeStart, ConeOffsetStart);
240
241 if constexpr (LocalEqRows == 0) {
243 std::make_tuple());
244 } else {
245 const auto equalities = std::make_tuple(convex::CanonicalEqualityRecord{
246 NodeId, 0, EqOffsetStart, LocalEqRows});
248 equalities);
249 }
250 }
251
259 template <LocalEqualityEmissionLike Emission>
260 constexpr auto add_equality_trace(const Emission& /*emission*/) const {
261 using EqMapT = affine::affine_mat_t<EqMap>;
262
263 constexpr std::size_t NodeId =
265 constexpr std::size_t LocalEqRows = emission_local_eq_rows_v<Emission>;
266 constexpr std::size_t EqOffsetStart = linalg::mat_rows_v<EqMapT>;
267
268 const auto nodes = std::make_tuple(convex::CanonicalNodeRecord{
270
271 if constexpr (LocalEqRows == 0) {
273 canonical_trace, nodes, std::make_tuple(), std::make_tuple());
274 } else {
275 const auto equalities = std::make_tuple(convex::CanonicalEqualityRecord{
276 NodeId, 0, EqOffsetStart, LocalEqRows});
278 std::make_tuple(), equalities);
279 }
280 }
281
300 template <std::size_t SharedCols>
301 struct ColumnLayout {
302 static constexpr std::size_t CoordCols = linalg::mat_cols_v<CoordMapT>;
303
305 constexpr ColumnLayout()
306 : prefix_cols(SharedCols),
307 auxiliary_start(
308 std::max({SharedCols, cone_block_map_cols_v<CB>,
309 linalg::mat_cols_v<affine::affine_mat_t<EqMap>>})),
310 full_cols(auxiliary_start + CoordCols - SharedCols) {
311 static_assert(SharedCols >= N && CoordCols >= SharedCols,
312 "Emission must preserve the decision/shared prefix");
313 }
314
315 const std::size_t
316 prefix_cols;
317 const std::size_t
318 auxiliary_start;
319 const std::size_t full_cols;
321 };
322
332 template <std::size_t SharedCols>
333 constexpr auto update_map() const {
334 const auto& cone_map = cb.map;
335 using ConeMapT = affine::affine_mat_t<decltype(cone_map)>;
336
337 static_assert(std::same_as<T, linalg::matrix_scalar_t<ConeMapT>>,
338 "type mismatch between cone map and coord map");
339
340 static constexpr std::size_t CoordDom = linalg::mat_cols_v<CoordMapT>;
341 static constexpr std::size_t CoordCodom = linalg::mat_rows_v<CoordMapT>;
342
343 static constexpr std::size_t ConeDom = linalg::mat_cols_v<ConeMapT>;
344 static constexpr std::size_t ConeCodom = linalg::mat_rows_v<ConeMapT>;
345 constexpr ColumnLayout<SharedCols> layout{};
346 constexpr std::size_t PrefixCols = layout.prefix_cols;
347 constexpr std::size_t CoordAux = CoordDom - PrefixCols;
348 constexpr std::size_t FullDom = layout.full_cols;
349 constexpr std::size_t FullCodom = ConeCodom + CoordCodom;
350 constexpr std::size_t CoordAuxStartCol = layout.auxiliary_start;
351 constexpr std::size_t CoordAuxEndCol = CoordAuxStartCol + CoordAux;
352
353 linalg::Matrix<T, FullCodom, FullDom> F{};
354 linalg::Vector<T, FullCodom> g{};
355
357 if constexpr (ConeDom != 0) {
358 F = linalg::set_block<0, ConeCodom, 0, ConeDom>(F,
359 cone_map.linear_part());
360 for (std::size_t i = 0; i < ConeCodom; ++i)
361 g(i) = cone_map.translation()(i);
362 }
363
365 const auto A = coord_map.linear_part();
366 const auto dec_spanning_blk =
367 linalg::get_block<0, CoordCodom, 0, PrefixCols>(A);
368 F = linalg::set_block<ConeCodom, FullCodom, 0, PrefixCols>(
369 F, dec_spanning_blk);
370
372 if constexpr (CoordAux > 0) {
373 static_assert(CoordAuxEndCol > CoordAuxStartCol);
374 const auto coord_aux_blk =
375 linalg::get_block<0, CoordCodom, PrefixCols, CoordDom>(A);
376 F = linalg::set_block<ConeCodom, FullCodom, CoordAuxStartCol,
377 CoordAuxEndCol>(F, coord_aux_blk);
378 }
379
380 for (std::size_t i = 0; i < CoordCodom; ++i)
381 g(i + ConeCodom) = coord_map.translation()(i);
382
383 return affine::AffineMap<decltype(F)>(F, g);
384 }
385
394 template <std::size_t SharedCols>
395 constexpr auto update_eq_map() const {
396 using EqMapT = affine::affine_mat_t<EqMap>;
397
398 static_assert(std::same_as<T, linalg::matrix_scalar_t<EqMapT>>,
399 "type mismatch between equality map and coord map");
400
401 static constexpr std::size_t EqDom = linalg::mat_cols_v<EqMapT>;
402 static constexpr std::size_t EqCodom = linalg::mat_rows_v<EqMapT>;
403 constexpr ColumnLayout<SharedCols> layout{};
404 constexpr std::size_t FullDom = layout.full_cols;
405
406 linalg::Matrix<T, EqCodom, FullDom> A{};
407 linalg::Vector<T, EqCodom> b{};
408
409 if constexpr (EqCodom != 0 && EqDom != 0) {
410 A = linalg::set_block<0, EqCodom, 0, EqDom>(A, eq_map.linear_part());
411 for (std::size_t i = 0; i < EqCodom; ++i) b(i) = eq_map.translation()(i);
412 }
413
414 // Equality coefficients always reference existing global columns.
415 // Fresh cone auxiliaries extend their width without rebasing any entries.
416 return affine::AffineMap<decltype(A)>(A, b);
417 }
418};
419
431template <linalg::RnSpace DecVarDom, affine::AffineMapLike CoordMap,
432 conic::ConeBlockLike CB, affine::AffineMapLike EqMap,
433 typename CanonicalTrace = convex::EmptyCanonicalTrace>
434constexpr auto make_builder(const CoordMap& coord_map, const CB& cb,
435 const EqMap& eq_map,
436 const CanonicalTrace& canonical_trace = {}) {
437 return CanonicalFormBuilder<DecVarDom, CoordMap, CB, EqMap, CanonicalTrace>(
438 coord_map, cb, eq_map, canonical_trace);
439}
440
447template <linalg::Field T>
448constexpr auto create_empty_cblk() {
449 constexpr auto empty = std::make_tuple();
450 constexpr conic::ConeBlock cb(
451 affine::AffineMap(linalg::Matrix<T, 0, 0>{}, linalg::Vector<T, 0>{}),
452 empty);
453 return cb;
454}
455
462template <linalg::Field T>
463constexpr auto create_empty_eq_map() {
464 return affine::AffineMap(linalg::Matrix<T, 0, 0>{}, linalg::Vector<T, 0>{});
465}
466
473template <linalg::RnSpace DecVarDom>
475 using T = linalg::space_type_t<DecVarDom>;
476 const auto cblk = create_empty_cblk<T>();
477 const auto eq_map = create_empty_eq_map<T>();
478 return make_builder<DecVarDom>(cblk.map, cblk, eq_map);
479}
480
481} // namespace conic
concept for cone block like used in canonical form
Concept for fixed-size atom-local canonical emission bundles.
typename emission_traits< linalg::remove_cvref_t< Emission > >::Cones emission_cones_t
constexpr auto create_empty_cblk()
Create an empty cone block.
constexpr auto create_empty_cblk_builder()
Create an empty canonical-form builder.
constexpr auto create_empty_eq_map()
Create an empty equality affine map.
constexpr auto make_builder(const CoordMap &coord_map, const CB &cb, const EqMap &eq_map, const CanonicalTrace &canonical_trace={})
Construct a canonical-form builder while deducing all realized component types (aside from dec var do...
typename emission_traits< linalg::remove_cvref_t< Emission > >::LocalEqualityMap emission_local_eq_map_t
constexpr std::size_t dom_size_v
AtomType
Atom category used by canonicalization and DCP checks.
constexpr auto extend_canonical_trace(const Trace &trace, const NodeRecords &nodes, const ConeRecords &cones, const EqualityRecords &equalities)
Extends canonical trace by tuple concatenation.
constexpr std::size_t canonical_no_parent
Sentinel used when a canonical node does not have a parent node.
Builder for conic canonical forms.
linalg::matrix_scalar_t< CoordMapT > T
Scalar field used by the canonical form.
constexpr auto make(const std::tuple< Cs... > &cones) const
make new builder based on tuple of cones (no input map or selector)
CB cb
Realized cone block containing cone tuple and affine rows.
CoordMap coord_map
public member vars
constexpr auto make(int idx, const LiftedMap &lift_map, const std::tuple< Cs... > &cones) const
make new builder with tuple of cones and input cone lift map selecting upon input index
constexpr auto realize(const Emission &emission) const
Realize atom-local equality rows.
EqMap eq_map
Realized equality map in the same column layout.
constexpr auto realize(int idx, const LiftedMap &lift_map, const Emission &emission) const
Realize an atom-local emission and select a lifted coordinate based on input cone lift map.
affine::affine_mat_t< CoordMap > CoordMapT
Affine matrix type for the current cone-coordinate selector.
static constexpr std::size_t N
Number of original decision variables before auxiliary columns.
constexpr CanonicalFormBuilder(const CoordMap &coord_in, const CB &cb_in, const EqMap &eq_in, const CanonicalTrace &trace_in={})
Construct a builder from existing canonical-form components.
constexpr auto realize(const Emission &emission) const
Realize cone rows using the child's shared prefix and select the assembled map (no lifted coordinate ...
CanonicalTrace canonical_trace
Realized structural canonical trace.
Cone block.
Equality-row slice emitted by one canonical trace node.
Structural atom emitted during canonicalization.
Compile-time canonicalization trace.