Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
linear_constraint_manager.cc
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
15
16#include <stdint.h>
17
18#include <algorithm>
19#include <cmath>
20#include <cstddef>
21#include <limits>
22#include <numeric>
23#include <string>
24#include <utility>
25#include <vector>
26
27#include "absl/container/btree_map.h"
28#include "absl/container/flat_hash_map.h"
29#include "absl/log/check.h"
30#include "absl/log/log.h"
31#include "absl/log/vlog_is_on.h"
32#include "absl/numeric/int128.h"
33#include "absl/strings/str_cat.h"
34#include "absl/strings/string_view.h"
35#include "absl/types/span.h"
36#include "ortools/base/hash.h"
41#include "ortools/sat/integer.h"
44#include "ortools/sat/model.h"
48#include "ortools/sat/util.h"
52
53namespace operations_research {
54namespace sat {
55
57 if (!VLOG_IS_ON(1)) return;
58 std::vector<std::pair<std::string, int64_t>> stats;
59 stats.push_back({"symmetrizer/overflows", num_overflows_});
60 shared_stats_->AddStats(stats);
61}
62
64 IntegerVariable sum_var, absl::Span<const IntegerVariable> orbit) {
65 CHECK_GT(orbit.size(), 1);
66
67 // Store the orbit info.
68 const int orbit_index = orbits_.size();
69 has_symmetry_ = true;
70 orbit_sum_vars_.push_back(sum_var);
71 orbits_.Add(orbit);
72
73 // And fill var_to_orbit_index_.
74 const int new_size = integer_trail_->NumIntegerVariables().value() / 2;
75 if (var_to_orbit_index_.size() < new_size) {
76 var_to_orbit_index_.resize(new_size, -1);
77 }
78 DCHECK(VariableIsPositive(sum_var));
79 DCHECK_EQ(var_to_orbit_index_[GetPositiveOnlyIndex(sum_var)], -1);
80 var_to_orbit_index_[GetPositiveOnlyIndex(sum_var)] = orbit_index;
81 for (const IntegerVariable var : orbit) {
82 DCHECK(VariableIsPositive(var));
83 DCHECK_EQ(var_to_orbit_index_[GetPositiveOnlyIndex(var)], -1);
84 var_to_orbit_index_[GetPositiveOnlyIndex(var)] = orbit_index;
85 }
86}
87
88// What we do here is basically equivalent to adding all the possible
89// permutations (under the problem symmetry group) of the constraint together.
90// When we do that all variables in the same orbit will have the same
91// coefficient (TODO(user): think how to prove this properly and especially
92// that the scaling is in 1/orbit_size) and will each appear once. We then
93// substitute each sum by the sum over the orbit, and divide coefficient by
94// their gcd.
95//
96// Any solution of the original LP can be transformed to a solution of the
97// folded LP with the same objective. So the folded LP will give us a tight and
98// valid objective lower bound but with a lot less variables! This is an
99// adaptation of "LP folding" to use in a MIP context. Introducing the orbit sum
100// allow to propagates and add cuts as these sum are still integer for us.
101//
102// The only issue is regarding scaling of the constraints. Basically each
103// orbit sum variable will appear with a factor 1/orbit_size in the original
104// constraint.
105//
106// We will remap & scale the constraint.
107// If not possible, we will drop it for now.
109 bool* folded) {
110 if (!has_symmetry_) return true;
111
112 // We assume the constraint had basic preprocessing with tight lb/ub for
113 // instance. First pass is to compute the scaling factor.
114 int64_t scaling_factor = 1;
115 for (int i = 0; i < ct->num_terms; ++i) {
116 const IntegerVariable var = ct->vars[i];
117 CHECK(VariableIsPositive(var));
118
119 const int orbit_index = var_to_orbit_index_[GetPositiveOnlyIndex(var)];
120 if (orbit_index == -1 || orbit_sum_vars_[orbit_index] == var) {
121 // If we have an orbit of size one, or the variable is its own
122 // representative (orbit sum), skip.
123 continue;
124 }
125
126 // Update the scaling factor.
127 const int orbit_size = orbits_[orbit_index].size();
128 if (AtMinOrMaxInt64(CapProd(orbit_size, scaling_factor))) {
129 ++num_overflows_;
130 VLOG(2) << "SYMMETRY skip constraint due to overflow";
131 return false;
132 }
133 scaling_factor = std::lcm(scaling_factor, orbit_size);
134 }
135
136 if (scaling_factor == 1) {
137 // No symmetric variables.
138 return true;
139 }
140
141 if (folded != nullptr) *folded = true;
142
143 // We need to multiply each term by scaling_factor / orbit_size.
144 //
145 // TODO(user): Now that we know the actual coefficient we could scale less.
146 // Maybe the coefficient of an orbit_var is already divisible by orbit_size.
147 builder_.Clear();
148 for (int i = 0; i < ct->num_terms; ++i) {
149 const IntegerVariable var = ct->vars[i];
150 const IntegerValue coeff = ct->coeffs[i];
151
152 const int orbit_index = var_to_orbit_index_[GetPositiveOnlyIndex(var)];
153 if (orbit_index == -1 || orbit_sum_vars_[orbit_index] == var) {
154 const int64_t scaled_coeff = CapProd(coeff.value(), scaling_factor);
155 if (AtMinOrMaxInt64(scaled_coeff)) {
156 ++num_overflows_;
157 VLOG(2) << "SYMMETRY skip constraint due to overflow";
158 return false;
159 }
160 builder_.AddTerm(var, scaled_coeff);
161 } else {
162 const int64_t orbit_size = orbits_[orbit_index].size();
163 const int64_t factor = scaling_factor / orbit_size;
164 const int64_t scaled_coeff = CapProd(coeff.value(), factor);
165 if (AtMinOrMaxInt64(scaled_coeff)) {
166 ++num_overflows_;
167 VLOG(2) << "SYMMETRY skip constraint due to overflow";
168 return false;
169 }
170 builder_.AddTerm(orbit_sum_vars_[orbit_index], scaled_coeff);
171 }
172 }
173
174 if (AtMinOrMaxInt64(CapProd(ct->lb.value(), scaling_factor)) ||
175 AtMinOrMaxInt64(CapProd(ct->ub.value(), scaling_factor))) {
176 ++num_overflows_;
177 VLOG(2) << "SYMMETRY skip constraint due to lb/ub overflow";
178 return false;
179 }
180 if (!builder_.BuildIntoConstraintAndCheckOverflow(
181 ct->lb * scaling_factor, ct->ub * scaling_factor, ct)) {
182 ++num_overflows_;
183 VLOG(2) << "SYMMETRY skip constraint due to overflow";
184 return false;
185 }
186
187 // Dividing by gcd can help.
188 DivideByGCD(ct);
189 if (PossibleOverflow(*integer_trail_, *ct)) {
190 ++num_overflows_;
191 VLOG(2) << "SYMMETRY skip constraint due to overflow factor = "
192 << scaling_factor;
193 return false;
194 }
195
196 // TODO(user): In some cases, this constraint will propagate/fix directly
197 // the orbit sum variables, we might want to propagate this in the cp world?
198 // This migth also remove bad scaling.
199 return true;
200}
201
202int LinearConstraintSymmetrizer::OrbitIndex(IntegerVariable var) const {
203 if (!has_symmetry_) return -1;
204 return var_to_orbit_index_[GetPositiveOnlyIndex(var)];
205}
206
207bool LinearConstraintSymmetrizer::IsOrbitSumVar(IntegerVariable var) const {
208 if (!has_symmetry_) return false;
209 const int orbit_index = var_to_orbit_index_[GetPositiveOnlyIndex(var)];
210 return orbit_index >= 0 && orbit_sum_vars_[orbit_index] == var;
211}
212
214 IntegerVariable var) const {
215 if (!has_symmetry_) return true;
216 const int orbit_index = var_to_orbit_index_[GetPositiveOnlyIndex(var)];
217 return orbit_index == -1 || orbit_sum_vars_[orbit_index] == var;
218}
219
220namespace {
221
222const LinearConstraintManager::ConstraintIndex kInvalidConstraintIndex(-1);
223
224size_t ComputeHashOfTerms(const LinearConstraint& ct) {
225 DCHECK(std::is_sorted(ct.vars.get(), ct.vars.get() + ct.num_terms));
226 size_t hash = 0;
227 const int num_terms = ct.num_terms;
228 for (int i = 0; i < num_terms; ++i) {
229 hash = util_hash::Hash(ct.vars[i].value(), hash);
230 hash = util_hash::Hash(ct.coeffs[i].value(), hash);
231 }
232 return hash;
233}
234
235} // namespace
236
238 if (!VLOG_IS_ON(1)) return;
239 if (model_->Get<SharedStatistics>() == nullptr) return;
240
241 std::vector<std::pair<std::string, int64_t>> cut_stats;
242 for (const auto& entry : type_to_num_cuts_) {
243 cut_stats.push_back({absl::StrCat("cut/", entry.first), entry.second});
244 }
245 model_->Mutable<SharedStatistics>()->AddStats(cut_stats);
246}
247
248void LinearConstraintManager::RescaleActiveCounts(const double scaling_factor) {
249 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
250 constraint_infos_[i].active_count *= scaling_factor;
251 }
252 constraint_active_count_increase_ *= scaling_factor;
253 VLOG(3) << "Rescaled active counts by " << scaling_factor;
254}
255
256bool LinearConstraintManager::MaybeRemoveSomeInactiveConstraints(
257 glop::BasisState* solution_state) {
258 if (solution_state->IsEmpty()) return false; // Mainly to simplify tests.
259 const glop::RowIndex num_rows(lp_constraints_.size());
260 const glop::ColIndex num_cols =
261 solution_state->statuses.size() - RowToColIndex(num_rows);
262 int new_size = 0;
263 for (int i = 0; i < num_rows; ++i) {
264 const ConstraintIndex constraint_index = lp_constraints_[i];
265 if (constraint_infos_[constraint_index].constraint.num_terms == 0) {
266 // Remove empty constraint.
267 //
268 // TODO(user): If the constraint is infeasible we could detect unsat
269 // right away, but hopefully this is a case where the propagation part
270 // of the solver can detect that too.
271 constraint_infos_[constraint_index].is_in_lp = false;
272 continue;
273 }
274
275 // Constraints that are not tight in the current solution have a basic
276 // status. We remove the ones that have been inactive in the last recent
277 // solves.
278 //
279 // TODO(user): More advanced heuristics might perform better, I didn't do
280 // a lot of tuning experiments yet.
281 const glop::VariableStatus row_status =
282 solution_state->statuses[num_cols + glop::ColIndex(i)];
283 if (row_status == glop::VariableStatus::BASIC) {
284 constraint_infos_[constraint_index].inactive_count++;
285 if (constraint_infos_[constraint_index].inactive_count >
286 sat_parameters_.max_consecutive_inactive_count()) {
287 constraint_infos_[constraint_index].is_in_lp = false;
288 continue; // Remove it.
289 }
290 } else {
291 // Only count consecutive inactivities.
292 constraint_infos_[constraint_index].inactive_count = 0;
293 }
294
295 lp_constraints_[new_size] = constraint_index;
296 solution_state->statuses[num_cols + glop::ColIndex(new_size)] = row_status;
297 new_size++;
298 }
299 const int num_removed_constraints = lp_constraints_.size() - new_size;
300 lp_constraints_.resize(new_size);
301 solution_state->statuses.resize(num_cols + glop::ColIndex(new_size));
302 if (num_removed_constraints > 0) {
303 VLOG(3) << "Removed " << num_removed_constraints << " constraints";
304 }
305 return num_removed_constraints > 0;
306}
307
308// Because sometimes we split a == constraint in two (>= and <=), it makes sense
309// to detect duplicate constraints and merge bounds. This is also relevant if
310// we regenerate identical cuts for some reason.
311LinearConstraintManager::ConstraintIndex LinearConstraintManager::Add(
312 LinearConstraint ct, bool* added, bool* folded) {
313 DCHECK_GT(ct.num_terms, 0);
314 DCHECK(!PossibleOverflow(integer_trail_, ct)) << ct.DebugString();
315 DCHECK(NoDuplicateVariable(ct));
316 SimplifyConstraint(&ct);
317 DivideByGCD(&ct);
319 DCHECK(DebugCheckConstraint(ct));
320
321 // If configured, store instead the folded version of this constraint.
322 // TODO(user): Shall we simplify again?
323 if (symmetrizer_->HasSymmetry() &&
324 !symmetrizer_->FoldLinearConstraint(&ct, folded)) {
325 return kInvalidConstraintIndex;
326 }
327 CHECK(std::is_sorted(ct.VarsAsSpan().begin(), ct.VarsAsSpan().end()));
328
329 // If an identical constraint exists, only updates its bound.
330 const size_t key = ComputeHashOfTerms(ct);
331 if (equiv_constraints_.contains(key)) {
332 const ConstraintIndex ct_index = equiv_constraints_[key];
333 if (constraint_infos_[ct_index].constraint.IsEqualIgnoringBounds(ct)) {
334 bool tightened = false;
335 if (ct.lb > constraint_infos_[ct_index].constraint.lb) {
336 tightened = true;
337 if (constraint_infos_[ct_index].is_in_lp) current_lp_is_changed_ = true;
338 constraint_infos_[ct_index].constraint.lb = ct.lb;
339 }
340 if (ct.ub < constraint_infos_[ct_index].constraint.ub) {
341 tightened = true;
342 if (constraint_infos_[ct_index].is_in_lp) current_lp_is_changed_ = true;
343 constraint_infos_[ct_index].constraint.ub = ct.ub;
344 }
345 if (added != nullptr) *added = tightened;
346 if (tightened) {
347 ++num_merged_constraints_;
348 FillDerivedFields(&constraint_infos_[ct_index]);
349 }
350 return ct_index;
351 }
352 }
353
354 if (added != nullptr) *added = true;
355 const ConstraintIndex ct_index(constraint_infos_.size());
356 ConstraintInfo ct_info;
357 ct_info.constraint = std::move(ct);
358 ct_info.l2_norm = ComputeL2Norm(ct_info.constraint);
359 FillDerivedFields(&ct_info);
360 ct_info.hash = key;
361 equiv_constraints_[key] = ct_index;
362 ct_info.active_count = constraint_active_count_increase_;
363 constraint_infos_.push_back(std::move(ct_info));
364 return ct_index;
365}
366
367bool LinearConstraintManager::UpdateConstraintLb(glop::RowIndex index_in_lp,
368 IntegerValue new_lb) {
369 const ConstraintIndex index = lp_constraints_[index_in_lp.value()];
370 ConstraintInfo& info = constraint_infos_[index];
371 if (new_lb <= info.constraint.lb) return false;
372 ++num_constraint_updates_;
373 current_lp_is_changed_ = true;
374 info.constraint.lb = new_lb;
375 return true;
376}
377
378bool LinearConstraintManager::UpdateConstraintUb(glop::RowIndex index_in_lp,
379 IntegerValue new_ub) {
380 const ConstraintIndex index = lp_constraints_[index_in_lp.value()];
381 ConstraintInfo& info = constraint_infos_[index];
382 if (new_ub >= info.constraint.ub) return false;
383 ++num_constraint_updates_;
384 current_lp_is_changed_ = true;
385 info.constraint.ub = new_ub;
386 return true;
387}
388
389void LinearConstraintManager::ComputeObjectiveParallelism(
390 const ConstraintIndex ct_index) {
391 CHECK(objective_is_defined_);
392 // lazy computation of objective norm.
393 if (!objective_norm_computed_) {
394 objective_l2_norm_ = std::sqrt(sum_of_squared_objective_coeffs_);
395 objective_norm_computed_ = true;
396 }
397 CHECK_GT(objective_l2_norm_, 0.0);
398
399 constraint_infos_[ct_index].objective_parallelism_computed = true;
400 if (constraint_infos_[ct_index].l2_norm == 0.0) {
401 constraint_infos_[ct_index].objective_parallelism = 0.0;
402 return;
403 }
404
405 const LinearConstraint& lc = constraint_infos_[ct_index].constraint;
406 double unscaled_objective_parallelism = 0.0;
407 for (int i = 0; i < lc.num_terms; ++i) {
408 const IntegerVariable var = lc.vars[i];
409 const auto it = objective_map_.find(var);
410 if (it == objective_map_.end()) continue;
411 unscaled_objective_parallelism += it->second * ToDouble(lc.coeffs[i]);
412 }
413 const double objective_parallelism =
414 unscaled_objective_parallelism /
415 (constraint_infos_[ct_index].l2_norm * objective_l2_norm_);
416 constraint_infos_[ct_index].objective_parallelism =
417 std::abs(objective_parallelism);
418}
419
420// Same as Add(), but logs some information about the newly added constraint.
421// Cuts are also handled slightly differently than normal constraints.
423 std::string extra_info) {
424 ++num_add_cut_calls_;
425 if (ct.num_terms == 0) return false;
426
427 const double activity = ComputeActivity(ct, expanded_lp_solution_);
428 const double violation =
429 std::max(activity - ToDouble(ct.ub), ToDouble(ct.lb) - activity);
430 const double l2_norm = ComputeL2Norm(ct);
431
432 // Only add cut with sufficient efficacy.
433 if (violation / l2_norm < 1e-4) {
434 VLOG(3) << "BAD Cut '" << type_name << "'"
435 << " size=" << ct.num_terms
436 << " max_magnitude=" << ComputeInfinityNorm(ct)
437 << " norm=" << l2_norm << " violation=" << violation
438 << " eff=" << violation / l2_norm << " " << extra_info;
439 return false;
440 }
441
442 // TODO(user): We could prevent overflow by dividing more. Note that mainly
443 // happen with super large variable domain since we usually restrict the size
444 // of the generated coefficients in our cuts. So it shouldn't be that
445 // important.
446 if (PossibleOverflow(integer_trail_, ct)) return false;
447
448 bool added = false;
449 const ConstraintIndex ct_index = Add(std::move(ct), &added);
450
451 // We only mark the constraint as a cut if it is not an update of an already
452 // existing one.
453 if (!added) return false;
454
455 // TODO(user): Use better heuristic here for detecting good cuts and mark
456 // them undeletable.
457 constraint_infos_[ct_index].is_deletable = true;
458
459 VLOG(2) << "Cut '" << type_name << "'"
460 << " size=" << constraint_infos_[ct_index].constraint.num_terms
461 << " max_magnitude="
462 << ComputeInfinityNorm(constraint_infos_[ct_index].constraint)
463 << " norm=" << l2_norm << " violation=" << violation
464 << " eff=" << violation / l2_norm << " " << extra_info;
465
466 num_cuts_++;
467 num_deletable_constraints_++;
468 type_to_num_cuts_[type_name]++;
469 return true;
470}
471
472void LinearConstraintManager::PermanentlyRemoveSomeConstraints() {
473 std::vector<double> deletable_constraint_counts;
474 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
475 if (constraint_infos_[i].is_deletable && !constraint_infos_[i].is_in_lp) {
476 deletable_constraint_counts.push_back(constraint_infos_[i].active_count);
477 }
478 }
479 if (deletable_constraint_counts.empty()) return;
480 std::sort(deletable_constraint_counts.begin(),
481 deletable_constraint_counts.end());
482
483 // We will delete the oldest (in the order they where added) cleanup target
484 // constraints with a count lower or equal to this.
485 double active_count_threshold = std::numeric_limits<double>::infinity();
486 if (sat_parameters_.cut_cleanup_target() <
487 deletable_constraint_counts.size()) {
488 active_count_threshold =
489 deletable_constraint_counts[sat_parameters_.cut_cleanup_target()];
490 }
491
492 ConstraintIndex new_size(0);
493 equiv_constraints_.clear();
494 util_intops::StrongVector<ConstraintIndex, ConstraintIndex> index_mapping(
495 constraint_infos_.size());
496 int num_deleted_constraints = 0;
497 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
498 if (constraint_infos_[i].is_deletable && !constraint_infos_[i].is_in_lp &&
499 constraint_infos_[i].active_count <= active_count_threshold &&
500 num_deleted_constraints < sat_parameters_.cut_cleanup_target()) {
501 ++num_deleted_constraints;
502 continue;
503 }
504
505 if (i != new_size) {
506 constraint_infos_[new_size] = std::move(constraint_infos_[i]);
507 }
508 index_mapping[i] = new_size;
509
510 // Make sure we recompute the hash_map of identical constraints.
511 equiv_constraints_[constraint_infos_[new_size].hash] = new_size;
512 new_size++;
513 }
514 constraint_infos_.resize(new_size.value());
515
516 // Also update lp_constraints_
517 for (int i = 0; i < lp_constraints_.size(); ++i) {
518 lp_constraints_[i] = index_mapping[lp_constraints_[i]];
519 }
520
521 if (num_deleted_constraints > 0) {
522 VLOG(3) << "Constraint manager cleanup: #deleted:"
523 << num_deleted_constraints;
524 }
525 num_deletable_constraints_ -= num_deleted_constraints;
526}
527
529 IntegerValue coeff) {
530 if (coeff == IntegerValue(0)) return;
531 objective_is_defined_ = true;
532 if (!VariableIsPositive(var)) {
533 var = NegationOf(var);
534 coeff = -coeff;
535 }
536 const double coeff_as_double = ToDouble(coeff);
537 const auto insert = objective_map_.insert({var, coeff_as_double});
538 CHECK(insert.second)
539 << "SetObjectiveCoefficient() called twice with same variable";
540 sum_of_squared_objective_coeffs_ += coeff_as_double * coeff_as_double;
541}
542
543// TODO(user): Also consider partial gcd simplification? see presolve.
544bool LinearConstraintManager::SimplifyConstraint(LinearConstraint* ct) {
545 bool term_changed = false;
546
547 IntegerValue min_sum(0);
548 IntegerValue max_sum(0);
549 IntegerValue max_magnitude(0);
550 IntegerValue min_magnitude = kMaxIntegerValue;
551 int new_size = 0;
552 const int num_terms = ct->num_terms;
553 for (int i = 0; i < num_terms; ++i) {
554 const IntegerVariable var = ct->vars[i];
555 const IntegerValue coeff = ct->coeffs[i];
556 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
557 const IntegerValue ub = integer_trail_.LevelZeroUpperBound(var);
558
559 // For now we do not change ct, but just compute its new_size if we where
560 // to remove a fixed term.
561 if (lb == ub) continue;
562 ++new_size;
563
564 const IntegerValue magnitude = IntTypeAbs(coeff);
565 max_magnitude = std::max(max_magnitude, magnitude);
566 min_magnitude = std::min(min_magnitude, magnitude);
567 if (coeff > 0.0) {
568 min_sum += coeff * lb;
569 max_sum += coeff * ub;
570 } else {
571 min_sum += coeff * ub;
572 max_sum += coeff * lb;
573 }
574 }
575
576 // Shorten the constraint if needed.
577 IntegerValue fixed_part = 0;
578 if (new_size < num_terms) {
579 term_changed = true;
580 ++num_shortened_constraints_;
581 new_size = 0;
582 for (int i = 0; i < num_terms; ++i) {
583 const IntegerVariable var = ct->vars[i];
584 const IntegerValue coeff = ct->coeffs[i];
585 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
586 const IntegerValue ub = integer_trail_.LevelZeroUpperBound(var);
587 if (lb == ub) {
588 fixed_part += coeff * lb;
589 continue;
590 }
591 ct->vars[new_size] = var;
592 ct->coeffs[new_size] = coeff;
593 ++new_size;
594 }
595 ct->resize(new_size);
596 }
597
598 // Clear constraints that are always true.
599 // We rely on the deletion code to remove them eventually.
600 if (min_sum + fixed_part >= ct->lb && max_sum + fixed_part <= ct->ub) {
601 ct->resize(0);
602 ct->lb = 0;
603 ct->ub = 0;
604 return true;
605 }
606
607 // Make sure bounds are finite.
608 ct->lb = std::max(ct->lb, min_sum + fixed_part);
609 ct->ub = std::min(ct->ub, max_sum + fixed_part);
610 ct->lb -= fixed_part;
611 ct->ub -= fixed_part;
612
613 // The variable can be shifted and complemented so we have constraints of
614 // the form:
615 // ... + |coeff| * X >= threshold_ub
616 // ... + |coeff| * X' >= threshold_lb
617 // In both case if coeff is big, we can reduce it and update the rhs
618 // accordingly.
619 const IntegerValue threshold_ub = max_sum - ct->ub;
620 const IntegerValue threshold_lb = ct->lb - min_sum;
621 const IntegerValue threshold = std::max(threshold_lb, threshold_ub);
622 CHECK_GT(threshold, 0); // Since we aborted for trivial constraint.
623
624 // TODO(user): In some case, we could split the constraint to reduce one of
625 // them further. But not sure that is a good thing.
626 if (threshold_ub > 0 && threshold_lb > 0 && threshold_lb != threshold_ub) {
627 if (max_magnitude > std::min(threshold_lb, threshold_ub)) {
628 ++num_split_constraints_;
629 }
630 }
631
632 // TODO(user): For constraint with both bound, we could reduce further for
633 // coefficient between threshold - min_magnitude and min(t_lb, t_ub).
634 const IntegerValue second_threshold = std::max(
635 {CeilRatio(threshold, IntegerValue(2)), threshold - min_magnitude,
636 std::min(threshold_lb, threshold_ub)});
637 if (max_magnitude > second_threshold) {
638 const int num_terms = ct->num_terms;
639 for (int i = 0; i < num_terms; ++i) {
640 // In all cases, we reason on a transformed constraint where the term
641 // is max_value - |coeff| * positive_X. If we change coeff, and
642 // retransform the constraint, we need to change the rhs by the
643 // constant term left.
644 const IntegerValue coeff = ct->coeffs[i];
645 const IntegerVariable var = ct->vars[i];
646 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
647 const IntegerValue ub = integer_trail_.LevelZeroUpperBound(var);
648 if (coeff > threshold) {
649 term_changed = true;
650 ++num_coeff_strenghtening_;
651 ct->coeffs[i] = threshold;
652 ct->ub -= (coeff - threshold) * ub;
653 ct->lb -= (coeff - threshold) * lb;
654 } else if (coeff > second_threshold && coeff < threshold) {
655 term_changed = true;
656 ++num_coeff_strenghtening_;
657 ct->coeffs[i] = second_threshold;
658 ct->ub -= (coeff - second_threshold) * ub;
659 ct->lb -= (coeff - second_threshold) * lb;
660 } else if (coeff < -threshold) {
661 term_changed = true;
662 ++num_coeff_strenghtening_;
663 ct->coeffs[i] = -threshold;
664 ct->ub -= (coeff + threshold) * lb;
665 ct->lb -= (coeff + threshold) * ub;
666 } else if (coeff < -second_threshold && coeff > -threshold) {
667 term_changed = true;
668 ++num_coeff_strenghtening_;
669 ct->coeffs[i] = -second_threshold;
670 ct->ub -= (coeff + second_threshold) * lb;
671 ct->lb -= (coeff + second_threshold) * ub;
672 }
673 }
674 }
675
676 return term_changed;
677}
678
679void LinearConstraintManager::FillDerivedFields(ConstraintInfo* info) {
680 IntegerValue min_sum(0);
681 IntegerValue max_sum(0);
682 const int num_terms = info->constraint.num_terms;
683 for (int i = 0; i < num_terms; ++i) {
684 const IntegerVariable var = info->constraint.vars[i];
685 const IntegerValue coeff = info->constraint.coeffs[i];
686 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
687 const IntegerValue ub = integer_trail_.LevelZeroUpperBound(var);
688 if (coeff > 0.0) {
689 min_sum += coeff * lb;
690 max_sum += coeff * ub;
691 } else {
692 min_sum += coeff * ub;
693 max_sum += coeff * lb;
694 }
695 }
696 info->constraint.lb = std::max(min_sum, info->constraint.lb);
697 info->constraint.ub = std::min(max_sum, info->constraint.ub);
698 CHECK_NE(CapSub(info->constraint.ub.value(), info->constraint.lb.value()),
699 std::numeric_limits<int64_t>::max());
700 info->lb_is_trivial = min_sum >= info->constraint.lb;
701 info->ub_is_trivial = max_sum <= info->constraint.ub;
702}
703
705 int* num_new_constraints) {
706 VLOG(3) << "Enter ChangeLP, scan " << constraint_infos_.size()
707 << " constraints";
708 const double saved_dtime = dtime_;
709 std::vector<std::pair<ConstraintIndex, double>> new_constraints_by_score;
710 std::vector<double> new_constraints_efficacies;
711 std::vector<double> new_constraints_orthogonalities;
712
713 const bool simplify_constraints =
714 integer_trail_.num_level_zero_enqueues() > last_simplification_timestamp_;
715 last_simplification_timestamp_ = integer_trail_.num_level_zero_enqueues();
716
717 // We keep any constraints that is already present, and otherwise, we add the
718 // ones that are currently not satisfied by at least "tolerance" to the set
719 // of potential new constraints.
720 bool rescale_active_count = false;
721 const double tolerance = 1e-6;
722 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
723 // Inprocessing of the constraint.
724 if (simplify_constraints &&
725 SimplifyConstraint(&constraint_infos_[i].constraint)) {
726 ++num_simplifications_;
727
728 // Note that the canonicalization shouldn't be needed since the order
729 // of the variable is not changed by the simplification, and we only
730 // reduce the coefficients at both end of the spectrum.
731 DivideByGCD(&constraint_infos_[i].constraint);
732 DCHECK(DebugCheckConstraint(constraint_infos_[i].constraint));
733
734 constraint_infos_[i].objective_parallelism_computed = false;
735 constraint_infos_[i].l2_norm =
736 ComputeL2Norm(constraint_infos_[i].constraint);
737 FillDerivedFields(&constraint_infos_[i]);
738
739 if (constraint_infos_[i].is_in_lp) current_lp_is_changed_ = true;
740 equiv_constraints_.erase(constraint_infos_[i].hash);
741 constraint_infos_[i].hash =
742 ComputeHashOfTerms(constraint_infos_[i].constraint);
743
744 // TODO(user): Because we simplified this constraint, it is possible that
745 // it is now a duplicate of another one. Merge them.
746 equiv_constraints_[constraint_infos_[i].hash] = i;
747 }
748
749 if (constraint_infos_[i].is_in_lp) continue;
750
751 // ComputeActivity() often represent the bulk of the time spent in
752 // ChangeLP().
753 dtime_ +=
754 1.7e-9 * static_cast<double>(constraint_infos_[i].constraint.num_terms);
755 const double activity =
756 ComputeActivity(constraint_infos_[i].constraint, expanded_lp_solution_);
757 const double lb_violation =
758 ToDouble(constraint_infos_[i].constraint.lb) - activity;
759 const double ub_violation =
760 activity - ToDouble(constraint_infos_[i].constraint.ub);
761 const double violation = std::max(lb_violation, ub_violation);
762 if (violation >= tolerance) {
763 constraint_infos_[i].inactive_count = 0;
764 if (objective_is_defined_ &&
765 !constraint_infos_[i].objective_parallelism_computed) {
766 ComputeObjectiveParallelism(i);
767 } else if (!objective_is_defined_) {
768 constraint_infos_[i].objective_parallelism = 0.0;
769 }
770
771 const double score = violation / constraint_infos_[i].l2_norm +
772 constraint_infos_[i].objective_parallelism;
773 new_constraints_by_score.push_back({i, score});
774
775 if (constraint_infos_[i].is_deletable) {
776 constraint_infos_[i].active_count += constraint_active_count_increase_;
777 if (constraint_infos_[i].active_count >
778 sat_parameters_.cut_max_active_count_value()) {
779 rescale_active_count = true;
780 }
781 }
782 }
783 }
784
785 // Bump activities of active constraints in LP.
786 if (solution_state != nullptr) {
787 const glop::RowIndex num_rows(lp_constraints_.size());
788 const glop::ColIndex num_cols =
789 solution_state->statuses.size() - RowToColIndex(num_rows);
790
791 for (int i = 0; i < num_rows; ++i) {
792 const ConstraintIndex constraint_index = lp_constraints_[i];
793 const glop::VariableStatus row_status =
794 solution_state->statuses[num_cols + glop::ColIndex(i)];
795 if (row_status != glop::VariableStatus::BASIC &&
796 constraint_infos_[constraint_index].is_deletable) {
797 constraint_infos_[constraint_index].active_count +=
798 constraint_active_count_increase_;
799 if (constraint_infos_[constraint_index].active_count >
800 sat_parameters_.cut_max_active_count_value()) {
801 rescale_active_count = true;
802 }
803 }
804 }
805 }
806
807 if (rescale_active_count) {
808 CHECK_GT(sat_parameters_.cut_max_active_count_value(), 0.0);
809 RescaleActiveCounts(1.0 / sat_parameters_.cut_max_active_count_value());
810 }
811
812 // Update the increment counter.
813 constraint_active_count_increase_ *=
814 1.0 / sat_parameters_.cut_active_count_decay();
815
816 // Remove constraints from the current LP that have been inactive for a while.
817 // We do that after we computed new_constraints so we do not need to iterate
818 // over the just deleted constraints.
819 if (MaybeRemoveSomeInactiveConstraints(solution_state)) {
820 current_lp_is_changed_ = true;
821 }
822
823 // Note that the algo below is in O(limit * new_constraint). In order to
824 // limit spending too much time on this, we first sort all the constraints
825 // with an imprecise score (no orthogonality), then limit the size of the
826 // vector of constraints to precisely score, then we do the actual scoring.
827 //
828 // On problem crossword_opt_grid-19.05_dict-80_sat with linearization_level=2,
829 // new_constraint.size() > 1.5M.
830 //
831 // TODO(user): This blowup factor could be adaptative w.r.t. the constraint
832 // limit.
833 const int kBlowupFactor = 4;
834 const int current_size = static_cast<int>(new_constraints_by_score.size());
835 int constraint_limit =
836 std::min(sat_parameters_.new_constraints_batch_size(), current_size);
837 if (lp_constraints_.empty()) {
838 constraint_limit = std::min(1000, current_size);
839 }
840 VLOG(3) << " - size = " << current_size << ", limit = " << constraint_limit;
841
842 std::stable_sort(new_constraints_by_score.begin(),
843 new_constraints_by_score.end(),
844 [&](std::pair<ConstraintIndex, double> a,
845 std::pair<ConstraintIndex, double> b) {
846 return a.second > b.second;
847 });
848 if (new_constraints_by_score.size() > kBlowupFactor * constraint_limit) {
849 VLOG(3) << "Resize candidate constraints from "
850 << new_constraints_by_score.size() << " down to "
851 << kBlowupFactor * constraint_limit;
852 new_constraints_by_score.resize(kBlowupFactor * constraint_limit);
853 }
854
855 int num_added = 0;
856 TimeLimitCheckEveryNCalls time_limit_check(1000, time_limit_);
857 ConstraintIndex last_added_candidate = kInvalidConstraintIndex;
858 std::vector<double> orthogonality_score(new_constraints_by_score.size(), 1.0);
859 for (int i = 0; i < constraint_limit; ++i) {
860 // Iterate through all new constraints and select the one with the best
861 // score.
862 //
863 // TODO(user): find better algo, this does 1000 * 4000 scalar product!
864 double best_score = 0.0;
865 ConstraintIndex best_candidate = kInvalidConstraintIndex;
866 for (int j = 0; j < new_constraints_by_score.size(); ++j) {
867 // Checks the time limit, and returns if the lp has changed.
868 if (time_limit_check.LimitReached()) return current_lp_is_changed_;
869
870 const ConstraintIndex new_index = new_constraints_by_score[j].first;
871 if (constraint_infos_[new_index].is_in_lp) continue;
872
873 if (last_added_candidate != kInvalidConstraintIndex) {
874 const double current_orthogonality =
875 1.0 - (std::abs(ScalarProduct(
876 constraint_infos_[last_added_candidate].constraint,
877 constraint_infos_[new_index].constraint)) /
878 (constraint_infos_[last_added_candidate].l2_norm *
879 constraint_infos_[new_index].l2_norm));
880 orthogonality_score[j] =
881 std::min(orthogonality_score[j], current_orthogonality);
882 }
883
884 // NOTE(user): It is safe to not add this constraint as the constraint
885 // that is almost parallel to this constraint is present in the LP or is
886 // inactive for a long time and is removed from the LP. In either case,
887 // this constraint is not adding significant value and is only making the
888 // LP larger.
889 if (orthogonality_score[j] <
890 sat_parameters_.min_orthogonality_for_lp_constraints()) {
891 continue;
892 }
893
894 // TODO(user): Experiment with different weights or different
895 // functions for computing score.
896 const double score =
897 orthogonality_score[j] + new_constraints_by_score[j].second;
898 CHECK_GE(score, 0.0);
899 if (score > best_score || best_candidate == kInvalidConstraintIndex) {
900 best_score = score;
901 best_candidate = new_index;
902 }
903 }
904
905 if (best_candidate != kInvalidConstraintIndex) {
906 // Add the best constraint in the LP.
907 constraint_infos_[best_candidate].is_in_lp = true;
908 // Note that it is important for LP incremental solving that the old
909 // constraints stays at the same position in this list (and thus in the
910 // returned GetLp()).
911 ++num_added;
912 current_lp_is_changed_ = true;
913 lp_constraints_.push_back(best_candidate);
914 last_added_candidate = best_candidate;
915 } else {
916 // Abort the addition loop.
917 break;
918 }
919 }
920
921 if (num_new_constraints != nullptr) {
922 *num_new_constraints = num_added;
923 }
924 if (num_added > 0) {
925 // We update the solution sate to match the new LP size.
926 VLOG(3) << "Added " << num_added << " constraints.";
927 solution_state->statuses.resize(solution_state->statuses.size() + num_added,
929 }
930
931 // TODO(user): Instead of comparing num_deletable_constraints with cut
932 // limit, compare number of deletable constraints not in lp against the limit.
933 if (num_deletable_constraints_ > sat_parameters_.max_num_cuts()) {
934 PermanentlyRemoveSomeConstraints();
935 }
936
937 time_limit_->AdvanceDeterministicTime(dtime_ - saved_dtime);
938
939 // The LP changed only if we added new constraints or if some constraints
940 // already inside changed (simplification or tighter bounds).
941 if (current_lp_is_changed_) {
942 current_lp_is_changed_ = false;
943 return true;
944 }
945 return false;
946}
947
949 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
950 if (constraint_infos_[i].is_in_lp) continue;
951 if (constraint_infos_[i].constraint.num_terms == 0) continue;
952
953 constraint_infos_[i].is_in_lp = true;
954 lp_constraints_.push_back(i);
955 }
956}
957
959 const LinearConstraint& cut) {
960 const DebugSolution* debug_solution = model_->Get<DebugSolution>();
961 if (debug_solution == nullptr || debug_solution->proto_values.empty()) {
962 return true;
963 }
964
965 absl::int128 activity(0);
966 for (int i = 0; i < cut.num_terms; ++i) {
967 const IntegerVariable var = cut.vars[i];
968 const IntegerValue coeff = cut.coeffs[i];
969 if (var >= debug_solution->ivar_has_value.size() ||
970 !debug_solution->ivar_has_value[var]) {
971 return true;
972 }
973 activity +=
974 absl::int128(coeff.value()) * debug_solution->ivar_values[var].value();
975 }
976 if (activity > cut.ub.value() || activity < cut.lb.value()) {
977 LOG(INFO) << cut.DebugString();
978 LOG(INFO) << "activity " << activity << " not in [" << cut.lb << ","
979 << cut.ub << "]";
980 return false;
981 }
982 return true;
983}
984
986 if (reduced_costs_is_cached_) return;
987 reduced_costs_is_cached_ = true;
988
989 const absl::int128 ub = reduced_cost_constraint_.ub.value();
990 absl::int128 level_zero_lb = 0;
991 for (int i = 0; i < reduced_cost_constraint_.num_terms; ++i) {
992 IntegerVariable var = reduced_cost_constraint_.vars[i];
993 IntegerValue coeff = reduced_cost_constraint_.coeffs[i];
994 if (coeff < 0) {
995 coeff = -coeff;
996 var = NegationOf(var);
997 }
998
999 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
1000 level_zero_lb += absl::int128(coeff.value()) * absl::int128(lb.value());
1001
1002 if (lb == integer_trail_.LevelZeroUpperBound(var)) continue;
1003 const LiteralIndex lit = integer_encoder_.GetAssociatedLiteral(
1004 IntegerLiteral::GreaterOrEqual(var, lb + 1));
1005 if (lit != kNoLiteralIndex) {
1006 reduced_costs_map_[Literal(lit)] = coeff;
1007 }
1008 }
1009 const absl::int128 gap = ub - level_zero_lb;
1010 if (gap > 0) {
1011 reduced_costs_gap_ = gap;
1012 } else {
1013 reduced_costs_gap_ = 0;
1014 reduced_costs_map_.clear();
1015 }
1016}
1017
1019 LinearConstraint ct, absl::string_view name,
1021 if (ct.num_terms == 0) return;
1022 const double normalized_violation = ct.NormalizedViolation(lp_solution);
1023 cuts_.Add({std::string(name), std::move(ct)}, normalized_violation);
1024}
1025
1027 for (CutCandidate& candidate : *cuts_.MutableUnorderedElements()) {
1028 manager->AddCut(std::move(candidate.cut), candidate.name);
1029 }
1030 cuts_.Clear();
1031}
1032
1033} // namespace sat
1034} // namespace operations_research
IntegerValue LevelZeroUpperBound(IntegerVariable var) const
Definition integer.h:1657
IntegerValue LevelZeroLowerBound(IntegerVariable var) const
Definition integer.h:1650
ConstraintIndex Add(LinearConstraint ct, bool *added=nullptr, bool *folded=nullptr)
bool ChangeLp(glop::BasisState *solution_state, int *num_new_constraints=nullptr)
bool AddCut(LinearConstraint ct, std::string type_name, std::string extra_info="")
bool UpdateConstraintUb(glop::RowIndex index_in_lp, IntegerValue new_ub)
bool UpdateConstraintLb(glop::RowIndex index_in_lp, IntegerValue new_lb)
void SetObjectiveCoefficient(IntegerVariable var, IntegerValue coeff)
bool FoldLinearConstraint(LinearConstraint *ct, bool *folded=nullptr)
void AddSymmetryOrbit(IntegerVariable sum_var, absl::Span< const IntegerVariable > orbit)
void TransferToManager(LinearConstraintManager *manager)
void AddCut(LinearConstraint ct, absl::string_view name, const util_intops::StrongVector< IntegerVariable, double > &lp_solution)
double ComputeActivity(const LinearConstraint &constraint, const util_intops::StrongVector< IntegerVariable, double > &values)
void DivideByGCD(LinearConstraint *constraint)
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
IntType IntTypeAbs(IntType t)
IntegerValue CeilRatio(IntegerValue dividend, IntegerValue positive_divisor)
const LiteralIndex kNoLiteralIndex(-1)
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Definition integer.cc:52
bool PossibleOverflow(const IntegerTrail &integer_trail, const LinearConstraint &constraint)
IntegerValue ComputeInfinityNorm(const LinearConstraint &ct)
double ComputeL2Norm(const LinearConstraint &ct)
PositiveOnlyIndex GetPositiveOnlyIndex(IntegerVariable var)
void MakeAllVariablesPositive(LinearConstraint *constraint)
bool NoDuplicateVariable(const LinearConstraint &ct)
bool VariableIsPositive(IntegerVariable i)
double ToDouble(IntegerValue value)
double ScalarProduct(const LinearConstraint &ct1, const LinearConstraint &ct2)
OR-Tools root namespace.
bool AtMinOrMaxInt64(int64_t x)
int64_t CapSub(int64_t x, int64_t y)
int64_t CapProd(int64_t x, int64_t y)
uint64_t Hash(uint64_t num, uint64_t c)
Definition hash.h:73
util_intops::StrongVector< IntegerVariable, bool > ivar_has_value
util_intops::StrongVector< IntegerVariable, IntegerValue > ivar_values
static IntegerLiteral GreaterOrEqual(IntegerVariable i, IntegerValue bound)
std::unique_ptr< IntegerValue[]> coeffs
std::unique_ptr< IntegerVariable[]> vars
absl::Span< const IntegerVariable > VarsAsSpan() const
double NormalizedViolation(const util_intops::StrongVector< IntegerVariable, double > &lp_values) const