Loading...
Searching...
No Matches
CglCutCallback.h
1//
2// Created by Henri on 25/03/2026.
3//
4
5#ifndef IDOL_CGLCUTS_H
6#define IDOL_CGLCUTS_H
7
8#include "idol/mixed-integer/optimizers/branch-and-bound/callbacks/BranchAndBoundCallbackFactory.h"
9#include "idol/mixed-integer/optimizers/branch-and-bound/callbacks/BranchAndBoundCallback.h"
10#include "idol/mixed-integer/optimizers/branch-and-bound/nodes/DefaultNodeInfo.h"
11#include "idol/mixed-integer/optimizers/callbacks/cutting-planes/CutFamily.h"
12
13#ifdef IDOL_USE_CGL
14#include <CglKnapsackCover.hpp>
15#include <CglFlowCover.hpp>
16#include <CglZeroHalf.hpp>
17#include <CglMixedIntegerRounding.hpp>
18#include <CglOddHole.hpp>
19#include <CglLandP.hpp>
20#include <CglResidualCapacity.hpp>
21#else
22class OsiRowCut;
23#endif
24
25namespace idol {
26 template<class> class CglCutCallback;
27}
28
29template<class NodeInfoT = idol::DefaultNodeInfo>
31public:
32 class Strategy : public BranchAndBoundCallback<NodeInfoT> {
33
34 class NodeCutContext {
35 const unsigned int m_node_id;
36 unsigned int m_pass_count = 0;
37 unsigned int m_n_accepted_cuts = 0;
38 public:
39 NodeCutContext(unsigned int node_id) : m_node_id(node_id) {}
40
41 [[nodiscard]] unsigned int node_id() const { return m_node_id; }
42 [[nodiscard]] bool is_root_node() const { return (m_node_id == 0); }
43 [[nodiscard]] unsigned int pass() const { return m_pass_count; }
44 [[nodiscard]] unsigned int n_added_cuts() const { return m_n_accepted_cuts; }
45
46 void increment_pass() { m_pass_count++; }
47 void add_accepted_cut() { m_n_accepted_cuts++; }
48 };
49
50 class OsiIdolCglSolverInterface;
51#ifdef IDOL_USE_CGL
52 std::unique_ptr<OsiIdolCglSolverInterface> m_osi_solver;
53#endif
54 std::vector<std::unique_ptr<CutFamily>> m_cut_families;
55 std::unique_ptr<NodeCutContext> m_cut_context;
56
57 unsigned int m_total_n_added_cuts = 0;
58 const unsigned int m_max_pass_at_aggressive_node = 20;
59 const unsigned int m_max_pass_per_regular_node = 1;
60 const unsigned int m_max_depth_for_cuts = 50;
61 const double m_effectiveness_threshold = 1e-3;
62 const unsigned int m_max_cut_at_aggressive_node = std::numeric_limits<unsigned int>::max();
63 const unsigned int m_max_cut_per_regular_node = std::numeric_limits<unsigned int>::max();
64 const unsigned int m_max_cuts = std::numeric_limits<unsigned int>::max();
65
66 std::list<TempCtr> to_idol_cuts(OsiCuts& t_cuts);
67 TempCtr to_idol_cut(OsiRowCut& t_cut);
68 std::vector<std::pair<TempCtr, double>> sort_and_filter_cuts_by_effectiveness(const std::list<TempCtr>& t_cuts);
69 void standard_scaling(std::list<TempCtr>& t_cuts);
70 protected:
71 NodeCutContext& get_cut_context();
72 const NodeCutContext& get_cut_context() const { return const_cast<Strategy*>(this)->get_cut_context(); }
73 void initialize() override;
74 void log_after_termination() override;
75 void operator()(CallbackEvent t_event) override;
76 };
77
78 Strategy* operator()() override;
79 [[nodiscard]] CglCutCallback* clone() const override;
80};
81
82template <class NodeInfoT>
83idol::CglCutCallback<NodeInfoT>::Strategy* idol::CglCutCallback<NodeInfoT>::operator()() {
84 return new Strategy();
85}
86
87template <class NodeInfoT>
88idol::CglCutCallback<NodeInfoT>* idol::CglCutCallback<NodeInfoT>::clone() const {
89 return new CglCutCallback<NodeInfoT>(*this);
90}
91
92#include "idol/mixed-integer/optimizers/callbacks/cutting-planes/OsiIdolCglSolverInterface.h"
93
94template <class NodeInfoT>
96#ifdef IDOL_USE_CGL
97
99
100 m_osi_solver = std::make_unique<OsiIdolCglSolverInterface>(*this);
101
102 m_cut_families.emplace_back(new CutFamily(new CglKnapsackCover(), "Cover"));
103 m_cut_families.emplace_back(new CutFamily(new CglFlowCover(), "Flow Cover"));
104 m_cut_families.emplace_back(new CutFamily(new CglZeroHalf(), "Zero Half"));
105 m_cut_families.emplace_back(new CutFamily(new CglMixedIntegerRounding(), "MIR"));
106 m_cut_families.emplace_back(new CutFamily(new CglResidualCapacity(), "Residual Capacity"));
107 //m_cut_families.emplace_back(new CutFamily(new CglLandP(), "Lift-and-Project")); // Needs getObjValue
108 //m_cut_families.emplace_back(new CutFamily(new CglOddHole(), "Odd Hole")); // Needs reduced costs
109
110#else
111 throw Exception("idol was not linked with Cgl.");
112#endif
113}
114
115template <class NodeInfoT>
116void idol::CglCutCallback<NodeInfoT>::Strategy::log_after_termination() {
117
118 BranchAndBoundCallback<NodeInfoT>::log_after_termination();
119
120 std::cout << "Cgl Cutting Planes:" << std::endl;
121
122 for (const auto& cut_family : m_cut_families) {
123 std::cout << '\t' << cut_family->name() << ": " << cut_family->n_accepted() << "\n";
124 }
125
126}
127
128template <class NodeInfoT>
130#ifdef IDOL_USE_CGL
131 if (t_event != InvalidSolution) {
132 return;
133 }
134
135 if (m_total_n_added_cuts > m_max_cuts) {
136 return;
137 }
138
139 if (this->node().level() > m_max_depth_for_cuts) {
140 return;
141 }
142
143 const double relative_gap = idol::relative_gap(this->best_bound(), this->best_obj()) * 100;
144
145 if (relative_gap < 1) {
146 return;
147 }
148
149 auto& cut_context = get_cut_context();
150 const bool is_root_node = cut_context.is_root_node();
151
152 bool be_aggressive = false;
153 if (is_root_node || relative_gap > 50) {
154 be_aggressive = true;
155 }
156
157 unsigned int max_n_added_cut_at_this_node = m_max_cut_per_regular_node;
158 unsigned int max_pass_at_this_node = m_max_pass_per_regular_node;
159 if (be_aggressive) {
160 max_n_added_cut_at_this_node = m_max_cut_at_aggressive_node;
161 max_pass_at_this_node = m_max_pass_at_aggressive_node;
162 }
163
164 if (cut_context.pass() > max_pass_at_this_node) {
165 return;
166 }
167
168 cut_context.increment_pass();
169
170 // Count number of fractional over integer ?
171 if (!be_aggressive) {
172 std::sort(m_cut_families.begin(), m_cut_families.end(), [](const auto& t_a, const auto& t_b) {
173 return t_a->score() > t_b->score();
174 });
175 }
176
177 m_osi_solver->update_current_solution();
178
179 auto& registry = this->side_effect_registry();
180 for (auto& cut_family : m_cut_families) {
181
182 if (!be_aggressive && cut_family->score() <= 1e-2) {
183 continue;
184 }
185
186 auto osi_cuts = cut_family->generate(*m_osi_solver, be_aggressive ? 100 : 0);
187 auto idol_cuts = to_idol_cuts(osi_cuts);
188 standard_scaling(idol_cuts);
189 auto sorted_cuts = sort_and_filter_cuts_by_effectiveness(idol_cuts);
190
191 for (auto& [cut, effectiveness] : sorted_cuts) {
192
193 if (m_total_n_added_cuts > m_max_cuts) {
194 return;
195 }
196
197 if (cut_context.n_added_cuts() > max_n_added_cut_at_this_node) {
198 return;
199 }
200
201 cut_family->add_effectiveness_statistics(effectiveness);
202
203 if (effectiveness <= m_effectiveness_threshold) {
204 continue;
205 }
206
207 const unsigned int old_n_added_user_cuts = registry.n_added_user_cuts;
208 this->add_user_cut(cut);
209 const bool cut_was_accepted_by_idol = registry.n_added_user_cuts > old_n_added_user_cuts;
210
211 if (cut_was_accepted_by_idol) {
212 m_total_n_added_cuts++;
213 cut_context.add_accepted_cut();
214 cut_family->add_accepted_cut();
215 }
216 }
217 }
218#endif
219}
220
221template <class NodeInfoT>
222std::list<idol::TempCtr> idol::CglCutCallback<NodeInfoT>::Strategy::to_idol_cuts(OsiCuts& t_cuts) {
223#ifdef IDOL_USE_CGL
224 std::list<TempCtr> result;
225
226 for (unsigned int k = 0, n = t_cuts.sizeRowCuts(); k < n; k++) {
227 auto& cut = *t_cuts.rowCutPtr(k);
228
229 if (!cut.consistent()) {
230 continue;
231 }
232
233 if (cut.infeasible(*m_osi_solver)) {
234 continue;
235 }
236
237 result.emplace_back(to_idol_cut(cut));
238 }
239
240 return result;
241#else
242 throw Exception("idol was not linked with Cgl.");
243#endif
244}
245
246template <class NodeInfoT>
247idol::TempCtr idol::CglCutCallback<NodeInfoT>::Strategy::to_idol_cut(OsiRowCut& t_cut) {
248#ifdef IDOL_USE_CGL
249 const auto& model = this->original_model();
250
251 TempCtr cut;
252
253 const auto osi_sense = t_cut.sense();
254 if (osi_sense == 'G') {
255 cut.set_type(GreaterOrEqual);
256 cut.set_rhs(t_cut.lb());
257 }
258 else if (osi_sense == 'L') {
259 cut.set_type(LessOrEqual);
260 cut.set_rhs(t_cut.ub());
261 }
262 else if (osi_sense == 'R') {
263 if (const double lb = t_cut.lb(); !is_neg_inf(lb)) {
264 cut.set_type(GreaterOrEqual);
265 cut.set_rhs(t_cut.lb());
266 }
267 else if (const double ub = t_cut.ub(); !is_pos_inf(ub)) {
268 cut.set_type(LessOrEqual);
269 cut.set_rhs(t_cut.ub());
270 }
271 else {
272 throw Exception("Cgl returned a cut of type R, which is not handled in idol.");
273 }
274 }
275 else {
276 throw Exception("Could not handle cut type from Cgl.");
277 }
278
279 const auto& osi_row = t_cut.row();
280 const auto* indices = osi_row.getIndices();
281 const auto* values = osi_row.getElements();
282
283 for (unsigned int k = 0, n = osi_row.getNumElements(); k < n; k++) {
284 const auto& var = model.get_var_by_index(indices[k]);
285 cut.lhs() += values[k] * var;
286 }
287
288 return cut;
289#else
290 throw Exception("idol was not linked with Cgl.");
291#endif
292}
293
294template <class NodeInfoT>
295std::vector<std::pair<idol::TempCtr, double>> idol::CglCutCallback<NodeInfoT>::Strategy::sort_and_filter_cuts_by_effectiveness(
296 const std::list<TempCtr>& t_cuts) {
297 std::vector<std::pair<TempCtr, double>> result;
298 const auto primal_solution = this->node().info().primal_solution();
299
300 for (auto& cut : t_cuts) {
301 double activity = 0.;
302 double norm = 0.;
303
304 for (const auto& [var, coefficient] : cut.lhs()) {
305 norm += coefficient * coefficient;
306 activity += coefficient * primal_solution.get(var);
307 }
308 norm = std::sqrt(norm);
309
310 double effectiveness = (activity - cut.rhs()) / norm;
311 if (cut.type() == GreaterOrEqual) {
312 effectiveness *= -1.;
313 }
314
315 result.emplace_back(std::move(cut), effectiveness);
316 }
317
318 std::sort(result.begin(), result.end(), [](const auto& t_a, const auto& t_b) {
319 return t_a.second > t_b.second;
320 });
321
322 return result;
323}
324
325template <class NodeInfoT>
326void idol::CglCutCallback<NodeInfoT>::Strategy::standard_scaling(std::list<TempCtr>& t_cuts) {
327
328 for (auto& cut : t_cuts) {
329
330 auto& row = cut.lhs();
331 double& rhs = cut.rhs();
332 double infinity_norm = 0;
333 for (const auto& [var, coeff] : row) {
334 infinity_norm = std::max(std::abs(coeff), infinity_norm);
335 }
336 infinity_norm = std::max(infinity_norm, std::abs(rhs));
337
338 if (is_zero(infinity_norm, Tolerance::Sparsity)) {
339 continue;
340 }
341
342 int e = 0;
343 std::frexp(infinity_norm, &e);
344 double closest_power_of_2 = std::ldexp(1.0, e - 1); // scale = 2^(e-1) or 2^e depending on your normalization choice
345 if (closest_power_of_2 != 1.) {
346 row /= closest_power_of_2;
347 rhs /= closest_power_of_2;
348 }
349
350 }
351}
352
353template <class NodeInfoT>
354idol::CglCutCallback<NodeInfoT>::Strategy::NodeCutContext& idol::CglCutCallback<
355 NodeInfoT>::Strategy::get_cut_context() {
356 const auto current_node_id = this->node().id();
357
358 if (!m_cut_context || m_cut_context->node_id() != current_node_id) {
359 m_cut_context.reset(new NodeCutContext(current_node_id));
360 }
361
362 return *m_cut_context;
363}
364
365#endif //IDOL_CGLCUTS_H
void add_user_cut(const TempCtr &t_cut)
const SideEffectRegistry & side_effect_registry() const
const Node< NodeInfoT > & node() const
void operator()(CallbackEvent t_event) override
static double Sparsity
Definition numericals.h:37