Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
cuts.h
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14#ifndef ORTOOLS_SAT_CUTS_H_
15#define ORTOOLS_SAT_CUTS_H_
16
17#include <stdint.h>
18
19#include <array>
20#include <cmath>
21#include <cstdlib>
22#include <functional>
23#include <string>
24#include <tuple>
25#include <utility>
26#include <vector>
27
28#include "absl/container/btree_map.h"
29#include "absl/container/btree_set.h"
30#include "absl/container/flat_hash_map.h"
31#include "absl/container/flat_hash_set.h"
32#include "absl/functional/any_invocable.h"
33#include "absl/numeric/int128.h"
34#include "absl/strings/str_cat.h"
35#include "absl/types/span.h"
37#include "ortools/sat/clause.h"
39#include "ortools/sat/integer.h"
43#include "ortools/sat/model.h"
46
47namespace operations_research {
48namespace sat {
49
50// A "cut" generator on a set of IntegerVariable.
51//
52// The generate_cuts() function can get the current LP solution with
53// manager->LpValues(). Note that a CutGenerator should:
54// - Only look at the lp_values positions that corresponds to its 'vars' or
55// their negation.
56// - Only add cuts in term of the same variables or their negation.
59 std::vector<IntegerVariable> vars;
60 absl::AnyInvocable<bool(LinearConstraintManager* manager)> generate_cuts;
61};
62
63// To simplify cut generation code, we use a more complex data structure than
64// just a LinearConstraint to represent a cut with shifted/complemented variable
65// and implied bound substitution.
66struct CutTerm {
67 bool IsBoolean() const { return bound_diff == 1; }
68 bool IsSimple() const { return expr_coeffs[1] == 0; }
69 bool HasRelevantLpValue() const { return lp_value > 1e-2; }
70 bool IsFractional() const {
71 return std::abs(lp_value - std::round(lp_value)) > 1e-4;
72 }
73 double LpDistToMaxValue() const {
74 return static_cast<double>(bound_diff.value()) - lp_value;
75 }
76
77 std::string DebugString() const;
78
79 // Do the subtitution X -> (1 - X') and update the rhs.
80 //
81 // Our precondition on the sum of variable domains fitting an int64_t should
82 // ensure that this can never overflow.
83 void Complement(absl::int128* rhs);
84
85 // This assumes bound_diff == 1. It replaces the inner expression by either
86 // var or 1 - var depending on the positiveness of var.
87 void ReplaceExpressionByLiteral(IntegerVariable var);
88
89 // If the term correspond to literal_view or (1 - literal_view) return the
90 // integer variable representation of that literal. Returns kNoIntegerVariable
91 // if this is not the case.
92 IntegerVariable GetUnderlyingLiteralOrNone() const;
93
94 // Each term is of the form coeff * X where X is a variable with given
95 // lp_value and with a domain in [0, bound_diff]. Note X is always >= 0.
96 double lp_value = 0.0;
97 IntegerValue coeff = IntegerValue(0);
98 IntegerValue bound_diff = IntegerValue(0);
99
100 // X = the given LinearExpression.
101 // We only support size 1 or 2 here which allow to inline the memory.
102 // When a coefficient is zero, we don't care about the variable.
103 //
104 // TODO(user): We might want to store that elsewhere, as sorting CutTerm is a
105 // bit slow and we don't need to look at that in most places. Same for the
106 // cached_implied_lb/ub below.
107 IntegerValue expr_offset = IntegerValue(0);
108 std::array<IntegerVariable, 2> expr_vars;
109 std::array<IntegerValue, 2> expr_coeffs;
110
111 // Refer to cached_data_ in ImpliedBoundsProcessor.
114};
115
116// Our cut are always of the form linear_expression <= rhs.
117struct CutData {
118 // We need level zero bounds and LP relaxation values to fill a CutData.
119 // Returns false if we encounter any integer overflow.
121 const LinearConstraint& base_ct,
123 IntegerTrail* integer_trail);
124
125 bool FillFromParallelVectors(IntegerValue ub,
126 absl::Span<const IntegerVariable> vars,
127 absl::Span<const IntegerValue> coeffs,
128 absl::Span<const double> lp_values,
129 absl::Span<const IntegerValue> lower_bounds,
130 absl::Span<const IntegerValue> upper_bounds);
131
132 bool AppendOneTerm(IntegerVariable var, IntegerValue coeff, double lp_value,
133 IntegerValue lb, IntegerValue ub);
134
135 // These functions transform the cut by complementation.
136 bool AllCoefficientsArePositive() const;
139
140 // Computes and returns the cut violation.
141 double ComputeViolation() const;
142 double ComputeEfficacy() const;
143
144 // This sorts terms by decreasing lp values and fills both
145 // num_relevant_entries and max_magnitude.
146 void SortRelevantEntries();
147
148 std::string DebugString() const;
149
150 // Note that we use a 128 bit rhs so we can freely complement variable without
151 // running into overflow.
152 absl::int128 rhs;
153 std::vector<CutTerm> terms;
154
155 // Only filled after SortRelevantEntries().
156 IntegerValue max_magnitude;
158};
159
160// Stores temporaries used to build or manipulate a CutData.
162 public:
163 // Returns false if we encounter an integer overflow.
164 bool ConvertToLinearConstraint(const CutData& cut, LinearConstraint* output);
165
166 // These function allow to merges entries corresponding to the same variable
167 // and complementation. That is (X - lb) and (ub - X) are NOT merged and kept
168 // as separate terms. Note that we currently only merge Booleans since this
169 // is the only case we need.
170 //
171 // Return num_merges.
172 int AddOrMergeBooleanTerms(absl::Span<CutTerm> terms, IntegerValue t,
173 CutData* cut);
174
175 private:
176 bool MergeIfPossible(IntegerValue t, CutTerm& to_add, CutTerm& target);
177
178 absl::flat_hash_map<IntegerVariable, int> bool_index_;
179 absl::flat_hash_map<IntegerVariable, int> secondary_bool_index_;
180 absl::btree_map<IntegerVariable, IntegerValue> tmp_map_;
181};
182
183// Given an upper-bounded linear relation (sum terms <= ub), this algorithm
184// inspects the integer variable appearing in the sum and try to replace each of
185// them by a tight lower bound (>= coeff * binary + lb) using the implied bound
186// repository. By tight, we mean that it will take the same value under the
187// current LP solution.
188//
189// We use a class to reuse memory of the tmp terms.
191 public:
192 // We will only replace IntegerVariable appearing in lp_vars_.
193 ImpliedBoundsProcessor(absl::Span<const IntegerVariable> lp_vars_,
194 IntegerTrail* integer_trail,
195 ImpliedBounds* implied_bounds)
196 : lp_vars_(lp_vars_.begin(), lp_vars_.end()),
197 integer_trail_(integer_trail),
198 implied_bounds_(implied_bounds) {}
199
200 // See if some of the implied bounds equation are violated and add them to
201 // the IB cut pool if it is the case.
202 //
203 // Important: This must be called before we process any constraints with a
204 // different lp_values or level zero bounds.
207
208 // This assumes the term is simple: expr[0] = var - LB / UB - var. We use an
209 // implied lower bound on this expr, independently of the term.coeff sign.
210 //
211 // If possible, returns true and express X = bool_term + slack_term.
212 // If coeff of X is positive, then all coeff will be positive here.
214 IntegerValue factor_t, CutTerm& bool_term,
215 CutTerm& slack_term);
216
217 // This assumes the term is simple: expr[0] = var - LB / UB - var. We use
218 // an implied upper bound on this expr, independently of term.coeff sign.
219 //
220 // If possible, returns true and express X = bool_term + slack_term.
221 // If coeff of X is positive, then bool_term will have a positive coeff but
222 // slack_term will have a negative one.
224 IntegerValue factor_t, CutTerm& bool_term,
225 CutTerm& slack_term);
226
227 // We are about to apply the super-additive function f() to the CutData. Use
228 // implied bound information to eventually substitute and make the cut
229 // stronger. Returns the number of {lb_ib, ub_ib, merges} applied.
230 //
231 // This should lead to stronger cuts even if the norms migth be worse.
232 std::tuple<int, int, int> PostprocessWithImpliedBound(
233 const std::function<IntegerValue(IntegerValue)>& f, IntegerValue factor_t,
234 CutData* cut);
235
236 // Precomputes quantities used by all cut generation.
237 // This allows to do that once rather than 6 times.
238 // Return false if there are no exploitable implied bounds.
239 bool CacheDataForCut(IntegerVariable first_slack, CutData* cut);
240
241 bool TryToExpandWithLowerImpliedbound(IntegerValue factor_t, bool complement,
242 CutTerm* term, absl::int128* rhs,
243 std::vector<CutTerm>* new_bool_terms);
244
245 // This can be used to share the hash-map memory.
246 CutDataBuilder* MutableCutBuilder() { return &cut_builder_; }
247
248 // This can be used as a temporary storage for
249 // TryToExpandWithLowerImpliedbound().
250 std::vector<CutTerm>* ClearedMutableTempTerms() {
251 tmp_terms_.clear();
252 return &tmp_terms_;
253 }
254
255 // Add a new variable that could be used in the new cuts.
256 // Note that the cache must be computed to take this into account.
257 void AddLpVariable(IntegerVariable var) { lp_vars_.insert(var); }
258
259 // Once RecomputeCacheAndSeparateSomeImpliedBoundCuts() has been called,
260 // we can get the best implied bound for each variables.
261 //
262 // Note that because the variable level zero lower bound might change since
263 // the time this was cached, we just store the implied bound here.
265 double var_lp_value = 0.0;
266 double bool_lp_value = 0.0;
267 IntegerValue implied_bound;
268
269 // When VariableIsPositive(bool_var) then it is when this is one that the
270 // bound is implied. Otherwise it is when this is zero.
271 IntegerVariable bool_var = kNoIntegerVariable;
272
273 double SlackLpValue(IntegerValue lb) const {
274 const double bool_term =
275 static_cast<double>((implied_bound - lb).value()) * bool_lp_value;
276 return var_lp_value - static_cast<double>(lb.value()) - bool_term;
277 }
278
279 std::string DebugString() const {
280 return absl::StrCat("var - lb == (", implied_bound.value(),
281 " - lb) * bool(", bool_lp_value, ") + slack.");
282 }
283 };
284 BestImpliedBoundInfo GetCachedImpliedBoundInfo(IntegerVariable var) const;
285
286 // As we compute the best implied bounds for each variable, we add violated
287 // cuts here.
288 TopNCuts& IbCutPool() { return ib_cut_pool_; }
289
290 private:
291 BestImpliedBoundInfo ComputeBestImpliedBound(
292 IntegerVariable var,
294
295 absl::flat_hash_set<IntegerVariable> lp_vars_;
296 mutable absl::flat_hash_map<IntegerVariable, BestImpliedBoundInfo> cache_;
297
298 // Temporary data used by CacheDataForCut().
299 std::vector<CutTerm> tmp_terms_;
300 CutDataBuilder cut_builder_;
301 std::vector<BestImpliedBoundInfo> cached_data_;
302
303 TopNCuts ib_cut_pool_ = TopNCuts(50);
304
305 // Data from the constructor.
306 IntegerTrail* integer_trail_;
307 ImpliedBounds* implied_bounds_;
308};
309
310// Visible for testing. Returns a function f on integers such that:
311// - f is non-decreasing.
312// - f is super-additive: f(a) + f(b) <= f(a + b)
313// - 1 <= f(divisor) <= max_scaling
314// - For all x, f(x * divisor) = x * f(divisor)
315// - For all x, f(x * divisor + remainder) = x * f(divisor)
316//
317// Preconditions:
318// - 0 <= remainder < divisor.
319// - 1 <= max_scaling.
320//
321// This is used in IntegerRoundingCut() and is responsible for "strengthening"
322// the cut. Just taking f(x) = x / divisor result in the non-strengthened cut
323// and using any function that stricly dominate this one is better.
324//
325// Algorithm:
326// - We first scale by a factor t so that rhs_remainder >= divisor / 2.
327// - Then, if max_scaling == 2, we use the function described
328// in "Strenghtening Chvatal-Gomory cuts and Gomory fractional cuts", Adam N.
329// Letchfrod, Andrea Lodi.
330// - Otherwise, we use a generalization of this which is a discretized version
331// of the classical MIR rounding function that only take the value of the
332// form "an_integer / max_scaling". As max_scaling goes to infinity, this
333// converge to the real-valued MIR function.
334//
335// Note that for each value of max_scaling we will get a different function.
336// And that there is no dominance relation between any of these functions. So
337// it could be nice to try to generate a cut using different values of
338// max_scaling.
339IntegerValue GetFactorT(IntegerValue rhs_remainder, IntegerValue divisor,
340 IntegerValue max_magnitude);
341std::function<IntegerValue(IntegerValue)> GetSuperAdditiveRoundingFunction(
342 IntegerValue rhs_remainder, IntegerValue divisor, IntegerValue t,
343 IntegerValue max_scaling);
344
345// If we have an equation sum ci.Xi >= rhs with everything positive, and all
346// ci are >= min_magnitude then any ci >= rhs can be set to rhs. Also if
347// some ci are in [rhs - min, rhs) then they can be strenghtened to rhs - min.
348//
349// If we apply this to the negated equation (sum -ci.Xi + sum cj.Xj <= -rhs)
350// with potentially positive terms, this reduce to apply a super-additive
351// function:
352//
353// Plot look like:
354// x=-rhs x=0
355// | |
356// y=0 : | ---------------------------------
357// | ---
358// | /
359// |---
360// y=-rhs -------
361//
362// TODO(user): Extend it for ci >= max_magnitude, we can probaly "lift" such
363// coefficient.
364std::function<IntegerValue(IntegerValue)> GetSuperAdditiveStrengtheningFunction(
365 IntegerValue positive_rhs, IntegerValue min_magnitude);
366
367// Similar to above but with scaling of the linear part to just have at most
368// scaling values.
369std::function<IntegerValue(IntegerValue)>
370GetSuperAdditiveStrengtheningMirFunction(IntegerValue positive_rhs,
371 IntegerValue scaling);
372
373// Given a super-additive non-decreasing function f(), we periodically extend
374// its restriction from [-period, 0] to Z. Such extension is not always
375// super-additive and it is up to the caller to know when this is true or not.
376inline std::function<IntegerValue(IntegerValue)> ExtendNegativeFunction(
377 std::function<IntegerValue(IntegerValue)> base_f, IntegerValue period) {
378 const IntegerValue m = -base_f(-period);
379 return [m, period, base_f](IntegerValue v) {
380 const IntegerValue r = PositiveRemainder(v, period);
381 const IntegerValue output_r = m + base_f(r - period);
382 return FloorRatio(v, period) * m + output_r;
383 };
384}
385
386// Exploit AtMosteOne from the model to derive stronger cut.
387// In the MIP community, using amo in this context is know as GUB (generalized
388// upper bound).
390 public:
391 explicit GUBHelper(Model* model)
392 : implications_(model->GetOrCreate<BinaryImplicationGraph>()),
393 integer_encoder_(model->GetOrCreate<IntegerEncoder>()),
394 integer_trail_(model->GetOrCreate<IntegerTrail>()),
395 shared_stats_(model->GetOrCreate<SharedStatistics>()) {}
396 ~GUBHelper();
397
399 const std::function<IntegerValue(IntegerValue)>& f, IntegerValue divisor,
400 CutData* cut);
401
402 // Stats on the last call to ApplyWithPotentialBumpAndGUB().
403 int last_num_lifts() const { return last_num_lifts_; }
404 int last_num_bumps() const { return last_num_bumps_; }
405 int last_num_gubs() const { return last_num_gubs_; }
406
407 private:
408 BinaryImplicationGraph* implications_;
409 IntegerEncoder* integer_encoder_;
410 IntegerTrail* integer_trail_;
411 SharedStatistics* shared_stats_;
412
413 struct LiteralInCut {
414 int index;
415 Literal literal;
416 IntegerValue original_coeff;
417 bool complemented;
418 };
419
420 // One call stats.
421 int last_num_lifts_;
422 int last_num_bumps_;
423 int last_num_gubs_;
424
425 // Tmp data.
426 std::vector<IntegerValue> initial_coeffs_;
427 std::vector<LiteralInCut> literals_;
428 absl::flat_hash_set<Literal> already_seen_;
429 absl::flat_hash_map<int, int> amo_count_;
430
431 // Stats.
432 int64_t num_improvements_ = 0;
433 int64_t num_todo_both_complements_ = 0;
434};
435
436// Given an upper bounded linear constraint, this function tries to transform it
437// to a valid cut that violate the given LP solution using integer rounding.
438// Note that the returned cut might not always violate the LP solution, in which
439// case it can be discarded.
440//
441// What this does is basically take the integer division of the constraint by an
442// integer. If the coefficients where doubles, this would be the same as scaling
443// the constraint and then rounding. We choose the coefficient of the most
444// fractional variable (rescaled by its coefficient) as the divisor, but there
445// are other possible alternatives.
446//
447// Note that if the constraint is tight under the given lp solution, and if
448// there is a unique variable not at one of its bounds and fractional, then we
449// are guaranteed to generate a cut that violate the current LP solution. This
450// should be the case for Chvatal-Gomory base constraints modulo our loss of
451// precision while doing exact integer computations.
452//
453// Precondition:
454// - We assumes that the given initial constraint is tight using the given lp
455// values. This could be relaxed, but for now it should always be the case, so
456// we log a message and abort if not, to ease debugging.
457// - The IntegerVariable of the cuts are not used here. We assumes that the
458// first three vectors are in one to one correspondence with the initial order
459// of the variable in the cut.
460//
461// TODO(user): There is a bunch of heuristic involved here, and we could spend
462// more effort tuning them. In particular, one can try many heuristics and keep
463// the best looking cut (or more than one). This is not on the critical code
464// path, so we can spend more effort in finding good cuts.
466 IntegerValue max_scaling = IntegerValue(60);
469};
471 public:
473 : gub_helper_(model),
474 shared_stats_(model->GetOrCreate<SharedStatistics>()) {}
475
477
478 // Returns true on success. The cut can be accessed via cut().
479 bool ComputeCut(RoundingOptions options, const CutData& base_ct,
480 ImpliedBoundsProcessor* ib_processor = nullptr);
481
482 // If successful, info about the last generated cut.
483 const CutData& cut() const { return cut_; }
484
485 // Single line of text that we append to the cut log line.
486 std::string Info() const { return absl::StrCat("ib_lift=", num_ib_used_); }
487
488 private:
489 double GetScaledViolation(IntegerValue divisor, IntegerValue max_scaling,
490 IntegerValue remainder_threshold,
491 const CutData& cut);
492
493 GUBHelper gub_helper_;
494 SharedStatistics* shared_stats_;
495
496 // The helper is just here to reuse the memory for these vectors.
497 std::vector<IntegerValue> divisors_;
498 std::vector<IntegerValue> remainders_;
499 std::vector<IntegerValue> rs_;
500 std::vector<IntegerValue> best_rs_;
501
502 int64_t num_ib_used_ = 0;
503 CutData cut_;
504
505 std::vector<std::pair<int, IntegerValue>> adjusted_coeffs_;
506 std::vector<std::pair<int, IntegerValue>> best_adjusted_coeffs_;
507
508 // Overall stats.
509 int64_t total_num_dominating_f_ = 0;
510 int64_t total_num_pos_lifts_ = 0;
511 int64_t total_num_neg_lifts_ = 0;
512 int64_t total_num_post_complements_ = 0;
513 int64_t total_num_overflow_abort_ = 0;
514 int64_t total_num_coeff_adjust_ = 0;
515 int64_t total_num_merges_ = 0;
516 int64_t total_num_bumps_ = 0;
517 int64_t total_num_final_complements_ = 0;
518
519 int64_t total_num_initial_ibs_ = 0;
520 int64_t total_num_initial_merges_ = 0;
521};
522
523// Helper to find knapsack cover cuts.
525 public:
526 explicit CoverCutHelper(Model* model)
527 : gub_helper_(model),
528 shared_stats_(model->GetOrCreate<SharedStatistics>()) {}
529
531
532 // Try to find a cut with a knapsack heuristic. This assumes an input with all
533 // coefficients positive. If this returns true, you can get the cut via cut().
534 //
535 // This uses a lifting procedure similar to what is described in "Lifting the
536 // Knapsack Cover Inequalities for the Knapsack Polytope", Adam N. Letchfod,
537 // Georgia Souli. In particular the section "Lifting via mixed-integer
538 // rounding".
539 bool TrySimpleKnapsack(const CutData& input_ct,
540 ImpliedBoundsProcessor* ib_processor = nullptr);
541
542 // Applies the lifting procedure described in "On Lifted Cover Inequalities: A
543 // New Lifting Procedure with Unusual Properties", Adam N. Letchford, Georgia
544 // Souli. This assumes an input with all coefficients positive.
545 //
546 // The algo is pretty simple, given a cover C for a given rhs. We compute
547 // a rational weight p/q so that sum_C min(w_i, p/q) = rhs. Note that q is
548 // pretty small (lower or equal to the size of C). The generated cut is then
549 // of the form
550 // sum X_i in C for which w_i <= p / q
551 // + sum gamma_i X_i for the other variable <= |C| - 1.
552 //
553 // gamma_i being the smallest k such that w_i <= sum of the k + 1 largest
554 // min(w_i, p/q) for i in C. In particular, it is zero if w_i <= p/q.
555 //
556 // Note that this accept a general constraint that has been canonicalized to
557 // sum coeff_i * X_i <= base_rhs. Each coeff_i >= 0 and each X_i >= 0.
558 //
559 // TODO(user): Generalize to non-Boolean, or use a different cover heuristic
560 // for this:
561 // - We want a Boolean only cover currently.
562 // - We can always use implied bound for this, since there is more chance
563 // for a Bool only cover.
564 // - Also, f() should be super additive on the value <= rhs, i.e. f(a + b) >=
565 // f(a) + f(b), so it is always good to use implied bounds of the form X =
566 // bound * B + Slack.
568 const CutData& input_ct, ImpliedBoundsProcessor* ib_processor = nullptr);
569
570 // It turns out that what FlowCoverCutHelper is doing is really just finding a
571 // cover and generating a cut via coefficient strenghtening instead of MIR
572 // rounding. This more generic version should just always outperform our old
573 // code.
574 bool TrySingleNodeFlow(const CutData& input_ct,
575 ImpliedBoundsProcessor* ib_processor = nullptr);
576
577 // If successful, info about the last generated cut.
578 const CutData& cut() const { return cut_; }
579
580 // Single line of text that we append to the cut log line.
581 std::string Info() const { return absl::StrCat("lift=", num_lifting_); }
582
583 void ClearCache() { has_bool_base_ct_ = false; }
584
585 private:
586 void InitializeCut(const CutData& input_ct);
587
588 // This looks at base_ct_ and reoder the terms so that the first ones are in
589 // the cover. return zero if no interesting cover was found.
590 template <class CompareAdd, class CompareRemove>
591 int GetCoverSize(int relevant_size);
592
593 // Same as GetCoverSize() but only look at Booleans, and use a different
594 // heuristic.
595 int GetCoverSizeForBooleans();
596
597 template <class Compare>
598 int MinimizeCover(int cover_size, absl::int128 slack);
599
600 GUBHelper gub_helper_;
601 SharedStatistics* shared_stats_;
602
603 // Here to reuse memory, cut_ is both the input and the output.
604 CutData cut_;
605 CutData temp_cut_;
606
607 // Hack to not sort twice.
608 bool has_bool_base_ct_ = false;
609 CutData bool_base_ct_;
610 int bool_cover_size_ = 0;
611
612 // Stats.
613 int64_t num_lifting_ = 0;
614
615 // Stats for the various type of cuts generated here.
616 struct CutStats {
617 int64_t num_cuts = 0;
618 int64_t num_initial_ibs = 0;
619 int64_t num_lb_ibs = 0;
620 int64_t num_ub_ibs = 0;
621 int64_t num_merges = 0;
622 int64_t num_bumps = 0;
623 int64_t num_lifting = 0;
624 int64_t num_gubs = 0;
625 int64_t num_overflow_aborts = 0;
626 };
627 CutStats flow_stats_;
628 CutStats cover_stats_;
629 CutStats ls_stats_;
630};
631
632// Separate RLT cuts.
633//
634// See for instance "Efficient Separation of RLT Cuts for Implicit and Explicit
635// Bilinear Products", Ksenia Bestuzheva, Ambros Gleixner, Tobias Achterberg,
636// https://arxiv.org/abs/2211.13545
638 public:
639 explicit BoolRLTCutHelper(Model* model)
640 : product_detector_(model->GetOrCreate<ProductDetector>()),
641 shared_stats_(model->GetOrCreate<SharedStatistics>()),
642 lp_values_(model->GetOrCreate<ModelLpValues>()) {};
644
645 // Precompute data according to the current lp relaxation.
646 // This also restrict any Boolean to be currently appearing in the LP.
647 void Initialize(absl::Span<const IntegerVariable> lp_vars);
648
649 // Tries RLT separation of the input constraint. Returns true on success.
650 bool TrySimpleSeparation(const CutData& input_ct);
651
652 // If successful, this contains the last generated cut.
653 const CutData& cut() const { return cut_; }
654
655 // Single line of text that we append to the cut log line.
656 std::string Info() const { return ""; }
657
658 private:
659 // LP value of a literal encoded as an IntegerVariable.
660 // That is lit(X) = X if X positive or 1 - X otherwise.
661 double GetLiteralLpValue(IntegerVariable var) const;
662
663 // Multiplies input by lit(factor) and linearize in the best possible way.
664 // The result will be stored in cut_.
665 bool TryProduct(IntegerVariable factor, const CutData& input);
666
667 bool enabled_ = false;
668 CutData filtered_input_;
669 CutData cut_;
670
671 ProductDetector* product_detector_;
672 SharedStatistics* shared_stats_;
673 ModelLpValues* lp_values_;
674
675 int64_t num_tried_ = 0;
676 int64_t num_tried_factors_ = 0;
677};
678
679// A cut generator for z = x * y (x and y >= 0).
680CutGenerator CreatePositiveMultiplicationCutGenerator(AffineExpression z,
681 AffineExpression x,
682 AffineExpression y,
683 int linearization_level,
684 Model* model);
685
686// Above hyperplan for square = x * x: square should be below the line
687// (x_lb, x_lb ^ 2) to (x_ub, x_ub ^ 2).
688// The slope of that line is (ub^2 - lb^2) / (ub - lb) = ub + lb.
689// square <= (x_lb + x_ub) * x - x_lb * x_ub
690// This only works for positive x.
691LinearConstraint ComputeHyperplanAboveSquare(AffineExpression x,
692 AffineExpression square,
693 IntegerValue x_lb,
694 IntegerValue x_ub, Model* model);
695
696// Below hyperplan for square = x * x: y should be above the line
697// (x_value, x_value ^ 2) to (x_value + 1, (x_value + 1) ^ 2)
698// The slope of that line is 2 * x_value + 1
699// square >= below_slope * (x - x_value) + x_value ^ 2
700// square >= below_slope * x - x_value ^ 2 - x_value
701LinearConstraint ComputeHyperplanBelowSquare(AffineExpression x,
702 AffineExpression square,
703 IntegerValue x_value,
704 Model* model);
705
706// A cut generator for y = x ^ 2 (x >= 0).
707// It will dynamically add a linear inequality to push y closer to the parabola.
708CutGenerator CreateSquareCutGenerator(AffineExpression y, AffineExpression x,
709 int linearization_level, Model* model);
710
711// A cut generator for all_diff(xi). Let the united domain of all xi be D. Sum
712// of any k-sized subset of xi need to be greater or equal to the sum of
713// smallest k values in D and lesser or equal to the sum of largest k values in
714// D. The cut generator first sorts the variables based on LP values and adds
715// cuts of the form described above if they are violated by lp solution. Note
716// that all the fixed variables are ignored while generating cuts.
718 absl::Span<const AffineExpression> exprs, Model* model);
719
720// Consider the Lin Max constraint with d expressions and n variables in the
721// form: target = max {exprs[k] = Sum (wki * xi + bk)}. k in {1,..,d}.
722// Li = lower bound of xi
723// Ui = upper bound of xi.
724// Let zk be in {0,1} for all k in {1,..,d}.
725// The target = exprs[k] when zk = 1.
726//
727// The following is a valid linearization for Lin Max.
728// target >= exprs[k], for all k in {1,..,d}
729// target <= Sum (wli * xi) + Sum((Nlk + bk) * zk), for all l in {1,..,d}
730// Where Nlk is a large number defined as:
731// Nlk = Sum (max((wki - wli)*Li, (wki - wli)*Ui))
732// = Sum (max corner difference for variable i, target expr l, max expr k)
733//
734// Consider a partition of variables xi into set {1,..,d} as I.
735// i.e. I(i) = j means xi is mapped to jth index.
736// The following inequality is valid and sharp cut for the lin max constraint
737// described above.
738//
739// target <= Sum(i=1..n)(wI(i)i * xi + Sum(k=1..d)(MPlusCoefficient_ki * zk))
740// + Sum(k=1..d)(bk * zk) ,
741// Where MPlusCoefficient_ki = max((wki - wI(i)i) * Li,
742// (wki - wI(i)i) * Ui)
743// = max corner difference for variable i,
744// target expr I(i), max expr k.
745//
746// For detailed proof of validity, refer
747// Reference: "Strong mixed-integer programming formulations for trained neural
748// networks" by Ross Anderson et. (https://arxiv.org/pdf/1811.01988.pdf).
749//
750// In the cut generator, we compute the most violated partition I by computing
751// the rhs value (wI(i)i * lp_value(xi) + Sum(k=1..d)(MPlusCoefficient_ki * zk))
752// for each variable for each partition index. We choose the partition index
753// that gives lowest rhs value for a given variable.
754//
755// Note: This cut generator requires all expressions to contain only positive
756// vars.
757CutGenerator CreateLinMaxCutGenerator(IntegerVariable target,
758 absl::Span<const LinearExpression> exprs,
759 absl::Span<const IntegerVariable> z_vars,
760 Model* model);
761
762// Helper for the affine max constraint.
763//
764// This function will reset the bounds of the builder.
766 const LinearExpression& target, IntegerVariable var,
767 absl::Span<const std::pair<IntegerValue, IntegerValue>> affines,
768 Model* model, LinearConstraintBuilder* builder);
769
770// By definition, the Max of affine functions is convex. The linear polytope is
771// bounded by all affine functions on the bottom, and by a single hyperplane
772// that join the two points at the extreme of the var domain, and their y-values
773// of the max of the affine functions.
774CutGenerator CreateMaxAffineCutGenerator(
775 LinearExpression target, IntegerVariable var,
776 std::vector<std::pair<IntegerValue, IntegerValue>> affines,
777 std::string cut_name, Model* model);
778
779// Extracts the variables that have a Literal view from base variables and
780// create a generator that will returns constraint of the form "at_most_one"
781// between such literals.
782CutGenerator CreateCliqueCutGenerator(
783 absl::Span<const IntegerVariable> base_variables, Model* model);
784
785// Utility class for the AllDiff cut generator.
787 public:
788 void Clear();
789 void Add(const AffineExpression& expr, int num_expr,
790 const IntegerTrail& integer_trail);
791
792 // Return int_max if the sum overflows.
793 IntegerValue SumOfMinDomainValues();
794 IntegerValue SumOfDifferentMins();
795 IntegerValue GetBestLowerBound(std::string& suffix);
796
797 int size() const { return expr_mins_.size(); }
798
799 private:
800 absl::btree_set<IntegerValue> min_values_;
801 std::vector<IntegerValue> expr_mins_;
802};
803
804} // namespace sat
805} // namespace operations_research
806
807#endif // ORTOOLS_SAT_CUTS_H_
Definition model.h:345
bool TrySimpleSeparation(const CutData &input_ct)
Definition cuts.cc:1904
void Initialize(absl::Span< const IntegerVariable > lp_vars)
Definition cuts.cc:1898
const CutData & cut() const
Definition cuts.h:653
const CutData & cut() const
Definition cuts.h:578
bool TrySimpleKnapsack(const CutData &input_ct, ImpliedBoundsProcessor *ib_processor=nullptr)
Definition cuts.cc:1534
bool TryWithLetchfordSouliLifting(const CutData &input_ct, ImpliedBoundsProcessor *ib_processor=nullptr)
Definition cuts.cc:1753
bool TrySingleNodeFlow(const CutData &input_ct, ImpliedBoundsProcessor *ib_processor=nullptr)
Definition cuts.cc:1651
bool ConvertToLinearConstraint(const CutData &cut, LinearConstraint *output)
Definition cuts.cc:353
int AddOrMergeBooleanTerms(absl::Span< CutTerm > terms, IntegerValue t, CutData *cut)
Definition cuts.cc:300
void ApplyWithPotentialBumpAndGUB(const std::function< IntegerValue(IntegerValue)> &f, IntegerValue divisor, CutData *cut)
Definition cuts.cc:476
std::vector< CutTerm > * ClearedMutableTempTerms()
Definition cuts.h:250
void RecomputeCacheAndSeparateSomeImpliedBoundCuts(const util_intops::StrongVector< IntegerVariable, double > &lp_values)
Definition cuts.cc:2338
bool CacheDataForCut(IntegerVariable first_slack, CutData *cut)
Definition cuts.cc:2548
bool TryToExpandWithLowerImpliedbound(IntegerValue factor_t, bool complement, CutTerm *term, absl::int128 *rhs, std::vector< CutTerm > *new_bool_terms)
Definition cuts.cc:2520
ImpliedBoundsProcessor(absl::Span< const IntegerVariable > lp_vars_, IntegerTrail *integer_trail, ImpliedBounds *implied_bounds)
Definition cuts.h:193
void AddLpVariable(IntegerVariable var)
Definition cuts.h:257
BestImpliedBoundInfo GetCachedImpliedBoundInfo(IntegerVariable var) const
Definition cuts.cc:2258
bool DecomposeWithImpliedLowerBound(const CutTerm &term, IntegerValue factor_t, CutTerm &bool_term, CutTerm &slack_term)
Definition cuts.cc:2348
std::tuple< int, int, int > PostprocessWithImpliedBound(const std::function< IntegerValue(IntegerValue)> &f, IntegerValue factor_t, CutData *cut)
Definition cuts.cc:2439
bool DecomposeWithImpliedUpperBound(const CutTerm &term, IntegerValue factor_t, CutTerm &bool_term, CutTerm &slack_term)
Definition cuts.cc:2418
bool ComputeCut(RoundingOptions options, const CutData &base_ct, ImpliedBoundsProcessor *ib_processor=nullptr)
Definition cuts.cc:1002
IntegerValue GetBestLowerBound(std::string &suffix)
Definition cuts.cc:2631
void Add(const AffineExpression &expr, int num_expr, const IntegerTrail &integer_trail)
Definition cuts.cc:2584
IntegerValue FloorRatio(IntegerValue dividend, IntegerValue positive_divisor)
bool BuildMaxAffineUpConstraint(const LinearExpression &target, IntegerVariable var, absl::Span< const std::pair< IntegerValue, IntegerValue > > affines, Model *model, LinearConstraintBuilder *builder)
Definition cuts.cc:2895
IntegerValue GetFactorT(IntegerValue rhs_remainder, IntegerValue divisor, IntegerValue max_magnitude)
Definition cuts.cc:695
CutGenerator CreatePositiveMultiplicationCutGenerator(AffineExpression z, AffineExpression x, AffineExpression y, int linearization_level, Model *model)
Definition cuts.cc:2103
LinearConstraint ComputeHyperplanBelowSquare(AffineExpression x, AffineExpression square, IntegerValue x_value, Model *model)
Definition cuts.cc:2210
const IntegerVariable kNoIntegerVariable(-1)
CutGenerator CreateLinMaxCutGenerator(const IntegerVariable target, absl::Span< const LinearExpression > exprs, absl::Span< const IntegerVariable > z_vars, Model *model)
Definition cuts.cc:2806
std::function< IntegerValue(IntegerValue)> GetSuperAdditiveStrengtheningFunction(IntegerValue positive_rhs, IntegerValue min_magnitude)
Definition cuts.cc:794
CutGenerator CreateAllDifferentCutGenerator(absl::Span< const AffineExpression > exprs, Model *model)
Definition cuts.cc:2694
CutGenerator CreateSquareCutGenerator(AffineExpression y, AffineExpression x, int linearization_level, Model *model)
Definition cuts.cc:2222
CutGenerator CreateCliqueCutGenerator(absl::Span< const IntegerVariable > base_variables, Model *model)
Definition cuts.cc:2971
IntegerValue PositiveRemainder(IntegerValue dividend, IntegerValue positive_divisor)
CutGenerator CreateMaxAffineCutGenerator(LinearExpression target, IntegerVariable var, std::vector< std::pair< IntegerValue, IntegerValue > > affines, const std::string cut_name, Model *model)
Definition cuts.cc:2949
LinearConstraint ComputeHyperplanAboveSquare(AffineExpression x, AffineExpression square, IntegerValue x_lb, IntegerValue x_ub, Model *model)
Definition cuts.cc:2198
std::function< IntegerValue(IntegerValue)> ExtendNegativeFunction(std::function< IntegerValue(IntegerValue)> base_f, IntegerValue period)
Definition cuts.h:376
std::function< IntegerValue(IntegerValue)> GetSuperAdditiveStrengtheningMirFunction(IntegerValue positive_rhs, IntegerValue scaling)
Definition cuts.cc:829
std::function< IntegerValue(IntegerValue)> GetSuperAdditiveRoundingFunction(IntegerValue rhs_remainder, IntegerValue divisor, IntegerValue t, IntegerValue max_scaling)
Definition cuts.cc:709
OR-Tools root namespace.
ClosedInterval::Iterator end(ClosedInterval interval)
ClosedInterval::Iterator begin(ClosedInterval interval)
static int input(yyscan_t yyscanner)
bool FillFromLinearConstraint(const LinearConstraint &base_ct, const util_intops::StrongVector< IntegerVariable, double > &lp_values, IntegerTrail *integer_trail)
Definition cuts.cc:174
bool AllCoefficientsArePositive() const
Definition cuts.cc:232
std::vector< CutTerm > terms
Definition cuts.h:153
bool FillFromParallelVectors(IntegerValue ub, absl::Span< const IntegerVariable > vars, absl::Span< const IntegerValue > coeffs, absl::Span< const double > lp_values, absl::Span< const IntegerValue > lower_bounds, absl::Span< const IntegerValue > upper_bounds)
Definition cuts.cc:192
double ComputeEfficacy() const
Definition cuts.cc:265
std::string DebugString() const
Definition cuts.cc:76
double ComputeViolation() const
Definition cuts.cc:257
bool AppendOneTerm(IntegerVariable var, IntegerValue coeff, double lp_value, IntegerValue lb, IntegerValue ub)
Definition cuts.cc:137
absl::AnyInvocable< bool(LinearConstraintManager *manager)> generate_cuts
Definition cuts.h:60
std::vector< IntegerVariable > vars
Definition cuts.h:59
IntegerVariable GetUnderlyingLiteralOrNone() const
Definition cuts.cc:117
std::string DebugString() const
Definition cuts.cc:68
double LpDistToMaxValue() const
Definition cuts.h:73
void ReplaceExpressionByLiteral(IntegerVariable var)
Definition cuts.cc:103
void Complement(absl::int128 *rhs)
Definition cuts.cc:84
std::array< IntegerVariable, 2 > expr_vars
Definition cuts.h:108
std::array< IntegerValue, 2 > expr_coeffs
Definition cuts.h:109
bool HasRelevantLpValue() const
Definition cuts.h:69