improve model_builder code

This commit is contained in:
Laurent Perron
2025-01-15 13:27:03 +01:00
parent 7352ffc18c
commit 866ef51b3d
3 changed files with 103 additions and 25 deletions

View File

@@ -53,7 +53,9 @@ _IndexOrSeries = Union[pd.Index, pd.Series]
_VariableOrConstraint = Union["LinearConstraint", mbh.Variable]
# Forward solve statuses.
AffineExpr = mbh.AffineExpr
BoundedLinearExpression = mbh.BoundedLinearExpression
FlatExpr = mbh.FlatExpr
LinearExpr = mbh.LinearExpr
SolveStatus = mbh.SolveStatus
Variable = mbh.Variable
@@ -107,7 +109,7 @@ def _add_linear_constraint_to_helper(
if name is not None:
helper.set_constraint_name(c.index, name)
return c
raise TypeError("invalid type={}".format(type(bounded_expr)))
raise TypeError(f"invalid type={type(bounded_expr).__name__!r}")
def _add_enforced_linear_constraint_to_helper(
@@ -171,7 +173,7 @@ def _add_enforced_linear_constraint_to_helper(
helper.set_constraint_name(c.index, name)
return c
raise TypeError("invalid type={}".format(type(bounded_expr)))
raise TypeError(f"invalid type={type(bounded_expr).__name__!r}")
class LinearConstraint:
@@ -631,8 +633,10 @@ class Model:
Returns:
a variable whose domain is [lb, ub].
"""
return Variable(self.__helper, lb, ub, is_integer, name)
if name:
return Variable(self.__helper, lb, ub, is_integer, name)
else:
return Variable(self.__helper, lb, ub, is_integer)
def new_int_var(
self, lb: NumberT, ub: NumberT, name: Optional[str] = None
@@ -712,16 +716,15 @@ class Model:
if not isinstance(index, pd.Index):
raise TypeError("Non-index object is used as index")
if not name.isidentifier():
raise ValueError("name={} is not a valid identifier".format(name))
raise ValueError(f"name={name!r} is not a valid identifier")
if (
mbn.is_a_number(lower_bounds)
and mbn.is_a_number(upper_bounds)
and lower_bounds > upper_bounds
):
raise ValueError(
"lower_bound={} is greater than upper_bound={} for variable set={}".format(
lower_bounds, upper_bounds, name
)
f"lower_bound={lower_bounds} is greater than"
f" upper_bound={upper_bounds} for variable set={name!r}"
)
if (
isinstance(is_integral, bool)
@@ -733,10 +736,9 @@ class Model:
and math.ceil(lower_bounds) > math.floor(upper_bounds)
):
raise ValueError(
"ceil(lower_bound={})={}".format(lower_bounds, math.ceil(lower_bounds))
+ " is greater than floor("
+ "upper_bound={})={}".format(upper_bounds, math.floor(upper_bounds))
+ " for variable set={}".format(name)
f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}"
f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}"
f" for variable set={name!r}"
)
lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index)
upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index)
@@ -871,8 +873,8 @@ class Model:
)
else:
raise TypeError(
f"Not supported: Model.add_linear_constraint({linear_expr})"
f" with type {type(linear_expr)}"
"Not supported:"
f" Model.add_linear_constraint({type(linear_expr).__name__!r})"
)
return ct
@@ -917,7 +919,7 @@ class Model:
],
)
else:
raise TypeError("Not supported: Model.add(" + str(ct) + ")")
raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})")
def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint:
"""Rebuilds a linear constraint object from the model and its index."""
@@ -954,8 +956,7 @@ class Model:
else:
raise TypeError(
"Not supported:"
f" Model.add_enforced_linear_constraint({linear_expr}) with"
f" type {type(linear_expr)}"
f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})"
)
return ct
@@ -1018,7 +1019,7 @@ class Model:
],
)
else:
raise TypeError("Not supported: Model.add_enforced(" + str(ct) + ")")
raise TypeError(f"Not supported: Model.add_enforced({type(ct).__name__!r}")
def enforced_linear_constraint_from_index(
self, index: IntegerT
@@ -1050,7 +1051,10 @@ class Model:
var_indices = [var.index for var in flat_expr.vars]
self.helper.set_objective_coefficients(var_indices, flat_expr.coeffs)
else:
raise TypeError(f"Not supported: Model.minimize/maximize({linear_expr})")
raise TypeError(
"Not supported:"
f" Model.minimize/maximize({type(linear_expr).__name__!r})"
)
@property
def objective_offset(self) -> np.double:
@@ -1233,7 +1237,7 @@ class Solver:
elif isinstance(expr, LinearExpr):
return self.__solve_helper.expression_value(expr)
else:
raise TypeError(f"Unknown expression {expr!r} of type {type(expr)}")
raise TypeError(f"Unknown expression {type(expr).__name__!r}")
def values(self, variables: _IndexOrSeries) -> pd.Series:
"""Returns the values of the input variables.
@@ -1401,7 +1405,7 @@ def _convert_to_series_and_validate_index(
else:
raise ValueError("index does not match")
else:
raise TypeError("invalid type={}".format(type(value_or_series)))
raise TypeError("invalid type={type(value_or_series).__name!r}")
return result
@@ -1429,7 +1433,7 @@ def _convert_to_var_series_and_validate_index(
else:
raise ValueError("index does not match")
else:
raise TypeError("invalid type={}".format(type(var_or_series)))
raise TypeError("invalid type={type(value_or_series).__name!r}")
return result

View File

@@ -13,6 +13,7 @@
# limitations under the License.
import math
import sys
from typing import Any, Callable, Dict, Mapping, Union
from absl.testing import absltest
@@ -32,9 +33,7 @@ from ortools.linear_solver.python import model_builder_helper as mbh
def build_dict(expr: mb.LinearExprT) -> Dict[mbh.Variable, float]:
res = {}
flat_expr = mbh.FlatExpr(expr)
print(f"expr = {expr} flat_expr = {flat_expr}", flush=True)
for var, coeff in zip(flat_expr.vars, flat_expr.coeffs):
print(f"process {var} * {coeff}", flush=True)
if not coeff:
continue
res[var] = coeff
@@ -46,6 +45,10 @@ class ModelBuilderTest(absltest.TestCase):
# checking primal, dual, objective values and other values.
NUM_PLACES = 5
def tearDown(self) -> None:
super().tearDown()
sys.stdout.flush()
# pylint: disable=too-many-statements
def run_minimal_linear_example(self, solver_name):
"""Minimal Linear Example."""
@@ -318,6 +321,49 @@ ENDATA
self.assertEqual(flat_e14.offset, 0.8)
self.assertEqual(e14.__str__(), "(x - t + 0.8)")
e15 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 1, -1])
self.assertIsInstance(e15, mb.Variable)
self.assertEqual(x.index, e15.index)
e16 = mb.LinearExpr.affine(x, 1.0, 0.0)
self.assertIsInstance(e16, mb.Variable)
self.assertEqual(x.index, e16.index)
e17 = -x
self.assertIsInstance(e17, mb.AffineExpr)
self.assertEqual(str(e17), "(-x)")
e18 = mb.LinearExpr.affine(x, 1.0, -2.0)
self.assertIsInstance(e18, mb.AffineExpr)
self.assertEqual(str(e18), "(x - 2)")
e19 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 1, -2])
self.assertIsInstance(e19, mb.AffineExpr)
self.assertEqual(str(e19), "(x - 1)")
e20 = mb.LinearExpr.affine(x, -2.0, 0.0)
self.assertIsInstance(e20, mb.AffineExpr)
self.assertEqual(str(e20), "(-2 * x)")
e21 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 2, -1])
self.assertIsInstance(e21, mb.AffineExpr)
self.assertEqual(str(e21), "(2 * x)")
c1 = x == 2
self.assertEqual(str(c1), "x == 2")
c2 = -x == 3
self.assertEqual(str(c2), "-x == 3")
c3 = x + y == 3
self.assertEqual(str(c3), "(x + y) == 3")
c4 = -x + y == 3
self.assertEqual(str(c4), "(-x + y) == 3")
c5 = x - y == 3
self.assertEqual(str(c5), "(x - y) == 3")
def test_variables(self):
model = mb.Model()
x = model.new_int_var(0.0, 4.0, "x")
@@ -330,6 +376,10 @@ ENDATA
self.assertEqual(1.0, x.lower_bound)
self.assertEqual(3.0, x.upper_bound)
self.assertTrue(x.is_integral)
n1 = model.new_int_var(0, 4)
self.assertEqual(n1.name, "variable#1")
n2 = model.new_int_var(0, 4, None)
self.assertEqual(n2.name, "variable#2")
# Tests the equality operator.
y = model.new_int_var(0.0, 4.0, "y")
@@ -449,6 +499,10 @@ ENDATA
class InternalHelperTest(absltest.TestCase):
def tearDown(self) -> None:
super().tearDown()
sys.stdout.flush()
def test_anonymous_variables(self):
helper = mb.Model().helper
index = helper.add_var()
@@ -641,6 +695,10 @@ class LinearBaseTest(parameterized.TestCase):
class LinearBaseErrorsTest(absltest.TestCase):
def tearDown(self) -> None:
super().tearDown()
sys.stdout.flush()
def test_unknown_linear_type(self):
with self.assertRaises(TypeError):
@@ -758,6 +816,10 @@ class BoundedLinearBaseTest(parameterized.TestCase):
class BoundedLinearBaseErrorsTest(absltest.TestCase):
def tearDown(self) -> None:
super().tearDown()
sys.stdout.flush()
def test_single_var_bounded_linear_expression_as_bool(self):
with self.assertRaisesRegex(
NotImplementedError, "Evaluating a BoundedLinearExpression"
@@ -775,6 +837,10 @@ class BoundedLinearBaseErrorsTest(absltest.TestCase):
class ModelBuilderErrorsTest(absltest.TestCase):
def tearDown(self) -> None:
super().tearDown()
sys.stdout.flush()
def test_new_var_series_errors(self):
with self.assertRaisesRegex(TypeError, r"Non-index object"):
model = mb.Model()
@@ -1607,6 +1673,10 @@ class ModelBuilderObjectiveTest(parameterized.TestCase):
class ModelBuilderProtoTest(absltest.TestCase):
def tearDown(self) -> None:
super().tearDown()
sys.stdout.flush()
def test_export_to_proto(self):
expected = linear_solver_pb2.MPModelProto()
text_format.Parse(
@@ -1970,6 +2040,11 @@ class SolverTest(parameterized.TestCase):
class ModelBuilderExamplesTest(absltest.TestCase):
def tearDown(self) -> None:
super().tearDown()
sys.stdout.flush()
def test_simple_problem(self):
# max 5x1 + 4x2 + 3x3
# s.t 2x1 + 3x2 + x3 <= 5