#!/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. """Tests for model_builder.""" import math import numpy as np import os from ortools.linear_solver.python import model_builder import unittest class ModelBuilderTest(unittest.TestCase): # Number of decimal places to use for numerical tolerance for # checking primal, dual, objective values and other values. NUM_PLACES = 5 # pylint: disable=too-many-statements def run_minimal_linear_example(self, solver_name): """Minimal Linear Example.""" model = model_builder.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') x3 = model.new_num_var(0.0, math.inf, 'x3') self.assertEqual(3, model.num_variables) self.assertFalse(x1.is_integral) self.assertEqual(0.0, x1.lower_bound) self.assertEqual(math.inf, x2.upper_bound) x1.lower_bound = 1.0 self.assertEqual(1.0, x1.lower_bound) model.maximize(10.0 * x1 + 6 * x2 + 4.0 * x3 - 3.5) self.assertEqual(4.0, x3.objective_coefficient) self.assertEqual(-3.5, model.objective_offset) model.objective_offset = -5.5 self.assertEqual(-5.5, model.objective_offset) c0 = model.add(x1 + x2 + x3 <= 100.0) self.assertEqual(100, c0.upper_bound) c1 = model.add(10 * x1 + 4.0 * x2 + 5.0 * x3 <= 600.0, 'c1') self.assertEqual('c1', c1.name) 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)) # The problem has an optimal solution. self.assertAlmostEqual(733.333333 + model.objective_offset, solver.objective_value, places=self.NUM_PLACES) self.assertAlmostEqual(33.333333, solver.value(x1), places=self.NUM_PLACES) self.assertAlmostEqual(66.666667, solver.value(x2), places=self.NUM_PLACES) self.assertAlmostEqual(0.0, solver.value(x3), places=self.NUM_PLACES) dual_objective_value = (solver.dual_value(c0) * c0.upper_bound + solver.dual_value(c1) * c1.upper_bound + solver.dual_value(c2) * c2.upper_bound + model.objective_offset) self.assertAlmostEqual(solver.objective_value, dual_objective_value, places=self.NUM_PLACES) # x1 and x2 are basic self.assertAlmostEqual(0.0, solver.reduced_cost(x1), places=self.NUM_PLACES) self.assertAlmostEqual(0.0, solver.reduced_cost(x2), places=self.NUM_PLACES) # x3 is non-basic x3_expected_reduced_cost = (4.0 - 1.0 * solver.dual_value(c0) - 5.0 * solver.dual_value(c1)) self.assertAlmostEqual(x3_expected_reduced_cost, solver.reduced_cost(x3), places=self.NUM_PLACES) self.assertIn('minimal_linear_example', model.export_to_lp_string(False)) self.assertIn('minimal_linear_example', model.export_to_mps_string(False)) def test_minimal_linear_example(self): self.run_minimal_linear_example('glop') def test_import_from_mps_string(self): mps_data = """ * Generated by MPModelProtoExporter * Name : SupportedMaximizationProblem * Format : Free * Constraints : 0 * Variables : 1 * Binary : 0 * Integer : 0 * Continuous : 1 NAME SupportedMaximizationProblem OBJSENSE MAX ROWS N COST COLUMNS X_ONE COST 1 BOUNDS UP BOUND X_ONE 4 ENDATA """ model = model_builder.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() self.assertTrue(model.import_from_mps_file(mps_path)) self.assertEqual(model.name, 'SupportedMaximizationProblem') def test_import_from_lp_string(self): lp_data = """ min: x + y; bin: b1, b2, b3; 1 <= x <= 42; constraint_num1: 5 b1 + 3b2 + x <= 7; 4 y + b2 - 3 b3 <= 2; constraint_num2: -4 b1 + b2 - 3 z <= -2; """ model = model_builder.ModelBuilder() self.assertTrue(model.import_from_lp_string(lp_data)) self.assertEqual(6, model.num_variables) self.assertEqual(3, model.num_constraints) self.assertEqual(1, model.var_from_index(0).lower_bound) self.assertEqual(42, model.var_from_index(0).upper_bound) self.assertEqual('x', model.var_from_index(0).name) def test_import_from_lp_file(self): path = os.path.dirname(__file__) lp_path = f'{path}/../testdata/small_model.lp' model = model_builder.ModelBuilder() self.assertTrue(model.import_from_lp_file(lp_path)) self.assertEqual(6, model.num_variables) self.assertEqual(3, model.num_constraints) self.assertEqual(1, model.var_from_index(0).lower_bound) self.assertEqual(42, model.var_from_index(0).upper_bound) self.assertEqual('x', model.var_from_index(0).name) def test_variables(self): model = model_builder.ModelBuilder() x = model.new_int_var(0.0, 4.0, 'x') self.assertEqual(0, x.index) self.assertEqual(0.0, x.lower_bound) self.assertEqual(4.0, x.upper_bound) self.assertEqual('x', x.name) x.lower_bound = 1.0 x.upper_bound = 3.0 self.assertEqual(1.0, x.lower_bound) self.assertEqual(3.0, x.upper_bound) self.assertTrue(x.is_integral) # Tests the equality operator. y = model.new_int_var(0.0, 4.0, 'y') x_copy = model.var_from_index(0) self.assertEqual(x, x) self.assertEqual(x, x_copy) self.assertNotEqual(x, y) # array xs = model.new_int_var_ndarray(10, 0.0, 5.0, '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_') 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_') 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])) bs = model.new_bool_var_ndarray([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) # Slices are [lb, ub) closed - open. self.assertEqual(5, bs[3, :].size) self.assertEqual(6, bs[1:3, 2:5].size) # Tests the hash method. var_set = set() var_set.add(x) self.assertIn(x, var_set) self.assertIn(x_copy, var_set) self.assertNotIn(y, var_set) def test_duplicate_variables(self): model = model_builder.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)) def test_issue_3614(self): total_number_of_choices = 5 + 1 total_unique_products = 3 standalone_features = list(range(5)) feature_bundle_incidence_matrix = {} for idx in range(len(standalone_features)): feature_bundle_incidence_matrix[idx, 0] = 0 feature_bundle_incidence_matrix[0, 0] = 1 feature_bundle_incidence_matrix[1, 0] = 1 bundle_start_idx = len(standalone_features) # Model model = model_builder.ModelBuilder() y = {} v = {} for i in range(total_number_of_choices): y[i] = model.new_bool_var(f'y_{i}') for j in range(total_unique_products): for i in range(len(standalone_features)): v[i, j] = model.new_bool_var(f'v_{(i,j)}') model.add(v[i, j] == (y[i] + (feature_bundle_incidence_matrix[(i, 0)] * y[bundle_start_idx]))) solver = model_builder.ModelSolver('scip') status = solver.solve(model) self.assertEqual(model_builder.SolveStatus.OPTIMAL, status) def test_varcompvar(self): model = model_builder.ModelBuilder() x = model.new_int_var(0.0, 4.0, 'x') y = model.new_int_var(0.0, 4.0, 'y') ct = x == y self.assertEqual(ct.left.index, x.index) self.assertEqual(ct.right.index, y.index) if __name__ == '__main__': unittest.main()