Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
cp_model.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# Copyright 2010-2025 Google LLC
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Methods for building and solving CP-SAT models.
16
17The following two sections describe the main
18methods for building and solving CP-SAT models.
19
20* [`CpModel`](#cp_model.CpModel): Methods for creating models, including
21 variables and constraints.
22* [`CpSolver`](#cp_model.CpSolver): Methods for solving a model and evaluating
23 solutions.
24
25The following methods implement callbacks that the
26solver calls each time it finds a new solution.
27
28* [`CpSolverSolutionCallback`](#cp_model.CpSolverSolutionCallback):
29 A general method for implementing callbacks.
30* [`ObjectiveSolutionPrinter`](#cp_model.ObjectiveSolutionPrinter):
31 Print objective values and elapsed time for intermediate solutions.
32* [`VarArraySolutionPrinter`](#cp_model.VarArraySolutionPrinter):
33 Print intermediate solutions (variable values, time).
34* [`VarArrayAndObjectiveSolutionPrinter`]
35 (#cp_model.VarArrayAndObjectiveSolutionPrinter):
36 Print both intermediate solutions and objective values.
37
38Additional methods for solving CP-SAT models:
39
40* [`Constraint`](#cp_model.Constraint): A few utility methods for modifying
41 constraints created by `CpModel`.
42* [`LinearExpr`](#lineacp_model.LinearExpr): Methods for creating constraints
43 and the objective from large arrays of coefficients.
44
45Other methods and functions listed are primarily used for developing OR-Tools,
46rather than for solving specific optimization problems.
47"""
48
49from collections.abc import Callable, Iterable, Sequence
50import copy
51import threading
52import time
53from typing import (
54 Any,
55 Optional,
56 Union,
57 overload,
58)
59import warnings
60
61import numpy as np
62import pandas as pd
63
64from ortools.sat.python import cp_model_helper as cmh
65from ortools.util.python import sorted_interval_list
66
67# Import external types.
68BoundedLinearExpression = cmh.BoundedLinearExpression
69Constraint = cmh.Constraint
70CpModelProto = cmh.CpModelProto
71CpSolverResponse = cmh.CpSolverResponse
72CpSolverStatus = cmh.CpSolverStatus
73Domain = sorted_interval_list.Domain
74FlatFloatExpr = cmh.FlatFloatExpr
75FlatIntExpr = cmh.FlatIntExpr
76IntervalVar = cmh.IntervalVar
77IntVar = cmh.IntVar
78LinearExpr = cmh.LinearExpr
79NotBooleanVariable = cmh.NotBooleanVariable
80SatParameters = cmh.SatParameters
81
82
83# The classes below allow linear expressions to be expressed naturally with the
84# usual arithmetic operators + - * / and with constant numbers, which makes the
85# python API very intuitive. See../ samples/*.py for examples.
86
87INT_MIN = -(2**63) # hardcoded to be platform independent.
88INT_MAX = 2**63 - 1
89INT32_MIN = -(2**31)
90INT32_MAX = 2**31 - 1
91
92# CpSolver status (exported to avoid importing cp_model_cp2).
93UNKNOWN = cmh.CpSolverStatus.UNKNOWN
94UNKNOWN = cmh.CpSolverStatus.UNKNOWN
95MODEL_INVALID = cmh.CpSolverStatus.MODEL_INVALID
96FEASIBLE = cmh.CpSolverStatus.FEASIBLE
97INFEASIBLE = cmh.CpSolverStatus.INFEASIBLE
98OPTIMAL = cmh.CpSolverStatus.OPTIMAL
99
100# Variable selection strategy
101CHOOSE_FIRST = cmh.DecisionStrategyProto.VariableSelectionStrategy.CHOOSE_FIRST
102CHOOSE_LOWEST_MIN = (
103 cmh.DecisionStrategyProto.VariableSelectionStrategy.CHOOSE_LOWEST_MIN
104)
105CHOOSE_HIGHEST_MAX = (
106 cmh.DecisionStrategyProto.VariableSelectionStrategy.CHOOSE_HIGHEST_MAX
107)
108CHOOSE_MIN_DOMAIN_SIZE = (
109 cmh.DecisionStrategyProto.VariableSelectionStrategy.CHOOSE_MIN_DOMAIN_SIZE
110)
111CHOOSE_MAX_DOMAIN_SIZE = (
112 cmh.DecisionStrategyProto.VariableSelectionStrategy.CHOOSE_MAX_DOMAIN_SIZE
113)
114
115# Domain reduction strategy
116SELECT_MIN_VALUE = cmh.DecisionStrategyProto.DomainReductionStrategy.SELECT_MIN_VALUE
117SELECT_MAX_VALUE = cmh.DecisionStrategyProto.DomainReductionStrategy.SELECT_MAX_VALUE
118SELECT_LOWER_HALF = cmh.DecisionStrategyProto.DomainReductionStrategy.SELECT_LOWER_HALF
119SELECT_UPPER_HALF = cmh.DecisionStrategyProto.DomainReductionStrategy.SELECT_UPPER_HALF
120SELECT_MEDIAN_VALUE = (
121 cmh.DecisionStrategyProto.DomainReductionStrategy.SELECT_MEDIAN_VALUE
122)
123SELECT_RANDOM_HALF = (
124 cmh.DecisionStrategyProto.DomainReductionStrategy.SELECT_RANDOM_HALF
125)
126
127# Search branching
128AUTOMATIC_SEARCH = cmh.SatParameters.SearchBranching.AUTOMATIC_SEARCH
129FIXED_SEARCH = cmh.SatParameters.SearchBranching.FIXED_SEARCH
130PORTFOLIO_SEARCH = cmh.SatParameters.SearchBranching.PORTFOLIO_SEARCH
131LP_SEARCH = cmh.SatParameters.SearchBranching.LP_SEARCH
132PSEUDO_COST_SEARCH = cmh.SatParameters.SearchBranching.PSEUDO_COST_SEARCH
133PORTFOLIO_WITH_QUICK_RESTART_SEARCH = (
134 cmh.SatParameters.SearchBranching.PORTFOLIO_WITH_QUICK_RESTART_SEARCH
135)
136HINT_SEARCH = cmh.SatParameters.SearchBranching.HINT_SEARCH
137PARTIAL_FIXED_SEARCH = cmh.SatParameters.SearchBranching.PARTIAL_FIXED_SEARCH
138RANDOMIZED_SEARCH = cmh.SatParameters.SearchBranching.RANDOMIZED_SEARCH
139
140# Type aliases
141IntegralT = Union[int, np.int8, np.uint8, np.int32, np.uint32, np.int64, np.uint64]
142IntegralTypes = (
143 int,
144 np.int8,
145 np.uint8,
146 np.int32,
147 np.uint32,
148 np.int64,
149 np.uint64,
150)
151NumberT = Union[
152 int,
153 float,
154 np.int8,
155 np.uint8,
156 np.int32,
157 np.uint32,
158 np.int64,
159 np.uint64,
160 np.double,
161]
162NumberTypes = (
163 int,
164 float,
165 np.int8,
166 np.uint8,
167 np.int32,
168 np.uint32,
169 np.int64,
170 np.uint64,
171 np.double,
172)
173
174LiteralT = Union[cmh.Literal, IntegralT, bool]
175BoolVarT = cmh.Literal
176VariableT = Union["IntVar", IntegralT]
177
178# We need to add 'IntVar' for pytype.
179LinearExprT = Union[LinearExpr, "IntVar", IntegralT]
180ObjLinearExprT = Union[LinearExpr, NumberT]
181
182ArcT = tuple[IntegralT, IntegralT, LiteralT]
183_IndexOrSeries = Union[pd.Index, pd.Series]
184
185
186# Helper functions.
187enable_warnings = False
188
189# warnings.deprecated is python3.13+. Not compatible with Open Source (3.10+).
190# pylint: disable=g-bare-generic
191def deprecated(message: str) -> Callable[[Callable], Callable]:
192 """Decorator that warns about a deprecated function."""
193
194 def deprecated_decorator(func) -> Callable:
195 def deprecated_func(*args, **kwargs):
196 if enable_warnings:
197 warnings.warn(
198 f"{func.__name__} is a deprecated function. {message}",
199 category=DeprecationWarning,
200 stacklevel=2,
201 )
202 warnings.simplefilter("default", DeprecationWarning)
203 return func(*args, **kwargs)
204
205 return deprecated_func
206
207 return deprecated_decorator
208
209
210def deprecated_method(func, old_name: str) -> Callable:
211 """Wrapper that warns about a deprecated method."""
212
213 def deprecated_func(*args, **kwargs) -> Any:
214 if enable_warnings:
215 warnings.warn(
216 f"{old_name} is a deprecated function. Use {func.__name__} instead.",
217 category=DeprecationWarning,
218 stacklevel=2,
219 )
220 warnings.simplefilter("default", DeprecationWarning)
221 return func(*args, **kwargs)
222
223 return deprecated_func
224
225
226# pylint: enable=g-bare-generic
227
228
229def snake_case_to_camel_case(name: str) -> str:
230 """Converts a snake_case name to CamelCase."""
231 words = name.split("_")
232 return (
233 "".join(word.capitalize() for word in words)
234 .replace("2d", "2D")
235 .replace("Xor", "XOr")
236 )
237
238
239def object_is_a_true_literal(literal: LiteralT) -> bool:
240 """Checks if literal is either True, or a Boolean literals fixed to True."""
241 if isinstance(literal, IntVar):
242 proto = literal.proto
243 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1
244 if isinstance(literal, cmh.NotBooleanVariable):
245 proto = literal.negated().proto
246 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0
247 if isinstance(literal, (bool, np.bool_)):
248 return bool(literal)
249 if isinstance(literal, IntegralTypes):
250 literal_as_int = int(literal)
251 return literal_as_int == 1 or literal_as_int == ~False
252 return False
253
254
255def object_is_a_false_literal(literal: LiteralT) -> bool:
256 """Checks if literal is either False, or a Boolean literals fixed to False."""
257 if isinstance(literal, IntVar):
258 proto = literal.proto
259 return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0
260 if isinstance(literal, cmh.NotBooleanVariable):
261 proto = literal.negated().proto
262 return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1
263 if isinstance(literal, (bool, np.bool_)):
264 return not bool(literal)
265 if isinstance(literal, IntegralTypes):
266 literal_as_int = int(literal)
267 return literal_as_int == 0 or literal_as_int == ~True
268 return False
269
270
271def _get_index(obj: _IndexOrSeries) -> pd.Index:
272 """Returns the indices of `obj` as a `pd.Index`."""
273 if isinstance(obj, pd.Series):
274 return obj.index
275 return obj
276
277
278@overload
280 value_or_series: Union[LinearExprT, pd.Series], index: pd.Index
281) -> pd.Series: ...
282
283
284@overload
286 value_or_series: Union[LiteralT, pd.Series], index: pd.Index
287) -> pd.Series: ...
288
289
290@overload
292 value_or_series: Union[IntegralT, pd.Series], index: pd.Index
293) -> pd.Series: ...
294
295
296def _convert_to_series_and_validate_index(value_or_series, index):
297 """Returns a pd.Series of the given index with the corresponding values."""
298 if isinstance(value_or_series, pd.Series):
299 if value_or_series.index.equals(index):
300 return value_or_series
301 else:
302 raise ValueError("index does not match")
303 return pd.Series(data=value_or_series, index=index)
304
305
306class CpModel(cmh.CpBaseModel):
307 """Methods for building a CP model.
308
309 Methods beginning with:
310
311 * ```new_``` create integer, boolean, or interval variables.
312 * ```add_``` create new constraints and add them to the model.
313 """
314
315 def __init__(self, model_proto: Optional[cmh.CpModelProto] = None) -> None:
316 cmh.CpBaseModel.__init__(self, model_proto)
318
319 # Naming.
320 @property
321 def name(self) -> str:
322 """Returns the name of the model."""
323 if not self.model_proto or not self.model_proto.name:
324 return ""
325 return self.model_proto.name
326
327 @name.setter
328 def name(self, name: str):
329 """Sets the name of the model."""
330 self.model_proto.name = name
331
332 # Integer variable.
333 def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar:
334 """Create an integer variable with domain [lb, ub].
335
336 The CP-SAT solver is limited to integer variables. If you have fractional
337 values, scale them up so that they become integers; if you have strings,
338 encode them as integers.
339
340 Args:
341 lb: Lower bound for the variable.
342 ub: Upper bound for the variable.
343 name: The name of the variable.
344
345 Returns:
346 a variable whose domain is [lb, ub].
347 """
348 return (
350 .with_name(name)
351 .with_domain(sorted_interval_list.Domain(lb, ub))
352 )
353
355 self, domain: sorted_interval_list.Domain, name: str
356 ) -> IntVar:
357 """Create an integer variable from a domain.
358
359 A domain is a set of integers specified by a collection of intervals.
360 For example, `model.new_int_var_from_domain(cp_model.
361 Domain.from_intervals([[1, 2], [4, 6]]), 'x')`
362
363 Args:
364 domain: An instance of the Domain class.
365 name: The name of the variable.
366
367 Returns:
368 a variable whose domain is the given domain.
369 """
370 return IntVar(self.model_proto).with_name(name).with_domain(domain)
371
372 def new_bool_var(self, name: str) -> IntVar:
373 """Creates a 0-1 variable with the given name."""
374 return (
375 IntVar(self.model_proto)
376 .with_name(name)
377 .with_domain(sorted_interval_list.Domain(0, 1))
378 )
379
380 def new_constant(self, value: IntegralT) -> IntVar:
381 """Declares a constant integer."""
382 return IntVar(self.model_proto, self.get_or_make_index_from_constant(value))
383
385 self,
386 name: str,
387 index: pd.Index,
388 lower_bounds: Union[IntegralT, pd.Series],
389 upper_bounds: Union[IntegralT, pd.Series],
390 ) -> pd.Series:
391 """Creates a series of (scalar-valued) variables with the given name.
392
393 Args:
394 name (str): Required. The name of the variable set.
395 index (pd.Index): Required. The index to use for the variable set.
396 lower_bounds (Union[int, pd.Series]): A lower bound for variables in the
397 set. If a `pd.Series` is passed in, it will be based on the
398 corresponding values of the pd.Series.
399 upper_bounds (Union[int, pd.Series]): An upper bound for variables in the
400 set. If a `pd.Series` is passed in, it will be based on the
401 corresponding values of the pd.Series.
402
403 Returns:
404 pd.Series: The variable set indexed by its corresponding dimensions.
405
406 Raises:
407 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
408 ValueError: if the `name` is not a valid identifier or already exists.
409 ValueError: if the `lowerbound` is greater than the `upperbound`.
410 ValueError: if the index of `lower_bound`, or `upper_bound` does not match
411 the input index.
412 """
413 if not isinstance(index, pd.Index):
414 raise TypeError("Non-index object is used as index")
415 if not name.isidentifier():
416 raise ValueError(f"name={name!r} is not a valid identifier")
417 if (
418 isinstance(lower_bounds, IntegralTypes)
419 and isinstance(upper_bounds, IntegralTypes)
420 and lower_bounds > upper_bounds
421 ):
422 raise ValueError(
423 f"lower_bound={lower_bounds} is greater than"
424 f" upper_bound={upper_bounds} for variable set={name}"
425 )
426
427 lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
428 upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
429 return pd.Series(
430 index=index,
431 data=[
432 # pylint: disable=g-complex-comprehension
433 IntVar(self.model_proto)
434 .with_name(f"{name}[{i}]")
435 .with_domain(
436 sorted_interval_list.Domain(lower_bounds[i], upper_bounds[i])
437 )
438 for i in index
439 ],
440 )
441
443 self,
444 name: str,
445 index: pd.Index,
446 ) -> pd.Series:
447 """Creates a series of (scalar-valued) variables with the given name.
448
449 Args:
450 name (str): Required. The name of the variable set.
451 index (pd.Index): Required. The index to use for the variable set.
452
453 Returns:
454 pd.Series: The variable set indexed by its corresponding dimensions.
455
456 Raises:
457 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
458 ValueError: if the `name` is not a valid identifier or already exists.
459 """
460 if not isinstance(index, pd.Index):
461 raise TypeError("Non-index object is used as index")
462 if not name.isidentifier():
463 raise ValueError(f"name={name!r} is not a valid identifier")
464 return pd.Series(
465 index=index,
466 data=[
467 # pylint: disable=g-complex-comprehension
468 IntVar(self.model_proto)
469 .with_name(f"{name}[{i}]")
470 .with_domain(sorted_interval_list.Domain(0, 1))
471 for i in index
472 ],
473 )
474
475 # Linear constraints.
476
478 self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT
479 ) -> Constraint:
480 """Adds the constraint: `lb <= linear_expr <= ub`."""
482 linear_expr, sorted_interval_list.Domain(lb, ub)
483 )
484
486 self,
487 linear_expr: LinearExprT,
488 domain: sorted_interval_list.Domain,
489 ) -> Constraint:
490 """Adds the constraint: `linear_expr` in `domain`."""
491 if isinstance(linear_expr, LinearExpr):
492 ble = BoundedLinearExpression(linear_expr, domain)
493 if not ble.ok:
494 raise TypeError(
495 "Cannot add a linear expression containing floating point"
496 f" coefficients or constants: {type(linear_expr).__name__!r}"
497 )
498 return self._add_bounded_linear_expression(ble)
499 if isinstance(linear_expr, IntegralTypes):
500 if not domain.contains(int(linear_expr)):
501 return self.add_bool_or([]) # Evaluate to false.
502 else:
503 return self.add_bool_and([]) # Evaluate to true.
504 raise TypeError(
505 "not supported:"
506 f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})"
507 )
508
509 def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint:
510 """Adds a `BoundedLinearExpression` to the model.
511
512 Args:
513 ct: A [`BoundedLinearExpression`](#boundedlinearexpression).
514
515 Returns:
516 An instance of the `Constraint` class.
517
518 Raises:
519 TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean.
520 """
521 if isinstance(ct, BoundedLinearExpression):
522 return self._add_bounded_linear_expression(ct)
523 if ct and self.is_boolean_value(ct):
524 return self.add_bool_or([True])
525 if not ct and self.is_boolean_value(ct):
526 return self.add_bool_or([]) # Evaluate to false.
527 raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})")
528
529 # General Integer Constraints.
530
531 @overload
532 def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ...
533
534 @overload
535 def add_all_different(self, *expressions: LinearExprT) -> Constraint: ...
536
537 def add_all_different(self, *expressions):
538 """Adds AllDifferent(expressions).
539
540 This constraint forces all expressions to have different values.
541
542 Args:
543 *expressions: simple expressions of the form a * var + constant.
544
545 Returns:
546 An instance of the `Constraint` class.
547 """
548 return self._add_all_different(*expressions)
549
551 self,
552 index: LinearExprT,
553 expressions: Sequence[LinearExprT],
554 target: LinearExprT,
555 ) -> Constraint:
556 """Adds the element constraint: `expressions[index] == target`.
557
558 Args:
559 index: The index of the selected expression in the array. It must be an
560 affine expression (a * var + b).
561 expressions: A list of affine expressions.
562 target: The expression constrained to be equal to the selected expression.
563 It must be an affine expression (a * var + b).
564
565 Returns:
566 An instance of the `Constraint` class.
567 """
568
569 if not expressions:
570 raise ValueError("add_element expects a non-empty expressions array")
571
572 if isinstance(index, IntegralTypes):
573 expression: LinearExprT = list(expressions)[int(index)]
574 return self.add(expression == target)
575
576 return self._add_element(index, expressions, target)
577
578 def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
579 """Adds Circuit(arcs).
580
581 Adds a circuit constraint from a sparse list of arcs that encode the graph.
582
583 A circuit is a unique Hamiltonian cycle in a subgraph of the total
584 graph. In case a node 'i' is not in the cycle, then there must be a
585 loop arc 'i -> i' associated with a true literal. Otherwise
586 this constraint will fail.
587
588 Args:
589 arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
590 literal). The arc is selected in the circuit if the literal is true.
591 Both source_node and destination_node must be integers between 0 and the
592 number of nodes - 1.
593
594 Returns:
595 An instance of the `Constraint` class.
596
597 Raises:
598 ValueError: If the list of arcs is empty.
599 """
600 if not arcs:
601 raise ValueError("add_circuit expects a non-empty array of arcs")
602 return self._add_circuit(arcs)
603
604 def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
605 """Adds a multiple circuit constraint, aka the 'VRP' constraint.
606
607 The direct graph where arc #i (from tails[i] to head[i]) is present iff
608 literals[i] is true must satisfy this set of properties:
609 - #incoming arcs == 1 except for node 0.
610 - #outgoing arcs == 1 except for node 0.
611 - for node zero, #incoming arcs == #outgoing arcs.
612 - There are no duplicate arcs.
613 - Self-arcs are allowed except for node 0.
614 - There is no cycle in this graph, except through node 0.
615
616 Args:
617 arcs: a list of arcs. An arc is a tuple (source_node, destination_node,
618 literal). The arc is selected in the circuit if the literal is true.
619 Both source_node and destination_node must be integers between 0 and the
620 number of nodes - 1.
621
622 Returns:
623 An instance of the `Constraint` class.
624
625 Raises:
626 ValueError: If the list of arcs is empty.
627 """
628 if not arcs:
629 raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
630 return self._add_routes(arcs)
631
633 self,
634 expressions: Sequence[LinearExprT],
635 tuples_list: Iterable[Sequence[IntegralT]],
636 ) -> Constraint:
637 """Adds AllowedAssignments(expressions, tuples_list).
638
639 An AllowedAssignments constraint is a constraint on an array of affine
640 expressions, which requires that when all expressions are assigned values,
641 the
642 resulting array equals one of the tuples in `tuple_list`.
643
644 Args:
645 expressions: A list of affine expressions (a * var + b).
646 tuples_list: A list of admissible tuples. Each tuple must have the same
647 length as the expressions, and the ith value of a tuple corresponds to
648 the ith expression.
649
650 Returns:
651 An instance of the `Constraint` class.
652
653 Raises:
654 TypeError: If a tuple does not have the same size as the list of
655 expressions.
656 ValueError: If the array of expressions is empty.
657 """
658
659 if not expressions:
660 raise ValueError(
661 "add_allowed_assignments expects a non-empty expressions array"
662 )
663
664 return self._add_table(expressions, tuples_list, False)
665
667 self,
668 expressions: Sequence[LinearExprT],
669 tuples_list: Iterable[Sequence[IntegralT]],
670 ) -> Constraint:
671 """Adds add_forbidden_assignments(expressions, [tuples_list]).
672
673 A ForbiddenAssignments constraint is a constraint on an array of affine
674 expressions where the list of impossible combinations is provided in the
675 tuples list.
676
677 Args:
678 expressions: A list of affine expressions (a * var + b).
679 tuples_list: A list of forbidden tuples. Each tuple must have the same
680 length as the expressions, and the *i*th value of a tuple corresponds to
681 the *i*th expression.
682
683 Returns:
684 An instance of the `Constraint` class.
685
686 Raises:
687 TypeError: If a tuple does not have the same size as the list of
688 expressions.
689 ValueError: If the array of expressions is empty.
690 """
691
692 if not expressions:
693 raise ValueError(
694 "add_forbidden_assignments expects a non-empty expressions array"
695 )
696
697 return self._add_table(expressions, tuples_list, True)
698
700 self,
701 transition_expressions: Sequence[LinearExprT],
702 starting_state: IntegralT,
703 final_states: Sequence[IntegralT],
704 transition_triples: Sequence[tuple[IntegralT, IntegralT, IntegralT]],
705 ) -> Constraint:
706 """Adds an automaton constraint.
707
708 An automaton constraint takes a list of affine expressions (a * var + b) (of
709 size *n*), an initial state, a set of final states, and a set of
710 transitions. A transition is a triplet (*tail*, *transition*, *head*), where
711 *tail* and *head* are states, and *transition* is the label of an arc from
712 *head* to *tail*, corresponding to the value of one expression in the list
713 of
714 expressions.
715
716 This automaton will be unrolled into a flow with *n* + 1 phases. Each phase
717 contains the possible states of the automaton. The first state contains the
718 initial state. The last phase contains the final states.
719
720 Between two consecutive phases *i* and *i* + 1, the automaton creates a set
721 of arcs. For each transition (*tail*, *transition*, *head*), it will add
722 an arc from the state *tail* of phase *i* and the state *head* of phase
723 *i* + 1. This arc is labeled by the value *transition* of the expression
724 `expressions[i]`. That is, this arc can only be selected if `expressions[i]`
725 is assigned the value *transition*.
726
727 A feasible solution of this constraint is an assignment of expressions such
728 that, starting from the initial state in phase 0, there is a path labeled by
729 the values of the expressions that ends in one of the final states in the
730 final phase.
731
732 Args:
733 transition_expressions: A non-empty list of affine expressions (a * var +
734 b) whose values correspond to the labels of the arcs traversed by the
735 automaton.
736 starting_state: The initial state of the automaton.
737 final_states: A non-empty list of admissible final states.
738 transition_triples: A list of transitions for the automaton, in the
739 following format (current_state, variable_value, next_state).
740
741 Returns:
742 An instance of the `Constraint` class.
743
744 Raises:
745 ValueError: if `transition_expressions`, `final_states`, or
746 `transition_triples` are empty.
747 """
748
749 if not transition_expressions:
750 raise ValueError(
751 "add_automaton expects a non-empty transition_expressions array"
752 )
753 if not final_states:
754 raise ValueError("add_automaton expects some final states")
755
756 if not transition_triples:
757 raise ValueError("add_automaton expects some transition triples")
758
759 return self._add_automaton(
760 transition_expressions,
761 starting_state,
762 final_states,
763 transition_triples,
764 )
765
767 self,
768 variables: Sequence[VariableT],
769 inverse_variables: Sequence[VariableT],
770 ) -> Constraint:
771 """Adds Inverse(variables, inverse_variables).
772
773 An inverse constraint enforces that if `variables[i]` is assigned a value
774 `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa.
775
776 Args:
777 variables: An array of integer variables.
778 inverse_variables: An array of integer variables.
779
780 Returns:
781 An instance of the `Constraint` class.
782
783 Raises:
784 TypeError: if variables and inverse_variables have different lengths, or
785 if they are empty.
786 """
787
788 if not variables or not inverse_variables:
789 raise TypeError("The Inverse constraint does not accept empty arrays")
790 if len(variables) != len(inverse_variables):
791 raise TypeError(
792 "In the inverse constraint, the two array variables and"
793 " inverse_variables must have the same length."
794 )
795 return self._add_inverse(variables, inverse_variables)
796
798 self,
799 times: Sequence[LinearExprT],
800 level_changes: Sequence[LinearExprT],
801 min_level: int,
802 max_level: int,
803 ) -> Constraint:
804 """Adds Reservoir(times, level_changes, min_level, max_level).
805
806 Maintains a reservoir level within bounds. The water level starts at 0, and
807 at any time, it must be between min_level and max_level.
808
809 If the affine expression `times[i]` is assigned a value t, then the current
810 level changes by `level_changes[i]`, which is constant, at time t.
811
812 Note that min level must be <= 0, and the max level must be >= 0. Please
813 use fixed level_changes to simulate initial state.
814
815 Therefore, at any time:
816 sum(level_changes[i] if times[i] <= t) in [min_level, max_level]
817
818 Args:
819 times: A list of 1-var affine expressions (a * x + b) which specify the
820 time of the filling or emptying the reservoir.
821 level_changes: A list of integer values that specifies the amount of the
822 emptying or filling. Currently, variable demands are not supported.
823 min_level: At any time, the level of the reservoir must be greater or
824 equal than the min level.
825 max_level: At any time, the level of the reservoir must be less or equal
826 than the max level.
827
828 Returns:
829 An instance of the `Constraint` class.
830
831 Raises:
832 ValueError: if max_level < min_level.
833
834 ValueError: if max_level < 0.
835
836 ValueError: if min_level > 0
837 """
838
839 return self._add_reservoir(
840 times,
841 level_changes,
842 [],
843 min_level,
844 max_level,
845 )
846
848 self,
849 times: Sequence[LinearExprT],
850 level_changes: Sequence[LinearExprT],
851 actives: Sequence[LiteralT],
852 min_level: int,
853 max_level: int,
854 ) -> Constraint:
855 """Adds Reservoir(times, level_changes, actives, min_level, max_level).
856
857 Maintains a reservoir level within bounds. The water level starts at 0, and
858 at any time, it must be between min_level and max_level.
859
860 If the variable `times[i]` is assigned a value t, and `actives[i]` is
861 `True`, then the current level changes by `level_changes[i]`, which is
862 constant,
863 at time t.
864
865 Note that min level must be <= 0, and the max level must be >= 0. Please
866 use fixed level_changes to simulate initial state.
867
868 Therefore, at any time:
869 sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level,
870 max_level]
871
872
873 The array of boolean variables 'actives', if defined, indicates which
874 actions are actually performed.
875
876 Args:
877 times: A list of 1-var affine expressions (a * x + b) which specify the
878 time of the filling or emptying the reservoir.
879 level_changes: A list of integer values that specifies the amount of the
880 emptying or filling. Currently, variable demands are not supported.
881 actives: a list of boolean variables. They indicates if the
882 emptying/refilling events actually take place.
883 min_level: At any time, the level of the reservoir must be greater or
884 equal than the min level.
885 max_level: At any time, the level of the reservoir must be less or equal
886 than the max level.
887
888 Returns:
889 An instance of the `Constraint` class.
890
891 Raises:
892 ValueError: if max_level < min_level.
893
894 ValueError: if max_level < 0.
895
896 ValueError: if min_level > 0
897 """
898
899 if max_level < min_level:
900 raise ValueError("Reservoir constraint must have a max_level >= min_level")
901
902 if max_level < 0:
903 raise ValueError("Reservoir constraint must have a max_level >= 0")
904
905 if min_level > 0:
906 raise ValueError("Reservoir constraint must have a min_level <= 0")
907
908 if not times:
909 raise ValueError("Reservoir constraint must have a non-empty times array")
910
911 return self._add_reservoir(
912 times,
913 level_changes,
914 actives,
915 min_level,
916 max_level,
917 )
918
920 self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0
921 ):
922 """Adds `var == i + offset <=> bool_var_array[i] == true for all i`."""
923 for i, bool_var in enumerate(bool_var_array):
924 self.add(var == i + offset).only_enforce_if(bool_var)
925 self.add(var != i + offset).only_enforce_if(~bool_var)
926
927 def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint:
928 """Adds `a => b` (`a` implies `b`)."""
929 return self.add_bool_and(b).only_enforce_if(a)
930
931 @overload
932 def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ...
933
934 @overload
935 def add_bool_or(self, *literals: LiteralT) -> Constraint: ...
936
937 def add_bool_or(self, *literals):
938 """Adds `Or(literals) == true`: sum(literals) >= 1."""
939 return self._add_bool_argument_constraint(
940 cmh.BoolArgumentConstraint.bool_or, *literals
941 )
942
943 @overload
944 def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
945
946 @overload
947 def add_at_least_one(self, *literals: LiteralT) -> Constraint: ...
948
949 def add_at_least_one(self, *literals):
950 """Same as `add_bool_or`: `sum(literals) >= 1`."""
951 return self._add_bool_argument_constraint(
952 cmh.BoolArgumentConstraint.bool_or, *literals
953 )
954
955 @overload
956 def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
957
958 @overload
959 def add_at_most_one(self, *literals: LiteralT) -> Constraint: ...
960
961 def add_at_most_one(self, *literals) -> Constraint:
962 """Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
963 return self._add_bool_argument_constraint(
964 cmh.BoolArgumentConstraint.at_most_one, *literals
965 )
966
967 @overload
968 def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
969
970 @overload
971 def add_exactly_one(self, *literals: LiteralT) -> Constraint: ...
972
973 def add_exactly_one(self, *literals):
974 """Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
975 return self._add_bool_argument_constraint(
976 cmh.BoolArgumentConstraint.exactly_one, *literals
977 )
978
979 @overload
980 def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ...
981
982 @overload
983 def add_bool_and(self, *literals: LiteralT) -> Constraint: ...
984
985 def add_bool_and(self, *literals):
986 """Adds `And(literals) == true`."""
987 return self._add_bool_argument_constraint(
988 cmh.BoolArgumentConstraint.bool_and, *literals
989 )
990
991 @overload
992 def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ...
993
994 @overload
995 def add_bool_xor(self, *literals: LiteralT) -> Constraint: ...
996
997 def add_bool_xor(self, *literals):
998 """Adds `XOr(literals) == true`.
999
1000 In contrast to add_bool_or and add_bool_and, it does not support
1001 .only_enforce_if().
1002
1003 Args:
1004 *literals: the list of literals in the constraint.
1005
1006 Returns:
1007 An `Constraint` object.
1008 """
1009 return self._add_bool_argument_constraint(
1010 cmh.BoolArgumentConstraint.bool_xor, *literals
1011 )
1012
1013 @overload
1015 self, target: LinearExprT, expressions: Iterable[LinearExprT]
1016 ) -> Constraint: ...
1017
1018 @overload
1020 self, target: LinearExprT, *expressions: LinearExprT
1021 ) -> Constraint: ...
1022
1023 def add_min_equality(self, target, *expressions) -> Constraint:
1024 """Adds `target == Min(expressions)`."""
1025 return self._add_linear_argument_constraint(
1026 cmh.LinearArgumentConstraint.min, target, *expressions
1027 )
1028
1029 @overload
1031 self, target: LinearExprT, expressions: Iterable[LinearExprT]
1032 ) -> Constraint: ...
1033
1034 @overload
1036 self, target: LinearExprT, *expressions: LinearExprT
1037 ) -> Constraint: ...
1038
1039 def add_max_equality(self, target, *expressions) -> Constraint:
1040 """Adds `target == Max(expressions)`."""
1041 return self._add_linear_argument_constraint(
1042 cmh.LinearArgumentConstraint.max, target, *expressions
1043 )
1044
1046 self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
1047 ) -> Constraint:
1048 """Adds `target == num // denom` (integer division rounded towards 0)."""
1049 return self._add_linear_argument_constraint(
1050 cmh.LinearArgumentConstraint.div, target, [num, denom]
1051 )
1052
1053 def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
1054 """Adds `target == Abs(expr)`."""
1055 return self._add_linear_argument_constraint(
1056 cmh.LinearArgumentConstraint.max, target, [expr, -expr]
1057 )
1058
1060 self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
1061 ) -> Constraint:
1062 """Adds `target = expr % mod`.
1063
1064 It uses the C convention, that is the result is the remainder of the
1065 integral division rounded towards 0.
1066
1067 For example:
1068 * 10 % 3 = 1
1069 * -10 % 3 = -1
1070 * 10 % -3 = 1
1071 * -10 % -3 = -1
1072
1073 Args:
1074 target: the target expression.
1075 expr: the expression to compute the modulo of.
1076 mod: the modulus expression.
1077
1078 Returns:
1079 A `Constraint` object.
1080 """
1081 return self._add_linear_argument_constraint(
1082 cmh.LinearArgumentConstraint.mod, target, [expr, mod]
1083 )
1084
1086 self,
1087 target: LinearExprT,
1088 *expressions: Union[Iterable[LinearExprT], LinearExprT],
1089 ) -> Constraint:
1090 """Adds `target == expressions[0] * .. * expressions[n]`."""
1091 return self._add_linear_argument_constraint(
1092 cmh.LinearArgumentConstraint.prod, target, *expressions
1093 )
1094
1095 # Scheduling support
1096
1098 self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str
1099 ) -> IntervalVar:
1100 """Creates an interval variable from start, size, and end.
1101
1102 An interval variable is a constraint, that is itself used in other
1103 constraints like NoOverlap.
1104
1105 Internally, it ensures that `start + size == end`.
1106
1107 Args:
1108 start: The start of the interval. It must be of the form a * var + b.
1109 size: The size of the interval. It must be of the form a * var + b.
1110 end: The end of the interval. It must be of the form a * var + b.
1111 name: The name of the interval variable.
1112
1113 Returns:
1114 An `IntervalVar` object.
1115 """
1116 return self._new_interval_var(name, start, size, end, [])
1117
1119 self,
1120 name: str,
1121 index: pd.Index,
1122 starts: Union[LinearExprT, pd.Series],
1123 sizes: Union[LinearExprT, pd.Series],
1124 ends: Union[LinearExprT, pd.Series],
1125 ) -> pd.Series:
1126 """Creates a series of interval variables with the given name.
1127
1128 Args:
1129 name (str): Required. The name of the variable set.
1130 index (pd.Index): Required. The index to use for the variable set.
1131 starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1132 set. If a `pd.Series` is passed in, it will be based on the
1133 corresponding values of the pd.Series.
1134 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1135 set. If a `pd.Series` is passed in, it will be based on the
1136 corresponding values of the pd.Series.
1137 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1138 set. If a `pd.Series` is passed in, it will be based on the
1139 corresponding values of the pd.Series.
1140
1141 Returns:
1142 pd.Series: The interval variable set indexed by its corresponding
1143 dimensions.
1144
1145 Raises:
1146 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1147 ValueError: if the `name` is not a valid identifier or already exists.
1148 ValueError: if the all the indexes do not match.
1149 """
1150 if not isinstance(index, pd.Index):
1151 raise TypeError("Non-index object is used as index")
1152 if not name.isidentifier():
1153 raise ValueError(f"name={name!r} is not a valid identifier")
1154
1155 starts = _convert_to_series_and_validate_index(starts, index)
1156 sizes = _convert_to_series_and_validate_index(sizes, index)
1157 ends = _convert_to_series_and_validate_index(ends, index)
1158 interval_array = []
1159 for i in index:
1160 interval_array.append(
1161 self.new_interval_var(
1162 start=starts[i],
1163 size=sizes[i],
1164 end=ends[i],
1165 name=f"{name}[{i}]",
1166 )
1167 )
1168 return pd.Series(index=index, data=interval_array)
1169
1171 self, start: LinearExprT, size: IntegralT, name: str
1172 ) -> IntervalVar:
1173 """Creates an interval variable from start, and a fixed size.
1174
1175 An interval variable is a constraint, that is itself used in other
1176 constraints like NoOverlap.
1177
1178 Args:
1179 start: The start of the interval. It must be of the form a * var + b.
1180 size: The size of the interval. It must be an integer value.
1181 name: The name of the interval variable.
1182
1183 Returns:
1184 An `IntervalVar` object.
1185 """
1186 return self._new_interval_var(name, start, size, start + size, [])
1187
1189 self,
1190 name: str,
1191 index: pd.Index,
1192 starts: Union[LinearExprT, pd.Series],
1193 sizes: Union[IntegralT, pd.Series],
1194 ) -> pd.Series:
1195 """Creates a series of interval variables with the given name.
1196
1197 Args:
1198 name (str): Required. The name of the variable set.
1199 index (pd.Index): Required. The index to use for the variable set.
1200 starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1201 set. If a `pd.Series` is passed in, it will be based on the
1202 corresponding values of the pd.Series.
1203 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1204 the set. If a `pd.Series` is passed in, it will be based on the
1205 corresponding values of the pd.Series.
1206
1207 Returns:
1208 pd.Series: The interval variable set indexed by its corresponding
1209 dimensions.
1210
1211 Raises:
1212 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1213 ValueError: if the `name` is not a valid identifier or already exists.
1214 ValueError: if the all the indexes do not match.
1215 """
1216 if not isinstance(index, pd.Index):
1217 raise TypeError("Non-index object is used as index")
1218 if not name.isidentifier():
1219 raise ValueError(f"name={name!r} is not a valid identifier")
1220
1221 starts = _convert_to_series_and_validate_index(starts, index)
1222 sizes = _convert_to_series_and_validate_index(sizes, index)
1223 interval_array = []
1224 for i in index:
1225 interval_array.append(
1227 start=starts[i],
1228 size=sizes[i],
1229 name=f"{name}[{i}]",
1230 )
1231 )
1232 return pd.Series(index=index, data=interval_array)
1233
1235 self,
1236 start: LinearExprT,
1237 size: LinearExprT,
1238 end: LinearExprT,
1239 is_present: LiteralT,
1240 name: str,
1241 ) -> IntervalVar:
1242 """Creates an optional interval var from start, size, end, and is_present.
1243
1244 An optional interval variable is a constraint, that is itself used in other
1245 constraints like NoOverlap. This constraint is protected by a presence
1246 literal that indicates if it is active or not.
1247
1248 Internally, it ensures that `is_present` implies `start + size ==
1249 end`.
1250
1251 Args:
1252 start: The start of the interval. It must be of the form a * var + b.
1253 size: The size of the interval. It must be of the form a * var + b.
1254 end: The end of the interval. It must be of the form a * var + b.
1255 is_present: A literal that indicates if the interval is active or not. A
1256 inactive interval is simply ignored by all constraints.
1257 name: The name of the interval variable.
1258
1259 Returns:
1260 An `IntervalVar` object.
1261 """
1262 return self._new_interval_var(
1263 name,
1264 start,
1265 size,
1266 end,
1267 [is_present],
1268 )
1269
1271 self,
1272 name: str,
1273 index: pd.Index,
1274 starts: Union[LinearExprT, pd.Series],
1275 sizes: Union[LinearExprT, pd.Series],
1276 ends: Union[LinearExprT, pd.Series],
1277 are_present: Union[LiteralT, pd.Series],
1278 ) -> pd.Series:
1279 """Creates a series of interval variables with the given name.
1280
1281 Args:
1282 name (str): Required. The name of the variable set.
1283 index (pd.Index): Required. The index to use for the variable set.
1284 starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1285 set. If a `pd.Series` is passed in, it will be based on the
1286 corresponding values of the pd.Series.
1287 sizes (Union[LinearExprT, pd.Series]): The size of each interval in the
1288 set. If a `pd.Series` is passed in, it will be based on the
1289 corresponding values of the pd.Series.
1290 ends (Union[LinearExprT, pd.Series]): The ends of each interval in the
1291 set. If a `pd.Series` is passed in, it will be based on the
1292 corresponding values of the pd.Series.
1293 are_present (Union[LiteralT, pd.Series]): The performed literal of each
1294 interval in the set. If a `pd.Series` is passed in, it will be based on
1295 the corresponding values of the pd.Series.
1296
1297 Returns:
1298 pd.Series: The interval variable set indexed by its corresponding
1299 dimensions.
1300
1301 Raises:
1302 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1303 ValueError: if the `name` is not a valid identifier or already exists.
1304 ValueError: if the all the indexes do not match.
1305 """
1306 if not isinstance(index, pd.Index):
1307 raise TypeError("Non-index object is used as index")
1308 if not name.isidentifier():
1309 raise ValueError(f"name={name!r} is not a valid identifier")
1310
1311 starts = _convert_to_series_and_validate_index(starts, index)
1312 sizes = _convert_to_series_and_validate_index(sizes, index)
1313 ends = _convert_to_series_and_validate_index(ends, index)
1314 are_present = _convert_to_series_and_validate_index(are_present, index)
1315
1316 interval_array = []
1317 for i in index:
1318 interval_array.append(
1320 start=starts[i],
1321 size=sizes[i],
1322 end=ends[i],
1323 is_present=are_present[i],
1324 name=f"{name}[{i}]",
1325 )
1326 )
1327 return pd.Series(index=index, data=interval_array)
1328
1330 self,
1331 start: LinearExprT,
1332 size: IntegralT,
1333 is_present: LiteralT,
1334 name: str,
1335 ) -> IntervalVar:
1336 """Creates an interval variable from start, and a fixed size.
1337
1338 An interval variable is a constraint, that is itself used in other
1339 constraints like NoOverlap.
1340
1341 Args:
1342 start: The start of the interval. It must be of the form a * var + b.
1343 size: The size of the interval. It must be an integer value.
1344 is_present: A literal that indicates if the interval is active or not. A
1345 inactive interval is simply ignored by all constraints.
1346 name: The name of the interval variable.
1347
1348 Returns:
1349 An `IntervalVar` object.
1350 """
1351 return self._new_interval_var(
1352 name,
1353 start,
1354 size,
1355 start + size,
1356 [is_present],
1357 )
1358
1360 self,
1361 name: str,
1362 index: pd.Index,
1363 starts: Union[LinearExprT, pd.Series],
1364 sizes: Union[IntegralT, pd.Series],
1365 are_present: Union[LiteralT, pd.Series],
1366 ) -> pd.Series:
1367 """Creates a series of interval variables with the given name.
1368
1369 Args:
1370 name (str): Required. The name of the variable set.
1371 index (pd.Index): Required. The index to use for the variable set.
1372 starts (Union[LinearExprT, pd.Series]): The start of each interval in the
1373 set. If a `pd.Series` is passed in, it will be based on the
1374 corresponding values of the pd.Series.
1375 sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in
1376 the set. If a `pd.Series` is passed in, it will be based on the
1377 corresponding values of the pd.Series.
1378 are_present (Union[LiteralT, pd.Series]): The performed literal of each
1379 interval in the set. If a `pd.Series` is passed in, it will be based on
1380 the corresponding values of the pd.Series.
1381
1382 Returns:
1383 pd.Series: The interval variable set indexed by its corresponding
1384 dimensions.
1385
1386 Raises:
1387 TypeError: if the `index` is invalid (e.g. a `DataFrame`).
1388 ValueError: if the `name` is not a valid identifier or already exists.
1389 ValueError: if the all the indexes do not match.
1390 """
1391 if not isinstance(index, pd.Index):
1392 raise TypeError("Non-index object is used as index")
1393 if not name.isidentifier():
1394 raise ValueError(f"name={name!r} is not a valid identifier")
1395
1396 starts = _convert_to_series_and_validate_index(starts, index)
1397 sizes = _convert_to_series_and_validate_index(sizes, index)
1398 are_present = _convert_to_series_and_validate_index(are_present, index)
1399 interval_array = []
1400 for i in index:
1401 interval_array.append(
1403 start=starts[i],
1404 size=sizes[i],
1405 is_present=are_present[i],
1406 name=f"{name}[{i}]",
1407 )
1408 )
1409 return pd.Series(index=index, data=interval_array)
1410
1411 def add_no_overlap(self, intervals: Iterable[IntervalVar]) -> Constraint:
1412 """Adds NoOverlap(interval_vars).
1413
1414 A NoOverlap constraint ensures that all present intervals do not overlap
1415 in time.
1416
1417 Args:
1418 intervals: The list of interval variables to constrain.
1419
1420 Returns:
1421 An instance of the `Constraint` class.
1422 """
1423 return self._add_no_overlap(intervals)
1424
1426 self,
1427 x_intervals: Iterable[IntervalVar],
1428 y_intervals: Iterable[IntervalVar],
1429 ) -> Constraint:
1430 """Adds NoOverlap2D(x_intervals, y_intervals).
1431
1432 A NoOverlap2D constraint ensures that all present rectangles do not overlap
1433 on a plane. Each rectangle is aligned with the X and Y axis, and is defined
1434 by two intervals which represent its projection onto the X and Y axis.
1435
1436 Furthermore, one box is optional if at least one of the x or y interval is
1437 optional.
1438
1439 Args:
1440 x_intervals: The X coordinates of the rectangles.
1441 y_intervals: The Y coordinates of the rectangles.
1442
1443 Returns:
1444 An instance of the `Constraint` class.
1445 """
1446 return self._add_no_overlap_2d(x_intervals, y_intervals)
1447
1449 self,
1450 intervals: Iterable[IntervalVar],
1451 demands: Iterable[LinearExprT],
1452 capacity: LinearExprT,
1453 ) -> Constraint:
1454 """Adds Cumulative(intervals, demands, capacity).
1455
1456 This constraint enforces that:
1457
1458 for all t:
1459 sum(demands[i]
1460 if (start(intervals[i]) <= t < end(intervals[i])) and
1461 (intervals[i] is present)) <= capacity
1462
1463 Args:
1464 intervals: The list of intervals.
1465 demands: The list of demands for each interval. Each demand must be >= 0.
1466 Each demand can be a 1-var affine expression (a * x + b).
1467 capacity: The maximum capacity of the cumulative constraint. It can be a
1468 1-var affine expression (a * x + b).
1469
1470 Returns:
1471 An instance of the `Constraint` class.
1472 """
1473 return self._add_cumulative(intervals, demands, capacity)
1474
1475 # Support for model cloning.
1476 def clone(self) -> "CpModel":
1477 """Reset the model, and creates a new one from a CpModelProto instance."""
1478 clone = CpModel()
1479 clone.proto.copy_from(self.proto)
1480 clone.rebuild_constant_map()
1481 return clone
1482
1483 def __copy__(self):
1484 return CpModel(self.model_proto)
1485
1486 def __deepcopy__(self, memo):
1487 return CpModel(copy.deepcopy(self.model_proto, memo))
1488
1489 def get_bool_var_from_proto_index(self, index: int) -> IntVar:
1490 """Returns an already created Boolean variable from its index."""
1491 if index < 0 or index >= len(self.model_proto.variables):
1492 raise ValueError(
1493 f"get_bool_var_from_proto_index: out of bound index {index}"
1494 )
1495 result = IntVar(self.model_proto, index)
1496 if not result.is_boolean:
1497 raise TypeError(
1498 f"get_bool_var_from_proto_index: index {index} does not reference a"
1499 " boolean variable"
1500 )
1501 return result
1502
1503 def get_int_var_from_proto_index(self, index: int) -> IntVar:
1504 """Returns an already created integer variable from its index."""
1505 if index < 0 or index >= len(self.model_proto.variables):
1506 raise ValueError(
1507 f"get_int_var_from_proto_index: out of bound index {index}"
1508 )
1509 return IntVar(self.model_proto, index)
1510
1511 def get_interval_var_from_proto_index(self, index: int) -> IntervalVar:
1512 """Returns an already created interval variable from its index."""
1513 if index < 0 or index >= len(self.model_proto.constraints):
1514 raise ValueError(
1515 f"get_interval_var_from_proto_index: out of bound index {index}"
1516 )
1517 ct = self.model_proto.constraints[index]
1518 if not ct.has_interval():
1519 raise ValueError(
1520 f"get_interval_var_from_proto_index: index {index} does not"
1521 " reference an" + " interval variable"
1522 )
1523
1524 return IntervalVar(self.model_proto, index)
1525
1526 def __str__(self) -> str:
1527 return str(self.model_proto)
1528
1529 @property
1530 def proto(self) -> cmh.CpModelProto:
1531 """Returns the underlying CpModelProto."""
1532 return self.model_proto
1533
1534 def negated(self, index: int) -> int:
1535 return -index - 1
1536
1537 def _set_objective(self, obj: ObjLinearExprT, maximize: bool):
1538 """Sets the objective of the model."""
1539 self.clear_objective()
1540 if isinstance(obj, IntegralTypes):
1541 self.model_proto.objective.offset = int(obj)
1542 self.model_proto.objective.scaling_factor = 1.0
1543 elif isinstance(obj, LinearExpr):
1544 if obj.is_integer():
1545 int_obj = cmh.FlatIntExpr(obj)
1546 for var in int_obj.vars:
1547 self.model_proto.objective.vars.append(var.index)
1548 if maximize:
1549 self.model_proto.objective.scaling_factor = -1.0
1550 self.model_proto.objective.offset = -int_obj.offset
1551 for c in int_obj.coeffs:
1552 self.model_proto.objective.coeffs.append(-c)
1553 else:
1554 self.model_proto.objective.scaling_factor = 1.0
1555 self.model_proto.objective.offset = int_obj.offset
1556 self.model_proto.objective.coeffs.extend(int_obj.coeffs)
1557 else:
1558 float_obj = cmh.FlatFloatExpr(obj)
1559 for var in float_obj.vars:
1560 self.model_proto.floating_point_objective.vars.append(var.index)
1561 self.model_proto.floating_point_objective.coeffs.extend(
1562 float_obj.coeffs
1563 )
1564 self.model_proto.floating_point_objective.maximize = maximize
1565 self.model_proto.floating_point_objective.offset = float_obj.offset
1566 else:
1567 raise TypeError(
1568 f"TypeError: {type(obj).__name__!r} is not a valid objective"
1569 )
1570
1571 def minimize(self, obj: ObjLinearExprT):
1572 """Sets the objective of the model to minimize(obj)."""
1573 self._set_objective(obj, maximize=False)
1574
1575 def maximize(self, obj: ObjLinearExprT):
1576 """Sets the objective of the model to maximize(obj)."""
1577 self._set_objective(obj, maximize=True)
1578
1579 def has_objective(self) -> bool:
1580 return (
1582 or self.model_proto.has_floating_point_objective()
1583 )
1584
1587 self.model_proto.clear_floating_point_objective()
1588
1590 self,
1591 variables: Iterable[IntVar],
1592 var_strategy: cmh.DecisionStrategyProto.VariableSelectionStrategy,
1593 domain_strategy: cmh.DecisionStrategyProto.DomainReductionStrategy,
1594 ) -> None:
1595 """Adds a search strategy to the model.
1596
1597 Args:
1598 variables: a list of variables this strategy will assign.
1599 var_strategy: heuristic to choose the next variable to assign.
1600 domain_strategy: heuristic to reduce the domain of the selected variable.
1601 Currently, this is advanced code: the union of all strategies added to
1602 the model must be complete, i.e. instantiates all variables. Otherwise,
1603 solve() will fail.
1604 """
1605
1606 strategy: cmh.DecisionStrategyProto = self.model_proto.search_strategy.add()
1607 for v in variables:
1608 expr = strategy.exprs.add()
1609 if v.index >= 0:
1610 expr.vars.append(v.index)
1611 expr.coeffs.append(1)
1612 else:
1613 expr.vars.append(self.negated(v.index))
1614 expr.coeffs.append(-1)
1615 expr.offset = 1
1616
1617 strategy.variable_selection_strategy = var_strategy
1618 strategy.domain_reduction_strategy = domain_strategy
1619
1620 def model_stats(self) -> str:
1621 """Returns a string containing some model statistics."""
1622 return cmh.CpSatHelper.model_stats(self.model_proto)
1623
1624 def validate(self) -> str:
1625 """Returns a string indicating that the model is invalid."""
1626 return cmh.CpSatHelper.validate_model(self.model_proto)
1627
1628 def export_to_file(self, file: str) -> bool:
1629 """Write the model as a protocol buffer to 'file'.
1630
1631 Args:
1632 file: file to write the model to. If the filename ends with 'txt', the
1633 model will be written as a text file, otherwise, the binary format will
1634 be used.
1635
1636 Returns:
1637 True if the model was correctly written.
1638 """
1639 return cmh.CpSatHelper.write_model_to_file(self.model_proto, file)
1640
1641 def remove_all_names(self) -> None:
1642 """Removes all names from the model."""
1643 self.model_proto.clear_name()
1644 for v in self.model_proto.variables:
1645 v.clear_name()
1646 for c in self.model_proto.constraints:
1647 c.clear_name()
1648
1649 @overload
1650 def add_hint(self, var: IntVar, value: int) -> None: ...
1651
1652 @overload
1653 def add_hint(self, literal: BoolVarT, value: bool) -> None: ...
1654
1655 def add_hint(self, var, value) -> None:
1656 """Adds 'var == value' as a hint to the solver."""
1657 if var.index >= 0:
1658 self.model_proto.solution_hint.vars.append(var.index)
1659 self.model_proto.solution_hint.values.append(int(value))
1660 else:
1661 self.model_proto.solution_hint.vars.append(self.negated(var.index))
1662 self.model_proto.solution_hint.values.append(int(not value))
1663
1664 def clear_hints(self):
1665 """Removes any solution hint from the model."""
1666 self.model_proto.clear_solution_hint()
1667
1668 def add_assumption(self, lit: LiteralT) -> None:
1669 """Adds the literal to the model as assumptions."""
1670 self.model_proto.assumptions.append(self.get_or_make_boolean_index(lit))
1671
1672 def add_assumptions(self, literals: Iterable[LiteralT]) -> None:
1673 """Adds the literals to the model as assumptions."""
1674 for lit in literals:
1675 self.add_assumption(lit)
1676
1677 def clear_assumptions(self) -> None:
1678 """Removes all assumptions from the model."""
1679 self.model_proto.assumptions.clear()
1680
1681 # Compatibility with pre PEP8
1682 # pylint: disable=invalid-name
1683
1684 def _add_pre_pep8_methods(self) -> None:
1685 for method_name in dir(self):
1686 if callable(getattr(self, method_name)) and (
1687 method_name.startswith("add_")
1688 or method_name.startswith("new_")
1689 or method_name.startswith("clear_")
1690 ):
1691 pre_pep8_name = snake_case_to_camel_case(method_name)
1692 setattr(
1693 self,
1694 pre_pep8_name,
1695 deprecated_method(getattr(self, method_name), pre_pep8_name),
1696 )
1697
1698 for other_method_name in [
1699 "add",
1700 "clone",
1701 "get_bool_var_from_proto_index",
1702 "get_int_var_from_proto_index",
1703 "get_interval_var_from_proto_index",
1704 "minimize",
1705 "maximize",
1706 "has_objective",
1707 "model_stats",
1708 "validate",
1709 "export_to_file",
1710 ]:
1711 pre_pep8_name = snake_case_to_camel_case(other_method_name)
1712 setattr(
1713 self,
1714 pre_pep8_name,
1715 deprecated_method(getattr(self, other_method_name), pre_pep8_name),
1716 )
1717
1718 @deprecated("Use name property instead.")
1719 def Name(self) -> str:
1720 return self.name
1721
1722 @deprecated("Use name property instead.")
1723 def SetName(self, name: str) -> None:
1724 self.name = name
1725
1726 @deprecated("Use proto property instead.")
1727 def Proto(self) -> cmh.CpModelProto:
1728 return self.proto
1729
1730 # pylint: enable=invalid-name
1731
1732
1734 """Main solver class.
1735
1736 The purpose of this class is to search for a solution to the model provided
1737 to the solve() method.
1738
1739 Once solve() is called, this class allows inspecting the solution found
1740 with the value() and boolean_value() methods, as well as general statistics
1741 about the solve procedure.
1742 """
1743
1744 def __init__(self) -> None:
1745 self.__response: Optional[cmh.CpSolverResponse] = None
1746 self.parameters: cmh.SatParameters = cmh.SatParameters()
1747 self.log_callback: Optional[Callable[[str], None]] = None
1748 self.best_bound_callback: Optional[Callable[[float], None]] = None
1749 self.__solve_wrapper: Optional[cmh.SolveWrapper] = None
1750 self.__lock: threading.Lock = threading.Lock()
1751
1753 self,
1754 model: CpModel,
1755 solution_callback: Optional["CpSolverSolutionCallback"] = None,
1756 ) -> cmh.CpSolverStatus:
1757 """Solves a problem and passes each solution to the callback if not null."""
1758 with self.__lock:
1759 self.__solve_wrapper = cmh.SolveWrapper()
1760
1761 self.__solve_wrapper.set_parameters(self.parameters)
1762 if solution_callback is not None:
1763 self.__solve_wrapper.add_solution_callback(solution_callback)
1764
1765 if self.log_callback is not None:
1766 self.__solve_wrapper.add_log_callback(self.log_callback)
1767
1768 if self.best_bound_callback is not None:
1769 self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback)
1770
1771 self.__response = self.__solve_wrapper.solve(model.proto)
1772
1773 if solution_callback is not None:
1774 self.__solve_wrapper.clear_solution_callback(solution_callback)
1775
1776 with self.__lock:
1777 self.__solve_wrapper = None
1778
1779 return self.__response.status
1780
1781 def stop_search(self) -> None:
1782 """Stops the current search asynchronously."""
1783 with self.__lock:
1784 if self.__solve_wrapper:
1786
1787 def value(self, expression: LinearExprT) -> int:
1788 """Returns the value of a linear expression after solve."""
1789 return cmh.ResponseHelper.value(self._checked_response, expression)
1790
1791 def values(self, variables: _IndexOrSeries) -> pd.Series:
1792 """Returns the values of the input variables.
1793
1794 If `variables` is a `pd.Index`, then the output will be indexed by the
1795 variables. If `variables` is a `pd.Series` indexed by the underlying
1796 dimensions, then the output will be indexed by the same underlying
1797 dimensions.
1798
1799 Args:
1800 variables (Union[pd.Index, pd.Series]): The set of variables from which to
1801 get the values.
1802
1803 Returns:
1804 pd.Series: The values of all variables in the set.
1805
1806 Raises:
1807 RuntimeError: if solve() has not been called.
1808 """
1809 response: cmh.CpSolverResponse = self._checked_response
1810 return pd.Series(
1811 data=[cmh.ResponseHelper.value(response, var) for var in variables],
1812 index=_get_index(variables),
1813 )
1814
1815 def float_value(self, expression: LinearExprT) -> float:
1816 """Returns the value of a linear expression after solve."""
1817 return cmh.ResponseHelper.float_value(self._checked_response, expression)
1818
1819 def float_values(self, expressions: _IndexOrSeries) -> pd.Series:
1820 """Returns the float values of the input linear expressions.
1821
1822 If `expressions` is a `pd.Index`, then the output will be indexed by the
1823 variables. If `variables` is a `pd.Series` indexed by the underlying
1824 dimensions, then the output will be indexed by the same underlying
1825 dimensions.
1826
1827 Args:
1828 expressions (Union[pd.Index, pd.Series]): The set of expressions from
1829 which to get the values.
1830
1831 Returns:
1832 pd.Series: The values of all variables in the set.
1833
1834 Raises:
1835 RuntimeError: if solve() has not been called.
1836 """
1837 response: cmh.CpSolverResponse = self._checked_response
1838 return pd.Series(
1839 data=[
1840 cmh.ResponseHelper.float_value(response, expr) for expr in expressions
1841 ],
1842 index=_get_index(expressions),
1843 )
1844
1845 def boolean_value(self, literal: LiteralT) -> bool:
1846 """Returns the boolean value of a literal after solve."""
1847 return cmh.ResponseHelper.boolean_value(self._checked_response, literal)
1848
1849 def boolean_values(self, variables: _IndexOrSeries) -> pd.Series:
1850 """Returns the values of the input variables.
1851
1852 If `variables` is a `pd.Index`, then the output will be indexed by the
1853 variables. If `variables` is a `pd.Series` indexed by the underlying
1854 dimensions, then the output will be indexed by the same underlying
1855 dimensions.
1856
1857 Args:
1858 variables (Union[pd.Index, pd.Series]): The set of variables from which to
1859 get the values.
1860
1861 Returns:
1862 pd.Series: The values of all variables in the set.
1863
1864 Raises:
1865 RuntimeError: if solve() has not been called.
1866 """
1867 response: cmh.CpSolverResponse = self._checked_response
1868 return pd.Series(
1869 data=[
1870 cmh.ResponseHelper.boolean_value(response, literal)
1871 for literal in variables
1872 ],
1873 index=_get_index(variables),
1874 )
1875
1876 @property
1877 def objective_value(self) -> float:
1878 """Returns the value of the objective after solve."""
1879 return self._checked_response.objective_value
1880
1881 @property
1882 def best_objective_bound(self) -> float:
1883 """Returns the best lower (upper) bound found when min(max)imizing."""
1884 return self._checked_response.best_objective_bound
1885
1886 @property
1887 def num_booleans(self) -> int:
1888 """Returns the number of boolean variables managed by the SAT solver."""
1889 return self._checked_response.num_booleans
1890
1891 @property
1892 def num_conflicts(self) -> int:
1893 """Returns the number of conflicts since the creation of the solver."""
1894 return self._checked_response.num_conflicts
1895
1896 @property
1897 def num_branches(self) -> int:
1898 """Returns the number of search branches explored by the solver."""
1899 return self._checked_response.num_branches
1900
1901 @property
1902 def num_binary_propagations(self) -> int:
1903 """Returns the number of Boolean propagations done by the solver."""
1904 return self._checked_response.num_binary_propagations
1905
1906 @property
1908 """Returns the number of integer propagations done by the solver."""
1909 return self._checked_response.num_integer_propagations
1910
1911 @property
1912 def deterministic_time(self) -> float:
1913 """Returns the deterministic time in seconds since the creation of the solver."""
1914 return self._checked_response.deterministic_time
1915
1916 @property
1917 def wall_time(self) -> float:
1918 """Returns the wall time in seconds since the creation of the solver."""
1919 return self._checked_response.wall_time
1920
1921 @property
1922 def user_time(self) -> float:
1923 """Returns the user time in seconds since the creation of the solver."""
1924 return self._checked_response.user_time
1925
1926 @property
1927 def solve_log(self) -> str:
1928 """Returns the solve log.
1929
1930 To enable this, the parameter log_to_response must be set to True.
1931 """
1932 return self._checked_response.solve_log
1933
1934 @property
1935 def solve_info(self) -> str:
1936 """Returns the information about the solve."""
1937 return self._checked_response.solve_info
1938
1939 @property
1940 def response_proto(self) -> cmh.CpSolverResponse:
1941 """Returns the response object."""
1942 return self._checked_response
1943
1944 def response_stats(self) -> str:
1945 """Returns some statistics on the solution found as a string."""
1946 return cmh.CpSatHelper.solver_response_stats(self._checked_response)
1947
1949 """Returns the indices of the infeasible assumptions."""
1950 return cmh.ResponseHelper.sufficient_assumptions_for_infeasibility(
1952 )
1953
1954 def status_name(self, status: Optional[Any] = None) -> str:
1955 """Returns the name of the status returned by solve()."""
1956 if status is None:
1957 status = self._checked_response.status()
1958 return status.name
1959
1960 def solution_info(self) -> str:
1961 """Returns some information on the solve process.
1962
1963 Returns some information on how the solution was found, or the reason
1964 why the model or the parameters are invalid.
1965
1966 Raises:
1967 RuntimeError: if solve() has not been called.
1968 """
1969 return self._checked_response.solution_info
1970
1971 @property
1972 def _checked_response(self) -> cmh.CpSolverResponse:
1973 """Checks solve() has been called, and returns a response wrapper."""
1974 if self.__response is None:
1975 raise RuntimeError("solve() has not been called.")
1976 return self.__response
1977
1978 # Compatibility with pre PEP8
1979 # pylint: disable=invalid-name
1980
1981 @deprecated("Use best_objective_bound property instead.")
1982 def BestObjectiveBound(self) -> float:
1983 return self.best_objective_bound
1984
1985 @deprecated("Use boolean_value() method instead.")
1986 def BooleanValue(self, lit: LiteralT) -> bool:
1987 return self.boolean_value(lit)
1988
1989 @deprecated("Use boolean_values() method instead.")
1990 def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series:
1991 return self.boolean_values(variables)
1992
1993 @deprecated("Use num_booleans property instead.")
1994 def NumBooleans(self) -> int:
1995 return self.num_booleans
1996
1997 @deprecated("Use num_conflicts property instead.")
1998 def NumConflicts(self) -> int:
1999 return self.num_conflicts
2000
2001 @deprecated("Use num_branches property instead.")
2002 def NumBranches(self) -> int:
2003 return self.num_branches
2004
2005 @deprecated("Use objective_value property instead.")
2006 def ObjectiveValue(self) -> float:
2007 return self.objective_value
2008
2009 @deprecated("Use response_proto property instead.")
2010 def ResponseProto(self) -> cmh.CpSolverResponse:
2011 return self.response_proto
2012
2013 @deprecated("Use response_stats() method instead.")
2014 def ResponseStats(self) -> str:
2015 return self.response_stats()
2016
2017 @deprecated("Use solve() method instead.")
2019 self, model: CpModel, callback: "CpSolverSolutionCallback" = None
2020 ) -> cmh.CpSolverStatus:
2021 return self.solve(model, callback)
2022
2023 @deprecated("Use solution_info() method instead.")
2024 def SolutionInfo(self) -> str:
2025 return self.solution_info()
2026
2027 @deprecated("Use status_name() method instead.")
2028 def StatusName(self, status: Optional[Any] = None) -> str:
2029 return self.status_name(status)
2030
2031 @deprecated("Use stop_search() method instead.")
2032 def StopSearch(self) -> None:
2033 self.stop_search()
2034
2035 @deprecated("Use sufficient_assumptions_for_infeasibility() method instead.")
2036 def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]:
2038
2039 @deprecated("Use user_time property instead.")
2040 def UserTime(self) -> float:
2041 return self.user_time
2042
2043 @deprecated("Use value() method instead.")
2044 def Value(self, expression: LinearExprT) -> int:
2045 return self.value(expression)
2046
2047 @deprecated("Use values() method instead.")
2048 def Values(self, expressions: _IndexOrSeries) -> pd.Series:
2049 return self.values(expressions)
2050
2051 @deprecated("Use wall_time property instead.")
2052 def WallTime(self) -> float:
2053 return self.wall_time
2054
2055 @deprecated("Use solve() with enumerate_all_solutions = True.")
2057 self, model: CpModel, callback: "CpSolverSolutionCallback"
2058 ) -> cmh.CpSolverStatus:
2059 """Search for all solutions of a satisfiability problem.
2060
2061 This method searches for all feasible solutions of a given model.
2062 Then it feeds the solution to the callback.
2063
2064 Note that the model cannot contain an objective.
2065
2066 Args:
2067 model: The model to solve.
2068 callback: The callback that will be called at each solution.
2069
2070 Returns:
2071 The status of the solve:
2072
2073 * *FEASIBLE* if some solutions have been found
2074 * *INFEASIBLE* if the solver has proved there are no solution
2075 * *OPTIMAL* if all solutions have been found
2076 """
2077 if model.has_objective():
2078 raise TypeError(
2079 "Search for all solutions is only defined on satisfiability problems"
2080 )
2081 # Store old parameter.
2082 enumerate_all = self.parameters.enumerate_all_solutions
2083 self.parameters.enumerate_all_solutions = True
2084
2085 status: cmh.CpSolverStatus = self.solve(model, callback)
2086
2087 # Restore parameter.
2088 self.parameters.enumerate_all_solutions = enumerate_all
2089 return status
2090
2091
2092# pylint: enable=invalid-name
2093
2094
2095class CpSolverSolutionCallback(cmh.SolutionCallback):
2096 """Solution callback.
2097
2098 This class implements a callback that will be called at each new solution
2099 found during search.
2100
2101 The method on_solution_callback() will be called by the solver, and must be
2102 implemented. The current solution can be queried using the boolean_value()
2103 and value() methods.
2104
2105 These methods returns the same information as their counterpart in the
2106 `CpSolver` class.
2107 """
2108
2109 def __init__(self) -> None:
2110 cmh.SolutionCallback.__init__(self)
2111
2112 # pylint: disable=invalid-name
2113 def OnSolutionCallback(self) -> None:
2114 """Proxy for the same method in snake case."""
2115 self.on_solution_callback()
2116
2117 # pylint: enable=invalid-name
2118
2119 def boolean_value(self, lit: LiteralT) -> bool:
2120 """Returns the boolean value of a boolean literal.
2121
2122 Args:
2123 lit: A boolean variable or its negation.
2124
2125 Returns:
2126 The Boolean value of the literal in the solution.
2127
2128 Raises:
2129 RuntimeError: if `lit` is not a boolean variable or its negation.
2130 """
2131 if not self.has_response():
2132 raise RuntimeError("solve() has not been called.")
2133 return self.BooleanValue(lit)
2134
2135 def value(self, expression: LinearExprT) -> int:
2136 """Evaluates an linear expression in the current solution.
2137
2138 Args:
2139 expression: a linear expression of the model.
2140
2141 Returns:
2142 An integer value equal to the evaluation of the linear expression
2143 against the current solution.
2144
2145 Raises:
2146 RuntimeError: if 'expression' is not a LinearExpr.
2147 """
2148 if not self.has_response():
2149 raise RuntimeError("solve() has not been called.")
2150 return self.Value(expression)
2151
2152 def float_value(self, expression: LinearExprT) -> float:
2153 """Evaluates an linear expression in the current solution.
2154
2155 Args:
2156 expression: a linear expression of the model.
2157
2158 Returns:
2159 An integer value equal to the evaluation of the linear expression
2160 against the current solution.
2161
2162 Raises:
2163 RuntimeError: if 'expression' is not a LinearExpr.
2164 """
2165 if not self.has_response():
2166 raise RuntimeError("solve() has not been called.")
2167 return self.FloatValue(expression)
2168
2169 def has_response(self) -> bool:
2170 return self.HasResponse()
2171
2172 def stop_search(self) -> None:
2173 """Stops the current search asynchronously."""
2174 if not self.has_response():
2175 raise RuntimeError("solve() has not been called.")
2176 self.StopSearch()
2177
2178 @property
2179 def objective_value(self) -> float:
2180 """Returns the value of the objective after solve."""
2181 if not self.has_response():
2182 raise RuntimeError("solve() has not been called.")
2183 return self.ObjectiveValue()
2184
2185 @property
2186 def best_objective_bound(self) -> float:
2187 """Returns the best lower (upper) bound found when min(max)imizing."""
2188 if not self.has_response():
2189 raise RuntimeError("solve() has not been called.")
2190 return self.BestObjectiveBound()
2191
2192 @property
2193 def num_booleans(self) -> int:
2194 """Returns the number of boolean variables managed by the SAT solver."""
2195 if not self.has_response():
2196 raise RuntimeError("solve() has not been called.")
2197 return self.NumBooleans()
2198
2199 @property
2200 def num_conflicts(self) -> int:
2201 """Returns the number of conflicts since the creation of the solver."""
2202 if not self.has_response():
2203 raise RuntimeError("solve() has not been called.")
2204 return self.NumConflicts()
2205
2206 @property
2207 def num_branches(self) -> int:
2208 """Returns the number of search branches explored by the solver."""
2209 if not self.has_response():
2210 raise RuntimeError("solve() has not been called.")
2211 return self.NumBranches()
2212
2213 @property
2215 """Returns the number of integer propagations done by the solver."""
2216 if not self.has_response():
2217 raise RuntimeError("solve() has not been called.")
2218 return self.NumIntegerPropagations()
2219
2220 @property
2221 def num_binary_propagations(self) -> int:
2222 """Returns the number of Boolean propagations done by the solver."""
2223 if not self.has_response():
2224 raise RuntimeError("solve() has not been called.")
2225 return self.NumBinaryPropagations()
2226
2227 @property
2228 def deterministic_time(self) -> float:
2229 """Returns the determistic time in seconds since the creation of the solver."""
2230 if not self.has_response():
2231 raise RuntimeError("solve() has not been called.")
2232 return self.DeterministicTime()
2233
2234 @property
2235 def wall_time(self) -> float:
2236 """Returns the wall time in seconds since the creation of the solver."""
2237 if not self.has_response():
2238 raise RuntimeError("solve() has not been called.")
2239 return self.WallTime()
2240
2241 @property
2242 def user_time(self) -> float:
2243 """Returns the user time in seconds since the creation of the solver."""
2244 if not self.has_response():
2245 raise RuntimeError("solve() has not been called.")
2246 return self.UserTime()
2247
2248 @property
2249 def response_proto(self) -> cmh.CpSolverResponse:
2250 """Returns the response object."""
2251 if not self.has_response():
2252 raise RuntimeError("solve() has not been called.")
2253 return self.Response()
2254
2255
2257 """Display the objective value and time of intermediate solutions."""
2258
2259 def __init__(self) -> None:
2260 CpSolverSolutionCallback.__init__(self)
2261 self.__solution_count = 0
2262 self.__start_time = time.time()
2263
2264 def on_solution_callback(self) -> None:
2265 """Called on each new solution."""
2266 current_time = time.time()
2267 obj = self.objective_value
2268 print(
2269 f"Solution {self.__solution_count}, time ="
2270 f" {current_time - self.__start_time:0.2f} s, objective = {obj}",
2271 flush=True,
2272 )
2273 self.__solution_count += 1
2274
2275 def solution_count(self) -> int:
2276 """Returns the number of solutions found."""
2277 return self.__solution_count
2278
2279
2281 """Print intermediate solutions (objective, variable values, time)."""
2282
2283 def __init__(self, variables: Sequence[IntVar]) -> None:
2284 CpSolverSolutionCallback.__init__(self)
2285 self.__variables: Sequence[IntVar] = variables
2286 self.__solution_count: int = 0
2287 self.__start_time: float = time.time()
2288
2289 def on_solution_callback(self) -> None:
2290 """Called on each new solution."""
2291 current_time = time.time()
2292 obj = self.objective_value
2293 print(
2294 f"Solution {self.__solution_count}, time ="
2295 f" {current_time - self.__start_time:0.2f} s, objective = {obj}"
2296 )
2297 for v in self.__variables:
2298 print(f" {v} = {self.value(v)}", end=" ")
2299 print(flush=True)
2300 self.__solution_count += 1
2301
2302 @property
2303 def solution_count(self) -> int:
2304 """Returns the number of solutions found."""
2305 return self.__solution_count
2306
2307
2309 """Print intermediate solutions (variable values, time)."""
2310
2311 def __init__(self, variables: Sequence[IntVar]) -> None:
2312 CpSolverSolutionCallback.__init__(self)
2313 self.__variables: Sequence[IntVar] = variables
2314 self.__solution_count: int = 0
2315 self.__start_time: float = time.time()
2316
2317 def on_solution_callback(self) -> None:
2318 """Called on each new solution."""
2319 current_time = time.time()
2320 print(
2321 f"Solution {self.__solution_count}, time ="
2322 f" {current_time - self.__start_time:0.2f} s"
2323 )
2324 for v in self.__variables:
2325 print(f" {v} = {self.value(v)}", end=" ")
2326 print(flush=True)
2327 self.__solution_count += 1
2328
2329 @property
2330 def solution_count(self) -> int:
2331 """Returns the number of solutions found."""
2332 return self.__solution_count
Constraint add_no_overlap_2d(self, Iterable[IntervalVar] x_intervals, Iterable[IntervalVar] y_intervals)
Definition cp_model.py:1429
None add_decision_strategy(self, Iterable[IntVar] variables, cmh.DecisionStrategyProto.VariableSelectionStrategy var_strategy, cmh.DecisionStrategyProto.DomainReductionStrategy domain_strategy)
Definition cp_model.py:1594
IntVar new_constant(self, IntegralT value)
Definition cp_model.py:380
cmh.CpModelProto Proto(self)
Definition cp_model.py:1727
IntervalVar new_optional_interval_var(self, LinearExprT start, LinearExprT size, LinearExprT end, LiteralT is_present, str name)
Definition cp_model.py:1241
bool export_to_file(self, str file)
Definition cp_model.py:1628
IntervalVar get_interval_var_from_proto_index(self, int index)
Definition cp_model.py:1511
maximize(self, ObjLinearExprT obj)
Definition cp_model.py:1575
Constraint add_element(self, LinearExprT index, Sequence[LinearExprT] expressions, LinearExprT target)
Definition cp_model.py:555
minimize(self, ObjLinearExprT obj)
Definition cp_model.py:1571
IntVar new_int_var_from_domain(self, sorted_interval_list.Domain domain, str name)
Definition cp_model.py:356
pd.Series new_bool_var_series(self, str name, pd.Index index)
Definition cp_model.py:446
Constraint add_max_equality(self, LinearExprT target, Iterable[LinearExprT] expressions)
Definition cp_model.py:1032
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:390
Constraint add_at_most_one(self, Iterable[LiteralT] literals)
Definition cp_model.py:956
add_map_domain(self, IntVar var, Iterable[IntVar] bool_var_array, IntegralT offset=0)
Definition cp_model.py:921
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:1125
IntVar new_int_var(self, IntegralT lb, IntegralT ub, str name)
Definition cp_model.py:333
None add_assumptions(self, Iterable[LiteralT] literals)
Definition cp_model.py:1672
Constraint add_bool_or(self, Iterable[LiteralT] literals)
Definition cp_model.py:932
Constraint add_bool_xor(self, Iterable[LiteralT] literals)
Definition cp_model.py:992
IntVar get_bool_var_from_proto_index(self, int index)
Definition cp_model.py:1489
_set_objective(self, ObjLinearExprT obj, bool maximize)
Definition cp_model.py:1537
Constraint add_abs_equality(self, LinearExprT target, LinearExprT expr)
Definition cp_model.py:1053
Constraint add_reservoir_constraint_with_active(self, Sequence[LinearExprT] times, Sequence[LinearExprT] level_changes, Sequence[LiteralT] actives, int min_level, int max_level)
Definition cp_model.py:854
None add_assumption(self, LiteralT lit)
Definition cp_model.py:1668
IntVar get_int_var_from_proto_index(self, int index)
Definition cp_model.py:1503
Constraint add_min_equality(self, LinearExprT target, Iterable[LinearExprT] expressions)
Definition cp_model.py:1016
Constraint add_linear_expression_in_domain(self, LinearExprT linear_expr, sorted_interval_list.Domain domain)
Definition cp_model.py:489
Constraint add_forbidden_assignments(self, Sequence[LinearExprT] expressions, Iterable[Sequence[IntegralT]] tuples_list)
Definition cp_model.py:670
IntervalVar new_optional_fixed_size_interval_var(self, LinearExprT start, IntegralT size, LiteralT is_present, str name)
Definition cp_model.py:1335
Constraint add_exactly_one(self, Iterable[LiteralT] literals)
Definition cp_model.py:968
Constraint add_linear_constraint(self, LinearExprT linear_expr, IntegralT lb, IntegralT ub)
Definition cp_model.py:479
IntVar new_bool_var(self, str name)
Definition cp_model.py:372
Constraint add_all_different(self, Iterable[LinearExprT] expressions)
Definition cp_model.py:532
Constraint add_cumulative(self, Iterable[IntervalVar] intervals, Iterable[LinearExprT] demands, LinearExprT capacity)
Definition cp_model.py:1453
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:1194
Constraint add_multiple_circuit(self, Sequence[ArcT] arcs)
Definition cp_model.py:604
None __init__(self, Optional[cmh.CpModelProto] model_proto=None)
Definition cp_model.py:315
None add_hint(self, IntVar var, int value)
Definition cp_model.py:1650
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:705
Constraint add(self, Union[BoundedLinearExpression, bool, np.bool_] ct)
Definition cp_model.py:509
Constraint add_at_least_one(self, Iterable[LiteralT] literals)
Definition cp_model.py:944
IntervalVar new_interval_var(self, LinearExprT start, LinearExprT size, LinearExprT end, str name)
Definition cp_model.py:1099
Constraint add_reservoir_constraint(self, Sequence[LinearExprT] times, Sequence[LinearExprT] level_changes, int min_level, int max_level)
Definition cp_model.py:803
Constraint add_multiplication_equality(self, LinearExprT target, *Union[Iterable[LinearExprT], LinearExprT] expressions)
Definition cp_model.py:1089
Constraint add_inverse(self, Sequence[VariableT] variables, Sequence[VariableT] inverse_variables)
Definition cp_model.py:770
Constraint add_no_overlap(self, Iterable[IntervalVar] intervals)
Definition cp_model.py:1411
Constraint add_bool_and(self, Iterable[LiteralT] literals)
Definition cp_model.py:980
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:1366
Constraint add_modulo_equality(self, LinearExprT target, LinearExprT expr, LinearExprT mod)
Definition cp_model.py:1061
Constraint add_allowed_assignments(self, Sequence[LinearExprT] expressions, Iterable[Sequence[IntegralT]] tuples_list)
Definition cp_model.py:636
IntervalVar new_fixed_size_interval_var(self, LinearExprT start, IntegralT size, str name)
Definition cp_model.py:1172
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:1278
Constraint add_implication(self, LiteralT a, LiteralT b)
Definition cp_model.py:927
Constraint add_circuit(self, Sequence[ArcT] arcs)
Definition cp_model.py:578
Constraint add_division_equality(self, LinearExprT target, LinearExprT num, LinearExprT denom)
Definition cp_model.py:1047
int value(self, LinearExprT expression)
Definition cp_model.py:2135
float float_value(self, LinearExprT expression)
Definition cp_model.py:2152
pd.Series BooleanValues(self, _IndexOrSeries variables)
Definition cp_model.py:1990
float float_value(self, LinearExprT expression)
Definition cp_model.py:1815
int Value(self, LinearExprT expression)
Definition cp_model.py:2044
pd.Series boolean_values(self, _IndexOrSeries variables)
Definition cp_model.py:1849
int value(self, LinearExprT expression)
Definition cp_model.py:1787
Optional[cmh.SolveWrapper] __solve_wrapper
Definition cp_model.py:1749
pd.Series Values(self, _IndexOrSeries expressions)
Definition cp_model.py:2048
bool boolean_value(self, LiteralT literal)
Definition cp_model.py:1845
cmh.CpSolverStatus SearchForAllSolutions(self, CpModel model, "CpSolverSolutionCallback" callback)
Definition cp_model.py:2058
cmh.CpSolverStatus Solve(self, CpModel model, "CpSolverSolutionCallback" callback=None)
Definition cp_model.py:2020
bool BooleanValue(self, LiteralT lit)
Definition cp_model.py:1986
str StatusName(self, Optional[Any] status=None)
Definition cp_model.py:2028
Optional[Callable[[str], None]] log_callback
Definition cp_model.py:1747
pd.Series float_values(self, _IndexOrSeries expressions)
Definition cp_model.py:1819
cmh.CpSolverStatus solve(self, CpModel model, Optional["CpSolverSolutionCallback"] solution_callback=None)
Definition cp_model.py:1756
cmh.CpSolverResponse ResponseProto(self)
Definition cp_model.py:2010
Optional[cmh.CpSolverResponse] __response
Definition cp_model.py:1745
cmh.CpSolverResponse response_proto(self)
Definition cp_model.py:1940
Optional[Callable[[float], None]] best_bound_callback
Definition cp_model.py:1748
str status_name(self, Optional[Any] status=None)
Definition cp_model.py:1954
pd.Series values(self, _IndexOrSeries variables)
Definition cp_model.py:1791
Sequence[int] sufficient_assumptions_for_infeasibility(self)
Definition cp_model.py:1948
Sequence[int] SufficientAssumptionsForInfeasibility(self)
Definition cp_model.py:2036
None __init__(self, Sequence[IntVar] variables)
Definition cp_model.py:2311
Callable deprecated_method(func, str old_name)
Definition cp_model.py:210
pd.Index _get_index(_IndexOrSeries obj)
Definition cp_model.py:271
Callable[[Callable], Callable] deprecated(str message)
Definition cp_model.py:191
str snake_case_to_camel_case(str name)
Definition cp_model.py:229
pd.Series _convert_to_series_and_validate_index(Union[LinearExprT, pd.Series] value_or_series, pd.Index index)
Definition cp_model.py:281
bool object_is_a_false_literal(LiteralT literal)
Definition cp_model.py:255
bool object_is_a_true_literal(LiteralT literal)
Definition cp_model.py:239