Files
ortools-clone/ortools/linear_solver/python/pandas_model_test.py

1749 lines
65 KiB
Python

#!/usr/bin/env python3
# Copyright 2010-2022 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
from typing import Callable, Union
import pandas as pd
#from google3.net.proto2.contrib.pyutil import compare
from google.protobuf import text_format
from absl.testing import absltest
from absl.testing import parameterized
from ortools.linear_solver import linear_solver_pb2
from ortools.linear_solver.python import pandas_model as pdm
# This test suite should not be depended on as a public API.
class InternalHelperTest(absltest.TestCase):
def test_anonymous_variables(self):
helper = pdm.OptimizationModel(name="test_name")._helper
index = helper.add_var()
variable = pdm._Variable(helper, index)
self.assertEqual(variable._name, f"variable#{index}")
def test_anonymous_constraints(self):
helper = pdm.OptimizationModel(name="test_name")._helper
index = helper.add_linear_constraint()
constraint = pdm._LinearConstraint(helper, index)
self.assertEqual(constraint._name, f"linear_constraint#{index}")
class LinearBaseTest(parameterized.TestCase):
def setUp(self):
super().setUp()
simple_model = pdm.OptimizationModel(name="test_name")
simple_model.create_variables(name="x", index=pd.Index(range(3), name="i"))
simple_model.create_variables(name="y", index=pd.Index(range(5), name="i"))
self.simple_model = simple_model
@parameterized.named_parameters(
# Variable / Indexing
dict(
testcase_name="x[0]",
expr=lambda x, y: x[0],
expected_repr="x[0]",
),
dict(
testcase_name="x[1]",
expr=lambda x, y: x[1],
expected_repr="x[1]",
),
dict(
testcase_name="x[2]",
expr=lambda x, y: x[2],
expected_repr="x[2]",
),
dict(
testcase_name="y[0]",
expr=lambda x, y: y[0],
expected_repr="y[0]",
),
dict(
testcase_name="y[4]",
expr=lambda x, y: y[4],
expected_repr="y[4]",
),
# Sum
dict(
testcase_name="x[0] + 5",
expr=lambda x, y: x[0] + 5,
expected_repr="5.0 + x[0]",
),
dict(
testcase_name="x[0] - 5",
expr=lambda x, y: x[0] - 5,
expected_repr="-5.0 + x[0]",
),
dict(
testcase_name="5 - x[0]",
expr=lambda x, y: 5 - x[0],
expected_repr="5.0 - x[0]",
),
dict(
testcase_name="5 + x[0]",
expr=lambda x, y: 5 + x[0],
expected_repr="5.0 + x[0]",
),
dict(
testcase_name="x[0] + y[0]",
expr=lambda x, y: x[0] + y[0],
expected_repr="0.0 + x[0] + y[0]",
),
dict(
testcase_name="x[0] + y[0] + 5",
expr=lambda x, y: x[0] + y[0] + 5,
expected_repr="5.0 + x[0] + y[0]",
),
dict(
testcase_name="5 + x[0] + y[0]",
expr=lambda x, y: 5 + x[0] + y[0],
expected_repr="5.0 + x[0] + y[0]",
),
dict(
testcase_name="5 + x[0] - x[0]",
expr=lambda x, y: 5 + x[0] - x[0],
expected_repr="5.0",
),
dict(
testcase_name="5 + x[0] - y[0]",
expr=lambda x, y: 5 + x[0] - y[0],
expected_repr="5.0 + x[0] - y[0]",
),
dict(
testcase_name="x.sum()",
expr=lambda x, y: x.sum(),
expected_repr="0.0 + x[0] + x[1] + x[2]",
),
dict(
testcase_name="x.add(y, fill_value=0).sum() + 5",
expr=lambda x, y: x.add(y, fill_value=0).sum() + 5,
expected_repr="5.0 + x[0] + x[1] + x[2] + y[0] + y[1] + ...",
),
# Product
dict(
testcase_name="- x.sum()",
expr=lambda x, y: -x.sum(),
expected_repr="0.0 - x[0] - x[1] - x[2]",
),
dict(
testcase_name="5 - x.sum()",
expr=lambda x, y: 5 - x.sum(),
expected_repr="5.0 - x[0] - x[1] - x[2]",
),
dict(
testcase_name="x.sum() / 2.0",
expr=lambda x, y: x.sum() / 2.0,
expected_repr="0.0 + 0.5 * x[0] + 0.5 * x[1] + 0.5 * x[2]",
),
dict(
testcase_name="(3 * x).sum()",
expr=lambda x, y: (3 * x).sum(),
expected_repr="0.0 + 3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]",
),
dict(
testcase_name="(x * 3).sum()",
expr=lambda x, y: (x * 3).sum(),
expected_repr="0.0 + 3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]",
),
dict(
testcase_name="x.sum() * 3",
expr=lambda x, y: x.sum() * 3,
expected_repr="0.0 + 3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]",
),
dict(
testcase_name="3 * x.sum()",
expr=lambda x, y: 3 * x.sum(),
expected_repr="0.0 + 3.0 * x[0] + 3.0 * x[1] + 3.0 * x[2]",
),
dict(
testcase_name="0 * x.sum() + y.sum()",
expr=lambda x, y: 0 * x.sum() + y.sum(),
expected_repr="0.0 + y[0] + y[1] + y[2] + y[3] + y[4]",
),
# LinearExpression
dict(
testcase_name="_as_flat_linear_expression(x.sum())",
expr=lambda x, y: pdm._as_flat_linear_expression(x.sum()),
expected_repr="0.0 + x[0] + x[1] + x[2]",
),
dict(
testcase_name=(
"_as_flat_linear_expression(_as_flat_linear_expression(x.sum()))"
),
# pylint: disable=g-long-lambda
expr=lambda x, y: pdm._as_flat_linear_expression(
pdm._as_flat_linear_expression(x.sum())
),
expected_repr="0.0 + x[0] + x[1] + x[2]",
),
dict(
testcase_name="""_as_flat_linear_expression(sum([
_as_flat_linear_expression(x.sum()),
_as_flat_linear_expression(x.sum()),
]))""",
# pylint: disable=g-long-lambda
expr=lambda x, y: pdm._as_flat_linear_expression(
sum(
[
pdm._as_flat_linear_expression(x.sum()),
pdm._as_flat_linear_expression(x.sum()),
]
)
),
expected_repr="0.0 + 2.0 * x[0] + 2.0 * x[1] + 2.0 * x[2]",
),
)
def test_repr(self, expr, expected_repr):
x = self.simple_model.get_variable_references("x")
y = self.simple_model.get_variable_references("y")
self.assertEqual(repr(expr(x, y)), expected_repr)
class LinearBaseErrorsTest(absltest.TestCase):
def test_unknown_linear_type(self):
with self.assertRaisesRegex(TypeError, r"Unrecognized linear expression"):
class UnknownLinearType(pdm._LinearBase):
pass
pdm._as_flat_linear_expression(UnknownLinearType())
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
model = pdm.OptimizationModel(name="divide_by_zero")
x = model.create_variables(name="x", index=pd.Index(range(1)))
print(x / 0)
def test_boolean_expression(self):
with self.assertRaisesRegex(
NotImplementedError, r"LinearExpression as a Boolean value"
):
model = pdm.OptimizationModel(name="boolean_expression")
x = model.create_variables(name="x", index=pd.Index(range(1)))
bool(x.sum())
class BoundedLinearBaseTest(parameterized.TestCase):
def setUp(self):
super().setUp()
simple_model = pdm.OptimizationModel(name="test_name")
simple_model.create_variables(name="x", index=pd.Index(range(3), name="i"))
simple_model.create_variables(name="y", index=pd.Index(range(5), name="i"))
self.simple_model = simple_model
@parameterized.product(
lhs=(
lambda x, y: x.sum(),
lambda x, y: -x.sum(),
lambda x, y: x.sum() * 0,
lambda x, y: x.sum() * 3,
lambda x, y: x[0],
lambda x, y: x[1],
lambda x, y: x[2],
lambda x, y: -math.inf,
lambda x, y: -1,
lambda x, y: 0,
lambda x, y: 1,
lambda x, y: 1.1,
lambda x, y: math.inf,
),
rhs=(
lambda x, y: y.sum(),
lambda x, y: -y.sum(),
lambda x, y: y.sum() * 0,
lambda x, y: y.sum() * 3,
lambda x, y: y[0],
lambda x, y: y[1],
lambda x, y: y[2],
lambda x, y: -math.inf,
lambda x, y: -1,
lambda x, y: 0,
lambda x, y: 1,
lambda x, y: 1.1,
lambda x, y: math.inf,
),
op=(
lambda lhs, rhs: lhs == rhs,
lambda lhs, rhs: lhs <= rhs,
lambda lhs, rhs: lhs >= rhs,
),
)
def test_repr(self, lhs, rhs, op):
x = self.simple_model.get_variable_references("x")
y = self.simple_model.get_variable_references("y")
l: pdm._LinearType = lhs(x, y)
r: pdm._LinearType = rhs(x, y)
result = op(l, r)
if isinstance(l, pdm._LinearBase) or isinstance(r, pdm._LinearBase):
self.assertIsInstance(result, pdm._BoundedLinearBase)
self.assertIn("=", repr(result), msg="is one of ==, <=, or >=")
else:
self.assertIsInstance(result, bool)
def test_doublesided_bounded_expressions(self):
x = self.simple_model.get_variable_references("x")
self.assertEqual(
"0 <= x[0] <= 1", repr(pdm._BoundedLinearExpression(x[0], 0, 1))
)
def test_free_bounded_expressions(self):
x = self.simple_model.get_variable_references("x")
self.assertEqual(
"x[0] free",
repr(pdm._BoundedLinearExpression(x[0], -math.inf, math.inf)),
)
def test_var_eq_var_as_bool(self):
x = self.simple_model.get_variable_references("x")
y = self.simple_model.get_variable_references("y")
self.assertEqual(x[0], x[0])
self.assertNotEqual(x[0], x[1])
self.assertNotEqual(x[0], y[0])
self.assertEqual(x[1], x[1])
self.assertNotEqual(x[1], x[0])
self.assertNotEqual(x[1], y[1])
self.assertEqual(y[0], y[0])
self.assertNotEqual(y[0], y[1])
self.assertNotEqual(y[0], x[0])
self.assertEqual(y[1], y[1])
self.assertNotEqual(y[1], y[0])
self.assertNotEqual(y[1], x[1])
class BoundedLinearBaseErrorsTest(absltest.TestCase):
def test_bounded_linear_expression_as_bool(self):
with self.assertRaisesRegex(NotImplementedError, "Boolean value"):
model = pdm.OptimizationModel(name="bounded_linear_expression_as_bool")
x = model.create_variables(name="x", index=pd.Index(range(1)))
bool(pdm._BoundedLinearExpression(x, 0, 1))
class OptimizationModelMetadataTest(absltest.TestCase):
def test_name(self):
model = pdm.OptimizationModel(name="test_name")
self.assertEqual("test_name", model.get_name())
def test_schema_empty(self):
model = pdm.OptimizationModel(name="test_name")
self.assertIsInstance(model.get_schema(), pd.DataFrame)
def test_schema_no_constraints(self):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(
name="x",
index=pd.Index(range(3)),
lower_bound=0,
upper_bound=1,
is_integer=True,
)
y = model.create_variables(
name="y",
index=pd.MultiIndex.from_tuples([(1, 2), (3, 4)], names=["i", "j"]),
lower_bound=0,
)
z = model.create_variables(
name="z",
index=pd.MultiIndex.from_product(((1, 2), ("a", "b", "c"))),
lower_bound=-5,
upper_bound=10,
is_integer=True,
)
schema = model.get_schema()
self.assertIsInstance(schema, pd.DataFrame)
self.assertLen(schema, 3)
self.assertSequenceAlmostEqual(
schema.columns, ["type", "name", "dimensions", "count"]
)
self.assertSequenceAlmostEqual(schema.type, ["variable"] * 3)
self.assertSequenceAlmostEqual(schema.name, ["x", "y", "z"])
self.assertSequenceAlmostEqual(
schema.dimensions, [(None,), ("i", "j"), (None, None)]
)
self.assertSequenceAlmostEqual(schema["count"], [len(x), len(y), len(z)])
self.assertEqual(
repr(model),
"""OptimizationModel(name=test_name) with the following schema:
type name dimensions count
0 variable x [None] 3
1 variable y ['i', 'j'] 2
2 variable z [None, None] 6""",
)
def test_full_schema(self):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(
name="x",
index=pd.Index(range(3)),
lower_bound=0,
upper_bound=1,
is_integer=True,
)
y = model.create_variables(
name="y",
index=pd.MultiIndex.from_tuples([(1, 2), (3, 4)], names=["i", "j"]),
lower_bound=0,
)
z = model.create_variables(
name="z",
index=pd.MultiIndex.from_product(
((1, 2), ("a", "b", "c")), names=["i", "k"]
),
lower_bound=-5,
upper_bound=10,
is_integer=True,
)
c1 = model.create_linear_constraints(
name="x_sum_le_constant",
bounded_exprs=(x.sum() <= 10),
)
c2 = model.create_linear_constraints(
name="y_groupbyj_sum_ge_constant",
bounded_exprs=y.groupby("j").sum().apply(lambda expr: expr >= 3),
)
c3 = model.create_linear_constraints(
name="y_groupbyi_sum_eq_z_groupbyi_sum",
bounded_exprs=y.groupby("i")
.sum()
.sub(z.groupby("i").sum())
.dropna()
.apply(lambda expr: expr == 0),
)
schema = model.get_schema()
self.assertIsInstance(schema, pd.DataFrame)
self.assertLen(schema, 6)
self.assertSequenceAlmostEqual(
schema.columns, ["type", "name", "dimensions", "count"]
)
self.assertSequenceAlmostEqual(
schema.type, ["variable"] * 3 + ["linear_constraint"] * 3
)
self.assertSequenceAlmostEqual(
schema.name,
[
"x",
"y",
"z",
"x_sum_le_constant",
"y_groupbyj_sum_ge_constant",
"y_groupbyi_sum_eq_z_groupbyi_sum",
],
)
self.assertSequenceAlmostEqual(
schema.dimensions,
[(None,), ("i", "j"), ("i", "k"), (None,), ("j",), ("i",)],
)
self.assertSequenceAlmostEqual(
schema["count"], [len(x), len(y), len(z), len(c1), len(c2), len(c3)]
)
self.assertEqual(
repr(model),
"""OptimizationModel(name=test_name) with the following schema:
type name dimensions count
0 variable x [None] 3
1 variable y ['i', 'j'] 2
2 variable z ['i', 'k'] 6
3 linear_constraint x_sum_le_constant [None] 1
4 linear_constraint y_groupbyj_sum_ge_constant ['j'] 2
5 linear_constraint y_groupbyi_sum_eq_z_groupbyi_sum ['i'] 3""",
)
class OptimizationModelErrorsTest(absltest.TestCase):
def test_name_errors(self):
with self.assertRaisesRegex(ValueError, r"not a valid identifier"):
pdm.OptimizationModel(name="")
def test_create_variables_errors(self):
with self.assertRaisesRegex(TypeError, r"Non-index object"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="", index=pd.DataFrame())
with self.assertRaisesRegex(TypeError, r"invalid type"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="x", index=pd.Index([0]), lower_bound="0")
with self.assertRaisesRegex(TypeError, r"invalid type"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="x", index=pd.Index([0]), upper_bound="0")
with self.assertRaisesRegex(TypeError, r"invalid type"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="x", index=pd.Index([0]), is_integer="True")
with self.assertRaisesRegex(ValueError, r"not a valid identifier"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="", index=pd.Index([0]))
with self.assertRaisesRegex(ValueError, r"already exists"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="x", index=pd.Index([0]))
model.create_variables(name="x", index=pd.Index([0]))
with self.assertRaisesRegex(ValueError, r"is greater than"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(
name="x",
index=pd.Index([0]),
lower_bound=0.2,
upper_bound=0.1,
)
with self.assertRaisesRegex(ValueError, r"is greater than"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(
name="x",
index=pd.Index([0]),
lower_bound=0.1,
upper_bound=0.2,
is_integer=True,
)
with self.assertRaisesRegex(ValueError, r"index does not match"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(
name="x", index=pd.Index([0]), lower_bound=pd.Series([1, 2])
)
with self.assertRaisesRegex(ValueError, r"index does not match"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(
name="x", index=pd.Index([0]), upper_bound=pd.Series([1, 2])
)
with self.assertRaisesRegex(ValueError, r"index does not match"):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(
name="x", index=pd.Index([0]), is_integer=pd.Series([False, True])
)
def test_get_variables_errors(self):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="x", index=pd.Index(range(3)))
with self.assertRaisesRegex(KeyError, r"no variable set named"):
model.get_variables(name="nonexistent_variable")
def test_get_variable_references_errors(self):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="x", index=pd.Index(range(3)))
with self.assertRaisesRegex(KeyError, r"no variable set named"):
model.get_variable_references(None)
with self.assertRaisesRegex(KeyError, r"no variable set named"):
model.get_variable_references(name="")
def test_create_linear_constraints_errors(self):
with self.assertRaisesRegex(ValueError, r"not a valid identifier"):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(name="x", index=pd.Index(range(1)))
model.create_linear_constraints(name="", bounded_exprs=x[0] == 0)
with self.assertRaisesRegex(ValueError, r"already exists"):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(name="x", index=pd.Index(range(1)))
model.create_linear_constraints(name="c", bounded_exprs=x[0] <= 0)
model.create_linear_constraints(name="c", bounded_exprs=x[0] >= 0)
with self.assertRaisesRegex(TypeError, r"invalid type"):
model = pdm.OptimizationModel(name="test_name")
model.create_linear_constraints(name="c", bounded_exprs="True")
with self.assertRaisesRegex(TypeError, r"invalid type"):
model = pdm.OptimizationModel(name="test_name")
model.create_linear_constraints(name="c", bounded_exprs=pd.Series(["T"]))
def test_get_linear_constraint_references_errors(self):
with self.assertRaises(KeyError):
model = pdm.OptimizationModel(name="test_name")
model.get_linear_constraint_references("c")
class OptimizationModelVariablesTest(parameterized.TestCase):
_variable_indices = (
pd.Index(range(3)),
pd.Index(range(5), name="i"),
pd.MultiIndex.from_product(((1, 2), ("a", "b", "c")), names=["i", "j"]),
pd.MultiIndex.from_product((("a", "b"), (1, 2, 3))),
)
_bounds = (
lambda index: (-math.inf, -10.5),
lambda index: (-math.inf, -1),
lambda index: (-math.inf, 0),
lambda index: (-math.inf, 10),
lambda index: (-math.inf, math.inf),
lambda index: (-10, -1.1),
lambda index: (-10, 0),
lambda index: (-10, -10),
lambda index: (-10, 3),
lambda index: (-9, math.inf),
lambda index: (-1, 1),
lambda index: (0, 0),
lambda index: (0, 1),
lambda index: (0, math.inf),
lambda index: (1, 1),
lambda index: (1, 10.1),
lambda index: (1, math.inf),
lambda index: (100.1, math.inf),
# pylint: disable=g-long-lambda
lambda index: (
pd.Series(-math.inf, index=index),
pd.Series(-10.5, index=index),
),
lambda index: (
pd.Series(-math.inf, index=index),
pd.Series(-1, index=index),
),
lambda index: (
pd.Series(-math.inf, index=index),
pd.Series(0, index=index),
),
lambda index: (
pd.Series(-math.inf, index=index),
pd.Series(10, index=index),
),
lambda index: (
pd.Series(-math.inf, index=index),
pd.Series(math.inf, index=index),
),
lambda index: (pd.Series(-10, index=index), pd.Series(-1.1, index=index)),
lambda index: (pd.Series(-10, index=index), pd.Series(0, index=index)),
lambda index: (pd.Series(-10, index=index), pd.Series(-10, index=index)),
lambda index: (pd.Series(-10, index=index), pd.Series(3, index=index)),
lambda index: (
pd.Series(-9, index=index),
pd.Series(math.inf, index=index),
),
lambda index: (pd.Series(-1, index=index), pd.Series(1, index=index)),
lambda index: (pd.Series(0, index=index), pd.Series(0, index=index)),
lambda index: (pd.Series(0, index=index), pd.Series(1, index=index)),
lambda index: (
pd.Series(0, index=index),
pd.Series(math.inf, index=index),
),
lambda index: (pd.Series(1, index=index), pd.Series(1, index=index)),
lambda index: (pd.Series(1, index=index), pd.Series(10.1, index=index)),
lambda index: (
pd.Series(1, index=index),
pd.Series(math.inf, index=index),
),
lambda index: (
pd.Series(100.1, index=index),
pd.Series(math.inf, index=index),
),
)
_is_integer = (
lambda index: False,
lambda index: True,
lambda index: pd.Series(False, index=index),
lambda index: pd.Series(True, index=index),
)
@parameterized.product(
index=_variable_indices, bounds=_bounds, is_integer=_is_integer
)
def test_create_variables(self, index, bounds, is_integer):
model = pdm.OptimizationModel(name="test_name")
variables = model.create_variables(
name="test_variable",
index=index,
lower_bound=bounds(index)[0],
upper_bound=bounds(index)[1],
is_integer=is_integer(index),
)
self.assertLen(variables, len(index))
self.assertLen(set(variables), len(index))
for i in index:
self.assertEqual(repr(variables[i]), f"test_variable[{i}]")
@parameterized.named_parameters(
dict(testcase_name="all", variable_name=None, variable_count=3 + 5),
dict(testcase_name="x", variable_name="x", variable_count=3),
dict(testcase_name="y", variable_name="y", variable_count=5),
)
def test_get_variables(self, variable_name, variable_count):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="x", index=pd.Index(range(3)))
model.create_variables(name="y", index=pd.Index(range(5)))
for variables, expected_count in (
(model.get_variables(), 3 + 5),
(model.get_variables(variable_name), variable_count),
(model.get_variables(name=variable_name), variable_count),
):
self.assertIsInstance(variables, pd.Index)
self.assertLen(variables, expected_count)
@parameterized.product(
index=_variable_indices, bounds=_bounds, is_integer=_is_integer
)
def test_get_variable_lower_bounds(self, index, bounds, is_integer):
lower_bound, upper_bound = bounds(index)
model = pdm.OptimizationModel(name="test_name")
model.create_variables(
name="x",
index=index,
lower_bound=lower_bound,
upper_bound=upper_bound,
is_integer=is_integer(index),
)
model.create_variables(
name="y",
index=index,
lower_bound=lower_bound,
upper_bound=upper_bound,
is_integer=is_integer(index),
)
for lower_bounds in (
model.get_variable_lower_bounds(model.get_variables("x")),
model.get_variable_lower_bounds(model.get_variables("y")),
model.get_variable_lower_bounds(model.get_variable_references("x")),
model.get_variable_lower_bounds(model.get_variable_references("y")),
):
self.assertSequenceAlmostEqual(
lower_bounds,
pdm._convert_to_series_and_validate_index(lower_bound, index),
)
self.assertSequenceAlmostEqual(
model.get_variable_lower_bounds(),
pd.concat(
[
model.get_variable_lower_bounds(model.get_variables("x")),
model.get_variable_lower_bounds(model.get_variables("y")),
]
),
)
for variables in (model.get_variables("x"), model.get_variables("y")):
lower_bounds = model.get_variable_lower_bounds(variables)
self.assertSequenceAlmostEqual(lower_bounds.index, variables)
for variables in (
model.get_variable_references("x"),
model.get_variable_references("y"),
):
lower_bounds = model.get_variable_lower_bounds(variables)
self.assertSequenceAlmostEqual(lower_bounds.index, variables.index)
@parameterized.named_parameters(
dict(testcase_name="x", variable_name="x", variable_count=3),
dict(testcase_name="y", variable_name="y", variable_count=5),
)
def test_get_variable_references(self, variable_name, variable_count):
model = pdm.OptimizationModel(name="test_name")
model.create_variables(name="x", index=pd.Index(range(3)))
model.create_variables(name="y", index=pd.Index(range(5)))
self.assertLen(model.get_variables(), 3 + 5)
for variables in (
model.get_variable_references(variable_name),
model.get_variable_references(name=variable_name),
):
self.assertLen(variables, variable_count)
@parameterized.product(
index=_variable_indices, bounds=_bounds, is_integer=_is_integer
)
def test_get_variable_upper_bounds(self, index, bounds, is_integer):
lower_bound, upper_bound = bounds(index)
model = pdm.OptimizationModel(name="test_name")
model.create_variables(
name="x",
index=index,
lower_bound=lower_bound,
upper_bound=upper_bound,
is_integer=is_integer(index),
)
model.create_variables(
name="y",
index=index,
lower_bound=lower_bound,
upper_bound=upper_bound,
is_integer=is_integer(index),
)
for upper_bounds in (
model.get_variable_upper_bounds(model.get_variables("x")),
model.get_variable_upper_bounds(model.get_variables("y")),
model.get_variable_upper_bounds(model.get_variable_references("x")),
model.get_variable_upper_bounds(model.get_variable_references("y")),
):
self.assertSequenceAlmostEqual(
upper_bounds,
pdm._convert_to_series_and_validate_index(upper_bound, index),
)
self.assertSequenceAlmostEqual(
model.get_variable_upper_bounds(),
pd.concat(
[
model.get_variable_upper_bounds(model.get_variables("x")),
model.get_variable_upper_bounds(model.get_variables("y")),
]
),
)
for variables in (model.get_variables("x"), model.get_variables("y")):
upper_bounds = model.get_variable_upper_bounds(variables)
self.assertSequenceAlmostEqual(upper_bounds.index, variables)
for variables in (
model.get_variable_references("x"),
model.get_variable_references("y"),
):
upper_bounds = model.get_variable_upper_bounds(variables)
self.assertSequenceAlmostEqual(upper_bounds.index, variables.index)
class OptimizationModelLinearConstraintsTest(parameterized.TestCase):
constraint_test_cases = [
# pylint: disable=g-long-lambda
dict(
testcase_name="True",
name="true",
bounded_exprs=lambda x, y: True,
constraint_count=1,
lower_bounds=[0],
upper_bounds=[0],
expression_terms=lambda x, y: [{}],
expression_offsets=[0],
),
dict(
testcase_name="False",
name="false",
bounded_exprs=lambda x, y: False,
constraint_count=1,
lower_bounds=[1],
upper_bounds=[1],
expression_terms=lambda x, y: [{}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] <= 1.5",
name="x0_le_c",
bounded_exprs=lambda x, y: x[0] <= 1.5,
constraint_count=1,
lower_bounds=[-math.inf],
upper_bounds=[1.5],
expression_terms=lambda x, y: [{x[0]: 1}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] == 1",
name="x0_eq_c",
bounded_exprs=lambda x, y: x[0] == 1,
constraint_count=1,
lower_bounds=[1],
upper_bounds=[1],
expression_terms=lambda x, y: [{x[0]: 1}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] >= -1",
name="x0_ge_c",
bounded_exprs=lambda x, y: x[0] >= -1,
constraint_count=1,
lower_bounds=[-1],
upper_bounds=[math.inf],
expression_terms=lambda x, y: [{x[0]: 1}],
expression_offsets=[0],
),
dict(
testcase_name="-1.5 <= x[0]",
name="c_le_x0",
bounded_exprs=lambda x, y: -1.5 <= x[0],
constraint_count=1,
lower_bounds=[-1.5],
upper_bounds=[math.inf],
expression_terms=lambda x, y: [{x[0]: 1}],
expression_offsets=[0],
),
dict(
testcase_name="0 == x[0]",
name="c_eq_x0",
bounded_exprs=lambda x, y: 0 == x[0],
constraint_count=1,
lower_bounds=[0],
upper_bounds=[0],
expression_terms=lambda x, y: [{x[0]: 1}],
expression_offsets=[0],
),
dict(
testcase_name="10 >= x[0]",
name="c_ge_x0",
bounded_exprs=lambda x, y: 10 >= x[0],
constraint_count=1,
lower_bounds=[-math.inf],
upper_bounds=[10],
expression_terms=lambda x, y: [{x[0]: 1}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] <= x[0]",
name="x0_le_x0",
bounded_exprs=lambda x, y: x[0] <= x[0],
constraint_count=1,
lower_bounds=[-math.inf],
upper_bounds=[0],
expression_terms=lambda x, y: [{x[0]: 0}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] == x[0]",
name="x0_eq_x0",
bounded_exprs=lambda x, y: x[0] == x[0],
constraint_count=1,
lower_bounds=[0],
upper_bounds=[0],
expression_terms=lambda x, y: [{x[0]: 0}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] >= x[0]",
name="x0_ge_x0",
bounded_exprs=lambda x, y: x[0] >= x[0],
constraint_count=1,
lower_bounds=[0],
upper_bounds=[math.inf],
expression_terms=lambda x, y: [{x[0]: 0}],
expression_offsets=[0],
),
dict(
# x[0] - x[0] <= 3
testcase_name="x[0] - 1 <= x[0] + 2",
name="x0c_le_x0c",
bounded_exprs=lambda x, y: pd.Series(x[0] - 1 <= x[0] + 2),
constraint_count=1,
lower_bounds=[-math.inf],
upper_bounds=[3],
expression_terms=lambda x, y: [{x[0]: 0}],
expression_offsets=[0],
),
dict(
# x[0] - x[0] == 3
testcase_name="x[0] - 1 == x[0] + 2",
name="x0c_eq_x0c",
bounded_exprs=lambda x, y: pd.Series(x[0] - 1 == x[0] + 2),
constraint_count=1,
lower_bounds=[3],
upper_bounds=[3],
expression_terms=lambda x, y: [{x[0]: 0}],
expression_offsets=[0],
),
dict(
# x[0] - x[0] >= 3
testcase_name="x[0] - 1 >= x[0] + 2",
name="x0c_ge_x0c",
bounded_exprs=lambda x, y: pd.Series(x[0] - 1 >= x[0] + 2),
constraint_count=1,
lower_bounds=[3],
upper_bounds=[math.inf],
expression_terms=lambda x, y: [{x[0]: 0}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] <= x[1]",
name="x0_le_x1",
bounded_exprs=lambda x, y: x[0] <= x[1],
constraint_count=1,
lower_bounds=[-math.inf],
upper_bounds=[0],
expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] == x[1]",
name="x0_eq_x1",
bounded_exprs=lambda x, y: x[0] == x[1],
constraint_count=1,
lower_bounds=[0],
upper_bounds=[0],
expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}],
expression_offsets=[0],
),
dict(
testcase_name="x[0] >= x[1]",
name="x0_ge_x1",
bounded_exprs=lambda x, y: x[0] >= x[1],
constraint_count=1,
lower_bounds=[0],
upper_bounds=[math.inf],
expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}],
expression_offsets=[0],
),
dict(
# x[0] - x[1] <= -3
testcase_name="x[0] + 1 <= x[1] - 2",
name="x0c_le_x1c",
bounded_exprs=lambda x, y: x[0] + 1 <= x[1] - 2,
constraint_count=1,
lower_bounds=[-math.inf],
upper_bounds=[-3],
expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}],
expression_offsets=[0],
),
dict(
# x[0] - x[1] == -3
testcase_name="x[0] + 1 == x[1] - 2",
name="x0c_eq_x1c",
bounded_exprs=lambda x, y: x[0] + 1 == x[1] - 2,
constraint_count=1,
lower_bounds=[-3],
upper_bounds=[-3],
expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}],
expression_offsets=[0],
),
dict(
# x[0] - x[1] >= -3
testcase_name="x[0] + 1 >= x[1] - 2",
name="x0c_ge_x1c",
bounded_exprs=lambda x, y: pd.Series(x[0] + 1 >= x[1] - 2),
constraint_count=1,
lower_bounds=[-3],
upper_bounds=[math.inf],
expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}],
expression_offsets=[0],
),
dict(
testcase_name="x <= 0",
name="x_le_c",
bounded_exprs=lambda x, y: x.apply(lambda expr: expr <= 0),
constraint_count=3,
lower_bounds=[-math.inf] * 3,
upper_bounds=[0] * 3,
expression_terms=lambda x, y: [{xi: 1} for xi in x],
expression_offsets=[0] * 3,
),
dict(
testcase_name="x >= 0",
name="x_ge_c",
bounded_exprs=lambda x, y: x.apply(lambda expr: expr >= 0),
constraint_count=3,
lower_bounds=[0] * 3,
upper_bounds=[math.inf] * 3,
expression_terms=lambda x, y: [{xi: 1} for xi in x],
expression_offsets=[0] * 3,
),
dict(
testcase_name="x == 0",
name="x_eq_c",
bounded_exprs=lambda x, y: x.apply(lambda expr: expr == 0),
constraint_count=3,
lower_bounds=[0] * 3,
upper_bounds=[0] * 3,
expression_terms=lambda x, y: [{xi: 1} for xi in x],
expression_offsets=[0] * 3,
),
dict(
testcase_name="y == 0",
name="y_eq_c",
bounded_exprs=(lambda x, y: y.apply(lambda expr: expr == 0)),
constraint_count=2 * 3,
lower_bounds=[0] * 2 * 3,
upper_bounds=[0] * 2 * 3,
expression_terms=lambda x, y: [{yi: 1} for yi in y],
expression_offsets=[0] * 3 * 2,
),
dict(
testcase_name='y.groupby("i").sum() == 0',
name="ygroupbyi_eq_c",
bounded_exprs=(
lambda x, y: y.groupby("i").sum().apply(lambda expr: expr == 0)
),
constraint_count=2,
lower_bounds=[0] * 2,
upper_bounds=[0] * 2,
expression_terms=lambda x, y: [
{y[1, "a"]: 1, y[1, "b"]: 1, y[1, "c"]: 1},
{y[2, "a"]: 1, y[2, "b"]: 1, y[2, "c"]: 1},
],
expression_offsets=[0] * 2,
),
dict(
testcase_name='y.groupby("j").sum() == 0',
name="ygroupbyj_eq_c",
bounded_exprs=(
lambda x, y: y.groupby("j").sum().apply(lambda expr: expr == 0)
),
constraint_count=3,
lower_bounds=[0] * 3,
upper_bounds=[0] * 3,
expression_terms=lambda x, y: [
{y[1, "a"]: 1, y[2, "a"]: 1},
{y[1, "b"]: 1, y[2, "b"]: 1},
{y[1, "c"]: 1, y[2, "c"]: 1},
],
expression_offsets=[0] * 3,
),
dict(
testcase_name='3 * x + y.groupby("i").sum() <= 0',
name="broadcast_align_fill",
bounded_exprs=(
lambda x, y: (3 * x)
.add(y.groupby("i").sum(), fill_value=0)
.apply(lambda expr: expr <= 0)
),
constraint_count=3,
lower_bounds=[-math.inf] * 3,
upper_bounds=[0] * 3,
expression_terms=lambda x, y: [
{x[0]: 3},
{x[1]: 3, y[1, "a"]: 1, y[1, "b"]: 1, y[1, "c"]: 1},
{x[2]: 3, y[2, "a"]: 1, y[2, "b"]: 1, y[2, "c"]: 1},
],
expression_offsets=[0] * 3,
),
]
def create_test_model(self, name, bounded_exprs):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(
name="x",
index=pd.Index(range(3), name="i"),
)
y = model.create_variables(
name="y",
index=pd.MultiIndex.from_product(
((1, 2), ("a", "b", "c")), names=["i", "j"]
),
)
model.create_linear_constraints(name=name, bounded_exprs=bounded_exprs(x, y))
return model
@parameterized.named_parameters(
# pylint: disable=g-complex-comprehension
{
f: tc[f]
for f in [
"testcase_name",
"name",
"bounded_exprs",
"constraint_count",
]
}
for tc in constraint_test_cases
)
def test_get_linear_constraints(
self,
name,
bounded_exprs,
constraint_count,
):
model = self.create_test_model(name, bounded_exprs)
for linear_constraints, expected_count in (
(model.get_linear_constraints(), constraint_count),
(model.get_linear_constraints(name), constraint_count),
(model.get_linear_constraints(name), constraint_count),
):
self.assertIsInstance(linear_constraints, pd.Index)
self.assertLen(linear_constraints, expected_count)
def test_get_linear_constraints_empty(self):
linear_constraints = pdm.OptimizationModel(
name="test_name"
).get_linear_constraints()
self.assertIsInstance(linear_constraints, pd.Index)
self.assertEmpty(linear_constraints)
@parameterized.named_parameters(
# pylint: disable=g-complex-comprehension
{
f: tc[f]
for f in [
"testcase_name",
"name",
"bounded_exprs",
"constraint_count",
]
}
for tc in constraint_test_cases
)
def test_get_linear_constraint_references(
self,
name,
bounded_exprs,
constraint_count,
):
model = self.create_test_model(name, bounded_exprs)
for linear_constraints, expected_count in (
(model.get_linear_constraint_references(name=name), constraint_count),
(model.get_linear_constraint_references(name=name), constraint_count),
):
self.assertIsInstance(linear_constraints, pd.Series)
self.assertLen(linear_constraints, expected_count)
for i in linear_constraints.index:
self.assertEqual(repr(linear_constraints[i]), f"{name}[{i}]")
@parameterized.named_parameters(
# pylint: disable=g-complex-comprehension
{
f: tc[f]
for f in [
"testcase_name",
"name",
"bounded_exprs",
"lower_bounds",
]
}
for tc in constraint_test_cases
)
def test_get_linear_constraint_lower_bounds(
self,
name,
bounded_exprs,
lower_bounds,
):
model = self.create_test_model(name, bounded_exprs)
for linear_constraint_lower_bounds in (
model.get_linear_constraint_lower_bounds(),
model.get_linear_constraint_lower_bounds(model.get_linear_constraints()),
model.get_linear_constraint_lower_bounds(
model.get_linear_constraint_references(name)
),
):
self.assertSequenceAlmostEqual(linear_constraint_lower_bounds, lower_bounds)
@parameterized.named_parameters(
# pylint: disable=g-complex-comprehension
{
f: tc[f]
for f in [
"testcase_name",
"name",
"bounded_exprs",
"upper_bounds",
]
}
for tc in constraint_test_cases
)
def test_get_linear_constraint_upper_bounds(
self,
name,
bounded_exprs,
upper_bounds,
):
model = self.create_test_model(name, bounded_exprs)
for linear_constraint_upper_bounds in (
model.get_linear_constraint_upper_bounds(),
model.get_linear_constraint_upper_bounds(model.get_linear_constraints()),
model.get_linear_constraint_upper_bounds(
model.get_linear_constraint_references(name)
),
):
self.assertSequenceAlmostEqual(linear_constraint_upper_bounds, upper_bounds)
@parameterized.named_parameters(
# pylint: disable=g-complex-comprehension
{
f: tc[f]
for f in [
"testcase_name",
"name",
"bounded_exprs",
"expression_terms",
"expression_offsets",
]
}
for tc in constraint_test_cases
)
def test_get_linear_constraint_expressions(
self,
name,
bounded_exprs,
expression_terms,
expression_offsets,
):
model = self.create_test_model(name, bounded_exprs)
x = model.get_variable_references(name="x")
y = model.get_variable_references(name="y")
for linear_constraint_expressions in (
model.get_linear_constraint_expressions(),
model.get_linear_constraint_expressions(model.get_linear_constraints()),
model.get_linear_constraint_expressions(
model.get_linear_constraint_references(name)
),
):
expr_terms = expression_terms(x, y)
self.assertLen(linear_constraint_expressions, len(expr_terms))
for expr, expr_term in zip(linear_constraint_expressions, expr_terms):
self.assertDictEqual(expr._terms, expr_term)
self.assertSequenceAlmostEqual(
[expr._offset for expr in linear_constraint_expressions],
expression_offsets,
)
class OptimizationModelObjectiveTest(parameterized.TestCase):
_expressions = (
lambda x, y: -3,
lambda x, y: 0,
lambda x, y: 10,
lambda x, y: x[0],
lambda x, y: x[1],
lambda x, y: x[2],
lambda x, y: y[0],
lambda x, y: y[1],
lambda x, y: x[0] + 5,
lambda x, y: -3 + y[1],
lambda x, y: 3 * x[0],
lambda x, y: x[0] * 3 * 5 - 3,
lambda x, y: x.sum(),
lambda x, y: 101 + 2 * 3 * x.sum(),
lambda x, y: x.sum() * 2,
lambda x, y: sum(y),
lambda x, y: x.sum() + 2 * y.sum() + 3,
)
_variable_indices = (
pd.Index(range(3)),
pd.Index(range(3), name="i"),
pd.Index(range(10), name="i"),
)
def assertLinearExpressionAlmostEqual(
self,
expr1: pdm._LinearExpression,
expr2: pdm._LinearExpression,
) -> None:
"""Test that the two linear expressions are almost equal."""
for variable, coeff in expr1._terms.items():
self.assertAlmostEqual(expr2._terms.get(variable, 0), coeff)
for variable, coeff in expr2._terms.items():
self.assertAlmostEqual(expr1._terms.get(variable, 0), coeff)
self.assertAlmostEqual(expr1._offset, expr2._offset)
@parameterized.product(
expression=_expressions,
variable_indices=_variable_indices,
sense=(pdm.ObjectiveSense.MINIMIZE, pdm.ObjectiveSense.MAXIMIZE),
)
def test_set_objective(
self,
expression: Callable[[pd.Series, pd.Series], pdm._LinearType],
variable_indices: pd.Index,
sense: pdm.ObjectiveSense,
):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(name="x", index=variable_indices)
y = model.create_variables(name="y", index=variable_indices)
objective_expression = pdm._as_flat_linear_expression(expression(x, y))
model.set_objective(expression=objective_expression, sense=sense)
self.assertEqual(model.get_objective_sense(), sense)
got_objective_expression = model.get_objective_expression()
self.assertLinearExpressionAlmostEqual(
got_objective_expression, objective_expression
)
def test_set_new_objective(self):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(name="x", index=pd.Index(range(3)))
old_objective_expression = 1
new_objective_expression = pdm._as_flat_linear_expression(x.sum() - 2.3)
# Set and check for old objective.
model.set_objective(
expression=old_objective_expression, sense=pdm.ObjectiveSense.MAXIMIZE
)
self.assertEqual(model.get_objective_sense(), pdm.ObjectiveSense.MAXIMIZE)
got_objective_expression = model.get_objective_expression()
for var_coeff in got_objective_expression._terms.values():
self.assertAlmostEqual(var_coeff, 0)
self.assertAlmostEqual(got_objective_expression._offset, 1)
# Set to a new objective and check that it is different.
model.set_objective(
expression=new_objective_expression, sense=pdm.ObjectiveSense.MINIMIZE
)
self.assertEqual(model.get_objective_sense(), pdm.ObjectiveSense.MINIMIZE)
got_objective_expression = model.get_objective_expression()
self.assertLinearExpressionAlmostEqual(
got_objective_expression, new_objective_expression
)
@parameterized.product(
expression=_expressions,
variable_indices=_variable_indices,
)
def test_minimize(
self,
expression: Callable[[pd.Series, pd.Series], pdm._LinearType],
variable_indices: pd.Index,
):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(name="x", index=variable_indices)
y = model.create_variables(name="y", index=variable_indices)
objective_expression = pdm._as_flat_linear_expression(expression(x, y))
model.minimize(expression=objective_expression)
self.assertEqual(model.get_objective_sense(), pdm.ObjectiveSense.MINIMIZE)
got_objective_expression = model.get_objective_expression()
self.assertLinearExpressionAlmostEqual(
got_objective_expression, objective_expression
)
@parameterized.product(
expression=_expressions,
variable_indices=_variable_indices,
)
def test_maximize(
self,
expression: Callable[[pd.Series, pd.Series], float],
variable_indices: pd.Index,
):
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables(name="x", index=variable_indices)
y = model.create_variables(name="y", index=variable_indices)
objective_expression = pdm._as_flat_linear_expression(expression(x, y))
model.maximize(expression=objective_expression)
self.assertEqual(model.get_objective_sense(), pdm.ObjectiveSense.MAXIMIZE)
got_objective_expression = model.get_objective_expression()
self.assertLinearExpressionAlmostEqual(
got_objective_expression, objective_expression
)
class OptimizationModelProtoTest(absltest.TestCase):
def test_to_proto(self):
expected = linear_solver_pb2.MPModelProto()
text_format.Parse(
"""
name: "test_name"
maximize: true
objective_offset: 0
variable {
lower_bound: 0
upper_bound: 1000
objective_coefficient: 1
is_integer: false
name: "x[0]"
}
variable {
lower_bound: 0
upper_bound: 1000
objective_coefficient: 1
is_integer: false
name: "x[1]"
}
constraint {
var_index: 0
coefficient: 1
lower_bound: -inf
upper_bound: 10
name: "Ct[0]"
}
constraint {
var_index: 1
coefficient: 1
lower_bound: -inf
upper_bound: 10
name: "Ct[1]"
}
""",
expected,
)
model = pdm.OptimizationModel(name="test_name")
x = model.create_variables("x", pd.Index(range(2)), 0, 1000)
model.create_linear_constraints("Ct", x.apply(lambda expr: expr <= 10))
model.maximize(expression=x.sum())
self.assertEqual(str(expected), str(model.to_proto()))
class SolverTest(parameterized.TestCase):
_solvers = (
{
"type": pdm.SolverType.CP_SAT,
"options": pdm.SolveOptions(),
"is_integer": True, # CP-SAT supports only pure integer variables.
},
{
"type": pdm.SolverType.GLOP,
"options": pdm.SolveOptions(
# Disable GLOP's presolve to correctly trigger unboundedness:
# https://github.com/google/or-tools/issues/3319
solver_specific_parameters="use_preprocessing: False"
),
"is_integer": False, # GLOP does not properly support integers.
},
{
"type": pdm.SolverType.SCIP,
"options": pdm.SolveOptions(),
"is_integer": False,
},
{
"type": pdm.SolverType.SCIP,
"options": pdm.SolveOptions(),
"is_integer": True,
},
)
_variable_indices = (
pd.Index(range(0)), # No variables.
pd.Index(range(1)), # Single variable.
pd.Index(range(3)), # Multiple variables.
)
_variable_bounds = (-1, 0, 10.1)
_solve_statuses = (
pdm.SolveStatus.OPTIMAL,
pdm.SolveStatus.INFEASIBLE,
pdm.SolveStatus.UNBOUNDED,
)
_set_objectives = (True, False)
_objective_senses = (
pdm.ObjectiveSense.MAXIMIZE,
pdm.ObjectiveSense.MINIMIZE,
)
_objective_expressions = (
lambda x: x.sum(),
lambda x: x.sum() + 5.2,
lambda x: -10.1,
lambda x: 0,
)
def _create_model(
self,
variable_indices: pd.Index = pd.Index(range(3)),
variable_bound: float = 0,
is_integer: bool = False,
solve_status: pdm.SolveStatus = pdm.SolveStatus.OPTIMAL,
set_objective: bool = True,
objective_sense: pdm.ObjectiveSense = pdm.ObjectiveSense.MAXIMIZE,
objective_expression: Callable[[pd.Series], float] = lambda x: x.sum(),
) -> pdm.OptimizationModel:
"""Constructs an optimization problem.
It has the following formulation:
```
objective_sense (MAXIMIZE / MINIMIZE) objective_expression(x)
satisfying constraints
(if solve_status != UNBOUNDED and objective_sense == MAXIMIZE)
x[variable_indices] <= variable_bound
(if solve_status != UNBOUNDED and objective_sense == MINIMIZE)
x[variable_indices] >= variable_bound
x[variable_indices] is_integer
False (if solve_status == INFEASIBLE)
```
Args:
variable_indices (pd.Index): The indices of the variable(s).
variable_bound (float): The upper- or lower-bound(s) of the variable(s).
is_integer (bool): Whether the variables should be integer.
solve_status (pdm.SolveStatus): The solve status to target.
set_objective (bool): Whether to set the objective of the model.
objective_sense (pdm.ObjectiveSense): MAXIMIZE or MINIMIZE,
objective_expression (Callable[[pd.Series], float]): The expression to
maximize or minimize if set_objective=True.
Returns:
pdm.OptimizationModel: The resulting problem.
"""
model = pdm.OptimizationModel(name="test")
# Variable(s)
x = model.create_variables(
name="x",
index=pd.Index(variable_indices),
is_integer=is_integer,
)
# Constraint(s)
if solve_status == pdm.SolveStatus.INFEASIBLE:
# Force infeasibility here to test that we get pd.NA later.
model.create_linear_constraints("bool", False)
elif solve_status != pdm.SolveStatus.UNBOUNDED:
if objective_sense == pdm.ObjectiveSense.MAXIMIZE:
model.create_linear_constraints(
"upper_bound", x.apply(lambda xi: xi <= variable_bound)
)
elif objective_sense == pdm.ObjectiveSense.MINIMIZE:
model.create_linear_constraints(
"lower_bound", x.apply(lambda xi: xi >= variable_bound)
)
# Objective
if set_objective:
model.set_objective(
expression=objective_expression(x), sense=objective_sense
)
return model
@parameterized.product(
solver=_solvers,
variable_indices=_variable_indices,
variable_bound=_variable_bounds,
solve_status=_solve_statuses,
set_objective=_set_objectives,
objective_sense=_objective_senses,
objective_expression=_objective_expressions,
)
def test_solve_status(
self,
solver: dict[str, Union[pdm.SolverType, pdm.SolveOptions, bool]],
variable_indices: pd.Index,
variable_bound: float,
solve_status: pdm.SolveStatus,
set_objective: bool,
objective_sense: pdm.ObjectiveSense,
objective_expression: Callable[[pd.Series], float],
):
model = self._create_model(
variable_indices=variable_indices,
variable_bound=variable_bound,
is_integer=solver["is_integer"],
solve_status=solve_status,
set_objective=set_objective,
objective_sense=objective_sense,
objective_expression=objective_expression,
)
solve_result = pdm.Solver(solver_type=solver["type"]).solve(
model=model, options=solver["options"]
)
# pylint: disable=g-explicit-length-test
# (we disable explicit-length-test here because `variable_indices: pd.Index`
# evaluates to an ambiguous boolean value.)
if len(variable_indices) > 0: # Test cases with >=1 variable.
self.assertNotEmpty(variable_indices)
if (
isinstance(
objective_expression(model.get_variable_references("x")),
(int, float),
)
and solve_status != pdm.SolveStatus.INFEASIBLE
):
# Feasibility implies optimality when objective is a constant term.
self.assertEqual(solve_result.get_status(), pdm.SolveStatus.OPTIMAL)
elif not set_objective and solve_status != pdm.SolveStatus.INFEASIBLE:
# Feasibility implies optimality when objective is not set.
self.assertEqual(solve_result.get_status(), pdm.SolveStatus.OPTIMAL)
elif (
solver["type"] == pdm.SolverType.CP_SAT
and solve_result.get_status() == pdm.SolveStatus.UNKNOWN
):
# CP_SAT returns unknown for some of the infeasible and unbounded cases.
self.assertIn(
solve_status,
(pdm.SolveStatus.INFEASIBLE, pdm.SolveStatus.UNBOUNDED),
)
else:
self.assertEqual(solve_result.get_status(), solve_status)
elif solve_status == pdm.SolveStatus.UNBOUNDED:
# Unbounded problems are optimal when there are no variables.
self.assertEqual(solve_result.get_status(), pdm.SolveStatus.OPTIMAL)
else:
self.assertEqual(solve_result.get_status(), solve_status)
@parameterized.product(
solver=_solvers,
variable_indices=_variable_indices,
variable_bound=_variable_bounds,
solve_status=_solve_statuses,
set_objective=_set_objectives,
objective_sense=_objective_senses,
objective_expression=_objective_expressions,
)
def test_get_variable_values(
self,
solver: dict[str, Union[pdm.SolverType, pdm.SolveOptions, bool]],
variable_indices: pd.Index,
variable_bound: float,
solve_status: pdm.SolveStatus,
set_objective: bool,
objective_sense: pdm.ObjectiveSense,
objective_expression: Callable[[pd.Series], float],
):
model = self._create_model(
variable_indices=variable_indices,
variable_bound=variable_bound,
is_integer=solver["is_integer"],
solve_status=solve_status,
set_objective=set_objective,
objective_sense=objective_sense,
objective_expression=objective_expression,
)
solve_result = pdm.Solver(solver_type=solver["type"]).solve(
model=model, options=solver["options"]
)
for variables in (
None, # We get all variables when none is specified.
model.get_variables()[:2], # We can filter to a subset (pd.Index).
model.get_variable_references("x")[:2], # It works for pd.Series.
):
variable_values = solve_result.get_variable_values(variables)
# Test the type of `variable_values` (we always get pd.Series)
self.assertIsInstance(variable_values, pd.Series)
# Test the index of `variable_values` (match the input variables [if any])
self.assertSequenceAlmostEqual(
variable_values.index,
pdm._get_index(model._get_variables(variables)),
)
if solve_result.get_status() not in (
pdm.SolveStatus.OPTIMAL,
pdm.SolveStatus.FEASIBLE,
):
# self.assertSequenceAlmostEqual does not work here because we cannot do
# equality comparison for NA values (NAs will propagate and we will get
# 'TypeError: boolean value of NA is ambiguous')
for variable_value in variable_values:
self.assertTrue(pd.isna(variable_value))
elif set_objective and not isinstance(
objective_expression(model.get_variable_references("x")),
(int, float),
):
# The variable values are only well-defined when the objective is set
# and depends on the variable(s).
if not solver["is_integer"]:
self.assertSequenceAlmostEqual(
variable_values, [variable_bound] * len(variable_values)
)
elif objective_sense == pdm.ObjectiveSense.MAXIMIZE:
self.assertTrue(solver["is_integer"]) # Assert a known assumption.
self.assertSequenceAlmostEqual(
variable_values,
[math.floor(variable_bound)] * len(variable_values),
)
else:
self.assertTrue(solver["is_integer"]) # Assert a known assumption.
self.assertEqual(objective_sense, pdm.ObjectiveSense.MINIMIZE)
self.assertSequenceAlmostEqual(
variable_values,
[math.ceil(variable_bound)] * len(variable_values),
)
@parameterized.product(
solver=_solvers,
variable_indices=_variable_indices,
variable_bound=_variable_bounds,
solve_status=_solve_statuses,
set_objective=_set_objectives,
objective_sense=_objective_senses,
objective_expression=_objective_expressions,
)
def test_get_objective_value(
self,
solver: dict[str, Union[pdm.SolverType, pdm.SolveOptions, bool]],
variable_indices: pd.Index,
variable_bound: float,
solve_status: pdm.SolveStatus,
set_objective: bool,
objective_sense: pdm.ObjectiveSense,
objective_expression: Callable[[pd.Series], float],
):
model = self._create_model(
variable_indices=variable_indices,
variable_bound=variable_bound,
is_integer=solver["is_integer"],
solve_status=solve_status,
set_objective=set_objective,
objective_sense=objective_sense,
objective_expression=objective_expression,
)
solve_result = pdm.Solver(solver_type=solver["type"]).solve(
model=model, options=solver["options"]
)
# Test objective value
if solve_result.get_status() not in (
pdm.SolveStatus.OPTIMAL,
pdm.SolveStatus.FEASIBLE,
):
self.assertTrue(pd.isna(solve_result.get_objective_value()))
return
if set_objective:
variable_values = solve_result.get_variable_values(model.get_variables())
self.assertAlmostEqual(
solve_result.get_objective_value(),
objective_expression(variable_values),
)
else:
self.assertAlmostEqual(solve_result.get_objective_value(), 0)
if __name__ == "__main__":
absltest.main()