Google OR-Tools v9.11
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-2024 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 <string>
23#include <utility>
24#include <vector>
25
26#include "absl/container/btree_map.h"
27#include "absl/container/flat_hash_map.h"
28#include "absl/log/check.h"
29#include "absl/meta/type_traits.h"
30#include "absl/strings/str_cat.h"
31#include "absl/strings/string_view.h"
32#include "ortools/base/hash.h"
37#include "ortools/sat/integer.h"
39#include "ortools/sat/model.h"
40#include "ortools/sat/sat_parameters.pb.h"
42#include "ortools/sat/util.h"
46
47namespace operations_research {
48namespace sat {
49
50namespace {
51
52const LinearConstraintManager::ConstraintIndex kInvalidConstraintIndex(-1);
53
54size_t ComputeHashOfTerms(const LinearConstraint& ct) {
55 DCHECK(std::is_sorted(ct.vars.get(), ct.vars.get() + ct.num_terms));
56 size_t hash = 0;
57 const int num_terms = ct.num_terms;
58 for (int i = 0; i < num_terms; ++i) {
59 hash = util_hash::Hash(ct.vars[i].value(), hash);
60 hash = util_hash::Hash(ct.coeffs[i].value(), hash);
61 }
62 return hash;
63}
64
65} // namespace
66
68 if (!VLOG_IS_ON(1)) return;
69 if (model_->Get<SharedStatistics>() == nullptr) return;
70
71 std::vector<std::pair<std::string, int64_t>> cut_stats;
72 for (const auto& entry : type_to_num_cuts_) {
73 cut_stats.push_back({absl::StrCat("cut/", entry.first), entry.second});
74 }
75 model_->Mutable<SharedStatistics>()->AddStats(cut_stats);
76}
77
78void LinearConstraintManager::RescaleActiveCounts(const double scaling_factor) {
79 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
80 constraint_infos_[i].active_count *= scaling_factor;
81 }
82 constraint_active_count_increase_ *= scaling_factor;
83 VLOG(3) << "Rescaled active counts by " << scaling_factor;
84}
85
86bool LinearConstraintManager::MaybeRemoveSomeInactiveConstraints(
87 glop::BasisState* solution_state) {
88 if (solution_state->IsEmpty()) return false; // Mainly to simplify tests.
89 const glop::RowIndex num_rows(lp_constraints_.size());
90 const glop::ColIndex num_cols =
91 solution_state->statuses.size() - RowToColIndex(num_rows);
92 int new_size = 0;
93 for (int i = 0; i < num_rows; ++i) {
94 const ConstraintIndex constraint_index = lp_constraints_[i];
95
96 // Constraints that are not tight in the current solution have a basic
97 // status. We remove the ones that have been inactive in the last recent
98 // solves.
99 //
100 // TODO(user): More advanced heuristics might perform better, I didn't do
101 // a lot of tuning experiments yet.
102 const glop::VariableStatus row_status =
103 solution_state->statuses[num_cols + glop::ColIndex(i)];
104 if (row_status == glop::VariableStatus::BASIC) {
105 constraint_infos_[constraint_index].inactive_count++;
106 if (constraint_infos_[constraint_index].inactive_count >
107 sat_parameters_.max_consecutive_inactive_count()) {
108 constraint_infos_[constraint_index].is_in_lp = false;
109 continue; // Remove it.
110 }
111 } else {
112 // Only count consecutive inactivities.
113 constraint_infos_[constraint_index].inactive_count = 0;
114 }
115
116 lp_constraints_[new_size] = constraint_index;
117 solution_state->statuses[num_cols + glop::ColIndex(new_size)] = row_status;
118 new_size++;
119 }
120 const int num_removed_constraints = lp_constraints_.size() - new_size;
121 lp_constraints_.resize(new_size);
122 solution_state->statuses.resize(num_cols + glop::ColIndex(new_size));
123 if (num_removed_constraints > 0) {
124 VLOG(3) << "Removed " << num_removed_constraints << " constraints";
125 }
126 return num_removed_constraints > 0;
127}
128
129// Because sometimes we split a == constraint in two (>= and <=), it makes sense
130// to detect duplicate constraints and merge bounds. This is also relevant if
131// we regenerate identical cuts for some reason.
132LinearConstraintManager::ConstraintIndex LinearConstraintManager::Add(
133 LinearConstraint ct, bool* added) {
134 DCHECK_GT(ct.num_terms, 0);
135 DCHECK(!PossibleOverflow(integer_trail_, ct)) << ct.DebugString();
136 DCHECK(NoDuplicateVariable(ct));
137 SimplifyConstraint(&ct);
138 DivideByGCD(&ct);
140 CHECK(std::is_sorted(ct.VarsAsSpan().begin(), ct.VarsAsSpan().end()));
141 DCHECK(DebugCheckConstraint(ct));
142
143 // If an identical constraint exists, only updates its bound.
144 const size_t key = ComputeHashOfTerms(ct);
145 if (equiv_constraints_.contains(key)) {
146 const ConstraintIndex ct_index = equiv_constraints_[key];
147 if (constraint_infos_[ct_index].constraint.IsEqualIgnoringBounds(ct)) {
148 bool tightened = false;
149 if (ct.lb > constraint_infos_[ct_index].constraint.lb) {
150 tightened = true;
151 if (constraint_infos_[ct_index].is_in_lp) current_lp_is_changed_ = true;
152 constraint_infos_[ct_index].constraint.lb = ct.lb;
153 }
154 if (ct.ub < constraint_infos_[ct_index].constraint.ub) {
155 tightened = true;
156 if (constraint_infos_[ct_index].is_in_lp) current_lp_is_changed_ = true;
157 constraint_infos_[ct_index].constraint.ub = ct.ub;
158 }
159 if (added != nullptr) *added = tightened;
160 if (tightened) {
161 ++num_merged_constraints_;
162 FillDerivedFields(&constraint_infos_[ct_index]);
163 }
164 return ct_index;
165 }
166 }
167
168 if (added != nullptr) *added = true;
169 const ConstraintIndex ct_index(constraint_infos_.size());
170 ConstraintInfo ct_info;
171 ct_info.constraint = std::move(ct);
172 ct_info.l2_norm = ComputeL2Norm(ct_info.constraint);
173 FillDerivedFields(&ct_info);
174 ct_info.hash = key;
175 equiv_constraints_[key] = ct_index;
176 ct_info.active_count = constraint_active_count_increase_;
177 constraint_infos_.push_back(std::move(ct_info));
178 return ct_index;
179}
180
181bool LinearConstraintManager::UpdateConstraintLb(glop::RowIndex index_in_lp,
182 IntegerValue new_lb) {
183 const ConstraintIndex index = lp_constraints_[index_in_lp.value()];
184 ConstraintInfo& info = constraint_infos_[index];
185 if (new_lb <= info.constraint.lb) return false;
186 ++num_constraint_updates_;
187 current_lp_is_changed_ = true;
188 info.constraint.lb = new_lb;
189 return true;
190}
191
192bool LinearConstraintManager::UpdateConstraintUb(glop::RowIndex index_in_lp,
193 IntegerValue new_ub) {
194 const ConstraintIndex index = lp_constraints_[index_in_lp.value()];
195 ConstraintInfo& info = constraint_infos_[index];
196 if (new_ub >= info.constraint.ub) return false;
197 ++num_constraint_updates_;
198 current_lp_is_changed_ = true;
199 info.constraint.ub = new_ub;
200 return true;
201}
202
203void LinearConstraintManager::ComputeObjectiveParallelism(
204 const ConstraintIndex ct_index) {
205 CHECK(objective_is_defined_);
206 // lazy computation of objective norm.
207 if (!objective_norm_computed_) {
208 objective_l2_norm_ = std::sqrt(sum_of_squared_objective_coeffs_);
209 objective_norm_computed_ = true;
210 }
211 CHECK_GT(objective_l2_norm_, 0.0);
212
213 constraint_infos_[ct_index].objective_parallelism_computed = true;
214 if (constraint_infos_[ct_index].l2_norm == 0.0) {
215 constraint_infos_[ct_index].objective_parallelism = 0.0;
216 return;
217 }
218
219 const LinearConstraint& lc = constraint_infos_[ct_index].constraint;
220 double unscaled_objective_parallelism = 0.0;
221 for (int i = 0; i < lc.num_terms; ++i) {
222 const IntegerVariable var = lc.vars[i];
223 const auto it = objective_map_.find(var);
224 if (it == objective_map_.end()) continue;
225 unscaled_objective_parallelism += it->second * ToDouble(lc.coeffs[i]);
226 }
227 const double objective_parallelism =
228 unscaled_objective_parallelism /
229 (constraint_infos_[ct_index].l2_norm * objective_l2_norm_);
230 constraint_infos_[ct_index].objective_parallelism =
231 std::abs(objective_parallelism);
232}
233
234// Same as Add(), but logs some information about the newly added constraint.
235// Cuts are also handled slightly differently than normal constraints.
237 std::string extra_info) {
238 ++num_add_cut_calls_;
239 if (ct.num_terms == 0) return false;
240
241 const double activity = ComputeActivity(ct, expanded_lp_solution_);
242 const double violation =
243 std::max(activity - ToDouble(ct.ub), ToDouble(ct.lb) - activity);
244 const double l2_norm = ComputeL2Norm(ct);
245
246 // Only add cut with sufficient efficacy.
247 if (violation / l2_norm < 1e-4) {
248 VLOG(3) << "BAD Cut '" << type_name << "'" << " size=" << ct.num_terms
249 << " max_magnitude=" << ComputeInfinityNorm(ct)
250 << " norm=" << l2_norm << " violation=" << violation
251 << " eff=" << violation / l2_norm << " " << extra_info;
252 return false;
253 }
254
255 // TODO(user): We could prevent overflow by dividing more. Note that mainly
256 // happen with super large variable domain since we usually restrict the size
257 // of the generated coefficients in our cuts. So it shouldn't be that
258 // important.
259 if (PossibleOverflow(integer_trail_, ct)) return false;
260
261 bool added = false;
262 const ConstraintIndex ct_index = Add(std::move(ct), &added);
263
264 // We only mark the constraint as a cut if it is not an update of an already
265 // existing one.
266 if (!added) return false;
267
268 // TODO(user): Use better heuristic here for detecting good cuts and mark
269 // them undeletable.
270 constraint_infos_[ct_index].is_deletable = true;
271
272 VLOG(2) << "Cut '" << type_name << "'"
273 << " size=" << constraint_infos_[ct_index].constraint.num_terms
274 << " max_magnitude="
275 << ComputeInfinityNorm(constraint_infos_[ct_index].constraint)
276 << " norm=" << l2_norm << " violation=" << violation
277 << " eff=" << violation / l2_norm << " " << extra_info;
278
279 num_cuts_++;
280 num_deletable_constraints_++;
281 type_to_num_cuts_[type_name]++;
282 return true;
283}
284
285void LinearConstraintManager::PermanentlyRemoveSomeConstraints() {
286 std::vector<double> deletable_constraint_counts;
287 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
288 if (constraint_infos_[i].is_deletable && !constraint_infos_[i].is_in_lp) {
289 deletable_constraint_counts.push_back(constraint_infos_[i].active_count);
290 }
291 }
292 if (deletable_constraint_counts.empty()) return;
293 std::sort(deletable_constraint_counts.begin(),
294 deletable_constraint_counts.end());
295
296 // We will delete the oldest (in the order they where added) cleanup target
297 // constraints with a count lower or equal to this.
298 double active_count_threshold = std::numeric_limits<double>::infinity();
299 if (sat_parameters_.cut_cleanup_target() <
300 deletable_constraint_counts.size()) {
301 active_count_threshold =
302 deletable_constraint_counts[sat_parameters_.cut_cleanup_target()];
303 }
304
305 ConstraintIndex new_size(0);
306 equiv_constraints_.clear();
308 constraint_infos_.size());
309 int num_deleted_constraints = 0;
310 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
311 if (constraint_infos_[i].is_deletable && !constraint_infos_[i].is_in_lp &&
312 constraint_infos_[i].active_count <= active_count_threshold &&
313 num_deleted_constraints < sat_parameters_.cut_cleanup_target()) {
314 ++num_deleted_constraints;
315 continue;
316 }
317
318 if (i != new_size) {
319 constraint_infos_[new_size] = std::move(constraint_infos_[i]);
320 }
321 index_mapping[i] = new_size;
322
323 // Make sure we recompute the hash_map of identical constraints.
324 equiv_constraints_[constraint_infos_[new_size].hash] = new_size;
325 new_size++;
326 }
327 constraint_infos_.resize(new_size.value());
328
329 // Also update lp_constraints_
330 for (int i = 0; i < lp_constraints_.size(); ++i) {
331 lp_constraints_[i] = index_mapping[lp_constraints_[i]];
332 }
333
334 if (num_deleted_constraints > 0) {
335 VLOG(3) << "Constraint manager cleanup: #deleted:"
336 << num_deleted_constraints;
337 }
338 num_deletable_constraints_ -= num_deleted_constraints;
339}
340
342 IntegerValue coeff) {
343 if (coeff == IntegerValue(0)) return;
344 objective_is_defined_ = true;
345 if (!VariableIsPositive(var)) {
346 var = NegationOf(var);
347 coeff = -coeff;
348 }
349 const double coeff_as_double = ToDouble(coeff);
350 const auto insert = objective_map_.insert({var, coeff_as_double});
351 CHECK(insert.second)
352 << "SetObjectiveCoefficient() called twice with same variable";
353 sum_of_squared_objective_coeffs_ += coeff_as_double * coeff_as_double;
354}
355
356// TODO(user): Also consider partial gcd simplification? see presolve.
357bool LinearConstraintManager::SimplifyConstraint(LinearConstraint* ct) {
358 bool term_changed = false;
359
360 IntegerValue min_sum(0);
361 IntegerValue max_sum(0);
362 IntegerValue max_magnitude(0);
363 IntegerValue min_magnitude = kMaxIntegerValue;
364 int new_size = 0;
365 const int num_terms = ct->num_terms;
366 for (int i = 0; i < num_terms; ++i) {
367 const IntegerVariable var = ct->vars[i];
368 const IntegerValue coeff = ct->coeffs[i];
369 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
370 const IntegerValue ub = integer_trail_.LevelZeroUpperBound(var);
371
372 // For now we do not change ct, but just compute its new_size if we where
373 // to remove a fixed term.
374 if (lb == ub) continue;
375 ++new_size;
376
377 const IntegerValue magnitude = IntTypeAbs(coeff);
378 max_magnitude = std::max(max_magnitude, magnitude);
379 min_magnitude = std::min(min_magnitude, magnitude);
380 if (coeff > 0.0) {
381 min_sum += coeff * lb;
382 max_sum += coeff * ub;
383 } else {
384 min_sum += coeff * ub;
385 max_sum += coeff * lb;
386 }
387 }
388
389 // Shorten the constraint if needed.
390 if (new_size < num_terms) {
391 term_changed = true;
392 ++num_shortened_constraints_;
393 new_size = 0;
394 for (int i = 0; i < num_terms; ++i) {
395 const IntegerVariable var = ct->vars[i];
396 const IntegerValue coeff = ct->coeffs[i];
397 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
398 const IntegerValue ub = integer_trail_.LevelZeroUpperBound(var);
399 if (lb == ub) {
400 const IntegerValue rhs_adjust = lb * coeff;
401 if (ct->lb > kMinIntegerValue) ct->lb -= rhs_adjust;
402 if (ct->ub < kMaxIntegerValue) ct->ub -= rhs_adjust;
403 continue;
404 }
405 ct->vars[new_size] = var;
406 ct->coeffs[new_size] = coeff;
407 ++new_size;
408 }
409 ct->resize(new_size);
410 }
411
412 // Clear constraints that are always true.
413 // We rely on the deletion code to remove them eventually.
414 if (min_sum >= ct->lb && max_sum <= ct->ub) {
415 ct->resize(0);
416 ct->lb = 0;
417 ct->ub = 0;
418 return true;
419 }
420
421 // Make sure bounds are finite.
422 ct->lb = std::max(ct->lb, min_sum);
423 ct->ub = std::min(ct->ub, max_sum);
424
425 // The variable can be shifted and complemented so we have constraints of
426 // the form:
427 // ... + |coeff| * X >= threshold_ub
428 // ... + |coeff| * X' >= threshold_lb
429 // In both case if coeff is big, we can reduce it and update the rhs
430 // accordingly.
431 const IntegerValue threshold_ub = max_sum - ct->ub;
432 const IntegerValue threshold_lb = ct->lb - min_sum;
433 const IntegerValue threshold = std::max(threshold_lb, threshold_ub);
434 CHECK_GT(threshold, 0); // Since we aborted for trivial constraint.
435
436 // TODO(user): In some case, we could split the constraint to reduce one of
437 // them further. But not sure that is a good thing.
438 if (threshold_ub > 0 && threshold_lb > 0 && threshold_lb != threshold_ub) {
439 if (max_magnitude > std::min(threshold_lb, threshold_ub)) {
440 ++num_split_constraints_;
441 }
442 }
443
444 // TODO(user): For constraint with both bound, we could reduce further for
445 // coefficient between threshold - min_magnitude and min(t_lb, t_ub).
446 const IntegerValue second_threshold = std::max(
447 {CeilRatio(threshold, IntegerValue(2)), threshold - min_magnitude,
448 std::min(threshold_lb, threshold_ub)});
449 if (max_magnitude > second_threshold) {
450 const int num_terms = ct->num_terms;
451 for (int i = 0; i < num_terms; ++i) {
452 // In all cases, we reason on a transformed constraint where the term
453 // is max_value - |coeff| * positive_X. If we change coeff, and
454 // retransform the constraint, we need to change the rhs by the
455 // constant term left.
456 const IntegerValue coeff = ct->coeffs[i];
457 const IntegerVariable var = ct->vars[i];
458 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
459 const IntegerValue ub = integer_trail_.LevelZeroUpperBound(var);
460 if (coeff > threshold) {
461 term_changed = true;
462 ++num_coeff_strenghtening_;
463 ct->coeffs[i] = threshold;
464 ct->ub -= (coeff - threshold) * ub;
465 ct->lb -= (coeff - threshold) * lb;
466 } else if (coeff > second_threshold && coeff < threshold) {
467 term_changed = true;
468 ++num_coeff_strenghtening_;
469 ct->coeffs[i] = second_threshold;
470 ct->ub -= (coeff - second_threshold) * ub;
471 ct->lb -= (coeff - second_threshold) * lb;
472 } else if (coeff < -threshold) {
473 term_changed = true;
474 ++num_coeff_strenghtening_;
475 ct->coeffs[i] = -threshold;
476 ct->ub -= (coeff + threshold) * lb;
477 ct->lb -= (coeff + threshold) * ub;
478 } else if (coeff < -second_threshold && coeff > -threshold) {
479 term_changed = true;
480 ++num_coeff_strenghtening_;
481 ct->coeffs[i] = -second_threshold;
482 ct->ub -= (coeff + second_threshold) * lb;
483 ct->lb -= (coeff + second_threshold) * ub;
484 }
485 }
486 }
487
488 return term_changed;
489}
490
491void LinearConstraintManager::FillDerivedFields(ConstraintInfo* info) {
492 IntegerValue min_sum(0);
493 IntegerValue max_sum(0);
494 const int num_terms = info->constraint.num_terms;
495 for (int i = 0; i < num_terms; ++i) {
496 const IntegerVariable var = info->constraint.vars[i];
497 const IntegerValue coeff = info->constraint.coeffs[i];
498 const IntegerValue lb = integer_trail_.LevelZeroLowerBound(var);
499 const IntegerValue ub = integer_trail_.LevelZeroUpperBound(var);
500 if (coeff > 0.0) {
501 min_sum += coeff * lb;
502 max_sum += coeff * ub;
503 } else {
504 min_sum += coeff * ub;
505 max_sum += coeff * lb;
506 }
507 }
508 info->constraint.lb = std::max(min_sum, info->constraint.lb);
509 info->constraint.ub = std::min(max_sum, info->constraint.ub);
510 CHECK_NE(CapSub(info->constraint.ub.value(), info->constraint.lb.value()),
511 std::numeric_limits<int64_t>::max());
512 info->lb_is_trivial = min_sum >= info->constraint.lb;
513 info->ub_is_trivial = max_sum <= info->constraint.ub;
514}
515
517 int* num_new_constraints) {
518 VLOG(3) << "Enter ChangeLP, scan " << constraint_infos_.size()
519 << " constraints";
520 const double saved_dtime = dtime_;
521 std::vector<std::pair<ConstraintIndex, double>> new_constraints_by_score;
522 std::vector<double> new_constraints_efficacies;
523 std::vector<double> new_constraints_orthogonalities;
524
525 const bool simplify_constraints =
526 integer_trail_.num_level_zero_enqueues() > last_simplification_timestamp_;
527 last_simplification_timestamp_ = integer_trail_.num_level_zero_enqueues();
528
529 // We keep any constraints that is already present, and otherwise, we add the
530 // ones that are currently not satisfied by at least "tolerance" to the set
531 // of potential new constraints.
532 bool rescale_active_count = false;
533 const double tolerance = 1e-6;
534 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
535 // Inprocessing of the constraint.
536 if (simplify_constraints &&
537 SimplifyConstraint(&constraint_infos_[i].constraint)) {
538 ++num_simplifications_;
539
540 // Note that the canonicalization shouldn't be needed since the order
541 // of the variable is not changed by the simplification, and we only
542 // reduce the coefficients at both end of the spectrum.
543 DivideByGCD(&constraint_infos_[i].constraint);
544 DCHECK(DebugCheckConstraint(constraint_infos_[i].constraint));
545
546 constraint_infos_[i].objective_parallelism_computed = false;
547 constraint_infos_[i].l2_norm =
548 ComputeL2Norm(constraint_infos_[i].constraint);
549 FillDerivedFields(&constraint_infos_[i]);
550
551 if (constraint_infos_[i].is_in_lp) current_lp_is_changed_ = true;
552 equiv_constraints_.erase(constraint_infos_[i].hash);
553 constraint_infos_[i].hash =
554 ComputeHashOfTerms(constraint_infos_[i].constraint);
555
556 // TODO(user): Because we simplified this constraint, it is possible that
557 // it is now a duplicate of another one. Merge them.
558 equiv_constraints_[constraint_infos_[i].hash] = i;
559 }
560
561 if (constraint_infos_[i].is_in_lp) continue;
562
563 // ComputeActivity() often represent the bulk of the time spent in
564 // ChangeLP().
565 dtime_ +=
566 1.7e-9 * static_cast<double>(constraint_infos_[i].constraint.num_terms);
567 const double activity =
568 ComputeActivity(constraint_infos_[i].constraint, expanded_lp_solution_);
569 const double lb_violation =
570 ToDouble(constraint_infos_[i].constraint.lb) - activity;
571 const double ub_violation =
572 activity - ToDouble(constraint_infos_[i].constraint.ub);
573 const double violation = std::max(lb_violation, ub_violation);
574 if (violation >= tolerance) {
575 constraint_infos_[i].inactive_count = 0;
576 if (objective_is_defined_ &&
577 !constraint_infos_[i].objective_parallelism_computed) {
578 ComputeObjectiveParallelism(i);
579 } else if (!objective_is_defined_) {
580 constraint_infos_[i].objective_parallelism = 0.0;
581 }
582
583 const double score = violation / constraint_infos_[i].l2_norm +
584 constraint_infos_[i].objective_parallelism;
585 new_constraints_by_score.push_back({i, score});
586
587 if (constraint_infos_[i].is_deletable) {
588 constraint_infos_[i].active_count += constraint_active_count_increase_;
589 if (constraint_infos_[i].active_count >
590 sat_parameters_.cut_max_active_count_value()) {
591 rescale_active_count = true;
592 }
593 }
594 }
595 }
596
597 // Bump activities of active constraints in LP.
598 if (solution_state != nullptr) {
599 const glop::RowIndex num_rows(lp_constraints_.size());
600 const glop::ColIndex num_cols =
601 solution_state->statuses.size() - RowToColIndex(num_rows);
602
603 for (int i = 0; i < num_rows; ++i) {
604 const ConstraintIndex constraint_index = lp_constraints_[i];
605 const glop::VariableStatus row_status =
606 solution_state->statuses[num_cols + glop::ColIndex(i)];
607 if (row_status != glop::VariableStatus::BASIC &&
608 constraint_infos_[constraint_index].is_deletable) {
609 constraint_infos_[constraint_index].active_count +=
610 constraint_active_count_increase_;
611 if (constraint_infos_[constraint_index].active_count >
612 sat_parameters_.cut_max_active_count_value()) {
613 rescale_active_count = true;
614 }
615 }
616 }
617 }
618
619 if (rescale_active_count) {
620 CHECK_GT(sat_parameters_.cut_max_active_count_value(), 0.0);
621 RescaleActiveCounts(1.0 / sat_parameters_.cut_max_active_count_value());
622 }
623
624 // Update the increment counter.
625 constraint_active_count_increase_ *=
626 1.0 / sat_parameters_.cut_active_count_decay();
627
628 // Remove constraints from the current LP that have been inactive for a while.
629 // We do that after we computed new_constraints so we do not need to iterate
630 // over the just deleted constraints.
631 if (MaybeRemoveSomeInactiveConstraints(solution_state)) {
632 current_lp_is_changed_ = true;
633 }
634
635 // Note that the algo below is in O(limit * new_constraint). In order to
636 // limit spending too much time on this, we first sort all the constraints
637 // with an imprecise score (no orthogonality), then limit the size of the
638 // vector of constraints to precisely score, then we do the actual scoring.
639 //
640 // On problem crossword_opt_grid-19.05_dict-80_sat with linearization_level=2,
641 // new_constraint.size() > 1.5M.
642 //
643 // TODO(user): This blowup factor could be adaptative w.r.t. the constraint
644 // limit.
645 const int kBlowupFactor = 4;
646 const int current_size = static_cast<int>(new_constraints_by_score.size());
647 int constraint_limit =
648 std::min(sat_parameters_.new_constraints_batch_size(), current_size);
649 if (lp_constraints_.empty()) {
650 constraint_limit = std::min(1000, current_size);
651 }
652 VLOG(3) << " - size = " << current_size << ", limit = " << constraint_limit;
653
654 std::stable_sort(new_constraints_by_score.begin(),
655 new_constraints_by_score.end(),
656 [&](std::pair<ConstraintIndex, double> a,
657 std::pair<ConstraintIndex, double> b) {
658 return a.second > b.second;
659 });
660 if (new_constraints_by_score.size() > kBlowupFactor * constraint_limit) {
661 VLOG(3) << "Resize candidate constraints from "
662 << new_constraints_by_score.size() << " down to "
663 << kBlowupFactor * constraint_limit;
664 new_constraints_by_score.resize(kBlowupFactor * constraint_limit);
665 }
666
667 int num_added = 0;
668 int num_skipped_checks = 0;
669 const int kCheckFrequency = 100;
670 ConstraintIndex last_added_candidate = kInvalidConstraintIndex;
671 std::vector<double> orthogonality_score(new_constraints_by_score.size(), 1.0);
672 for (int i = 0; i < constraint_limit; ++i) {
673 // Iterate through all new constraints and select the one with the best
674 // score.
675 //
676 // TODO(user): find better algo, this does 1000 * 4000 scalar product!
677 double best_score = 0.0;
678 ConstraintIndex best_candidate = kInvalidConstraintIndex;
679 for (int j = 0; j < new_constraints_by_score.size(); ++j) {
680 // Checks the time limit, and returns if the lp has changed.
681 if (++num_skipped_checks >= kCheckFrequency) {
682 if (time_limit_->LimitReached()) return current_lp_is_changed_;
683 num_skipped_checks = 0;
684 }
685
686 const ConstraintIndex new_index = new_constraints_by_score[j].first;
687 if (constraint_infos_[new_index].is_in_lp) continue;
688
689 if (last_added_candidate != kInvalidConstraintIndex) {
690 const double current_orthogonality =
691 1.0 - (std::abs(ScalarProduct(
692 constraint_infos_[last_added_candidate].constraint,
693 constraint_infos_[new_index].constraint)) /
694 (constraint_infos_[last_added_candidate].l2_norm *
695 constraint_infos_[new_index].l2_norm));
696 orthogonality_score[j] =
697 std::min(orthogonality_score[j], current_orthogonality);
698 }
699
700 // NOTE(user): It is safe to not add this constraint as the constraint
701 // that is almost parallel to this constraint is present in the LP or is
702 // inactive for a long time and is removed from the LP. In either case,
703 // this constraint is not adding significant value and is only making the
704 // LP larger.
705 if (orthogonality_score[j] <
706 sat_parameters_.min_orthogonality_for_lp_constraints()) {
707 continue;
708 }
709
710 // TODO(user): Experiment with different weights or different
711 // functions for computing score.
712 const double score =
713 orthogonality_score[j] + new_constraints_by_score[j].second;
714 CHECK_GE(score, 0.0);
715 if (score > best_score || best_candidate == kInvalidConstraintIndex) {
716 best_score = score;
717 best_candidate = new_index;
718 }
719 }
720
721 if (best_candidate != kInvalidConstraintIndex) {
722 // Add the best constraint in the LP.
723 constraint_infos_[best_candidate].is_in_lp = true;
724 // Note that it is important for LP incremental solving that the old
725 // constraints stays at the same position in this list (and thus in the
726 // returned GetLp()).
727 ++num_added;
728 current_lp_is_changed_ = true;
729 lp_constraints_.push_back(best_candidate);
730 last_added_candidate = best_candidate;
731 } else {
732 // Abort the addition loop.
733 break;
734 }
735 }
736
737 if (num_new_constraints != nullptr) {
738 *num_new_constraints = num_added;
739 }
740 if (num_added > 0) {
741 // We update the solution sate to match the new LP size.
742 VLOG(3) << "Added " << num_added << " constraints.";
743 solution_state->statuses.resize(solution_state->statuses.size() + num_added,
745 }
746
747 // TODO(user): Instead of comparing num_deletable_constraints with cut
748 // limit, compare number of deletable constraints not in lp against the limit.
749 if (num_deletable_constraints_ > sat_parameters_.max_num_cuts()) {
750 PermanentlyRemoveSomeConstraints();
751 }
752
753 time_limit_->AdvanceDeterministicTime(dtime_ - saved_dtime);
754
755 // The LP changed only if we added new constraints or if some constraints
756 // already inside changed (simplification or tighter bounds).
757 if (current_lp_is_changed_) {
758 current_lp_is_changed_ = false;
759 return true;
760 }
761 return false;
762}
763
765 for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) {
766 if (constraint_infos_[i].is_in_lp) continue;
767 constraint_infos_[i].is_in_lp = true;
768 lp_constraints_.push_back(i);
769 }
770}
771
773 const LinearConstraint& cut) {
774 if (model_->Get<DebugSolution>() == nullptr) return true;
775 const auto& debug_solution = *(model_->Get<DebugSolution>());
776
777 IntegerValue activity(0);
778 for (int i = 0; i < cut.num_terms; ++i) {
779 const IntegerVariable var = cut.vars[i];
780 const IntegerValue coeff = cut.coeffs[i];
781 CHECK(debug_solution.ivar_has_value[var]);
782 activity += coeff * debug_solution.ivar_values[var];
783 }
784 if (activity > cut.ub || activity < cut.lb) {
785 LOG(INFO) << cut.DebugString();
786 LOG(INFO) << "activity " << activity << " not in [" << cut.lb << ","
787 << cut.ub << "]";
788 return false;
789 }
790 return true;
791}
792
794 LinearConstraint ct, absl::string_view name,
796 if (ct.num_terms == 0) return;
797 const double activity = ComputeActivity(ct, lp_solution);
798 const double violation =
799 std::max(activity - ToDouble(ct.ub), ToDouble(ct.lb) - activity);
800 const double l2_norm = ComputeL2Norm(ct);
801 cuts_.Add({std::string(name), std::move(ct)}, violation / l2_norm);
802}
803
805 for (CutCandidate& candidate : *cuts_.MutableUnorderedElements()) {
806 manager->AddCut(std::move(candidate.cut), candidate.name);
807 }
808 cuts_.Clear();
809}
810
811} // namespace sat
812} // namespace operations_research
void AdvanceDeterministicTime(double deterministic_duration)
Definition time_limit.h:183
int64_t num_level_zero_enqueues() const
Same as num_enqueues but only count the level zero changes.
Definition integer.h:1064
IntegerValue LevelZeroUpperBound(IntegerVariable var) const
Definition integer.h:1817
IntegerValue LevelZeroLowerBound(IntegerVariable var) const
Returns globally valid lower/upper bound on the given integer variable.
Definition integer.h:1812
bool ChangeLp(glop::BasisState *solution_state, int *num_new_constraints=nullptr)
ConstraintIndex Add(LinearConstraint ct, bool *added=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)
T Get(std::function< T(const Model &)> f) const
Similar to Add() but this is const.
Definition model.h:95
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.
void Add(Element e, Score score)
Definition util.h:641
std::vector< Element > * MutableUnorderedElements()
Definition util.h:665
int64_t a
Definition table.cc:44
const std::string name
A name for logging purposes.
const Constraint * ct
IntVar * var
int constraint_index
int ct_index
int index
int64_t hash
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)
Definition integer.h:81
double ComputeL2Norm(const LinearConstraint &ct)
Returns sqrt(sum square(coeff)).
IntegerValue CeilRatio(IntegerValue dividend, IntegerValue positive_divisor)
Definition integer.h:85
IntegerValue ComputeInfinityNorm(const LinearConstraint &ct)
Returns the maximum absolute value of the coefficients.
std::vector< IntegerVariable > NegationOf(const std::vector< IntegerVariable > &vars)
Returns the vector of the negated variables.
Definition integer.cc:51
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
bool PossibleOverflow(const IntegerTrail &integer_trail, const LinearConstraint &constraint)
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)
Definition integer.h:189
double ToDouble(IntegerValue value)
Definition integer.h:73
double ScalarProduct(const LinearConstraint &ct1, const LinearConstraint &ct2)
In SWIG mode, we don't want anything besides these top-level includes.
int64_t CapSub(int64_t x, int64_t y)
uint64_t Hash(uint64_t num, uint64_t c)
Definition hash.h:73
double l2_norm
The L2 norm of the finite values.
std::unique_ptr< IntegerValue[]> coeffs
std::unique_ptr< IntegerVariable[]> vars