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