Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
logical_constraint_tests.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 <memory>
17#include <ostream>
18#include <utility>
19
20#include "absl/status/status.h"
21#include "absl/strings/string_view.h"
22#include "gtest/gtest.h"
23#include "ortools/base/gmock.h"
29
31
55
56std::ostream& operator<<(std::ostream& out,
57 const LogicalConstraintTestParameters& params) {
58 out << "{ solver_type: " << params.solver_type
59 << ", parameters: " << ProtobufShortDebugString(params.parameters.Proto())
60 << ", supports_integer_variables: "
61 << (params.supports_integer_variables ? "true" : "false")
62 << ", supports_sos1: " << (params.supports_sos1 ? "true" : "false")
63 << ", supports_sos2: " << (params.supports_sos2 ? "true" : "false")
64 << ", supports_indicator_constraints: "
65 << (params.supports_indicator_constraints ? "true" : "false")
66 << ", supports_incremental_add_and_deletes: "
67 << (params.supports_incremental_add_and_deletes ? "true" : "false")
68 << ", supports_incremental_variable_deletions: "
69 << (params.supports_incremental_variable_deletions ? "true" : "false")
70 << ", supports_deleting_indicator_variables: "
71 << (params.supports_deleting_indicator_variables ? "true" : "false")
72 << ", supports_updating_binary_variables: "
73 << (params.supports_updating_binary_variables ? "true" : "false")
74 << ", supports_sos_on_expressions: "
75 << (params.supports_sos_on_expressions ? "true" : "false") << " }";
76 return out;
77}
78
79namespace {
80
81using ::testing::AnyOf;
82using ::testing::HasSubstr;
83using ::testing::status::IsOkAndHolds;
84using ::testing::status::StatusIs;
85
86constexpr absl::string_view no_sos1_support_message =
87 "This test is disabled as the solver does not support sos1 constraints";
88constexpr absl::string_view no_sos2_support_message =
89 "This test is disabled as the solver does not support sos2 constraints";
90constexpr absl::string_view no_indicator_support_message =
91 "This test is disabled as the solver does not support indicator "
92 "constraints";
93constexpr absl::string_view no_updating_binary_variables_message =
94 "This test is disabled as the solver does not support updating "
95 "binary variables";
96constexpr absl::string_view no_deleting_indicator_variables_message =
97 "This test is disabled as the solver does not support deleting "
98 "indicator variables";
99constexpr absl::string_view no_incremental_add_and_deletes_message =
100 "This test is disabled as the solver does not support incremental "
101 "add/delete";
102
103// We test SOS1 constraints with both explicit weights and default weights.
104TEST_P(SimpleLogicalConstraintTest, CanBuildSos1Model) {
105 if (!GetParam().supports_sos_on_expressions) {
106 GTEST_SKIP() << "skipped since SOS on expressions are not supported";
107 }
108 Model model;
109 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
110 model.AddSos1Constraint({3.0 * x + 2.0}, {3.0});
111 model.AddSos1Constraint({2.0 * x + 1.0}, {});
112 if (GetParam().supports_sos1) {
113 EXPECT_OK(NewIncrementalSolver(&model, GetParam().solver_type, {}));
114 } else {
115 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
116 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
117 absl::StatusCode::kUnimplemented),
118 HasSubstr("sos1 constraints")));
119 }
120}
121
122// We test SOS2 constraints with both explicit weights and default weights.
123TEST_P(SimpleLogicalConstraintTest, CanBuildSos2Model) {
124 if (!GetParam().supports_sos_on_expressions) {
125 GTEST_SKIP() << "skipped since SOS on expressions are not supported";
126 }
127 Model model;
128 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
129 model.AddSos2Constraint({3.0 * x + 2.0}, {3.0});
130 model.AddSos2Constraint({2.0 * x + 1.0}, {});
131 if (GetParam().supports_sos2) {
132 EXPECT_OK(NewIncrementalSolver(&model, GetParam().solver_type, {}));
133 } else {
134 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
135 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
136 absl::StatusCode::kUnimplemented),
137 HasSubstr("sos2 constraints")));
138 }
139}
140
141// We solve
142//
143// max x + 2y
144// s.t. {x, y} is SOS-1
145// 0 <= x, y <= 1
146//
147// The optimal solution is (x*, y*) = (0, 1) with objective value 2.
148TEST_P(SimpleLogicalConstraintTest, SimpleSos1Instance) {
149 if (!GetParam().supports_sos1) {
150 GTEST_SKIP() << no_sos1_support_message;
151 }
152 Model model;
153 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
154 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
155 model.Maximize(x + 2.0 * y);
156 model.AddSos1Constraint({x, y}, {});
157
158 EXPECT_THAT(SimpleSolve(model),
159 IsOkAndHolds(IsOptimalWithSolution(2.0, {{x, 0.0}, {y, 1.0}})));
160}
161
162// We solve
163//
164// max 2x + y + 3z
165// s.t. {x, y, z} is SOS-2
166// 0 <= x, y, z <= 1
167//
168// The optimal solution is (x*, y*, z*) = (0, 1, 1) with objective value 4.
169TEST_P(SimpleLogicalConstraintTest, SimpleSos2Instance) {
170 if (!GetParam().supports_sos2) {
171 GTEST_SKIP() << no_sos2_support_message;
172 }
173 Model model;
174 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
175 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
176 const Variable z = model.AddContinuousVariable(0.0, 1.0, "z");
177 model.Maximize(2.0 * x + 1.0 * y + 3.0 * z);
178 model.AddSos2Constraint({x, y, z}, {});
179
181 4.0, {{x, 0.0}, {y, 1.0}, {z, 1.0}})));
182}
183
184// We solve
185//
186// max 2x + 1.5y + 3z
187// s.t. {y, z} is SOS-1
188// {x, y, z} is SOS-2
189// 0 <= x, y, z <= 1
190//
191// The optimal solution is (x*, y*, z*) = (1, 1, 0) with objective value 3.5.
192TEST_P(SimpleLogicalConstraintTest, InstanceWithSos1AndSos2) {
193 if (!GetParam().supports_sos1) {
194 GTEST_SKIP() << no_sos1_support_message;
195 }
196 if (!GetParam().supports_sos2) {
197 GTEST_SKIP() << no_sos2_support_message;
198 }
199 Model model;
200 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
201 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
202 const Variable z = model.AddContinuousVariable(0.0, 1.0, "z");
203 model.Maximize(2.0 * x + 1.5 * y + 3.0 * z);
204 model.AddSos1Constraint({y, z}, {});
205 model.AddSos2Constraint({x, y, z}, {});
206
208 3.5, {{x, 1.0}, {y, 1.0}, {z, 0.0}})));
209}
210
211// We solve
212//
213// min x + y
214// s.t. {2x - 1, y - 0.75} is SOS-1
215// 0 <= x, y <= 1
216//
217// The optimal solution is (x*, y*) = (0.5, 0) with objective value 0.5.
218TEST_P(SimpleLogicalConstraintTest, Sos1WithExpressions) {
219 if (!GetParam().supports_sos1) {
220 GTEST_SKIP() << no_sos1_support_message;
221 }
222 Model model;
223 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
224 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
225 model.Minimize(x + y);
226 model.AddSos1Constraint({2 * x - 1, y - 0.75}, {});
227
228 EXPECT_THAT(SimpleSolve(model),
229 IsOkAndHolds(IsOptimalWithSolution(0.5, {{x, 0.5}, {y, 0.0}})));
230}
231
232// We solve
233//
234// max x + y + z
235// s.t. {2x + 1, 8y + 1, 4z + 1} is SOS-2
236// -1 <= x, y, z <= 1
237//
238// The optimal solution is (x*, y*) = (1, 1, -0.25) with objective value 1.75.
239TEST_P(SimpleLogicalConstraintTest, Sos2WithExpressions) {
240 if (!GetParam().supports_sos2) {
241 GTEST_SKIP() << no_sos2_support_message;
242 }
243 Model model;
244 const Variable x = model.AddContinuousVariable(-1.0, 1.0, "x");
245 const Variable y = model.AddContinuousVariable(-1.0, 1.0, "y");
246 const Variable z = model.AddContinuousVariable(-1.0, 1.0, "z");
247 model.Maximize(x + y + z);
248 model.AddSos2Constraint({2 * x + 1, 8 * y + 1, 4 * z + 1}, {});
249
251 1.75, {{x, 1.0}, {y, 1.0}, {z, -0.25}})));
252}
253
254// We solve
255//
256// min x
257// s.t. {x, x} is SOS-1
258// -1 <= x <= 1
259//
260// The optimal solution is x* = 0 with objective value 0.
261TEST_P(SimpleLogicalConstraintTest, Sos1VariableInMultipleTerms) {
262 if (!GetParam().supports_sos1) {
263 GTEST_SKIP() << no_sos2_support_message;
264 }
265 Model model;
266 const Variable x = model.AddContinuousVariable(-1.0, 1.0, "x");
267 model.Minimize(x);
268 model.AddSos1Constraint({x, x});
269
270 EXPECT_THAT(SimpleSolve(model),
271 IsOkAndHolds(IsOptimalWithSolution(0.0, {{x, 0.0}})));
272}
273
274// We solve
275//
276// min x
277// s.t. {x, 0, x} is SOS-2
278// -1 <= x <= 1
279//
280// The optimal solution is x* = 0 with objective value 0.
281TEST_P(SimpleLogicalConstraintTest, Sos2VariableInMultipleTerms) {
282 if (!GetParam().supports_sos2) {
283 GTEST_SKIP() << no_sos2_support_message;
284 }
285 Model model;
286 const Variable x = model.AddContinuousVariable(-1.0, 1.0, "x");
287 model.Minimize(x);
288 model.AddSos2Constraint({x, 0.0, x});
289
290 EXPECT_THAT(SimpleSolve(model),
291 IsOkAndHolds(IsOptimalWithSolution(0.0, {{x, 0.0}})));
292}
293
294// We start with the LP
295//
296// max x + 2y
297// s.t. 0 <= x, y <= 1
298//
299// We then add the SOS1 constraint
300//
301// {x, y} is SOS-1
302//
303// The optimal solution for the modified problem is (x*, y*) = (0, 1) with
304// objective value 2.
305TEST_P(IncrementalLogicalConstraintTest, LinearToSos1Update) {
306 if (!GetParam().supports_incremental_add_and_deletes) {
307 GTEST_SKIP() << no_incremental_add_and_deletes_message;
308 }
309 Model model;
310 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
311 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
312 model.Maximize(x + 2.0 * y);
313
315 const auto solver,
316 NewIncrementalSolver(&model, GetParam().solver_type, {}));
317 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
318 IsOkAndHolds(IsOptimalWithSolution(3.0, {{x, 1.0}, {y, 1.0}})));
319
320 model.AddSos1Constraint({x, y}, {});
321
322 if (!GetParam().supports_sos1) {
323 // Here we test that solvers that don't support SOS1 constraints return
324 // false in SolverInterface::CanUpdate(). Thus they should fail in their
325 // factory function instead of failing in their SolverInterface::Update()
326 // function. To assert we rely on status annotations added by
327 // IncrementalSolver::Update() to the returned status of Solver::Update()
328 // and Solver::New().
330 solver->Update(),
331 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
332 absl::StatusCode::kUnimplemented),
333 AllOf(HasSubstr("sos1 constraint"),
334 // Sub-string expected for Solver::Update() error.
335 Not(HasSubstr("update failed")),
336 // Sub-string expected for Solver::New() error.
337 HasSubstr("solver re-creation failed"))));
338 return;
339 }
340 ASSERT_THAT(solver->Update(),
341 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
342 ? DidUpdate()
343 : Not(DidUpdate())));
344 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
345 IsOkAndHolds(IsOptimalWithSolution(2.0, {{x, 0.0}, {y, 1.0}})));
346}
347
348// We start with the LP
349//
350// max 2x + y + 3z
351// s.t. 0 <= x, y, z <= 1
352//
353// We then add the SOS2 constraint
354//
355// {x, y, z} is SOS-2
356//
357// The optimal solution for the modified problem is (x*, y*, z*) = (0, 1, 1)
358// with objective value 4.
359TEST_P(IncrementalLogicalConstraintTest, LinearToSos2Update) {
360 if (!GetParam().supports_incremental_add_and_deletes) {
361 GTEST_SKIP() << no_incremental_add_and_deletes_message;
362 }
363 Model model;
364 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
365 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
366 const Variable z = model.AddContinuousVariable(0.0, 1.0, "z");
367 model.Maximize(2.0 * x + 1.0 * y + 3.0 * z);
368
370 const auto solver,
371 NewIncrementalSolver(&model, GetParam().solver_type, {}));
372 ASSERT_THAT(
373 solver->Solve({.parameters = GetParam().parameters}),
374 IsOkAndHolds(IsOptimalWithSolution(6.0, {{x, 1.0}, {y, 1.0}, {z, 1.0}})));
375
376 model.AddSos2Constraint({x, y, z}, {});
377
378 if (!GetParam().supports_sos2) {
379 // Here we test that solvers that don't support SOS2 constraints return
380 // false in SolverInterface::CanUpdate(). Thus they should fail in their
381 // factory function instead of failing in their SolverInterface::Update()
382 // function. To assert we rely on status annotations added by
383 // IncrementalSolver::Update() to the returned status of Solver::Update()
384 // and Solver::New().
386 solver->Update(),
387 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
388 absl::StatusCode::kUnimplemented),
389 AllOf(HasSubstr("sos2 constraint"),
390 // Sub-string expected for Solver::Update() error.
391 Not(HasSubstr("update failed")),
392 // Sub-string expected for Solver::New() error.
393 HasSubstr("solver re-creation failed"))));
394 return;
395 }
396 ASSERT_THAT(solver->Update(),
397 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
398 ? DidUpdate()
399 : Not(DidUpdate())));
401 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
402 IsOkAndHolds(IsOptimalWithSolution(4.0, {{x, 0.0}, {y, 1.0}, {z, 1.0}})));
403}
404
405// We start with:
406//
407// max x + 3y
408// s.t. {2x - 1, 4y - 1} is SOS-1
409// x + y <= 1
410// 0 <= x, y <= 1
411//
412// The optimal solution is (x*, y*) = (0.25, 0.75) with objective value 2.5.
413//
414// Then we delete the SOS-1 constraint, leaving the LP:
415//
416// max x + 3y
417// s.t. x + y <= 1
418// 0 <= x, y <= 1
419//
420// The optimal solution is (x*, y*) = (0, 1) with objective value 3.
421TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesSos1Constraint) {
422 if (!GetParam().supports_sos1) {
423 GTEST_SKIP() << no_sos1_support_message;
424 }
425 Model model;
426 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
427 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
428 model.Maximize(x + 3 * y);
429 model.AddLinearConstraint(x + y <= 1);
430 const Sos1Constraint c = model.AddSos1Constraint({2 * x - 1, 4 * y - 3}, {});
431
433 const auto solver,
434 NewIncrementalSolver(&model, GetParam().solver_type, {}));
435 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
436 IsOkAndHolds(IsOptimalWithSolution(2.5, {{x, 0.25}, {y, 0.75}})));
437
438 model.DeleteSos1Constraint(c);
439
440 ASSERT_THAT(solver->Update(),
441 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
442 ? DidUpdate()
443 : Not(DidUpdate())));
444 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
445 IsOkAndHolds(IsOptimalWithSolution(3.0, {{x, 0.0}, {y, 1.0}})));
446}
447
448// We start with:
449//
450// max x + 3y + 2z
451// s.t. {2x - 1, 8y - 1, 4z - 1} is SOS-2
452// x + y + z <= 2
453// 0 <= x, y, z <= 1
454//
455// The optimal solution is (x*, y*, z*) = (0.5, 1, 0.5) with objective value
456// 4.5.
457//
458// Then we delete the SOS-2 constraint, leaving the LP:
459//
460// max x + 3y + 2z
461// s.t. x + y + z <= 2
462// 0 <= x, y, z <= 1
463//
464// The optimal solution is (x*, y*, z*) = (0, 1, 1) with objective value 5.
465TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesSos2Constraint) {
466 if (!GetParam().supports_sos2) {
467 GTEST_SKIP() << no_sos1_support_message;
468 }
469 Model model;
470 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
471 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
472 const Variable z = model.AddContinuousVariable(0.0, 1.0, "z");
473 model.Maximize(x + 3 * y + 2 * z);
474 model.AddLinearConstraint(x + y + z <= 2);
475 const Sos2Constraint c =
476 model.AddSos2Constraint({2 * x - 1, 8 * y - 1, 4 * z - 1}, {});
477
479 const auto solver,
480 NewIncrementalSolver(&model, GetParam().solver_type, {}));
481 ASSERT_THAT(
482 solver->Solve({.parameters = GetParam().parameters}),
483 IsOkAndHolds(IsOptimalWithSolution(4.5, {{x, 0.5}, {y, 1.0}, {z, 0.5}})));
484
485 model.DeleteSos2Constraint(c);
486
487 ASSERT_THAT(solver->Update(),
488 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
489 ? DidUpdate()
490 : Not(DidUpdate())));
492 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
493 IsOkAndHolds(IsOptimalWithSolution(5.0, {{x, 0.0}, {y, 1.0}, {z, 1.0}})));
494}
495
496// We start with:
497//
498// max 2x + 2y + z + w
499// s.t. {x, y + w, z} is SOS-1
500// 0 <= x, y, z, w <= 1
501//
502// The optimal solution is (x*, y*, z*, w) = (0, 1, 0, 1) with objective value
503// 3.
504//
505// We then delete the y variable, leaving the problem:
506//
507// max 2x + z + w
508// s.t. {x, w, z} is SOS-1
509// 0 <= x, z, w <= 1
510//
511// The optimal solution is (x*, z*, w*) = (1, 0, 0) with objective value 2.
512// TODO(b/237076465): With C++ API, also test deletion of single variable term.
514 UpdateDeletesVariableInSos1Constraint) {
515 if (!GetParam().supports_sos1) {
516 GTEST_SKIP() << no_sos1_support_message;
517 }
518 Model model;
519 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
520 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
521 const Variable z = model.AddContinuousVariable(0.0, 1.0, "z");
522 const Variable w = model.AddContinuousVariable(0.0, 1.0, "w");
523 model.Maximize(2.0 * x + 2.0 * y + z + w);
524 model.AddSos1Constraint({x, y + w, z}, {});
525
527 const auto solver,
528 NewIncrementalSolver(&model, GetParam().solver_type, {}));
529 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
531 3.0, {{x, 0.0}, {y, 1.0}, {z, 0.0}, {w, 1.0}})));
532
533 model.DeleteVariable(y);
534
535 ASSERT_THAT(solver->Update(),
536 IsOkAndHolds(GetParam().supports_incremental_variable_deletions
537 ? DidUpdate()
538 : Not(DidUpdate())));
540 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
541 IsOkAndHolds(IsOptimalWithSolution(2.0, {{x, 1.0}, {z, 0.0}, {w, 0.0}})));
542}
543
544// We start with:
545//
546// max 2x + 2y + 2z + w
547// s.t. {x, y, z + w} is SOS-2
548// 0 <= x, y, z, w <= 1
549//
550// The optimal solution is (x*, y*, z*, w*) = (0, 1, 1, 1) with objective value
551// 5.
552//
553// We then delete the z variable, leaving the problem:
554//
555// max 2x + 2y + w
556// s.t. {x, y, w} is SOS-2
557// 0 <= x, y, w <= 1
558//
559// The optimal solution is (x*, y*, w*) = (1, 1, 0) with objective value 4.
560// TODO(b/237076465): With C++ API, also test deletion of single variable term.
562 UpdateDeletesVariableInSos2Constraint) {
563 if (!GetParam().supports_sos2) {
564 GTEST_SKIP() << no_sos2_support_message;
565 }
566 Model model;
567 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
568 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
569 const Variable z = model.AddContinuousVariable(0.0, 1.0, "z");
570 const Variable w = model.AddContinuousVariable(0.0, 1.0, "w");
571 model.Maximize(2.0 * x + 2.0 * y + 2.0 * z + w);
572 model.AddSos2Constraint({x, y, z + w}, {});
573
575 const auto solver,
576 NewIncrementalSolver(&model, GetParam().solver_type, {}));
577 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
579 5.0, {{x, 0.0}, {y, 1.0}, {z, 1.0}, {w, 1.0}})));
580
581 model.DeleteVariable(z);
582
583 ASSERT_THAT(solver->Update(),
584 IsOkAndHolds(GetParam().supports_incremental_variable_deletions
585 ? DidUpdate()
586 : Not(DidUpdate())));
588 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
589 IsOkAndHolds(IsOptimalWithSolution(4.0, {{x, 1.0}, {y, 1.0}, {w, 0.0}})));
590}
591
592// We start with:
593//
594// max 2x + 1.5y + 3z
595// s.t. {y, z} is SOS-1
596// {x, y, z} is SOS-2
597// 0 <= x, y, z <= 1
598//
599// The optimal solution is (x*, y*, z*) = (1, 1, 0) with objective value 3.5.
600//
601// We then delete the SOS-1 constraint, leaving:
602//
603// max x + 1.5y + 3z
604// s.t. {x, y, z} is SOS-2
605// 0 <= x, y, z <= 1
606//
607// The optimal solution is (x*, y*, z*) = (0, 1, 1) with objective value 4.5.
608TEST_P(IncrementalLogicalConstraintTest, InstanceWithSos1AndSos2AndDeletion) {
609 if (!GetParam().supports_sos1) {
610 GTEST_SKIP() << no_sos1_support_message;
611 }
612 if (!GetParam().supports_sos2) {
613 GTEST_SKIP() << no_sos2_support_message;
614 }
615 Model model;
616 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
617 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
618 const Variable z = model.AddContinuousVariable(0.0, 1.0, "z");
619 model.Maximize(2.0 * x + 1.5 * y + 3.0 * z);
620 const Sos1Constraint c = model.AddSos1Constraint({y, z}, {});
621 model.AddSos2Constraint({x, y, z}, {});
622
624 const auto solver,
625 NewIncrementalSolver(&model, GetParam().solver_type, {}));
626 ASSERT_THAT(
627 solver->Solve({.parameters = GetParam().parameters}),
628 IsOkAndHolds(IsOptimalWithSolution(3.5, {{x, 1.0}, {y, 1.0}, {z, 0.0}})));
629
630 model.DeleteSos1Constraint(c);
631
632 ASSERT_THAT(solver->Update(),
633 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
634 ? DidUpdate()
635 : Not(DidUpdate())));
637 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
638 IsOkAndHolds(IsOptimalWithSolution(4.5, {{x, 0.0}, {y, 1.0}, {z, 1.0}})));
639}
640
641TEST_P(SimpleLogicalConstraintTest, CanBuildIndicatorModel) {
642 Model model;
643 // Technically `x` should be binary, but the validator will not enforce this.
644 // Instead, we expect that solvers will reject solving any models containing
645 // non-binary indicator variables (this is tested elsewhere). Therefore, here
646 // we want to test that solvers that do not support either indicator
647 // constraints or integer variables will reject indicator constraints with a
648 // useful message, regardless if the indicator is binary.
649 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
650 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
651 model.AddIndicatorConstraint(x, y <= 0.5);
652
653 if (GetParam().supports_indicator_constraints) {
654 EXPECT_OK(NewIncrementalSolver(&model, GetParam().solver_type, {}));
655 } else {
656 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
657 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
658 absl::StatusCode::kUnimplemented),
659 HasSubstr("indicator constraints")));
660 }
661}
662
663// Here we test that each solver supporting indicator constraints will raise an
664// error when attempting to solve a model containing non-binary indicator
665// variables.
666TEST_P(SimpleLogicalConstraintTest, SolveFailsWithNonBinaryIndicatorVariable) {
667 if (!GetParam().supports_indicator_constraints) {
668 GTEST_SKIP() << no_indicator_support_message;
669 }
670 Model model;
671 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
672 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
673 model.AddIndicatorConstraint(x, y >= 0.5);
674
675 EXPECT_THAT(SimpleSolve(model),
676 StatusIs(absl::StatusCode::kInvalidArgument,
677 HasSubstr("indicator variable is not binary")));
678}
679
680// We solve
681//
682// min -x + y
683// s.t. x = 1 --> y >= 0.5
684// x in {0,1}
685// 0 <= y <= 1
686//
687// The optimal solution is (x*, y*) = (1, 0.5) with objective value -0.5.
688TEST_P(SimpleLogicalConstraintTest, SimpleIndicatorInstance) {
689 if (!GetParam().supports_indicator_constraints) {
690 GTEST_SKIP() << no_indicator_support_message;
691 }
692 Model model;
693 const Variable x = model.AddBinaryVariable("x");
694 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
695 model.Minimize(-x + y);
696 model.AddIndicatorConstraint(x, y >= 0.5);
697
698 EXPECT_THAT(SimpleSolve(model),
699 IsOkAndHolds(IsOptimalWithSolution(-0.5, {{x, 1.0}, {y, 0.5}})));
700}
701
702// We solve
703//
704// min x + y
705// s.t. x = 0 --> y >= 0.5
706// x in {0,1}
707// 0 <= y <= 1
708//
709// The optimal solution is (x*, y*) = (0, 0.5) with objective value 0.5.
710TEST_P(SimpleLogicalConstraintTest, ActivationOnZero) {
711 if (!GetParam().supports_indicator_constraints) {
712 GTEST_SKIP() << no_indicator_support_message;
713 }
714 Model model;
715 const Variable x = model.AddBinaryVariable("x");
716 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
717 model.Minimize(x + y);
718 model.AddIndicatorConstraint(x, y >= 0.5, /*activate_on_zero=*/true, "c");
719
720 EXPECT_THAT(SimpleSolve(model),
721 IsOkAndHolds(IsOptimalWithSolution(0.5, {{x, 0.0}, {y, 0.5}})));
722}
723
724// As of 2022-08-30, ModelProto supports indicator constraints with ranged
725// implied constraints, although no solver supports this functionality. If a
726// solver does add support in the future, this test should be updated and the
727// test parameters should be suitably modified to track this support.
728TEST_P(SimpleLogicalConstraintTest, IndicatorWithRangedImpliedConstraint) {
729 if (!GetParam().supports_indicator_constraints) {
730 GTEST_SKIP() << no_indicator_support_message;
731 }
732 Model model;
733 const Variable x = model.AddBinaryVariable("x");
734 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
735 model.AddIndicatorConstraint(x, 0.25 <= y <= 0.75);
736
737 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
738 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
739 absl::StatusCode::kUnimplemented),
740 HasSubstr("ranged")));
741}
742
743// We write the model:
744//
745// max x
746// s.t. (unset variable id) = 1 --> x = 0.5
747// 0 <= x <= 1
748//
749// As the indicator variable is unset, the indicator constraint should be
750// ignored, and the optimal solution is x* = 1 with objective value 1.
751//
752// To get an unset indicator variable, we simply add an indicator variable, add
753// the constraint, and then delete the indicator variable.
754TEST_P(SimpleLogicalConstraintTest, UnsetIndicatorVariable) {
755 if (!GetParam().supports_indicator_constraints) {
756 GTEST_SKIP() << no_indicator_support_message;
757 }
758 Model model;
759 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
760 const Variable indicator = model.AddBinaryVariable("indicator");
761 model.Maximize(x);
762 model.AddIndicatorConstraint(indicator, x == 0.5);
763 model.DeleteVariable(indicator);
764
765 EXPECT_THAT(SimpleSolve(model),
766 IsOkAndHolds(IsOptimalWithSolution(1.0, {{x, 1.0}})));
767}
768
769// We test that indicator variables may have custom bounds set as long as the
770// variables are integer and those bounds are contained in [0, 1]. The model is
771//
772// max v + w
773// s.t. x = 1 --> w >= 1.5
774// y = 1 --> v <= 0.6
775// z = 1 --> w <= 0.4
776// x == 0
777// y == 1
778// 0.5 <= z <= 1
779// 0 <= v, w <= 1
780// x, y, z in {0, 1}.
781//
782// The unique optimal solution is (x, y, z, v, w) = (0, 1, 1, 0.6, 0.4) with
783// objective value 1.0.
784TEST_P(SimpleLogicalConstraintTest, IndicatorsWithOddButValidBounds) {
785 if (!GetParam().supports_indicator_constraints) {
786 GTEST_SKIP() << no_indicator_support_message;
787 }
788 if (GetParam().solver_type == SolverType::kXpress) {
789 // This test does not work with Xpress because the Xpress library will
790 // refuse to create an indicator constraint if the indicator variable is
791 // not a binary variable (it will raise error 1029);
792 GTEST_SKIP() << "This test is disabled as Xpress does not support "
793 "indicator constraints on non-binary variables";
794 }
795 Model model;
796 const Variable x = model.AddIntegerVariable(0.0, 0.0, "x");
797 const Variable y = model.AddIntegerVariable(1.0, 1.0, "y");
798 const Variable z = model.AddIntegerVariable(0.5, 1.0, "z");
799 const Variable v = model.AddContinuousVariable(0.0, 1.0, "v");
800 const Variable w = model.AddContinuousVariable(0.0, 1.0, "w");
801 model.Maximize(v + w);
802 model.AddIndicatorConstraint(x, w >= 1.5);
803 model.AddIndicatorConstraint(y, v <= 0.6);
804 model.AddIndicatorConstraint(z, w <= 0.4);
805
806 EXPECT_THAT(SimpleSolve(model),
808 1.0, {{x, 0.0}, {y, 1.0}, {z, 1.0}, {v, 0.6}, {w, 0.4}})));
809}
810
811// We start with the LP
812//
813// max x + y
814// s.t. x in {0,1}
815// 0 <= y <= 1
816//
817// The optimal solution is (x*, y*) = (1, 1) with objective value 2.
818//
819// We then add the indicator constraint
820//
821// x = 1 --> y <= 0.5
822//
823// The optimal solution for the modified problem is (x*, y*) = (1, 0.5) with
824// objective value 1.5.
825TEST_P(IncrementalLogicalConstraintTest, LinearToIndicatorUpdate) {
826 Model model;
827 // We want to test that, even for solvers that do not support either integer
828 // variables or indicator constraints, that we get a meaningful error message.
829 const Variable x = GetParam().supports_integer_variables
830 ? model.AddBinaryVariable("x")
831 : model.AddContinuousVariable(0.0, 1.0, "x");
832 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
833 model.Maximize(x + y);
834
836 const auto solver,
837 NewIncrementalSolver(&model, GetParam().solver_type, {}));
838 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
839 IsOkAndHolds(IsOptimalWithSolution(2.0, {{x, 1.0}, {y, 1.0}})));
840
841 model.AddIndicatorConstraint(x, y <= 0.5);
842
843 if (!GetParam().supports_indicator_constraints) {
844 // Here we test that solvers that don't support indicator constraints return
845 // false in SolverInterface::CanUpdate(). Thus they should fail in their
846 // factory function instead of failing in their SolverInterface::Update()
847 // function. To assert we rely on status annotations added by
848 // IncrementalSolver::Update() to the returned status of Solver::Update()
849 // and Solver::New().
851 solver->Update(),
852 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
853 absl::StatusCode::kUnimplemented),
854 AllOf(HasSubstr("indicator constraint"),
855 // Sub-string expected for Solver::Update() error.
856 Not(HasSubstr("update failed")),
857 // Sub-string expected for Solver::New() error.
858 HasSubstr("solver re-creation failed"))));
859 return;
860 }
861 ASSERT_THAT(solver->Update(),
862 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
863 ? DidUpdate()
864 : Not(DidUpdate())));
865 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
866 IsOkAndHolds(IsOptimalWithSolution(1.5, {{x, 1.0}, {y, 0.5}})));
867}
868
869// We start with the problem:
870//
871// max x + y
872// s.t. x = 1 --> y <= 0.5
873// x in {0,1}
874// 0 <= y <= 1
875//
876// The optimal solution is (x*, y*) = (1, 0.5) with objective value 1.5.
877//
878// We then delete the indicator constraint, leaving the LP:
879//
880// max x + y
881// s.t. x in {0,1}
882// 0 <= y <= 1
883//
884// The optimal solution for the modified problem is (x*, y*) = (1, 1) with
885// objective value 2.
886TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesIndicatorConstraint) {
887 if (!GetParam().supports_indicator_constraints) {
888 GTEST_SKIP() << no_indicator_support_message;
889 }
890 if (!GetParam().supports_incremental_add_and_deletes) {
891 GTEST_SKIP() << no_incremental_add_and_deletes_message;
892 }
893 Model model;
894 const Variable x = model.AddBinaryVariable("x");
895 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
896 model.Maximize(x + y);
897 const IndicatorConstraint c = model.AddIndicatorConstraint(x, y <= 0.5);
899 const auto solver,
900 NewIncrementalSolver(&model, GetParam().solver_type, {}));
901 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
902 IsOkAndHolds(IsOptimalWithSolution(1.5, {{x, 1.0}, {y, 0.5}})));
903
904 model.DeleteIndicatorConstraint(c);
905
906 ASSERT_THAT(solver->Update(),
907 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
908 ? DidUpdate()
909 : Not(DidUpdate())));
910 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
911 IsOkAndHolds(IsOptimalWithSolution(2.0, {{x, 1.0}, {y, 1.0}})));
912}
913
914// We start with the problem:
915//
916// max x
917// s.t. (unset variable id) = 1 --> x <= 0.5
918// 0 <= x <= 1
919//
920// The optimal solution is x* = 1 with objective value 1. To write this model,
921// we add a placeholder indicator variable, add the indicator constraint, delete
922// that constraint, and only then initialize the solver.
923//
924// We then delete the indicator constraint, leaving the LP:
925//
926// max x
927// s.t. 0 <= x <= 1
928//
929// The optimal solution for the modified problem is also x* = 1 with objective
930// value 1.
932 UpdateDeletesIndicatorConstraintWithUnsetIndicatorVariable) {
933 if (!GetParam().supports_indicator_constraints) {
934 GTEST_SKIP() << no_indicator_support_message;
935 }
936 if (!GetParam().supports_incremental_add_and_deletes) {
937 GTEST_SKIP() << no_incremental_add_and_deletes_message;
938 }
939 Model model;
940 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
941 const Variable indicator = model.AddBinaryVariable("indicator");
942 model.Maximize(x);
943 const IndicatorConstraint c =
944 model.AddIndicatorConstraint(indicator, x <= 0.5);
945 model.DeleteVariable(indicator);
946
948 const auto solver,
949 NewIncrementalSolver(&model, GetParam().solver_type, {}));
950 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
951 IsOkAndHolds(IsOptimalWithSolution(1.0, {{x, 1.0}})));
952
953 model.DeleteIndicatorConstraint(c);
954
955 ASSERT_THAT(solver->Update(),
956 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
957 ? DidUpdate()
958 : Not(DidUpdate())));
959 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
960 IsOkAndHolds(IsOptimalWithSolution(1.0, {{x, 1.0}})));
961}
962
963// We start with the problem:
964//
965// max x + y
966// s.t. x = 1 --> y <= 0.5
967// x in {0,1}
968// 0 <= y <= 1
969//
970// The optimal solution is (x*, y*) = (1, 0.5) with objective value 1.5.
971//
972// We then delete the indicator variable x. If the solver supports this form of
973// update, we then solve the problem:
974//
975// max y
976// s.t. 0 <= y <= 1
977//
978// The optimal solution is y* = 1 with objective value 1.
979TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesIndicatorVariable) {
980 if (!GetParam().supports_indicator_constraints) {
981 GTEST_SKIP() << no_indicator_support_message;
982 }
983 if (!GetParam().supports_deleting_indicator_variables) {
984 GTEST_SKIP() << no_deleting_indicator_variables_message;
985 }
986 Model model;
987 const Variable x = model.AddBinaryVariable("x");
988 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
989 model.Maximize(x + y);
990 model.AddIndicatorConstraint(x, y <= 0.5);
991
993 const auto solver,
994 NewIncrementalSolver(&model, GetParam().solver_type, {}));
995 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
996 IsOkAndHolds(IsOptimalWithSolution(1.5, {{x, 1.0}, {y, 0.5}})));
997
998 model.DeleteVariable(x);
999
1000 ASSERT_THAT(solver->Update(),
1001 IsOkAndHolds(GetParam().supports_deleting_indicator_variables
1002 ? DidUpdate()
1003 : Not(DidUpdate())));
1004 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
1005 IsOkAndHolds(IsOptimalWithSolution(1.0, {{y, 1.0}})));
1006}
1007
1008// We start with the problem:
1009//
1010// max x + 2y + z
1011// s.t. x = 1 --> y <= 0.5
1012// x = 1 --> z <= 0.5
1013// x in {0,1}
1014// 0 <= y, z <= 1
1015//
1016// The optimal solution is (x*, y*, z*) = (0, 1, 1) with objective value 3.
1017//
1018// We then delete the variable y, leaving the problem:
1019//
1020// max x + z
1021// s.t. x = 1 --> 0 <= 0.5
1022// x = 1 --> z <= 0.5
1023// x in {0,1}
1024// 0 <= z <= 1
1025//
1026// The optimal solution for the modified problem is (x*, z*) = (1, 0.5) with
1027// objective value 1.5.
1029 UpdateDeletesVariableInImpliedExpression) {
1030 if (!GetParam().supports_indicator_constraints) {
1031 GTEST_SKIP() << no_indicator_support_message;
1032 }
1033 Model model;
1034 const Variable x = model.AddBinaryVariable("x");
1035 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
1036 const Variable z = model.AddContinuousVariable(0.0, 1.0, "z");
1037 model.Maximize(x + 2.0 * y + z);
1038 model.AddIndicatorConstraint(x, y <= 0.5);
1039 model.AddIndicatorConstraint(x, z <= 0.5);
1040
1042 const auto solver,
1043 NewIncrementalSolver(&model, GetParam().solver_type, {}));
1044 ASSERT_THAT(
1045 solver->Solve({.parameters = GetParam().parameters}),
1046 IsOkAndHolds(IsOptimalWithSolution(3.0, {{x, 0.0}, {y, 1.0}, {z, 1.0}})));
1047
1048 model.DeleteVariable(y);
1049
1050 ASSERT_THAT(solver->Update(),
1051 IsOkAndHolds(GetParam().supports_incremental_variable_deletions
1052 ? DidUpdate()
1053 : Not(DidUpdate())));
1054 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
1055 IsOkAndHolds(IsOptimalWithSolution(1.5, {{x, 1.0}, {z, 0.5}})));
1056}
1057
1058// We start with a simple, valid indicator constraint with binary indicator
1059// variable. We then update the indicator variable to be continuous. The solver
1060// should permit the model update, but return an error when solving.
1062 UpdateMakesIndicatorVariableTypeInvalid) {
1063 if (!GetParam().supports_indicator_constraints) {
1064 GTEST_SKIP() << no_indicator_support_message;
1065 }
1066 if (!GetParam().supports_updating_binary_variables) {
1067 GTEST_SKIP() << no_updating_binary_variables_message;
1068 }
1069 Model model;
1070 const Variable x = model.AddBinaryVariable("x");
1071 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
1072 model.AddIndicatorConstraint(x, y <= 0.5);
1073
1075 const auto solver,
1076 NewIncrementalSolver(&model, GetParam().solver_type, {}));
1077 ASSERT_OK(solver->Solve({.parameters = GetParam().parameters}));
1078
1079 model.set_continuous(x);
1080
1081 ASSERT_THAT(solver->Update(),
1082 IsOkAndHolds(GetParam().supports_updating_binary_variables
1083 ? DidUpdate()
1084 : Not(DidUpdate())));
1085 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
1086 StatusIs(absl::StatusCode::kInvalidArgument,
1087 HasSubstr("indicator variable is not binary")));
1088}
1089
1090// We test that we can update indicator variable bounds as long as they are
1091// still contained in [0, 1]. The model is:
1092//
1093// max u + v
1094// s.t. x + y == 1
1095// x = 1 --> u <= 0.6
1096// y = 1 --> v <= 0.4
1097// x, y in {0, 1}
1098// 0 <= u, v <= 1
1099//
1100// The optimal solution is (x, y, u, v) = (1, 0, 0.6, 1.0) with objective value
1101// 1.6.
1102//
1103// If we update bounds to x == 0, the optimal solution is then (x, y, u, v) =
1104// (0, 1, 1, 0.4) with objective value 1.4.
1105//
1106// Alternatively, if we update bounds to 0.5 <= x <= 1 and 0 <= y <= 0.5, the
1107// optimal solution is then (x, y, u, v) = (1, 0, 0.6, 1.0) with objective
1108// value 1.6.
1109//
1110// Alternatively, if we update bounds to y == 1, the optimal solution is then
1111// (x, y, u, v) = (0, 1, 1, 0.4) with objective value 1.4.
1112TEST_P(IncrementalLogicalConstraintTest, UpdateChangesIndicatorVariableBound) {
1113 if (!GetParam().supports_indicator_constraints) {
1114 GTEST_SKIP() << no_indicator_support_message;
1115 }
1116 if (!GetParam().supports_updating_binary_variables) {
1117 GTEST_SKIP() << no_updating_binary_variables_message;
1118 }
1119 Model model;
1120 const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
1121 const Variable y = model.AddIntegerVariable(0.0, 1.0, "y");
1122 const Variable u = model.AddContinuousVariable(0.0, 1.0, "u");
1123 const Variable v = model.AddContinuousVariable(0.0, 1.0, "z");
1124 model.Maximize(u + v);
1125 model.AddLinearConstraint(x + y == 1.0);
1126 model.AddIndicatorConstraint(x, u <= 0.6);
1127 model.AddIndicatorConstraint(y, v <= 0.4);
1128
1130 const auto solver,
1131 NewIncrementalSolver(&model, GetParam().solver_type, {}));
1132 EXPECT_THAT(solver->Solve({.parameters = GetParam().parameters}),
1134 1.6, {{x, 1.0}, {y, 0.0}, {u, 0.6}, {v, 1.0}})));
1135
1136 model.set_lower_bound(x, 0.0);
1137 model.set_upper_bound(x, 0.0);
1138
1139 ASSERT_THAT(solver->Update(),
1140 IsOkAndHolds(GetParam().supports_updating_binary_variables
1141 ? DidUpdate()
1142 : Not(DidUpdate())));
1143 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
1145 1.4, {{x, 0.0}, {y, 1.0}, {u, 1.0}, {v, 0.4}})));
1146
1147 model.set_lower_bound(x, 0.5);
1148 model.set_upper_bound(x, 1.0);
1149 model.set_lower_bound(y, 0.0);
1150 model.set_upper_bound(y, 0.5);
1151
1152 ASSERT_THAT(solver->Update(),
1153 IsOkAndHolds(GetParam().supports_updating_binary_variables
1154 ? DidUpdate()
1155 : Not(DidUpdate())));
1156 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
1158 1.6, {{x, 1.0}, {y, 0.0}, {u, 0.6}, {v, 1.0}})));
1159
1160 model.set_lower_bound(x, 0.0);
1161 model.set_upper_bound(x, 1.0);
1162 model.set_lower_bound(y, 1.0);
1163 model.set_upper_bound(y, 1.0);
1164
1165 ASSERT_THAT(solver->Update(),
1166 IsOkAndHolds(GetParam().supports_updating_binary_variables
1167 ? DidUpdate()
1168 : Not(DidUpdate())));
1169 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
1171 1.4, {{x, 0.0}, {y, 1.0}, {u, 1.0}, {v, 0.4}})));
1172}
1173
1174// We start with a simple, valid indicator constraint with binary indicator
1175// variable. We then update the indicator variable to have a larger upper bound,
1176// meaning it is integer but no longer binary. The solver should permit the
1177// model update, but return an error when solving.
1179 UpdateMakesIndicatorVariableBoundsInvalid) {
1180 if (!GetParam().supports_indicator_constraints) {
1181 GTEST_SKIP() << no_indicator_support_message;
1182 }
1183 if (!GetParam().supports_updating_binary_variables) {
1184 GTEST_SKIP() << no_updating_binary_variables_message;
1185 }
1186 Model model;
1187 const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
1188 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
1189 model.AddIndicatorConstraint(x, y <= 0.5);
1190
1192 const auto solver,
1193 NewIncrementalSolver(&model, GetParam().solver_type, {}));
1194 ASSERT_OK(solver->Solve({.parameters = GetParam().parameters}));
1195
1196 model.set_upper_bound(x, 2.0);
1197
1198 ASSERT_THAT(solver->Update(),
1199 IsOkAndHolds(GetParam().supports_updating_binary_variables
1200 ? DidUpdate()
1201 : Not(DidUpdate())));
1202 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
1203 StatusIs(absl::StatusCode::kInvalidArgument,
1204 HasSubstr("indicator variable is not binary")));
1205}
1206
1207} // namespace
1208} // namespace operations_research::math_opt
Definition model.h:345
void Maximize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1074
void Minimize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1067
#define ASSERT_OK(expression)
Definition gmock.h:46
#define EXPECT_OK(expression)
Definition gmock.h:45
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
Definition gmock.h:53
Matcher< SolveResult > IsOptimalWithSolution(const double expected_objective, const VariableMap< double > expected_variable_values, const double tolerance)
Definition matchers.cc:778
EXPECT_THAT(ComputeInfeasibleSubsystem(model, GetParam().solver_type), IsOkAndHolds(IsInfeasible(true, ModelSubset{ .variable_bounds={{x, ModelSubset::Bounds{.lower=false,.upper=true}}},.linear_constraints={ {c, ModelSubset::Bounds{.lower=true,.upper=false}}}})))
TEST_P(InfeasibleSubsystemTest, CanComputeInfeasibleSubsystem)
<=x<=1 IncrementalMipTest::IncrementalMipTest() :model_("incremental_solve_test"), x_(model_.AddContinuousVariable(0.0, 1.0, "x")), y_(model_.AddIntegerVariable(0.0, 2.0, "y")), c_(model_.AddLinearConstraint(0<=x_+y_<=1.5, "c")) { model_.Maximize(3.0 *x_+2.0 *y_+0.1);solver_=NewIncrementalSolver(&model_, TestedSolver()).value();const SolveResult first_solve=solver_->Solve().value();CHECK(first_solve.has_primal_feasible_solution());CHECK_LE(std::abs(first_solve.objective_value() - 3.6), kTolerance)<< first_solve.objective_value();} namespace { TEST_P(SimpleMipTest, OneVarMax) { Model model;const Variable x=model.AddVariable(0.0, 4.0, false, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(8.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));} TEST_P(SimpleMipTest, OneVarMin) { Model model;const Variable x=model.AddVariable(-2.4, 4.0, false, "x");model.Minimize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(-4.8));EXPECT_THAT(result.variable_values(), IsNear({{x, -2.4}}));} TEST_P(SimpleMipTest, OneIntegerVar) { Model model;const Variable x=model.AddVariable(0.0, 4.5, true, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(8.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));} TEST_P(SimpleMipTest, SimpleLinearConstraint) { Model model;const Variable x=model.AddBinaryVariable("x");const Variable y=model.AddBinaryVariable("y");model.Maximize(2.0 *x+y);model.AddLinearConstraint(0.0<=x+y<=1.5, "c");ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(2.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 1}, {y, 0}}));} TEST_P(SimpleMipTest, Unbounded) { Model model;const Variable x=model.AddVariable(0.0, kInf, true, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));if(GetParam().report_unboundness_correctly) { ASSERT_THAT(result, TerminatesWithOneOf({TerminationReason::kUnbounded, TerminationReason::kInfeasibleOrUnbounded}));} else { ASSERT_THAT(result, TerminatesWith(TerminationReason::kOtherError));} } TEST_P(SimpleMipTest, Infeasible) { Model model;const Variable x=model.AddVariable(0.0, 3.0, true, "x");model.Maximize(2.0 *x);model.AddLinearConstraint(x >=4.0);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, TerminatesWith(TerminationReason::kInfeasible));} TEST_P(SimpleMipTest, FractionalBoundsContainNoInteger) { if(GetParam().solver_type==SolverType::kGurobi) { GTEST_SKIP()<< "TODO(b/272298816): Gurobi bindings are broken here.";} if(GetParam().solver_type==SolverType::kXpress) { GTEST_SKIP()<< "Xpress does not support contradictory bounds.";} Model model;const Variable x=model.AddIntegerVariable(0.5, 0.6, "x");model.Maximize(x);EXPECT_THAT(Solve(model, GetParam().solver_type), IsOkAndHolds(TerminatesWith(TerminationReason::kInfeasible)));} TEST_P(IncrementalMipTest, EmptyUpdate) { ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());ASSERT_THAT(result, IsOptimal(3.6));EXPECT_THAT(result.variable_values(), IsNear({{x_, 0.5}, {y_, 1.0}}));} TEST_P(IncrementalMipTest, MakeContinuous) { model_.set_continuous(y_);ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());ASSERT_THAT(result, IsOptimal(4.1));EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.5}}));} TEST_P(IncrementalMipTest, DISABLED_MakeContinuousWithNonIntegralBounds) { solver_.reset();Model model("bounds");const Variable x=model.AddIntegerVariable(0.5, 1.5, "x");model.Maximize(x);ASSERT_OK_AND_ASSIGN(const auto solver, NewIncrementalSolver(&model, TestedSolver()));ASSERT_THAT(solver->Solve(), IsOkAndHolds(IsOptimal(1.0)));model.set_continuous(x);ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));ASSERT_THAT(solver->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(1.5)));model.Minimize(x);ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));ASSERT_THAT(solver-> IsOkAndHolds(IsOptimal(0.5)))
absl::StatusOr< std::unique_ptr< IncrementalSolver > > NewIncrementalSolver(Model *model, SolverType solver_type, SolverInitArguments arguments)
Definition solve.cc:82
std::ostream & operator<<(std::ostream &ostr, const SecondOrderConeConstraint &constraint)
Matcher< UpdateResult > DidUpdate()
Definition matchers.cc:1028
BoolVar Not(BoolVar x)
Definition cp_model.cc:87
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:46
STL namespace.
trees with all degrees equal w the current value of w
LogicalConstraintTestParameters(SolverType solver_type, SolveParameters parameters, bool supports_integer_variables, bool supports_sos1, bool supports_sos2, bool supports_indicator_constraints, bool supports_incremental_add_and_deletes, bool supports_incremental_variable_deletions, bool supports_deleting_indicator_variables, bool supports_updating_binary_variables, bool supports_sos_on_expressions=true)