Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
cp_model.py
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
14"""Methods for building and solving CP-SAT models.
15
16The following two sections describe the main
17methods for building and solving CP-SAT models.
18
19* [`CpModel`](#cp_model.CpModel): Methods for creating
20models, including variables and constraints.
21* [`CPSolver`](#cp_model.CpSolver): Methods for solving
22a model and evaluating solutions.
23
24The following methods implement callbacks that the
25solver calls each time it finds a new solution.
26
27* [`CpSolverSolutionCallback`](#cp_model.CpSolverSolutionCallback):
28 A general method for implementing callbacks.
29* [`ObjectiveSolutionPrinter`](#cp_model.ObjectiveSolutionPrinter):
30 Print objective values and elapsed time for intermediate solutions.
31* [`VarArraySolutionPrinter`](#cp_model.VarArraySolutionPrinter):
32 Print intermediate solutions (variable values, time).
33* [`VarArrayAndObjectiveSolutionPrinter`]
34 (#cp_model.VarArrayAndObjectiveSolutionPrinter):
35 Print both intermediate solutions and objective values.
36
37Additional methods for solving CP-SAT models:
38
39* [`Constraint`](#cp_model.Constraint): A few utility methods for modifying
40 constraints created by `CpModel`.
41* [`LinearExpr`](#lineacp_model.LinearExpr): Methods for creating constraints
42 and the objective from large arrays of coefficients.
43
44Other methods and functions listed are primarily used for developing OR-Tools,
45rather than for solving specific optimization problems.
46"""
47
48import threading
49import time
50from typing import (
51 Any,
52 Callable,
53 Dict,
54 Iterable,
55 Optional,
56 Sequence,
57 Tuple,
58 Union,
59 cast,
60 overload,
61)
62import warnings
63
64import numpy as np
65import pandas as pd
66
67from ortools.sat import cp_model_pb2
68from ortools.sat import sat_parameters_pb2
69from ortools.sat.python import cp_model_helper as cmh
70from ortools.sat.python import cp_model_numbers as cmn
71from ortools.util.python import sorted_interval_list
72
73# Import external types.
74Domain = sorted_interval_list.Domain
75BoundedLinearExpression = cmh.BoundedLinearExpression
76FlatFloatExpr = cmh.FlatFloatExpr
77FlatIntExpr = cmh.FlatIntExpr
78LinearExpr = cmh.LinearExpr
79NotBooleanVariable = cmh.NotBooleanVariable
80
81
82# The classes below allow linear expressions to be expressed naturally with the
83# usual arithmetic operators + - * / and with constant numbers, which makes the
84# python API very intuitive. See../ samples/*.py for examples.
85
86INT_MIN = -(2**63) # hardcoded to be platform independent.
87INT_MAX = 2**63 - 1
88INT32_MIN = -(2**31)
89INT32_MAX = 2**31 - 1
90
91# CpSolver status (exported to avoid importing cp_model_cp2).
92UNKNOWN = cp_model_pb2.UNKNOWN
93MODEL_INVALID = cp_model_pb2.MODEL_INVALID
94FEASIBLE = cp_model_pb2.FEASIBLE
95INFEASIBLE = cp_model_pb2.INFEASIBLE
96OPTIMAL = cp_model_pb2.OPTIMAL
97
98# Variable selection strategy
99CHOOSE_FIRST = cp_model_pb2.DecisionStrategyProto.CHOOSE_FIRST
100CHOOSE_LOWEST_MIN = cp_model_pb2.DecisionStrategyProto.CHOOSE_LOWEST_MIN
101CHOOSE_HIGHEST_MAX = cp_model_pb2.DecisionStrategyProto.CHOOSE_HIGHEST_MAX
102CHOOSE_MIN_DOMAIN_SIZE = cp_model_pb2.DecisionStrategyProto.CHOOSE_MIN_DOMAIN_SIZE
103CHOOSE_MAX_DOMAIN_SIZE = cp_model_pb2.DecisionStrategyProto.CHOOSE_MAX_DOMAIN_SIZE
104
105# Domain reduction strategy
106SELECT_MIN_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MIN_VALUE
107SELECT_MAX_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MAX_VALUE
108SELECT_LOWER_HALF = cp_model_pb2.DecisionStrategyProto.SELECT_LOWER_HALF
109SELECT_UPPER_HALF = cp_model_pb2.DecisionStrategyProto.SELECT_UPPER_HALF
110SELECT_MEDIAN_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MEDIAN_VALUE
111SELECT_RANDOM_HALF = cp_model_pb2.DecisionStrategyProto.SELECT_RANDOM_HALF
112
113# Search branching
114AUTOMATIC_SEARCH = sat_parameters_pb2.SatParameters.AUTOMATIC_SEARCH
115FIXED_SEARCH = sat_parameters_pb2.SatParameters.FIXED_SEARCH
116PORTFOLIO_SEARCH = sat_parameters_pb2.SatParameters.PORTFOLIO_SEARCH
117LP_SEARCH = sat_parameters_pb2.SatParameters.LP_SEARCH
118PSEUDO_COST_SEARCH = sat_parameters_pb2.SatParameters.PSEUDO_COST_SEARCH
119PORTFOLIO_WITH_QUICK_RESTART_SEARCH = (
120 sat_parameters_pb2.SatParameters.PORTFOLIO_WITH_QUICK_RESTART_SEARCH
121)
122HINT_SEARCH = sat_parameters_pb2.SatParameters.HINT_SEARCH
123PARTIAL_FIXED_SEARCH = sat_parameters_pb2.SatParameters.PARTIAL_FIXED_SEARCH
124RANDOMIZED_SEARCH = sat_parameters_pb2.SatParameters.RANDOMIZED_SEARCH
125
126# Type aliases
127IntegralT = Union[int, np.int8, np.uint8, np.int32, np.uint32, np.int64, np.uint64]
128IntegralTypes = (
129 int,
130 np.int8,
131 np.uint8,
132 np.int32,
133 np.uint32,
134 np.int64,
135 np.uint64,
136)
137NumberT = Union[
138 int,
139 float,
140 np.int8,
141 np.uint8,
142 np.int32,
143 np.uint32,
144 np.int64,
145 np.uint64,
146 np.double,
147]
148NumberTypes = (
149 int,
150 float,
151 np.int8,
152 np.uint8,
153 np.int32,
154 np.uint32,
155 np.int64,
156 np.uint64,
157 np.double,
158)
159
160LiteralT = Union[cmh.Literal, IntegralT, bool]
161BoolVarT = cmh.Literal
162VariableT = Union["IntVar", IntegralT]
163
164# We need to add 'IntVar' for pytype.
165LinearExprT = Union[LinearExpr, "IntVar", IntegralT]
166ObjLinearExprT = Union[LinearExpr, NumberT]
167
168ArcT = Tuple[IntegralT, IntegralT, LiteralT]
169_IndexOrSeries = Union[pd.Index, pd.Series]
170
171
172def display_bounds(bounds: Sequence[int]) -> str:
173 """Displays a flattened list of intervals."""
174 out = ""
175 for i in range(0, len(bounds), 2):
176 if i != 0:
177 out += ", "
178 if bounds[i] == bounds[i + 1]:
179 out += str(bounds[i])
180 else:
181 out += str(bounds[i]) + ".." + str(bounds[i + 1])
182 return out
183
184
185def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str:
186 """Returns a short name of an integer variable, or its negation."""
187 if i < 0:
188 return f"not({short_name(model, -i - 1)})"
189 v = model.variables[i]
190 if v.name:
191 return v.name
192 elif len(v.domain) == 2 and v.domain[0] == v.domain[1]:
193 return str(v.domain[0])
194 else:
195 return f"[{display_bounds(v.domain)}]"
196
197
199 model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto
200) -> str:
201 """Pretty-print LinearExpressionProto instances."""
202 if not e.vars:
203 return str(e.offset)
204 if len(e.vars) == 1:
205 var_name = short_name(model, e.vars[0])
206 coeff = e.coeffs[0]
207 result = ""
208 if coeff == 1:
209 result = var_name
210 elif coeff == -1:
211 result = f"-{var_name}"
212 elif coeff != 0:
213 result = f"{coeff} * {var_name}"
214 if e.offset > 0:
215 result = f"{result} + {e.offset}"
216 elif e.offset < 0:
217 result = f"{result} - {-e.offset}"
218 return result
219 # TODO(user): Support more than affine expressions.
220 return str(e)
221
222
223class IntVar(cmh.BaseIntVar):
224 """An integer variable.
225
226 An IntVar is an object that can take on any integer value within defined
227 ranges. Variables appear in constraint like:
228
229 x + y >= 5
230 AllDifferent([x, y, z])
231
232 Solving a model is equivalent to finding, for each variable, a single value
233 from the set of initial values (called the initial domain), such that the
234 model is feasible, or optimal if you provided an objective function.
235 """
236
238 self,
239 model: cp_model_pb2.CpModelProto,
240 domain: Union[int, sorted_interval_list.Domain],
241 is_boolean: bool,
242 name: Optional[str],
243 ) -> None:
244 """See CpModel.new_int_var below."""
245 self.__var: cp_model_pb2.IntegerVariableProto
246 # Python do not support multiple __init__ methods.
247 # This method is only called from the CpModel class.
248 # We hack the parameter to support the two cases:
249 # case 1:
250 # model is a CpModelProto, domain is a Domain, and name is a string.
251 # case 2:
252 # model is a CpModelProto, domain is an index (int), and name is None.
253 if isinstance(domain, IntegralTypes) and name is None:
254 cmh.BaseIntVar.__init__(self, int(domain), is_boolean)
255 self.__var = model.variables[domain]
256 else:
257 cmh.BaseIntVar.__init__(self, len(model.variables), is_boolean)
258 self.__var = model.variables.add()
259 self.__var.domain.extend(
260 cast(sorted_interval_list.Domain, domain).flattened_intervals()
261 )
262 if name is not None:
263 self.__var.name = name
264
265 @property
266 def proto(self) -> cp_model_pb2.IntegerVariableProto:
267 """Returns the variable protobuf."""
268 return self.__var
269
270 def is_equal_to(self, other: Any) -> bool:
271 """Returns true if self == other in the python sense."""
272 if not isinstance(other, IntVar):
273 return False
274 return self.index == other.index
275
276 def __str__(self) -> str:
277 if not self.__var.name:
278 if (
279 len(self.__var.domain) == 2
280 and self.__var.domain[0] == self.__var.domain[1]
281 ):
282 # Special case for constants.
283 return str(self.__var.domain[0])
284 elif self.is_boolean:
285 return f"BooleanVar({self.__index})"
286 else:
287 return f"IntVar({self.__index})"
288 else:
289 return self.__var.name
290
291 def __repr__(self) -> str:
292 return f"{self}({display_bounds(self.__var.domain)})"
293
294 @property
295 def name(self) -> str:
296 if not self.__var or not self.__var.name:
297 return ""
298 return self.__var.name
299
300 # Pre PEP8 compatibility.
301 # pylint: disable=invalid-name
302 def Name(self) -> str:
303 return self.name
304
305 def Proto(self) -> cp_model_pb2.IntegerVariableProto:
306 return self.proto
307
308 # pylint: enable=invalid-name
309
310
312 """Base class for constraints.
313
314 Constraints are built by the CpModel through the add<XXX> methods.
315 Once created by the CpModel class, they are automatically added to the model.
316 The purpose of this class is to allow specification of enforcement literals
317 for this constraint.
318
319 b = model.new_bool_var('b')
320 x = model.new_int_var(0, 10, 'x')
321 y = model.new_int_var(0, 10, 'y')
322
323 model.add(x + 2 * y == 5).only_enforce_if(b.negated())
324 """
325
327 self,
328 cp_model: "CpModel",
329 ) -> None:
330 self.__index: int = len(cp_model.proto.constraints)
331 self.__cp_model: "CpModel" = cp_model
332 self.__constraint: cp_model_pb2.ConstraintProto = (
333 cp_model.proto.constraints.add()
334 )
335
336 @overload
337 def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": ...
338
339 @overload
340 def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": ...
341
342 def only_enforce_if(self, *boolvar) -> "Constraint":
343 """Adds an enforcement literal to the constraint.
344
345 This method adds one or more literals (that is, a boolean variable or its
346 negation) as enforcement literals. The conjunction of all these literals
347 determines whether the constraint is active or not. It acts as an
348 implication, so if the conjunction is true, it implies that the constraint
349 must be enforced. If it is false, then the constraint is ignored.
350
351 BoolOr, BoolAnd, and linear constraints all support enforcement literals.
352
353 Args:
354 *boolvar: One or more Boolean literals.
355
356 Returns:
357 self.
358 """
359 for lit in expand_generator_or_tuple(boolvar):
360 if (cmn.is_boolean(lit) and lit) or (
361 isinstance(lit, IntegralTypes) and lit == 1
362 ):
363 # Always true. Do nothing.
364 pass
365 elif (cmn.is_boolean(lit) and not lit) or (
366 isinstance(lit, IntegralTypes) and lit == 0
367 ):
368 self.__constraint.enforcement_literal.append(
369 self.__cp_model.new_constant(0).index
370 )
371 else:
372 self.__constraint.enforcement_literal.append(
373 cast(cmh.Literal, lit).index
374 )
375 return self
376
377 def with_name(self, name: str) -> "Constraint":
378 """Sets the name of the constraint."""
379 if name:
380 self.__constraint.name = name
381 else:
382 self.__constraint.ClearField("name")
383 return self
384
385 @property
386 def name(self) -> str:
387 """Returns the name of the constraint."""
388 if not self.__constraint or not self.__constraint.name:
389 return ""
390 return self.__constraint.name
391
392 @property
393 def index(self) -> int:
394 """Returns the index of the constraint in the model."""
395 return self.__index
396
397 @property
398 def proto(self) -> cp_model_pb2.ConstraintProto:
399 """Returns the constraint protobuf."""
400 return self.__constraint
401
402 # Pre PEP8 compatibility.
403 # pylint: disable=invalid-name
404 OnlyEnforceIf = only_enforce_if
405 WithName = with_name
406
407 def Name(self) -> str:
408 return self.name
409
410 def Index(self) -> int:
411 return self.index
412
413 def Proto(self) -> cp_model_pb2.ConstraintProto:
414 return self.proto
415
416 # pylint: enable=invalid-name
417
418
420 """Stores all integer variables of the model."""
421
422 def __init__(self) -> None:
423 self.__var_list: list[IntVar] = []
424
425 def append(self, var: IntVar) -> None:
426 assert var.index == len(self.__var_list)
427 self.__var_list.append(var)
428
429 def get(self, index: int) -> IntVar:
430 if index < 0 or index >= len(self.__var_list):
431 raise ValueError("Index out of bounds.")
432 return self.__var_list[index]
433
435 self,
436 proto: cp_model_pb2.LinearExpressionProto,
437 ) -> LinearExprT:
438 """Recreate a LinearExpr from a LinearExpressionProto."""
439 num_elements = len(proto.vars)
440 if num_elements == 0:
441 return proto.offset
442 elif num_elements == 1:
443 var = self.get(proto.vars[0])
444 return LinearExpr.affine(
445 var, proto.coeffs[0], proto.offset
446 ) # pytype: disable=bad-return-type
447 else:
448 variables = []
449 for var_index in range(len(proto.vars)):
450 var = self.get(var_index)
451 variables.append(var)
452 if proto.offset != 0:
453 coeffs = []
454 coeffs.extend(proto.coeffs)
455 coeffs.append(1)
456 variables.append(proto.offset)
457 return LinearExpr.weighted_sum(variables, coeffs)
458 else:
459 return LinearExpr.weighted_sum(variables, proto.coeffs)
460
461
463 """Represents an Interval variable.
464
465 An interval variable is both a constraint and a variable. It is defined by
466 three integer variables: start, size, and end.
467
468 It is a constraint because, internally, it enforces that start + size == end.
469
470 It is also a variable as it can appear in specific scheduling constraints:
471 NoOverlap, NoOverlap2D, Cumulative.
472
473 Optionally, an enforcement literal can be added to this constraint, in which
474 case these scheduling constraints will ignore interval variables with
475 enforcement literals assigned to false. Conversely, these constraints will
476 also set these enforcement literals to false if they cannot fit these
477 intervals into the schedule.
478
479 Raises:
480 ValueError: if start, size, end are not defined, or have the wrong type.
481 """
482
484 self,
485 model: cp_model_pb2.CpModelProto,
486 var_list: VariableList,
487 start: Union[cp_model_pb2.LinearExpressionProto, int],
488 size: Optional[cp_model_pb2.LinearExpressionProto],
489 end: Optional[cp_model_pb2.LinearExpressionProto],
490 is_present_index: Optional[int],
491 name: Optional[str],
492 ) -> None:
493 self.__model: cp_model_pb2.CpModelProto = model
494 self.__var_list: VariableList = var_list
495 self.__index: int
496 self.__ct: cp_model_pb2.ConstraintProto
497 # As with the IntVar::__init__ method, we hack the __init__ method to
498 # support two use cases:
499 # case 1: called when creating a new interval variable.
500 # {start|size|end} are linear expressions, is_present_index is either
501 # None or the index of a Boolean literal. name is a string
502 # case 2: called when querying an existing interval variable.
503 # start_index is an int, all parameters after are None.
504 if isinstance(start, int):
505 if size is not None:
506 raise ValueError("size should be None")
507 if end is not None:
508 raise ValueError("end should be None")
509 if is_present_index is not None:
510 raise ValueError("is_present_index should be None")
511 self.__index = cast(int, start)
512 self.__ct = model.constraints[self.__index]
513 else:
514 self.__index = len(model.constraints)
515 self.__ct = self.__model.constraints.add()
516 if start is None:
517 raise TypeError("start is not defined")
518 self.__ct.interval.start.CopyFrom(start)
519 if size is None:
520 raise TypeError("size is not defined")
521 self.__ct.interval.size.CopyFrom(size)
522 if end is None:
523 raise TypeError("end is not defined")
524 self.__ct.interval.end.CopyFrom(end)
525 if is_present_index is not None:
526 self.__ct.enforcement_literal.append(is_present_index)
527 if name:
528 self.__ct.name = name
529
530 @property
531 def index(self) -> int:
532 """Returns the index of the interval constraint in the model."""
533 return self.__index
534
535 @property
536 def proto(self) -> cp_model_pb2.IntervalConstraintProto:
537 """Returns the interval protobuf."""
538 return self.__ct.interval
539
540 def __str__(self):
541 return self.__ct.name
542
543 def __repr__(self):
544 interval = self.__ct.interval
545 if self.__ct.enforcement_literal:
546 return (
547 f"{self.__ct.name}(start ="
548 f" {short_expr_name(self.__model, interval.start)}, size ="
549 f" {short_expr_name(self.__model, interval.size)}, end ="
550 f" {short_expr_name(self.__model, interval.end)}, is_present ="
551 f" {short_name(self.__model, self.__ct.enforcement_literal[0])})"
552 )
553 else:
554 return (
555 f"{self.__ct.name}(start ="
556 f" {short_expr_name(self.__model, interval.start)}, size ="
557 f" {short_expr_name(self.__model, interval.size)}, end ="
558 f" {short_expr_name(self.__model, interval.end)})"
559 )
560
561 @property
562 def name(self) -> str:
563 if not self.__ct or not self.__ct.name:
564 return ""
565 return self.__ct.name
566
567 def start_expr(self) -> LinearExprT:
568 return self.__var_list.rebuild_expr(self.__ct.interval.start)
569
570 def size_expr(self) -> LinearExprT:
571 return self.__var_list.rebuild_expr(self.__ct.interval.size)
572
573 def end_expr(self) -> LinearExprT:
574 return self.__var_list.rebuild_expr(self.__ct.interval.end)
575
576 # Pre PEP8 compatibility.
577 # pylint: disable=invalid-name
578 def Name(self) -> str:
579 return self.name
580
581 def Index(self) -> int:
582 return self.index
583
584 def Proto(self) -> cp_model_pb2.IntervalConstraintProto:
585 return self.proto
586
587 StartExpr = start_expr
588 SizeExpr = size_expr
589 EndExpr = end_expr
590
591 # pylint: enable=invalid-name
592
593
594def object_is_a_true_literal(literal: LiteralT) -> bool:
595 """Checks if literal is either True, or a Boolean literals fixed to True."""
596 if isinstance(literal, IntVar):
597 proto = literal.proto
598 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1
599 if isinstance(literal, cmh.NotBooleanVariable):
600 proto = literal.negated().proto
601 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0
602 if isinstance(literal, IntegralTypes):
603 return int(literal) == 1
604 return False
605
606
607def object_is_a_false_literal(literal: LiteralT) -> bool:
608 """Checks if literal is either False, or a Boolean literals fixed to False."""
609 if isinstance(literal, IntVar):
610 proto = literal.proto
611 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0
612 if isinstance(literal, cmh.NotBooleanVariable):
613 proto = literal.negated().proto
614 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1
615 if isinstance(literal, IntegralTypes):
616 return int(literal) == 0
617 return False
618
619
621 """Methods for building a CP model.
622
623 Methods beginning with:
624
625 * ```New``` create integer, boolean, or interval variables.
626 * ```add``` create new constraints and add them to the model.
627 """
628
629 def __init__(self) -> None:
630 self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto()
631 self.__constant_map: Dict[IntegralT, int] = {}
632 self.__var_list: VariableList = VariableList()
633
634 # Naming.
635 @property
636 def name(self) -> str:
637 """Returns the name of the model."""
638 if not self.__model or not self.__model.name:
639 return ""
640 return self.__model.name
641
642 @name.setter
643 def name(self, name: str):
644 """Sets the name of the model."""
645 self.__model.name = name
646
647 # Integer variable.
648
649 def _append_int_var(self, var: IntVar) -> IntVar:
650 """Appends an integer variable to the list of variables."""
651 self.__var_list.append(var)
652 return var
653
654 def _get_int_var(self, index: int) -> IntVar:
655 return self.__var_list.get(index)
656
658 self,
659 proto: cp_model_pb2.LinearExpressionProto,
660 ) -> LinearExpr:
661 return self.__var_list.rebuild_expr(proto)
662
663 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar:
664 """Create an integer variable with domain [lb, ub].
665
666 The CP-SAT solver is limited to integer variables. If you have fractional
667 values, scale them up so that they become integers; if you have strings,
668 encode them as integers.
669
670 Args:
671 lb: Lower bound for the variable.
672 ub: Upper bound for the variable.
673 name: The name of the variable.
674
675 Returns:
676 a variable whose domain is [lb, ub].
677 """
678 domain_is_boolean = lb >= 0 and ub <= 1
679 return self._append_int_var(
680 IntVar(
681 self.__model,
682 sorted_interval_list.Domain(lb, ub),
683 domain_is_boolean,
684 name,
685 )
686 )
687
689 self, domain: sorted_interval_list.Domain, name: str
690 ) -> IntVar:
691 """Create an integer variable from a domain.
692
693 A domain is a set of integers specified by a collection of intervals.
694 For example, `model.new_int_var_from_domain(cp_model.
695 Domain.from_intervals([[1, 2], [4, 6]]), 'x')`
696
697 Args:
698 domain: An instance of the Domain class.
699 name: The name of the variable.
700
701 Returns:
702 a variable whose domain is the given domain.
703 """
704 domain_is_boolean = domain.min() >= 0 and domain.max() <= 1
705 return self._append_int_var(
706 IntVar(self.__model, domain, domain_is_boolean, name)
707 )
708
709 def new_bool_var(self, name: str) -> IntVar:
710 """Creates a 0-1 variable with the given name."""
711 return self._append_int_var(
712 IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name)
713 )
714
715 def new_constant(self, value: IntegralT) -> IntVar:
716 """Declares a constant integer."""
717 index: int = self.get_or_make_index_from_constant(value)
718 return self._get_int_var(index)
719
721 self,
722 name: str,
723 index: pd.Index,
724 lower_bounds: Union[IntegralT, pd.Series],
725 upper_bounds: Union[IntegralT, pd.Series],
726 ) -> pd.Series:
727 """Creates a series of (scalar-valued) variables with the given name.
728
729 Args:
730 name (str): Required. The name of the variable set.
731 index (pd.Index): Required. The index to use for the variable set.
732 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the
733 set. If a `pd.Series` is passed in, it will be based on the
734 corresponding values of the pd.Series.
735 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the
736 set. If a `pd.Series` is passed in, it will be based on the
737 corresponding values of the pd.Series.
738
739 Returns:
740 pd.Series: The variable set indexed by its corresponding dimensions.
741
742 Raises:
743 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
744 ValueError: if the `name` is not a valid identifier or already exists.
745 ValueError: if the `lowerbound` is greater than the `upperbound`.
746 ValueError: if the index of `lower_bound`, or `upper_bound` does not match
747 the input index.
748 """
749 if not isinstance(index, pd.Index):
750 raise TypeError("Non-index object is used as index")
751 if not name.isidentifier():
752 raise ValueError(f"name={name!r} is not a valid identifier")
753 if (
754 isinstance(lower_bounds, IntegralTypes)
755 and isinstance(upper_bounds, IntegralTypes)
756 and lower_bounds > upper_bounds
757 ):
758 raise ValueError(
759 f"lower_bound={lower_bounds} is greater than"
760 f" upper_bound={upper_bounds} for variable set={name}"
761 )
762
764 lower_bounds, index
765 )
767 upper_bounds, index
768 )
769 return pd.Series(
770 index=index,
771 data=[
772 # pylint: disable=g-complex-comprehension
773 self._append_int_var(
774 IntVar(
775 model=self.__model,
776 name=f"{name}[{i}]",
777 domain=sorted_interval_list.Domain(
778 lower_bounds[i], upper_bounds[i]
779 ),
780 is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1,
781 )
782 )
783 for i in index
784 ],
785 )
786
788 self,
789 name: str,
790 index: pd.Index,
791 ) -> pd.Series:
792 """Creates a series of (scalar-valued) variables with the given name.
793
794 Args:
795 name (str): Required. The name of the variable set.
796 index (pd.Index): Required. The index to use for the variable set.
797
798 Returns:
799 pd.Series: The variable set indexed by its corresponding dimensions.
800
801 Raises:
802 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
803 ValueError: if the `name` is not a valid identifier or already exists.
804 """
805 if not isinstance(index, pd.Index):
806 raise TypeError("Non-index object is used as index")
807 if not name.isidentifier():
808 raise ValueError(f"name={name!r} is not a valid identifier")
809 return pd.Series(
810 index=index,
811 data=[
812 # pylint: disable=g-complex-comprehension
813 self._append_int_var(
814 IntVar(
815 model=self.__model,
816 name=f"{name}[{i}]",
817 domain=sorted_interval_list.Domain(0, 1),
818 is_boolean=True,
819 )
820 )
821 for i in index
822 ],
823 )
824
825 # Linear constraints.
826
828 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT
829 ) -> Constraint:
830 """Adds the constraint: `lb <= linear_expr <= ub`."""
832 linear_expr, sorted_interval_list.Domain(lb, ub)
833 )
834
836 self,
837 linear_expr: LinearExprT,
838 domain: sorted_interval_list.Domain,
839 ) -> Constraint:
840 """Adds the constraint: `linear_expr` in `domain`."""
841 if isinstance(linear_expr, LinearExpr):
842 ble = BoundedLinearExpression(linear_expr, domain)
843 if not ble.ok:
844 raise TypeError(
845 "Cannot add a linear expression containing floating point"
846 f" coefficients or constants: {type(linear_expr).__name__!r}"
847 )
848 return self.add(ble)
849 if isinstance(linear_expr, IntegralTypes):
850 if not domain.contains(int(linear_expr)):
851 return self.add_bool_or([]) # Evaluate to false.
852 else:
853 return self.add_bool_and([]) # Evaluate to true.
854 raise TypeError(
855 "not supported:"
856 f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})"
857 )
858
859 def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint:
860 """Adds a `BoundedLinearExpression` to the model.
861
862 Args:
863 ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
864
865 Returns:
866 An instance of the `Constraint` class.
867
868 Raises:
869 TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean.
870 """
871 if isinstance(ct, BoundedLinearExpression):
872 result = Constraint(self)
873 model_ct = self.__model.constraints[result.index]
874 for var in ct.vars:
875 model_ct.linear.vars.append(var.index)
876 model_ct.linear.coeffs.extend(ct.coeffs)
877 model_ct.linear.domain.extend(
878 [
879 cmn.capped_subtraction(x, ct.offset)
880 for x in ct.bounds.flattened_intervals()
881 ]
882 )
883 return result
884 if ct and cmn.is_boolean(ct):
885 return self.add_bool_or([True])
886 if not ct and cmn.is_boolean(ct):
887 return self.add_bool_or([]) # Evaluate to false.
888 raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})")
889
890 # General Integer Constraints.
891
892 @overload
893 def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ...
894
895 @overload
896 def add_all_different(self, *expressions: LinearExprT) -> Constraint: ...
897
898 def add_all_different(self, *expressions):
899 """Adds AllDifferent(expressions).
900
901 This constraint forces all expressions to have different values.
902
903 Args:
904 *expressions: simple expressions of the form a * var + constant.
905
906 Returns:
907 An instance of the `Constraint` class.
908 """
909 ct = Constraint(self)
910 model_ct = self.__model.constraints[ct.index]
911 expanded = expand_generator_or_tuple(expressions)
912 model_ct.all_diff.exprs.extend(
913 self.parse_linear_expression(x) for x in expanded
914 )
915 return ct
916
918 self,
919 index: LinearExprT,
920 expressions: Sequence[LinearExprT],
921 target: LinearExprT,
922 ) -> Constraint:
923 """Adds the element constraint: `expressions[index] == target`.
924
925 Args:
926 index: The index of the selected expression in the array. It must be an
927 affine expression (a * var + b).
928 expressions: A list of affine expressions.
929 target: The expression constrained to be equal to the selected expression.
930 It must be an affine expression (a * var + b).
931
932 Returns:
933 An instance of the `Constraint` class.
934 """
935
936 if not expressions:
937 raise ValueError("add_element expects a non-empty expressions array")
938
939 if isinstance(index, IntegralTypes):
940 expression: LinearExprT = list(expressions)[int(index)]
941 return self.add(expression == target)
942
943 ct = Constraint(self)
944 model_ct = self.__model.constraints[ct.index]
945 model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index))
946 model_ct.element.exprs.extend(
947 [self.parse_linear_expression(e) for e in expressions]
948 )
949 model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target))
950 return ct
951
952 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
953 """Adds Circuit(arcs).
954
955 Adds a circuit constraint from a sparse list of arcs that encode the graph.
956
957 A circuit is a unique Hamiltonian cycle in a subgraph of the total
958 graph. In case a node 'i' is not in the cycle, then there must be a
959 loop arc 'i -> i' associated with a true literal. Otherwise
960 this constraint will fail.
961
962 Args:
963 arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
964 literal). The arc is selected in the circuit if the literal is true.
965 Both source_node and destination_node must be integers between 0 and the
966 number of nodes - 1.
967
968 Returns:
969 An instance of the `Constraint` class.
970
971 Raises:
972 ValueError: If the list of arcs is empty.
973 """
974 if not arcs:
975 raise ValueError("add_circuit expects a non-empty array of arcs")
976 ct = Constraint(self)
977 model_ct = self.__model.constraints[ct.index]
978 for arc in arcs:
979 model_ct.circuit.tails.append(arc[0])
980 model_ct.circuit.heads.append(arc[1])
981 model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2]))
982 return ct
983
984 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
985 """Adds a multiple circuit constraint, aka the 'VRP' constraint.
986
987 The direct graph where arc #i (from tails[i] to head[i]) is present iff
988 literals[i] is true must satisfy this set of properties:
989 - #incoming arcs == 1 except for node 0.
990 - #outgoing arcs == 1 except for node 0.
991 - for node zero, #incoming arcs == #outgoing arcs.
992 - There are no duplicate arcs.
993 - Self-arcs are allowed except for node 0.
994 - There is no cycle in this graph, except through node 0.
995
996 Args:
997 arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
998 literal). The arc is selected in the circuit if the literal is true.
999 Both source_node and destination_node must be integers between 0 and the
1000 number of nodes - 1.
1001
1002 Returns:
1003 An instance of the `Constraint` class.
1004
1005 Raises:
1006 ValueError: If the list of arcs is empty.
1007 """
1008 if not arcs:
1009 raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
1010 ct = Constraint(self)
1011 model_ct = self.__model.constraints[ct.index]
1012 for arc in arcs:
1013 model_ct.routes.tails.append(arc[0])
1014 model_ct.routes.heads.append(arc[1])
1015 model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2]))
1016 return ct
1017
1019 self,
1020 expressions: Sequence[LinearExprT],
1021 tuples_list: Iterable[Sequence[IntegralT]],
1022 ) -> Constraint:
1023 """Adds AllowedAssignments(expressions, tuples_list).
1024
1025 An AllowedAssignments constraint is a constraint on an array of affine
1026 expressions, which requires that when all expressions are assigned values,
1027 the
1028 resulting array equals one of the tuples in `tuple_list`.
1029
1030 Args:
1031 expressions: A list of affine expressions (a * var + b).
1032 tuples_list: A list of admissible tuples. Each tuple must have the same
1033 length as the expressions, and the ith value of a tuple corresponds to
1034 the ith expression.
1035
1036 Returns:
1037 An instance of the `Constraint` class.
1038
1039 Raises:
1040 TypeError: If a tuple does not have the same size as the list of
1041 expressions.
1042 ValueError: If the array of expressions is empty.
1043 """
1044
1045 if not expressions:
1046 raise ValueError(
1047 "add_allowed_assignments expects a non-empty expressions array"
1048 )
1049
1050 ct: Constraint = Constraint(self)
1051 model_ct = self.__model.constraints[ct.index]
1052 model_ct.table.exprs.extend(
1053 [self.parse_linear_expression(e) for e in expressions]
1054 )
1055 arity: int = len(expressions)
1056 for one_tuple in tuples_list:
1057 if len(one_tuple) != arity:
1058 raise TypeError(f"Tuple {one_tuple!r} has the wrong arity")
1059
1060 # duck-typing (no explicit type checks here)
1061 try:
1062 for one_tuple in tuples_list:
1063 model_ct.table.values.extend(one_tuple)
1064 except ValueError as ex:
1065 raise TypeError(
1066 "add_xxx_assignment: Not an integer or does not fit in an int64_t:"
1067 f" {type(ex.args).__name__!r}"
1068 ) from ex
1069
1070 return ct
1071
1073 self,
1074 expressions: Sequence[LinearExprT],
1075 tuples_list: Iterable[Sequence[IntegralT]],
1076 ) -> Constraint:
1077 """Adds add_forbidden_assignments(expressions, [tuples_list]).
1078
1079 A ForbiddenAssignments constraint is a constraint on an array of affine
1080 expressions where the list of impossible combinations is provided in the
1081 tuples list.
1082
1083 Args:
1084 expressions: A list of affine expressions (a * var + b).
1085 tuples_list: A list of forbidden tuples. Each tuple must have the same
1086 length as the expressions, and the *i*th value of a tuple corresponds to
1087 the *i*th expression.
1088
1089 Returns:
1090 An instance of the `Constraint` class.
1091
1092 Raises:
1093 TypeError: If a tuple does not have the same size as the list of
1094 expressions.
1095 ValueError: If the array of expressions is empty.
1096 """
1097
1098 if not expressions:
1099 raise ValueError(
1100 "add_forbidden_assignments expects a non-empty expressions array"
1101 )
1102
1103 index: int = len(self.__model.constraints)
1104 ct: Constraint = self.add_allowed_assignments(expressions, tuples_list)
1105 self.__model.constraints[index].table.negated = True
1106 return ct
1107
1109 self,
1110 transition_expressions: Sequence[LinearExprT],
1111 starting_state: IntegralT,
1112 final_states: Sequence[IntegralT],
1113 transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]],
1114 ) -> Constraint:
1115 """Adds an automaton constraint.
1116
1117 An automaton constraint takes a list of affine expressions (a * var + b) (of
1118 size *n*), an initial state, a set of final states, and a set of
1119 transitions. A transition is a triplet (*tail*, *transition*, *head*), where
1120 *tail* and *head* are states, and *transition* is the label of an arc from
1121 *head* to *tail*, corresponding to the value of one expression in the list
1122 of
1123 expressions.
1124
1125 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase
1126 contains the possible states of the automaton. The first state contains the
1127 initial state. The last phase contains the final states.
1128
1129 Between two consecutive phases *i* and *i* + 1, the automaton creates a set
1130 of arcs. For each transition (*tail*, *transition*, *head*), it will add
1131 an arc from the state *tail* of phase *i* and the state *head* of phase
1132 *i* + 1. This arc is labeled by the value *transition* of the expression
1133 `expressions[i]`. That is, this arc can only be selected if `expressions[i]`
1134 is assigned the value *transition*.
1135
1136 A feasible solution of this constraint is an assignment of expressions such
1137 that, starting from the initial state in phase 0, there is a path labeled by
1138 the values of the expressions that ends in one of the final states in the
1139 final phase.
1140
1141 Args:
1142 transition_expressions: A non-empty list of affine expressions (a * var +
1143 b) whose values correspond to the labels of the arcs traversed by the
1144 automaton.
1145 starting_state: The initial state of the automaton.
1146 final_states: A non-empty list of admissible final states.
1147 transition_triples: A list of transitions for the automaton, in the
1148 following format (current_state, variable_value, next_state).
1149
1150 Returns:
1151 An instance of the `Constraint` class.
1152
1153 Raises:
1154 ValueError: if `transition_expressions`, `final_states`, or
1155 `transition_triples` are empty.
1156 """
1157
1158 if not transition_expressions:
1159 raise ValueError(
1160 "add_automaton expects a non-empty transition_expressions array"
1161 )
1162 if not final_states:
1163 raise ValueError("add_automaton expects some final states")
1164
1165 if not transition_triples:
1166 raise ValueError("add_automaton expects some transition triples")
1167
1168 ct = Constraint(self)
1169 model_ct = self.__model.constraints[ct.index]
1170 model_ct.automaton.exprs.extend(
1171 [self.parse_linear_expression(e) for e in transition_expressions]
1172 )
1173 model_ct.automaton.starting_state = starting_state
1174 for v in final_states:
1175 model_ct.automaton.final_states.append(v)
1176 for t in transition_triples:
1177 if len(t) != 3:
1178 raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)")
1179 model_ct.automaton.transition_tail.append(t[0])
1180 model_ct.automaton.transition_label.append(t[1])
1181 model_ct.automaton.transition_head.append(t[2])
1182 return ct
1183
1185 self,
1186 variables: Sequence[VariableT],
1187 inverse_variables: Sequence[VariableT],
1188 ) -> Constraint:
1189 """Adds Inverse(variables, inverse_variables).
1190
1191 An inverse constraint enforces that if `variables[i]` is assigned a value
1192 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa.
1193
1194 Args:
1195 variables: An array of integer variables.
1196 inverse_variables: An array of integer variables.
1197
1198 Returns:
1199 An instance of the `Constraint` class.
1200
1201 Raises:
1202 TypeError: if variables and inverse_variables have different lengths, or
1203 if they are empty.
1204 """
1205
1206 if not variables or not inverse_variables:
1207 raise TypeError("The Inverse constraint does not accept empty arrays")
1208 if len(variables) != len(inverse_variables):
1209 raise TypeError(
1210 "In the inverse constraint, the two array variables and"
1211 " inverse_variables must have the same length."
1212 )
1213 ct = Constraint(self)
1214 model_ct = self.__model.constraints[ct.index]
1215 model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables])
1216 model_ct.inverse.f_inverse.extend(
1217 [self.get_or_make_index(x) for x in inverse_variables]
1218 )
1219 return ct
1220
1222 self,
1223 times: Iterable[LinearExprT],
1224 level_changes: Iterable[LinearExprT],
1225 min_level: int,
1226 max_level: int,
1227 ) -> Constraint:
1228 """Adds Reservoir(times, level_changes, min_level, max_level).
1229
1230 Maintains a reservoir level within bounds. The water level starts at 0, and
1231 at any time, it must be between min_level and max_level.
1232
1233 If the affine expression `times[i]` is assigned a value t, then the current
1234 level changes by `level_changes[i]`, which is constant, at time t.
1235
1236 Note that min level must be <= 0, and the max level must be >= 0. Please
1237 use fixed level_changes to simulate initial state.
1238
1239 Therefore, at any time:
1240 sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
1241
1242 Args:
1243 times: A list of 1-var affine expressions (a * x + b) which specify the
1244 time of the filling or emptying the reservoir.
1245 level_changes: A list of integer values that specifies the amount of the
1246 emptying or filling. Currently, variable demands are not supported.
1247 min_level: At any time, the level of the reservoir must be greater or
1248 equal than the min level.
1249 max_level: At any time, the level of the reservoir must be less or equal
1250 than the max level.
1251
1252 Returns:
1253 An instance of the `Constraint` class.
1254
1255 Raises:
1256 ValueError: if max_level < min_level.
1257
1258 ValueError: if max_level < 0.
1259
1260 ValueError: if min_level > 0
1261 """
1262
1263 if max_level < min_level:
1264 raise ValueError("Reservoir constraint must have a max_level >= min_level")
1265
1266 if max_level < 0:
1267 raise ValueError("Reservoir constraint must have a max_level >= 0")
1268
1269 if min_level > 0:
1270 raise ValueError("Reservoir constraint must have a min_level <= 0")
1271
1272 ct = Constraint(self)
1273 model_ct = self.__model.constraints[ct.index]
1274 model_ct.reservoir.time_exprs.extend(
1275 [self.parse_linear_expression(x) for x in times]
1276 )
1277 model_ct.reservoir.level_changes.extend(
1278 [self.parse_linear_expression(x) for x in level_changes]
1279 )
1280 model_ct.reservoir.min_level = min_level
1281 model_ct.reservoir.max_level = max_level
1282 return ct
1283
1285 self,
1286 times: Iterable[LinearExprT],
1287 level_changes: Iterable[LinearExprT],
1288 actives: Iterable[LiteralT],
1289 min_level: int,
1290 max_level: int,
1291 ) -> Constraint:
1292 """Adds Reservoir(times, level_changes, actives, min_level, max_level).
1293
1294 Maintains a reservoir level within bounds. The water level starts at 0, and
1295 at any time, it must be between min_level and max_level.
1296
1297 If the variable `times[i]` is assigned a value t, and `actives[i]` is
1298 `True`, then the current level changes by `level_changes[i]`, which is
1299 constant,
1300 at time t.
1301
1302 Note that min level must be <= 0, and the max level must be >= 0. Please
1303 use fixed level_changes to simulate initial state.
1304
1305 Therefore, at any time:
1306 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level,
1307 max_level]
1308
1309
1310 The array of boolean variables 'actives', if defined, indicates which
1311 actions are actually performed.
1312
1313 Args:
1314 times: A list of 1-var affine expressions (a * x + b) which specify the
1315 time of the filling or emptying the reservoir.
1316 level_changes: A list of integer values that specifies the amount of the
1317 emptying or filling. Currently, variable demands are not supported.
1318 actives: a list of boolean variables. They indicates if the
1319 emptying/refilling events actually take place.
1320 min_level: At any time, the level of the reservoir must be greater or
1321 equal than the min level.
1322 max_level: At any time, the level of the reservoir must be less or equal
1323 than the max level.
1324
1325 Returns:
1326 An instance of the `Constraint` class.
1327
1328 Raises:
1329 ValueError: if max_level < min_level.
1330
1331 ValueError: if max_level < 0.
1332
1333 ValueError: if min_level > 0
1334 """
1335
1336 if max_level < min_level:
1337 raise ValueError("Reservoir constraint must have a max_level >= min_level")
1338
1339 if max_level < 0:
1340 raise ValueError("Reservoir constraint must have a max_level >= 0")
1341
1342 if min_level > 0:
1343 raise ValueError("Reservoir constraint must have a min_level <= 0")
1344
1345 ct = Constraint(self)
1346 model_ct = self.__model.constraints[ct.index]
1347 model_ct.reservoir.time_exprs.extend(
1348 [self.parse_linear_expression(x) for x in times]
1349 )
1350 model_ct.reservoir.level_changes.extend(
1351 [self.parse_linear_expression(x) for x in level_changes]
1352 )
1353 model_ct.reservoir.active_literals.extend(
1354 [self.get_or_make_boolean_index(x) for x in actives]
1355 )
1356 model_ct.reservoir.min_level = min_level
1357 model_ct.reservoir.max_level = max_level
1358 return ct
1359
1361 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0
1362 ):
1363 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`."""
1364
1365 for i, bool_var in enumerate(bool_var_array):
1366 b_index = bool_var.index
1367 var_index = var.index
1368 model_ct = self.__model.constraints.add()
1369 model_ct.linear.vars.append(var_index)
1370 model_ct.linear.coeffs.append(1)
1371 offset_as_int = int(offset)
1372 model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i])
1373 model_ct.enforcement_literal.append(b_index)
1374
1375 model_ct = self.__model.constraints.add()
1376 model_ct.linear.vars.append(var_index)
1377 model_ct.linear.coeffs.append(1)
1378 model_ct.enforcement_literal.append(-b_index - 1)
1379 if offset + i - 1 >= INT_MIN:
1380 model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1])
1381 if offset + i + 1 <= INT_MAX:
1382 model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX])
1383
1384 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint:
1385 """Adds `a => b` (`a` implies `b`)."""
1386 ct = Constraint(self)
1387 model_ct = self.__model.constraints[ct.index]
1388 model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b))
1389 model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a))
1390 return ct
1391
1392 @overload
1393 def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ...
1394
1395 @overload
1396 def add_bool_or(self, *literals: LiteralT) -> Constraint: ...
1397
1398 def add_bool_or(self, *literals):
1399 """Adds `Or(literals) == true`: sum(literals) >= 1."""
1400 ct = Constraint(self)
1401 model_ct = self.__model.constraints[ct.index]
1402 model_ct.bool_or.literals.extend(
1403 [
1405 for x in expand_generator_or_tuple(literals)
1406 ]
1407 )
1408 return ct
1409
1410 @overload
1411 def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
1412
1413 @overload
1414 def add_at_least_one(self, *literals: LiteralT) -> Constraint: ...
1415
1416 def add_at_least_one(self, *literals):
1417 """Same as `add_bool_or`: `sum(literals) >= 1`."""
1418 return self.add_bool_or(*literals)
1419
1420 @overload
1421 def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
1422
1423 @overload
1424 def add_at_most_one(self, *literals: LiteralT) -> Constraint: ...
1425
1426 def add_at_most_one(self, *literals):
1427 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
1428 ct = Constraint(self)
1429 model_ct = self.__model.constraints[ct.index]
1430 model_ct.at_most_one.literals.extend(
1431 [
1433 for x in expand_generator_or_tuple(literals)
1434 ]
1435 )
1436 return ct
1437
1438 @overload
1439 def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
1440
1441 @overload
1442 def add_exactly_one(self, *literals: LiteralT) -> Constraint: ...
1443
1444 def add_exactly_one(self, *literals):
1445 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
1446 ct = Constraint(self)
1447 model_ct = self.__model.constraints[ct.index]
1448 model_ct.exactly_one.literals.extend(
1449 [
1451 for x in expand_generator_or_tuple(literals)
1452 ]
1453 )
1454 return ct
1455
1456 @overload
1457 def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ...
1458
1459 @overload
1460 def add_bool_and(self, *literals: LiteralT) -> Constraint: ...
1461
1462 def add_bool_and(self, *literals):
1463 """Adds `And(literals) == true`."""
1464 ct = Constraint(self)
1465 model_ct = self.__model.constraints[ct.index]
1466 model_ct.bool_and.literals.extend(
1467 [
1469 for x in expand_generator_or_tuple(literals)
1470 ]
1471 )
1472 return ct
1473
1474 @overload
1475 def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ...
1476
1477 @overload
1478 def add_bool_xor(self, *literals: LiteralT) -> Constraint: ...
1479
1480 def add_bool_xor(self, *literals):
1481 """Adds `XOr(literals) == true`.
1482
1483 In contrast to add_bool_or and add_bool_and, it does not support
1484 .only_enforce_if().
1485
1486 Args:
1487 *literals: the list of literals in the constraint.
1488
1489 Returns:
1490 An `Constraint` object.
1491 """
1492 ct = Constraint(self)
1493 model_ct = self.__model.constraints[ct.index]
1494 model_ct.bool_xor.literals.extend(
1495 [
1497 for x in expand_generator_or_tuple(literals)
1498 ]
1499 )
1500 return ct
1501
1503 self, target: LinearExprT, exprs: Iterable[LinearExprT]
1504 ) -> Constraint:
1505 """Adds `target == Min(exprs)`."""
1506 ct = Constraint(self)
1507 model_ct = self.__model.constraints[ct.index]
1508 model_ct.lin_max.exprs.extend(
1509 [self.parse_linear_expression(x, True) for x in exprs]
1510 )
1511 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True))
1512 return ct
1513
1515 self, target: LinearExprT, exprs: Iterable[LinearExprT]
1516 ) -> Constraint:
1517 """Adds `target == Max(exprs)`."""
1518 ct = Constraint(self)
1519 model_ct = self.__model.constraints[ct.index]
1520 model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs])
1521 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
1522 return ct
1523
1525 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
1526 ) -> Constraint:
1527 """Adds `target == num // denom` (integer division rounded towards 0)."""
1528 ct = Constraint(self)
1529 model_ct = self.__model.constraints[ct.index]
1530 model_ct.int_div.exprs.append(self.parse_linear_expression(num))
1531 model_ct.int_div.exprs.append(self.parse_linear_expression(denom))
1532 model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target))
1533 return ct
1534
1535 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
1536 """Adds `target == Abs(expr)`."""
1537 ct = Constraint(self)
1538 model_ct = self.__model.constraints[ct.index]
1539 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr))
1540 model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True))
1541 model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target))
1542 return ct
1543
1545 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
1546 ) -> Constraint:
1547 """Adds `target = expr % mod`.
1548
1549 It uses the C convention, that is the result is the remainder of the
1550 integral division rounded towards 0.
1551
1552 For example:
1553 * 10 % 3 = 1
1554 * -10 % 3 = -1
1555 * 10 % -3 = 1
1556 * -10 % -3 = -1
1557
1558 Args:
1559 target: the target expression.
1560 expr: the expression to compute the modulo of.
1561 mod: the modulus expression.
1562
1563 Returns:
1564 A `Constraint` object.
1565 """
1566 ct = Constraint(self)
1567 model_ct = self.__model.constraints[ct.index]
1568 model_ct.int_mod.exprs.append(self.parse_linear_expression(expr))
1569 model_ct.int_mod.exprs.append(self.parse_linear_expression(mod))
1570 model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target))
1571 return ct
1572
1574 self,
1575 target: LinearExprT,
1576 *expressions: Union[Iterable[LinearExprT], LinearExprT],
1577 ) -> Constraint:
1578 """Adds `target == expressions[0] * .. * expressions[n]`."""
1579 ct = Constraint(self)
1580 model_ct = self.__model.constraints[ct.index]
1581 model_ct.int_prod.exprs.extend(
1582 [
1583 self.parse_linear_expression(expr)
1584 for expr in expand_generator_or_tuple(expressions)
1585 ]
1586 )
1587 model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target))
1588 return ct
1589
1590 # Scheduling support
1591
1593 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str
1594 ) -> IntervalVar:
1595 """Creates an interval variable from start, size, and end.
1596
1597 An interval variable is a constraint, that is itself used in other
1598 constraints like NoOverlap.
1599
1600 Internally, it ensures that `start + size == end`.
1601
1602 Args:
1603 start: The start of the interval. It must be of the form a * var + b.
1604 size: The size of the interval. It must be of the form a * var + b.
1605 end: The end of the interval. It must be of the form a * var + b.
1606 name: The name of the interval variable.
1607
1608 Returns:
1609 An `IntervalVar` object.
1610 """
1611
1612 start_expr = self.parse_linear_expression(start)
1613 size_expr = self.parse_linear_expression(size)
1614 end_expr = self.parse_linear_expression(end)
1615 if len(start_expr.vars) > 1:
1616 raise TypeError(
1617 "cp_model.new_interval_var: start must be 1-var affine or constant."
1618 )
1619 if len(size_expr.vars) > 1:
1620 raise TypeError(
1621 "cp_model.new_interval_var: size must be 1-var affine or constant."
1622 )
1623 if len(end_expr.vars) > 1:
1624 raise TypeError(
1625 "cp_model.new_interval_var: end must be 1-var affine or constant."
1626 )
1627 return IntervalVar(
1628 self.__model,
1629 self.__var_list,
1630 start_expr,
1631 size_expr,
1632 end_expr,
1633 None,
1634 name,
1635 )
1636
1638 self,
1639 name: str,
1640 index: pd.Index,
1641 starts: Union[LinearExprT, pd.Series],
1642 sizes: Union[LinearExprT, pd.Series],
1643 ends: Union[LinearExprT, pd.Series],
1644 ) -> pd.Series:
1645 """Creates a series of interval variables with the given name.
1646
1647 Args:
1648 name (str): Required. The name of the variable set.
1649 index (pd.Index): Required. The index to use for the variable set.
1650 starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1651 set. If a `pd.Series` is passed in, it will be based on the
1652 corresponding values of the pd.Series.
1653 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1654 set. If a `pd.Series` is passed in, it will be based on the
1655 corresponding values of the pd.Series.
1656 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1657 set. If a `pd.Series` is passed in, it will be based on the
1658 corresponding values of the pd.Series.
1659
1660 Returns:
1661 pd.Series: The interval variable set indexed by its corresponding
1662 dimensions.
1663
1664 Raises:
1665 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1666 ValueError: if the `name` is not a valid identifier or already exists.
1667 ValueError: if the all the indexes do not match.
1668 """
1669 if not isinstance(index, pd.Index):
1670 raise TypeError("Non-index object is used as index")
1671 if not name.isidentifier():
1672 raise ValueError(f"name={name!r} is not a valid identifier")
1673
1677 interval_array = []
1678 for i in index:
1679 interval_array.append(
1680 self.new_interval_var(
1681 start=starts[i],
1682 size=sizes[i],
1683 end=ends[i],
1684 name=f"{name}[{i}]",
1685 )
1686 )
1687 return pd.Series(index=index, data=interval_array)
1688
1690 self, start: LinearExprT, size: IntegralT, name: str
1691 ) -> IntervalVar:
1692 """Creates an interval variable from start, and a fixed size.
1693
1694 An interval variable is a constraint, that is itself used in other
1695 constraints like NoOverlap.
1696
1697 Args:
1698 start: The start of the interval. It must be of the form a * var + b.
1699 size: The size of the interval. It must be an integer value.
1700 name: The name of the interval variable.
1701
1702 Returns:
1703 An `IntervalVar` object.
1704 """
1705 start_expr = self.parse_linear_expression(start)
1706 size_expr = self.parse_linear_expression(size)
1707 end_expr = self.parse_linear_expression(start + size)
1708 if len(start_expr.vars) > 1:
1709 raise TypeError(
1710 "cp_model.new_interval_var: start must be affine or constant."
1711 )
1712 return IntervalVar(
1713 self.__model,
1714 self.__var_list,
1715 start_expr,
1716 size_expr,
1717 end_expr,
1718 None,
1719 name,
1720 )
1721
1723 self,
1724 name: str,
1725 index: pd.Index,
1726 starts: Union[LinearExprT, pd.Series],
1727 sizes: Union[IntegralT, pd.Series],
1728 ) -> pd.Series:
1729 """Creates a series of interval variables with the given name.
1730
1731 Args:
1732 name (str): Required. The name of the variable set.
1733 index (pd.Index): Required. The index to use for the variable set.
1734 starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1735 set. If a `pd.Series` is passed in, it will be based on the
1736 corresponding values of the pd.Series.
1737 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1738 the set. If a `pd.Series` is passed in, it will be based on the
1739 corresponding values of the pd.Series.
1740
1741 Returns:
1742 pd.Series: The interval variable set indexed by its corresponding
1743 dimensions.
1744
1745 Raises:
1746 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1747 ValueError: if the `name` is not a valid identifier or already exists.
1748 ValueError: if the all the indexes do not match.
1749 """
1750 if not isinstance(index, pd.Index):
1751 raise TypeError("Non-index object is used as index")
1752 if not name.isidentifier():
1753 raise ValueError(f"name={name!r} is not a valid identifier")
1754
1757 interval_array = []
1758 for i in index:
1759 interval_array.append(
1761 start=starts[i],
1762 size=sizes[i],
1763 name=f"{name}[{i}]",
1764 )
1765 )
1766 return pd.Series(index=index, data=interval_array)
1767
1769 self,
1770 start: LinearExprT,
1771 size: LinearExprT,
1772 end: LinearExprT,
1773 is_present: LiteralT,
1774 name: str,
1775 ) -> IntervalVar:
1776 """Creates an optional interval var from start, size, end, and is_present.
1777
1778 An optional interval variable is a constraint, that is itself used in other
1779 constraints like NoOverlap. This constraint is protected by a presence
1780 literal that indicates if it is active or not.
1781
1782 Internally, it ensures that `is_present` implies `start + size ==
1783 end`.
1784
1785 Args:
1786 start: The start of the interval. It must be of the form a * var + b.
1787 size: The size of the interval. It must be of the form a * var + b.
1788 end: The end of the interval. It must be of the form a * var + b.
1789 is_present: A literal that indicates if the interval is active or not. A
1790 inactive interval is simply ignored by all constraints.
1791 name: The name of the interval variable.
1792
1793 Returns:
1794 An `IntervalVar` object.
1795 """
1796
1797 # Creates the IntervalConstraintProto object.
1798 is_present_index = self.get_or_make_boolean_index(is_present)
1799 start_expr = self.parse_linear_expression(start)
1800 size_expr = self.parse_linear_expression(size)
1801 end_expr = self.parse_linear_expression(end)
1802 if len(start_expr.vars) > 1:
1803 raise TypeError(
1804 "cp_model.new_interval_var: start must be affine or constant."
1805 )
1806 if len(size_expr.vars) > 1:
1807 raise TypeError(
1808 "cp_model.new_interval_var: size must be affine or constant."
1809 )
1810 if len(end_expr.vars) > 1:
1811 raise TypeError(
1812 "cp_model.new_interval_var: end must be affine or constant."
1813 )
1814 return IntervalVar(
1815 self.__model,
1816 self.__var_list,
1817 start_expr,
1818 size_expr,
1819 end_expr,
1820 is_present_index,
1821 name,
1822 )
1823
1825 self,
1826 name: str,
1827 index: pd.Index,
1828 starts: Union[LinearExprT, pd.Series],
1829 sizes: Union[LinearExprT, pd.Series],
1830 ends: Union[LinearExprT, pd.Series],
1831 are_present: Union[LiteralT, pd.Series],
1832 ) -> pd.Series:
1833 """Creates a series of interval variables with the given name.
1834
1835 Args:
1836 name (str): Required. The name of the variable set.
1837 index (pd.Index): Required. The index to use for the variable set.
1838 starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1839 set. If a `pd.Series` is passed in, it will be based on the
1840 corresponding values of the pd.Series.
1841 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1842 set. If a `pd.Series` is passed in, it will be based on the
1843 corresponding values of the pd.Series.
1844 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1845 set. If a `pd.Series` is passed in, it will be based on the
1846 corresponding values of the pd.Series.
1847 are_present (Union[LiteralT, pd.Series]): The performed literal of each
1848 interval in the set. If a `pd.Series` is passed in, it will be based on
1849 the corresponding values of the pd.Series.
1850
1851 Returns:
1852 pd.Series: The interval variable set indexed by its corresponding
1853 dimensions.
1854
1855 Raises:
1856 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1857 ValueError: if the `name` is not a valid identifier or already exists.
1858 ValueError: if the all the indexes do not match.
1859 """
1860 if not isinstance(index, pd.Index):
1861 raise TypeError("Non-index object is used as index")
1862 if not name.isidentifier():
1863 raise ValueError(f"name={name!r} is not a valid identifier")
1864
1868 are_present = _convert_to_literal_series_and_validate_index(are_present, index)
1869
1870 interval_array = []
1871 for i in index:
1872 interval_array.append(
1874 start=starts[i],
1875 size=sizes[i],
1876 end=ends[i],
1877 is_present=are_present[i],
1878 name=f"{name}[{i}]",
1879 )
1880 )
1881 return pd.Series(index=index, data=interval_array)
1882
1884 self,
1885 start: LinearExprT,
1886 size: IntegralT,
1887 is_present: LiteralT,
1888 name: str,
1889 ) -> IntervalVar:
1890 """Creates an interval variable from start, and a fixed size.
1891
1892 An interval variable is a constraint, that is itself used in other
1893 constraints like NoOverlap.
1894
1895 Args:
1896 start: The start of the interval. It must be of the form a * var + b.
1897 size: The size of the interval. It must be an integer value.
1898 is_present: A literal that indicates if the interval is active or not. A
1899 inactive interval is simply ignored by all constraints.
1900 name: The name of the interval variable.
1901
1902 Returns:
1903 An `IntervalVar` object.
1904 """
1905 start_expr = self.parse_linear_expression(start)
1906 size_expr = self.parse_linear_expression(size)
1907 end_expr = self.parse_linear_expression(start + size)
1908 if len(start_expr.vars) > 1:
1909 raise TypeError(
1910 "cp_model.new_interval_var: start must be affine or constant."
1911 )
1912 is_present_index = self.get_or_make_boolean_index(is_present)
1913 return IntervalVar(
1914 self.__model,
1915 self.__var_list,
1916 start_expr,
1917 size_expr,
1918 end_expr,
1919 is_present_index,
1920 name,
1921 )
1922
1924 self,
1925 name: str,
1926 index: pd.Index,
1927 starts: Union[LinearExprT, pd.Series],
1928 sizes: Union[IntegralT, pd.Series],
1929 are_present: Union[LiteralT, pd.Series],
1930 ) -> pd.Series:
1931 """Creates a series of interval variables with the given name.
1932
1933 Args:
1934 name (str): Required. The name of the variable set.
1935 index (pd.Index): Required. The index to use for the variable set.
1936 starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1937 set. If a `pd.Series` is passed in, it will be based on the
1938 corresponding values of the pd.Series.
1939 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1940 the set. If a `pd.Series` is passed in, it will be based on the
1941 corresponding values of the pd.Series.
1942 are_present (Union[LiteralT, pd.Series]): The performed literal of each
1943 interval in the set. If a `pd.Series` is passed in, it will be based on
1944 the corresponding values of the pd.Series.
1945
1946 Returns:
1947 pd.Series: The interval variable set indexed by its corresponding
1948 dimensions.
1949
1950 Raises:
1951 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1952 ValueError: if the `name` is not a valid identifier or already exists.
1953 ValueError: if the all the indexes do not match.
1954 """
1955 if not isinstance(index, pd.Index):
1956 raise TypeError("Non-index object is used as index")
1957 if not name.isidentifier():
1958 raise ValueError(f"name={name!r} is not a valid identifier")
1959
1962 are_present = _convert_to_literal_series_and_validate_index(are_present, index)
1963 interval_array = []
1964 for i in index:
1965 interval_array.append(
1967 start=starts[i],
1968 size=sizes[i],
1969 is_present=are_present[i],
1970 name=f"{name}[{i}]",
1971 )
1972 )
1973 return pd.Series(index=index, data=interval_array)
1974
1975 def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint:
1976 """Adds NoOverlap(interval_vars).
1977
1978 A NoOverlap constraint ensures that all present intervals do not overlap
1979 in time.
1980
1981 Args:
1982 interval_vars: The list of interval variables to constrain.
1983
1984 Returns:
1985 An instance of the `Constraint` class.
1986 """
1987 ct = Constraint(self)
1988 model_ct = self.__model.constraints[ct.index]
1989 model_ct.no_overlap.intervals.extend(
1990 [self.get_interval_index(x) for x in interval_vars]
1991 )
1992 return ct
1993
1995 self,
1996 x_intervals: Iterable[IntervalVar],
1997 y_intervals: Iterable[IntervalVar],
1998 ) -> Constraint:
1999 """Adds NoOverlap2D(x_intervals, y_intervals).
2000
2001 A NoOverlap2D constraint ensures that all present rectangles do not overlap
2002 on a plane. Each rectangle is aligned with the X and Y axis, and is defined
2003 by two intervals which represent its projection onto the X and Y axis.
2004
2005 Furthermore, one box is optional if at least one of the x or y interval is
2006 optional.
2007
2008 Args:
2009 x_intervals: The X coordinates of the rectangles.
2010 y_intervals: The Y coordinates of the rectangles.
2011
2012 Returns:
2013 An instance of the `Constraint` class.
2014 """
2015 ct = Constraint(self)
2016 model_ct = self.__model.constraints[ct.index]
2017 model_ct.no_overlap_2d.x_intervals.extend(
2018 [self.get_interval_index(x) for x in x_intervals]
2019 )
2020 model_ct.no_overlap_2d.y_intervals.extend(
2021 [self.get_interval_index(x) for x in y_intervals]
2022 )
2023 return ct
2024
2026 self,
2027 intervals: Iterable[IntervalVar],
2028 demands: Iterable[LinearExprT],
2029 capacity: LinearExprT,
2030 ) -> Constraint:
2031 """Adds Cumulative(intervals, demands, capacity).
2032
2033 This constraint enforces that:
2034
2035 for all t:
2036 sum(demands[i]
2037 if (start(intervals[i]) <= t < end(intervals[i])) and
2038 (intervals[i] is present)) <= capacity
2039
2040 Args:
2041 intervals: The list of intervals.
2042 demands: The list of demands for each interval. Each demand must be >= 0.
2043 Each demand can be a 1-var affine expression (a * x + b).
2044 capacity: The maximum capacity of the cumulative constraint. It can be a
2045 1-var affine expression (a * x + b).
2046
2047 Returns:
2048 An instance of the `Constraint` class.
2049 """
2050 cumulative = Constraint(self)
2051 model_ct = self.__model.constraints[cumulative.index]
2052 model_ct.cumulative.intervals.extend(
2053 [self.get_interval_index(x) for x in intervals]
2054 )
2055 for d in demands:
2056 model_ct.cumulative.demands.append(self.parse_linear_expression(d))
2057 model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity))
2058 return cumulative
2059
2060 # Support for model cloning.
2061 def clone(self) -> "CpModel":
2062 """Reset the model, and creates a new one from a CpModelProto instance."""
2063 clone = CpModel()
2064 clone.proto.CopyFrom(self.proto)
2065 clone.rebuild_var_and_constant_map()
2066 return clone
2067
2068 def __copy__(self):
2069 return self.clone()
2070
2071 def __deepcopy__(self, memo):
2072 return self.clone()
2073
2075 """Internal method used during model cloning."""
2076 for i, var in enumerate(self.__model.variables):
2077 if len(var.domain) == 2 and var.domain[0] == var.domain[1]:
2078 self.__constant_map[var.domain[0]] = i
2079 is_boolean = (
2080 len(var.domain) == 2 and var.domain[0] >= 0 and var.domain[1] <= 1
2081 )
2082 self.__var_list.append(IntVar(self.__model, i, is_boolean, None))
2083
2084 def get_bool_var_from_proto_index(self, index: int) -> IntVar:
2085 """Returns an already created Boolean variable from its index."""
2086 result = self._get_int_var(index)
2087 if not result.is_boolean:
2088 raise ValueError(
2089 f"get_bool_var_from_proto_index: index {index} does not reference a"
2090 " boolean variable"
2091 )
2092 return result
2093
2094 def get_int_var_from_proto_index(self, index: int) -> IntVar:
2095 """Returns an already created integer variable from its index."""
2096 return self._get_int_var(index)
2097
2098 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
2099 """Returns an already created interval variable from its index."""
2100 if index < 0 or index >= len(self.__model.constraints):
2101 raise ValueError(
2102 f"get_interval_var_from_proto_index: out of bound index {index}"
2103 )
2104 ct = self.__model.constraints[index]
2105 if not ct.HasField("interval"):
2106 raise ValueError(
2107 f"get_interval_var_from_proto_index: index {index} does not"
2108 " reference an" + " interval variable"
2109 )
2110
2111 return IntervalVar(self.__model, self.__var_list, index, None, None, None, None)
2112
2113 # Helpers.
2114
2115 def __str__(self) -> str:
2116 return str(self.__model)
2117
2118 @property
2119 def proto(self) -> cp_model_pb2.CpModelProto:
2120 """Returns the underlying CpModelProto."""
2121 return self.__model
2122
2123 def negated(self, index: int) -> int:
2124 return -index - 1
2125
2126 def get_or_make_index(self, arg: VariableT) -> int:
2127 """Returns the index of a variable, its negation, or a number."""
2128 if isinstance(arg, IntVar):
2129 return arg.index
2130 if isinstance(arg, IntegralTypes):
2131 return self.get_or_make_index_from_constant(arg)
2132 raise TypeError(
2133 f"NotSupported: model.get_or_make_index({type(arg).__name__!r})"
2134 )
2135
2136 def get_or_make_boolean_index(self, arg: LiteralT) -> int:
2137 """Returns an index from a boolean expression."""
2138 if isinstance(arg, IntVar):
2140 return arg.index
2141 if isinstance(arg, cmh.NotBooleanVariable):
2142 self.assert_is_boolean_variable(arg.negated())
2143 return arg.index
2144 if isinstance(arg, IntegralTypes):
2145 if arg == ~False: # -1
2146 return self.get_or_make_index_from_constant(1)
2147 if arg == ~True: # -2
2148 return self.get_or_make_index_from_constant(0)
2149 arg = cmn.assert_is_zero_or_one(arg)
2150 return self.get_or_make_index_from_constant(arg)
2151 if cmn.is_boolean(arg):
2152 return self.get_or_make_index_from_constant(int(arg))
2153 raise TypeError(
2154 "not supported:" f" model.get_or_make_boolean_index({type(arg).__name__!r})"
2155 )
2156
2157 def get_interval_index(self, arg: IntervalVar) -> int:
2158 if not isinstance(arg, IntervalVar):
2159 raise TypeError(
2160 f"NotSupported: model.get_interval_index({type(arg).__name__!r})"
2161 )
2162 return arg.index
2163
2164 def get_or_make_index_from_constant(self, value: IntegralT) -> int:
2165 if value in self.__constant_map:
2166 return self.__constant_map[value]
2167 constant_var = self.new_int_var(value, value, "")
2168 self.__constant_map[value] = constant_var.index
2169 return constant_var.index
2170
2172 self, linear_expr: LinearExprT, negate: bool = False
2173 ) -> cp_model_pb2.LinearExpressionProto:
2174 """Returns a LinearExpressionProto built from a LinearExpr instance."""
2175 result: cp_model_pb2.LinearExpressionProto = (
2176 cp_model_pb2.LinearExpressionProto()
2177 )
2178 mult = -1 if negate else 1
2179 if isinstance(linear_expr, IntegralTypes):
2180 result.offset = int(linear_expr) * mult
2181 return result
2182
2183 # Raises TypeError if linear_expr is not an integer.
2184 flat_expr = cmh.FlatIntExpr(linear_expr)
2185 result.offset = flat_expr.offset
2186 for var in flat_expr.vars:
2187 result.vars.append(var.index)
2188 for coeff in flat_expr.coeffs:
2189 result.coeffs.append(coeff * mult)
2190 return result
2191
2192 def _set_objective(self, obj: ObjLinearExprT, minimize: bool):
2193 """Sets the objective of the model."""
2194 self.clear_objective()
2195 if isinstance(obj, IntegralTypes):
2196 self.__model.objective.offset = int(obj)
2197 self.__model.objective.scaling_factor = 1.0
2198 elif isinstance(obj, LinearExpr):
2199 if obj.is_integer():
2200 int_obj = cmh.FlatIntExpr(obj)
2201 for var in int_obj.vars:
2202 self.__model.objective.vars.append(var.index)
2203 if minimize:
2204 self.__model.objective.scaling_factor = 1.0
2205 self.__model.objective.offset = int_obj.offset
2206 self.__model.objective.coeffs.extend(int_obj.coeffs)
2207 else:
2208 self.__model.objective.scaling_factor = -1.0
2209 self.__model.objective.offset = -int_obj.offset
2210 for c in int_obj.coeffs:
2211 self.__model.objective.coeffs.append(-c)
2212 else:
2213 float_obj = cmh.FlatFloatExpr(obj)
2214 for var in float_obj.vars:
2215 self.__model.floating_point_objective.vars.append(var.index)
2216 self.__model.floating_point_objective.coeffs.extend(float_obj.coeffs)
2217 self.__model.floating_point_objective.maximize = not minimize
2218 self.__model.floating_point_objective.offset = float_obj.offset
2219 else:
2220 raise TypeError(
2221 f"TypeError: {type(obj).__name__!r} is not a valid objective"
2222 )
2223
2224 def minimize(self, obj: ObjLinearExprT):
2225 """Sets the objective of the model to minimize(obj)."""
2226 self._set_objective(obj, minimize=True)
2227
2228 def maximize(self, obj: ObjLinearExprT):
2229 """Sets the objective of the model to maximize(obj)."""
2230 self._set_objective(obj, minimize=False)
2231
2232 def has_objective(self) -> bool:
2233 return self.__model.HasField("objective") or self.__model.HasField(
2234 "floating_point_objective"
2235 )
2236
2238 self.__model.ClearField("objective")
2239 self.__model.ClearField("floating_point_objective")
2240
2242 self,
2243 variables: Sequence[IntVar],
2244 var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy,
2245 domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy,
2246 ) -> None:
2247 """Adds a search strategy to the model.
2248
2249 Args:
2250 variables: a list of variables this strategy will assign.
2251 var_strategy: heuristic to choose the next variable to assign.
2252 domain_strategy: heuristic to reduce the domain of the selected variable.
2253 Currently, this is advanced code: the union of all strategies added to
2254 the model must be complete, i.e. instantiates all variables. Otherwise,
2255 solve() will fail.
2256 """
2257
2258 strategy: cp_model_pb2.DecisionStrategyProto = (
2259 self.__model.search_strategy.add()
2260 )
2261 for v in variables:
2262 expr = strategy.exprs.add()
2263 if v.index >= 0:
2264 expr.vars.append(v.index)
2265 expr.coeffs.append(1)
2266 else:
2267 expr.vars.append(self.negated(v.index))
2268 expr.coeffs.append(-1)
2269 expr.offset = 1
2270
2271 strategy.variable_selection_strategy = var_strategy
2272 strategy.domain_reduction_strategy = domain_strategy
2273
2274 def model_stats(self) -> str:
2275 """Returns a string containing some model statistics."""
2276 return cmh.CpSatHelper.model_stats(self.__model)
2277
2278 def validate(self) -> str:
2279 """Returns a string indicating that the model is invalid."""
2280 return cmh.CpSatHelper.validate_model(self.__model)
2281
2282 def export_to_file(self, file: str) -> bool:
2283 """Write the model as a protocol buffer to 'file'.
2284
2285 Args:
2286 file: file to write the model to. If the filename ends with 'txt', the
2287 model will be written as a text file, otherwise, the binary format will
2288 be used.
2289
2290 Returns:
2291 True if the model was correctly written.
2292 """
2293 return cmh.CpSatHelper.write_model_to_file(self.__model, file)
2294
2295 @overload
2296 def add_hint(self, var: IntVar, value: int) -> None: ...
2297
2298 @overload
2299 def add_hint(self, literal: BoolVarT, value: bool) -> None: ...
2300
2301 def add_hint(self, var, value) -> None:
2302 """Adds 'var == value' as a hint to the solver."""
2303 if var.index >= 0:
2304 self.__model.solution_hint.vars.append(self.get_or_make_index(var))
2305 self.__model.solution_hint.values.append(int(value))
2306 else:
2307 self.__model.solution_hint.vars.append(self.negated(var.index))
2308 self.__model.solution_hint.values.append(int(not value))
2309
2310 def clear_hints(self):
2311 """Removes any solution hint from the model."""
2312 self.__model.ClearField("solution_hint")
2313
2314 def add_assumption(self, lit: LiteralT) -> None:
2315 """Adds the literal to the model as assumptions."""
2316 self.__model.assumptions.append(self.get_or_make_boolean_index(lit))
2317
2318 def add_assumptions(self, literals: Iterable[LiteralT]) -> None:
2319 """Adds the literals to the model as assumptions."""
2320 for lit in literals:
2321 self.add_assumption(lit)
2322
2323 def clear_assumptions(self) -> None:
2324 """Removes all assumptions from the model."""
2325 self.__model.ClearField("assumptions")
2326
2327 # Helpers.
2328 def assert_is_boolean_variable(self, x: LiteralT) -> None:
2329 if isinstance(x, IntVar):
2330 var = self.__model.variables[x.index]
2331 if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1:
2332 raise TypeError(
2333 f"TypeError: {type(x).__name__!r} is not a boolean variable"
2334 )
2335 elif not isinstance(x, cmh.NotBooleanVariable):
2336 raise TypeError(
2337 f"TypeError: {type(x).__name__!r} is not a boolean variable"
2338 )
2339
2340 # Compatibility with pre PEP8
2341 # pylint: disable=invalid-name
2342
2343 def Name(self) -> str:
2344 return self.name
2345
2346 def SetName(self, name: str) -> None:
2347 self.name = name
2348
2349 def Proto(self) -> cp_model_pb2.CpModelProto:
2350 return self.proto
2351
2352 NewIntVar = new_int_var
2353 NewIntVarFromDomain = new_int_var_from_domain
2354 NewBoolVar = new_bool_var
2355 NewConstant = new_constant
2356 NewIntVarSeries = new_int_var_series
2357 NewBoolVarSeries = new_bool_var_series
2358 AddLinearConstraint = add_linear_constraint
2359 AddLinearExpressionInDomain = add_linear_expression_in_domain
2360 Add = add
2361 AddAllDifferent = add_all_different
2362 AddElement = add_element
2363 AddCircuit = add_circuit
2364 AddMultipleCircuit = add_multiple_circuit
2365 AddAllowedAssignments = add_allowed_assignments
2366 AddForbiddenAssignments = add_forbidden_assignments
2367 AddAutomaton = add_automaton
2368 AddInverse = add_inverse
2369 AddReservoirConstraint = add_reservoir_constraint
2370 AddReservoirConstraintWithActive = add_reservoir_constraint_with_active
2371 AddImplication = add_implication
2372 AddBoolOr = add_bool_or
2373 AddAtLeastOne = add_at_least_one
2374 AddAtMostOne = add_at_most_one
2375 AddExactlyOne = add_exactly_one
2376 AddBoolAnd = add_bool_and
2377 AddBoolXOr = add_bool_xor
2378 AddMinEquality = add_min_equality
2379 AddMaxEquality = add_max_equality
2380 AddDivisionEquality = add_division_equality
2381 AddAbsEquality = add_abs_equality
2382 AddModuloEquality = add_modulo_equality
2383 AddMultiplicationEquality = add_multiplication_equality
2384 NewIntervalVar = new_interval_var
2385 NewIntervalVarSeries = new_interval_var_series
2386 NewFixedSizeIntervalVar = new_fixed_size_interval_var
2387 NewOptionalIntervalVar = new_optional_interval_var
2388 NewOptionalIntervalVarSeries = new_optional_interval_var_series
2389 NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var
2390 NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series
2391 AddNoOverlap = add_no_overlap
2392 AddNoOverlap2D = add_no_overlap_2d
2393 AddCumulative = add_cumulative
2394 Clone = clone
2395 GetBoolVarFromProtoIndex = get_bool_var_from_proto_index
2396 GetIntVarFromProtoIndex = get_int_var_from_proto_index
2397 GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index
2398 Minimize = minimize
2399 Maximize = maximize
2400 HasObjective = has_objective
2401 ClearObjective = clear_objective
2402 AddDecisionStrategy = add_decision_strategy
2403 ModelStats = model_stats
2404 Validate = validate
2405 ExportToFile = export_to_file
2406 AddHint = add_hint
2407 ClearHints = clear_hints
2408 AddAssumption = add_assumption
2409 AddAssumptions = add_assumptions
2410 ClearAssumptions = clear_assumptions
2411
2412 # pylint: enable=invalid-name
2413
2414
2415@overload
2417 args: Union[Tuple[LiteralT, ...], Iterable[LiteralT]],
2418) -> Union[Iterable[LiteralT], LiteralT]: ...
2419
2420
2421@overload
2423 args: Union[Tuple[LinearExprT, ...], Iterable[LinearExprT]],
2424) -> Union[Iterable[LinearExprT], LinearExprT]: ...
2425
2426
2428 if hasattr(args, "__len__"): # Tuple
2429 if len(args) != 1:
2430 return args
2431 if isinstance(args[0], (NumberTypes, LinearExpr)):
2432 return args
2433 # Generator
2434 return args[0]
2435
2436
2438 """Main solver class.
2439
2440 The purpose of this class is to search for a solution to the model provided
2441 to the solve() method.
2442
2443 Once solve() is called, this class allows inspecting the solution found
2444 with the value() and boolean_value() methods, as well as general statistics
2445 about the solve procedure.
2446 """
2447
2448 def __init__(self) -> None:
2449 self.__response_wrapper: Optional[cmh.ResponseWrapper] = None
2450 self.parameters: sat_parameters_pb2.SatParameters = (
2451 sat_parameters_pb2.SatParameters()
2452 )
2453 self.log_callback: Optional[Callable[[str], None]] = None
2454 self.best_bound_callback: Optional[Callable[[float], None]] = None
2455 self.__solve_wrapper: Optional[cmh.SolveWrapper] = None
2456 self.__lock: threading.Lock = threading.Lock()
2457
2459 self,
2460 model: CpModel,
2461 solution_callback: Optional["CpSolverSolutionCallback"] = None,
2462 ) -> cp_model_pb2.CpSolverStatus:
2463 """Solves a problem and passes each solution to the callback if not null."""
2464 with self.__lock:
2465 self.__solve_wrapper = cmh.SolveWrapper()
2466
2467 self.__solve_wrapper.set_parameters(self.parameters)
2468 if solution_callback is not None:
2469 self.__solve_wrapper.add_solution_callback(solution_callback)
2470
2471 if self.log_callback is not None:
2472 self.__solve_wrapper.add_log_callback(self.log_callback)
2473
2474 if self.best_bound_callback is not None:
2475 self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback)
2476
2477 self.__response_wrapper = (
2478 self.__solve_wrapper.solve_and_return_response_wrapper(model.proto)
2479 )
2480
2481 if solution_callback is not None:
2482 self.__solve_wrapper.clear_solution_callback(solution_callback)
2483
2484 with self.__lock:
2485 self.__solve_wrapper = None
2486
2487 return self.__response_wrapper.status()
2488
2489 def stop_search(self) -> None:
2490 """Stops the current search asynchronously."""
2491 with self.__lock:
2492 if self.__solve_wrapper:
2494
2495 def value(self, expression: LinearExprT) -> int:
2496 """Returns the value of a linear expression after solve."""
2497 return self._checked_response.value(expression)
2498
2499 def values(self, variables: _IndexOrSeries) -> pd.Series:
2500 """Returns the values of the input variables.
2501
2502 If `variables` is a `pd.Index`, then the output will be indexed by the
2503 variables. If `variables` is a `pd.Series` indexed by the underlying
2504 dimensions, then the output will be indexed by the same underlying
2505 dimensions.
2506
2507 Args:
2508 variables (Union[pd.Index, pd.Series]): The set of variables from which to
2509 get the values.
2510
2511 Returns:
2512 pd.Series: The values of all variables in the set.
2513
2514 Raises:
2515 RuntimeError: if solve() has not been called.
2516 """
2517 if self.__response_wrapper is None:
2518 raise RuntimeError("solve() has not been called.")
2519 return pd.Series(
2520 data=[self.__response_wrapper.value(var) for var in variables],
2521 index=_get_index(variables),
2522 )
2523
2524 def boolean_value(self, literal: LiteralT) -> bool:
2525 """Returns the boolean value of a literal after solve."""
2526 return self._checked_response.boolean_value(literal)
2527
2528 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series:
2529 """Returns the values of the input variables.
2530
2531 If `variables` is a `pd.Index`, then the output will be indexed by the
2532 variables. If `variables` is a `pd.Series` indexed by the underlying
2533 dimensions, then the output will be indexed by the same underlying
2534 dimensions.
2535
2536 Args:
2537 variables (Union[pd.Index, pd.Series]): The set of variables from which to
2538 get the values.
2539
2540 Returns:
2541 pd.Series: The values of all variables in the set.
2542
2543 Raises:
2544 RuntimeError: if solve() has not been called.
2545 """
2546 if self.__response_wrapper is None:
2547 raise RuntimeError("solve() has not been called.")
2548 return pd.Series(
2549 data=[
2550 self.__response_wrapper.boolean_value(literal) for literal in variables
2551 ],
2552 index=_get_index(variables),
2553 )
2554
2555 @property
2556 def objective_value(self) -> float:
2557 """Returns the value of the objective after solve."""
2559
2560 @property
2561 def best_objective_bound(self) -> float:
2562 """Returns the best lower (upper) bound found when min(max)imizing."""
2564
2565 @property
2566 def num_booleans(self) -> int:
2567 """Returns the number of boolean variables managed by the SAT solver."""
2568 return self._checked_response.num_booleans()
2569
2570 @property
2571 def num_conflicts(self) -> int:
2572 """Returns the number of conflicts since the creation of the solver."""
2573 return self._checked_response.num_conflicts()
2574
2575 @property
2576 def num_branches(self) -> int:
2577 """Returns the number of search branches explored by the solver."""
2578 return self._checked_response.num_branches()
2579
2580 @property
2581 def wall_time(self) -> float:
2582 """Returns the wall time in seconds since the creation of the solver."""
2583 return self._checked_response.wall_time()
2584
2585 @property
2586 def user_time(self) -> float:
2587 """Returns the user time in seconds since the creation of the solver."""
2588 return self._checked_response.user_time()
2589
2590 @property
2591 def response_proto(self) -> cp_model_pb2.CpSolverResponse:
2592 """Returns the response object."""
2593 return self._checked_response.response()
2594
2595 def response_stats(self) -> str:
2596 """Returns some statistics on the solution found as a string."""
2597 return self._checked_response.response_stats()
2598
2600 """Returns the indices of the infeasible assumptions."""
2602
2603 def status_name(self, status: Optional[Any] = None) -> str:
2604 """Returns the name of the status returned by solve()."""
2605 if status is None:
2606 status = self._checked_response.status()
2607 return cp_model_pb2.CpSolverStatus.Name(status)
2608
2609 def solution_info(self) -> str:
2610 """Returns some information on the solve process.
2611
2612 Returns some information on how the solution was found, or the reason
2613 why the model or the parameters are invalid.
2614
2615 Raises:
2616 RuntimeError: if solve() has not been called.
2617 """
2618 return self._checked_response.solution_info()
2619
2620 @property
2621 def _checked_response(self) -> cmh.ResponseWrapper:
2622 """Checks solve() has been called, and returns a response wrapper."""
2623 if self.__response_wrapper is None:
2624 raise RuntimeError("solve() has not been called.")
2625 return self.__response_wrapper
2626
2627 # Compatibility with pre PEP8
2628 # pylint: disable=invalid-name
2629
2630 def BestObjectiveBound(self) -> float:
2631 return self.best_objective_bound
2632
2633 def BooleanValue(self, literal: LiteralT) -> bool:
2634 return self.boolean_value(literal)
2635
2636 def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series:
2637 return self.boolean_values(variables)
2638
2639 def NumBooleans(self) -> int:
2640 return self.num_booleans
2641
2642 def NumConflicts(self) -> int:
2643 return self.num_conflicts
2644
2645 def NumBranches(self) -> int:
2646 return self.num_branches
2647
2648 def ObjectiveValue(self) -> float:
2649 return self.objective_value
2650
2651 def ResponseProto(self) -> cp_model_pb2.CpSolverResponse:
2652 return self.response_proto
2653
2654 def ResponseStats(self) -> str:
2655 return self.response_stats()
2656
2658 self,
2659 model: CpModel,
2660 solution_callback: Optional["CpSolverSolutionCallback"] = None,
2661 ) -> cp_model_pb2.CpSolverStatus:
2662 return self.solve(model, solution_callback)
2663
2664 def SolutionInfo(self) -> str:
2665 return self.solution_info()
2666
2667 def StatusName(self, status: Optional[Any] = None) -> str:
2668 return self.status_name(status)
2669
2670 def StopSearch(self) -> None:
2671 self.stop_search()
2672
2673 def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]:
2675
2676 def UserTime(self) -> float:
2677 return self.user_time
2678
2679 def Value(self, expression: LinearExprT) -> int:
2680 return self.value(expression)
2681
2682 def Values(self, variables: _IndexOrSeries) -> pd.Series:
2683 return self.values(variables)
2684
2685 def WallTime(self) -> float:
2686 return self.wall_time
2687
2689 self, model: CpModel, callback: "CpSolverSolutionCallback"
2690 ) -> cp_model_pb2.CpSolverStatus:
2691 """DEPRECATED Use solve() with the callback argument."""
2692 warnings.warn(
2693 "solve_with_solution_callback is deprecated; use solve() with"
2694 + "the callback argument.",
2695 DeprecationWarning,
2696 )
2697 return self.solve(model, callback)
2698
2700 self, model: CpModel, callback: "CpSolverSolutionCallback"
2701 ) -> cp_model_pb2.CpSolverStatus:
2702 """DEPRECATED Use solve() with the right parameter.
2703
2704 Search for all solutions of a satisfiability problem.
2705
2706 This method searches for all feasible solutions of a given model.
2707 Then it feeds the solution to the callback.
2708
2709 Note that the model cannot contain an objective.
2710
2711 Args:
2712 model: The model to solve.
2713 callback: The callback that will be called at each solution.
2714
2715 Returns:
2716 The status of the solve:
2717
2718 * *FEASIBLE* if some solutions have been found
2719 * *INFEASIBLE* if the solver has proved there are no solution
2720 * *OPTIMAL* if all solutions have been found
2721 """
2722 warnings.warn(
2723 "search_for_all_solutions is deprecated; use solve() with"
2724 + "enumerate_all_solutions = True.",
2725 DeprecationWarning,
2726 )
2727 if model.has_objective():
2728 raise TypeError(
2729 "Search for all solutions is only defined on satisfiability problems"
2730 )
2731 # Store old parameter.
2732 enumerate_all = self.parameters.enumerate_all_solutions
2733 self.parameters.enumerate_all_solutions = True
2734
2735 status: cp_model_pb2.CpSolverStatus = self.solve(model, callback)
2736
2737 # Restore parameter.
2738 self.parameters.enumerate_all_solutions = enumerate_all
2739 return status
2740
2741
2742# pylint: enable=invalid-name
2743
2744
2745class CpSolverSolutionCallback(cmh.SolutionCallback):
2746 """Solution callback.
2747
2748 This class implements a callback that will be called at each new solution
2749 found during search.
2750
2751 The method on_solution_callback() will be called by the solver, and must be
2752 implemented. The current solution can be queried using the boolean_value()
2753 and value() methods.
2754
2755 These methods returns the same information as their counterpart in the
2756 `CpSolver` class.
2757 """
2758
2759 def __init__(self) -> None:
2760 cmh.SolutionCallback.__init__(self)
2761
2762 def OnSolutionCallback(self) -> None:
2763 """Proxy for the same method in snake case."""
2764 self.on_solution_callback()
2765
2766 def boolean_value(self, lit: LiteralT) -> bool:
2767 """Returns the boolean value of a boolean literal.
2768
2769 Args:
2770 lit: A boolean variable or its negation.
2771
2772 Returns:
2773 The Boolean value of the literal in the solution.
2774
2775 Raises:
2776 RuntimeError: if `lit` is not a boolean variable or its negation.
2777 """
2778 if not self.has_response():
2779 raise RuntimeError("solve() has not been called.")
2780 return self.BooleanValue(lit)
2781
2782 def value(self, expression: LinearExprT) -> int:
2783 """Evaluates an linear expression in the current solution.
2784
2785 Args:
2786 expression: a linear expression of the model.
2787
2788 Returns:
2789 An integer value equal to the evaluation of the linear expression
2790 against the current solution.
2791
2792 Raises:
2793 RuntimeError: if 'expression' is not a LinearExpr.
2794 """
2795 if not self.has_response():
2796 raise RuntimeError("solve() has not been called.")
2797 return self.Value(expression)
2798
2799 def has_response(self) -> bool:
2800 return self.HasResponse()
2801
2802 def stop_search(self) -> None:
2803 """Stops the current search asynchronously."""
2804 if not self.has_response():
2805 raise RuntimeError("solve() has not been called.")
2806 self.StopSearch()
2807
2808 @property
2809 def objective_value(self) -> float:
2810 """Returns the value of the objective after solve."""
2811 if not self.has_response():
2812 raise RuntimeError("solve() has not been called.")
2813 return self.ObjectiveValue()
2814
2815 @property
2816 def best_objective_bound(self) -> float:
2817 """Returns the best lower (upper) bound found when min(max)imizing."""
2818 if not self.has_response():
2819 raise RuntimeError("solve() has not been called.")
2820 return self.BestObjectiveBound()
2821
2822 @property
2823 def num_booleans(self) -> int:
2824 """Returns the number of boolean variables managed by the SAT solver."""
2825 if not self.has_response():
2826 raise RuntimeError("solve() has not been called.")
2827 return self.NumBooleans()
2828
2829 @property
2830 def num_conflicts(self) -> int:
2831 """Returns the number of conflicts since the creation of the solver."""
2832 if not self.has_response():
2833 raise RuntimeError("solve() has not been called.")
2834 return self.NumConflicts()
2835
2836 @property
2837 def num_branches(self) -> int:
2838 """Returns the number of search branches explored by the solver."""
2839 if not self.has_response():
2840 raise RuntimeError("solve() has not been called.")
2841 return self.NumBranches()
2842
2843 @property
2845 """Returns the number of integer propagations done by the solver."""
2846 if not self.has_response():
2847 raise RuntimeError("solve() has not been called.")
2848 return self.NumIntegerPropagations()
2849
2850 @property
2852 """Returns the number of Boolean propagations done by the solver."""
2853 if not self.has_response():
2854 raise RuntimeError("solve() has not been called.")
2855 return self.NumBooleanPropagations()
2856
2857 @property
2858 def deterministic_time(self) -> float:
2859 """Returns the determistic time in seconds since the creation of the solver."""
2860 if not self.has_response():
2861 raise RuntimeError("solve() has not been called.")
2862 return self.DeterministicTime()
2863
2864 @property
2865 def wall_time(self) -> float:
2866 """Returns the wall time in seconds since the creation of the solver."""
2867 if not self.has_response():
2868 raise RuntimeError("solve() has not been called.")
2869 return self.WallTime()
2870
2871 @property
2872 def user_time(self) -> float:
2873 """Returns the user time in seconds since the creation of the solver."""
2874 if not self.has_response():
2875 raise RuntimeError("solve() has not been called.")
2876 return self.UserTime()
2877
2878 @property
2879 def response_proto(self) -> cp_model_pb2.CpSolverResponse:
2880 """Returns the response object."""
2881 if not self.has_response():
2882 raise RuntimeError("solve() has not been called.")
2883 return self.Response()
2884
2885
2887 """Display the objective value and time of intermediate solutions."""
2888
2889 def __init__(self) -> None:
2890 CpSolverSolutionCallback.__init__(self)
2891 self.__solution_count = 0
2892 self.__start_time = time.time()
2893
2894 def on_solution_callback(self) -> None:
2895 """Called on each new solution."""
2896 current_time = time.time()
2897 obj = self.objective_value
2898 print(
2899 f"Solution {self.__solution_count}, time ="
2900 f" {current_time - self.__start_time:0.2f} s, objective = {obj}",
2901 flush=True,
2902 )
2903 self.__solution_count += 1
2904
2905 def solution_count(self) -> int:
2906 """Returns the number of solutions found."""
2907 return self.__solution_count
2908
2909
2911 """Print intermediate solutions (objective, variable values, time)."""
2912
2913 def __init__(self, variables: Sequence[IntVar]) -> None:
2914 CpSolverSolutionCallback.__init__(self)
2915 self.__variables: Sequence[IntVar] = variables
2916 self.__solution_count: int = 0
2917 self.__start_time: float = time.time()
2918
2919 def on_solution_callback(self) -> None:
2920 """Called on each new solution."""
2921 current_time = time.time()
2922 obj = self.objective_value
2923 print(
2924 f"Solution {self.__solution_count}, time ="
2925 f" {current_time - self.__start_time:0.2f} s, objective = {obj}"
2926 )
2927 for v in self.__variables:
2928 print(f" {v} = {self.value(v)}", end=" ")
2929 print(flush=True)
2930 self.__solution_count += 1
2931
2932 @property
2933 def solution_count(self) -> int:
2934 """Returns the number of solutions found."""
2935 return self.__solution_count
2936
2937
2939 """Print intermediate solutions (variable values, time)."""
2940
2941 def __init__(self, variables: Sequence[IntVar]) -> None:
2942 CpSolverSolutionCallback.__init__(self)
2943 self.__variables: Sequence[IntVar] = variables
2944 self.__solution_count: int = 0
2945 self.__start_time: float = time.time()
2946
2947 def on_solution_callback(self) -> None:
2948 """Called on each new solution."""
2949 current_time = time.time()
2950 print(
2951 f"Solution {self.__solution_count}, time ="
2952 f" {current_time - self.__start_time:0.2f} s"
2953 )
2954 for v in self.__variables:
2955 print(f" {v} = {self.value(v)}", end=" ")
2956 print(flush=True)
2957 self.__solution_count += 1
2958
2959 @property
2960 def solution_count(self) -> int:
2961 """Returns the number of solutions found."""
2962 return self.__solution_count
2963
2964
2965def _get_index(obj: _IndexOrSeries) -> pd.Index:
2966 """Returns the indices of `obj` as a `pd.Index`."""
2967 if isinstance(obj, pd.Series):
2968 return obj.index
2969 return obj
2970
2971
2973 value_or_series: Union[IntegralT, pd.Series], index: pd.Index
2974) -> pd.Series:
2975 """Returns a pd.Series of the given index with the corresponding values.
2976
2977 Args:
2978 value_or_series: the values to be converted (if applicable).
2979 index: the index of the resulting pd.Series.
2980
2981 Returns:
2982 pd.Series: The set of values with the given index.
2983
2984 Raises:
2985 TypeError: If the type of `value_or_series` is not recognized.
2986 ValueError: If the index does not match.
2987 """
2988 if isinstance(value_or_series, IntegralTypes):
2989 return pd.Series(data=value_or_series, index=index)
2990 elif isinstance(value_or_series, pd.Series):
2991 if value_or_series.index.equals(index):
2992 return value_or_series
2993 else:
2994 raise ValueError("index does not match")
2995 else:
2996 raise TypeError(f"invalid type={type(value_or_series).__name__!r}")
2997
2998
3000 value_or_series: Union[LinearExprT, pd.Series], index: pd.Index
3001) -> pd.Series:
3002 """Returns a pd.Series of the given index with the corresponding values.
3003
3004 Args:
3005 value_or_series: the values to be converted (if applicable).
3006 index: the index of the resulting pd.Series.
3007
3008 Returns:
3009 pd.Series: The set of values with the given index.
3010
3011 Raises:
3012 TypeError: If the type of `value_or_series` is not recognized.
3013 ValueError: If the index does not match.
3014 """
3015 if isinstance(value_or_series, IntegralTypes):
3016 return pd.Series(data=value_or_series, index=index)
3017 elif isinstance(value_or_series, pd.Series):
3018 if value_or_series.index.equals(index):
3019 return value_or_series
3020 else:
3021 raise ValueError("index does not match")
3022 else:
3023 raise TypeError(f"invalid type={type(value_or_series).__name__!r}")
3024
3025
3027 value_or_series: Union[LiteralT, pd.Series], index: pd.Index
3028) -> pd.Series:
3029 """Returns a pd.Series of the given index with the corresponding values.
3030
3031 Args:
3032 value_or_series: the values to be converted (if applicable).
3033 index: the index of the resulting pd.Series.
3034
3035 Returns:
3036 pd.Series: The set of values with the given index.
3037
3038 Raises:
3039 TypeError: If the type of `value_or_series` is not recognized.
3040 ValueError: If the index does not match.
3041 """
3042 if isinstance(value_or_series, IntegralTypes):
3043 return pd.Series(data=value_or_series, index=index)
3044 elif isinstance(value_or_series, pd.Series):
3045 if value_or_series.index.equals(index):
3046 return value_or_series
3047 else:
3048 raise ValueError("index does not match")
3049 else:
3050 raise TypeError(f"invalid type={type(value_or_series).__name__!r}")
None __init__(self, "CpModel" cp_model)
Definition cp_model.py:329
cp_model_pb2.ConstraintProto proto(self)
Definition cp_model.py:398
cp_model_pb2.ConstraintProto Proto(self)
Definition cp_model.py:413
"Constraint" only_enforce_if(self, Iterable[LiteralT] boolvar)
Definition cp_model.py:337
"Constraint" with_name(self, str name)
Definition cp_model.py:377
Constraint add_no_overlap_2d(self, Iterable[IntervalVar] x_intervals, Iterable[IntervalVar] y_intervals)
Definition cp_model.py:1998
IntVar new_constant(self, IntegralT value)
Definition cp_model.py:715
IntervalVar new_optional_interval_var(self, LinearExprT start, LinearExprT size, LinearExprT end, LiteralT is_present, str name)
Definition cp_model.py:1775
Constraint add_no_overlap(self, Iterable[IntervalVar] interval_vars)
Definition cp_model.py:1975
bool export_to_file(self, str file)
Definition cp_model.py:2282
IntervalVar get_interval_var_from_proto_index(self, int index)
Definition cp_model.py:2098
maximize(self, ObjLinearExprT obj)
Definition cp_model.py:2228
Constraint add_element(self, LinearExprT index, Sequence[LinearExprT] expressions, LinearExprT target)
Definition cp_model.py:922
minimize(self, ObjLinearExprT obj)
Definition cp_model.py:2224
IntVar new_int_var_from_domain(self, sorted_interval_list.Domain domain, str name)
Definition cp_model.py:690
pd.Series new_bool_var_series(self, str name, pd.Index index)
Definition cp_model.py:791
IntVar _get_int_var(self, int index)
Definition cp_model.py:654
Constraint add_automaton(self, Sequence[LinearExprT] transition_expressions, IntegralT starting_state, Sequence[IntegralT] final_states, Sequence[Tuple[IntegralT, IntegralT, IntegralT]] transition_triples)
Definition cp_model.py:1114
pd.Series new_int_var_series(self, str name, pd.Index index, Union[IntegralT, pd.Series] lower_bounds, Union[IntegralT, pd.Series] upper_bounds)
Definition cp_model.py:726
Constraint add_at_most_one(self, Iterable[LiteralT] literals)
Definition cp_model.py:1421
add_map_domain(self, IntVar var, Iterable[IntVar] bool_var_array, IntegralT offset=0)
Definition cp_model.py:1362
pd.Series new_interval_var_series(self, str name, pd.Index index, Union[LinearExprT, pd.Series] starts, Union[LinearExprT, pd.Series] sizes, Union[LinearExprT, pd.Series] ends)
Definition cp_model.py:1644
IntVar new_int_var(self, IntegralT lb, IntegralT ub, str name)
Definition cp_model.py:663
None add_assumptions(self, Iterable[LiteralT] literals)
Definition cp_model.py:2318
Constraint add_bool_or(self, Iterable[LiteralT] literals)
Definition cp_model.py:1393
Constraint add_bool_xor(self, Iterable[LiteralT] literals)
Definition cp_model.py:1475
IntVar get_bool_var_from_proto_index(self, int index)
Definition cp_model.py:2084
cp_model_pb2.CpModelProto Proto(self)
Definition cp_model.py:2349
_set_objective(self, ObjLinearExprT obj, bool minimize)
Definition cp_model.py:2192
Constraint add_abs_equality(self, LinearExprT target, LinearExprT expr)
Definition cp_model.py:1535
Constraint add_reservoir_constraint_with_active(self, Iterable[LinearExprT] times, Iterable[LinearExprT] level_changes, Iterable[LiteralT] actives, int min_level, int max_level)
Definition cp_model.py:1291
None add_assumption(self, LiteralT lit)
Definition cp_model.py:2314
int get_or_make_index_from_constant(self, IntegralT value)
Definition cp_model.py:2164
IntVar get_int_var_from_proto_index(self, int index)
Definition cp_model.py:2094
int get_or_make_boolean_index(self, LiteralT arg)
Definition cp_model.py:2136
Constraint add_linear_expression_in_domain(self, LinearExprT linear_expr, sorted_interval_list.Domain domain)
Definition cp_model.py:839
Constraint add_forbidden_assignments(self, Sequence[LinearExprT] expressions, Iterable[Sequence[IntegralT]] tuples_list)
Definition cp_model.py:1076
IntervalVar new_optional_fixed_size_interval_var(self, LinearExprT start, IntegralT size, LiteralT is_present, str name)
Definition cp_model.py:1889
Constraint add_exactly_one(self, Iterable[LiteralT] literals)
Definition cp_model.py:1439
Constraint add_linear_constraint(self, LinearExprT linear_expr, IntegralT lb, IntegralT ub)
Definition cp_model.py:829
Constraint add_min_equality(self, LinearExprT target, Iterable[LinearExprT] exprs)
Definition cp_model.py:1504
IntVar new_bool_var(self, str name)
Definition cp_model.py:709
int get_or_make_index(self, VariableT arg)
Definition cp_model.py:2126
Constraint add_all_different(self, Iterable[LinearExprT] expressions)
Definition cp_model.py:893
Constraint add_reservoir_constraint(self, Iterable[LinearExprT] times, Iterable[LinearExprT] level_changes, int min_level, int max_level)
Definition cp_model.py:1227
Constraint add_cumulative(self, Iterable[IntervalVar] intervals, Iterable[LinearExprT] demands, LinearExprT capacity)
Definition cp_model.py:2030
pd.Series new_fixed_size_interval_var_series(self, str name, pd.Index index, Union[LinearExprT, pd.Series] starts, Union[IntegralT, pd.Series] sizes)
Definition cp_model.py:1728
Constraint add_multiple_circuit(self, Sequence[ArcT] arcs)
Definition cp_model.py:984
cp_model_pb2.LinearExpressionProto parse_linear_expression(self, LinearExprT linear_expr, bool negate=False)
Definition cp_model.py:2173
cp_model_pb2.CpModelProto __model
Definition cp_model.py:630
None add_hint(self, IntVar var, int value)
Definition cp_model.py:2296
Constraint add(self, Union[BoundedLinearExpression, bool, np.bool_] ct)
Definition cp_model.py:859
Constraint add_at_least_one(self, Iterable[LiteralT] literals)
Definition cp_model.py:1411
IntervalVar new_interval_var(self, LinearExprT start, LinearExprT size, LinearExprT end, str name)
Definition cp_model.py:1594
int get_interval_index(self, IntervalVar arg)
Definition cp_model.py:2157
Constraint add_multiplication_equality(self, LinearExprT target, *Union[Iterable[LinearExprT], LinearExprT] expressions)
Definition cp_model.py:1577
Constraint add_inverse(self, Sequence[VariableT] variables, Sequence[VariableT] inverse_variables)
Definition cp_model.py:1188
None assert_is_boolean_variable(self, LiteralT x)
Definition cp_model.py:2328
Constraint add_bool_and(self, Iterable[LiteralT] literals)
Definition cp_model.py:1457
Constraint add_max_equality(self, LinearExprT target, Iterable[LinearExprT] exprs)
Definition cp_model.py:1516
pd.Series new_optional_fixed_size_interval_var_series(self, str name, pd.Index index, Union[LinearExprT, pd.Series] starts, Union[IntegralT, pd.Series] sizes, Union[LiteralT, pd.Series] are_present)
Definition cp_model.py:1930
Constraint add_modulo_equality(self, LinearExprT target, LinearExprT expr, LinearExprT mod)
Definition cp_model.py:1546
Constraint add_allowed_assignments(self, Sequence[LinearExprT] expressions, Iterable[Sequence[IntegralT]] tuples_list)
Definition cp_model.py:1022
IntervalVar new_fixed_size_interval_var(self, LinearExprT start, IntegralT size, str name)
Definition cp_model.py:1691
None add_decision_strategy(self, Sequence[IntVar] variables, cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy var_strategy, cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy domain_strategy)
Definition cp_model.py:2246
LinearExpr rebuild_from_linear_expression_proto(self, cp_model_pb2.LinearExpressionProto proto)
Definition cp_model.py:660
pd.Series new_optional_interval_var_series(self, str name, pd.Index index, Union[LinearExprT, pd.Series] starts, Union[LinearExprT, pd.Series] sizes, Union[LinearExprT, pd.Series] ends, Union[LiteralT, pd.Series] are_present)
Definition cp_model.py:1832
Constraint add_implication(self, LiteralT a, LiteralT b)
Definition cp_model.py:1384
Constraint add_circuit(self, Sequence[ArcT] arcs)
Definition cp_model.py:952
IntVar _append_int_var(self, IntVar var)
Definition cp_model.py:649
Constraint add_division_equality(self, LinearExprT target, LinearExprT num, LinearExprT denom)
Definition cp_model.py:1526
int value(self, LinearExprT expression)
Definition cp_model.py:2782
cp_model_pb2.CpSolverResponse response_proto(self)
Definition cp_model.py:2879
cp_model_pb2.CpSolverResponse response_proto(self)
Definition cp_model.py:2591
cp_model_pb2.CpSolverStatus solve(self, CpModel model, Optional["CpSolverSolutionCallback"] solution_callback=None)
Definition cp_model.py:2462
pd.Series BooleanValues(self, _IndexOrSeries variables)
Definition cp_model.py:2636
cp_model_pb2.CpSolverStatus SolveWithSolutionCallback(self, CpModel model, "CpSolverSolutionCallback" callback)
Definition cp_model.py:2690
cp_model_pb2.CpSolverStatus SearchForAllSolutions(self, CpModel model, "CpSolverSolutionCallback" callback)
Definition cp_model.py:2701
int Value(self, LinearExprT expression)
Definition cp_model.py:2679
pd.Series Values(self, _IndexOrSeries variables)
Definition cp_model.py:2682
cp_model_pb2.CpSolverStatus Solve(self, CpModel model, Optional["CpSolverSolutionCallback"] solution_callback=None)
Definition cp_model.py:2661
pd.Series boolean_values(self, _IndexOrSeries variables)
Definition cp_model.py:2528
int value(self, LinearExprT expression)
Definition cp_model.py:2495
Optional[cmh.SolveWrapper] __solve_wrapper
Definition cp_model.py:2455
Optional[cmh.ResponseWrapper] __response_wrapper
Definition cp_model.py:2449
bool boolean_value(self, LiteralT literal)
Definition cp_model.py:2524
cmh.ResponseWrapper _checked_response(self)
Definition cp_model.py:2621
bool BooleanValue(self, LiteralT literal)
Definition cp_model.py:2633
cp_model_pb2.CpSolverResponse ResponseProto(self)
Definition cp_model.py:2651
str StatusName(self, Optional[Any] status=None)
Definition cp_model.py:2667
Optional[Callable[[str], None]] log_callback
Definition cp_model.py:2453
Optional[Callable[[float], None]] best_bound_callback
Definition cp_model.py:2454
str status_name(self, Optional[Any] status=None)
Definition cp_model.py:2603
pd.Series values(self, _IndexOrSeries variables)
Definition cp_model.py:2499
Sequence[int] sufficient_assumptions_for_infeasibility(self)
Definition cp_model.py:2599
Sequence[int] SufficientAssumptionsForInfeasibility(self)
Definition cp_model.py:2673
cp_model_pb2.IntegerVariableProto __var
Definition cp_model.py:245
cp_model_pb2.IntegerVariableProto proto(self)
Definition cp_model.py:266
cp_model_pb2.IntegerVariableProto Proto(self)
Definition cp_model.py:305
None __init__(self, cp_model_pb2.CpModelProto model, Union[int, sorted_interval_list.Domain] domain, bool is_boolean, Optional[str] name)
Definition cp_model.py:243
bool is_equal_to(self, Any other)
Definition cp_model.py:270
cp_model_pb2.CpModelProto __model
Definition cp_model.py:493
cp_model_pb2.IntervalConstraintProto proto(self)
Definition cp_model.py:536
cp_model_pb2.ConstraintProto __ct
Definition cp_model.py:496
None __init__(self, cp_model_pb2.CpModelProto model, VariableList var_list, Union[cp_model_pb2.LinearExpressionProto, int] start, Optional[cp_model_pb2.LinearExpressionProto] size, Optional[cp_model_pb2.LinearExpressionProto] end, Optional[int] is_present_index, Optional[str] name)
Definition cp_model.py:492
cp_model_pb2.IntervalConstraintProto Proto(self)
Definition cp_model.py:584
None __init__(self, Sequence[IntVar] variables)
Definition cp_model.py:2941
LinearExprT rebuild_expr(self, cp_model_pb2.LinearExpressionProto proto)
Definition cp_model.py:437
Union[Iterable[LiteralT], LiteralT] expand_generator_or_tuple(Union[Tuple[LiteralT,...], Iterable[LiteralT]] args)
Definition cp_model.py:2418
pd.Index _get_index(_IndexOrSeries obj)
Definition cp_model.py:2965
pd.Series _convert_to_linear_expr_series_and_validate_index(Union[LinearExprT, pd.Series] value_or_series, pd.Index index)
Definition cp_model.py:3001
pd.Series _convert_to_integral_series_and_validate_index(Union[IntegralT, pd.Series] value_or_series, pd.Index index)
Definition cp_model.py:2974
str short_expr_name(cp_model_pb2.CpModelProto model, cp_model_pb2.LinearExpressionProto e)
Definition cp_model.py:200
pd.Series _convert_to_literal_series_and_validate_index(Union[LiteralT, pd.Series] value_or_series, pd.Index index)
Definition cp_model.py:3028
str short_name(cp_model_pb2.CpModelProto model, int i)
Definition cp_model.py:185
str display_bounds(Sequence[int] bounds)
Definition cp_model.py:172
bool object_is_a_false_literal(LiteralT literal)
Definition cp_model.py:607
bool object_is_a_true_literal(LiteralT literal)
Definition cp_model.py:594