diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 596f52b1259..fd5322ba686 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -1934,7 +1934,14 @@ def valid_problem_types(self): Model object.""" return [ProblemFormat.pyomo] - def write(self, filename=None, format=None, solver_capability=None, io_options={}): + def write( + self, + filename=None, + format=None, + solver_capability=None, + io_options={}, + int_marker=False, + ): """ Write the model to a file, with a given format. """ @@ -1968,7 +1975,8 @@ def write(self, filename=None, format=None, solver_capability=None, io_options={ "Filename '%s' likely does not match specified " "file format (%s)" % (filename, format) ) - problem_writer = WriterFactory(format) + int_marker_kwds = {"int_marker": int_marker} if int_marker else {} + problem_writer = WriterFactory(format, **int_marker_kwds) if problem_writer is None: raise ValueError( "Cannot write model in format '%s': no model " diff --git a/pyomo/repn/plugins/mps.py b/pyomo/repn/plugins/mps.py index 89420929778..f40c7666278 100644 --- a/pyomo/repn/plugins/mps.py +++ b/pyomo/repn/plugins/mps.py @@ -55,7 +55,7 @@ def _get_bound(exp): @WriterFactory.register('mps', 'Generate the corresponding MPS file') class ProblemWriter_mps(AbstractProblemWriter): - def __init__(self): + def __init__(self, int_marker=False): AbstractProblemWriter.__init__(self, ProblemFormat.mps) # the MPS writer is responsible for tracking which variables are @@ -78,6 +78,8 @@ def __init__(self): # the number's sign. self._precision_string = '.17g' + self._int_marker = int_marker + def __call__(self, model, output_filename, solver_capability, io_options): # Make sure not to modify the user's dictionary, # they may be reusing it outside of this call @@ -515,10 +517,27 @@ def yield_all_constraints(): column_template = " %s %s %" + self._precision_string + "\n" output_file.write("COLUMNS\n") cnt = 0 + in_integer_section = False + mark_cnt = 0 for vardata in variable_list: col_entries = column_data[variable_to_column[vardata]] cnt += 1 if len(col_entries) > 0: + if self._int_marker: + if vardata.is_integer(): + if not in_integer_section: + output_file.write( + f" MARK{mark_cnt:04d} 'MARKER' 'INTORG'\n" + ) + in_integer_section = True + mark_cnt += 1 + elif in_integer_section: # and not vardata.is_integer() + output_file.write( + f" MARK{mark_cnt:04d} 'MARKER' 'INTEND'\n" + ) + in_integer_section = False + mark_cnt += 1 + var_label = variable_symbol_dictionary[id(vardata)] for i, (row_label, coef) in enumerate(col_entries): output_file.write( @@ -536,6 +555,9 @@ def yield_all_constraints(): var_label = variable_symbol_dictionary[id(vardata)] output_file.write(column_template % (var_label, objective_label, 0)) + if self._int_marker and in_integer_section: + output_file.write(f" MARK{mark_cnt:04d} 'MARKER' 'INTEND'\n") + assert cnt == len(column_data) - 1 if len(column_data[-1]) > 0: col_entries = column_data[-1] diff --git a/pyomo/repn/tests/mps/integer_variable_declaration_with_marker.mps.baseline b/pyomo/repn/tests/mps/integer_variable_declaration_with_marker.mps.baseline new file mode 100644 index 00000000000..d455b38af0c --- /dev/null +++ b/pyomo/repn/tests/mps/integer_variable_declaration_with_marker.mps.baseline @@ -0,0 +1,27 @@ +* Source: Pyomo MPS Writer +* Format: Free MPS +* +NAME Example-mix-integer-linear-problem +OBJSENSE + MIN +ROWS + N obj + G c_l_const1_ + L c_u_const2_ +COLUMNS + MARK0000 'MARKER' 'INTORG' + x1 obj 3 + x1 c_l_const1_ 4 + x1 c_u_const2_ 1 + MARK0001 'MARKER' 'INTEND' + x2 obj 2 + x2 c_l_const1_ 3 + x2 c_u_const2_ 2 +RHS + RHS c_l_const1_ 10 + RHS c_u_const2_ 7 +BOUNDS + LI BOUND x1 0 + UI BOUND x1 10E20 + LO BOUND x2 0 +ENDATA diff --git a/pyomo/repn/tests/mps/knapsack_problem_binary_variable_declaration_with_marker.mps.baseline b/pyomo/repn/tests/mps/knapsack_problem_binary_variable_declaration_with_marker.mps.baseline new file mode 100644 index 00000000000..d19c9285e5c --- /dev/null +++ b/pyomo/repn/tests/mps/knapsack_problem_binary_variable_declaration_with_marker.mps.baseline @@ -0,0 +1,40 @@ +* Source: Pyomo MPS Writer +* Format: Free MPS +* +NAME knapsack problem +OBJSENSE + MIN +ROWS + N obj + G c_l_const1_ +COLUMNS + MARK0000 'MARKER' 'INTORG' + x(_1_) obj 3 + x(_1_) c_l_const1_ 30 + x(_2_) obj 2 + x(_2_) c_l_const1_ 24 + x(_3_) obj 2 + x(_3_) c_l_const1_ 11 + x(_4_) obj 4 + x(_4_) c_l_const1_ 35 + x(_5_) obj 5 + x(_5_) c_l_const1_ 29 + x(_6_) obj 4 + x(_6_) c_l_const1_ 8 + x(_7_) obj 3 + x(_7_) c_l_const1_ 31 + x(_8_) obj 1 + x(_8_) c_l_const1_ 18 + MARK0001 'MARKER' 'INTEND' +RHS + RHS c_l_const1_ 60 +BOUNDS + BV BOUND x(_1_) + BV BOUND x(_2_) + BV BOUND x(_3_) + BV BOUND x(_4_) + BV BOUND x(_5_) + BV BOUND x(_6_) + BV BOUND x(_7_) + BV BOUND x(_8_) +ENDATA diff --git a/pyomo/repn/tests/mps/test_mps.py b/pyomo/repn/tests/mps/test_mps.py index 44f3d93b75e..9be45a17870 100644 --- a/pyomo/repn/tests/mps/test_mps.py +++ b/pyomo/repn/tests/mps/test_mps.py @@ -18,7 +18,17 @@ from filecmp import cmp import pyomo.common.unittest as unittest -from pyomo.environ import ConcreteModel, Var, Objective, Constraint, ComponentMap +from pyomo.environ import ( + ConcreteModel, + Var, + Objective, + Constraint, + ComponentMap, + minimize, + Binary, + NonNegativeReals, + NonNegativeIntegers, +) thisdir = os.path.dirname(os.path.abspath(__file__)) @@ -36,11 +46,15 @@ def _get_fnames(self): return prefix + ".mps.baseline", prefix + ".mps.out" def _check_baseline(self, model, **kwds): + int_marker = kwds.pop("int_marker", False) baseline_fname, test_fname = self._get_fnames() self._cleanup(test_fname) io_options = {"symbolic_solver_labels": True} io_options.update(kwds) - model.write(test_fname, format="mps", io_options=io_options) + model.write( + test_fname, format="mps", io_options=io_options, int_marker=int_marker + ) + self.assertTrue( cmp(test_fname, baseline_fname), msg="Files %s and %s differ" % (test_fname, baseline_fname), @@ -185,6 +199,52 @@ def test_row_ordering(self): row_order[model.con4[2]] = -1 self._check_baseline(model, row_order=row_order) + def test_knapsack_problem_binary_variable_declaration_with_marker(self): + elements_size = [30, 24, 11, 35, 29, 8, 31, 18] + elements_weight = [3, 2, 2, 4, 5, 4, 3, 1] + capacity = 60 + + model = ConcreteModel("knapsack problem") + var_names = [f"{i + 1}" for i in range(len(elements_size))] + + model.x = Var(var_names, within=Binary) + + model.obj = Objective( + expr=sum( + model.x[var_names[i]] * elements_weight[i] + for i in range(len(elements_size)) + ), + sense=minimize, + name="obj", + ) + + model.const1 = Constraint( + expr=sum( + model.x[var_names[i]] * elements_size[i] + for i in range(len(elements_size)) + ) + >= capacity, + name="const", + ) + + self._check_baseline(model, int_marker=True) + + def test_integer_variable_declaration_with_marker(self): + model = ConcreteModel("Example-mix-integer-linear-problem") + + # Define the decision variables + model.x1 = Var(within=NonNegativeIntegers) # Integer variable + model.x2 = Var(within=NonNegativeReals) # Continuous variable + + # Define the objective function + model.obj = Objective(expr=3 * model.x1 + 2 * model.x2, sense=minimize) + + # Define the constraints + model.const1 = Constraint(expr=4 * model.x1 + 3 * model.x2 >= 10) + model.const2 = Constraint(expr=model.x1 + 2 * model.x2 <= 7) + + self._check_baseline(model, int_marker=True) + if __name__ == "__main__": unittest.main()