add typing info to model_builder/python; add numpy variable support; update samples
This commit is contained in:
@@ -13,7 +13,8 @@
|
||||
|
||||
set_property(SOURCE linear_solver.i PROPERTY CPLUSPLUS ON)
|
||||
set_property(SOURCE linear_solver.i PROPERTY SWIG_MODULE_NAME pywraplp)
|
||||
set_property(SOURCE linear_solver.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=)
|
||||
set_property(SOURCE linear_solver.i PROPERTY COMPILE_DEFINITIONS
|
||||
${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=)
|
||||
swig_add_library(pywraplp
|
||||
TYPE MODULE
|
||||
LANGUAGE python
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,43 +13,56 @@
|
||||
"""helpers methods for the cp_model_builder module."""
|
||||
|
||||
import numbers
|
||||
from typing import Any, Sequence, Union
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
# Custom types.
|
||||
NumberT = Union[numbers.Number, np.number]
|
||||
|
||||
|
||||
def is_integral(x):
|
||||
def is_integral(x: Any) -> bool:
|
||||
"""Checks if x has either a number.Integral or a np.integer type."""
|
||||
return isinstance(x, numbers.Integral) or isinstance(x, np.integer)
|
||||
|
||||
|
||||
def is_a_number(x):
|
||||
def is_a_number(x: Any) -> bool:
|
||||
"""Checks if x has either a number.Number or a np.double type."""
|
||||
return isinstance(x, numbers.Number) or isinstance(
|
||||
x, np.double) or isinstance(x, np.integer)
|
||||
|
||||
|
||||
def is_zero(x):
|
||||
def is_zero(x: Any) -> bool:
|
||||
"""Checks if the x is 0 or 0.0."""
|
||||
return (is_integral(x) and int(x) == 0) or (is_a_number(x) and
|
||||
float(x) == 0.0)
|
||||
|
||||
|
||||
def is_one(x):
|
||||
def is_one(x: Any) -> bool:
|
||||
"""Checks if x is 1 or 1.0."""
|
||||
return (is_integral(x) and int(x) == 1) or (is_a_number(x) and
|
||||
float(x) == 1.0)
|
||||
|
||||
|
||||
def is_minus_one(x):
|
||||
def is_minus_one(x: Any) -> bool:
|
||||
"""Checks if x is -1 or -1.0."""
|
||||
return (is_integral(x) and int(x) == -1) or (is_a_number(x) and
|
||||
float(x) == -1.0)
|
||||
|
||||
|
||||
def assert_is_a_number(x):
|
||||
"""Asserts that x is a number and returns it."""
|
||||
def assert_is_a_number(x: NumberT) -> np.double:
|
||||
"""Asserts that x is a number and converts to a np.double."""
|
||||
if not is_a_number(x):
|
||||
raise TypeError('Not a number: %s' % x)
|
||||
elif is_integral(x):
|
||||
return int(x)
|
||||
else:
|
||||
return float(x)
|
||||
return np.double(x)
|
||||
|
||||
|
||||
def assert_is_a_number_array(x: Sequence[NumberT]) -> npt.NDArray[np.double]:
|
||||
"""Asserts x is a list of numbers and converts it to np.array(np.double)."""
|
||||
result = np.empty(len(x), dtype=np.double)
|
||||
pos = 0
|
||||
for c in x:
|
||||
result[pos] = assert_is_a_number(c)
|
||||
pos += 1
|
||||
assert pos == len(x)
|
||||
return result
|
||||
|
||||
@@ -168,7 +168,7 @@ class PywrapModelBuilderHelperTest(unittest.TestCase):
|
||||
self.assertEqual([0], model.constraint_var_indices(1))
|
||||
self.assertEqual([2.0], model.constraint_coefficients(1))
|
||||
|
||||
var_array = model.add_var_ndarray([10], 1.0, 5.0, True, 'var_')
|
||||
var_array = model.add_var_array([10], 1.0, 5.0, True, 'var_')
|
||||
self.assertEqual(1, var_array.ndim)
|
||||
self.assertEqual(10, var_array.size)
|
||||
self.assertEqual((10,), var_array.shape)
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
# 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.
|
||||
"""Tests for model_builder."""
|
||||
"""Tests for ModelBuilder."""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
import numpy.testing as np_testing
|
||||
import os
|
||||
|
||||
from ortools.linear_solver.python import model_builder
|
||||
from ortools.linear_solver.python import model_builder as mb
|
||||
import unittest
|
||||
|
||||
|
||||
@@ -29,7 +30,7 @@ class ModelBuilderTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-statements
|
||||
def run_minimal_linear_example(self, solver_name):
|
||||
"""Minimal Linear Example."""
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
model.name = 'minimal_linear_example'
|
||||
x1 = model.new_num_var(0.0, math.inf, 'x1')
|
||||
x2 = model.new_num_var(0.0, math.inf, 'x2')
|
||||
@@ -54,13 +55,18 @@ class ModelBuilderTest(unittest.TestCase):
|
||||
c2 = model.add(2.0 * x1 + 2.0 * x2 + 6.0 * x3 <= 300.0)
|
||||
self.assertEqual(-math.inf, c2.lower_bound)
|
||||
|
||||
solver = model_builder.ModelSolver(solver_name)
|
||||
self.assertEqual(model_builder.SolveStatus.OPTIMAL, solver.solve(model))
|
||||
solver = mb.ModelSolver(solver_name)
|
||||
self.assertEqual(mb.SolveStatus.OPTIMAL, solver.solve(model))
|
||||
|
||||
# The problem has an optimal solution.
|
||||
self.assertAlmostEqual(733.333333 + model.objective_offset,
|
||||
solver.objective_value,
|
||||
places=self.NUM_PLACES)
|
||||
self.assertAlmostEqual(
|
||||
solver.value(10.0 * x1 + 6 * x2 + 4.0 * x3 - 5.5),
|
||||
solver.objective_value,
|
||||
places=self.NUM_PLACES,
|
||||
)
|
||||
self.assertAlmostEqual(33.333333,
|
||||
solver.value(x1),
|
||||
places=self.NUM_PLACES)
|
||||
@@ -120,14 +126,14 @@ BOUNDS
|
||||
UP BOUND X_ONE 4
|
||||
ENDATA
|
||||
"""
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
self.assertTrue(model.import_from_mps_string(mps_data))
|
||||
self.assertEqual(model.name, 'SupportedMaximizationProblem')
|
||||
|
||||
def test_import_from_mps_file(self):
|
||||
path = os.path.dirname(__file__)
|
||||
mps_path = f'{path}/../testdata/maximization.mps'
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
self.assertTrue(model.import_from_mps_file(mps_path))
|
||||
self.assertEqual(model.name, 'SupportedMaximizationProblem')
|
||||
|
||||
@@ -140,7 +146,7 @@ ENDATA
|
||||
4 y + b2 - 3 b3 <= 2;
|
||||
constraint_num2: -4 b1 + b2 - 3 z <= -2;
|
||||
"""
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
self.assertTrue(model.import_from_lp_string(lp_data))
|
||||
self.assertEqual(6, model.num_variables)
|
||||
self.assertEqual(3, model.num_constraints)
|
||||
@@ -151,7 +157,7 @@ ENDATA
|
||||
def test_import_from_lp_file(self):
|
||||
path = os.path.dirname(__file__)
|
||||
lp_path = f'{path}/../testdata/small_model.lp'
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
self.assertTrue(model.import_from_lp_file(lp_path))
|
||||
self.assertEqual(6, model.num_variables)
|
||||
self.assertEqual(3, model.num_constraints)
|
||||
@@ -159,8 +165,77 @@ ENDATA
|
||||
self.assertEqual(42, model.var_from_index(0).upper_bound)
|
||||
self.assertEqual('x', model.var_from_index(0).name)
|
||||
|
||||
def test_class_api(self):
|
||||
model = mb.ModelBuilder()
|
||||
x = model.new_int_var(0, 10, 'x')
|
||||
y = model.new_int_var(1, 10, 'y')
|
||||
z = model.new_int_var(2, 10, 'z')
|
||||
t = model.new_int_var(3, 10, 't')
|
||||
|
||||
e1 = mb.LinearExpr.sum([x, y, z])
|
||||
expected_vars = np.array([0, 1, 2], dtype=np.int32)
|
||||
np_testing.assert_array_equal(expected_vars, e1.variable_indices)
|
||||
np_testing.assert_array_equal(np.array([1, 1, 1], dtype=np.double),
|
||||
e1.coefficients)
|
||||
self.assertEqual(e1.constant, 0.0)
|
||||
self.assertEqual(e1.pretty_string(model.helper), 'x + y + z')
|
||||
|
||||
e2 = mb.LinearExpr.sum([e1, 4.0])
|
||||
np_testing.assert_array_equal(expected_vars, e2.variable_indices)
|
||||
np_testing.assert_array_equal(np.array([1, 1, 1], dtype=np.double),
|
||||
e2.coefficients)
|
||||
self.assertEqual(e2.constant, 4.0)
|
||||
self.assertEqual(e2.pretty_string(model.helper), 'x + y + z + 4.0')
|
||||
|
||||
e3 = mb.LinearExpr.term(e2, 2)
|
||||
np_testing.assert_array_equal(expected_vars, e3.variable_indices)
|
||||
np_testing.assert_array_equal(np.array([2, 2, 2], dtype=np.double),
|
||||
e3.coefficients)
|
||||
self.assertEqual(e3.constant, 8.0)
|
||||
self.assertEqual(e3.pretty_string(model.helper),
|
||||
'2.0 * x + 2.0 * y + 2.0 * z + 8.0')
|
||||
|
||||
e4 = mb.LinearExpr.weighted_sum([x, t], [-1, 1], constant=2)
|
||||
np_testing.assert_array_equal(np.array([0, 3], dtype=np.int32),
|
||||
e4.variable_indices)
|
||||
np_testing.assert_array_equal(np.array([-1, 1], dtype=np.double),
|
||||
e4.coefficients)
|
||||
self.assertEqual(e4.constant, 2.0)
|
||||
self.assertEqual(e4.pretty_string(model.helper), '-x + t + 2.0')
|
||||
|
||||
e4b = e4 * 3.0
|
||||
np_testing.assert_array_equal(np.array([0, 3], dtype=np.int32),
|
||||
e4b.variable_indices)
|
||||
np_testing.assert_array_equal(np.array([-3, 3], dtype=np.double),
|
||||
e4b.coefficients)
|
||||
self.assertEqual(e4b.constant, 6.0)
|
||||
self.assertEqual(e4b.pretty_string(model.helper),
|
||||
'-3.0 * x + 3.0 * t + 6.0')
|
||||
|
||||
e5 = mb.LinearExpr.sum([e1, -3, e4])
|
||||
np_testing.assert_array_equal(np.array([0, 1, 2, 0, 3], dtype=np.int32),
|
||||
e5.variable_indices)
|
||||
np_testing.assert_array_equal(
|
||||
np.array([1, 1, 1, -1, 1], dtype=np.double), e5.coefficients)
|
||||
self.assertEqual(e5.constant, -1.0)
|
||||
self.assertEqual(e5.pretty_string(model.helper),
|
||||
'x + y + z - x + t - 1.0')
|
||||
|
||||
e6 = mb.LinearExpr.term(x, 2.0, constant=1.0)
|
||||
np_testing.assert_array_equal(np.array([0], dtype=np.int32),
|
||||
e6.variable_indices)
|
||||
np_testing.assert_array_equal(np.array([2], dtype=np.double),
|
||||
e6.coefficients)
|
||||
self.assertEqual(e6.constant, 1.0)
|
||||
|
||||
e7 = mb.LinearExpr.term(x, 1.0, constant=0.0)
|
||||
self.assertEqual(x, e7)
|
||||
|
||||
e8 = mb.LinearExpr.term(2, 3, constant=4)
|
||||
self.assertEqual(e8, 10)
|
||||
|
||||
def test_variables(self):
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
x = model.new_int_var(0.0, 4.0, 'x')
|
||||
self.assertEqual(0, x.index)
|
||||
self.assertEqual(0.0, x.lower_bound)
|
||||
@@ -180,32 +255,59 @@ ENDATA
|
||||
self.assertNotEqual(x, y)
|
||||
|
||||
# array
|
||||
xs = model.new_int_var_ndarray(10, 0.0, 5.0, 'xs_')
|
||||
xs = model.new_int_var_array(shape=10,
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=5.0,
|
||||
name='xs_')
|
||||
self.assertEqual(10, xs.size)
|
||||
self.assertEqual('xs_4', str(xs[4]))
|
||||
lbs = np.array([1.0, 2.0, 3.0])
|
||||
ubs = np.array([3.0, 4.0, 5.0])
|
||||
ys = model.new_int_var_ndarray_with_bounds(lbs, ubs, 'ys_')
|
||||
ubs = [3.0, 4.0, 5.0]
|
||||
ys = model.new_int_var_array(lower_bounds=lbs,
|
||||
upper_bounds=ubs,
|
||||
name='ys_')
|
||||
self.assertEqual('VariableContainer([12 13 14])', str(ys))
|
||||
zs = model.new_int_var_ndarray_with_bounds([1.0, 2.0, 3], [4, 4, 4],
|
||||
'zs_')
|
||||
zs = model.new_int_var_array(lower_bounds=[1.0, 2.0, 3],
|
||||
upper_bounds=[4, 4, 4],
|
||||
name='zs_')
|
||||
self.assertEqual(3, zs.size)
|
||||
self.assertEqual((3,), zs.shape)
|
||||
self.assertEqual('zs_1', str(zs[1]))
|
||||
self.assertEqual('zs_2(index=17, lb=3.0, ub=4.0, integer)', repr(zs[2]))
|
||||
self.assertTrue(zs[2].is_integral)
|
||||
|
||||
bs = model.new_bool_var_ndarray([4, 5], 'bs_')
|
||||
bs = model.new_bool_var_array([4, 5], 'bs_')
|
||||
self.assertEqual((4, 5), bs.shape)
|
||||
self.assertEqual((5, 4), bs.T.shape)
|
||||
self.assertEqual(31, bs.index_at((2, 3)))
|
||||
self.assertEqual(20, bs.size)
|
||||
self.assertEqual((20,), bs.flatten.shape)
|
||||
self.assertEqual((20,), bs.ravel.shape)
|
||||
self.assertTrue(bs[1, 1].is_integral)
|
||||
|
||||
# Slices are [lb, ub) closed - open.
|
||||
self.assertEqual(5, bs[3, :].size)
|
||||
self.assertEqual(6, bs[1:3, 2:5].size)
|
||||
|
||||
sum_bs = np.sum(bs)
|
||||
self.assertEqual(20, sum_bs.variable_indices.size)
|
||||
np_testing.assert_array_equal(sum_bs.variable_indices,
|
||||
bs.variable_indices.flatten())
|
||||
np_testing.assert_array_equal(sum_bs.coefficients, np.ones(20))
|
||||
times_bs = np.multiply(bs, 4)
|
||||
np_testing.assert_array_equal(times_bs.variable_indices,
|
||||
bs.variable_indices.flatten())
|
||||
np_testing.assert_array_equal(times_bs.coefficients, np.full(20, 4.0))
|
||||
times_bs_rev = np.multiply(4, bs)
|
||||
np_testing.assert_array_equal(times_bs_rev.variable_indices,
|
||||
bs.variable_indices.flatten())
|
||||
np_testing.assert_array_equal(times_bs_rev.coefficients,
|
||||
np.full(20, 4.0))
|
||||
dot_bs = np.dot(bs[2], np.array([1, 2, 3, 4, 5], dtype=np.double))
|
||||
np_testing.assert_array_equal(dot_bs.variable_indices,
|
||||
bs[2].variable_indices)
|
||||
np_testing.assert_array_equal(dot_bs.coefficients, [1, 2, 3, 4, 5])
|
||||
|
||||
# Tests the hash method.
|
||||
var_set = set()
|
||||
var_set.add(x)
|
||||
@@ -213,15 +315,187 @@ ENDATA
|
||||
self.assertIn(x_copy, var_set)
|
||||
self.assertNotIn(y, var_set)
|
||||
|
||||
def test_numpy_var_arrays(self):
|
||||
model = mb.ModelBuilder()
|
||||
|
||||
x = model.new_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=4.0,
|
||||
shape=[2, 3],
|
||||
is_integral=False,
|
||||
)
|
||||
np_testing.assert_array_equal(x.shape, [2, 3])
|
||||
|
||||
y = model.new_var_array(
|
||||
lower_bounds=[[0.0, 1.0, 2.0], [0.0, 0.0, 2.0]],
|
||||
upper_bounds=4.0,
|
||||
is_integral=False,
|
||||
name='y',
|
||||
)
|
||||
np_testing.assert_array_equal(y.shape, [2, 3])
|
||||
|
||||
z = model.new_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=[[2.0, 1.0, 2.0], [3.0, 4.0, 2.0]],
|
||||
is_integral=False,
|
||||
name='z',
|
||||
)
|
||||
np_testing.assert_array_equal(z.shape, [2, 3])
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=4.0,
|
||||
is_integral=False,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_var_array(
|
||||
lower_bounds=[0, 0],
|
||||
upper_bounds=[1, 2, 3],
|
||||
is_integral=False,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_var_array(
|
||||
shape=[2, 3],
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=[1, 2, 3],
|
||||
is_integral=False,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_var_array(
|
||||
shape=[2, 3],
|
||||
lower_bounds=[1, 2],
|
||||
upper_bounds=4.0,
|
||||
is_integral=False,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_var_array(
|
||||
shape=[2, 3],
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=4.0,
|
||||
is_integral=[False, True],
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_var_array(
|
||||
lower_bounds=[1, 2],
|
||||
upper_bounds=4.0,
|
||||
is_integral=[False, False, False],
|
||||
)
|
||||
|
||||
def test_numpy_num_var_arrays(self):
|
||||
model = mb.ModelBuilder()
|
||||
|
||||
x = model.new_num_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=4.0,
|
||||
shape=[2, 3],
|
||||
)
|
||||
np_testing.assert_array_equal(x.shape, [2, 3])
|
||||
|
||||
y = model.new_num_var_array(
|
||||
lower_bounds=[[0.0, 1.0, 2.0], [0.0, 0.0, 2.0]],
|
||||
upper_bounds=4.0,
|
||||
name='y',
|
||||
)
|
||||
np_testing.assert_array_equal(y.shape, [2, 3])
|
||||
|
||||
z = model.new_num_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=[[2.0, 1.0, 2.0], [3.0, 4.0, 2.0]],
|
||||
name='z',
|
||||
)
|
||||
np_testing.assert_array_equal(z.shape, [2, 3])
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_num_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=4.0,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_num_var_array(
|
||||
lower_bounds=[0, 0],
|
||||
upper_bounds=[1, 2, 3],
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_num_var_array(
|
||||
shape=[2, 3],
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=[1, 2, 3],
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_num_var_array(
|
||||
shape=[2, 3],
|
||||
lower_bounds=[1, 2],
|
||||
upper_bounds=4.0,
|
||||
)
|
||||
|
||||
def test_numpy_int_var_arrays(self):
|
||||
model = mb.ModelBuilder()
|
||||
|
||||
x = model.new_int_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=4.0,
|
||||
shape=[2, 3],
|
||||
)
|
||||
np_testing.assert_array_equal(x.shape, [2, 3])
|
||||
|
||||
y = model.new_int_var_array(
|
||||
lower_bounds=[[0.0, 1.0, 2.0], [0.0, 0.0, 2.0]],
|
||||
upper_bounds=4.0,
|
||||
name='y',
|
||||
)
|
||||
np_testing.assert_array_equal(y.shape, [2, 3])
|
||||
|
||||
z = model.new_int_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=[[2.0, 1.0, 2.0], [3.0, 4.0, 2.0]],
|
||||
name='z',
|
||||
)
|
||||
np_testing.assert_array_equal(z.shape, [2, 3])
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_int_var_array(
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=4.0,
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_int_var_array(
|
||||
lower_bounds=[0, 0],
|
||||
upper_bounds=[1, 2, 3],
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_int_var_array(
|
||||
shape=[2, 3],
|
||||
lower_bounds=0.0,
|
||||
upper_bounds=[1, 2, 3],
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
x = model.new_int_var_array(
|
||||
shape=[2, 3],
|
||||
lower_bounds=[1, 2],
|
||||
upper_bounds=4.0,
|
||||
)
|
||||
|
||||
def test_duplicate_variables(self):
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
x = model.new_int_var(0.0, 4.0, 'x')
|
||||
y = model.new_int_var(0.0, 4.0, 'y')
|
||||
z = model.new_int_var(0.0, 4.0, 'z')
|
||||
model.add(x + 2 * y == x - z)
|
||||
model.minimize(x + y + z)
|
||||
solver = model_builder.ModelSolver('scip')
|
||||
self.assertEqual(model_builder.SolveStatus.OPTIMAL, solver.solve(model))
|
||||
solver = mb.ModelSolver('scip')
|
||||
self.assertEqual(mb.SolveStatus.OPTIMAL, solver.solve(model))
|
||||
|
||||
def test_issue_3614(self):
|
||||
total_number_of_choices = 5 + 1
|
||||
@@ -235,7 +509,7 @@ ENDATA
|
||||
|
||||
bundle_start_idx = len(standalone_features)
|
||||
# Model
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
y = {}
|
||||
v = {}
|
||||
for i in range(total_number_of_choices):
|
||||
@@ -248,12 +522,12 @@ ENDATA
|
||||
(feature_bundle_incidence_matrix[(i, 0)] *
|
||||
y[bundle_start_idx])))
|
||||
|
||||
solver = model_builder.ModelSolver('scip')
|
||||
solver = mb.ModelSolver('scip')
|
||||
status = solver.solve(model)
|
||||
self.assertEqual(model_builder.SolveStatus.OPTIMAL, status)
|
||||
self.assertEqual(mb.SolveStatus.OPTIMAL, status)
|
||||
|
||||
def test_varcompvar(self):
|
||||
model = model_builder.ModelBuilder()
|
||||
model = mb.ModelBuilder()
|
||||
x = model.new_int_var(0.0, 4.0, 'x')
|
||||
y = model.new_int_var(0.0, 4.0, 'y')
|
||||
ct = x == y
|
||||
|
||||
@@ -13,13 +13,19 @@
|
||||
|
||||
// A pybind11 wrapper for model_builder_helper.
|
||||
|
||||
#include <algorithm>
|
||||
#include <complex>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Eigen/Core"
|
||||
#include "Eigen/SparseCore"
|
||||
#include "absl/log/check.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/linear_solver/linear_solver.pb.h"
|
||||
@@ -106,6 +112,38 @@ void BuildModelFromSparseData(
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, double>> SortedGroupedTerms(
|
||||
const std::vector<int>& indices, const std::vector<double>& coefficients) {
|
||||
CHECK_EQ(indices.size(), coefficients.size());
|
||||
std::vector<std::pair<int, double>> terms;
|
||||
terms.reserve(indices.size());
|
||||
for (int i = 0; i < indices.size(); ++i) {
|
||||
terms.emplace_back(indices[i], coefficients[i]);
|
||||
}
|
||||
std::sort(
|
||||
terms.begin(), terms.end(),
|
||||
[](const std::pair<int, double>& a, const std::pair<int, double>& b) {
|
||||
if (a.first != b.first) return a.first < b.first;
|
||||
if (std::abs(a.second) != std::abs(b.second)) {
|
||||
return std::abs(a.second) < std::abs(b.second);
|
||||
}
|
||||
return a.second < b.second;
|
||||
});
|
||||
int pos = 0;
|
||||
for (int i = 0; i < terms.size(); ++i) {
|
||||
if (i == 0 || terms[i].first != terms[i - 1].first) {
|
||||
if (i != pos) {
|
||||
terms[pos] = terms[i];
|
||||
}
|
||||
pos++;
|
||||
} else {
|
||||
terms[pos].second += terms[i].second;
|
||||
}
|
||||
}
|
||||
terms.resize(pos);
|
||||
return terms;
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(pywrap_model_builder_helper, m) {
|
||||
py::class_<MPModelExportOptions>(m, "MPModelExportOptions")
|
||||
.def(py::init<>())
|
||||
@@ -151,7 +189,7 @@ PYBIND11_MODULE(pywrap_model_builder_helper, m) {
|
||||
arg("objective_coefficients"), arg("constraint_lower_bounds"),
|
||||
arg("constraint_upper_bounds"), arg("constraint_matrix"))
|
||||
.def("add_var", &ModelBuilderHelper::AddVar)
|
||||
.def("add_var_ndarray",
|
||||
.def("add_var_array",
|
||||
[](ModelBuilderHelper* helper, std::vector<size_t> shape, double lb,
|
||||
double ub, bool is_integral, absl::string_view name_prefix) {
|
||||
int size = shape[0];
|
||||
@@ -174,17 +212,13 @@ PYBIND11_MODULE(pywrap_model_builder_helper, m) {
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.def("add_var_ndarray_with_bounds",
|
||||
.def("add_var_array_with_bounds",
|
||||
[](ModelBuilderHelper* helper, py::array_t<double> lbs,
|
||||
py::array_t<double> ubs, py::array_t<bool> are_integral,
|
||||
absl::string_view name_prefix) {
|
||||
py::buffer_info buf_lbs = lbs.request();
|
||||
py::buffer_info buf_ubs = ubs.request();
|
||||
py::buffer_info buf_are_integral = are_integral.request();
|
||||
if (buf_lbs.ndim != 1 || buf_ubs.ndim != 1 ||
|
||||
buf_are_integral.ndim != 1) {
|
||||
throw std::runtime_error("Number of dimensions must be one");
|
||||
}
|
||||
const int size = buf_lbs.size;
|
||||
if (size != buf_ubs.size || size != buf_are_integral.size) {
|
||||
throw std::runtime_error("Input sizes must match");
|
||||
@@ -222,6 +256,14 @@ PYBIND11_MODULE(pywrap_model_builder_helper, m) {
|
||||
.def("set_var_objective_coefficient",
|
||||
&ModelBuilderHelper::SetVarObjectiveCoefficient, arg("var_index"),
|
||||
arg("coeff"))
|
||||
.def("set_objective_coefficients",
|
||||
[](ModelBuilderHelper* helper, const std::vector<int>& indices,
|
||||
const std::vector<double>& coefficients) {
|
||||
for (const auto& [i, c] :
|
||||
SortedGroupedTerms(indices, coefficients)) {
|
||||
helper->SetVarObjectiveCoefficient(i, c);
|
||||
}
|
||||
})
|
||||
.def("set_var_name", &ModelBuilderHelper::SetVarName, arg("var_index"),
|
||||
arg("name"))
|
||||
.def("add_linear_constraint", &ModelBuilderHelper::AddLinearConstraint)
|
||||
@@ -233,6 +275,15 @@ PYBIND11_MODULE(pywrap_model_builder_helper, m) {
|
||||
arg("ub"))
|
||||
.def("add_term_to_constraint", &ModelBuilderHelper::AddConstraintTerm,
|
||||
arg("ct_index"), arg("var_index"), arg("coeff"))
|
||||
.def("add_terms_to_constraint",
|
||||
[](ModelBuilderHelper* helper, int ct_index,
|
||||
const std::vector<int>& indices,
|
||||
const std::vector<double>& coefficients) {
|
||||
for (const auto& [i, c] :
|
||||
SortedGroupedTerms(indices, coefficients)) {
|
||||
helper->AddConstraintTerm(ct_index, i, c);
|
||||
}
|
||||
})
|
||||
.def("set_constraint_name", &ModelBuilderHelper::SetConstraintName,
|
||||
arg("ct_index"), arg("name"))
|
||||
.def("num_variables", &ModelBuilderHelper::num_variables)
|
||||
@@ -339,7 +390,10 @@ PYBIND11_MODULE(pywrap_model_builder_helper, m) {
|
||||
.def("dual_value", &ModelSolverHelper::dual_value, arg("ct_index"))
|
||||
.def("variable_values",
|
||||
[](const ModelSolverHelper& helper) {
|
||||
if (!helper.has_response()) return Eigen::VectorXd();
|
||||
if (!helper.has_response()) {
|
||||
throw std::logic_error(
|
||||
"Accessing a solution value when none has been found.");
|
||||
}
|
||||
const MPSolutionResponse& response = helper.response();
|
||||
Eigen::VectorXd vec(response.variable_value_size());
|
||||
for (int i = 0; i < response.variable_value_size(); ++i) {
|
||||
@@ -347,9 +401,26 @@ PYBIND11_MODULE(pywrap_model_builder_helper, m) {
|
||||
}
|
||||
return vec;
|
||||
})
|
||||
.def("expression_value",
|
||||
[](const ModelSolverHelper& helper, const std::vector<int>& indices,
|
||||
const std::vector<double>& coefficients, double constant) {
|
||||
if (!helper.has_response()) {
|
||||
throw std::logic_error(
|
||||
"Accessing a solution value when none has been found.");
|
||||
}
|
||||
const MPSolutionResponse& response = helper.response();
|
||||
for (int i = 0; i < indices.size(); ++i) {
|
||||
constant +=
|
||||
response.variable_value(indices[i]) * coefficients[i];
|
||||
}
|
||||
return constant;
|
||||
})
|
||||
.def("reduced_costs",
|
||||
[](const ModelSolverHelper& helper) {
|
||||
if (!helper.has_response()) return Eigen::VectorXd();
|
||||
if (!helper.has_response()) {
|
||||
throw std::logic_error(
|
||||
"Accessing a solution value when none has been found.");
|
||||
}
|
||||
const MPSolutionResponse& response = helper.response();
|
||||
Eigen::VectorXd vec(response.reduced_cost_size());
|
||||
for (int i = 0; i < response.reduced_cost_size(); ++i) {
|
||||
@@ -358,7 +429,10 @@ PYBIND11_MODULE(pywrap_model_builder_helper, m) {
|
||||
return vec;
|
||||
})
|
||||
.def("dual_values", [](const ModelSolverHelper& helper) {
|
||||
if (!helper.has_response()) return Eigen::VectorXd();
|
||||
if (!helper.has_response()) {
|
||||
throw std::logic_error(
|
||||
"Accessing a solution value when none has been found.");
|
||||
}
|
||||
const MPSolutionResponse& response = helper.response();
|
||||
Eigen::VectorXd vec(response.dual_value_size());
|
||||
for (int i = 0; i < response.dual_value_size(); ++i) {
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"""MIP example that solves an assignment problem."""
|
||||
# [START program]
|
||||
# [START import]
|
||||
import numpy as np
|
||||
|
||||
from ortools.linear_solver.python import model_builder
|
||||
# [END import]
|
||||
|
||||
@@ -41,30 +43,23 @@ def main():
|
||||
# [START variables]
|
||||
# x[i, j] is an array of 0-1 variables, which will be 1
|
||||
# if worker i is assigned to task j.
|
||||
x = {}
|
||||
for i in range(num_workers):
|
||||
for j in range(num_tasks):
|
||||
x[i, j] = model.new_bool_var(f'x_{i}_{j}')
|
||||
x = model.new_bool_var_array(shape=[num_workers, num_tasks], name='x')
|
||||
# [END variables]
|
||||
|
||||
# Constraints
|
||||
# [START constraints]
|
||||
# Each worker is assigned to at most 1 task.
|
||||
for i in range(num_workers):
|
||||
model.add(sum(x[i, j] for j in range(num_tasks)) <= 1)
|
||||
model.add(np.sum(x[i, :]) <= 1)
|
||||
|
||||
# Each task is assigned to exactly one worker.
|
||||
for j in range(num_tasks):
|
||||
model.add(sum(x[i, j] for i in range(num_workers)) == 1)
|
||||
model.add(np.sum(x[:, j]) == 1)
|
||||
# [END constraints]
|
||||
|
||||
# Objective
|
||||
# [START objective]
|
||||
objective_expr = 0
|
||||
for i in range(num_workers):
|
||||
for j in range(num_tasks):
|
||||
objective_expr += costs[i][j] * x[i, j]
|
||||
model.minimize(objective_expr)
|
||||
model.minimize(np.multiply(x, costs))
|
||||
# [END objective]
|
||||
|
||||
# [START solve]
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"""Solve a simple bin packing problem using a MIP solver."""
|
||||
# [START program]
|
||||
# [START import]
|
||||
import numpy as np
|
||||
|
||||
from ortools.linear_solver.python import model_builder
|
||||
# [END import]
|
||||
|
||||
@@ -36,6 +38,8 @@ def create_data_model():
|
||||
def main():
|
||||
# [START data]
|
||||
data = create_data_model()
|
||||
num_items = len(data['items'])
|
||||
num_bins = len(data['bins'])
|
||||
# [END data]
|
||||
# [END program_part1]
|
||||
|
||||
@@ -48,33 +52,27 @@ def main():
|
||||
# [START variables]
|
||||
# Variables
|
||||
# x[i, j] = 1 if item i is packed in bin j.
|
||||
x = {}
|
||||
for i in data['items']:
|
||||
for j in data['bins']:
|
||||
x[(i, j)] = model.new_bool_var(f'x_{i}_{j}')
|
||||
x = model.new_bool_var_array(shape=[num_items, num_bins], name='x')
|
||||
|
||||
# y[j] = 1 if bin j is used.
|
||||
y = {}
|
||||
for j in data['bins']:
|
||||
y[j] = model.new_bool_var(f'y_{j}')
|
||||
y = model.new_bool_var_array(shape=[num_bins], name='y')
|
||||
# [END variables]
|
||||
|
||||
# [START constraints]
|
||||
# Constraints
|
||||
# Each item must be in exactly one bin.
|
||||
for i in data['items']:
|
||||
model.add(sum(x[i, j] for j in data['bins']) == 1)
|
||||
model.add(np.sum(x[i, :]) == 1)
|
||||
|
||||
# The amount packed in each bin cannot exceed its capacity.
|
||||
for j in data['bins']:
|
||||
model.add(
|
||||
sum(x[(i, j)] * data['weights'][i] for i in data['items']) <= y[j] *
|
||||
data['bin_capacity'])
|
||||
np.dot(x[:, j], data['weights']) <= y[j] * data['bin_capacity'])
|
||||
# [END constraints]
|
||||
|
||||
# [START objective]
|
||||
# Objective: minimize the number of bins used.
|
||||
model.minimize(model_builder.LinearExpr.sum([y[j] for j in data['bins']]))
|
||||
model.minimize(np.sum(y))
|
||||
# [END objective]
|
||||
|
||||
# [START solve]
|
||||
|
||||
Reference in New Issue
Block a user