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_cuts_by_effectiveness(const std::list<TempCtr>& t_cuts);
69 protected:
70 NodeCutContext& get_cut_context();
71 const NodeCutContext& get_cut_context() const { return const_cast<Strategy*>(this)->get_cut_context(); }
72 void initialize() override;
73 void log_after_termination() override;
74 void operator()(CallbackEvent t_event) override;
75 };
76
77 Strategy* operator()() override;
78 [[nodiscard]] CglCutCallback* clone() const override;
79};
80
81template <class NodeInfoT>
82idol::CglCutCallback<NodeInfoT>::Strategy* idol::CglCutCallback<NodeInfoT>::operator()() {
83 return new Strategy();
84}
85
86template <class NodeInfoT>
87idol::CglCutCallback<NodeInfoT>* idol::CglCutCallback<NodeInfoT>::clone() const {
88 return new CglCutCallback<NodeInfoT>(*this);
89}
90
91#include "idol/mixed-integer/optimizers/callbacks/cutting-planes/OsiIdolCglSolverInterface.h"
92
93template <class NodeInfoT>
95#ifdef IDOL_USE_CGL
96
98
99 m_osi_solver = std::make_unique<OsiIdolCglSolverInterface>(*this);
100
101 m_cut_families.emplace_back(new CutFamily(new CglKnapsackCover(), "Cover"));
102 m_cut_families.emplace_back(new CutFamily(new CglFlowCover(), "Flow Cover"));
103 m_cut_families.emplace_back(new CutFamily(new CglZeroHalf(), "Zero Half"));
104 m_cut_families.emplace_back(new CutFamily(new CglMixedIntegerRounding(), "MIR"));
105 m_cut_families.emplace_back(new CutFamily(new CglResidualCapacity(), "Residual Capacity"));
106 //m_cut_families.emplace_back(new CutFamily(new CglLandP(), "Lift-and-Project")); // Needs getObjValue
107 //m_cut_families.emplace_back(new CutFamily(new CglOddHole(), "Odd Hole")); // Needs reduced costs
108
109#else
110 throw Exception("idol was not linked with Cgl.");
111#endif
112}
113
114template <class NodeInfoT>
115void idol::CglCutCallback<NodeInfoT>::Strategy::log_after_termination() {
116
117 BranchAndBoundCallback<NodeInfoT>::log_after_termination();
118
119 std::cout << "Cgl Cutting Planes:" << std::endl;
120
121 for (const auto& cut_family : m_cut_families) {
122 std::cout << '\t' << cut_family->name() << ": " << cut_family->n_accepted() << "\n";
123 }
124
125}
126
127template <class NodeInfoT>
129#ifdef IDOL_USE_CGL
130 if (t_event != InvalidSolution) {
131 return;
132 }
133
134 if (m_total_n_added_cuts > m_max_cuts) {
135 return;
136 }
137
138 if (this->node().level() > m_max_depth_for_cuts) {
139 return;
140 }
141
142 const double relative_gap = idol::relative_gap(this->best_bound(), this->best_obj()) * 100;
143
144 if (relative_gap < 1) {
145 return;
146 }
147
148 auto& cut_context = get_cut_context();
149 const bool is_root_node = cut_context.is_root_node();
150
151 bool be_aggressive = false;
152 if (is_root_node || relative_gap > 50) {
153 be_aggressive = true;
154 }
155
156 unsigned int max_n_added_cut_at_this_node = m_max_cut_per_regular_node;
157 unsigned int max_pass_at_this_node = m_max_pass_per_regular_node;
158 if (be_aggressive) {
159 max_n_added_cut_at_this_node = m_max_cut_at_aggressive_node;
160 max_pass_at_this_node = m_max_pass_at_aggressive_node;
161 }
162
163 if (cut_context.pass() > max_pass_at_this_node) {
164 return;
165 }
166
167 cut_context.increment_pass();
168
169 // Count number of fractional over integer ?
170 if (!be_aggressive) {
171 std::sort(m_cut_families.begin(), m_cut_families.end(), [](const auto& t_a, const auto& t_b) {
172 return t_a->score() > t_b->score();
173 });
174 }
175
176 m_osi_solver->update_current_solution();
177
178 auto& registry = this->side_effect_registry();
179 for (auto& cut_family : m_cut_families) {
180
181 if (!be_aggressive && cut_family->score() <= 1e-2) {
182 continue;
183 }
184
185 auto osi_cuts = cut_family->generate(*m_osi_solver, be_aggressive ? 100 : 0);
186 auto idol_cuts = to_idol_cuts(osi_cuts);
187 auto sorted_cuts = sort_cuts_by_effectiveness(idol_cuts);
188
189 for (auto& [cut, effectiveness] : sorted_cuts) {
190
191 if (m_total_n_added_cuts > m_max_cuts) {
192 return;
193 }
194
195 if (cut_context.n_added_cuts() > max_n_added_cut_at_this_node) {
196 return;
197 }
198
199 cut_family->add_effectiveness_statistics(effectiveness);
200
201 if (effectiveness <= m_effectiveness_threshold) {
202 continue;
203 }
204
205 const unsigned int old_n_added_user_cuts = registry.n_added_user_cuts;
206 this->add_user_cut(cut);
207 const bool cut_was_accepted_by_idol = registry.n_added_user_cuts > old_n_added_user_cuts;
208
209 if (cut_was_accepted_by_idol) {
210 m_total_n_added_cuts++;
211 cut_context.add_accepted_cut();
212 cut_family->add_accepted_cut();
213 }
214 }
215 }
216#endif
217}
218
219template <class NodeInfoT>
220std::list<idol::TempCtr> idol::CglCutCallback<NodeInfoT>::Strategy::to_idol_cuts(OsiCuts& t_cuts) {
221#ifdef IDOL_USE_CGL
222 std::list<TempCtr> result;
223
224 for (unsigned int k = 0, n = t_cuts.sizeRowCuts(); k < n; k++) {
225 auto& cut = *t_cuts.rowCutPtr(k);
226
227 if (!cut.consistent()) {
228 continue;
229 }
230
231 if (cut.infeasible(*m_osi_solver)) {
232 continue;
233 }
234
235 result.emplace_back(to_idol_cut(cut));
236 }
237
238 return result;
239#else
240 throw Exception("idol was not linked with Cgl.");
241#endif
242}
243
244template <class NodeInfoT>
245idol::TempCtr idol::CglCutCallback<NodeInfoT>::Strategy::to_idol_cut(OsiRowCut& t_cut) {
246#ifdef IDOL_USE_CGL
247 const auto& model = this->original_model();
248
249 TempCtr cut;
250
251 const auto osi_sense = t_cut.sense();
252 if (osi_sense == 'G') {
253 cut.set_type(GreaterOrEqual);
254 cut.set_rhs(t_cut.lb());
255 }
256 else if (osi_sense == 'L') {
257 cut.set_type(LessOrEqual);
258 cut.set_rhs(t_cut.ub());
259 }
260 else if (osi_sense == 'R') {
261 if (const double lb = t_cut.lb(); !is_neg_inf(lb)) {
262 cut.set_type(GreaterOrEqual);
263 cut.set_rhs(t_cut.lb());
264 }
265 else if (const double ub = t_cut.ub(); !is_pos_inf(ub)) {
266 cut.set_type(LessOrEqual);
267 cut.set_rhs(t_cut.ub());
268 }
269 else {
270 throw Exception("Cgl returned a cut of type R, which is not handled in idol.");
271 }
272 }
273 else {
274 throw Exception("Could not handle cut type from Cgl.");
275 }
276
277 const auto& osi_row = t_cut.row();
278 const auto* indices = osi_row.getIndices();
279 const auto* values = osi_row.getElements();
280
281 for (unsigned int k = 0, n = osi_row.getNumElements(); k < n; k++) {
282 const auto& var = model.get_var_by_index(indices[k]);
283 cut.lhs() += values[k] * var;
284 }
285
286 return cut;
287#else
288 throw Exception("idol was not linked with Cgl.");
289#endif
290}
291
292template <class NodeInfoT>
293std::vector<std::pair<idol::TempCtr, double>> idol::CglCutCallback<NodeInfoT>::Strategy::sort_cuts_by_effectiveness(
294 const std::list<TempCtr>& t_cuts) {
295 std::vector<std::pair<TempCtr, double>> result;
296 const auto primal_solution = this->node().info().primal_solution();
297
298 for (auto& cut : t_cuts) {
299 double activity = 0.;
300 double norm = 0.;
301
302 for (const auto& [var, coefficient] : cut.lhs()) {
303 norm += coefficient * coefficient;
304 activity += coefficient * primal_solution.get(var);
305 }
306 norm = std::sqrt(norm);
307
308 double effectiveness = (activity - cut.rhs()) / norm;
309 if (cut.type() == GreaterOrEqual) {
310 effectiveness *= -1.;
311 }
312
313 result.emplace_back(std::move(cut), effectiveness);
314 }
315
316 std::sort(result.begin(), result.end(), [](const auto& t_a, const auto& t_b) {
317 return t_a.second > t_b.second;
318 });
319
320 return result;
321}
322
323template <class NodeInfoT>
324idol::CglCutCallback<NodeInfoT>::Strategy::NodeCutContext& idol::CglCutCallback<
325 NodeInfoT>::Strategy::get_cut_context() {
326 const auto current_node_id = this->node().id();
327
328 if (!m_cut_context || m_cut_context->node_id() != current_node_id) {
329 m_cut_context.reset(new NodeCutContext(current_node_id));
330 }
331
332 return *m_cut_context;
333}
334
335#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