OR-Tools  9.3
cp_model_lns.cc
Go to the documentation of this file.
1// Copyright 2010-2021 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 <cmath>
18#include <cstdint>
19#include <limits>
20#include <random>
21#include <string>
22#include <utility>
23#include <vector>
24
25#include "absl/container/flat_hash_map.h"
26#include "absl/container/flat_hash_set.h"
27#include "absl/meta/type_traits.h"
28#include "absl/random/bit_gen_ref.h"
29#include "absl/random/distributions.h"
30#include "absl/strings/str_cat.h"
31#include "absl/strings/str_join.h"
32#include "absl/synchronization/mutex.h"
33#include "absl/time/clock.h"
34#include "absl/time/time.h"
35#include "absl/types/span.h"
38#include "ortools/sat/cp_model.pb.h"
41#include "ortools/sat/integer.h"
42#include "ortools/sat/model.h"
44#include "ortools/sat/rins.h"
45#include "ortools/sat/sat_parameters.pb.h"
53
54namespace operations_research {
55namespace sat {
56
58 CpModelProto const* model_proto, SatParameters const* parameters,
59 SharedResponseManager* shared_response, SharedTimeLimit* shared_time_limit,
60 SharedBoundsManager* shared_bounds)
61 : SubSolver(""),
62 parameters_(*parameters),
63 model_proto_(*model_proto),
64 shared_time_limit_(shared_time_limit),
65 shared_bounds_(shared_bounds),
66 shared_response_(shared_response) {
67 CHECK(shared_response_ != nullptr);
68 if (shared_bounds_ != nullptr) {
69 shared_bounds_id_ = shared_bounds_->RegisterNewId();
70 }
71 *model_proto_with_only_variables_.mutable_variables() =
72 model_proto_.variables();
73 InitializeHelperData();
74 RecomputeHelperData();
76 last_logging_time_ = absl::Now();
77}
78
80 if (shared_bounds_ != nullptr) {
81 std::vector<int> model_variables;
82 std::vector<int64_t> new_lower_bounds;
83 std::vector<int64_t> new_upper_bounds;
84 shared_bounds_->GetChangedBounds(shared_bounds_id_, &model_variables,
85 &new_lower_bounds, &new_upper_bounds);
86
87 bool new_variables_have_been_fixed = false;
88
89 {
90 absl::MutexLock domain_lock(&domain_mutex_);
91
92 for (int i = 0; i < model_variables.size(); ++i) {
93 const int var = model_variables[i];
94 const int64_t new_lb = new_lower_bounds[i];
95 const int64_t new_ub = new_upper_bounds[i];
96 if (VLOG_IS_ON(3)) {
97 const auto& domain =
98 model_proto_with_only_variables_.variables(var).domain();
99 const int64_t old_lb = domain.Get(0);
100 const int64_t old_ub = domain.Get(domain.size() - 1);
101 VLOG(3) << "Variable: " << var << " old domain: [" << old_lb << ", "
102 << old_ub << "] new domain: [" << new_lb << ", " << new_ub
103 << "]";
104 }
105 const Domain old_domain = ReadDomainFromProto(
106 model_proto_with_only_variables_.variables(var));
107 const Domain new_domain =
108 old_domain.IntersectionWith(Domain(new_lb, new_ub));
109 if (new_domain.IsEmpty()) {
110 // This can mean two things:
111 // 1/ This variable is a normal one and the problem is UNSAT or
112 // 2/ This variable is optional, and its associated literal must be
113 // set to false.
114 //
115 // Currently, we wait for any full solver to pick the crossing bounds
116 // and do the correct stuff on their own. We do not want to have empty
117 // domain in the proto as this would means INFEASIBLE. So we just
118 // ignore such bounds here.
119 //
120 // TODO(user): We could set the optional literal to false directly in
121 // the bound sharing manager. We do have to be careful that all the
122 // different solvers have the same optionality definition though.
123 continue;
124 }
126 new_domain,
127 model_proto_with_only_variables_.mutable_variables(var));
128 new_variables_have_been_fixed |= new_domain.IsFixed();
129 }
130 }
131
132 // Only trigger the computation if needed.
133 if (new_variables_have_been_fixed) {
134 RecomputeHelperData();
135 }
136 }
137}
138
139bool NeighborhoodGeneratorHelper::ObjectiveDomainIsConstraining() const {
140 if (!model_proto_.has_objective()) return false;
141 if (model_proto_.objective().domain().empty()) return false;
142
143 int64_t min_activity = 0;
144 int64_t max_activity = 0;
145 const int num_terms = model_proto_.objective().vars().size();
146 for (int i = 0; i < num_terms; ++i) {
147 const int var = PositiveRef(model_proto_.objective().vars(i));
148 const int64_t coeff = model_proto_.objective().coeffs(i);
149 const auto& var_domain =
150 model_proto_with_only_variables_.variables(var).domain();
151 const int64_t v1 = coeff * var_domain[0];
152 const int64_t v2 = coeff * var_domain[var_domain.size() - 1];
153 min_activity += std::min(v1, v2);
154 max_activity += std::max(v1, v2);
155 }
156
157 const Domain obj_domain = ReadDomainFromProto(model_proto_.objective());
158 const Domain inferred_domain =
159 Domain(min_activity, max_activity)
162 return !inferred_domain.IsIncludedIn(obj_domain);
163}
164
165void NeighborhoodGeneratorHelper::InitializeHelperData() {
166 type_to_constraints_.clear();
167 const int num_constraints = model_proto_.constraints_size();
168 for (int c = 0; c < num_constraints; ++c) {
169 const int type = model_proto_.constraints(c).constraint_case();
170 if (type >= type_to_constraints_.size()) {
171 type_to_constraints_.resize(type + 1);
172 }
173 type_to_constraints_[type].push_back(c);
174 }
175
176 const int num_variables = model_proto_.variables().size();
177 is_in_objective_.resize(num_variables, false);
178 if (model_proto_.has_objective()) {
179 for (const int ref : model_proto_.objective().vars()) {
180 is_in_objective_[PositiveRef(ref)] = true;
181 }
182 }
183}
184
185// Recompute all the data when new variables have been fixed. Note that this
186// shouldn't be called if there is no change as it is in O(problem size).
187void NeighborhoodGeneratorHelper::RecomputeHelperData() {
188 absl::MutexLock graph_lock(&graph_mutex_);
189 absl::ReaderMutexLock domain_lock(&domain_mutex_);
190
191 // Do basic presolving to have a more precise graph.
192 // Here we just remove trivially true constraints.
193 //
194 // Note(user): We do that each time a new variable is fixed. It might be too
195 // much, but on the miplib and in 1200s, we do that only about 1k time on the
196 // worst case problem.
197 //
198 // TODO(user): Change API to avoid a few copy?
199 // TODO(user): We could keep the context in the class.
200 // TODO(user): We can also start from the previous simplified model instead.
201 {
202 Model local_model;
203 CpModelProto mapping_proto;
204 simplied_model_proto_.Clear();
205 *simplied_model_proto_.mutable_variables() =
206 model_proto_with_only_variables_.variables();
207 PresolveContext context(&local_model, &simplied_model_proto_,
208 &mapping_proto);
209 ModelCopy copier(&context);
210
211 // TODO(user): Not sure what to do if the model is UNSAT.
212 // This shouldn't matter as it should be dealt with elsewhere.
213 copier.ImportAndSimplifyConstraints(model_proto_, {});
214 }
215
216 // Compute the constraint <-> variable graph.
217 //
218 // TODO(user): Remove duplicate constraints?
219 const auto& constraints = simplied_model_proto_.constraints();
220 var_to_constraint_.assign(model_proto_.variables_size(), {});
221 constraint_to_var_.assign(constraints.size(), {});
222 int reduced_ct_index = 0;
223 for (int ct_index = 0; ct_index < constraints.size(); ++ct_index) {
224 // We remove the interval constraints since we should have an equivalent
225 // linear constraint somewhere else. This is not the case if we have a fixed
226 // size optional interval variable. But it should not matter as the
227 // intervals are replaced by their underlying variables in the scheduling
228 // constrainst.
229 if (constraints[ct_index].constraint_case() == ConstraintProto::kInterval) {
230 continue;
231 }
232
233 for (const int var : UsedVariables(constraints[ct_index])) {
234 if (IsConstant(var)) continue;
235 constraint_to_var_[reduced_ct_index].push_back(var);
236 }
237
238 // We replace intervals by their underlying integer variables. Note that
239 // this is needed for a correct decomposition into independent part.
240 for (const int interval : UsedIntervals(constraints[ct_index])) {
241 for (const int var : UsedVariables(constraints[interval])) {
242 if (IsConstant(var)) continue;
243 constraint_to_var_[reduced_ct_index].push_back(var);
244 }
245 }
246
247 // We remove constraint of size 0 and 1 since they are not useful for LNS
248 // based on this graph.
249 if (constraint_to_var_[reduced_ct_index].size() <= 1) {
250 constraint_to_var_[reduced_ct_index].clear();
251 continue;
252 }
253
254 // Keep this constraint.
255 for (const int var : constraint_to_var_[reduced_ct_index]) {
256 var_to_constraint_[var].push_back(reduced_ct_index);
257 }
258 ++reduced_ct_index;
259 }
260 constraint_to_var_.resize(reduced_ct_index);
261
262 // We mark as active all non-constant variables.
263 // Non-active variable will never be fixed in standard LNS fragment.
264 active_variables_.clear();
265 const int num_variables = model_proto_.variables_size();
266 active_variables_set_.assign(num_variables, false);
267 for (int i = 0; i < num_variables; ++i) {
268 if (!IsConstant(i)) {
269 active_variables_.push_back(i);
270 active_variables_set_[i] = true;
271 }
272 }
273
274 // Compute connected components.
275 // Note that fixed variable are just ignored.
277 union_find.SetNumberOfNodes(num_variables);
278 for (const std::vector<int>& var_in_constraint : constraint_to_var_) {
279 if (var_in_constraint.size() <= 1) continue;
280 for (int i = 1; i < var_in_constraint.size(); ++i) {
281 union_find.AddEdge(var_in_constraint[0], var_in_constraint[i]);
282 }
283 }
284
285 // If we have a lower bound on the objective, then this "objective constraint"
286 // might link components together.
287 if (ObjectiveDomainIsConstraining()) {
288 const auto& refs = model_proto_.objective().vars();
289 const int num_terms = refs.size();
290 for (int i = 1; i < num_terms; ++i) {
291 union_find.AddEdge(PositiveRef(refs[0]), PositiveRef(refs[i]));
292 }
293 }
294
295 // Compute all components involving non-fixed variables.
296 //
297 // TODO(user): If a component has no objective, we can fix it to any feasible
298 // solution. This will automatically be done by LNS fragment covering such
299 // component though.
300 components_.clear();
301 var_to_component_index_.assign(num_variables, -1);
302 for (int var = 0; var < num_variables; ++var) {
303 if (IsConstant(var)) continue;
304 const int root = union_find.FindRoot(var);
305 CHECK_LT(root, var_to_component_index_.size());
306 int& index = var_to_component_index_[root];
307 if (index == -1) {
308 index = components_.size();
309 components_.push_back({});
310 }
311 var_to_component_index_[var] = index;
312 components_[index].push_back(var);
313 }
314
315 // Display information about the reduced problem.
316 //
317 // TODO(user): Exploit connected component while generating fragments.
318 // TODO(user): Do not generate fragment not touching the objective.
319 if (!shared_response_->LoggingIsEnabled()) return;
320
321 std::vector<int> component_sizes;
322 for (const std::vector<int>& component : components_) {
323 component_sizes.push_back(component.size());
324 }
325 std::sort(component_sizes.begin(), component_sizes.end(),
326 std::greater<int>());
327 std::string compo_message;
328 if (component_sizes.size() > 1) {
329 if (component_sizes.size() <= 10) {
330 compo_message =
331 absl::StrCat(" compo:", absl::StrJoin(component_sizes, ","));
332 } else {
333 component_sizes.resize(10);
334 compo_message =
335 absl::StrCat(" compo:", absl::StrJoin(component_sizes, ","), ",...");
336 }
337 }
338 shared_response_->LogPeriodicMessage(
339 "Model",
340 absl::StrCat("var:", active_variables_.size(), "/", num_variables,
341 " constraints:", simplied_model_proto_.constraints().size(),
342 "/", model_proto_.constraints().size(), compo_message),
343 &last_logging_time_);
344}
345
347 return active_variables_set_[var];
348}
349
350bool NeighborhoodGeneratorHelper::IsConstant(int var) const {
351 return model_proto_with_only_variables_.variables(var).domain_size() == 2 &&
352 model_proto_with_only_variables_.variables(var).domain(0) ==
353 model_proto_with_only_variables_.variables(var).domain(1);
354}
355
357 Neighborhood neighborhood;
358 neighborhood.is_reduced = false;
359 neighborhood.is_generated = true;
360 {
361 absl::ReaderMutexLock lock(&domain_mutex_);
362 *neighborhood.delta.mutable_variables() =
363 model_proto_with_only_variables_.variables();
364 }
365 return neighborhood;
366}
367
369 Neighborhood neighborhood;
370 neighborhood.is_generated = false;
371 return neighborhood;
372}
373
375 const CpSolverResponse& initial_solution) const {
376 std::vector<int> active_intervals;
377 absl::ReaderMutexLock lock(&domain_mutex_);
378 for (const int i : TypeToConstraints(ConstraintProto::kInterval)) {
379 const ConstraintProto& interval_ct = ModelProto().constraints(i);
380 // We only look at intervals that are performed in the solution. The
381 // unperformed intervals should be automatically freed during the generation
382 // phase.
383 if (interval_ct.enforcement_literal().size() == 1) {
384 const int enforcement_ref = interval_ct.enforcement_literal(0);
385 const int enforcement_var = PositiveRef(enforcement_ref);
386 const int value = initial_solution.solution(enforcement_var);
387 if (RefIsPositive(enforcement_ref) == (value == 0)) {
388 continue;
389 }
390 }
391
392 // We filter out fixed intervals. Because of presolve, if there is an
393 // enforcement literal, it cannot be fixed.
394 if (interval_ct.enforcement_literal().empty()) {
395 bool is_constant = true;
396 for (const int v : interval_ct.interval().start().vars()) {
397 if (!IsConstant(v)) {
398 is_constant = false;
399 break;
400 }
401 }
402 for (const int v : interval_ct.interval().size().vars()) {
403 if (!IsConstant(v)) {
404 is_constant = false;
405 break;
406 }
407 }
408 for (const int v : interval_ct.interval().end().vars()) {
409 if (!IsConstant(v)) {
410 is_constant = false;
411 break;
412 }
413 }
414 if (is_constant) continue;
415 }
416
417 active_intervals.push_back(i);
418 }
419 return active_intervals;
420}
421
423 const CpSolverResponse& initial_solution) const {
424 struct HeadAndArcLiteral {
425 int head;
426 int literal;
427 };
428
429 std::vector<std::vector<int>> result;
430 absl::flat_hash_map<int, HeadAndArcLiteral> tail_to_head_and_arc_literal;
431
432 for (const int i : TypeToConstraints(ConstraintProto::kCircuit)) {
433 const CircuitConstraintProto& ct = ModelProto().constraints(i).circuit();
434
435 // Collect arcs.
436 int min_node = std::numeric_limits<int>::max();
437 tail_to_head_and_arc_literal.clear();
438 for (int i = 0; i < ct.literals_size(); ++i) {
439 const int literal = ct.literals(i);
440 const int head = ct.heads(i);
441 const int tail = ct.tails(i);
442 const int bool_var = PositiveRef(literal);
443 const int64_t value = initial_solution.solution(bool_var);
444 // Skip unselected arcs.
445 if (RefIsPositive(literal) == (value == 0)) continue;
446 // Ignore self loops.
447 if (head == tail) continue;
448 tail_to_head_and_arc_literal[tail] = {head, bool_var};
449 min_node = std::min(tail, min_node);
450 }
451 if (tail_to_head_and_arc_literal.empty()) continue;
452
453 // Unroll the path.
454 int current_node = min_node;
455 std::vector<int> path;
456 do {
457 auto it = tail_to_head_and_arc_literal.find(current_node);
458 CHECK(it != tail_to_head_and_arc_literal.end());
459 current_node = it->second.head;
460 path.push_back(it->second.literal);
461 } while (current_node != min_node);
462 result.push_back(std::move(path));
463 }
464
465 std::vector<HeadAndArcLiteral> route_starts;
466 for (const int i : TypeToConstraints(ConstraintProto::kRoutes)) {
467 const RoutesConstraintProto& ct = ModelProto().constraints(i).routes();
468 tail_to_head_and_arc_literal.clear();
469 route_starts.clear();
470
471 // Collect route starts and arcs.
472 for (int i = 0; i < ct.literals_size(); ++i) {
473 const int literal = ct.literals(i);
474 const int head = ct.heads(i);
475 const int tail = ct.tails(i);
476 const int bool_var = PositiveRef(literal);
477 const int64_t value = initial_solution.solution(bool_var);
478 // Skip unselected arcs.
479 if (RefIsPositive(literal) == (value == 0)) continue;
480 // Ignore self loops.
481 if (head == tail) continue;
482 if (tail == 0) {
483 route_starts.push_back({head, bool_var});
484 } else {
485 tail_to_head_and_arc_literal[tail] = {head, bool_var};
486 }
487 }
488
489 // Unroll all routes.
490 for (const HeadAndArcLiteral& head_var : route_starts) {
491 std::vector<int> path;
492 int current_node = head_var.head;
493 path.push_back(head_var.literal);
494 do {
495 auto it = tail_to_head_and_arc_literal.find(current_node);
496 CHECK(it != tail_to_head_and_arc_literal.end());
497 current_node = it->second.head;
498 path.push_back(it->second.literal);
499 } while (current_node != 0);
500 result.push_back(std::move(path));
501 }
502 }
503
504 return result;
505}
506
508 const CpSolverResponse& base_solution,
509 const absl::flat_hash_set<int>& variables_to_fix) const {
510 Neighborhood neighborhood;
511
512 // Fill in neighborhood.delta all variable domains.
513 {
514 absl::ReaderMutexLock domain_lock(&domain_mutex_);
515
516 const int num_variables =
517 model_proto_with_only_variables_.variables().size();
518 neighborhood.delta.mutable_variables()->Reserve(num_variables);
519 for (int i = 0; i < num_variables; ++i) {
520 const IntegerVariableProto& current_var =
521 model_proto_with_only_variables_.variables(i);
522 IntegerVariableProto* new_var = neighborhood.delta.add_variables();
523
524 // We only copy the name in debug mode.
525 if (DEBUG_MODE) new_var->set_name(current_var.name());
526
527 const Domain domain = ReadDomainFromProto(current_var);
528 const int64_t base_value = base_solution.solution(i);
529
530 // It seems better to always start from a feasible point, so if the base
531 // solution is no longer valid under the new up to date bound, we make
532 // sure to relax the domain so that it is.
533 if (!domain.Contains(base_value)) {
534 // TODO(user): this can happen when variables_to_fix.contains(i). But we
535 // should probably never consider as "active" such variable in the first
536 // place.
537 //
538 // TODO(user): This might lead to incompatibility when the base solution
539 // is not compatible with variable we fixed in a connected component! so
540 // maybe it is not great to do that. Initial investigation didn't see
541 // a big change. More work needed.
542 FillDomainInProto(domain.UnionWith(Domain(base_solution.solution(i))),
543 new_var);
544 } else if (variables_to_fix.contains(i)) {
545 new_var->add_domain(base_value);
546 new_var->add_domain(base_value);
547 } else {
548 FillDomainInProto(domain, new_var);
549 }
550 }
551 }
552
553 // Fill some statistic fields and detect if we cover a full component.
554 //
555 // TODO(user): If there is just one component, we can skip some computation.
556 {
557 absl::ReaderMutexLock graph_lock(&graph_mutex_);
558 std::vector<int> count(components_.size(), 0);
559 const int num_variables = neighborhood.delta.variables().size();
560 for (int var = 0; var < num_variables; ++var) {
561 const auto& domain = neighborhood.delta.variables(var).domain();
562 if (domain.size() != 2 || domain[0] != domain[1]) {
563 ++neighborhood.num_relaxed_variables;
564 if (is_in_objective_[var]) {
566 }
567 const int c = var_to_component_index_[var];
568 if (c != -1) count[c]++;
569 }
570 }
571
572 for (int i = 0; i < components_.size(); ++i) {
573 if (count[i] == components_[i].size()) {
576 components_[i].begin(), components_[i].end());
577 }
578 }
579 }
580
581 // If the objective domain might cut the optimal solution, we cannot exploit
582 // the connected components. We compute this outside the mutex to avoid
583 // any deadlock risk.
584 //
585 // TODO(user): We could handle some complex domain (size > 2).
586 if (model_proto_.has_objective() &&
587 (model_proto_.objective().domain().size() != 2 ||
588 shared_response_->GetInnerObjectiveLowerBound() <
589 model_proto_.objective().domain(0))) {
591 }
592
593 AddSolutionHinting(base_solution, &neighborhood.delta);
594
595 neighborhood.is_generated = true;
596 neighborhood.is_reduced = !variables_to_fix.empty();
597 neighborhood.is_simple = true;
598
599 // TODO(user): force better objective? Note that this is already done when the
600 // hint above is successfully loaded (i.e. if it passes the presolve
601 // correctly) since the solver will try to find better solution than the
602 // current one.
603 return neighborhood;
604}
605
607 const CpSolverResponse& initial_solution, CpModelProto* model_proto) const {
608 // Set the current solution as a hint.
609 model_proto->clear_solution_hint();
610 const auto is_fixed = [model_proto](int var) {
611 const IntegerVariableProto& var_proto = model_proto->variables(var);
612 return var_proto.domain_size() == 2 &&
613 var_proto.domain(0) == var_proto.domain(1);
614 };
615 for (int var = 0; var < model_proto->variables_size(); ++var) {
616 if (is_fixed(var)) continue;
617
618 model_proto->mutable_solution_hint()->add_vars(var);
619 model_proto->mutable_solution_hint()->add_values(
620 initial_solution.solution(var));
621 }
622}
623
625 const std::vector<int>& constraints_to_remove) const {
626 Neighborhood neighborhood = FullNeighborhood();
627
628 if (constraints_to_remove.empty()) return neighborhood;
629 neighborhood.is_reduced = false;
630 neighborhood.constraints_to_ignore = constraints_to_remove;
631 return neighborhood;
632}
633
635 const CpSolverResponse& initial_solution,
636 const std::vector<int>& relaxed_variables) const {
637 std::vector<bool> relaxed_variables_set(model_proto_.variables_size(), false);
638 for (const int var : relaxed_variables) relaxed_variables_set[var] = true;
639 absl::flat_hash_set<int> fixed_variables;
640 {
641 absl::ReaderMutexLock graph_lock(&graph_mutex_);
642 for (const int i : active_variables_) {
643 if (!relaxed_variables_set[i]) {
644 fixed_variables.insert(i);
645 }
646 }
647 }
648 return FixGivenVariables(initial_solution, fixed_variables);
649}
650
652 const CpSolverResponse& initial_solution) const {
653 const std::vector<int>& all_variables = ActiveVariables();
654 const absl::flat_hash_set<int> fixed_variables(all_variables.begin(),
655 all_variables.end());
656 return FixGivenVariables(initial_solution, fixed_variables);
657}
658
661}
662
663double NeighborhoodGenerator::GetUCBScore(int64_t total_num_calls) const {
664 absl::ReaderMutexLock mutex_lock(&generator_mutex_);
665 DCHECK_GE(total_num_calls, num_calls_);
666 if (num_calls_ <= 10) return std::numeric_limits<double>::infinity();
667 return current_average_ + sqrt((2 * log(total_num_calls)) / num_calls_);
668}
669
671 absl::MutexLock mutex_lock(&generator_mutex_);
672
673 // To make the whole update process deterministic, we currently sort the
674 // SolveData.
675 std::sort(solve_data_.begin(), solve_data_.end());
676
677 // This will be used to update the difficulty of this neighborhood.
678 int num_fully_solved_in_batch = 0;
679 int num_not_fully_solved_in_batch = 0;
680
681 for (const SolveData& data : solve_data_) {
683 ++num_calls_;
684
685 // INFEASIBLE or OPTIMAL means that we "fully solved" the local problem.
686 // If we didn't, then we cannot be sure that there is no improving solution
687 // in that neighborhood.
688 if (data.status == CpSolverStatus::INFEASIBLE ||
689 data.status == CpSolverStatus::OPTIMAL) {
690 ++num_fully_solved_calls_;
691 ++num_fully_solved_in_batch;
692 } else {
693 ++num_not_fully_solved_in_batch;
694 }
695
696 // It seems to make more sense to compare the new objective to the base
697 // solution objective, not the best one. However this causes issue in the
698 // logic below because on some problems the neighborhood can always lead
699 // to a better "new objective" if the base solution wasn't the best one.
700 //
701 // This might not be a final solution, but it does work ok for now.
702 const IntegerValue best_objective_improvement =
704 ? IntegerValue(CapSub(data.new_objective_bound.value(),
705 data.initial_best_objective_bound.value()))
706 : IntegerValue(CapSub(data.initial_best_objective.value(),
707 data.new_objective.value()));
708 if (best_objective_improvement > 0) {
709 num_consecutive_non_improving_calls_ = 0;
710 } else {
711 ++num_consecutive_non_improving_calls_;
712 }
713
714 // TODO(user): Weight more recent data.
715 // degrade the current average to forget old learnings.
716 const double gain_per_time_unit =
717 std::max(0.0, static_cast<double>(best_objective_improvement.value())) /
718 (1.0 + data.deterministic_time);
719 if (num_calls_ <= 100) {
720 current_average_ += (gain_per_time_unit - current_average_) / num_calls_;
721 } else {
722 current_average_ = 0.9 * current_average_ + 0.1 * gain_per_time_unit;
723 }
724
725 deterministic_time_ += data.deterministic_time;
726 }
727
728 // Update the difficulty.
729 difficulty_.Update(/*num_decreases=*/num_not_fully_solved_in_batch,
730 /*num_increases=*/num_fully_solved_in_batch);
731
732 // Bump the time limit if we saw no better solution in the last few calls.
733 // This means that as the search progress, we likely spend more and more time
734 // trying to solve individual neighborhood.
735 //
736 // TODO(user): experiment with resetting the time limit if a solution is
737 // found.
738 if (num_consecutive_non_improving_calls_ > 50) {
739 num_consecutive_non_improving_calls_ = 0;
740 deterministic_limit_ *= 1.02;
741
742 // We do not want the limit to go to high. Intuitively, the goal is to try
743 // out a lot of neighborhoods, not just spend a lot of time on a few.
744 deterministic_limit_ = std::min(60.0, deterministic_limit_);
745 }
746
747 solve_data_.clear();
748}
749
750namespace {
751
752void GetRandomSubset(double relative_size, std::vector<int>* base,
753 absl::BitGenRef random) {
754 if (base->empty()) return;
755
756 // TODO(user): we could generate this more efficiently than using random
757 // shuffle.
758 std::shuffle(base->begin(), base->end(), random);
759 const int target_size = std::round(relative_size * base->size());
760 base->resize(target_size);
761}
762
763} // namespace
764
766 const CpSolverResponse& initial_solution, double difficulty,
767 absl::BitGenRef random) {
768 std::vector<int> fixed_variables = helper_.ActiveVariables();
769 GetRandomSubset(1.0 - difficulty, &fixed_variables, random);
771 initial_solution, {fixed_variables.begin(), fixed_variables.end()});
772}
773
775 const CpSolverResponse& initial_solution, double difficulty,
776 absl::BitGenRef random) {
778 return helper_.FullNeighborhood();
779 }
780
781 std::vector<int> relaxed_variables;
782 {
783 absl::ReaderMutexLock graph_lock(&helper_.graph_mutex_);
784 const int num_active_constraints = helper_.ConstraintToVar().size();
785 std::vector<int> active_constraints(num_active_constraints);
786 for (int c = 0; c < num_active_constraints; ++c) {
787 active_constraints[c] = c;
788 }
789 std::shuffle(active_constraints.begin(), active_constraints.end(), random);
790
791 const int num_model_vars = helper_.ModelProto().variables_size();
792 std::vector<bool> visited_variables_set(num_model_vars, false);
793
794 const int num_active_vars =
796 const int target_size = std::ceil(difficulty * num_active_vars);
797 CHECK_GT(target_size, 0);
798
799 for (const int constraint_index : active_constraints) {
800 for (const int var : helper_.ConstraintToVar()[constraint_index]) {
801 if (visited_variables_set[var]) continue;
802 visited_variables_set[var] = true;
803 if (helper_.IsActive(var)) {
804 relaxed_variables.push_back(var);
805 if (relaxed_variables.size() == target_size) break;
806 }
807 }
808 if (relaxed_variables.size() == target_size) break;
809 }
810 }
811
812 return helper_.RelaxGivenVariables(initial_solution, relaxed_variables);
813}
814
816 const CpSolverResponse& initial_solution, double difficulty,
817 absl::BitGenRef random) {
819 return helper_.FullNeighborhood();
820 }
821
822 const int num_model_vars = helper_.ModelProto().variables_size();
823 std::vector<bool> visited_variables_set(num_model_vars, false);
824 std::vector<int> relaxed_variables;
825 std::vector<int> visited_variables;
826
827 // It is important complexity wise to never scan a constraint twice!
828 const int num_model_constraints = helper_.ModelProto().constraints_size();
829 std::vector<bool> scanned_constraints(num_model_constraints, false);
830
831 std::vector<int> random_variables;
832 {
833 absl::ReaderMutexLock graph_lock(&helper_.graph_mutex_);
834
835 // The number of active variables can decrease asynchronously.
836 // We read the exact number while locked.
837 const int num_active_vars =
839 const int target_size = std::ceil(difficulty * num_active_vars);
840 CHECK_GT(target_size, 0) << difficulty << " " << num_active_vars;
841
842 const int first_var =
843 helper_.ActiveVariablesWhileHoldingLock()[absl::Uniform<int>(
844 random, 0, num_active_vars)];
845
846 visited_variables_set[first_var] = true;
847 visited_variables.push_back(first_var);
848 relaxed_variables.push_back(first_var);
849
850 for (int i = 0; i < visited_variables.size(); ++i) {
851 random_variables.clear();
852 // Collect all the variables that appears in the same constraints as
853 // visited_variables[i].
854 for (const int ct : helper_.VarToConstraint()[visited_variables[i]]) {
855 if (scanned_constraints[ct]) continue;
856 scanned_constraints[ct] = true;
857 for (const int var : helper_.ConstraintToVar()[ct]) {
858 if (visited_variables_set[var]) continue;
859 visited_variables_set[var] = true;
860 random_variables.push_back(var);
861 }
862 }
863 // We always randomize to change the partial subgraph explored
864 // afterwards.
865 std::shuffle(random_variables.begin(), random_variables.end(), random);
866 for (const int var : random_variables) {
867 if (relaxed_variables.size() < target_size) {
868 visited_variables.push_back(var);
869 if (helper_.IsActive(var)) {
870 relaxed_variables.push_back(var);
871 }
872 } else {
873 break;
874 }
875 }
876 if (relaxed_variables.size() >= target_size) break;
877 }
878 }
879 return helper_.RelaxGivenVariables(initial_solution, relaxed_variables);
880}
881
883 const CpSolverResponse& initial_solution, double difficulty,
884 absl::BitGenRef random) {
885 const int num_model_constraints = helper_.ModelProto().constraints_size();
886 if (num_model_constraints == 0 ||
888 return helper_.FullNeighborhood();
889 }
890
891 const int num_model_vars = helper_.ModelProto().variables_size();
892 std::vector<bool> visited_variables_set(num_model_vars, false);
893 std::vector<int> relaxed_variables;
894
895 std::vector<bool> added_constraints(num_model_constraints, false);
896 std::vector<int> next_constraints;
897
898 std::vector<int> random_variables;
899 {
900 absl::ReaderMutexLock graph_lock(&helper_.graph_mutex_);
901 const int num_active_vars =
903 const int target_size = std::ceil(difficulty * num_active_vars);
904 CHECK_GT(target_size, 0);
905
906 // Start by a random constraint.
907 const int num_active_constraints = helper_.ConstraintToVar().size();
908 if (num_active_constraints != 0) {
909 next_constraints.push_back(
910 absl::Uniform<int>(random, 0, num_active_constraints));
911 added_constraints[next_constraints.back()] = true;
912 }
913
914 while (relaxed_variables.size() < target_size) {
915 // Stop if we have a full connected component.
916 if (next_constraints.empty()) break;
917
918 // Pick a random unprocessed constraint.
919 const int i = absl::Uniform<int>(random, 0, next_constraints.size());
920 const int constraint_index = next_constraints[i];
921 std::swap(next_constraints[i], next_constraints.back());
922 next_constraints.pop_back();
923
924 // Add all the variable of this constraint and increase the set of next
925 // possible constraints.
926 CHECK_LT(constraint_index, num_active_constraints);
927 random_variables = helper_.ConstraintToVar()[constraint_index];
928 std::shuffle(random_variables.begin(), random_variables.end(), random);
929 for (const int var : random_variables) {
930 if (visited_variables_set[var]) continue;
931 visited_variables_set[var] = true;
932 if (helper_.IsActive(var)) {
933 relaxed_variables.push_back(var);
934 }
935 if (relaxed_variables.size() == target_size) break;
936
937 for (const int ct : helper_.VarToConstraint()[var]) {
938 if (added_constraints[ct]) continue;
939 added_constraints[ct] = true;
940 next_constraints.push_back(ct);
941 }
942 }
943 }
944 }
945 return helper_.RelaxGivenVariables(initial_solution, relaxed_variables);
946}
947
948namespace {
949
950int64_t GetLinearExpressionValue(const LinearExpressionProto& expr,
951 const CpSolverResponse& initial_solution) {
952 int64_t result = expr.offset();
953 for (int i = 0; i < expr.vars_size(); ++i) {
954 result += expr.coeffs(i) * initial_solution.solution(expr.vars(i));
955 }
956 return result;
957}
958
959void AddLinearExpressionToConstraint(const int64_t coeff,
960 const LinearExpressionProto& expr,
961 LinearConstraintProto* constraint,
962 int64_t* rhs_offset) {
963 *rhs_offset -= coeff * expr.offset();
964 for (int i = 0; i < expr.vars_size(); ++i) {
965 constraint->add_vars(expr.vars(i));
966 constraint->add_coeffs(expr.coeffs(i) * coeff);
967 }
968}
969
970void AddPrecedenceConstraints(const absl::Span<const int> intervals,
971 const absl::flat_hash_set<int>& ignored_intervals,
972 const CpSolverResponse& initial_solution,
973 const NeighborhoodGeneratorHelper& helper,
974 Neighborhood* neighborhood) {
975 // Sort all non-relaxed intervals of this constraint by current start
976 // time.
977 std::vector<std::pair<int64_t, int>> start_interval_pairs;
978 for (const int i : intervals) {
979 if (ignored_intervals.contains(i)) continue;
980 const ConstraintProto& interval_ct = helper.ModelProto().constraints(i);
981
982 // TODO(user): we ignore size zero for now.
983 const LinearExpressionProto& size_var = interval_ct.interval().size();
984 if (GetLinearExpressionValue(size_var, initial_solution) == 0) continue;
985
986 const LinearExpressionProto& start_var = interval_ct.interval().start();
987 const int64_t start_value =
988 GetLinearExpressionValue(start_var, initial_solution);
989
990 start_interval_pairs.push_back({start_value, i});
991 }
992 std::sort(start_interval_pairs.begin(), start_interval_pairs.end());
993
994 // Add precedence between the remaining intervals, forcing their order.
995 for (int i = 0; i + 1 < start_interval_pairs.size(); ++i) {
996 const LinearExpressionProto& before_start =
997 helper.ModelProto()
998 .constraints(start_interval_pairs[i].second)
999 .interval()
1000 .start();
1001 const LinearExpressionProto& before_end =
1002 helper.ModelProto()
1003 .constraints(start_interval_pairs[i].second)
1004 .interval()
1005 .end();
1006 const LinearExpressionProto& after_start =
1007 helper.ModelProto()
1008 .constraints(start_interval_pairs[i + 1].second)
1009 .interval()
1010 .start();
1011
1012 // If the end was smaller we keep it that way, otherwise we just order the
1013 // start variables.
1014 LinearConstraintProto* linear =
1015 neighborhood->delta.add_constraints()->mutable_linear();
1016 linear->add_domain(std::numeric_limits<int64_t>::min());
1017 int64_t rhs_offset = 0;
1018 if (GetLinearExpressionValue(before_end, initial_solution) <=
1019 GetLinearExpressionValue(after_start, initial_solution)) {
1020 // If the end was smaller than the next start, keep it that way.
1021 AddLinearExpressionToConstraint(1, before_end, linear, &rhs_offset);
1022 } else {
1023 // Otherwise, keep the same minimum separation. This is done in order
1024 // to "simplify" the neighborhood.
1025 rhs_offset = GetLinearExpressionValue(before_start, initial_solution) -
1026 GetLinearExpressionValue(after_start, initial_solution);
1027 AddLinearExpressionToConstraint(1, before_start, linear, &rhs_offset);
1028 }
1029
1030 AddLinearExpressionToConstraint(-1, after_start, linear, &rhs_offset);
1031 linear->add_domain(rhs_offset);
1032
1033 // The linear constraint should be satisfied by the current solution.
1034 if (DEBUG_MODE) {
1035 int64_t activity = 0;
1036 for (int i = 0; i < linear->vars().size(); ++i) {
1037 activity +=
1038 linear->coeffs(i) * initial_solution.solution(linear->vars(i));
1039 }
1040 CHECK_GE(activity, linear->domain(0));
1041 CHECK_LE(activity, linear->domain(1));
1042 }
1043 }
1044}
1045} // namespace
1046
1048 const absl::Span<const int> intervals_to_relax,
1049 const CpSolverResponse& initial_solution,
1050 const NeighborhoodGeneratorHelper& helper) {
1051 Neighborhood neighborhood = helper.FullNeighborhood();
1052 neighborhood.is_reduced =
1053 (intervals_to_relax.size() <
1054 helper.TypeToConstraints(ConstraintProto::kInterval).size());
1055
1056 // We will extend the set with some interval that we cannot fix.
1057 absl::flat_hash_set<int> ignored_intervals(intervals_to_relax.begin(),
1058 intervals_to_relax.end());
1059
1060 // Fix the presence/absence of non-relaxed intervals.
1061 for (const int i : helper.TypeToConstraints(ConstraintProto::kInterval)) {
1062 DCHECK_GE(i, 0);
1063 if (ignored_intervals.contains(i)) continue;
1064
1065 const ConstraintProto& interval_ct = helper.ModelProto().constraints(i);
1066 if (interval_ct.enforcement_literal().empty()) continue;
1067
1068 CHECK_EQ(interval_ct.enforcement_literal().size(), 1);
1069 const int enforcement_ref = interval_ct.enforcement_literal(0);
1070 const int enforcement_var = PositiveRef(enforcement_ref);
1071 const int value = initial_solution.solution(enforcement_var);
1072
1073 // If the interval is not enforced, we just relax it. If it belongs to an
1074 // exactly one constraint, and the enforced interval is not relaxed, then
1075 // propagation will force this interval to stay not enforced. Otherwise,
1076 // LNS will be able to change which interval will be enforced among all
1077 // alternatives.
1078 if (RefIsPositive(enforcement_ref) == (value == 0)) {
1079 ignored_intervals.insert(i);
1080 continue;
1081 }
1082
1083 // Fix the value.
1084 neighborhood.delta.mutable_variables(enforcement_var)->clear_domain();
1085 neighborhood.delta.mutable_variables(enforcement_var)->add_domain(value);
1086 neighborhood.delta.mutable_variables(enforcement_var)->add_domain(value);
1087 }
1088
1089 for (const int c : helper.TypeToConstraints(ConstraintProto::kNoOverlap)) {
1090 AddPrecedenceConstraints(
1091 helper.ModelProto().constraints(c).no_overlap().intervals(),
1092 ignored_intervals, initial_solution, helper, &neighborhood);
1093 }
1094 for (const int c : helper.TypeToConstraints(ConstraintProto::kCumulative)) {
1095 AddPrecedenceConstraints(
1096 helper.ModelProto().constraints(c).cumulative().intervals(),
1097 ignored_intervals, initial_solution, helper, &neighborhood);
1098 }
1099 for (const int c : helper.TypeToConstraints(ConstraintProto::kNoOverlap2D)) {
1100 AddPrecedenceConstraints(
1101 helper.ModelProto().constraints(c).no_overlap_2d().x_intervals(),
1102 ignored_intervals, initial_solution, helper, &neighborhood);
1103 AddPrecedenceConstraints(
1104 helper.ModelProto().constraints(c).no_overlap_2d().y_intervals(),
1105 ignored_intervals, initial_solution, helper, &neighborhood);
1106 }
1107
1108 // Set the current solution as a hint.
1109 helper.AddSolutionHinting(initial_solution, &neighborhood.delta);
1110 neighborhood.is_generated = true;
1111
1112 return neighborhood;
1113}
1114
1116 const CpSolverResponse& initial_solution, double difficulty,
1117 absl::BitGenRef random) {
1118 std::vector<int> intervals_to_relax =
1119 helper_.GetActiveIntervals(initial_solution);
1120 GetRandomSubset(difficulty, &intervals_to_relax, random);
1121
1122 return GenerateSchedulingNeighborhoodForRelaxation(intervals_to_relax,
1123 initial_solution, helper_);
1124}
1125
1127 const CpSolverResponse& initial_solution, double difficulty,
1128 absl::BitGenRef random) {
1129 std::vector<std::pair<int64_t, int>> start_interval_pairs;
1130 const std::vector<int> active_intervals =
1131 helper_.GetActiveIntervals(initial_solution);
1132 std::vector<int> intervals_to_relax;
1133
1134 if (active_intervals.empty()) return helper_.FullNeighborhood();
1135
1136 for (const int i : active_intervals) {
1137 const ConstraintProto& interval_ct = helper_.ModelProto().constraints(i);
1138 const LinearExpressionProto& start_var = interval_ct.interval().start();
1139 const int64_t start_value =
1140 GetLinearExpressionValue(start_var, initial_solution);
1141 start_interval_pairs.push_back({start_value, i});
1142 }
1143 std::sort(start_interval_pairs.begin(), start_interval_pairs.end());
1144 const int relaxed_size = std::floor(difficulty * start_interval_pairs.size());
1145
1146 std::uniform_int_distribution<int> random_var(
1147 0, start_interval_pairs.size() - relaxed_size - 1);
1148 const int random_start_index = random_var(random);
1149
1150 // TODO(user): Consider relaxing more than one time window
1151 // intervals. This seems to help with Giza models.
1152 for (int i = random_start_index; i < relaxed_size; ++i) {
1153 intervals_to_relax.push_back(start_interval_pairs[i].second);
1154 }
1155
1156 return GenerateSchedulingNeighborhoodForRelaxation(intervals_to_relax,
1157 initial_solution, helper_);
1158}
1159
1161 const CpSolverResponse& initial_solution, double difficulty,
1162 absl::BitGenRef random) {
1163 const std::vector<std::vector<int>> all_paths =
1164 helper_.GetRoutingPaths(initial_solution);
1165
1166 // Collect all unique variables.
1167 absl::flat_hash_set<int> all_path_variables;
1168 for (auto& path : all_paths) {
1169 all_path_variables.insert(path.begin(), path.end());
1170 }
1171 std::vector<int> fixed_variables(all_path_variables.begin(),
1172 all_path_variables.end());
1173 std::sort(fixed_variables.begin(), fixed_variables.end());
1174 GetRandomSubset(1.0 - difficulty, &fixed_variables, random);
1176 initial_solution, {fixed_variables.begin(), fixed_variables.end()});
1177}
1178
1180 const CpSolverResponse& initial_solution, double difficulty,
1181 absl::BitGenRef random) {
1182 std::vector<std::vector<int>> all_paths =
1183 helper_.GetRoutingPaths(initial_solution);
1184
1185 // Collect all unique variables.
1186 absl::flat_hash_set<int> all_path_variables;
1187 for (const auto& path : all_paths) {
1188 all_path_variables.insert(path.begin(), path.end());
1189 }
1190
1191 // Select variables to relax.
1192 const int num_variables_to_relax =
1193 static_cast<int>(all_path_variables.size() * difficulty);
1194 absl::flat_hash_set<int> relaxed_variables;
1195 while (relaxed_variables.size() < num_variables_to_relax) {
1196 DCHECK(!all_paths.empty());
1197 const int path_index = absl::Uniform<int>(random, 0, all_paths.size());
1198 std::vector<int>& path = all_paths[path_index];
1199 const int path_size = path.size();
1200 const int segment_length =
1201 std::min(path_size, absl::Uniform<int>(random, 4, 8));
1202 const int segment_start =
1203 absl::Uniform<int>(random, 0, path_size - segment_length);
1204 for (int i = segment_start; i < segment_start + segment_length; ++i) {
1205 relaxed_variables.insert(path[i]);
1206 }
1207
1208 // Remove segment and clean up empty paths.
1209 path.erase(path.begin() + segment_start,
1210 path.begin() + segment_start + segment_length);
1211 if (path.empty()) {
1212 std::swap(all_paths[path_index], all_paths.back());
1213 all_paths.pop_back();
1214 }
1215 }
1216
1217 // Compute the set of variables to fix.
1218 absl::flat_hash_set<int> fixed_variables;
1219 for (const int var : all_path_variables) {
1220 if (!relaxed_variables.contains(var)) fixed_variables.insert(var);
1221 }
1222 return helper_.FixGivenVariables(initial_solution, fixed_variables);
1223}
1224
1226 const CpSolverResponse& initial_solution, double difficulty,
1227 absl::BitGenRef random) {
1228 std::vector<std::vector<int>> all_paths =
1229 helper_.GetRoutingPaths(initial_solution);
1230 // Remove a corner case where all paths are empty.
1231 if (all_paths.empty()) {
1232 return helper_.NoNeighborhood();
1233 }
1234
1235 // Collect all unique variables.
1236 absl::flat_hash_set<int> all_path_variables;
1237 for (const auto& path : all_paths) {
1238 all_path_variables.insert(path.begin(), path.end());
1239 }
1240
1241 // Select variables to relax.
1242 const int num_variables_to_relax =
1243 static_cast<int>(all_path_variables.size() * difficulty);
1244 absl::flat_hash_set<int> relaxed_variables;
1245
1246 // Relax the start and end of each path to ease relocation.
1247 for (const auto& path : all_paths) {
1248 relaxed_variables.insert(path.front());
1249 relaxed_variables.insert(path.back());
1250 }
1251
1252 // Randomize paths.
1253 for (auto& path : all_paths) {
1254 std::shuffle(path.begin(), path.end(), random);
1255 }
1256
1257 // Relax all variables (if possible) in one random path.
1258 const int path_to_clean = absl::Uniform<int>(random, 0, all_paths.size());
1259 while (relaxed_variables.size() < num_variables_to_relax &&
1260 !all_paths[path_to_clean].empty()) {
1261 relaxed_variables.insert(all_paths[path_to_clean].back());
1262 all_paths[path_to_clean].pop_back();
1263 }
1264 if (all_paths[path_to_clean].empty()) {
1265 std::swap(all_paths[path_to_clean], all_paths.back());
1266 all_paths.pop_back();
1267 }
1268
1269 // Relax more variables until the target is reached.
1270 while (relaxed_variables.size() < num_variables_to_relax) {
1271 DCHECK(!all_paths.empty());
1272 const int path_index = absl::Uniform<int>(random, 0, all_paths.size());
1273 relaxed_variables.insert(all_paths[path_index].back());
1274
1275 // Remove variable and clean up empty paths.
1276 all_paths[path_index].pop_back();
1277 if (all_paths[path_index].empty()) {
1278 std::swap(all_paths[path_index], all_paths.back());
1279 all_paths.pop_back();
1280 }
1281 }
1282
1283 // Compute the set of variables to fix.
1284 absl::flat_hash_set<int> fixed_variables;
1285 for (const int var : all_path_variables) {
1286 if (!relaxed_variables.contains(var)) fixed_variables.insert(var);
1287 }
1288 return helper_.FixGivenVariables(initial_solution, fixed_variables);
1289}
1290
1292 if (incomplete_solutions_ != nullptr) {
1293 return incomplete_solutions_->HasNewSolution();
1294 }
1295
1296 if (response_manager_ != nullptr) {
1297 if (response_manager_->SolutionsRepository().NumSolutions() == 0) {
1298 return false;
1299 }
1300 }
1301
1302 // At least one relaxation solution should be available to generate a
1303 // neighborhood.
1304 if (lp_solutions_ != nullptr && lp_solutions_->NumSolutions() > 0) {
1305 return true;
1306 }
1307
1308 if (relaxation_solutions_ != nullptr &&
1309 relaxation_solutions_->NumSolutions() > 0) {
1310 return true;
1311 }
1312 return false;
1313}
1314
1316 const CpSolverResponse& initial_solution, double difficulty,
1317 absl::BitGenRef random) {
1318 Neighborhood neighborhood = helper_.FullNeighborhood();
1319 neighborhood.is_generated = false;
1320
1321 const bool lp_solution_available =
1322 (lp_solutions_ != nullptr && lp_solutions_->NumSolutions() > 0);
1323
1324 const bool relaxation_solution_available =
1325 (relaxation_solutions_ != nullptr &&
1326 relaxation_solutions_->NumSolutions() > 0);
1327
1328 const bool incomplete_solution_available =
1329 (incomplete_solutions_ != nullptr &&
1330 incomplete_solutions_->HasNewSolution());
1331
1332 if (!lp_solution_available && !relaxation_solution_available &&
1333 !incomplete_solution_available) {
1334 return neighborhood;
1335 }
1336
1337 RINSNeighborhood rins_neighborhood;
1338 // Randomly select the type of relaxation if both lp and relaxation solutions
1339 // are available.
1340 // TODO(user): Tune the probability value for this.
1341 std::bernoulli_distribution random_bool(0.5);
1342 const bool use_lp_relaxation =
1343 (lp_solution_available && relaxation_solution_available)
1344 ? random_bool(random)
1345 : lp_solution_available;
1346 if (use_lp_relaxation) {
1347 rins_neighborhood =
1348 GetRINSNeighborhood(response_manager_,
1349 /*relaxation_solutions=*/nullptr, lp_solutions_,
1350 incomplete_solutions_, random);
1351 neighborhood.source_info =
1352 incomplete_solution_available ? "incomplete" : "lp";
1353 } else {
1354 CHECK(relaxation_solution_available || incomplete_solution_available);
1355 rins_neighborhood = GetRINSNeighborhood(
1356 response_manager_, relaxation_solutions_,
1357 /*lp_solutions=*/nullptr, incomplete_solutions_, random);
1358 neighborhood.source_info =
1359 incomplete_solution_available ? "incomplete" : "relaxation";
1360 }
1361
1362 if (rins_neighborhood.fixed_vars.empty() &&
1363 rins_neighborhood.reduced_domain_vars.empty()) {
1364 return neighborhood;
1365 }
1366
1367 absl::ReaderMutexLock graph_lock(&helper_.graph_mutex_);
1368 // Fix the variables in the local model.
1369 for (const std::pair</*model_var*/ int, /*value*/ int64_t>& fixed_var :
1370 rins_neighborhood.fixed_vars) {
1371 const int var = fixed_var.first;
1372 const int64_t value = fixed_var.second;
1373 if (var >= neighborhood.delta.variables_size()) continue;
1374 if (!helper_.IsActive(var)) continue;
1375
1376 if (!DomainInProtoContains(neighborhood.delta.variables(var), value)) {
1377 // TODO(user): Instead of aborting, pick the closest point in the domain?
1378 return neighborhood;
1379 }
1380
1381 neighborhood.delta.mutable_variables(var)->clear_domain();
1382 neighborhood.delta.mutable_variables(var)->add_domain(value);
1383 neighborhood.delta.mutable_variables(var)->add_domain(value);
1384 neighborhood.is_reduced = true;
1385 }
1386
1387 for (const std::pair</*model_var*/ int,
1388 /*domain*/ std::pair<int64_t, int64_t>>& reduced_var :
1389 rins_neighborhood.reduced_domain_vars) {
1390 const int var = reduced_var.first;
1391 const int64_t lb = reduced_var.second.first;
1392 const int64_t ub = reduced_var.second.second;
1393 if (var >= neighborhood.delta.variables_size()) continue;
1394 if (!helper_.IsActive(var)) continue;
1395 Domain domain = ReadDomainFromProto(neighborhood.delta.variables(var));
1396 domain = domain.IntersectionWith(Domain(lb, ub));
1397 if (domain.IsEmpty()) {
1398 // TODO(user): Instead of aborting, pick the closest point in the domain?
1399 return neighborhood;
1400 }
1401 FillDomainInProto(domain, neighborhood.delta.mutable_variables(var));
1402 neighborhood.is_reduced = true;
1403 }
1404 neighborhood.is_generated = true;
1405 return neighborhood;
1406}
1407
1409 const CpSolverResponse& initial_solution, double difficulty,
1410 absl::BitGenRef random) {
1411 std::vector<int> removable_constraints;
1412 const int num_constraints = helper_.ModelProto().constraints_size();
1413 removable_constraints.reserve(num_constraints);
1414 for (int c = 0; c < num_constraints; ++c) {
1415 // Removing intervals is not easy because other constraint might require
1416 // them, so for now, we don't remove them.
1417 if (helper_.ModelProto().constraints(c).constraint_case() ==
1418 ConstraintProto::kInterval) {
1419 continue;
1420 }
1421 removable_constraints.push_back(c);
1422 }
1423
1424 const int target_size =
1425 std::round((1.0 - difficulty) * removable_constraints.size());
1426
1427 const int random_start_index =
1428 absl::Uniform<int>(random, 0, removable_constraints.size());
1429 std::vector<int> removed_constraints;
1430 removed_constraints.reserve(target_size);
1431 int c = random_start_index;
1432 while (removed_constraints.size() < target_size) {
1433 removed_constraints.push_back(removable_constraints[c]);
1434 ++c;
1435 if (c == removable_constraints.size()) {
1436 c = 0;
1437 }
1438 }
1439
1440 return helper_.RemoveMarkedConstraints(removed_constraints);
1441}
1442
1445 NeighborhoodGeneratorHelper const* helper, const std::string& name)
1446 : NeighborhoodGenerator(name, helper) {
1447 std::vector<int> removable_constraints;
1448 const int num_constraints = helper_.ModelProto().constraints_size();
1449 constraint_weights_.reserve(num_constraints);
1450 // TODO(user): Experiment with different starting weights.
1451 for (int c = 0; c < num_constraints; ++c) {
1452 switch (helper_.ModelProto().constraints(c).constraint_case()) {
1453 case ConstraintProto::kCumulative:
1454 case ConstraintProto::kAllDiff:
1455 case ConstraintProto::kElement:
1456 case ConstraintProto::kRoutes:
1457 case ConstraintProto::kCircuit:
1458 constraint_weights_.push_back(3.0);
1459 num_removable_constraints_++;
1460 break;
1461 case ConstraintProto::kBoolOr:
1462 case ConstraintProto::kBoolAnd:
1463 case ConstraintProto::kBoolXor:
1464 case ConstraintProto::kIntProd:
1465 case ConstraintProto::kIntDiv:
1466 case ConstraintProto::kIntMod:
1467 case ConstraintProto::kLinMax:
1468 case ConstraintProto::kNoOverlap:
1469 case ConstraintProto::kNoOverlap2D:
1470 constraint_weights_.push_back(2.0);
1471 num_removable_constraints_++;
1472 break;
1473 case ConstraintProto::kLinear:
1474 case ConstraintProto::kTable:
1475 case ConstraintProto::kAutomaton:
1476 case ConstraintProto::kInverse:
1477 case ConstraintProto::kReservoir:
1478 case ConstraintProto::kAtMostOne:
1479 case ConstraintProto::kExactlyOne:
1480 constraint_weights_.push_back(1.0);
1481 num_removable_constraints_++;
1482 break;
1483 case ConstraintProto::CONSTRAINT_NOT_SET:
1484 case ConstraintProto::kDummyConstraint:
1485 case ConstraintProto::kInterval:
1486 // Removing intervals is not easy because other constraint might require
1487 // them, so for now, we don't remove them.
1488 constraint_weights_.push_back(0.0);
1489 break;
1490 }
1491 }
1492}
1493
1494void WeightedRandomRelaxationNeighborhoodGenerator::
1495 AdditionalProcessingOnSynchronize(const SolveData& solve_data) {
1496 const IntegerValue best_objective_improvement =
1497 solve_data.new_objective_bound - solve_data.initial_best_objective_bound;
1498
1499 const std::vector<int>& removed_constraints =
1500 removed_constraints_[solve_data.neighborhood_id];
1501
1502 // Heuristic: We change the weights of the removed constraints if the
1503 // neighborhood is solved (status is OPTIMAL or INFEASIBLE) or we observe an
1504 // improvement in objective bounds. Otherwise we assume that the
1505 // difficulty/time wasn't right for us to record feedbacks.
1506 //
1507 // If the objective bounds are improved, we bump up the weights. If the
1508 // objective bounds are worse and the problem status is OPTIMAL, we bump down
1509 // the weights. Otherwise if the new objective bounds are same as current
1510 // bounds (which happens a lot on some instances), we do not update the
1511 // weights as we do not have a clear signal whether the constraints removed
1512 // were good choices or not.
1513 // TODO(user): We can improve this heuristic with more experiments.
1514 if (best_objective_improvement > 0) {
1515 // Bump up the weights of all removed constraints.
1516 for (int c : removed_constraints) {
1517 if (constraint_weights_[c] <= 90.0) {
1518 constraint_weights_[c] += 10.0;
1519 } else {
1520 constraint_weights_[c] = 100.0;
1521 }
1522 }
1523 } else if (solve_data.status == CpSolverStatus::OPTIMAL &&
1524 best_objective_improvement < 0) {
1525 // Bump down the weights of all removed constraints.
1526 for (int c : removed_constraints) {
1527 if (constraint_weights_[c] > 0.5) {
1528 constraint_weights_[c] -= 0.5;
1529 }
1530 }
1531 }
1532 removed_constraints_.erase(solve_data.neighborhood_id);
1533}
1534
1536 const CpSolverResponse& initial_solution, double difficulty,
1537 absl::BitGenRef random) {
1538 const int target_size =
1539 std::round((1.0 - difficulty) * num_removable_constraints_);
1540
1541 std::vector<int> removed_constraints;
1542
1543 // Generate a random number between (0,1) = u[i] and use score[i] =
1544 // u[i]^(1/w[i]) and then select top k items with largest scores.
1545 // Reference: https://utopia.duth.gr/~pefraimi/research/data/2007EncOfAlg.pdf
1546 std::vector<std::pair<double, int>> constraint_removal_scores;
1547 std::uniform_real_distribution<double> random_var(0.0, 1.0);
1548 for (int c = 0; c < constraint_weights_.size(); ++c) {
1549 if (constraint_weights_[c] <= 0) continue;
1550 const double u = random_var(random);
1551 const double score = std::pow(u, (1 / constraint_weights_[c]));
1552 constraint_removal_scores.push_back({score, c});
1553 }
1554 std::sort(constraint_removal_scores.rbegin(),
1555 constraint_removal_scores.rend());
1556 for (int i = 0; i < target_size; ++i) {
1557 removed_constraints.push_back(constraint_removal_scores[i].second);
1558 }
1559
1560 Neighborhood result = helper_.RemoveMarkedConstraints(removed_constraints);
1561
1562 absl::MutexLock mutex_lock(&generator_mutex_);
1563 result.id = next_available_id_;
1564 next_available_id_++;
1565 removed_constraints_.insert({result.id, removed_constraints});
1566 return result;
1567}
1568
1569} // namespace sat
1570} // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
int64_t min
Definition: alldiff_cst.cc:139
#define CHECK(condition)
Definition: base/logging.h:495
#define CHECK_LT(val1, val2)
Definition: base/logging.h:706
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:703
#define CHECK_GE(val1, val2)
Definition: base/logging.h:707
#define CHECK_GT(val1, val2)
Definition: base/logging.h:708
#define DCHECK_GE(val1, val2)
Definition: base/logging.h:895
#define DCHECK(condition)
Definition: base/logging.h:890
#define CHECK_LE(val1, val2)
Definition: base/logging.h:705
#define VLOG(verboselevel)
Definition: base/logging.h:984
bool AddEdge(int node1, int node2)
void Update(int num_decreases, int num_increases)
We call domain any subset of Int64 = [kint64min, kint64max].
bool IsIncludedIn(const Domain &domain) const
Returns true iff D is included in the given domain.
bool Contains(int64_t value) const
Returns true iff value is in Domain.
Domain UnionWith(const Domain &domain) const
Returns the union of D and domain.
bool IsFixed() const
Returns true iff the domain is reduced to a single value.
Domain IntersectionWith(const Domain &domain) const
Returns the intersection of D and domain.
bool IsEmpty() const
Returns true if this is the empty set.
int64_t Max() const
Returns the max value of the domain.
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
NeighborhoodGeneratorHelper(CpModelProto const *model_proto, SatParameters const *parameters, SharedResponseManager *shared_response, SharedTimeLimit *shared_time_limit=nullptr, SharedBoundsManager *shared_bounds=nullptr)
Definition: cp_model_lns.cc:57
Neighborhood FixAllVariables(const CpSolverResponse &initial_solution) const
const absl::Span< const int > TypeToConstraints(ConstraintProto::ConstraintCase type) const
Definition: cp_model_lns.h:187
Neighborhood FixGivenVariables(const CpSolverResponse &base_solution, const absl::flat_hash_set< int > &variables_to_fix) const
bool DifficultyMeansFullNeighborhood(double difficulty) const
Definition: cp_model_lns.h:162
Neighborhood RelaxGivenVariables(const CpSolverResponse &initial_solution, const std::vector< int > &relaxed_variables) const
std::vector< int > GetActiveIntervals(const CpSolverResponse &initial_solution) const
const SharedResponseManager & shared_response() const
Definition: cp_model_lns.h:213
const std::vector< std::vector< int > > & VarToConstraint() const ABSL_SHARED_LOCKS_REQUIRED(graph_mutex_)
Definition: cp_model_lns.h:181
bool IsActive(int var) const ABSL_SHARED_LOCKS_REQUIRED(graph_mutex_)
const std::vector< int > & ActiveVariablesWhileHoldingLock() const ABSL_SHARED_LOCKS_REQUIRED(graph_mutex_)
Definition: cp_model_lns.h:170
Neighborhood RemoveMarkedConstraints(const std::vector< int > &constraints_to_remove) const
void AddSolutionHinting(const CpSolverResponse &initial_solution, CpModelProto *model_proto) const
const std::vector< std::vector< int > > & ConstraintToVar() const ABSL_SHARED_LOCKS_REQUIRED(graph_mutex_)
Definition: cp_model_lns.h:177
std::vector< std::vector< int > > GetRoutingPaths(const CpSolverResponse &initial_solution) const
double GetUCBScore(int64_t total_num_calls) const
virtual void AdditionalProcessingOnSynchronize(const SolveData &solve_data)
Definition: cp_model_lns.h:437
const NeighborhoodGeneratorHelper & helper_
Definition: cp_model_lns.h:440
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
void GetChangedBounds(int id, std::vector< int > *variables, std::vector< int64_t > *new_lower_bounds, std::vector< int64_t > *new_upper_bounds)
void LogPeriodicMessage(const std::string &prefix, const std::string &message, absl::Time *last_logging_time)
const SharedSolutionRepository< int64_t > & SolutionsRepository() const
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
WeightedRandomRelaxationNeighborhoodGenerator(NeighborhoodGeneratorHelper const *helper, const std::string &name)
Neighborhood Generate(const CpSolverResponse &initial_solution, double difficulty, absl::BitGenRef random) final
SatParameters parameters
CpModelProto const * model_proto
const std::string name
const Constraint * ct
int64_t value
IntVar * var
Definition: expr_array.cc:1874
GurobiMPCallbackContext * context
int index
const bool DEBUG_MODE
Definition: macros.h:24
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
Definition: id_map.h:262
std::vector< int > UsedVariables(const ConstraintProto &ct)
bool RefIsPositive(int ref)
std::vector< int > UsedIntervals(const ConstraintProto &ct)
bool DomainInProtoContains(const ProtoWithDomain &proto, int64_t value)
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
Neighborhood GenerateSchedulingNeighborhoodForRelaxation(const absl::Span< const int > intervals_to_relax, const CpSolverResponse &initial_solution, const NeighborhoodGeneratorHelper &helper)
RINSNeighborhood GetRINSNeighborhood(const SharedResponseManager *response_manager, const SharedRelaxationSolutionRepository *relaxation_solutions, const SharedLPSolutionRepository *lp_solutions, SharedIncompleteSolutionManager *incomplete_solutions, absl::BitGenRef random)
Definition: rins.cc:107
Collection of objects used to extend the Constraint Solver library.
int64_t CapSub(int64_t x, int64_t y)
Literal literal
Definition: optimization.cc:89
IntervalVar * interval
Definition: resource.cc:100
int64_t tail
int64_t head
std::vector< int > variables_that_can_be_fixed_to_local_optimum
Definition: cp_model_lns.h:90
std::vector< int > constraints_to_ignore
Definition: cp_model_lns.h:65
std::vector< std::pair< int, int64_t > > fixed_vars
Definition: rins.h:61
std::vector< std::pair< int, std::pair< int64_t, int64_t > > > reduced_domain_vars
Definition: rins.h:64
const double coeff
#define VLOG_IS_ON(verboselevel)
Definition: vlog_is_on.h:44