[CP-SAT] Python: rewrite support for variable numbers of arguments

This commit is contained in:
Laurent Perron
2025-07-26 21:29:09 +02:00
parent f2cf147e79
commit 8b4c79dd75
3 changed files with 185 additions and 266 deletions

View File

@@ -46,6 +46,7 @@ rather than for solving specific optimization problems.
"""
from collections.abc import Callable, Iterable, Sequence
import copy
import threading
import time
from typing import (
@@ -182,13 +183,14 @@ _IndexOrSeries = Union[pd.Index, pd.Series]
# Helper functions.
def arg_is_boolean(x: Any) -> bool:
"""Checks if the x is a boolean."""
if isinstance(x, bool):
return True
if isinstance(x, np.bool_):
return True
return False
def snake_case_to_camel_case(name: str) -> str:
"""Converts a snake_case name to camelCase."""
words = name.split("_")
return (
"".join(word.capitalize() for word in words)
.replace("2d", "2D")
.replace("Xor", "XOr")
)
def object_is_a_true_literal(literal: LiteralT) -> bool:
@@ -217,32 +219,6 @@ def object_is_a_false_literal(literal: LiteralT) -> bool:
return False
def expand_exprs_generator_or_tuple(
expressions: Union[tuple[LinearExprT, ...], Iterable[LinearExprT]],
) -> Union[Iterable[LinearExprT], LinearExprT]:
if hasattr(expressions, "__len__"): # Tuple
if len(expressions) != 1:
return expressions
if isinstance(expressions[0], (NumberTypes, LinearExpr)):
return expressions
# The `expressions` is a tuple. If it contains a single element which is
# itself an iterable (e.g. a generator), unpack it.
return expressions[0]
def expand_literals_generator_or_tuple(
literals: Union[tuple[LiteralT, ...], Iterable[LiteralT]],
) -> Union[Iterable[LiteralT], LiteralT]:
if hasattr(literals, "__len__"): # Tuple
if len(literals) != 1:
return literals
if isinstance(literals[0], (NumberTypes, bool, cmh.Literal)):
return literals
# The `expressions` is a tuple. If it contains a single element which is
# itself an iterable (e.g. a generator), unpack it.
return literals[0]
def _get_index(obj: _IndexOrSeries) -> pd.Index:
"""Returns the indices of `obj` as a `pd.Index`."""
if isinstance(obj, pd.Series):
@@ -287,8 +263,22 @@ class CpModel(cmh.CpBaseModel):
* ```add_``` create new constraints and add them to the model.
"""
def __init__(self) -> None:
cmh.CpBaseModel.__init__(self)
def __init__(self, model_proto: Optional[cmh.CpModelProto] = None) -> None:
if model_proto is None:
cmh.CpBaseModel.__init__(self)
else:
cmh.CpBaseModel.__init__(self, model_proto)
self._add_pre_pep8_methods()
def _add_pre_pep8_methods(self) -> None:
for method_name in dir(self):
if callable(getattr(self, method_name)) and (
method_name.startswith("add_")
or method_name.startswith("new_")
or method_name.startswith("clear_")
):
pre_pep8_name = snake_case_to_camel_case(method_name)
setattr(self, pre_pep8_name, getattr(self, method_name))
# Naming.
@property
@@ -469,7 +459,7 @@ class CpModel(cmh.CpBaseModel):
"Cannot add a linear expression containing floating point"
f" coefficients or constants: {type(linear_expr).__name__!r}"
)
return self.add_bounded_linear_expression_internal(ble)
return self._add_bounded_linear_expression(ble)
if isinstance(linear_expr, IntegralTypes):
if not domain.contains(int(linear_expr)):
return self.add_bool_or([]) # Evaluate to false.
@@ -493,10 +483,10 @@ class CpModel(cmh.CpBaseModel):
TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean.
"""
if isinstance(ct, BoundedLinearExpression):
return self.add_bounded_linear_expression_internal(ct)
if ct and arg_is_boolean(ct):
return self._add_bounded_linear_expression(ct)
if ct and self.is_boolean_value(ct):
return self.add_bool_or([True])
if not ct and arg_is_boolean(ct):
if not ct and self.is_boolean_value(ct):
return self.add_bool_or([]) # Evaluate to false.
raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})")
@@ -519,9 +509,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An instance of the `Constraint` class.
"""
return self.add_all_different_internal(
list(expand_exprs_generator_or_tuple(expressions))
)
return self._add_all_different(*expressions)
def add_element(
self,
@@ -549,7 +537,7 @@ class CpModel(cmh.CpBaseModel):
expression: LinearExprT = list(expressions)[int(index)]
return self.add(expression == target)
return self.add_element_internal(index, expressions, target)
return self._add_element(index, expressions, target)
def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
"""Adds Circuit(arcs).
@@ -575,7 +563,7 @@ class CpModel(cmh.CpBaseModel):
"""
if not arcs:
raise ValueError("add_circuit expects a non-empty array of arcs")
return self.add_circuit_internal(arcs)
return self._add_circuit(arcs)
def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint:
"""Adds a multiple circuit constraint, aka the 'VRP' constraint.
@@ -603,7 +591,7 @@ class CpModel(cmh.CpBaseModel):
"""
if not arcs:
raise ValueError("add_multiple_circuit expects a non-empty array of arcs")
return self.add_routes_internal(arcs)
return self._add_routes(arcs)
def add_allowed_assignments(
self,
@@ -637,7 +625,7 @@ class CpModel(cmh.CpBaseModel):
"add_allowed_assignments expects a non-empty expressions array"
)
return self.add_table_internal(expressions, tuples_list, False)
return self._add_table(expressions, tuples_list, False)
def add_forbidden_assignments(
self,
@@ -670,7 +658,7 @@ class CpModel(cmh.CpBaseModel):
"add_forbidden_assignments expects a non-empty expressions array"
)
return self.add_table_internal(expressions, tuples_list, True)
return self._add_table(expressions, tuples_list, True)
def add_automaton(
self,
@@ -732,7 +720,7 @@ class CpModel(cmh.CpBaseModel):
if not transition_triples:
raise ValueError("add_automaton expects some transition triples")
return self.add_automaton_internal(
return self._add_automaton(
transition_expressions,
starting_state,
final_states,
@@ -768,7 +756,7 @@ class CpModel(cmh.CpBaseModel):
"In the inverse constraint, the two array variables and"
" inverse_variables must have the same length."
)
return self.add_inverse_internal(variables, inverse_variables)
return self._add_inverse(variables, inverse_variables)
def add_reservoir_constraint(
self,
@@ -812,7 +800,7 @@ class CpModel(cmh.CpBaseModel):
ValueError: if min_level > 0
"""
return self.add_reservoir_internal(
return self._add_reservoir(
times,
level_changes,
[],
@@ -884,7 +872,7 @@ class CpModel(cmh.CpBaseModel):
if not times:
raise ValueError("Reservoir constraint must have a non-empty times array")
return self.add_reservoir_internal(
return self._add_reservoir(
times,
level_changes,
actives,
@@ -912,9 +900,7 @@ class CpModel(cmh.CpBaseModel):
def add_bool_or(self, *literals):
"""Adds `Or(literals) == true`: sum(literals) >= 1."""
return self.add_bool_or_internal(
list(expand_literals_generator_or_tuple(literals))
)
return self._add_bool_argument_constraint("or", *literals)
@overload
def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
@@ -924,7 +910,7 @@ class CpModel(cmh.CpBaseModel):
def add_at_least_one(self, *literals):
"""Same as `add_bool_or`: `sum(literals) >= 1`."""
return self.add_bool_or(*literals)
return self._add_bool_argument_constraint("or", *literals)
@overload
def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
@@ -934,9 +920,7 @@ class CpModel(cmh.CpBaseModel):
def add_at_most_one(self, *literals) -> Constraint:
"""Adds `AtMostOne(literals)`: `sum(literals) <= 1`."""
return self.add_at_most_one_internal(
list(expand_literals_generator_or_tuple(literals))
)
return self._add_bool_argument_constraint("at_most_one", *literals)
@overload
def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ...
@@ -946,9 +930,7 @@ class CpModel(cmh.CpBaseModel):
def add_exactly_one(self, *literals):
"""Adds `ExactlyOne(literals)`: `sum(literals) == 1`."""
return self.add_exactly_one_internal(
list(expand_literals_generator_or_tuple(literals))
)
return self._add_bool_argument_constraint("exactly_one", *literals)
@overload
def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ...
@@ -958,9 +940,7 @@ class CpModel(cmh.CpBaseModel):
def add_bool_and(self, *literals):
"""Adds `And(literals) == true`."""
return self.add_bool_and_internal(
list(expand_literals_generator_or_tuple(literals))
)
return self._add_bool_argument_constraint("and", *literals)
@overload
def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ...
@@ -980,9 +960,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An `Constraint` object.
"""
return self.add_bool_xor_internal(
list(expand_literals_generator_or_tuple(literals))
)
return self._add_bool_argument_constraint("xor", *literals)
@overload
def add_min_equality(
@@ -996,11 +974,7 @@ class CpModel(cmh.CpBaseModel):
def add_min_equality(self, target, *expressions) -> Constraint:
"""Adds `target == Min(expressions)`."""
return self.add_linear_argument_constraint_internal(
"min",
target,
list(expand_exprs_generator_or_tuple(expressions)),
)
return self._add_linear_argument_constraint("min", target, *expressions)
@overload
def add_max_equality(
@@ -1014,23 +988,17 @@ class CpModel(cmh.CpBaseModel):
def add_max_equality(self, target, *expressions) -> Constraint:
"""Adds `target == Max(expressions)`."""
return self.add_linear_argument_constraint_internal(
"max",
target,
list(expand_exprs_generator_or_tuple(expressions)),
)
return self._add_linear_argument_constraint("max", target, *expressions)
def add_division_equality(
self, target: LinearExprT, num: LinearExprT, denom: LinearExprT
) -> Constraint:
"""Adds `target == num // denom` (integer division rounded towards 0)."""
return self.add_linear_argument_constraint_internal("div", target, [num, denom])
return self._add_linear_argument_constraint("div", target, [num, denom])
def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint:
"""Adds `target == Abs(expr)`."""
return self.add_linear_argument_constraint_internal(
"max", target, [expr, -expr]
)
return self._add_linear_argument_constraint("max", target, [expr, -expr])
def add_modulo_equality(
self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT
@@ -1054,7 +1022,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
A `Constraint` object.
"""
return self.add_linear_argument_constraint_internal("mod", target, [expr, mod])
return self._add_linear_argument_constraint("mod", target, [expr, mod])
def add_multiplication_equality(
self,
@@ -1062,13 +1030,7 @@ class CpModel(cmh.CpBaseModel):
*expressions: Union[Iterable[LinearExprT], LinearExprT],
) -> Constraint:
"""Adds `target == expressions[0] * .. * expressions[n]`."""
return (
self.add_linear_argument_constraint_internal(
"prod",
target,
list(expand_exprs_generator_or_tuple(expressions)),
),
)
return self._add_linear_argument_constraint("prod", target, *expressions)
# Scheduling support
@@ -1091,7 +1053,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An `IntervalVar` object.
"""
return self.new_interval_var_internal(name, start, size, end, [])
return self._new_interval_var(name, start, size, end, [])
def new_interval_var_series(
self,
@@ -1161,7 +1123,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An `IntervalVar` object.
"""
return self.new_interval_var_internal(name, start, size, start + size, [])
return self._new_interval_var(name, start, size, start + size, [])
def new_fixed_size_interval_var_series(
self,
@@ -1237,7 +1199,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An `IntervalVar` object.
"""
return self.new_interval_var_internal(
return self._new_interval_var(
name,
start,
size,
@@ -1326,7 +1288,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An `IntervalVar` object.
"""
return self.new_interval_var_internal(
return self._new_interval_var(
name,
start,
size,
@@ -1398,7 +1360,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An instance of the `Constraint` class.
"""
return self.add_no_overlap_internal(intervals)
return self._add_no_overlap(intervals)
def add_no_overlap_2d(
self,
@@ -1421,7 +1383,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An instance of the `Constraint` class.
"""
return self.add_no_overlap_2d_internal(x_intervals, y_intervals)
return self._add_no_overlap_2d(x_intervals, y_intervals)
def add_cumulative(
self,
@@ -1448,7 +1410,7 @@ class CpModel(cmh.CpBaseModel):
Returns:
An instance of the `Constraint` class.
"""
return self.add_cumulative_internal(intervals, demands, capacity)
return self._add_cumulative(intervals, demands, capacity)
# Support for model cloning.
def clone(self) -> "CpModel":
@@ -1458,6 +1420,12 @@ class CpModel(cmh.CpBaseModel):
clone.rebuild_constant_map()
return clone
def __copy__(self):
return CpModel(self.model_proto)
def __deepcopy__(self, memo):
return CpModel(copy.deepcopy(self.model_proto, memo))
def get_bool_var_from_proto_index(self, index: int) -> IntVar:
"""Returns an already created Boolean variable from its index."""
if index < 0 or index >= len(self.model_proto.variables):
@@ -1662,48 +1630,7 @@ class CpModel(cmh.CpBaseModel):
def Proto(self) -> cmh.CpModelProto:
return self.proto
NewIntVar = new_int_var
NewIntVarFromDomain = new_int_var_from_domain
NewBoolVar = new_bool_var
NewConstant = new_constant
NewIntVarSeries = new_int_var_series
NewBoolVarSeries = new_bool_var_series
AddLinearConstraint = add_linear_constraint
AddLinearExpressionInDomain = add_linear_expression_in_domain
Add = add
AddAllDifferent = add_all_different
AddElement = add_element
AddCircuit = add_circuit
AddMultipleCircuit = add_multiple_circuit
AddAllowedAssignments = add_allowed_assignments
AddForbiddenAssignments = add_forbidden_assignments
AddAutomaton = add_automaton
AddInverse = add_inverse
AddReservoirConstraint = add_reservoir_constraint
AddReservoirConstraintWithActive = add_reservoir_constraint_with_active
AddImplication = add_implication
AddBoolOr = add_bool_or
AddAtLeastOne = add_at_least_one
AddAtMostOne = add_at_most_one
AddExactlyOne = add_exactly_one
AddBoolAnd = add_bool_and
AddBoolXOr = add_bool_xor
AddMinEquality = add_min_equality
AddMaxEquality = add_max_equality
AddDivisionEquality = add_division_equality
AddAbsEquality = add_abs_equality
AddModuloEquality = add_modulo_equality
AddMultiplicationEquality = add_multiplication_equality
NewIntervalVar = new_interval_var
NewIntervalVarSeries = new_interval_var_series
NewFixedSizeIntervalVar = new_fixed_size_interval_var
NewOptionalIntervalVar = new_optional_interval_var
NewOptionalIntervalVarSeries = new_optional_interval_var_series
NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var
NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series
AddNoOverlap = add_no_overlap
AddNoOverlap2D = add_no_overlap_2d
AddCumulative = add_cumulative
Clone = clone
GetBoolVarFromProtoIndex = get_bool_var_from_proto_index
GetIntVarFromProtoIndex = get_int_var_from_proto_index
@@ -1711,16 +1638,12 @@ class CpModel(cmh.CpBaseModel):
Minimize = minimize
Maximize = maximize
HasObjective = has_objective
ClearObjective = clear_objective
AddDecisionStrategy = add_decision_strategy
ModelStats = model_stats
Validate = validate
ExportToFile = export_to_file
AddHint = add_hint
ClearHints = clear_hints
AddAssumption = add_assumption
AddAssumptions = add_assumptions
ClearAssumptions = clear_assumptions
# add_XXX, new_XXX, and clear_XXX methods are already duplicated
# automatically.
# pylint: enable=invalid-name
@@ -2092,10 +2015,13 @@ class CpSolverSolutionCallback(cmh.SolutionCallback):
def __init__(self) -> None:
cmh.SolutionCallback.__init__(self)
# pylint: disable=invalid-name
def OnSolutionCallback(self) -> None:
"""Proxy for the same method in snake case."""
self.on_solution_callback()
# pylint: enable=invalid-name
def boolean_value(self, lit: LiteralT) -> bool:
"""Returns the boolean value of a boolean literal.

View File

@@ -464,9 +464,12 @@ class IntervalVar;
class CpBaseModel : public std::enable_shared_from_this<CpBaseModel> {
public:
CpBaseModel() : model_proto_(std::make_shared<CpModelProto>()) {}
CpBaseModel()
: model_proto_(std::make_shared<CpModelProto>()),
numpy_bool_type_(py::dtype::of<bool>().attr("type").cast<py::type>()) {}
explicit CpBaseModel(std::shared_ptr<CpModelProto> model_proto)
: model_proto_(model_proto) {}
: model_proto_(model_proto),
numpy_bool_type_(py::dtype::of<bool>().attr("type").cast<py::type>()) {}
std::shared_ptr<CpModelProto> model_proto() const { return model_proto_; }
@@ -501,9 +504,7 @@ class CpBaseModel : public std::enable_shared_from_this<CpBaseModel> {
literal.cast<std::shared_ptr<NotBooleanVariable>>();
AssertVariableIsBoolean(not_var);
return not_var->index();
} else if (py::isinstance<py::bool_>(literal) ||
py::type::of(literal).attr("__name__").cast<std::string>() ==
"bool") {
} else if (IsBooleanValue(literal)) {
const bool value = literal.cast<py::bool_>();
if (value) {
return GetOrMakeIndexFromConstant(1);
@@ -559,20 +560,20 @@ class CpBaseModel : public std::enable_shared_from_this<CpBaseModel> {
}
}
std::shared_ptr<Constraint> AddAllDifferentInternal(py::sequence exprs);
bool IsBooleanValue(py::handle value) {
return py::isinstance<py::bool_>(value) ||
py::isinstance(value, numpy_bool_type_);
}
std::shared_ptr<Constraint> AddAtMostOneInternal(py::sequence literals);
std::shared_ptr<Constraint> AddAllDifferentInternal(py::args exprs);
std::shared_ptr<Constraint> AddAutomatonInternal(
py::sequence transition_expressions, int64_t starting_state,
const std::vector<int64_t>& final_states,
const std::vector<std::vector<int64_t>>& transition_triples);
std::shared_ptr<Constraint> AddBoolAndInternal(py::sequence literals);
std::shared_ptr<Constraint> AddBoolOrInternal(py::sequence literals);
std::shared_ptr<Constraint> AddBoolXOrInternal(py::sequence literals);
std::shared_ptr<Constraint> AddBoolArgumentConstraintInternal(
const std::string& name, py::args literals);
std::shared_ptr<Constraint> AddBoundedLinearExpressionInternal(
BoundedLinearExpression* ble);
@@ -581,13 +582,11 @@ class CpBaseModel : public std::enable_shared_from_this<CpBaseModel> {
py::sequence exprs,
const py::handle& target);
std::shared_ptr<Constraint> AddExactlyOneInternal(py::sequence literals);
std::shared_ptr<Constraint> AddInverseInternal(py::sequence direct,
py::sequence inverse);
std::shared_ptr<Constraint> AddLinearArgumentConstraintInternal(
const std::string& name, const py::handle& target, py::sequence exprs);
const std::string& name, const py::handle& target, py::args exprs);
std::shared_ptr<Constraint> AddReservoirInternal(py::sequence times,
py::sequence level_changes,
@@ -625,6 +624,7 @@ class CpBaseModel : public std::enable_shared_from_this<CpBaseModel> {
private:
std::shared_ptr<CpModelProto> model_proto_;
absl::flat_hash_map<int64_t, int> cache_;
py::type numpy_bool_type_;
};
class Constraint {
@@ -661,22 +661,17 @@ class Constraint {
};
std::shared_ptr<Constraint> CpBaseModel::AddAllDifferentInternal(
py::sequence exprs) {
py::args exprs) {
const int ct_index = model_proto_->constraints_size();
ConstraintProto* ct = model_proto_->add_constraints();
for (const auto& expr : exprs) {
LinearExprToProto(expr, 1, ct->mutable_all_diff()->add_exprs());
}
return std::make_shared<Constraint>(shared_from_this(), ct_index);
}
std::shared_ptr<Constraint> CpBaseModel::AddAtMostOneInternal(
py::sequence literals) {
const int ct_index = model_proto_->constraints_size();
ConstraintProto* ct = model_proto_->add_constraints();
ct->mutable_at_most_one(); // If literals is empty.
for (const auto& literal : literals) {
ct->mutable_at_most_one()->add_literals(GetOrMakeBooleanIndex(literal));
if (exprs.size() == 1 && py::isinstance<py::iterable>(exprs[0])) {
for (const auto& expr : exprs[0]) {
LinearExprToProto(expr, 1, ct->mutable_all_diff()->add_exprs());
}
} else {
for (const auto& expr : exprs) {
LinearExprToProto(expr, 1, ct->mutable_all_diff()->add_exprs());
}
}
return std::make_shared<Constraint>(shared_from_this(), ct_index);
}
@@ -706,35 +701,33 @@ std::shared_ptr<Constraint> CpBaseModel::AddAutomatonInternal(
return std::make_shared<Constraint>(shared_from_this(), ct_index);
}
std::shared_ptr<Constraint> CpBaseModel::AddBoolAndInternal(
py::sequence literals) {
std::shared_ptr<Constraint> CpBaseModel::AddBoolArgumentConstraintInternal(
const std::string& name, py::args literals) {
const int ct_index = model_proto_->constraints_size();
ConstraintProto* ct = model_proto_->add_constraints();
ct->mutable_bool_and(); // If literals is empty.
for (const auto& literal : literals) {
ct->mutable_bool_and()->add_literals(GetOrMakeBooleanIndex(literal));
BoolArgumentProto* proto = nullptr;
if (name == "or") {
proto = ct->mutable_bool_or();
} else if (name == "and") {
proto = ct->mutable_bool_and();
} else if (name == "xor") {
proto = ct->mutable_bool_xor();
} else if (name == "at_most_one") {
proto = ct->mutable_at_most_one();
} else if (name == "exactly_one") {
proto = ct->mutable_exactly_one();
} else {
ThrowError(PyExc_ValueError,
absl::StrCat("Unknown boolean argument constraint: ", name));
}
return std::make_shared<Constraint>(shared_from_this(), ct_index);
}
std::shared_ptr<Constraint> CpBaseModel::AddBoolOrInternal(
py::sequence literals) {
const int ct_index = model_proto_->constraints_size();
ConstraintProto* ct = model_proto_->add_constraints();
ct->mutable_bool_or(); // If literals is empty.
for (const auto& literal : literals) {
ct->mutable_bool_or()->add_literals(GetOrMakeBooleanIndex(literal));
}
return std::make_shared<Constraint>(shared_from_this(), ct_index);
}
std::shared_ptr<Constraint> CpBaseModel::AddBoolXOrInternal(
py::sequence literals) {
const int ct_index = model_proto_->constraints_size();
ConstraintProto* ct = model_proto_->add_constraints();
ct->mutable_bool_xor(); // If literals is empty.
for (const auto& literal : literals) {
ct->mutable_bool_xor()->add_literals(GetOrMakeBooleanIndex(literal));
if (literals.size() == 1 && py::isinstance<py::iterable>(literals[0])) {
for (const auto& literal : literals[0]) {
proto->add_literals(GetOrMakeBooleanIndex(literal));
}
} else {
for (const auto& literal : literals) {
proto->add_literals(GetOrMakeBooleanIndex(literal));
}
}
return std::make_shared<Constraint>(shared_from_this(), ct_index);
}
@@ -774,17 +767,6 @@ std::shared_ptr<Constraint> CpBaseModel::AddElementInternal(
return std::make_shared<Constraint>(shared_from_this(), ct_index);
}
std::shared_ptr<Constraint> CpBaseModel::AddExactlyOneInternal(
py::sequence literals) {
const int ct_index = model_proto_->constraints_size();
ConstraintProto* ct = model_proto_->add_constraints();
ct->mutable_exactly_one(); // If literals is empty.
for (const auto& literal : literals) {
ct->mutable_exactly_one()->add_literals(GetOrMakeBooleanIndex(literal));
}
return std::make_shared<Constraint>(shared_from_this(), ct_index);
}
std::shared_ptr<Constraint> CpBaseModel::AddInverseInternal(
py::sequence direct, py::sequence inverse) {
const int ct_index = model_proto_->constraints_size();
@@ -799,7 +781,7 @@ std::shared_ptr<Constraint> CpBaseModel::AddInverseInternal(
}
std::shared_ptr<Constraint> CpBaseModel::AddLinearArgumentConstraintInternal(
const std::string& name, const py::handle& target, py::sequence exprs) {
const std::string& name, const py::handle& target, py::args exprs) {
const int ct_index = model_proto_->constraints_size();
ConstraintProto* ct = model_proto_->add_constraints();
LinearArgumentProto* proto;
@@ -821,8 +803,15 @@ std::shared_ptr<Constraint> CpBaseModel::AddLinearArgumentConstraintInternal(
}
LinearExprToProto(target, multiplier, proto->mutable_target());
for (const auto& expr : exprs) {
LinearExprToProto(expr, multiplier, proto->add_exprs());
if (exprs.size() == 1 && py::isinstance<py::iterable>(exprs[0])) {
for (const auto& expr : exprs[0]) {
LinearExprToProto(expr, multiplier, proto->add_exprs());
}
} else {
for (const auto& expr : exprs) {
LinearExprToProto(expr, multiplier, proto->add_exprs());
}
}
return std::make_shared<Constraint>(shared_from_this(), ct_index);
@@ -1885,6 +1874,7 @@ PYBIND11_MODULE(cp_model_helper, m) {
py::class_<CpBaseModel, std::shared_ptr<CpBaseModel>>(
m, "CpBaseModel", "Base class for the CP model.")
.def(py::init<>())
.def(py::init<std::shared_ptr<CpModelProto>>())
.def_property_readonly("model_proto", &CpBaseModel::model_proto,
"Returns the CP model protobuf")
.def("get_or_make_index_from_constant",
@@ -1895,68 +1885,42 @@ PYBIND11_MODULE(cp_model_helper, m) {
.def("get_or_make_variable_index", &CpBaseModel::GetOrMakeVariableIndex,
py::arg("arg"),
"Returns the index of the given variable or constant variable.")
.def("is_boolean_value", &CpBaseModel::IsBooleanValue, py::arg("value"))
.def("rebuild_constant_map", &CpBaseModel::RebuildConstantMap)
.def("add_all_different_internal", &CpBaseModel::AddAllDifferentInternal,
py::arg("expressions"))
.def("add_at_most_one_internal", &CpBaseModel::AddAtMostOneInternal,
py::arg("literals"))
.def("add_automaton_internal", &CpBaseModel::AddAutomatonInternal,
.def("_add_all_different", &CpBaseModel::AddAllDifferentInternal)
.def("_add_automaton", &CpBaseModel::AddAutomatonInternal,
py::arg("transition_expressions"), py::arg("starting_state"),
py::arg("final_states"), py::arg("transition_triples"))
.def("add_bool_and_internal", &CpBaseModel::AddBoolAndInternal,
py::arg("literals"))
.def("add_bool_or_internal", &CpBaseModel::AddBoolOrInternal,
py::arg("literals"))
.def("add_bool_xor_internal", &CpBaseModel::AddBoolXOrInternal,
py::arg("literals"))
.def("add_bounded_linear_expression_internal",
.def("_add_bool_argument_constraint",
&CpBaseModel::AddBoolArgumentConstraintInternal, py::arg("name"))
.def("_add_bounded_linear_expression",
&CpBaseModel::AddBoundedLinearExpressionInternal, py::arg("ble"))
.def("add_element_internal", &CpBaseModel::AddElementInternal,
.def("_add_element", &CpBaseModel::AddElementInternal,
py::arg("index").none(false), py::arg("expressions"),
py::arg("target").none(false))
.def("add_exactly_one_internal", &CpBaseModel::AddExactlyOneInternal,
py::arg("literals"))
.def("add_linear_argument_constraint_internal",
.def("_add_linear_argument_constraint",
&CpBaseModel::AddLinearArgumentConstraintInternal,
py::arg("name").none(false), py::arg("target").none(false),
py::arg("exprs"))
.def("add_inverse_internal", &CpBaseModel::AddInverseInternal,
py::arg("direct"), py::arg("inverse"))
.def("add_reservoir_internal", &CpBaseModel::AddReservoirInternal,
py::arg("name").none(false), py::arg("target").none(false))
.def("_add_inverse", &CpBaseModel::AddInverseInternal, py::arg("direct"),
py::arg("inverse"))
.def("_add_reservoir", &CpBaseModel::AddReservoirInternal,
py::arg("times"), py::arg("level_changes"), py::arg("actives"),
py::arg("min_level"), py::arg("max_level"))
.def("add_table_internal", &CpBaseModel::AddTableInternal,
py::arg("expressions"), py::arg("values"), py::arg("negated"))
.def("_add_table", &CpBaseModel::AddTableInternal, py::arg("expressions"),
py::arg("values"), py::arg("negated"))
// Scheduling support.
.def("new_interval_var_internal", &CpBaseModel::NewIntervalVarInternal,
.def("_new_interval_var", &CpBaseModel::NewIntervalVarInternal,
py::arg("name"), py::arg("start"), py::arg("size"), py::arg("end"),
py::arg("Literals"))
.def("add_no_overlap_internal", &CpBaseModel::AddNoOverlapInternal,
.def("_add_no_overlap", &CpBaseModel::AddNoOverlapInternal,
py::arg("intervals"))
.def("add_no_overlap_2d_internal", &CpBaseModel::AddNoOverlap2DInternal,
.def("_add_no_overlap_2d", &CpBaseModel::AddNoOverlap2DInternal,
py::arg("x_intervals"), py::arg("y_intervals"))
.def("add_cumulative_internal", &CpBaseModel::AddCumulativeInternal,
.def("_add_cumulative", &CpBaseModel::AddCumulativeInternal,
py::arg("intervals"), py::arg("demands"), py::arg("capacity"))
// Routing support.
.def("add_circuit_internal", &CpBaseModel::AddCircuitInternal,
py::arg("arcs"))
.def("add_routes_internal", &CpBaseModel::AddRoutesInternal,
py::arg("arcs"))
// Misc support
.def(py::pickle(
[](std::shared_ptr<CpBaseModel> p) { // __getstate__
/* Return a tuple that fully encodes the state of the object */
return py::make_tuple(p->model_proto());
},
[](py::tuple t) { // __setstate__
if (t.size() != 1) throw std::runtime_error("Invalid state!");
std::shared_ptr<CpModelProto> model_proto =
t[0].cast<std::shared_ptr<CpModelProto>>();
if (model_proto == nullptr) {
throw std::runtime_error("Invalid state!");
}
return std::make_shared<CpBaseModel>(model_proto);
}));
.def("_add_circuit", &CpBaseModel::AddCircuitInternal, py::arg("arcs"))
.def("_add_routes", &CpBaseModel::AddRoutesInternal, py::arg("arcs"));
static const char* kConstraintDoc = R"doc(
Base class for constraints.

View File

@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import copy
import itertools
import sys
@@ -184,12 +185,13 @@ class CpModelTest(absltest.TestCase):
sys.stdout.flush()
def test_is_boolean(self):
self.assertTrue(cp_model.arg_is_boolean(True))
self.assertTrue(cp_model.arg_is_boolean(False))
self.assertFalse(cp_model.arg_is_boolean(1))
self.assertFalse(cp_model.arg_is_boolean(0))
self.assertTrue(cp_model.arg_is_boolean(np.bool_(1)))
self.assertTrue(cp_model.arg_is_boolean(np.bool_(0)))
model = cp_model.CpModel()
self.assertTrue(model.is_boolean_value(True))
self.assertTrue(model.is_boolean_value(False))
self.assertFalse(model.is_boolean_value(1))
self.assertFalse(model.is_boolean_value(0))
self.assertTrue(model.is_boolean_value(np.bool_(1)))
self.assertTrue(model.is_boolean_value(np.bool_(0)))
def test_create_integer_variable(self) -> None:
model = cp_model.CpModel()
@@ -220,6 +222,12 @@ class CpModelTest(absltest.TestCase):
variables = set()
variables.add(var_a)
accumulator = collections.defaultdict(int)
accumulator[var_a] += 1
self.assertEqual(accumulator[var_a], 1)
accumulator[model.get_int_var_from_proto_index(var_a.index)] += 3
self.assertEqual(accumulator[var_a], 4)
def test_literal(self) -> None:
model = cp_model.CpModel()
x = model.new_bool_var("x")
@@ -934,6 +942,7 @@ class CpModelTest(absltest.TestCase):
self.assertLen(model.proto.constraints, 1)
self.assertLen(model.proto.constraints[0].table.exprs, 5)
self.assertLen(model.proto.constraints[0].table.values, 15)
self.assertFalse(model.proto.constraints[0].table.negated)
with self.assertRaises(ValueError):
model.add_allowed_assignments(
x,
@@ -1293,12 +1302,14 @@ class CpModelTest(absltest.TestCase):
model.add_bool_or(True, x[0], x[2])
model.add_bool_or(False, x[0])
model.add_bool_or(x[i] for i in [0, 2, 3, 4])
model.add_bool_or(x[3])
self.assertLen(model.proto.variables, 7)
self.assertLen(model.proto.constraints, 4)
self.assertLen(model.proto.constraints, 5)
self.assertLen(model.proto.constraints[0].bool_or.literals, 5)
self.assertLen(model.proto.constraints[1].bool_or.literals, 3)
self.assertLen(model.proto.constraints[2].bool_or.literals, 2)
self.assertLen(model.proto.constraints[3].bool_or.literals, 4)
self.assertLen(model.proto.constraints[4].bool_or.literals, 1)
def test_at_least_one(self) -> None:
model = cp_model.CpModel()
@@ -2672,6 +2683,24 @@ TRFM"""
self.assertEqual(-2, prod.coefficient)
self.assertEqual(2, prod.offset)
def test_pre_pep8(self):
model = cp_model.CpModel()
x = [model.NewBoolVar(f"x{i}") for i in range(5)]
model.AddBoolOr(x)
self.assertLen(model.proto.variables, 5)
self.assertLen(model.proto.constraints, 1)
self.assertLen(model.proto.constraints[0].bool_or.literals, 5)
model_copy = copy.copy(model)
self.assertTrue(hasattr(model_copy, "AddBoolOr"))
self.assertTrue(hasattr(model_copy, "AddBoolXOr"))
self.assertTrue(hasattr(model_copy, "AddNoOverlap2D"))
model_deepcopy = copy.deepcopy(model)
self.assertTrue(hasattr(model_deepcopy, "AddBoolOr"))
self.assertTrue(hasattr(model_deepcopy, "AddBoolXOr"))
self.assertTrue(hasattr(model_deepcopy, "AddNoOverlap2D"))
if __name__ == "__main__":
absltest.main()