Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
cp_model_postsolve.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cstdint>
18#include <limits>
19#include <vector>
20
21#include "absl/log/check.h"
24#include "ortools/sat/cp_model.pb.h"
28
29namespace operations_research {
30namespace sat {
31
32// This postsolve is "special". If the clause is not satisfied, we fix the
33// first literal in the clause to true (even if it was fixed to false). This
34// allows to handle more complex presolve operations used by the SAT presolver.
35//
36// Also, any "free" Boolean should be fixed to some value for the subsequent
37// postsolve steps.
38void PostsolveClause(const ConstraintProto& ct, std::vector<Domain>* domains) {
39 const int size = ct.bool_or().literals_size();
40 CHECK_NE(size, 0);
41 bool satisfied = false;
42 for (int i = 0; i < size; ++i) {
43 const int ref = ct.bool_or().literals(i);
44 const int var = PositiveRef(ref);
45 if ((*domains)[var].IsFixed()) {
46 if ((*domains)[var].FixedValue() == (RefIsPositive(ref) ? 1 : 0)) {
47 satisfied = true;
48 }
49 } else {
50 // We still need to assign free variable. Any value should work.
51 (*domains)[PositiveRef(ref)] = Domain(0);
52 }
53 }
54 if (satisfied) return;
55
56 // Change the value of the first variable (which was chosen at presolve).
57 const int first_ref = ct.bool_or().literals(0);
58 (*domains)[PositiveRef(first_ref)] = Domain(RefIsPositive(first_ref) ? 1 : 0);
59}
60
61void PostsolveExactlyOne(const ConstraintProto& ct,
62 std::vector<Domain>* domains) {
63 bool satisfied = false;
64 std::vector<int> free_variables;
65 for (const int ref : ct.exactly_one().literals()) {
66 const int var = PositiveRef(ref);
67 if ((*domains)[var].IsFixed()) {
68 if ((*domains)[var].FixedValue() == (RefIsPositive(ref) ? 1 : 0)) {
69 CHECK(!satisfied) << "Two variables at one in exactly one.";
70 satisfied = true;
71 }
72 } else {
73 free_variables.push_back(ref);
74 }
75 }
76 if (!satisfied) {
77 // Fix one at true.
78 CHECK(!free_variables.empty()) << "All zero in exactly one";
79 const int ref = free_variables.back();
80 (*domains)[PositiveRef(ref)] = Domain(RefIsPositive(ref) ? 1 : 0);
81 free_variables.pop_back();
82 }
83
84 // Fix any free variable left at false.
85 for (const int ref : free_variables) {
86 (*domains)[PositiveRef(ref)] = Domain(RefIsPositive(ref) ? 0 : 1);
87 }
88}
89
90// For now we set the first unset enforcement literal to false.
91// There must be one.
92void SetEnforcementLiteralToFalse(const ConstraintProto& ct,
93 std::vector<Domain>* domains) {
94 CHECK(!ct.enforcement_literal().empty());
95 bool has_free_enforcement_literal = false;
96 for (const int enf : ct.enforcement_literal()) {
97 if ((*domains)[PositiveRef(enf)].IsFixed()) continue;
98 has_free_enforcement_literal = true;
99 if (RefIsPositive(enf)) {
100 (*domains)[enf] = Domain(0);
101 } else {
102 (*domains)[PositiveRef(enf)] = Domain(1);
103 }
104 break;
105 }
106 if (!has_free_enforcement_literal) {
107 LOG(FATAL)
108 << "Unsatisfied linear constraint with no free enforcement literal: "
110 }
111}
112
113// Here we simply assign all non-fixed variable to a feasible value. Which
114// should always exists by construction.
115void PostsolveLinear(const ConstraintProto& ct, std::vector<Domain>* domains) {
116 int64_t fixed_activity = 0;
117 const int size = ct.linear().vars().size();
118 std::vector<int> free_vars;
119 std::vector<int64_t> free_coeffs;
120 for (int i = 0; i < size; ++i) {
121 const int var = ct.linear().vars(i);
122 const int64_t coeff = ct.linear().coeffs(i);
123 CHECK_LT(var, domains->size());
124 if (coeff == 0) continue;
125 if ((*domains)[var].IsFixed()) {
126 fixed_activity += (*domains)[var].FixedValue() * coeff;
127 } else {
128 free_vars.push_back(var);
129 free_coeffs.push_back(coeff);
130 }
131 }
132 if (free_vars.empty()) {
133 const Domain rhs = ReadDomainFromProto(ct.linear());
134 if (!rhs.Contains(fixed_activity)) {
136 }
137 return;
138 }
139
140 // Fast track for the most common case.
141 const Domain initial_rhs = ReadDomainFromProto(ct.linear());
142 if (free_vars.size() == 1) {
143 const int var = free_vars[0];
144 const Domain domain = initial_rhs.AdditionWith(Domain(-fixed_activity))
145 .InverseMultiplicationBy(free_coeffs[0])
146 .IntersectionWith((*domains)[var]);
147 if (domain.IsEmpty()) {
149 return;
150 }
151 (*domains)[var] = Domain(domain.SmallestValue());
152 return;
153 }
154
155 // The postsolve code is a bit involved if there is more than one free
156 // variable, we have to postsolve them one by one.
157 //
158 // Here we recompute the same domains as during the presolve. Everything is
159 // like if we where substiting the variable one by one:
160 // terms[i] + fixed_activity \in rhs_domains[i]
161 // In the reverse order.
162 std::vector<Domain> rhs_domains;
163 rhs_domains.push_back(initial_rhs);
164 for (int i = 0; i + 1 < free_vars.size(); ++i) {
165 // Note that these should be exactly the same computation as the one done
166 // during presolve and should be exact. However, we have some tests that do
167 // not comply, so we don't check exactness here. Also, as long as we don't
168 // get empty domain below, and the complexity of the domain do not explode
169 // here, we should be fine.
170 Domain term = (*domains)[free_vars[i]].MultiplicationBy(-free_coeffs[i]);
171 rhs_domains.push_back(term.AdditionWith(rhs_domains.back()));
172 }
173 for (int i = free_vars.size() - 1; i >= 0; --i) {
174 // Choose a value for free_vars[i] that fall into rhs_domains[i] -
175 // fixed_activity. This will crash if the intersection is empty, but it
176 // shouldn't be.
177 const int var = free_vars[i];
178 const int64_t coeff = free_coeffs[i];
179 const Domain domain = rhs_domains[i]
180 .AdditionWith(Domain(-fixed_activity))
181 .InverseMultiplicationBy(coeff)
182 .IntersectionWith((*domains)[var]);
183
184 // TODO(user): I am not 100% that the algo here might cover all the presolve
185 // case, so if this fail, it might indicate an issue here and not in the
186 // presolve/solver code.
187 CHECK(!domain.IsEmpty()) << ProtobufShortDebugString(ct);
188 const int64_t value = domain.SmallestValue();
189 (*domains)[var] = Domain(value);
190
191 fixed_activity += coeff * value;
192 }
193 DCHECK(initial_rhs.Contains(fixed_activity));
194}
195
196namespace {
197
198// Note that if a domain is not fixed, we just take its Min() value.
199int64_t EvaluateLinearExpression(const LinearExpressionProto& expr,
200 const std::vector<Domain>& domains) {
201 int64_t value = expr.offset();
202 for (int i = 0; i < expr.vars_size(); ++i) {
203 const int ref = expr.vars(i);
204 const int64_t increment =
205 domains[PositiveRef(expr.vars(i))].Min() * expr.coeffs(i);
206 value += RefIsPositive(ref) ? increment : -increment;
207 }
208 return value;
209}
210
211} // namespace
212
213// Compute the max of each expression, and assign it to the target expr. We only
214// support post-solving the case where whatever the value of all expression,
215// there will be a valid target.
216void PostsolveLinMax(const ConstraintProto& ct, std::vector<Domain>* domains) {
217 int64_t max_value = std::numeric_limits<int64_t>::min();
218 for (const LinearExpressionProto& expr : ct.lin_max().exprs()) {
219 // In most case all expression are fixed, except in the corner case where
220 // one of the expression refer to the target itself !
221 max_value = std::max(max_value, EvaluateLinearExpression(expr, *domains));
222 }
223
224 const LinearExpressionProto& target = ct.lin_max().target();
225 CHECK_EQ(target.vars().size(), 1);
226 CHECK(RefIsPositive(target.vars(0)));
227
228 max_value -= target.offset();
229 CHECK_EQ(max_value % target.coeffs(0), 0);
230 max_value /= target.coeffs(0);
231 (*domains)[target.vars(0)] = Domain(max_value);
232}
233
234// We only support 3 cases in the presolve currently.
235void PostsolveElement(const ConstraintProto& ct, std::vector<Domain>* domains) {
236 const int index_ref = ct.element().index();
237 const int index_var = PositiveRef(index_ref);
238 const int target_ref = ct.element().target();
239 const int target_var = PositiveRef(target_ref);
240
241 // Deal with non-fixed target and non-fixed index. This only happen if
242 // whatever the value of the index and selected variable, we can choose a
243 // valid target, so we just fix the index to its min value in this case.
244 if (!(*domains)[target_var].IsFixed() && !(*domains)[index_var].IsFixed()) {
245 const int64_t index_var_value = (*domains)[index_var].Min();
246 (*domains)[index_var] = Domain(index_var_value);
247
248 // If the selected variable is not fixed, we also need to fix it.
249 const int selected_ref = ct.element().vars(
250 RefIsPositive(index_ref) ? index_var_value : -index_var_value);
251 const int selected_var = PositiveRef(selected_ref);
252 if (!(*domains)[selected_var].IsFixed()) {
253 (*domains)[selected_var] = Domain((*domains)[selected_var].Min());
254 }
255 }
256
257 // Deal with fixed index.
258 if ((*domains)[index_var].IsFixed()) {
259 const int64_t index_var_value = (*domains)[index_var].FixedValue();
260 const int selected_ref = ct.element().vars(
261 RefIsPositive(index_ref) ? index_var_value : -index_var_value);
262 const int selected_var = PositiveRef(selected_ref);
263 if ((*domains)[selected_var].IsFixed()) {
264 const int64_t selected_value = (*domains)[selected_var].FixedValue();
265 (*domains)[target_var] = (*domains)[target_var].IntersectionWith(
266 Domain(RefIsPositive(target_ref) == RefIsPositive(selected_ref)
267 ? selected_value
268 : -selected_value));
269 DCHECK(!(*domains)[target_var].IsEmpty());
270 } else {
271 const bool same_sign =
272 (selected_var == selected_ref) == (target_var == target_ref);
273 const Domain target_domain = (*domains)[target_var];
274 const Domain selected_domain = same_sign
275 ? (*domains)[selected_var]
276 : (*domains)[selected_var].Negation();
277 const Domain final = target_domain.IntersectionWith(selected_domain);
278 const int64_t value = final.SmallestValue();
279 (*domains)[target_var] =
280 (*domains)[target_var].IntersectionWith(Domain(value));
281 (*domains)[selected_var] = (*domains)[selected_var].IntersectionWith(
282 Domain(same_sign ? value : -value));
283 DCHECK(!(*domains)[target_var].IsEmpty());
284 DCHECK(!(*domains)[selected_var].IsEmpty());
285 }
286 return;
287 }
288
289 // Deal with fixed target (and constant vars).
290 const int64_t target_value = (*domains)[target_var].FixedValue();
291 int selected_index_value = -1;
292 for (const int64_t v : (*domains)[index_var].Values()) {
293 const int64_t i = index_var == index_ref ? v : -v;
294 if (i < 0 || i >= ct.element().vars_size()) continue;
295
296 const int ref = ct.element().vars(i);
297 const int var = PositiveRef(ref);
298 const int64_t value = (*domains)[var].FixedValue();
299 if (RefIsPositive(target_ref) == RefIsPositive(ref)) {
300 if (value == target_value) {
301 selected_index_value = i;
302 break;
303 }
304 } else {
305 if (value == -target_value) {
306 selected_index_value = i;
307 break;
308 }
309 }
310 }
311
312 CHECK_NE(selected_index_value, -1);
313 (*domains)[index_var] = (*domains)[index_var].IntersectionWith(Domain(
314 RefIsPositive(index_ref) ? selected_index_value : -selected_index_value));
315 DCHECK(!(*domains)[index_var].IsEmpty());
316}
317
318// We only support assigning to an affine target.
319void PostsolveIntMod(const ConstraintProto& ct, std::vector<Domain>* domains) {
320 const int64_t exp = EvaluateLinearExpression(ct.int_mod().exprs(0), *domains);
321 const int64_t mod = EvaluateLinearExpression(ct.int_mod().exprs(1), *domains);
322 CHECK_NE(mod, 0);
323 const int64_t target_value = exp % mod;
324
325 const LinearExpressionProto& target = ct.int_mod().target();
326 CHECK_EQ(target.vars().size(), 1);
327 const int64_t term_value = target_value - target.offset();
328 CHECK_EQ(term_value % target.coeffs(0), 0);
329 const int64_t value = term_value / target.coeffs(0);
330 CHECK((*domains)[target.vars(0)].Contains(value));
331 (*domains)[target.vars(0)] = Domain(value);
332}
333
334void PostsolveResponse(const int64_t num_variables_in_original_model,
335 const CpModelProto& mapping_proto,
336 const std::vector<int>& postsolve_mapping,
337 std::vector<int64_t>* solution) {
338 CHECK_EQ(solution->size(), postsolve_mapping.size());
339
340 // Read the initial variable domains, either from the fixed solution of the
341 // presolved problems or from the mapping model.
342 std::vector<Domain> domains(mapping_proto.variables_size());
343 for (int i = 0; i < postsolve_mapping.size(); ++i) {
344 CHECK_LE(postsolve_mapping[i], domains.size());
345 domains[postsolve_mapping[i]] = Domain((*solution)[i]);
346 }
347 for (int i = 0; i < domains.size(); ++i) {
348 if (domains[i].IsEmpty()) {
349 domains[i] = ReadDomainFromProto(mapping_proto.variables(i));
350 }
351 CHECK(!domains[i].IsEmpty());
352 }
353
354 // Process the constraints in reverse order.
355 const int num_constraints = mapping_proto.constraints_size();
356 for (int i = num_constraints - 1; i >= 0; i--) {
357 const ConstraintProto& ct = mapping_proto.constraints(i);
358
359 // We ignore constraint with an enforcement literal set to false. If the
360 // enforcement is still unclear, we still process this constraint.
361 bool constraint_can_be_ignored = false;
362 for (const int enf : ct.enforcement_literal()) {
363 const int var = PositiveRef(enf);
364 const bool is_false =
365 domains[var].IsFixed() &&
366 RefIsPositive(enf) == (domains[var].FixedValue() == 0);
367 if (is_false) {
368 constraint_can_be_ignored = true;
369 break;
370 }
371 }
372 if (constraint_can_be_ignored) continue;
373
374 switch (ct.constraint_case()) {
375 case ConstraintProto::kBoolOr:
376 PostsolveClause(ct, &domains);
377 break;
378 case ConstraintProto::kExactlyOne:
379 PostsolveExactlyOne(ct, &domains);
380 break;
381 case ConstraintProto::kLinear:
382 PostsolveLinear(ct, &domains);
383 break;
384 case ConstraintProto::kLinMax:
385 PostsolveLinMax(ct, &domains);
386 break;
387 case ConstraintProto::kElement:
388 PostsolveElement(ct, &domains);
389 break;
390 case ConstraintProto::kIntMod:
391 PostsolveIntMod(ct, &domains);
392 break;
393 default:
394 // This should never happen as we control what kind of constraint we
395 // add to the mapping_proto;
396 LOG(FATAL) << "Unsupported constraint: "
398 }
399 }
400
401 // Fill the response.
402 // Maybe fix some still unfixed variable.
403 solution->clear();
404 CHECK_LE(num_variables_in_original_model, domains.size());
405 for (int i = 0; i < num_variables_in_original_model; ++i) {
406 solution->push_back(domains[i].SmallestValue());
407 }
408}
409
410void FillTightenedDomainInResponse(const CpModelProto& original_model,
411 const CpModelProto& mapping_proto,
412 const std::vector<int>& postsolve_mapping,
413 const std::vector<Domain>& search_domains,
414 CpSolverResponse* response,
415 SolverLogger* logger) {
416 // The [0, num_vars) part will contain the tightened domains.
417 const int num_original_vars = original_model.variables().size();
418 const int num_expanded_vars = mapping_proto.variables().size();
419 CHECK_LE(num_original_vars, num_expanded_vars);
420 std::vector<Domain> domains(num_expanded_vars);
421
422 // Start with the domain from the mapping proto. Note that by construction
423 // this should be tighter than the original variable domains.
424 for (int i = 0; i < num_expanded_vars; ++i) {
425 domains[i] = ReadDomainFromProto(mapping_proto.variables(i));
426 if (i < num_original_vars) {
427 CHECK(domains[i].IsIncludedIn(
428 ReadDomainFromProto(original_model.variables(i))));
429 }
430 }
431
432 // The first test is for the corner case of presolve closing the problem,
433 // in which case there is no more info to process.
434 int num_common_vars = 0;
435 int num_affine_reductions = 0;
436 if (!search_domains.empty()) {
437 if (postsolve_mapping.empty()) {
438 // Currently no mapping should mean all variables are in common. This
439 // happen when presolve is disabled, but we might still have more
440 // variables due to expansion for instance.
441 //
442 // There is also the corner case of presolve closing the problem,
443 CHECK_GE(search_domains.size(), num_original_vars);
444 num_common_vars = num_original_vars;
445 for (int i = 0; i < num_original_vars; ++i) {
446 domains[i] = domains[i].IntersectionWith(search_domains[i]);
447 }
448 } else {
449 // This is the normal presolve case.
450 // Intersect the domain of the variables in common.
451 CHECK_EQ(postsolve_mapping.size(), search_domains.size());
452 for (int search_i = 0; search_i < postsolve_mapping.size(); ++search_i) {
453 const int i_in_mapping_model = postsolve_mapping[search_i];
454 if (i_in_mapping_model < num_original_vars) {
455 ++num_common_vars;
456 }
457 domains[i_in_mapping_model] =
458 domains[i_in_mapping_model].IntersectionWith(
459 search_domains[search_i]);
460 }
461
462 // Look for affine relation, and do more intersection.
463 for (const ConstraintProto& ct : mapping_proto.constraints()) {
464 if (ct.constraint_case() != ConstraintProto::kLinear) continue;
465 const LinearConstraintProto& lin = ct.linear();
466 if (lin.vars().size() != 2) continue;
467 if (lin.domain().size() != 2) continue;
468 if (lin.domain(0) != lin.domain(1)) continue;
469 int v1 = lin.vars(0);
470 int v2 = lin.vars(1);
471 int c1 = lin.coeffs(0);
472 int c2 = lin.coeffs(1);
473 if (v2 < num_original_vars && v1 >= num_original_vars) {
474 std::swap(v1, v2);
475 std::swap(c1, c2);
476 }
477 if (v1 < num_original_vars && v2 >= num_original_vars) {
478 // We can reduce the domain of v1 by using the affine relation
479 // and the domain of v2.
480 // We have c1 * v2 + c2 * v2 = offset;
481 const int64_t offset = lin.domain(0);
482 const Domain restriction =
483 Domain(offset)
484 .AdditionWith(domains[v2].ContinuousMultiplicationBy(-c2))
486 if (!domains[v1].IsIncludedIn(restriction)) {
487 ++num_affine_reductions;
488 domains[v1] = domains[v1].IntersectionWith(restriction);
489 }
490 }
491 }
492 }
493 }
494
495 // Copy the names and replace domains.
496 *response->mutable_tightened_variables() = original_model.variables();
497 int num_tigher_domains = 0;
498 int num_empty = 0;
499 int num_fixed = 0;
500 for (int i = 0; i < num_original_vars; ++i) {
501 FillDomainInProto(domains[i], response->mutable_tightened_variables(i));
502 if (domains[i].IsEmpty()) {
503 ++num_empty;
504 continue;
505 }
506
507 if (domains[i].IsFixed()) num_fixed++;
508 const Domain original = ReadDomainFromProto(original_model.variables(i));
509 if (domains[i] != original) {
510 DCHECK(domains[i].IsIncludedIn(original));
511 ++num_tigher_domains;
512 }
513 }
514
515 // Some stats.
516 if (num_empty > 0) {
517 SOLVER_LOG(logger, num_empty,
518 " tightened domains are empty. This should not happen except if "
519 "we proven infeasibility or optimality.");
520 }
521 SOLVER_LOG(logger, "Filled tightened domains in the response.");
522 SOLVER_LOG(logger, "[TighteningInfo] num_tighter:", num_tigher_domains,
523 " num_fixed:", num_fixed,
524 " num_affine_reductions:", num_affine_reductions);
525 SOLVER_LOG(logger,
526 "[TighteningInfo] original_num_variables:", num_original_vars,
527 " during_presolve:", num_expanded_vars,
528 " after:", search_domains.size(), " in_common:", num_common_vars);
529 SOLVER_LOG(logger, "");
530}
531
532} // namespace sat
533} // namespace operations_research
IntegerValue size
Domain MultiplicationBy(int64_t coeff, bool *exact=nullptr) const
Domain IntersectionWith(const Domain &domain) const
bool Contains(int64_t value) const
Domain AdditionWith(const Domain &domain) const
Domain InverseMultiplicationBy(int64_t coeff) const
const Constraint * ct
int64_t value
IntVar * var
double solution
void PostsolveElement(const ConstraintProto &ct, std::vector< Domain > *domains)
We only support 3 cases in the presolve currently.
void PostsolveLinear(const ConstraintProto &ct, std::vector< Domain > *domains)
void FillTightenedDomainInResponse(const CpModelProto &original_model, const CpModelProto &mapping_proto, const std::vector< int > &postsolve_mapping, const std::vector< Domain > &search_domains, CpSolverResponse *response, SolverLogger *logger)
void PostsolveResponse(const int64_t num_variables_in_original_model, const CpModelProto &mapping_proto, const std::vector< int > &postsolve_mapping, std::vector< int64_t > *solution)
std::function< bool(const Model &)> IsFixed(IntegerVariable v)
Definition integer.h:1967
void PostsolveExactlyOne(const ConstraintProto &ct, std::vector< Domain > *domains)
void SetEnforcementLiteralToFalse(const ConstraintProto &ct, std::vector< Domain > *domains)
void PostsolveLinMax(const ConstraintProto &ct, std::vector< Domain > *domains)
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
Serializes a Domain into the domain field of a proto.
void PostsolveIntMod(const ConstraintProto &ct, std::vector< Domain > *domains)
We only support assigning to an affine target.
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
Reads a Domain from the domain field of a proto.
void PostsolveClause(const ConstraintProto &ct, std::vector< Domain > *domains)
In SWIG mode, we don't want anything besides these top-level includes.
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
#define SOLVER_LOG(logger,...)
Definition logging.h:109