Repository: Pyomo/pyomo

Issue:
mps writer does not mark discrete variables
As described in the MPS documention below, discrete variables should be separated from continuous variable through the use of markers. It does not seem as though the current MPS writer does this.

http://lpsolve.sourceforge.net/5.0/mps-format.htm




Discussion:

Resolving patch:
diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py
--- 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
--- 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]


Test patch:
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
--- /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
--- /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
--- 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()