Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
linear_programming_constraint.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 <algorithm>
17#include <array>
18#include <cmath>
19#include <cstddef>
20#include <cstdint>
21#include <functional>
22#include <limits>
23#include <memory>
24#include <numeric>
25#include <optional>
26#include <string>
27#include <utility>
28#include <vector>
29
30#include "absl/algorithm/container.h"
31#include "absl/container/flat_hash_map.h"
32#include "absl/log/check.h"
33#include "absl/numeric/int128.h"
34#include "absl/strings/str_cat.h"
35#include "absl/strings/str_format.h"
36#include "absl/strings/string_view.h"
37#include "absl/types/span.h"
42#include "ortools/glop/parameters.pb.h"
44#include "ortools/glop/status.h"
51#include "ortools/sat/cuts.h"
53#include "ortools/sat/integer.h"
59#include "ortools/sat/model.h"
61#include "ortools/sat/sat_parameters.pb.h"
64#include "ortools/sat/util.h"
66#include "ortools/util/bitset.h"
67#include "ortools/util/rev.h"
71
72namespace operations_research {
73namespace sat {
74
75using glop::ColIndex;
77using glop::RowIndex;
78
80 if (is_sparse_) {
81 for (const glop::ColIndex col : non_zeros_) {
82 dense_vector_[col] = IntegerValue(0);
83 }
84 dense_vector_.resize(size, IntegerValue(0));
85 } else {
86 dense_vector_.assign(size, IntegerValue(0));
87 }
88 for (const glop::ColIndex col : non_zeros_) {
89 is_zeros_[col] = true;
90 }
91 is_zeros_.resize(size, true);
92 non_zeros_.clear();
93 is_sparse_ = true;
94}
95
96bool ScatteredIntegerVector::Add(glop::ColIndex col, IntegerValue value) {
97 const int64_t add = CapAdd(value.value(), dense_vector_[col].value());
98 if (add == std::numeric_limits<int64_t>::min() ||
99 add == std::numeric_limits<int64_t>::max())
100 return false;
101 dense_vector_[col] = IntegerValue(add);
102 if (is_sparse_ && is_zeros_[col]) {
103 is_zeros_[col] = false;
104 non_zeros_.push_back(col);
105 }
106 return true;
107}
108
109template <bool check_overflow>
111 const IntegerValue multiplier, absl::Span<const glop::ColIndex> cols,
112 absl::Span<const IntegerValue> coeffs, IntegerValue max_coeff_magnitude) {
113 // Since we have the norm, this avoid checking each products below.
114 if (check_overflow) {
115 const IntegerValue prod = CapProdI(max_coeff_magnitude, multiplier);
116 if (AtMinOrMaxInt64(prod.value())) return false;
117 }
118
119 IntegerValue* data = dense_vector_.data();
120 const double threshold = 0.1 * static_cast<double>(dense_vector_.size());
121 const int num_terms = cols.size();
122 if (is_sparse_ && static_cast<double>(num_terms) < threshold) {
123 for (int i = 0; i < num_terms; ++i) {
124 const glop::ColIndex col = cols[i];
125 if (is_zeros_[col]) {
126 is_zeros_[col] = false;
127 non_zeros_.push_back(col);
128 }
129 const IntegerValue product = multiplier * coeffs[i];
130 if (check_overflow) {
131 if (AddIntoOverflow(product.value(),
132 data[col.value()].mutable_value())) {
133 return false;
134 }
135 } else {
136 data[col.value()] += product;
137 }
138 }
139 if (static_cast<double>(non_zeros_.size()) > threshold) {
140 is_sparse_ = false;
141 }
142 } else {
143 is_sparse_ = false;
144 for (int i = 0; i < num_terms; ++i) {
145 const glop::ColIndex col = cols[i];
146 const IntegerValue product = multiplier * coeffs[i];
147 if (check_overflow) {
148 if (AddIntoOverflow(product.value(),
149 data[col.value()].mutable_value())) {
150 return false;
151 }
152 } else {
153 data[col.value()] += product;
154 }
155 }
156 }
157 return true;
158}
159
160// Force instantations for tests.
162 IntegerValue multiplier, absl::Span<const glop::ColIndex> cols,
163 absl::Span<const IntegerValue> coeffs, IntegerValue max_coeff_magnitude);
164
166 IntegerValue multiplier, absl::Span<const glop::ColIndex> cols,
167 absl::Span<const IntegerValue> coeffs, IntegerValue max_coeff_magnitude);
168
170 absl::Span<const IntegerVariable> integer_variables,
171 IntegerValue upper_bound,
172 std::optional<std::pair<IntegerVariable, IntegerValue>> extra_term) {
173 // We first do one pass to compute the exact size and not overallocate.
174 int final_size = 0;
175 if (is_sparse_) {
176 for (const glop::ColIndex col : non_zeros_) {
177 const IntegerValue coeff = dense_vector_[col];
178 if (coeff == 0) continue;
179 ++final_size;
180 }
181 } else {
182 for (const IntegerValue coeff : dense_vector_) {
183 if (coeff != 0) ++final_size;
184 }
185 }
186 if (extra_term != std::nullopt) ++final_size;
187
188 // Allocate once.
189 LinearConstraint result;
190 result.resize(final_size);
191
192 // Copy terms.
193 int new_size = 0;
194 if (is_sparse_) {
195 std::sort(non_zeros_.begin(), non_zeros_.end());
196 for (const glop::ColIndex col : non_zeros_) {
197 const IntegerValue coeff = dense_vector_[col];
198 if (coeff == 0) continue;
199 result.vars[new_size] = integer_variables[col.value()];
200 result.coeffs[new_size] = coeff;
201 ++new_size;
202 }
203 } else {
204 const int size = dense_vector_.size();
205 for (glop::ColIndex col(0); col < size; ++col) {
206 const IntegerValue coeff = dense_vector_[col];
207 if (coeff == 0) continue;
208 result.vars[new_size] = integer_variables[col.value()];
209 result.coeffs[new_size] = coeff;
210 ++new_size;
211 }
212 }
213
214 result.lb = kMinIntegerValue;
215 result.ub = upper_bound;
216
217 if (extra_term != std::nullopt) {
218 result.vars[new_size] += extra_term->first;
219 result.coeffs[new_size] += extra_term->second;
220 ++new_size;
221 }
222
223 CHECK_EQ(new_size, final_size);
224 DivideByGCD(&result);
225 return result;
226}
227
229 absl::int128 rhs, absl::Span<const IntegerVariable> integer_variables,
230 absl::Span<const double> lp_solution, IntegerTrail* integer_trail,
231 CutData* result) {
232 result->terms.clear();
233 result->rhs = rhs;
234 absl::Span<const IntegerValue> dense_vector = dense_vector_;
235 if (is_sparse_) {
236 std::sort(non_zeros_.begin(), non_zeros_.end());
237 for (const glop::ColIndex col : non_zeros_) {
238 const IntegerValue coeff = dense_vector[col.value()];
239 if (coeff == 0) continue;
240 const IntegerVariable var = integer_variables[col.value()];
241 CHECK(result->AppendOneTerm(var, coeff, lp_solution[col.value()],
242 integer_trail->LevelZeroLowerBound(var),
243 integer_trail->LevelZeroUpperBound(var)));
244 }
245 } else {
246 for (int col(0); col < dense_vector.size(); ++col) {
247 const IntegerValue coeff = dense_vector[col];
248 if (coeff == 0) continue;
249 const IntegerVariable var = integer_variables[col];
250 CHECK(result->AppendOneTerm(var, coeff, lp_solution[col],
251 integer_trail->LevelZeroLowerBound(var),
252 integer_trail->LevelZeroUpperBound(var)));
253 }
254 }
255}
256
257std::vector<std::pair<glop::ColIndex, IntegerValue>>
259 std::vector<std::pair<glop::ColIndex, IntegerValue>> result;
260 if (is_sparse_) {
261 std::sort(non_zeros_.begin(), non_zeros_.end());
262 for (const glop::ColIndex col : non_zeros_) {
263 const IntegerValue coeff = dense_vector_[col];
264 if (coeff != 0) result.push_back({col, coeff});
265 }
266 } else {
267 const int size = dense_vector_.size();
268 for (glop::ColIndex col(0); col < size; ++col) {
269 const IntegerValue coeff = dense_vector_[col];
270 if (coeff != 0) result.push_back({col, coeff});
271 }
272 }
273 return result;
274}
275
276// TODO(user): make SatParameters singleton too, otherwise changing them after
277// a constraint was added will have no effect on this class.
279 Model* model, absl::Span<const IntegerVariable> vars)
280 : constraint_manager_(model),
281 parameters_(*(model->GetOrCreate<SatParameters>())),
282 model_(model),
283 time_limit_(model->GetOrCreate<TimeLimit>()),
284 integer_trail_(model->GetOrCreate<IntegerTrail>()),
285 trail_(model->GetOrCreate<Trail>()),
286 watcher_(model->GetOrCreate<GenericLiteralWatcher>()),
287 integer_encoder_(model->GetOrCreate<IntegerEncoder>()),
288 product_detector_(model->GetOrCreate<ProductDetector>()),
289 objective_definition_(model->GetOrCreate<ObjectiveDefinition>()),
290 shared_stats_(model->GetOrCreate<SharedStatistics>()),
291 shared_response_manager_(model->GetOrCreate<SharedResponseManager>()),
292 random_(model->GetOrCreate<ModelRandomGenerator>()),
293 symmetrizer_(model->GetOrCreate<LinearConstraintSymmetrizer>()),
294 linear_propagator_(model->GetOrCreate<LinearPropagator>()),
295 rlt_cut_helper_(model),
296 implied_bounds_processor_({}, integer_trail_,
297 model->GetOrCreate<ImpliedBounds>()),
298 dispatcher_(model->GetOrCreate<LinearProgrammingDispatcher>()),
299 mirror_lp_variable_(*model->GetOrCreate<ModelLpVariableMapping>()),
300 expanded_lp_solution_(*model->GetOrCreate<ModelLpValues>()),
301 expanded_reduced_costs_(*model->GetOrCreate<ModelReducedCosts>()) {
302 // Tweak the default parameters to make the solve incremental.
303 simplex_params_.set_use_dual_simplex(true);
304 simplex_params_.set_primal_feasibility_tolerance(
305 parameters_.lp_primal_tolerance());
306 simplex_params_.set_dual_feasibility_tolerance(
307 parameters_.lp_dual_tolerance());
308 if (parameters_.use_exact_lp_reason()) {
309 simplex_params_.set_change_status_to_imprecise(false);
310 }
311 simplex_.SetParameters(simplex_params_);
312 if (parameters_.search_branching() == SatParameters::LP_SEARCH) {
313 compute_reduced_cost_averages_ = true;
314 }
315
316 // Register our local rev int repository.
317 integer_trail_->RegisterReversibleClass(&rc_rev_int_repository_);
318 mirror_lp_variable_.resize(integer_trail_->NumIntegerVariables(),
320
321 // Register SharedStatistics with the cut helpers.
322 auto* stats = model->GetOrCreate<SharedStatistics>();
323 integer_rounding_cut_helper_.SetSharedStatistics(stats);
324 cover_cut_helper_.SetSharedStatistics(stats);
325
326 // Initialize the IntegerVariable -> ColIndex mapping.
327 CHECK(std::is_sorted(vars.begin(), vars.end()));
328
329 // TODO(user): We shouldn't need to add variable from the orbit here in the
330 // presence of symmetry. However they can still appear in cut, so it is a
331 // bit tricky and require some refactoring to be tried.
332 ColIndex col{0};
333 for (const IntegerVariable positive_variable : vars) {
334 CHECK(VariableIsPositive(positive_variable));
335 implied_bounds_processor_.AddLpVariable(positive_variable);
336 (*dispatcher_)[positive_variable] = this;
337
338 if (!symmetrizer_->AppearInFoldedProblem(positive_variable)) continue;
339
340 integer_variables_.push_back(positive_variable);
341 extended_integer_variables_.push_back(positive_variable);
342 DCHECK_EQ(mirror_lp_variable_[positive_variable], glop::kInvalidCol);
343 mirror_lp_variable_[positive_variable] = col;
344 ++col;
345 }
346
347 // Complete the extended variables with the orbit afterwards.
348 if (symmetrizer_->HasSymmetry()) {
349 for (const IntegerVariable var : integer_variables_) {
350 if (!symmetrizer_->IsOrbitSumVar(var)) continue;
351 const int orbit_index = symmetrizer_->OrbitIndex(var);
352 for (const IntegerVariable var : symmetrizer_->Orbit(orbit_index)) {
353 extended_integer_variables_.push_back(var);
354 DCHECK_EQ(mirror_lp_variable_[var], glop::kInvalidCol);
355 mirror_lp_variable_[var] = col;
356 ++col;
357 }
358 }
359 }
360
361 // We use the "extended" vector for lp_solution_/lp_reduced_cost_.
362 CHECK_EQ(vars.size(), extended_integer_variables_.size());
363 lp_solution_.assign(vars.size(), std::numeric_limits<double>::infinity());
364 lp_reduced_cost_.assign(vars.size(), 0.0);
365
366 if (!vars.empty()) {
367 const IntegerVariable max_index = std::max(
368 NegationOf(vars.back()), integer_trail_->NumIntegerVariables() - 1);
369 if (max_index >= expanded_lp_solution_.size()) {
370 expanded_lp_solution_.assign(max_index.value() + 1, 0.0);
371 }
372 if (max_index >= expanded_reduced_costs_.size()) {
373 expanded_reduced_costs_.assign(max_index.value() + 1, 0.0);
374 }
375 }
376}
377
379 DCHECK(!lp_constraint_is_registered_);
380 bool added = false;
381 bool folded = false;
382 const auto index = constraint_manager_.Add(std::move(ct), &added, &folded);
383
384 // If we added a folded constraints, lets add it to the CP propagators.
385 // This is important as this should tighten the bounds of the orbit sum vars.
386 if (added && folded) {
387 const auto& info = constraint_manager_.AllConstraints()[index];
388 const LinearConstraint& new_ct = info.constraint;
389 const absl::Span<const IntegerVariable> vars = new_ct.VarsAsSpan();
390 if (!info.ub_is_trivial) {
391 if (!linear_propagator_->AddConstraint({}, vars, new_ct.CoeffsAsSpan(),
392 new_ct.ub)) {
393 return false;
394 }
395 }
396 if (!info.lb_is_trivial) {
397 tmp_vars_.assign(vars.begin(), vars.end());
398 for (IntegerVariable& var : tmp_vars_) var = NegationOf(var);
399 if (!linear_propagator_->AddConstraint(
400 {}, tmp_vars_, new_ct.CoeffsAsSpan(), -new_ct.lb)) {
401 return false;
402 }
403 }
404 }
405 return true;
406}
407
409 DCHECK(!lp_constraint_is_registered_);
410 lp_constraint_is_registered_ = true;
411 model->GetOrCreate<LinearProgrammingConstraintCollection>()->push_back(this);
412
413 // Copy objective data to the constraint_manager_.
414 //
415 // Note(user): the sort is not really needed but should lead to better cache
416 // locality.
417 std::sort(integer_objective_.begin(), integer_objective_.end());
418 objective_infinity_norm_ = 0;
419 for (const auto [col, coeff] : integer_objective_) {
420 constraint_manager_.SetObjectiveCoefficient(integer_variables_[col.value()],
421 coeff);
422 objective_infinity_norm_ =
423 std::max(objective_infinity_norm_, IntTypeAbs(coeff));
424 }
425
426 // Set the LP to its initial content.
427 //
428 // Note that we always add LP constraint lazily if we have A LOT of them.
429 // This is because currently on large problem with millions of constraints,
430 // our LP is usually not fast enough anyway.
431 if (!parameters_.add_lp_constraints_lazily() &&
432 constraint_manager_.num_constraints() < 1e6) {
433 constraint_manager_.AddAllConstraintsToLp();
434 }
435 if (!CreateLpFromConstraintManager()) {
436 model->GetOrCreate<SatSolver>()->NotifyThatModelIsUnsat();
437 return;
438 }
439
440 watcher_id_ = watcher_->Register(this);
441 const int num_vars = integer_variables_.size();
442 orbit_indices_.clear();
443 for (int i = 0; i < num_vars; i++) {
444 const IntegerVariable pos_var = integer_variables_[i];
445 DCHECK(symmetrizer_->AppearInFoldedProblem(pos_var));
446 watcher_->WatchIntegerVariable(pos_var, watcher_id_, i);
447 if (symmetrizer_->IsOrbitSumVar(pos_var)) {
448 orbit_indices_.push_back(symmetrizer_->OrbitIndex(pos_var));
449 }
450 }
451 if (objective_is_defined_) {
452 watcher_->WatchUpperBound(objective_cp_, watcher_id_);
453 }
454 watcher_->SetPropagatorPriority(watcher_id_, 2);
455 watcher_->AlwaysCallAtLevelZero(watcher_id_);
456
457 // Registering it with the trail make sure this class is always in sync when
458 // it is used in the decision heuristics.
459 integer_trail_->RegisterReversibleClass(this);
460 watcher_->RegisterReversibleInt(watcher_id_, &rev_optimal_constraints_size_);
461}
462
463glop::ColIndex LinearProgrammingConstraint::GetMirrorVariable(
464 IntegerVariable positive_variable) {
465 DCHECK(VariableIsPositive(positive_variable));
466 return mirror_lp_variable_[positive_variable];
467}
468
470 IntegerValue coeff) {
471 CHECK(!lp_constraint_is_registered_);
472 objective_is_defined_ = true;
473 IntegerVariable pos_var = VariableIsPositive(ivar) ? ivar : NegationOf(ivar);
474 if (ivar != pos_var) coeff = -coeff;
475 integer_objective_.push_back({GetMirrorVariable(pos_var), coeff});
476}
477
478// TODO(user): As the search progress, some variables might get fixed. Exploit
479// this to reduce the number of variables in the LP and in the
480// ConstraintManager? We might also detect during the search that two variable
481// are equivalent.
482//
483// TODO(user): On TSP/VRP with a lot of cuts, this can take 20% of the overall
484// running time. We should be able to almost remove most of this from the
485// profile by being more incremental (modulo LP scaling).
486//
487// TODO(user): A longer term idea for LP with a lot of variables is to not
488// add all variables to each LP solve and do some "sifting". That can be useful
489// for TSP for instance where the number of edges is large, but only a small
490// fraction will be used in the optimal solution.
491bool LinearProgrammingConstraint::CreateLpFromConstraintManager() {
492 // Fill integer_lp_.
493 integer_lp_.clear();
494 integer_lp_cols_.clear();
495 integer_lp_coeffs_.clear();
496
497 infinity_norms_.clear();
498 const auto& all_constraints = constraint_manager_.AllConstraints();
499 for (const auto index : constraint_manager_.LpConstraints()) {
500 const LinearConstraint& ct = all_constraints[index].constraint;
501 if (ct.lb > ct.ub) {
502 VLOG(1) << "Trivial infeasible bound in an LP constraint";
503 return false;
504 }
505
506 integer_lp_.push_back(LinearConstraintInternal());
507 LinearConstraintInternal& new_ct = integer_lp_.back();
508 new_ct.lb = ct.lb;
509 new_ct.ub = ct.ub;
510 new_ct.lb_is_trivial = all_constraints[index].lb_is_trivial;
511 new_ct.ub_is_trivial = all_constraints[index].ub_is_trivial;
512
513 IntegerValue infinity_norm = 0;
514 infinity_norm = std::max(infinity_norm, IntTypeAbs(ct.lb));
515 infinity_norm = std::max(infinity_norm, IntTypeAbs(ct.ub));
516 new_ct.start_in_buffer = integer_lp_cols_.size();
517
518 // TODO(user): Make sure we don't have empty constraint!
519 // this currently can happen in some corner cases.
520 const int size = ct.num_terms;
521 new_ct.num_terms = size;
522 for (int i = 0; i < size; ++i) {
523 // We only use positive variable inside this class.
524 const IntegerVariable var = ct.vars[i];
525 const IntegerValue coeff = ct.coeffs[i];
526 infinity_norm = std::max(infinity_norm, IntTypeAbs(coeff));
527 integer_lp_cols_.push_back(GetMirrorVariable(var));
528 integer_lp_coeffs_.push_back(coeff);
529 }
530 infinity_norms_.push_back(infinity_norm);
531
532 // It is important to keep lp_data_ "clean".
533 DCHECK(std::is_sorted(
534 integer_lp_cols_.data() + new_ct.start_in_buffer,
535 integer_lp_cols_.data() + new_ct.start_in_buffer + new_ct.num_terms));
536 }
537
538 // We remove fixed variables from the objective. This should help the LP
539 // scaling, but also our integer reason computation.
540 int new_size = 0;
541 objective_infinity_norm_ = 0;
542 for (const auto& entry : integer_objective_) {
543 const IntegerVariable var = integer_variables_[entry.first.value()];
544 if (integer_trail_->IsFixedAtLevelZero(var)) {
545 integer_objective_offset_ +=
546 entry.second * integer_trail_->LevelZeroLowerBound(var);
547 continue;
548 }
549 objective_infinity_norm_ =
550 std::max(objective_infinity_norm_, IntTypeAbs(entry.second));
551 integer_objective_[new_size++] = entry;
552 }
553 integer_objective_.resize(new_size);
554 objective_infinity_norm_ =
555 std::max(objective_infinity_norm_, IntTypeAbs(integer_objective_offset_));
556
557 // Scale everything.
558 // TODO(user): As we have an idea of the LP optimal after the first solves,
559 // maybe we can adapt the scaling accordingly.
560 ComputeIntegerLpScalingFactors();
561
562 // Tricky: we use level zero bounds here for the second scaling step below.
563 FillLpData();
564
565 // Fills the helper.
566 scaler_.ConfigureFromFactors(row_factors_, col_factors_);
567 scaler_.AverageCostScaling(&obj_with_slack_);
568 scaler_.ContainOneBoundScaling(simplex_.MutableLowerBounds(),
569 simplex_.MutableUpperBounds());
570
571 // Since we used level zero bounds above, fix them.
572 UpdateBoundsOfLpVariables();
573
574 // Set the information for the step to polish the LP basis. All our variables
575 // are integer, but for now, we just try to minimize the fractionality of the
576 // binary variables.
577 if (parameters_.polish_lp_solution()) {
578 simplex_.ClearIntegralityScales();
579 const int num_vars = integer_variables_.size();
580 for (int i = 0; i < num_vars; ++i) {
581 const IntegerVariable cp_var = integer_variables_[i];
582 const IntegerValue lb = integer_trail_->LevelZeroLowerBound(cp_var);
583 const IntegerValue ub = integer_trail_->LevelZeroUpperBound(cp_var);
584 if (lb != 0 || ub != 1) continue;
585 simplex_.SetIntegralityScale(
586 glop::ColIndex(i),
587 1.0 / scaler_.VariableScalingFactor(glop::ColIndex(i)));
588 }
589 }
590
591 VLOG(3) << "LP relaxation: " << integer_lp_.size() << " x "
592 << integer_variables_.size() << ". "
593 << constraint_manager_.AllConstraints().size()
594 << " Managed constraints.";
595 return true;
596}
597
598// TODO(user): This is a duplicate of glop scaling code, but it allows to
599// work directly on our representation...
600void LinearProgrammingConstraint::ComputeIntegerLpScalingFactors() {
601 const int num_rows = integer_lp_.size();
602 const int num_cols = integer_variables_.size();
603
604 // Assign vectors.
605 const double infinity = std::numeric_limits<double>::infinity();
606 row_factors_.assign(num_rows, 1.0);
607 col_factors_.assign(num_cols, 1.0);
608
609 // Cache pointers to avoid refetching them.
610 IntegerValue* coeffs = integer_lp_coeffs_.data();
611 glop::ColIndex* cols = integer_lp_cols_.data();
612 double* row_factors = row_factors_.data();
613 double* col_factors = col_factors_.data();
614
615 col_min_.assign(num_cols, infinity);
616 col_max_.assign(num_cols, 0.0);
617 double* col_min = col_min_.data();
618 double* col_max = col_max_.data();
619
620 for (int i = 0; i < 4; ++i) {
621 // Scale row geometrically.
622 for (int row = 0; row < num_rows; ++row) {
623 double min_scaled = +infinity;
624 double max_scaled = 0.0;
625 const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)];
626 for (int i = 0; i < ct.num_terms; ++i) {
627 const int index = ct.start_in_buffer + i;
628 const int col = cols[index].value();
629 const double coeff = static_cast<double>(coeffs[index].value());
630 const double scaled_magnitude = col_factors[col] * std::abs(coeff);
631 min_scaled = std::min(min_scaled, scaled_magnitude);
632 max_scaled = std::max(max_scaled, scaled_magnitude);
633 }
634
635 if (ct.num_terms == 0) continue;
636 const Fractional factor(std::sqrt(max_scaled * min_scaled));
637 row_factors[row] = 1.0 / factor;
638 }
639
640 // Scale columns geometrically.
641 for (int row = 0; row < num_rows; ++row) {
642 const double row_factor = row_factors[row];
643 const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)];
644 for (int i = 0; i < ct.num_terms; ++i) {
645 const int index = ct.start_in_buffer + i;
646 const int col = cols[index].value();
647 const double coeff = static_cast<double>(coeffs[index].value());
648 const double scaled_magnitude = row_factor * std::abs(coeff);
649 col_min[col] = std::min(col_min[col], scaled_magnitude);
650 col_max[col] = std::max(col_max[col], scaled_magnitude);
651 }
652 }
653 for (int col = 0; col < num_cols; ++col) {
654 if (col_min[col] == infinity) continue; // Empty.
655 col_factors[col] = 1.0 / std::sqrt(col_min[col] * col_max[col]);
656
657 // Reset, in case we have many fixed variable, faster than assign again.
658 col_min[col] = infinity;
659 col_max[col] = 0;
660 }
661 }
662
663 // Now we equilibrate (i.e. just divide by the max) the row
664 for (int row = 0; row < num_rows; ++row) {
665 double max_scaled = 0.0;
666 const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)];
667 for (int i = 0; i < ct.num_terms; ++i) {
668 const int index = ct.start_in_buffer + i;
669 const int col = cols[index].value();
670 const double coeff = static_cast<double>(coeffs[index].value());
671 const double scaled_magnitude = col_factors[col] * std::abs(coeff);
672 max_scaled = std::max(max_scaled, scaled_magnitude);
673 }
674 if (ct.num_terms == 0) continue;
675 row_factors[row] = 1.0 / max_scaled;
676 }
677
678 // And finally the columns.
679 for (int row = 0; row < num_rows; ++row) {
680 const double row_factor = row_factors[row];
681 const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)];
682 for (int i = 0; i < ct.num_terms; ++i) {
683 const int index = ct.start_in_buffer + i;
684 const int col = cols[index].value();
685 const double coeff = static_cast<double>(coeffs[index].value());
686 const double scaled_magnitude = row_factor * std::abs(coeff);
687 col_max[col] = std::max(col_max[col], scaled_magnitude);
688 }
689 }
690 for (int col = 0; col < num_cols; ++col) {
691 if (col_max[col] == 0) continue; // Empty.
692 col_factors[col] = 1.0 / col_max[col];
693 }
694}
695
696void LinearProgrammingConstraint::FillLpData() {
697 const int num_rows = integer_lp_.size();
698 const int num_cols = integer_variables_.size();
699 IntegerValue* coeffs = integer_lp_coeffs_.data();
700 glop::ColIndex* cols = integer_lp_cols_.data();
701 double* row_factors = row_factors_.data();
702 double* col_factors = col_factors_.data();
703
704 // Now fill the tranposed matrix
705 glop::CompactSparseMatrix* data = simplex_.MutableTransposedMatrixWithSlack();
706 data->Reset(glop::RowIndex(num_cols + num_rows));
707 for (int row = 0; row < num_rows; ++row) {
708 const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)];
709 const double row_factor = row_factors[row];
710 for (int i = 0; i < ct.num_terms; ++i) {
711 const int index = ct.start_in_buffer + i;
712 const int col = cols[index].value();
713 const double coeff = static_cast<double>(coeffs[index].value());
714 const double scaled_coeff = row_factor * col_factors[col] * coeff;
715 data->AddEntryToCurrentColumn(RowIndex(col), scaled_coeff);
716 }
717
718 // Add slack.
719 data->AddEntryToCurrentColumn(RowIndex(num_cols + row), 1.0);
720
721 // Close column.
722 data->CloseCurrentColumn();
723 }
724
725 // Fill and scale the objective.
726 const glop::ColIndex num_cols_with_slacks(num_rows + num_cols);
727 obj_with_slack_.assign(num_cols_with_slacks, 0.0);
728 for (const auto [col, value] : integer_objective_) {
729 obj_with_slack_[col] = ToDouble(value) * col_factors[col.value()];
730 }
731
732 // Fill and scales the bound.
733 simplex_.MutableLowerBounds()->resize(num_cols_with_slacks);
734 simplex_.MutableUpperBounds()->resize(num_cols_with_slacks);
735 Fractional* lb_with_slack = simplex_.MutableLowerBounds()->data();
736 Fractional* ub_with_slack = simplex_.MutableUpperBounds()->data();
737 const double infinity = std::numeric_limits<double>::infinity();
738 for (int row = 0; row < integer_lp_.size(); ++row) {
739 const LinearConstraintInternal& ct = integer_lp_[glop::RowIndex(row)];
740
741 // TODO(user): Using trivial bound might be good for things like
742 // sum bool <= 1 since setting the slack in [0, 1] can lead to bound flip in
743 // the simplex. However if the bound is large, maybe it make more sense to
744 // use +/- infinity.
745 const double factor = row_factors[row];
746 lb_with_slack[num_cols + row] =
747 ct.ub_is_trivial ? -infinity : ToDouble(-ct.ub) * factor;
748 ub_with_slack[num_cols + row] =
749 ct.lb_is_trivial ? +infinity : ToDouble(-ct.lb) * factor;
750 }
751
752 // We scale the LP using the level zero bounds that we later override
753 // with the current ones.
754 //
755 // TODO(user): As part of the scaling, we may also want to shift the initial
756 // variable bounds so that each variable contain the value zero in their
757 // domain. Maybe just once and for all at the beginning.
758 const int num_vars = integer_variables_.size();
759 for (int i = 0; i < num_vars; i++) {
760 const IntegerVariable cp_var = integer_variables_[i];
761 const double factor = col_factors[i];
762 lb_with_slack[i] =
763 ToDouble(integer_trail_->LevelZeroLowerBound(cp_var)) * factor;
764 ub_with_slack[i] =
765 ToDouble(integer_trail_->LevelZeroUpperBound(cp_var)) * factor;
766 }
767}
768
769void LinearProgrammingConstraint::FillReducedCostReasonIn(
770 const glop::DenseRow& reduced_costs,
771 std::vector<IntegerLiteral>* integer_reason) {
772 integer_reason->clear();
773 const int num_vars = integer_variables_.size();
774 for (int i = 0; i < num_vars; i++) {
775 const double rc = reduced_costs[glop::ColIndex(i)];
776 if (rc > kLpEpsilon) {
777 integer_reason->push_back(
778 integer_trail_->LowerBoundAsLiteral(integer_variables_[i]));
779 } else if (rc < -kLpEpsilon) {
780 integer_reason->push_back(
781 integer_trail_->UpperBoundAsLiteral(integer_variables_[i]));
782 }
783 }
784
785 integer_trail_->RemoveLevelZeroBounds(integer_reason);
786}
787
789 // Get rid of all optimal constraint each time we go back to level zero.
790 if (level == 0) rev_optimal_constraints_size_ = 0;
791 optimal_constraints_.resize(rev_optimal_constraints_size_);
792 cumulative_optimal_constraint_sizes_.resize(rev_optimal_constraints_size_);
793 if (lp_solution_is_set_ && level < lp_solution_level_) {
794 lp_solution_is_set_ = false;
795 }
796
797 if (level < previous_level_) {
798 lp_at_optimal_ = false;
799 lp_objective_lower_bound_ = -std::numeric_limits<double>::infinity();
800 }
801 previous_level_ = level;
802
803 // Special case for level zero, we "reload" any previously known optimal
804 // solution from that level.
805 //
806 // TODO(user): Keep all optimal solution in the current branch?
807 // TODO(user): Still try to add cuts/constraints though!
808 // TODO(user): Reload the basis? This might cause issue with the basis
809 // saving/loading code in lb_tree_search.
810 if (level == 0 && !lp_solution_is_set_ && !level_zero_lp_solution_.empty()) {
811 lp_solution_is_set_ = true;
812 lp_solution_ = level_zero_lp_solution_;
813 lp_solution_level_ = 0;
814 // Add the fixed variables. They might have been skipped when we did the
815 // linear relaxation of the model, but cut generators expect all variables
816 // to have an LP value.
817 if (expanded_lp_solution_.size() < integer_trail_->NumIntegerVariables()) {
818 expanded_lp_solution_.resize(integer_trail_->NumIntegerVariables());
819 }
820 for (IntegerVariable i(0); i < integer_trail_->NumIntegerVariables(); ++i) {
821 if (integer_trail_->IsFixed(i)) {
822 expanded_lp_solution_[i] = ToDouble(integer_trail_->LowerBound(i));
823 }
824 }
825 for (int i = 0; i < lp_solution_.size(); i++) {
826 const IntegerVariable var = extended_integer_variables_[i];
827 expanded_lp_solution_[var] = lp_solution_[i];
828 expanded_lp_solution_[NegationOf(var)] = -lp_solution_[i];
829 }
830 }
831}
832
834 cut_generators_.push_back(std::move(generator));
835}
836
838 const std::vector<int>& watch_indices) {
839 if (!enabled_) return true;
840
841 // If we have a really deep branch, with a lot of LP explanation constraint,
842 // we could take a quadratic amount of memory: O(num_var) per number of
843 // propagation in that branch. To avoid that, once the memory starts to be
844 // over a few GB, we only propagate from time to time. This way we do not need
845 // to keep that many constraints around.
846 //
847 // Note that 100 Millions int32_t variables, with the int128 coefficients and
848 // extra propagation vector is already a few GB.
849 if (!cumulative_optimal_constraint_sizes_.empty()) {
850 const double current_size =
851 static_cast<double>(cumulative_optimal_constraint_sizes_.back());
852 const double low_limit = 1e7;
853 if (current_size > low_limit) {
854 // We only propagate if we use less that 100 times the number of current
855 // integer literal enqueued.
856 const double num_enqueues = static_cast<double>(integer_trail_->Index());
857 if ((current_size - low_limit) > 100 * num_enqueues) return true;
858 }
859 }
860
861 if (!lp_solution_is_set_) {
862 return Propagate();
863 }
864
865 // At level zero, if there is still a chance to add cuts or lazy constraints,
866 // we re-run the LP.
867 if (trail_->CurrentDecisionLevel() == 0 && !lp_at_level_zero_is_final_) {
868 return Propagate();
869 }
870
871 // Check whether the change breaks the current LP solution. If it does, call
872 // Propagate() on the current LP.
873 for (const int index : watch_indices) {
874 const double lb =
875 ToDouble(integer_trail_->LowerBound(integer_variables_[index]));
876 const double ub =
877 ToDouble(integer_trail_->UpperBound(integer_variables_[index]));
878 const double value = lp_solution_[index];
879 if (value < lb - kCpEpsilon || value > ub + kCpEpsilon) return Propagate();
880 }
881
882 // TODO(user): The saved lp solution is still valid given the current variable
883 // bounds, so the LP optimal didn't change. However we might still want to add
884 // new cuts or new lazy constraints?
885 //
886 // TODO(user): Propagate the last optimal_constraint? Note that we need
887 // to be careful since the reversible int in IntegerSumLE are not registered.
888 // However, because we delete "optimalconstraints" on backtrack, we might not
889 // care.
890 //
891 // Remark: Note that if we do the sequence SetBasis() / Propagate() /
892 // GetAndSaveBasis() and we are in the case where the solution is still
893 // optimal, we should get the basis from when the lp solution was set which
894 // should be what we want. Even if we set garbage during SetBasis() it should
895 // be ignored. TODO(user): We might still have problem at level zero.
896 return true;
897}
898
899glop::Fractional LinearProgrammingConstraint::GetVariableValueAtCpScale(
900 glop::ColIndex var) {
901 return scaler_.UnscaleVariableValue(var, simplex_.GetVariableValue(var));
902}
903
905 IntegerVariable variable) const {
906 return lp_solution_[mirror_lp_variable_[variable].value()];
907}
908
909void LinearProgrammingConstraint::UpdateBoundsOfLpVariables() {
910 const int num_vars = integer_variables_.size();
911 Fractional* lb_with_slack = simplex_.MutableLowerBounds()->data();
912 Fractional* ub_with_slack = simplex_.MutableUpperBounds()->data();
913 for (int i = 0; i < num_vars; i++) {
914 const IntegerVariable cp_var = integer_variables_[i];
915 const double lb =
916 static_cast<double>(integer_trail_->LowerBound(cp_var).value());
917 const double ub =
918 static_cast<double>(integer_trail_->UpperBound(cp_var).value());
919 const double factor = scaler_.VariableScalingFactor(glop::ColIndex(i));
920 lb_with_slack[i] = lb * factor;
921 ub_with_slack[i] = ub * factor;
922 }
923}
924
925bool LinearProgrammingConstraint::SolveLp() {
926 const int level = trail_->CurrentDecisionLevel();
927 if (level == 0) {
928 lp_at_level_zero_is_final_ = false;
929 }
930
931 const double unscaling_factor = 1.0 / scaler_.ObjectiveScalingFactor();
932 const double offset_before_unscaling =
933 ToDouble(integer_objective_offset_) * scaler_.ObjectiveScalingFactor();
934 const auto status = simplex_.MinimizeFromTransposedMatrixWithSlack(
935 obj_with_slack_, unscaling_factor, offset_before_unscaling, time_limit_);
936
937 // Lets resolve from scratch if we encounter this status.
938 if (simplex_.GetProblemStatus() == glop::ProblemStatus::ABNORMAL) {
939 VLOG(2) << "The LP solver returned abnormal, resolving from scratch";
940 simplex_.ClearStateForNextSolve();
941 const auto status = simplex_.MinimizeFromTransposedMatrixWithSlack(
942 obj_with_slack_, unscaling_factor, offset_before_unscaling,
943 time_limit_);
944 }
945
946 state_ = simplex_.GetState();
947 total_num_simplex_iterations_ += simplex_.GetNumberOfIterations();
948 if (!status.ok()) {
949 VLOG(2) << "The LP solver encountered an error: " << status.error_message();
950 simplex_.ClearStateForNextSolve();
951 return false;
952 }
953 average_degeneracy_.AddData(CalculateDegeneracy());
954 if (average_degeneracy_.CurrentAverage() >= 1000.0) {
955 VLOG(2) << "High average degeneracy: "
956 << average_degeneracy_.CurrentAverage();
957 }
958
959 const int status_as_int = static_cast<int>(simplex_.GetProblemStatus());
960 if (status_as_int >= num_solves_by_status_.size()) {
961 num_solves_by_status_.resize(status_as_int + 1);
962 }
963 num_solves_++;
964 num_solves_by_status_[status_as_int]++;
965 VLOG(2) << DimensionString() << " lvl:" << trail_->CurrentDecisionLevel()
966 << " " << simplex_.GetProblemStatus()
967 << " iter:" << simplex_.GetNumberOfIterations()
968 << " obj:" << simplex_.GetObjectiveValue() << " scaled:"
969 << objective_definition_->ScaleObjective(
970 simplex_.GetObjectiveValue());
971
972 if (simplex_.GetProblemStatus() == glop::ProblemStatus::OPTIMAL ||
973 simplex_.GetProblemStatus() == glop::ProblemStatus::DUAL_FEASIBLE) {
974 lp_objective_lower_bound_ = simplex_.GetObjectiveValue();
975 }
976 lp_at_optimal_ = simplex_.GetProblemStatus() == glop::ProblemStatus::OPTIMAL;
977
978 // If stop_after_root_propagation() is true, we still copy whatever we have as
979 // these values will be used for the local-branching lns heuristic.
980 if (simplex_.GetProblemStatus() == glop::ProblemStatus::OPTIMAL ||
981 parameters_.stop_after_root_propagation()) {
982 lp_solution_is_set_ = true;
983 lp_solution_level_ = trail_->CurrentDecisionLevel();
984 const int num_vars = integer_variables_.size();
985 const auto reduced_costs = simplex_.GetReducedCosts().const_view();
986 for (int i = 0; i < num_vars; i++) {
987 const glop::ColIndex col(i);
988 const IntegerVariable var = integer_variables_[i];
989
990 const glop::Fractional value = GetVariableValueAtCpScale(col);
991 lp_solution_[i] = value;
992 expanded_lp_solution_[var] = value;
993 expanded_lp_solution_[NegationOf(var)] = -value;
994
995 const glop::Fractional rc =
996 scaler_.UnscaleReducedCost(col, reduced_costs[col]);
997 lp_reduced_cost_[i] = rc;
998 expanded_reduced_costs_[var] = rc;
999 expanded_reduced_costs_[NegationOf(var)] = -rc;
1000 }
1001
1002 // Lets fix the result in case of symmetry since the variable in symmetry
1003 // are actually not part of the LP, they will just be at their bounds.
1004 for (const int orbit_index : orbit_indices_) {
1005 const IntegerVariable sum_var = symmetrizer_->OrbitSumVar(orbit_index);
1006 const double lp_value = expanded_lp_solution_[sum_var];
1007 const absl::Span<const IntegerVariable> orbit =
1008 symmetrizer_->Orbit(orbit_index);
1009
1010 // At level zero, we want to focus on the cuts, and we split the value
1011 // evently across all the orbit. however at positive level we don't do
1012 // cuts and we want values that make more sense to follow heuristically.
1013 // In particular, if the sum_var is at its lower/upper bounds, we want
1014 // the same for all variable from the orbit. This partially address the
1015 // TODO below.
1016 bool to_lower = false;
1017 bool to_upper = false;
1018 if (trail_->CurrentDecisionLevel() > 0) {
1019 const double lb =
1020 static_cast<double>(integer_trail_->LowerBound(sum_var).value());
1021 const double ub =
1022 static_cast<double>(integer_trail_->UpperBound(sum_var).value());
1023 if (std::abs(lp_value - lb) < 1e-2) {
1024 to_lower = true;
1025 } else if (std::abs(lp_value - ub) < 1e-2) {
1026 to_upper = true;
1027 }
1028 }
1029
1030 // We assign sum / orbit_size to each variables.
1031 // This is still an LP optimal, but not necessarily a good heuristic.
1032 //
1033 // TODO(user): using sum / orbit_size is good for the cut generation that
1034 // might still use these variables, any violated cuts on the original
1035 // problem where all variables in the orbit have the same value will
1036 // result in a violated cut for the folded problem. However it is probably
1037 // not so good for the heuristics that uses the LP values. In particular
1038 // it might result in LP value not even within the bounds of the
1039 // individual variable since as we branch, we don't have an identical
1040 // domain for all variables in an orbit. Maybe we can generate two
1041 // solutions vectors, one for the cuts and one for the heuristics, or we
1042 // can add custom code to the cuts so that they don't depend on this.
1043 const double even_split = lp_value / static_cast<double>(orbit.size());
1044
1045 // For the reduced costs, they are the same. There should be no
1046 // complication there.
1047 const double new_rc = expanded_reduced_costs_[sum_var];
1048
1049 for (const IntegerVariable var : orbit) {
1050 const glop::ColIndex col = GetMirrorVariable(var);
1051
1052 double new_value = even_split;
1053 if (to_lower) {
1054 new_value =
1055 static_cast<double>(integer_trail_->LowerBound(var).value());
1056 } else if (to_upper) {
1057 new_value =
1058 static_cast<double>(integer_trail_->UpperBound(var).value());
1059 }
1060
1061 lp_solution_[col.value()] = new_value;
1062 expanded_lp_solution_[var] = new_value;
1063 expanded_lp_solution_[NegationOf(var)] = -new_value;
1064
1065 lp_reduced_cost_[col.value()] = new_rc;
1066 expanded_reduced_costs_[var] = new_rc;
1067 expanded_reduced_costs_[NegationOf(var)] = -new_rc;
1068 }
1069 }
1070
1071 // Compute integrality.
1072 lp_solution_is_integer_ = true;
1073 for (const double value : lp_solution_) {
1074 if (std::abs(value - std::round(value)) > kCpEpsilon) {
1075 lp_solution_is_integer_ = false;
1076 break;
1077 }
1078 }
1079
1080 if (lp_solution_level_ == 0) {
1081 level_zero_lp_solution_ = lp_solution_;
1082 }
1083 }
1084
1085 return true;
1086}
1087
1088bool LinearProgrammingConstraint::AnalyzeLp() {
1089 // A dual-unbounded problem is infeasible. We use the dual ray reason.
1090 if (simplex_.GetProblemStatus() == glop::ProblemStatus::DUAL_UNBOUNDED) {
1091 if (parameters_.use_exact_lp_reason()) {
1092 return PropagateExactDualRay();
1093 }
1094 FillReducedCostReasonIn(simplex_.GetDualRayRowCombination(),
1095 &integer_reason_);
1096 return integer_trail_->ReportConflict(integer_reason_);
1097 }
1098
1099 // TODO(user): Update limits for DUAL_UNBOUNDED status as well.
1100 UpdateSimplexIterationLimit(/*min_iter=*/10, /*max_iter=*/1000);
1101
1102 // Optimality deductions if problem has an objective.
1103 // If there is no objective, then all duals will just be zero.
1104 if (objective_is_defined_ &&
1105 (simplex_.GetProblemStatus() == glop::ProblemStatus::OPTIMAL ||
1106 simplex_.GetProblemStatus() == glop::ProblemStatus::DUAL_FEASIBLE)) {
1107 // TODO(user): Maybe do a bit less computation when we cannot propagate
1108 // anything.
1109 if (parameters_.use_exact_lp_reason()) {
1110 if (!PropagateExactLpReason()) return false;
1111
1112 // Display when the inexact bound would have propagated more.
1113 if (VLOG_IS_ON(2)) {
1114 const double relaxed_optimal_objective = simplex_.GetObjectiveValue();
1115 const IntegerValue approximate_new_lb(static_cast<int64_t>(
1116 std::ceil(relaxed_optimal_objective - kCpEpsilon)));
1117 const IntegerValue propagated_lb =
1118 integer_trail_->LowerBound(objective_cp_);
1119 if (approximate_new_lb > propagated_lb) {
1120 VLOG(2) << "LP objective [ " << ToDouble(propagated_lb) << ", "
1121 << ToDouble(integer_trail_->UpperBound(objective_cp_))
1122 << " ] approx_lb += "
1123 << ToDouble(approximate_new_lb - propagated_lb) << " gap: "
1124 << integer_trail_->UpperBound(objective_cp_) - propagated_lb;
1125 }
1126 }
1127 } else {
1128 // Try to filter optimal objective value. Note that GetObjectiveValue()
1129 // already take care of the scaling so that it returns an objective in the
1130 // CP world.
1131 FillReducedCostReasonIn(simplex_.GetReducedCosts(), &integer_reason_);
1132 const double objective_cp_ub =
1133 ToDouble(integer_trail_->UpperBound(objective_cp_));
1134 const double relaxed_optimal_objective = simplex_.GetObjectiveValue();
1135 ReducedCostStrengtheningDeductions(objective_cp_ub -
1136 relaxed_optimal_objective);
1137 if (!deductions_.empty()) {
1138 deductions_reason_ = integer_reason_;
1139 deductions_reason_.push_back(
1140 integer_trail_->UpperBoundAsLiteral(objective_cp_));
1141 }
1142
1143 // Push new objective lb.
1144 const IntegerValue approximate_new_lb(static_cast<int64_t>(
1145 std::ceil(relaxed_optimal_objective - kCpEpsilon)));
1146 if (approximate_new_lb > integer_trail_->LowerBound(objective_cp_)) {
1147 const IntegerLiteral deduction =
1148 IntegerLiteral::GreaterOrEqual(objective_cp_, approximate_new_lb);
1149 if (!integer_trail_->Enqueue(deduction, {}, integer_reason_)) {
1150 return false;
1151 }
1152 }
1153
1154 // Push reduced cost strengthening bounds.
1155 if (!deductions_.empty()) {
1156 const int trail_index_with_same_reason = integer_trail_->Index();
1157 for (const IntegerLiteral deduction : deductions_) {
1158 if (!integer_trail_->Enqueue(deduction, {}, deductions_reason_,
1159 trail_index_with_same_reason)) {
1160 return false;
1161 }
1162 }
1163 }
1164 }
1165 }
1166
1167 // Copy more info about the current solution.
1168 if (compute_reduced_cost_averages_ &&
1169 simplex_.GetProblemStatus() == glop::ProblemStatus::OPTIMAL) {
1170 CHECK(lp_solution_is_set_);
1171 UpdateAverageReducedCosts();
1172 }
1173
1174 // On some problem, LP solves and cut rounds can be slow, so we report
1175 // the current possible objective improvement in the middle of the
1176 // propagation, not just at the end.
1177 //
1178 // Note that we currently only do that if the LP is "full" and its objective
1179 // is the same as the one of the whole problem.
1180 if (objective_is_defined_ &&
1181 objective_definition_->objective_var == objective_cp_ &&
1182 trail_->CurrentDecisionLevel() == 0) {
1183 shared_response_manager_->UpdateInnerObjectiveBounds(
1184 model_->Name(), integer_trail_->LowerBound(objective_cp_),
1185 integer_trail_->UpperBound(objective_cp_));
1186 }
1187
1188 return true;
1189}
1190
1191// Note that since we call this on the constraint with slack, we actually have
1192// linear expression == rhs, we can use this to propagate more!
1193//
1194// TODO(user): Also propagate on -cut ? in practice we already do that in many
1195// places were we try to generate the cut on -cut... But we could do it sooner
1196// and more cleanly here.
1197bool LinearProgrammingConstraint::PreprocessCut(IntegerVariable first_slack,
1198 CutData* cut) {
1199 // Because of complement, all coeffs and all terms are positive after this.
1200 cut->ComplementForPositiveCoefficients();
1201 if (cut->rhs < 0) {
1202 problem_proven_infeasible_by_cuts_ = true;
1203 return false;
1204 }
1205
1206 // Limited DP to compute first few reachable values.
1207 // Note that all coeff are positive.
1208 reachable_.Reset();
1209 for (const CutTerm& term : cut->terms) {
1210 reachable_.Add(term.coeff.value());
1211 }
1212
1213 // Extra propag since we know it is actually an equality constraint.
1214 if (cut->rhs < absl::int128(reachable_.LastValue()) &&
1215 !reachable_.MightBeReachable(static_cast<int64_t>(cut->rhs))) {
1216 problem_proven_infeasible_by_cuts_ = true;
1217 return false;
1218 }
1219
1220 bool some_fixed_terms = false;
1221 bool some_fractional_positions = false;
1222 for (CutTerm& term : cut->terms) {
1223 const absl::int128 magnitude128 = term.coeff.value();
1224 const absl::int128 range =
1225 absl::int128(term.bound_diff.value()) * magnitude128;
1226
1227 IntegerValue new_diff = term.bound_diff;
1228 if (range > cut->rhs) {
1229 new_diff = static_cast<int64_t>(cut->rhs / magnitude128);
1230 }
1231 {
1232 // Extra propag since we know it is actually an equality constraint.
1233 absl::int128 rest128 =
1234 cut->rhs - absl::int128(new_diff.value()) * magnitude128;
1235 while (rest128 < absl::int128(reachable_.LastValue()) &&
1236 !reachable_.MightBeReachable(static_cast<int64_t>(rest128))) {
1237 ++total_num_eq_propagations_;
1238 CHECK_GT(new_diff, 0);
1239 --new_diff;
1240 rest128 += magnitude128;
1241 }
1242 }
1243
1244 if (new_diff < term.bound_diff) {
1245 term.bound_diff = new_diff;
1246
1247 const IntegerVariable var = term.expr_vars[0];
1248 if (var < first_slack) {
1249 // Normal variable.
1250 ++total_num_cut_propagations_;
1251
1252 // Note that at this stage we only have X - lb or ub - X.
1253 if (term.expr_coeffs[0] == 1) {
1254 // X + offset <= bound_diff
1255 if (!integer_trail_->Enqueue(
1257 var, term.bound_diff - term.expr_offset),
1258 {}, {})) {
1259 problem_proven_infeasible_by_cuts_ = true;
1260 return false;
1261 }
1262 } else {
1263 CHECK_EQ(term.expr_coeffs[0], -1);
1264 // offset - X <= bound_diff
1265 if (!integer_trail_->Enqueue(
1267 var, term.expr_offset - term.bound_diff),
1268 {}, {})) {
1269 problem_proven_infeasible_by_cuts_ = true;
1270 return false;
1271 }
1272 }
1273 } else {
1274 // This is a tighter bound on one of the constraint! like a cut. Note
1275 // that in some corner case, new cut can be merged and update the bounds
1276 // of the constraint before this code.
1277 const int slack_index = (var.value() - first_slack.value()) / 2;
1278 const glop::RowIndex row = tmp_slack_rows_[slack_index];
1279 if (term.expr_coeffs[0] == 1) {
1280 // slack = ct + offset <= bound_diff;
1281 const IntegerValue new_ub = term.bound_diff - term.expr_offset;
1282 if (constraint_manager_.UpdateConstraintUb(row, new_ub)) {
1283 integer_lp_[row].ub = new_ub;
1284 }
1285 } else {
1286 // slack = offset - ct <= bound_diff;
1287 CHECK_EQ(term.expr_coeffs[0], -1);
1288 const IntegerValue new_lb = term.expr_offset - term.bound_diff;
1289 if (constraint_manager_.UpdateConstraintLb(row, new_lb)) {
1290 integer_lp_[row].lb = new_lb;
1291 }
1292 }
1293 }
1294 }
1295
1296 if (term.bound_diff == 0) {
1297 some_fixed_terms = true;
1298 } else {
1299 if (term.IsFractional()) {
1300 some_fractional_positions = true;
1301 }
1302 }
1303 }
1304
1305 // Remove fixed terms if any.
1306 if (some_fixed_terms) {
1307 int new_size = 0;
1308 for (const CutTerm& term : cut->terms) {
1309 if (term.bound_diff == 0) continue;
1310 cut->terms[new_size++] = term;
1311 }
1312 cut->terms.resize(new_size);
1313 }
1314 return some_fractional_positions;
1315}
1316
1317bool LinearProgrammingConstraint::AddCutFromConstraints(
1318 absl::string_view name,
1319 absl::Span<const std::pair<RowIndex, IntegerValue>> integer_multipliers) {
1320 // This is initialized to a valid linear constraint (by taking linear
1321 // combination of the LP rows) and will be transformed into a cut if
1322 // possible.
1323 //
1324 // TODO(user): For CG cuts, Ideally this linear combination should have only
1325 // one fractional variable (basis_col). But because of imprecision, we can get
1326 // a bunch of fractional entry with small coefficient (relative to the one of
1327 // basis_col). We try to handle that in IntegerRoundingCut(), but it might be
1328 // better to add small multiple of the involved rows to get rid of them.
1329 IntegerValue cut_ub;
1330 if (!ComputeNewLinearConstraint(integer_multipliers, &tmp_scattered_vector_,
1331 &cut_ub)) {
1332 ++num_cut_overflows_;
1333 VLOG(1) << "Issue, overflow!";
1334 return false;
1335 }
1336
1337 // Important: because we use integer_multipliers below to create the slack,
1338 // we cannot try to adjust this linear combination easily.
1339 //
1340 // TODO(user): the conversion col_index -> IntegerVariable is slow and could
1341 // in principle be removed. Easy for cuts, but not so much for
1342 // implied_bounds_processor_. Note that in theory this could allow us to
1343 // use Literal directly without the need to have an IntegerVariable for them.
1344 tmp_scattered_vector_.ConvertToCutData(
1345 cut_ub.value(), extended_integer_variables_, lp_solution_, integer_trail_,
1346 &base_ct_);
1347
1348 // If there are no integer (all Booleans), no need to try implied bounds
1349 // heurititics. By setting this to nullptr, we are a bit faster.
1350 ImpliedBoundsProcessor* ib_processor = nullptr;
1351 {
1352 bool some_ints = false;
1353 bool some_fractional_positions = false;
1354 for (const CutTerm& term : base_ct_.terms) {
1355 if (term.bound_diff > 1) some_ints = true;
1356 if (term.IsFractional()) {
1357 some_fractional_positions = true;
1358 }
1359 }
1360
1361 // If all value are integer, we will not be able to cut anything.
1362 if (!some_fractional_positions) return false;
1363 if (some_ints) ib_processor = &implied_bounds_processor_;
1364 }
1365
1366 // Add constraint slack.
1367 // This is important for correctness here.
1368 const IntegerVariable first_slack(expanded_lp_solution_.size());
1369 CHECK_EQ(first_slack.value() % 2, 0);
1370 tmp_slack_rows_.clear();
1371 for (const auto [row, coeff] : integer_multipliers) {
1372 if (integer_lp_[row].lb == integer_lp_[row].ub) continue;
1373
1374 CutTerm entry;
1375 entry.coeff = coeff > 0 ? coeff : -coeff;
1376 entry.lp_value = 0.0;
1377 entry.bound_diff = integer_lp_[row].ub - integer_lp_[row].lb;
1378 entry.expr_vars[0] =
1379 first_slack + 2 * IntegerVariable(tmp_slack_rows_.size());
1380 entry.expr_coeffs[1] = 0;
1381 const double activity = scaler_.UnscaleConstraintActivity(
1382 row, simplex_.GetConstraintActivity(row));
1383 if (coeff > 0) {
1384 // Slack = ub - constraint;
1385 entry.lp_value = ToDouble(integer_lp_[row].ub) - activity;
1386 entry.expr_coeffs[0] = IntegerValue(-1);
1387 entry.expr_offset = integer_lp_[row].ub;
1388 } else {
1389 // Slack = constraint - lb;
1390 entry.lp_value = activity - ToDouble(integer_lp_[row].lb);
1391 entry.expr_coeffs[0] = IntegerValue(1);
1392 entry.expr_offset = -integer_lp_[row].lb;
1393 }
1394
1395 base_ct_.terms.push_back(entry);
1396 tmp_slack_rows_.push_back(row);
1397 }
1398
1399 // This also make all coefficients positive.
1400 if (!PreprocessCut(first_slack, &base_ct_)) return false;
1401
1402 // We cannot cut sum Bool <= 1.
1403 if (base_ct_.rhs == 1) return false;
1404
1405 // Constraint with slack should be tight.
1406 if (DEBUG_MODE) {
1407 double activity = 0.0;
1408 double norm = 0.0;
1409 for (const CutTerm& term : base_ct_.terms) {
1410 const double coeff = ToDouble(term.coeff);
1411 activity += term.lp_value * coeff;
1412 norm += coeff * coeff;
1413 }
1414 if (std::abs(activity - static_cast<double>(base_ct_.rhs)) / norm > 1e-4) {
1415 VLOG(1) << "Cut not tight " << activity
1416 << " <= " << static_cast<double>(base_ct_.rhs);
1417 return false;
1418 }
1419 }
1420
1421 bool at_least_one_added = false;
1422 DCHECK(base_ct_.AllCoefficientsArePositive());
1423
1424 // Try RLT cuts.
1425 //
1426 // TODO(user): try this for more than just "base" constraints?
1427 if (integer_multipliers.size() == 1 && parameters_.add_rlt_cuts()) {
1428 if (rlt_cut_helper_.TrySimpleSeparation(base_ct_)) {
1429 at_least_one_added |= PostprocessAndAddCut(
1430 absl::StrCat(name, "_RLT"), rlt_cut_helper_.Info(), first_slack,
1431 rlt_cut_helper_.cut());
1432 }
1433 }
1434
1435 // Note that the indexing will survive ComplementForSmallerLpValues() below.
1436 if (ib_processor != nullptr) {
1437 if (!ib_processor->CacheDataForCut(first_slack, &base_ct_)) {
1438 ib_processor = nullptr;
1439 }
1440 }
1441
1442 // Try cover approach to find cut.
1443 // TODO(user): Share common computation between kinds.
1444 {
1445 cover_cut_helper_.ClearCache();
1446
1447 if (cover_cut_helper_.TrySingleNodeFlow(base_ct_, ib_processor)) {
1448 at_least_one_added |= PostprocessAndAddCut(
1449 absl::StrCat(name, "_FF"), cover_cut_helper_.Info(), first_slack,
1450 cover_cut_helper_.cut());
1451 }
1452 if (cover_cut_helper_.TrySimpleKnapsack(base_ct_, ib_processor)) {
1453 at_least_one_added |= PostprocessAndAddCut(
1454 absl::StrCat(name, "_K"), cover_cut_helper_.Info(), first_slack,
1455 cover_cut_helper_.cut());
1456 }
1457
1458 // This one need to be called after TrySimpleKnapsack() in order to reuse
1459 // some cached data if possible.
1460 if (cover_cut_helper_.TryWithLetchfordSouliLifting(base_ct_,
1461 ib_processor)) {
1462 at_least_one_added |= PostprocessAndAddCut(
1463 absl::StrCat(name, "_KL"), cover_cut_helper_.Info(), first_slack,
1464 cover_cut_helper_.cut());
1465 }
1466 }
1467
1468 // Try integer rounding heuristic to find cut.
1469 {
1470 base_ct_.ComplementForSmallerLpValues();
1471
1472 RoundingOptions options;
1473 options.max_scaling = parameters_.max_integer_rounding_scaling();
1474 options.use_ib_before_heuristic = false;
1475 if (integer_rounding_cut_helper_.ComputeCut(options, base_ct_,
1476 ib_processor)) {
1477 at_least_one_added |= PostprocessAndAddCut(
1478 absl::StrCat(name, "_R"), integer_rounding_cut_helper_.Info(),
1479 first_slack, integer_rounding_cut_helper_.cut());
1480 }
1481
1482 options.use_ib_before_heuristic = true;
1483 options.prefer_positive_ib = false;
1484 if (ib_processor != nullptr && integer_rounding_cut_helper_.ComputeCut(
1485 options, base_ct_, ib_processor)) {
1486 at_least_one_added |= PostprocessAndAddCut(
1487 absl::StrCat(name, "_RB"), integer_rounding_cut_helper_.Info(),
1488 first_slack, integer_rounding_cut_helper_.cut());
1489 }
1490
1491 options.use_ib_before_heuristic = true;
1492 options.prefer_positive_ib = true;
1493 if (ib_processor != nullptr && integer_rounding_cut_helper_.ComputeCut(
1494 options, base_ct_, ib_processor)) {
1495 at_least_one_added |= PostprocessAndAddCut(
1496 absl::StrCat(name, "_RBP"), integer_rounding_cut_helper_.Info(),
1497 first_slack, integer_rounding_cut_helper_.cut());
1498 }
1499 }
1500
1501 return at_least_one_added;
1502}
1503
1504bool LinearProgrammingConstraint::PostprocessAndAddCut(
1505 const std::string& name, const std::string& info,
1506 IntegerVariable first_slack, const CutData& cut) {
1507 if (cut.rhs > absl::int128(std::numeric_limits<int64_t>::max()) ||
1508 cut.rhs < absl::int128(std::numeric_limits<int64_t>::min())) {
1509 VLOG(2) << "RHS overflow " << name << " " << info;
1510 ++num_cut_overflows_;
1511 return false;
1512 }
1513
1514 // We test this here to avoid doing the costly conversion below.
1515 //
1516 // TODO(user): Ideally we should detect this even earlier during the cut
1517 // generation.
1518 if (cut.ComputeViolation() < 1e-4) {
1519 VLOG(3) << "Bad cut " << name << " " << info;
1520 ++num_bad_cuts_;
1521 return false;
1522 }
1523
1524 // Substitute any slack left.
1525 tmp_scattered_vector_.ClearAndResize(extended_integer_variables_.size());
1526 IntegerValue cut_ub = static_cast<int64_t>(cut.rhs);
1527 for (const CutTerm& term : cut.terms) {
1528 if (term.coeff == 0) continue;
1529 if (!AddProductTo(-term.coeff, term.expr_offset, &cut_ub)) {
1530 VLOG(2) << "Overflow in conversion";
1531 ++num_cut_overflows_;
1532 return false;
1533 }
1534 for (int i = 0; i < 2; ++i) {
1535 if (term.expr_coeffs[i] == 0) continue;
1536 const IntegerVariable var = term.expr_vars[i];
1537 const IntegerValue coeff =
1538 CapProd(term.coeff.value(), term.expr_coeffs[i].value());
1539 if (AtMinOrMaxInt64(coeff.value())) {
1540 VLOG(2) << "Overflow in conversion";
1541 ++num_cut_overflows_;
1542 return false;
1543 }
1544
1545 // Simple copy for non-slack variables.
1546 if (var < first_slack) {
1547 const glop::ColIndex col = mirror_lp_variable_[PositiveVariable(var)];
1548 if (VariableIsPositive(var)) {
1549 tmp_scattered_vector_.Add(col, coeff);
1550 } else {
1551 tmp_scattered_vector_.Add(col, -coeff);
1552 }
1553 continue;
1554 } else {
1555 // Replace slack from LP constraints.
1556 const int slack_index = (var.value() - first_slack.value()) / 2;
1557 const glop::RowIndex row = tmp_slack_rows_[slack_index];
1558 if (!tmp_scattered_vector_.AddLinearExpressionMultiple(
1559 coeff, IntegerLpRowCols(row), IntegerLpRowCoeffs(row),
1560 infinity_norms_[row])) {
1561 VLOG(2) << "Overflow in slack removal";
1562 ++num_cut_overflows_;
1563 return false;
1564 }
1565 }
1566 }
1567 }
1568
1569 // TODO(user): avoid allocating memory if it turns out this is a duplicate of
1570 // something we already added. This tends to happen if the duplicate was
1571 // already a generated cut which is currently not part of the LP.
1572 LinearConstraint converted_cut =
1573 tmp_scattered_vector_.ConvertToLinearConstraint(
1574 extended_integer_variables_, cut_ub);
1575
1576 // TODO(user): We probably generate too many cuts, keep best one only?
1577 // Note that we do need duplicate removal and maybe some orthogonality here?
1578 if (/*DISABLES CODE*/ (false)) {
1579 top_n_cuts_.AddCut(std::move(converted_cut), name, expanded_lp_solution_);
1580 return true;
1581 }
1582 return constraint_manager_.AddCut(std::move(converted_cut), name, info);
1583}
1584
1585// TODO(user): This can be still too slow on some problems like
1586// 30_70_45_05_100.mps.gz. Not this actual function, but the set of computation
1587// it triggers. We should add heuristics to abort earlier if a cut is not
1588// promising. Or only test a few positions and not all rows.
1589void LinearProgrammingConstraint::AddCGCuts() {
1590 std::vector<std::pair<RowIndex, double>> sorted_columns;
1591 const RowIndex num_rows(integer_lp_.size());
1592 glop::DenseColumn::ConstView norms = simplex_.GetDualSquaredNorms();
1593 for (RowIndex index(0); index < num_rows; ++index) {
1594 const ColIndex basis_col = simplex_.GetBasis(index);
1595
1596 // We used to skip slack and also not to do "classical" gomory and instead
1597 // call IgnoreTrivialConstraintMultipliers() heuristic. It is usually faster
1598 // but on some problem like neos*creuse or neos-888544, this do not find
1599 // good cut though.
1600 //
1601 // TODO(user): Tune this. It seems better but we need to handle nicely the
1602 // extra amount of cuts this produces.
1603 if (basis_col >= integer_variables_.size()) continue;
1604
1605 // Get he variable value at cp-scale. Similar to GetVariableValueAtCpScale()
1606 // but this works for slack variable too.
1607 const Fractional lp_value =
1608 simplex_.GetVariableValue(basis_col) /
1609 scaler_.VariableScalingFactorWithSlack(basis_col);
1610
1611 // Only consider fractional basis element. We ignore element that are close
1612 // to an integer to reduce the amount of positions we try.
1613 //
1614 // TODO(user): We could just look at the diff with std::floor() in the hope
1615 // that when we are just under an integer, the exact computation below will
1616 // also be just under it.
1617 const double fractionality = std::abs(lp_value - std::round(lp_value));
1618 if (fractionality < 0.01) continue;
1619
1620 const double score = fractionality * (1.0 - fractionality) / norms[index];
1621 sorted_columns.push_back({index, score});
1622 }
1623 absl::c_sort(sorted_columns, [](const std::pair<RowIndex, double>& a,
1624 const std::pair<RowIndex, double>& b) {
1625 return a.second > b.second;
1626 });
1627
1628 int num_added = 0;
1629 for (const auto [index, _] : sorted_columns) {
1630 if (time_limit_->LimitReached()) return;
1631 // We multiply by row_factors_ directly, which might be slightly more
1632 // precise than dividing by 1/factor like UnscaleLeftSolveValue() does.
1633 //
1634 // TODO(user): Avoid code duplication between the sparse/dense path.
1635 tmp_lp_multipliers_.clear();
1636 const glop::ScatteredRow& lambda = simplex_.GetUnitRowLeftInverse(index);
1637 if (lambda.non_zeros.empty()) {
1638 for (RowIndex row(0); row < num_rows; ++row) {
1639 const double value = lambda.values[glop::RowToColIndex(row)];
1640 if (std::abs(value) < kZeroTolerance) continue;
1641 tmp_lp_multipliers_.push_back({row, row_factors_[row.value()] * value});
1642 }
1643 } else {
1644 for (const ColIndex col : lambda.non_zeros) {
1645 const RowIndex row = glop::ColToRowIndex(col);
1646 const double value = lambda.values[col];
1647 if (std::abs(value) < kZeroTolerance) continue;
1648 tmp_lp_multipliers_.push_back({row, row_factors_[row.value()] * value});
1649 }
1650 }
1651
1652 // Size 1 is already done with MIR.
1653 if (tmp_lp_multipliers_.size() <= 1) continue;
1654
1655 IntegerValue scaling;
1656 for (int i = 0; i < 2; ++i) {
1657 tmp_cg_multipliers_ = tmp_lp_multipliers_;
1658 if (i == 1) {
1659 // Try other sign.
1660 //
1661 // TODO(user): Maybe add an heuristic to know beforehand which sign to
1662 // use?
1663 for (std::pair<RowIndex, double>& p : tmp_cg_multipliers_) {
1664 p.second = -p.second;
1665 }
1666 }
1667
1668 // Remove constraints that shouldn't be helpful.
1669 //
1670 // In practice, because we can complement the slack, it might still be
1671 // useful to have some constraint with a trivial upper bound. Also
1672 // removing this seem to generate a lot more cuts, so we need to be more
1673 // efficient in dealing with them.
1674 if (true) {
1675 IgnoreTrivialConstraintMultipliers(&tmp_cg_multipliers_);
1676 if (tmp_cg_multipliers_.size() <= 1) continue;
1677 }
1678 ScaleMultipliers(tmp_cg_multipliers_,
1679 /*take_objective_into_account=*/false, &scaling,
1680 &tmp_integer_multipliers_);
1681 if (scaling != 0) {
1682 if (AddCutFromConstraints("CG", tmp_integer_multipliers_)) {
1683 ++num_added;
1684 }
1685 }
1686 }
1687
1688 // Stop if we already added more than 10 cuts this round.
1689 if (num_added > 10) break;
1690 }
1691}
1692
1693// Because we know the objective is integer, the constraint objective >= lb can
1694// sometime cut the current lp optimal, and it can make a big difference to add
1695// it. Or at least use it when constructing more advanced cuts. See
1696// 'multisetcover_batch_0_case_115_instance_0_small_subset_elements_3_sumreqs
1697// _1295_candidates_41.fzn'
1698//
1699// TODO(user): It might be better to just integrate this with the MIR code so
1700// that we not only consider MIR1 involving the objective but we also consider
1701// combining it with other constraints.
1702void LinearProgrammingConstraint::AddObjectiveCut() {
1703 if (integer_objective_.size() <= 1) return;
1704
1705 // We only try to add such cut if the LB objective is "far" from the current
1706 // objective lower bound. Note that this is in term of the "internal" integer
1707 // objective.
1708 const double obj_lp_value = simplex_.GetObjectiveValue();
1709 const IntegerValue obj_lower_bound =
1710 integer_trail_->LevelZeroLowerBound(objective_cp_);
1711 if (obj_lp_value + 1.0 >= ToDouble(obj_lower_bound)) return;
1712
1713 // We negate everything to have a <= base constraint.
1714 LinearConstraint objective_ct;
1715 objective_ct.lb = kMinIntegerValue;
1716 objective_ct.ub = integer_objective_offset_ -
1717 integer_trail_->LevelZeroLowerBound(objective_cp_);
1718 IntegerValue obj_coeff_magnitude(0);
1719 objective_ct.resize(integer_objective_.size());
1720 int i = 0;
1721 for (const auto& [col, coeff] : integer_objective_) {
1722 const IntegerVariable var = integer_variables_[col.value()];
1723 objective_ct.vars[i] = var;
1724 objective_ct.coeffs[i] = -coeff;
1725 obj_coeff_magnitude = std::max(obj_coeff_magnitude, IntTypeAbs(coeff));
1726 ++i;
1727 }
1728
1729 if (!base_ct_.FillFromLinearConstraint(objective_ct, expanded_lp_solution_,
1730 integer_trail_)) {
1731 return;
1732 }
1733
1734 // If the magnitude is small enough, just try to add the full objective. Other
1735 // cuts will be derived in subsequent passes. Otherwise, try normal cut
1736 // heuristic that should result in a cut with reasonable coefficients.
1737 if (obj_coeff_magnitude < 1e9 &&
1738 constraint_manager_.AddCut(std::move(objective_ct), "Objective")) {
1739 return;
1740 }
1741
1742 // If there are no integer (all Booleans), no need to try implied bounds
1743 // heurititics. By setting this to nullptr, we are a bit faster.
1744 ImpliedBoundsProcessor* ib_processor = nullptr;
1745 {
1746 bool some_ints = false;
1747 bool some_relevant_positions = false;
1748 for (const CutTerm& term : base_ct_.terms) {
1749 if (term.bound_diff > 1) some_ints = true;
1750 if (term.HasRelevantLpValue()) some_relevant_positions = true;
1751 }
1752
1753 // If all value are integer, we will not be able to cut anything.
1754 if (!some_relevant_positions) return;
1755 if (some_ints) ib_processor = &implied_bounds_processor_;
1756 }
1757
1758 // Note that the indexing will survive the complement of terms below.
1759 const IntegerVariable first_slack(
1760 std::numeric_limits<IntegerVariable::ValueType>::max());
1761 if (ib_processor != nullptr) {
1762 if (!ib_processor->CacheDataForCut(first_slack, &base_ct_)) {
1763 ib_processor = nullptr;
1764 }
1765 }
1766
1767 // Try knapsack.
1768 base_ct_.ComplementForPositiveCoefficients();
1769 cover_cut_helper_.ClearCache();
1770 if (cover_cut_helper_.TrySimpleKnapsack(base_ct_, ib_processor)) {
1771 PostprocessAndAddCut("Objective_K", cover_cut_helper_.Info(), first_slack,
1772 cover_cut_helper_.cut());
1773 }
1774
1775 // Try rounding.
1776 RoundingOptions options;
1777 options.max_scaling = parameters_.max_integer_rounding_scaling();
1778 base_ct_.ComplementForSmallerLpValues();
1779 if (integer_rounding_cut_helper_.ComputeCut(options, base_ct_,
1780 ib_processor)) {
1781 PostprocessAndAddCut("Objective_R", integer_rounding_cut_helper_.Info(),
1782 first_slack, integer_rounding_cut_helper_.cut());
1783 }
1784}
1785
1786void LinearProgrammingConstraint::AddMirCuts() {
1787 // Heuristic to generate MIR_n cuts by combining a small number of rows. This
1788 // works greedily and follow more or less the MIR cut description in the
1789 // literature. We have a current cut, and we add one more row to it while
1790 // eliminating a variable of the current cut whose LP value is far from its
1791 // bound.
1792 //
1793 // A notable difference is that we randomize the variable we eliminate and
1794 // the row we use to do so. We still have weights to indicate our preferred
1795 // choices. This allows to generate different cuts when called again and
1796 // again.
1797 //
1798 // TODO(user): We could combine n rows to make sure we eliminate n variables
1799 // far away from their bounds by solving exactly in integer small linear
1800 // system.
1801 util_intops::StrongVector<ColIndex, IntegerValue> dense_cut(
1802 integer_variables_.size(), IntegerValue(0));
1803 SparseBitset<ColIndex> non_zeros_(ColIndex(integer_variables_.size()));
1804
1805 // We compute all the rows that are tight, these will be used as the base row
1806 // for the MIR_n procedure below.
1807 const int num_cols = integer_variables_.size();
1808 const int num_rows = integer_lp_.size();
1809 std::vector<std::pair<RowIndex, IntegerValue>> base_rows;
1810 util_intops::StrongVector<RowIndex, double> row_weights(num_rows, 0.0);
1811 Fractional* lb_with_slack = simplex_.MutableLowerBounds()->data();
1812 Fractional* ub_with_slack = simplex_.MutableUpperBounds()->data();
1813 for (RowIndex row(0); row < num_rows; ++row) {
1814 // We only consider tight rows.
1815 // We use both the status and activity to have as much options as possible.
1816 //
1817 // TODO(user): shall we consider rows that are not tight?
1818 // TODO(user): Ignore trivial rows? Note that we do that for MIR1 since it
1819 // cannot be good.
1820 const auto status = simplex_.GetConstraintStatus(row);
1821 const double activity = simplex_.GetConstraintActivity(row);
1822 const double ct_lb = -ub_with_slack[num_cols + row.value()];
1823 const double ct_ub = -lb_with_slack[num_cols + row.value()];
1824 if (activity > ct_ub - 1e-4 ||
1827 base_rows.push_back({row, IntegerValue(1)});
1828 }
1829 if (activity < ct_lb + 1e-4 ||
1832 base_rows.push_back({row, IntegerValue(-1)});
1833 }
1834
1835 // For now, we use the dual values for the row "weights".
1836 //
1837 // Note that we use the dual at LP scale so that it make more sense when we
1838 // compare different rows since the LP has been scaled.
1839 //
1840 // TODO(user): In Kati Wolter PhD "Implementation of Cutting Plane
1841 // Separators for Mixed Integer Programs" which describe SCIP's MIR cuts
1842 // implementation (or at least an early version of it), a more complex score
1843 // is used.
1844 //
1845 // Note(user): Because we only consider tight rows under the current lp
1846 // solution (i.e. non-basic rows), most should have a non-zero dual values.
1847 // But there is some degenerate problem where these rows have a really low
1848 // weight (or even zero), and having only weight of exactly zero in
1849 // std::discrete_distribution will result in a crash.
1850 row_weights[row] = std::max(1e-8, std::abs(simplex_.GetDualValue(row)));
1851 }
1852
1853 // The code here can be really slow, so we put a limit on the number of
1854 // entries we process. We randomize the base_rows so that on the next calls
1855 // we do not do exactly the same if we can't process many base row.
1856 int64_t dtime_num_entries = 0;
1857 std::shuffle(base_rows.begin(), base_rows.end(), *random_);
1858
1859 std::vector<double> weights;
1860 util_intops::StrongVector<RowIndex, bool> used_rows;
1861 std::vector<std::pair<RowIndex, IntegerValue>> integer_multipliers;
1862 const auto matrix = simplex_.MatrixWithSlack().view();
1863 for (const std::pair<RowIndex, IntegerValue>& entry : base_rows) {
1864 if (time_limit_->LimitReached()) break;
1865 if (dtime_num_entries > 1e7) break;
1866
1867 const glop::RowIndex base_row = entry.first;
1868 const IntegerValue multiplier = entry.second;
1869
1870 // First try to generate a cut directly from this base row (MIR1).
1871 //
1872 // Note(user): We abort on success like it seems to be done in the
1873 // literature. Note that we don't succeed that often in generating an
1874 // efficient cut, so I am not sure aborting will make a big difference
1875 // speedwise. We might generate similar cuts though, but hopefully the cut
1876 // management can deal with that.
1877 integer_multipliers = {entry};
1878 if ((multiplier > 0 && !integer_lp_[base_row].ub_is_trivial) ||
1879 (multiplier < 0 && !integer_lp_[base_row].lb_is_trivial)) {
1880 if (AddCutFromConstraints("MIR_1", integer_multipliers)) continue;
1881 }
1882
1883 // Cleanup.
1884 for (const ColIndex col : non_zeros_.PositionsSetAtLeastOnce()) {
1885 dense_cut[col] = IntegerValue(0);
1886 }
1887 non_zeros_.SparseClearAll();
1888
1889 // Copy cut.
1890 const LinearConstraintInternal& ct = integer_lp_[entry.first];
1891 for (int i = 0; i < ct.num_terms; ++i) {
1892 const int index = ct.start_in_buffer + i;
1893 const ColIndex col = integer_lp_cols_[index];
1894 const IntegerValue coeff = integer_lp_coeffs_[index];
1895 non_zeros_.Set(col);
1896 dense_cut[col] += coeff * multiplier;
1897 }
1898
1899 used_rows.assign(num_rows, false);
1900 used_rows[entry.first] = true;
1901
1902 // We will aggregate at most kMaxAggregation more rows.
1903 //
1904 // TODO(user): optim + tune.
1905 const int kMaxAggregation = 5;
1906 for (int i = 0; i < kMaxAggregation; ++i) {
1907 // First pick a variable to eliminate. We currently pick a random one with
1908 // a weight that depend on how far it is from its closest bound.
1909 IntegerValue max_magnitude(0);
1910 weights.clear();
1911 std::vector<ColIndex> col_candidates;
1912 for (const ColIndex col : non_zeros_.PositionsSetAtLeastOnce()) {
1913 if (dense_cut[col] == 0) continue;
1914
1915 max_magnitude = std::max(max_magnitude, IntTypeAbs(dense_cut[col]));
1916 const int col_degree = matrix.ColumnNumEntries(col).value();
1917 if (col_degree <= 1) continue;
1918 if (simplex_.GetVariableStatus(col) != glop::VariableStatus::BASIC) {
1919 continue;
1920 }
1921
1922 const IntegerVariable var = integer_variables_[col.value()];
1923 const double lp_value = expanded_lp_solution_[var];
1924 const double lb = ToDouble(integer_trail_->LevelZeroLowerBound(var));
1925 const double ub = ToDouble(integer_trail_->LevelZeroUpperBound(var));
1926 const double bound_distance = std::min(ub - lp_value, lp_value - lb);
1927 if (bound_distance > 1e-2) {
1928 weights.push_back(bound_distance);
1929 col_candidates.push_back(col);
1930 }
1931 }
1932 if (col_candidates.empty()) break;
1933
1934 const ColIndex var_to_eliminate =
1935 col_candidates[WeightedPick(weights, *random_)];
1936
1937 // What rows can we add to eliminate var_to_eliminate?
1938 std::vector<RowIndex> possible_rows;
1939 weights.clear();
1940 for (const auto entry_index : matrix.Column(var_to_eliminate)) {
1941 const RowIndex row = matrix.EntryRow(entry_index);
1942 const glop::Fractional coeff = matrix.EntryCoefficient(entry_index);
1943
1944 // We disallow all the rows that contain a variable that we already
1945 // eliminated (or are about to). This mean that we choose rows that
1946 // form a "triangular" matrix on the position we choose to eliminate.
1947 if (used_rows[row]) continue;
1948 used_rows[row] = true;
1949
1950 // Note that we consider all rows here, not only tight one. This makes a
1951 // big difference on problem like blp-ic98.pb.gz. We can also use the
1952 // integrality of the slack when adding a non-tight row to derive good
1953 // cuts. Also, non-tight row will have a low weight, so they should
1954 // still be chosen after the tight-one in most situation.
1955 bool add_row = false;
1956 if (!integer_lp_[row].ub_is_trivial) {
1957 if (coeff > 0.0) {
1958 if (dense_cut[var_to_eliminate] < 0) add_row = true;
1959 } else {
1960 if (dense_cut[var_to_eliminate] > 0) add_row = true;
1961 }
1962 }
1963 if (!integer_lp_[row].lb_is_trivial) {
1964 if (coeff > 0.0) {
1965 if (dense_cut[var_to_eliminate] > 0) add_row = true;
1966 } else {
1967 if (dense_cut[var_to_eliminate] < 0) add_row = true;
1968 }
1969 }
1970 if (add_row) {
1971 possible_rows.push_back(row);
1972 weights.push_back(row_weights[row]);
1973 }
1974 }
1975 if (possible_rows.empty()) break;
1976
1977 const RowIndex row_to_combine =
1978 possible_rows[WeightedPick(weights, *random_)];
1979
1980 // Find the coefficient of the variable to eliminate.
1981 IntegerValue to_combine_coeff = 0;
1982 const LinearConstraintInternal& ct_to_combine =
1983 integer_lp_[row_to_combine];
1984 for (int i = 0; i < ct_to_combine.num_terms; ++i) {
1985 const int index = ct_to_combine.start_in_buffer + i;
1986 if (integer_lp_cols_[index] == var_to_eliminate) {
1987 to_combine_coeff = integer_lp_coeffs_[index];
1988 break;
1989 }
1990 }
1991 CHECK_NE(to_combine_coeff, 0);
1992
1993 IntegerValue mult1 = -to_combine_coeff;
1994 IntegerValue mult2 = dense_cut[var_to_eliminate];
1995 CHECK_NE(mult2, 0);
1996 if (mult1 < 0) {
1997 mult1 = -mult1;
1998 mult2 = -mult2;
1999 }
2000
2001 const IntegerValue gcd = IntegerValue(
2002 MathUtil::GCD64(std::abs(mult1.value()), std::abs(mult2.value())));
2003 CHECK_NE(gcd, 0);
2004 mult1 /= gcd;
2005 mult2 /= gcd;
2006
2007 // Overflow detection.
2008 //
2009 // TODO(user): do that in the possible_rows selection? only problem is
2010 // that we do not have the integer coefficient there...
2011 for (std::pair<RowIndex, IntegerValue>& entry : integer_multipliers) {
2012 max_magnitude = std::max(max_magnitude, IntTypeAbs(entry.second));
2013 }
2014 if (CapAdd(CapProd(max_magnitude.value(), std::abs(mult1.value())),
2015 CapProd(infinity_norms_[row_to_combine].value(),
2016 std::abs(mult2.value()))) ==
2017 std::numeric_limits<int64_t>::max()) {
2018 break;
2019 }
2020
2021 for (std::pair<RowIndex, IntegerValue>& entry : integer_multipliers) {
2022 dtime_num_entries += integer_lp_[entry.first].num_terms;
2023 entry.second *= mult1;
2024 }
2025 dtime_num_entries += integer_lp_[row_to_combine].num_terms;
2026 integer_multipliers.push_back({row_to_combine, mult2});
2027
2028 // TODO(user): Not supper efficient to recombine the rows.
2029 if (AddCutFromConstraints(absl::StrCat("MIR_", i + 2),
2030 integer_multipliers)) {
2031 break;
2032 }
2033
2034 // Minor optim: the computation below is only needed if we do one more
2035 // iteration.
2036 if (i + 1 == kMaxAggregation) break;
2037
2038 for (ColIndex col : non_zeros_.PositionsSetAtLeastOnce()) {
2039 dense_cut[col] *= mult1;
2040 }
2041 for (int i = 0; i < ct_to_combine.num_terms; ++i) {
2042 const int index = ct_to_combine.start_in_buffer + i;
2043 const ColIndex col = integer_lp_cols_[index];
2044 const IntegerValue coeff = integer_lp_coeffs_[index];
2045 non_zeros_.Set(col);
2046 dense_cut[col] += coeff * mult2;
2047 }
2048 }
2049 }
2050}
2051
2052void LinearProgrammingConstraint::AddZeroHalfCuts() {
2053 if (time_limit_->LimitReached()) return;
2054
2055 tmp_lp_values_.clear();
2056 tmp_var_lbs_.clear();
2057 tmp_var_ubs_.clear();
2058 for (const IntegerVariable var : integer_variables_) {
2059 tmp_lp_values_.push_back(expanded_lp_solution_[var]);
2060 tmp_var_lbs_.push_back(integer_trail_->LevelZeroLowerBound(var));
2061 tmp_var_ubs_.push_back(integer_trail_->LevelZeroUpperBound(var));
2062 }
2063
2064 // TODO(user): See if it make sense to try to use implied bounds there.
2065 zero_half_cut_helper_.ProcessVariables(tmp_lp_values_, tmp_var_lbs_,
2066 tmp_var_ubs_);
2067 for (glop::RowIndex row(0); row < integer_lp_.size(); ++row) {
2068 // Even though we could use non-tight row, for now we prefer to use tight
2069 // ones.
2070 const auto status = simplex_.GetConstraintStatus(row);
2071 if (status == glop::ConstraintStatus::BASIC) continue;
2072 if (status == glop::ConstraintStatus::FREE) continue;
2073
2074 zero_half_cut_helper_.AddOneConstraint(
2075 row, IntegerLpRowCols(row), IntegerLpRowCoeffs(row),
2076 integer_lp_[row].lb, integer_lp_[row].ub);
2077 }
2078 for (const std::vector<std::pair<RowIndex, IntegerValue>>& multipliers :
2079 zero_half_cut_helper_.InterestingCandidates(random_)) {
2080 if (time_limit_->LimitReached()) break;
2081
2082 // TODO(user): Make sure that if the resulting linear coefficients are not
2083 // too high, we do try a "divisor" of two and thus try a true zero-half cut
2084 // instead of just using our best MIR heuristic (which might still be better
2085 // though).
2086 AddCutFromConstraints("ZERO_HALF", multipliers);
2087 }
2088}
2089
2090void LinearProgrammingConstraint::UpdateSimplexIterationLimit(
2091 const int64_t min_iter, const int64_t max_iter) {
2092 if (parameters_.linearization_level() < 2) return;
2093 const int64_t num_degenerate_columns = CalculateDegeneracy();
2094 const int64_t num_cols = simplex_.GetProblemNumCols().value();
2095 if (num_cols <= 0) {
2096 return;
2097 }
2098 CHECK_GT(num_cols, 0);
2099 const int64_t decrease_factor = (10 * num_degenerate_columns) / num_cols;
2100 if (simplex_.GetProblemStatus() == glop::ProblemStatus::DUAL_FEASIBLE) {
2101 // We reached here probably because we predicted wrong. We use this as a
2102 // signal to increase the iterations or punish less for degeneracy compare
2103 // to the other part.
2104 if (is_degenerate_) {
2105 next_simplex_iter_ /= std::max(int64_t{1}, decrease_factor);
2106 } else {
2107 next_simplex_iter_ *= 2;
2108 }
2109 } else if (simplex_.GetProblemStatus() == glop::ProblemStatus::OPTIMAL) {
2110 if (is_degenerate_) {
2111 next_simplex_iter_ /= std::max(int64_t{1}, 2 * decrease_factor);
2112 } else {
2113 // This is the most common case. We use the size of the problem to
2114 // determine the limit and ignore the previous limit.
2115 next_simplex_iter_ = num_cols / 40;
2116 }
2117 }
2118 next_simplex_iter_ =
2119 std::max(min_iter, std::min(max_iter, next_simplex_iter_));
2120}
2121
2123 if (!enabled_) return true;
2124 if (time_limit_->LimitReached()) return true;
2125 UpdateBoundsOfLpVariables();
2126
2127 // TODO(user): It seems the time we loose by not stopping early might be worth
2128 // it because we end up with a better explanation at optimality.
2129 if (/* DISABLES CODE */ (false) && objective_is_defined_) {
2130 // We put a limit on the dual objective since there is no point increasing
2131 // it past our current objective upper-bound (we will already fail as soon
2132 // as we pass it). Note that this limit is properly transformed using the
2133 // objective scaling factor and offset stored in lp_data_.
2134 //
2135 // Note that we use a bigger epsilon here to be sure that if we abort
2136 // because of this, we will report a conflict.
2137 simplex_params_.set_objective_upper_limit(
2138 static_cast<double>(integer_trail_->UpperBound(objective_cp_).value() +
2139 100.0 * kCpEpsilon));
2140 }
2141
2142 // Put an iteration limit on the work we do in the simplex for this call. Note
2143 // that because we are "incremental", even if we don't solve it this time we
2144 // will make progress towards a solve in the lower node of the tree search.
2145 if (trail_->CurrentDecisionLevel() == 0) {
2146 simplex_params_.set_max_number_of_iterations(
2147 parameters_.root_lp_iterations());
2148 } else {
2149 simplex_params_.set_max_number_of_iterations(next_simplex_iter_);
2150 }
2151
2152 simplex_.SetParameters(simplex_params_);
2153 if (!SolveLp()) return true;
2154 if (!AnalyzeLp()) return false;
2155
2156 // Add new constraints to the LP and resolve?
2157 const int max_cuts_rounds = trail_->CurrentDecisionLevel() == 0
2158 ? parameters_.max_cut_rounds_at_level_zero()
2159 : 1;
2160 int cuts_round = 0;
2161 while (simplex_.GetProblemStatus() == glop::ProblemStatus::OPTIMAL &&
2162 cuts_round < max_cuts_rounds) {
2163 // We wait for the first batch of problem constraints to be added before we
2164 // begin to generate cuts. Note that we rely on num_solves_ since on some
2165 // problems there is no other constraints than the cuts.
2166 cuts_round++;
2167 if (parameters_.cut_level() > 0 &&
2168 (num_solves_ > 1 || !parameters_.add_lp_constraints_lazily())) {
2169 // This must be called first.
2170 implied_bounds_processor_.RecomputeCacheAndSeparateSomeImpliedBoundCuts(
2171 expanded_lp_solution_);
2172 if (parameters_.add_rlt_cuts()) {
2173 rlt_cut_helper_.Initialize(extended_integer_variables_);
2174 }
2175
2176 // The "generic" cuts are currently part of this class as they are using
2177 // data from the current LP.
2178 //
2179 // TODO(user): Refactor so that they are just normal cut generators?
2180 const int level = trail_->CurrentDecisionLevel();
2181 if (trail_->CurrentDecisionLevel() == 0) {
2182 problem_proven_infeasible_by_cuts_ = false;
2183 if (parameters_.add_objective_cut()) AddObjectiveCut();
2184 if (parameters_.add_mir_cuts()) AddMirCuts();
2185 if (parameters_.add_cg_cuts()) AddCGCuts();
2186 if (parameters_.add_zero_half_cuts()) AddZeroHalfCuts();
2187 if (problem_proven_infeasible_by_cuts_) {
2188 return integer_trail_->ReportConflict({});
2189 }
2190 top_n_cuts_.TransferToManager(&constraint_manager_);
2191 }
2192
2193 // Try to add cuts.
2194 if (level == 0 || !parameters_.only_add_cuts_at_level_zero()) {
2195 for (CutGenerator& generator : cut_generators_) {
2196 if (level > 0 && generator.only_run_at_level_zero) continue;
2197 if (!generator.generate_cuts(&constraint_manager_)) {
2198 return false;
2199 }
2200 }
2201 }
2202
2203 implied_bounds_processor_.IbCutPool().TransferToManager(
2204 &constraint_manager_);
2205 }
2206
2207 int num_added = 0;
2208 if (constraint_manager_.ChangeLp(&state_, &num_added)) {
2209 ++num_lp_changes_;
2210 simplex_.LoadStateForNextSolve(state_);
2211 if (!CreateLpFromConstraintManager()) {
2212 return integer_trail_->ReportConflict({});
2213 }
2214
2215 // If we didn't add any new constraint, we delay the next Solve() since
2216 // likely the optimal didn't change.
2217 if (num_added == 0) {
2218 break;
2219 }
2220
2221 const double old_obj = simplex_.GetObjectiveValue();
2222 if (!SolveLp()) return true;
2223 if (!AnalyzeLp()) return false;
2224 if (simplex_.GetProblemStatus() == glop::ProblemStatus::OPTIMAL) {
2225 VLOG(3) << "Relaxation improvement " << old_obj << " -> "
2226 << simplex_.GetObjectiveValue()
2227 << " diff: " << simplex_.GetObjectiveValue() - old_obj
2228 << " level: " << trail_->CurrentDecisionLevel();
2229 }
2230 } else {
2231 if (trail_->CurrentDecisionLevel() == 0) {
2232 lp_at_level_zero_is_final_ = true;
2233 }
2234 break;
2235 }
2236 }
2237
2238 return true;
2239}
2240
2241absl::int128 LinearProgrammingConstraint::GetImpliedLowerBound(
2242 const LinearConstraint& terms) const {
2243 absl::int128 lower_bound(0);
2244 const int size = terms.num_terms;
2245 for (int i = 0; i < size; ++i) {
2246 const IntegerVariable var = terms.vars[i];
2247 const IntegerValue coeff = terms.coeffs[i];
2248 const IntegerValue bound = coeff > 0 ? integer_trail_->LowerBound(var)
2249 : integer_trail_->UpperBound(var);
2250 lower_bound += absl::int128(bound.value()) * absl::int128(coeff.value());
2251 }
2252 return lower_bound;
2253}
2254
2255bool LinearProgrammingConstraint::ScalingCanOverflow(
2256 int power, bool take_objective_into_account,
2257 absl::Span<const std::pair<glop::RowIndex, double>> multipliers,
2258 int64_t overflow_cap) const {
2259 int64_t bound = 0;
2260 const int64_t factor = int64_t{1} << power;
2261 const double factor_as_double = static_cast<double>(factor);
2262 if (take_objective_into_account) {
2263 bound = CapAdd(bound, CapProd(factor, objective_infinity_norm_.value()));
2264 if (bound >= overflow_cap) return true;
2265 }
2266 for (const auto [row, double_coeff] : multipliers) {
2267 const double magnitude =
2268 std::abs(std::round(double_coeff * factor_as_double));
2269 if (std::isnan(magnitude)) return true;
2270 if (magnitude >= static_cast<double>(std::numeric_limits<int64_t>::max())) {
2271 return true;
2272 }
2273 bound = CapAdd(bound, CapProd(static_cast<int64_t>(magnitude),
2274 infinity_norms_[row].value()));
2275 if (bound >= overflow_cap) return true;
2276 }
2277 return bound >= overflow_cap;
2278}
2279
2280void LinearProgrammingConstraint::IgnoreTrivialConstraintMultipliers(
2281 std::vector<std::pair<RowIndex, double>>* lp_multipliers) {
2282 int new_size = 0;
2283 for (const std::pair<RowIndex, double>& p : *lp_multipliers) {
2284 const RowIndex row = p.first;
2285 const Fractional lp_multi = p.second;
2286 if (lp_multi > 0.0 && integer_lp_[row].ub_is_trivial) continue;
2287 if (lp_multi < 0.0 && integer_lp_[row].lb_is_trivial) continue;
2288 (*lp_multipliers)[new_size++] = p;
2289 }
2290 lp_multipliers->resize(new_size);
2291}
2292
2293void LinearProgrammingConstraint::ScaleMultipliers(
2294 absl::Span<const std::pair<RowIndex, double>> lp_multipliers,
2295 bool take_objective_into_account, IntegerValue* scaling,
2296 std::vector<std::pair<RowIndex, IntegerValue>>* output) const {
2297 *scaling = 0;
2298
2299 output->clear();
2300 if (lp_multipliers.empty()) {
2301 // Empty linear combinaison.
2302 return;
2303 }
2304
2305 // TODO(user): we currently do not support scaling down, so we just abort
2306 // if with a scaling of 1, we reach the overflow_cap.
2307 const int64_t overflow_cap = std::numeric_limits<int64_t>::max();
2308 if (ScalingCanOverflow(/*power=*/0, take_objective_into_account,
2309 lp_multipliers, overflow_cap)) {
2310 ++num_scaling_issues_;
2311 return;
2312 }
2313
2314 // Note that we don't try to scale by more than 63 since in practice the
2315 // constraint multipliers should not be all super small.
2316 //
2317 // TODO(user): We could be faster here, but trying to compute the exact power
2318 // in one go with floating points seems tricky. So we just do around 6 passes
2319 // plus the one above for zero.
2320 const int power = BinarySearch<int>(0, 63, [&](int candidate) {
2321 // Because BinarySearch() wants f(upper_bound) to fail, we bypass the test
2322 // here as when the set of floating points are really small, we could pass
2323 // with a large power.
2324 if (candidate >= 63) return false;
2325
2326 return !ScalingCanOverflow(candidate, take_objective_into_account,
2327 lp_multipliers, overflow_cap);
2328 });
2329 *scaling = int64_t{1} << power;
2330
2331 // Scale the multipliers by *scaling.
2332 // Note that we use the exact same formula as in ScalingCanOverflow().
2333 int64_t gcd = scaling->value();
2334 const double scaling_as_double = static_cast<double>(scaling->value());
2335 for (const auto [row, double_coeff] : lp_multipliers) {
2336 const IntegerValue coeff(std::round(double_coeff * scaling_as_double));
2337 if (coeff != 0) {
2338 gcd = std::gcd(gcd, std::abs(coeff.value()));
2339 output->push_back({row, coeff});
2340 }
2341 }
2342 if (gcd > 1) {
2343 *scaling /= gcd;
2344 for (auto& entry : *output) {
2345 entry.second /= gcd;
2346 }
2347 }
2348}
2349
2350template <bool check_overflow>
2351bool LinearProgrammingConstraint::ComputeNewLinearConstraint(
2352 absl::Span<const std::pair<RowIndex, IntegerValue>> integer_multipliers,
2353 ScatteredIntegerVector* scattered_vector, IntegerValue* upper_bound) const {
2354 // Initialize the new constraint.
2355 *upper_bound = 0;
2356 scattered_vector->ClearAndResize(extended_integer_variables_.size());
2357
2358 // Compute the new constraint by taking the linear combination given by
2359 // integer_multipliers of the integer constraints in integer_lp_.
2360 for (const std::pair<RowIndex, IntegerValue>& term : integer_multipliers) {
2361 const RowIndex row = term.first;
2362 const IntegerValue multiplier = term.second;
2363 DCHECK_LT(row, integer_lp_.size());
2364
2365 // Update the constraint.
2366 if (!scattered_vector->AddLinearExpressionMultiple<check_overflow>(
2367 multiplier, IntegerLpRowCols(row), IntegerLpRowCoeffs(row),
2368 infinity_norms_[row])) {
2369 return false;
2370 }
2371
2372 // Update the upper bound.
2373 const IntegerValue bound =
2374 multiplier > 0 ? integer_lp_[row].ub : integer_lp_[row].lb;
2375 if (!AddProductTo(multiplier, bound, upper_bound)) return false;
2376 }
2377
2378 return true;
2379}
2380
2381// TODO(user): no need to update the multipliers.
2382void LinearProgrammingConstraint::AdjustNewLinearConstraint(
2383 std::vector<std::pair<glop::RowIndex, IntegerValue>>* integer_multipliers,
2384 ScatteredIntegerVector* scattered_vector, IntegerValue* upper_bound) const {
2385 const IntegerValue kMaxWantedCoeff(1e18);
2386 bool adjusted = false;
2387 for (std::pair<RowIndex, IntegerValue>& term : *integer_multipliers) {
2388 const RowIndex row = term.first;
2389 const IntegerValue multiplier = term.second;
2390 if (multiplier == 0) continue;
2391
2392 // We will only allow change of the form "multiplier += to_add" with to_add
2393 // in [-negative_limit, positive_limit].
2394 //
2395 // We do not want to_add * row to overflow.
2396 IntegerValue negative_limit =
2397 FloorRatio(kMaxWantedCoeff, infinity_norms_[row]);
2398 IntegerValue positive_limit = negative_limit;
2399
2400 // Make sure we never change the sign of the multiplier, except if the
2401 // row is an equality in which case we don't care.
2402 if (integer_lp_[row].ub != integer_lp_[row].lb) {
2403 if (multiplier > 0) {
2404 negative_limit = std::min(negative_limit, multiplier);
2405 } else {
2406 positive_limit = std::min(positive_limit, -multiplier);
2407 }
2408 }
2409
2410 // Make sure upper_bound + to_add * row_bound never overflow.
2411 const IntegerValue row_bound =
2412 multiplier > 0 ? integer_lp_[row].ub : integer_lp_[row].lb;
2413 if (row_bound != 0) {
2414 const IntegerValue limit1 = FloorRatio(
2415 std::max(IntegerValue(0), kMaxWantedCoeff - IntTypeAbs(*upper_bound)),
2416 IntTypeAbs(row_bound));
2417 const IntegerValue limit2 =
2418 FloorRatio(kMaxWantedCoeff, IntTypeAbs(row_bound));
2419 if ((*upper_bound > 0) == (row_bound > 0)) { // Same sign.
2420 positive_limit = std::min(positive_limit, limit1);
2421 negative_limit = std::min(negative_limit, limit2);
2422 } else {
2423 negative_limit = std::min(negative_limit, limit1);
2424 positive_limit = std::min(positive_limit, limit2);
2425 }
2426 }
2427
2428 // If we add the row to the scattered_vector, diff will indicate by how much
2429 // |upper_bound - ImpliedLB(scattered_vector)| will change. That correspond
2430 // to increasing the multiplier by 1.
2431 //
2432 // At this stage, we are not sure computing sum coeff * bound will not
2433 // overflow, so we use floating point numbers. It is fine to do so since
2434 // this is not directly involved in the actual exact constraint generation:
2435 // these variables are just used in an heuristic.
2436 double common_diff = ToDouble(row_bound);
2437 double positive_diff = 0.0;
2438 double negative_diff = 0.0;
2439
2440 // TODO(user): we could relax a bit some of the condition and allow a sign
2441 // change. It is just trickier to compute the diff when we allow such
2442 // changes.
2443 const LinearConstraintInternal& ct = integer_lp_[row];
2444 for (int i = 0; i < ct.num_terms; ++i) {
2445 const int index = ct.start_in_buffer + i;
2446 const ColIndex col = integer_lp_cols_[index];
2447 const IntegerValue coeff = integer_lp_coeffs_[index];
2448 DCHECK_NE(coeff, 0);
2449
2450 // Moving a variable away from zero seems to improve the bound even
2451 // if it reduces the number of non-zero. Note that this is because of
2452 // this that positive_diff and negative_diff are not the same.
2453 const IntegerValue current = (*scattered_vector)[col];
2454 if (current == 0) {
2455 const IntegerVariable var = integer_variables_[col.value()];
2456 const IntegerValue lb = integer_trail_->LowerBound(var);
2457 const IntegerValue ub = integer_trail_->UpperBound(var);
2458 if (coeff > 0) {
2459 positive_diff -= ToDouble(coeff) * ToDouble(lb);
2460 negative_diff -= ToDouble(coeff) * ToDouble(ub);
2461 } else {
2462 positive_diff -= ToDouble(coeff) * ToDouble(ub);
2463 negative_diff -= ToDouble(coeff) * ToDouble(lb);
2464 }
2465 continue;
2466 }
2467
2468 // We don't want to change the sign of current (except if the variable is
2469 // fixed, but in practice we should have removed any fixed variable, so we
2470 // don't care here) or to have an overflow.
2471 //
2472 // Corner case:
2473 // - IntTypeAbs(current) can be larger than kMaxWantedCoeff!
2474 // - The code assumes that 2 * kMaxWantedCoeff do not overflow.
2475 //
2476 // Note that because we now that limit * abs_coeff will never overflow
2477 // because we used infinity_norms_[row] above.
2478 const IntegerValue abs_coeff = IntTypeAbs(coeff);
2479 const IntegerValue current_magnitude = IntTypeAbs(current);
2480 const IntegerValue overflow_threshold =
2481 std::max(IntegerValue(0), kMaxWantedCoeff - current_magnitude);
2482 if ((current > 0) == (coeff > 0)) { // Same sign.
2483 if (negative_limit * abs_coeff > current_magnitude) {
2484 negative_limit = current_magnitude / abs_coeff;
2485 if (positive_limit == 0 && negative_limit == 0) break;
2486 }
2487 if (positive_limit * abs_coeff > overflow_threshold) {
2488 positive_limit = overflow_threshold / abs_coeff;
2489 if (positive_limit == 0 && negative_limit == 0) break;
2490 }
2491 } else {
2492 if (negative_limit * abs_coeff > overflow_threshold) {
2493 negative_limit = overflow_threshold / abs_coeff;
2494 if (positive_limit == 0 && negative_limit == 0) break;
2495 }
2496 if (positive_limit * abs_coeff > current_magnitude) {
2497 positive_limit = current_magnitude / abs_coeff;
2498 if (positive_limit == 0 && negative_limit == 0) break;
2499 }
2500 }
2501
2502 // This is how diff change.
2503 const IntegerVariable var = integer_variables_[col.value()];
2504 const IntegerValue implied = current > 0
2505 ? integer_trail_->LowerBound(var)
2506 : integer_trail_->UpperBound(var);
2507 if (implied != 0) {
2508 common_diff -= ToDouble(coeff) * ToDouble(implied);
2509 }
2510 }
2511
2512 positive_diff += common_diff;
2513 negative_diff += common_diff;
2514
2515 // Only add a multiple of this row if it tighten the final constraint.
2516 // The positive_diff/negative_diff are supposed to be integer modulo the
2517 // double precision, so we only add a multiple if they seems far away from
2518 // zero.
2519 IntegerValue to_add(0);
2520 if (positive_diff <= -1.0 && positive_limit > 0) {
2521 to_add = positive_limit;
2522 }
2523 if (negative_diff >= 1.0 && negative_limit > 0) {
2524 // Pick this if it is better than the positive sign.
2525 if (to_add == 0 ||
2526 std::abs(ToDouble(negative_limit) * negative_diff) >
2527 std::abs(ToDouble(positive_limit) * positive_diff)) {
2528 to_add = -negative_limit;
2529 }
2530 }
2531 if (to_add != 0) {
2532 term.second += to_add;
2533 *upper_bound += to_add * row_bound;
2534 adjusted = true;
2535 CHECK(scattered_vector
2536 ->AddLinearExpressionMultiple</*check_overflow=*/false>(
2537 to_add, IntegerLpRowCols(row), IntegerLpRowCoeffs(row),
2538 infinity_norms_[row]));
2539 }
2540 }
2541 if (adjusted) ++num_adjusts_;
2542}
2543
2544bool LinearProgrammingConstraint::PropagateLpConstraint(LinearConstraint ct) {
2545 DCHECK(constraint_manager_.DebugCheckConstraint(ct));
2546
2547 // We need to cache this before we std::move() the constraint!
2548 const int num_terms = ct.num_terms;
2549 if (num_terms == 0) {
2550 if (ct.ub >= 0) return true;
2551 return integer_trail_->ReportConflict({}); // Unsat.
2552 }
2553
2554 std::unique_ptr<IntegerSumLE128> cp_constraint(
2555 new IntegerSumLE128(std::move(ct), model_));
2556
2557 // We always propagate level zero bounds first.
2558 // If we are at level zero, there is nothing else to do.
2559 if (!cp_constraint->PropagateAtLevelZero()) return false;
2560 if (trail_->CurrentDecisionLevel() == 0) return true;
2561
2562 // To optimize the memory usage, if the constraint didn't propagate anything,
2563 // we don't need to keep it around.
2564 const int64_t stamp = integer_trail_->num_enqueues();
2565 const bool no_conflict = cp_constraint->Propagate();
2566 if (no_conflict && integer_trail_->num_enqueues() == stamp) return true;
2567
2568 const int64_t current_size =
2569 cumulative_optimal_constraint_sizes_.empty()
2570 ? 0
2571 : cumulative_optimal_constraint_sizes_.back();
2572 optimal_constraints_.push_back(std::move(cp_constraint));
2573 cumulative_optimal_constraint_sizes_.push_back(current_size + num_terms);
2574 rev_optimal_constraints_size_ = optimal_constraints_.size();
2575 return no_conflict;
2576}
2577
2578// The "exact" computation go as follows:
2579//
2580// Given any INTEGER linear combination of the LP constraints, we can create a
2581// new integer constraint that is valid (its computation must not overflow
2582// though). Lets call this "linear_combination <= ub". We can then always add to
2583// it the inequality "objective_terms <= objective_var", so we get:
2584// ImpliedLB(objective_terms + linear_combination) - ub <= objective_var.
2585// where ImpliedLB() is computed from the variable current bounds.
2586//
2587// Now, if we use for the linear combination and approximation of the optimal
2588// negated dual LP values (by scaling them and rounding them to integer), we
2589// will get an EXACT objective lower bound that is more or less the same as the
2590// inexact bound given by the LP relaxation. This allows to derive exact reasons
2591// for any propagation done by this constraint.
2592bool LinearProgrammingConstraint::PropagateExactLpReason() {
2593 // Clear old reason and deductions.
2594 integer_reason_.clear();
2595 deductions_.clear();
2596 deductions_reason_.clear();
2597
2598 // The row multipliers will be the negation of the LP duals.
2599 //
2600 // TODO(user): Provide and use a sparse API in Glop to get the duals.
2601 const RowIndex num_rows = simplex_.GetProblemNumRows();
2602 tmp_lp_multipliers_.clear();
2603 for (RowIndex row(0); row < num_rows; ++row) {
2604 const double value = -simplex_.GetDualValue(row);
2605 if (std::abs(value) < kZeroTolerance) continue;
2606 tmp_lp_multipliers_.push_back({row, scaler_.UnscaleDualValue(row, value)});
2607 }
2608
2609 // In this case, the LP lower bound match the basic objective "constraint"
2610 // propagation. That is there is an LP solution with all objective variable at
2611 // their current best bound. There is no need to do more work here.
2612 if (tmp_lp_multipliers_.empty()) return true;
2613
2614 // For the corner case of an objective of size 1, we do not want or need
2615 // to take it into account.
2616 bool take_objective_into_account = true;
2617 if (objective_cp_is_part_of_lp_) {
2618 // The objective is part of the lp.
2619 // This should only happen for objective with a single term.
2620 CHECK_EQ(integer_objective_.size(), 1);
2621 CHECK_EQ(integer_objective_[0].first, mirror_lp_variable_[objective_cp_]);
2622 CHECK_EQ(integer_objective_[0].second, IntegerValue(1));
2623
2624 take_objective_into_account = false;
2625 }
2626
2627 IntegerValue scaling = 0;
2628 IgnoreTrivialConstraintMultipliers(&tmp_lp_multipliers_);
2629 ScaleMultipliers(tmp_lp_multipliers_, take_objective_into_account, &scaling,
2630 &tmp_integer_multipliers_);
2631 if (scaling == 0) {
2632 VLOG(1) << simplex_.GetProblemStatus();
2633 VLOG(1) << "Issue while computing the exact LP reason. Aborting.";
2634 return true;
2635 }
2636
2637 IntegerValue rc_ub;
2638 CHECK(ComputeNewLinearConstraint</*check_overflow=*/false>(
2639 tmp_integer_multipliers_, &tmp_scattered_vector_, &rc_ub));
2640
2641 std::optional<std::pair<IntegerVariable, IntegerValue>> extra_term =
2642 std::nullopt;
2643 if (take_objective_into_account) {
2644 // The "objective constraint" behave like if the unscaled cp multiplier was
2645 // 1.0, so we will multiply it by this number and add it to reduced_costs.
2646 const IntegerValue obj_scale = scaling;
2647
2648 // TODO(user): Maybe avoid this conversion.
2649 tmp_cols_.clear();
2650 tmp_coeffs_.clear();
2651 for (const auto [col, coeff] : integer_objective_) {
2652 tmp_cols_.push_back(col);
2653 tmp_coeffs_.push_back(coeff);
2654 }
2655 CHECK(tmp_scattered_vector_
2656 .AddLinearExpressionMultiple</*check_overflow=*/false>(
2657 obj_scale, tmp_cols_, tmp_coeffs_, objective_infinity_norm_));
2658 CHECK(AddProductTo(-obj_scale, integer_objective_offset_, &rc_ub));
2659
2660 extra_term = {objective_cp_, -obj_scale};
2661 }
2662
2663 // TODO(user): It seems when the LP as a single variable and the equation is
2664 // obj >= lower_bound, this removes it ? For now we disable this if the
2665 // objective variable is part of the LP (i.e. single objective).
2666 if (take_objective_into_account) {
2667 AdjustNewLinearConstraint(&tmp_integer_multipliers_, &tmp_scattered_vector_,
2668 &rc_ub);
2669 }
2670
2671 // Create the IntegerSumLE that will allow to propagate the objective and more
2672 // generally do the reduced cost fixing.
2673 LinearConstraint explanation =
2674 tmp_scattered_vector_.ConvertToLinearConstraint(
2675 extended_integer_variables_, rc_ub, extra_term);
2676
2677 // Corner case where prevent overflow removed all terms.
2678 if (explanation.num_terms == 0) {
2679 trail_->MutableConflict()->clear();
2680 return explanation.ub >= 0;
2681 }
2682
2683 return PropagateLpConstraint(std::move(explanation));
2684}
2685
2686bool LinearProgrammingConstraint::PropagateExactDualRay() {
2687 IntegerValue scaling;
2688 const glop::DenseColumn ray = simplex_.GetDualRay();
2689 tmp_lp_multipliers_.clear();
2690 for (RowIndex row(0); row < ray.size(); ++row) {
2691 const double value = ray[row];
2692 if (std::abs(value) < kZeroTolerance) continue;
2693
2694 // This is the same as UnscaleLeftSolveValue(). Note that we don't need to
2695 // scale by the objective factor here like we do in UnscaleDualValue().
2696 tmp_lp_multipliers_.push_back({row, row_factors_[row.value()] * value});
2697 }
2698 IgnoreTrivialConstraintMultipliers(&tmp_lp_multipliers_);
2699 ScaleMultipliers(tmp_lp_multipliers_, /*take_objective_into_account=*/false,
2700 &scaling, &tmp_integer_multipliers_);
2701 if (scaling == 0) {
2702 VLOG(1) << "Isse while computing the exact dual ray reason. Aborting.";
2703 return true;
2704 }
2705
2706 IntegerValue new_constraint_ub;
2707 CHECK(ComputeNewLinearConstraint</*check_overflow=*/false>(
2708 tmp_integer_multipliers_, &tmp_scattered_vector_, &new_constraint_ub))
2709 << scaling;
2710
2711 AdjustNewLinearConstraint(&tmp_integer_multipliers_, &tmp_scattered_vector_,
2712 &new_constraint_ub);
2713
2714 LinearConstraint explanation =
2715 tmp_scattered_vector_.ConvertToLinearConstraint(
2716 extended_integer_variables_, new_constraint_ub);
2717
2718 std::string message;
2719 if (VLOG_IS_ON(1)) {
2720 // Unfortunately, we need to set this up before we std::move() it.
2721 message = absl::StrCat("LP exact dual ray not infeasible,",
2722 " implied_lb: ", GetImpliedLowerBound(explanation),
2723 " ub: ", explanation.ub.value());
2724 }
2725
2726 // This should result in a conflict if the precision is good enough.
2727 if (!PropagateLpConstraint(std::move(explanation))) return false;
2728
2729 VLOG(1) << message;
2730 return true;
2731}
2732
2733int64_t LinearProgrammingConstraint::CalculateDegeneracy() {
2734 int num_non_basic_with_zero_rc = 0;
2735 const auto reduced_costs = simplex_.GetReducedCosts().const_view();
2736 for (const glop::ColIndex i : simplex_.GetNotBasicBitRow()) {
2737 if (reduced_costs[i] == 0.0) {
2738 num_non_basic_with_zero_rc++;
2739 }
2740 }
2741 const int64_t num_cols = simplex_.GetProblemNumCols().value();
2742 is_degenerate_ = num_non_basic_with_zero_rc >= 0.3 * num_cols;
2743 return num_non_basic_with_zero_rc;
2744}
2745
2746void LinearProgrammingConstraint::ReducedCostStrengtheningDeductions(
2747 double cp_objective_delta) {
2748 deductions_.clear();
2749
2750 const double lp_objective_delta =
2751 cp_objective_delta / scaler_.ObjectiveScalingFactor();
2752 const int num_vars = integer_variables_.size();
2753 for (int i = 0; i < num_vars; i++) {
2754 const IntegerVariable cp_var = integer_variables_[i];
2755 const glop::ColIndex lp_var = glop::ColIndex(i);
2756 const double rc = simplex_.GetReducedCost(lp_var);
2757 const double value = simplex_.GetVariableValue(lp_var);
2758
2759 if (rc == 0.0) continue;
2760 const double lp_other_bound = value + lp_objective_delta / rc;
2761 const double cp_other_bound =
2762 scaler_.UnscaleVariableValue(lp_var, lp_other_bound);
2763
2764 if (rc > kLpEpsilon) {
2765 const double ub = ToDouble(integer_trail_->UpperBound(cp_var));
2766 const double new_ub = std::floor(cp_other_bound + kCpEpsilon);
2767 if (new_ub < ub) {
2768 // TODO(user): Because rc > kLpEpsilon, the lower_bound of cp_var
2769 // will be part of the reason returned by FillReducedCostsReason(), but
2770 // we actually do not need it here. Same below.
2771 const IntegerValue new_ub_int(static_cast<IntegerValue>(new_ub));
2772 deductions_.push_back(IntegerLiteral::LowerOrEqual(cp_var, new_ub_int));
2773 }
2774 } else if (rc < -kLpEpsilon) {
2775 const double lb = ToDouble(integer_trail_->LowerBound(cp_var));
2776 const double new_lb = std::ceil(cp_other_bound - kCpEpsilon);
2777 if (new_lb > lb) {
2778 const IntegerValue new_lb_int(static_cast<IntegerValue>(new_lb));
2779 deductions_.push_back(
2780 IntegerLiteral::GreaterOrEqual(cp_var, new_lb_int));
2781 }
2782 }
2783 }
2784}
2785
2786void LinearProgrammingConstraint::UpdateAverageReducedCosts() {
2787 const int num_vars = integer_variables_.size();
2788 if (sum_cost_down_.size() < num_vars) {
2789 sum_cost_down_.resize(num_vars, 0.0);
2790 num_cost_down_.resize(num_vars, 0);
2791 sum_cost_up_.resize(num_vars, 0.0);
2792 num_cost_up_.resize(num_vars, 0);
2793 rc_scores_.resize(num_vars, 0.0);
2794 }
2795
2796 // Decay averages.
2797 num_calls_since_reduced_cost_averages_reset_++;
2798 if (num_calls_since_reduced_cost_averages_reset_ == 10000) {
2799 for (int i = 0; i < num_vars; i++) {
2800 sum_cost_up_[i] /= 2;
2801 num_cost_up_[i] /= 2;
2802 sum_cost_down_[i] /= 2;
2803 num_cost_down_[i] /= 2;
2804 }
2805 num_calls_since_reduced_cost_averages_reset_ = 0;
2806 }
2807
2808 // Accumulate reduced costs of all unassigned variables.
2809 for (int i = 0; i < num_vars; i++) {
2810 const IntegerVariable var = integer_variables_[i];
2811
2812 // Skip fixed variables.
2813 if (integer_trail_->IsFixed(var)) continue;
2814
2815 // Skip reduced costs that are zero or close.
2816 const double rc = lp_reduced_cost_[i];
2817 if (std::abs(rc) < kCpEpsilon) continue;
2818
2819 if (rc < 0.0) {
2820 sum_cost_down_[i] -= rc;
2821 num_cost_down_[i]++;
2822 } else {
2823 sum_cost_up_[i] += rc;
2824 num_cost_up_[i]++;
2825 }
2826 }
2827
2828 // Tricky, we artificially reset the rc_rev_int_repository_ to level zero
2829 // so that the rev_rc_start_ is zero.
2830 rc_rev_int_repository_.SetLevel(0);
2831 rc_rev_int_repository_.SetLevel(trail_->CurrentDecisionLevel());
2832 rev_rc_start_ = 0;
2833
2834 // Cache the new score (higher is better) using the average reduced costs
2835 // as a signal.
2836 positions_by_decreasing_rc_score_.clear();
2837 for (int i = 0; i < num_vars; i++) {
2838 // If only one direction exist, we takes its value divided by 2, so that
2839 // such variable should have a smaller cost than the min of the two side
2840 // except if one direction have a really high reduced costs.
2841 const double a_up =
2842 num_cost_up_[i] > 0 ? sum_cost_up_[i] / num_cost_up_[i] : 0.0;
2843 const double a_down =
2844 num_cost_down_[i] > 0 ? sum_cost_down_[i] / num_cost_down_[i] : 0.0;
2845 if (num_cost_down_[i] > 0 && num_cost_up_[i] > 0) {
2846 rc_scores_[i] = std::min(a_up, a_down);
2847 } else {
2848 rc_scores_[i] = 0.5 * (a_down + a_up);
2849 }
2850
2851 // We ignore scores of zero (i.e. no data) and will follow the default
2852 // search heuristic if all variables are like this.
2853 if (rc_scores_[i] > 0.0) {
2854 positions_by_decreasing_rc_score_.push_back({-rc_scores_[i], i});
2855 }
2856 }
2857 std::sort(positions_by_decreasing_rc_score_.begin(),
2858 positions_by_decreasing_rc_score_.end());
2859}
2860
2861std::function<IntegerLiteral()>
2863 return [this]() { return this->LPReducedCostAverageDecision(); };
2864}
2865
2866IntegerLiteral LinearProgrammingConstraint::LPReducedCostAverageDecision() {
2867 // Select noninstantiated variable with highest positive average reduced cost.
2868 int selected_index = -1;
2869 const int size = positions_by_decreasing_rc_score_.size();
2870 rc_rev_int_repository_.SaveState(&rev_rc_start_);
2871 for (int i = rev_rc_start_; i < size; ++i) {
2872 const int index = positions_by_decreasing_rc_score_[i].second;
2873 const IntegerVariable var = integer_variables_[index];
2874 if (integer_trail_->IsFixed(var)) continue;
2875 selected_index = index;
2876 rev_rc_start_ = i;
2877 break;
2878 }
2879
2880 if (selected_index == -1) return IntegerLiteral();
2881 const IntegerVariable var = integer_variables_[selected_index];
2882
2883 // If ceil(value) is current upper bound, try var == upper bound first.
2884 // Guarding with >= prevents numerical problems.
2885 // With 0/1 variables, this will tend to try setting to 1 first,
2886 // which produces more shallow trees.
2887 const IntegerValue ub = integer_trail_->UpperBound(var);
2888 const IntegerValue value_ceil(
2889 std::ceil(this->GetSolutionValue(var) - kCpEpsilon));
2890 if (value_ceil >= ub) {
2891 return IntegerLiteral::GreaterOrEqual(var, ub);
2892 }
2893
2894 // If floor(value) is current lower bound, try var == lower bound first.
2895 // Guarding with <= prevents numerical problems.
2896 const IntegerValue lb = integer_trail_->LowerBound(var);
2897 const IntegerValue value_floor(
2898 std::floor(this->GetSolutionValue(var) + kCpEpsilon));
2899 if (value_floor <= lb) {
2900 return IntegerLiteral::LowerOrEqual(var, lb);
2901 }
2902
2903 // Here lb < value_floor <= value_ceil < ub.
2904 // Try the most promising split between var <= floor or var >= ceil.
2905 const double a_up =
2906 num_cost_up_[selected_index] > 0
2907 ? sum_cost_up_[selected_index] / num_cost_up_[selected_index]
2908 : 0.0;
2909 const double a_down =
2910 num_cost_down_[selected_index] > 0
2911 ? sum_cost_down_[selected_index] / num_cost_down_[selected_index]
2912 : 0.0;
2913 if (a_down < a_up) {
2914 return IntegerLiteral::LowerOrEqual(var, value_floor);
2915 } else {
2916 return IntegerLiteral::GreaterOrEqual(var, value_ceil);
2917 }
2918}
2919
2920absl::Span<const glop::ColIndex> LinearProgrammingConstraint::IntegerLpRowCols(
2921 glop::RowIndex row) const {
2922 const int start = integer_lp_[row].start_in_buffer;
2923 const size_t num_terms = static_cast<size_t>(integer_lp_[row].num_terms);
2924 return {integer_lp_cols_.data() + start, num_terms};
2925}
2926
2927absl::Span<const IntegerValue> LinearProgrammingConstraint::IntegerLpRowCoeffs(
2928 glop::RowIndex row) const {
2929 const int start = integer_lp_[row].start_in_buffer;
2930 const size_t num_terms = static_cast<size_t>(integer_lp_[row].num_terms);
2931 return {integer_lp_coeffs_.data() + start, num_terms};
2932}
2933
2935 return absl::StrFormat("%d rows, %d columns, %d entries", integer_lp_.size(),
2936 integer_variables_.size(), integer_lp_coeffs_.size());
2937}
2938
2939} // namespace sat
2940} // namespace operations_research
static int64_t GCD64(int64_t x, int64_t y)
Definition mathutil.h:108
void SaveState(T *object)
Definition rev.h:62
Fractional VariableScalingFactor(ColIndex col) const
Fractional UnscaleVariableValue(ColIndex col, Fractional value) const
Transforms corresponding value from the scaled domain to the original one.
Fractional GetVariableValue(ColIndex col) const
StrictITISpan< RowIndex, const Fractional > ConstView
Definition lp_types.h:291
IntegerValue LowerBound(IntegerVariable i) const
Returns the current lower/upper bound of the given integer variable.
Definition integer.h:1301
IntegerValue UpperBound(IntegerVariable i) const
Definition integer.h:1305
bool IsFixed(IntegerVariable i) const
Checks if the variable is fixed.
Definition integer.h:1309
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
const std::vector< ConstraintIndex > & LpConstraints() const
const util_intops::StrongVector< ConstraintIndex, ConstraintInfo > & AllConstraints() const
All the constraints managed by this class.
A class that stores the collection of all LP constraints in a model.
void SetLevel(int level) override
ReversibleInterface API.
void AddCutGenerator(CutGenerator generator)
Register a new cut generator with this constraint.
void SetObjectiveCoefficient(IntegerVariable ivar, IntegerValue coeff)
bool IncrementalPropagate(const std::vector< int > &watch_indices) override
LinearProgrammingConstraint(Model *model, absl::Span< const IntegerVariable > vars)
bool Add(glop::ColIndex col, IntegerValue value)
Does vector[col] += value and return false in case of overflow.
LinearConstraint ConvertToLinearConstraint(absl::Span< const IntegerVariable > integer_variables, IntegerValue upper_bound, std::optional< std::pair< IntegerVariable, IntegerValue > > extra_term=std::nullopt)
void ConvertToCutData(absl::int128 rhs, absl::Span< const IntegerVariable > integer_variables, absl::Span< const double > lp_solution, IntegerTrail *integer_trail, CutData *result)
std::vector< std::pair< glop::ColIndex, IntegerValue > > GetTerms()
Similar to ConvertToLinearConstraint().
bool AddLinearExpressionMultiple(IntegerValue multiplier, absl::Span< const glop::ColIndex > cols, absl::Span< const IntegerValue > coeffs, IntegerValue max_coeff_magnitude)
Force instantations for tests.
Simple class to add statistics by name and print them at the end.
void assign(size_type n, const value_type &val)
value_type * data()
– Pass-through methods to STL vector ----------------------------------—
void push_back(const value_type &val)
const bool DEBUG_MODE
Definition macros.h:26
constexpr ColIndex kInvalidCol(-1)
ColIndex RowToColIndex(RowIndex row)
Get the ColIndex corresponding to the column # row.
Definition lp_types.h:54
RowIndex ColToRowIndex(ColIndex col)
Get the RowIndex corresponding to the row # col.
Definition lp_types.h:57
StrictITIVector< RowIndex, Fractional > DenseColumn
Column-vector types. Column-vector types are indexed by a row index.
Definition lp_types.h:380
StrictITIVector< ColIndex, Fractional > DenseRow
Row-vector types. Row-vector types are indexed by a column index.
Definition lp_types.h:351
@ ABNORMAL
An error occurred during the solving process.
Definition lp_types.h:158
IntegerValue FloorRatio(IntegerValue dividend, IntegerValue positive_divisor)
bool AddProductTo(IntegerValue a, IntegerValue b, IntegerValue *result)
Computes result += a * b, and return false iff there is an overflow.
void DivideByGCD(LinearConstraint *constraint)
IntType IntTypeAbs(IntType t)
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Returns the vector of the negated variables.
Definition integer.cc:52
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
IntegerVariable PositiveVariable(IntegerVariable i)
int WeightedPick(absl::Span< const double > input, absl::BitGenRef random)
Definition util.cc:426
bool VariableIsPositive(IntegerVariable i)
LinearConstraintPropagator< true > IntegerSumLE128
IntegerValue CapProdI(IntegerValue a, IntegerValue b)
Overflows and saturated arithmetic.
double ToDouble(IntegerValue value)
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 CapAdd(int64_t x, int64_t y)
bool AddIntoOverflow(int64_t x, int64_t *y)
int64_t CapProd(int64_t x, int64_t y)
Point BinarySearch(Point x_true, Point x_false, std::function< bool(Point)> f)
Our cut are always of the form linear_expression <= rhs.
Definition cuts.h:116
std::vector< CutTerm > terms
Definition cuts.h:152
bool AppendOneTerm(IntegerVariable var, IntegerValue coeff, double lp_value, IntegerValue lb, IntegerValue ub)
Definition cuts.cc:135
static IntegerLiteral GreaterOrEqual(IntegerVariable i, IntegerValue bound)
static IntegerLiteral LowerOrEqual(IntegerVariable i, IntegerValue bound)
std::unique_ptr< IntegerValue[]> coeffs
std::unique_ptr< IntegerVariable[]> vars
absl::Span< const IntegerValue > CoeffsAsSpan() const
absl::Span< const IntegerVariable > VarsAsSpan() const
Same as ModelLpValues for reduced costs.