Repository: Pyomo/pyomo

Issue:
Symbolic solver labels causing GAMS writer to produce element names too long for GAMS
GAMS apparently has a limit on label lengths of 63 characters. We need to make sure what is generated is shorter than that.

http://yetanothermathprogrammingconsultant.blogspot.com/2015/06/gams-limits-on-labels.html


Discussion:
I don't know that you can do that very easily without risking name clashes. It feels like this is not worth the effort of trying to "fix", beyond possibly writing a warning that name lengths are exceeding 63 characters.

If the symbolic labels are being used by an application for more than just debugging, then the symbol map can used to map what is in the GAMS file to actual component names (e.g., if parsing the GAMS file for some reason). If they are using it for debugging, then a GAMS file can still be written to allow visual inspection, even if it can not technically be parsed by GAMS.
I can conceive of a way to address this. If we have a `ShortNameLabeler`, we can do what Windows would do to maintain DOS compatibility: truncate the name to a certain number of characters, then add a special indicator character and a sequential (or random) number to the name.

Otherwise, we need to document how to get and use the `SymbolMap` easier. A lot of times, I can see users looking at the `*.lst` file and trying to work out what's going on, and then running into the `x148` or `c1078`. I can guarantee that if it's not obvious to me, there's plenty of users who struggle with this.

Resolving patch:
diff --git a/pyomo/core/base/label.py b/pyomo/core/base/label.py
--- a/pyomo/core/base/label.py
+++ b/pyomo/core/base/label.py
@@ -9,7 +9,8 @@
 #  ___________________________________________________________________________
 
 __all__ = ['CounterLabeler', 'NumericLabeler', 'CNameLabeler', 'TextLabeler',
-           'AlphaNumericTextLabeler','NameLabeler', 'CuidLabeler']
+           'AlphaNumericTextLabeler','NameLabeler', 'CuidLabeler',
+           'ShortNameLabeler']
 
 import six
 if six.PY3:
@@ -150,3 +151,22 @@ def __init__(self):
 
     def __call__(self, obj):
         return obj.getname(True, self.name_buffer)
+
+class ShortNameLabeler(object):
+    def __init__(self, limit, prefix, start=0, labeler=None):
+        self.id = start
+        self.prefix = prefix
+        self.limit = limit
+        if labeler is not None:
+            self.labeler = labeler
+        else:
+            self.labeler = AlphaNumericTextLabeler()
+
+    def __call__(self, obj=None):
+        lbl = self.labeler(obj)
+        if (len(lbl) < self.limit) or ((len(lbl) == self.limit) and (lbl[-len(self.prefix):] != self.prefix)):
+            return lbl
+        else:
+            self.id += 1
+            suffix = self.prefix + str(self.id) + self.prefix
+            return lbl[-self.limit + len(suffix):] + suffix
diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py
--- a/pyomo/repn/plugins/gams_writer.py
+++ b/pyomo/repn/plugins/gams_writer.py
@@ -20,7 +20,7 @@
 from pyomo.core.expr import current as EXPR
 from pyomo.core.expr.numvalue import is_fixed, value, as_numeric, native_types, native_numeric_types
 from pyomo.core.base import (
-    SymbolMap, AlphaNumericTextLabeler, NumericLabeler,
+    SymbolMap, ShortNameLabeler, NumericLabeler,
     Block, Constraint, Expression, Objective, Var, Set, RangeSet, Param,
     minimize, Suffix, SortComponents, Connector)
 
@@ -242,7 +242,7 @@ def __call__(self,
                              "I/O options is forbidden")
 
         if symbolic_solver_labels:
-            var_labeler = con_labeler = AlphaNumericTextLabeler()
+            var_labeler = con_labeler = ShortNameLabeler(63, '_')
         elif labeler is None:
             var_labeler = NumericLabeler('x')
             con_labeler = NumericLabeler('c')


Test patch:
diff --git a/pyomo/core/tests/unit/test_labelers.py b/pyomo/core/tests/unit/test_labelers.py
new file mode 100644
--- /dev/null
+++ b/pyomo/core/tests/unit/test_labelers.py
@@ -0,0 +1,156 @@
+#  ___________________________________________________________________________
+#
+#  Pyomo: Python Optimization Modeling Objects
+#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
+#  Under the terms of Contract DE-NA0003525 with National Technology and
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+#  rights in this software.
+#  This software is distributed under the 3-clause BSD License.
+#  ___________________________________________________________________________
+
+
+import pyutilib.th as unittest
+from pyomo.environ import *
+
+
+class LabelerTests(unittest.TestCase):
+
+    def setUp(self):
+        m = ConcreteModel()
+        m.mycomp = Var()
+        m.that = Var()
+        self.long1 = m.myverylongcomponentname = Var()
+        self.long2 = m.myverylongcomponentnamerighthere = Var()
+        self.long3 = m.anotherlongonebutdifferent = Var()
+        self.long4 = m.anotherlongonebutdifferentlongcomponentname = Var()
+        self.long5 = m.longcomponentname_1_ = Var()
+        m.s = RangeSet(10)
+        m.ind = Var(m.s)
+        m.myblock = Block()
+        m.myblock.mystreet = Constraint()
+        m.add_component("myblock.mystreet", Var())
+        self.thecopy = m.__getattribute__("myblock.mystreet")
+        self.m = m
+
+    def test_counterlabeler(self):
+        m = self.m
+        lbl = CounterLabeler()
+        self.assertEqual(lbl(m.mycomp), 1)
+        self.assertEqual(lbl(m.mycomp), 2)
+        self.assertEqual(lbl(m.that), 3)
+        self.assertEqual(lbl(self.long1), 4)
+        self.assertEqual(lbl(m.myblock), 5)
+        self.assertEqual(lbl(m.myblock.mystreet), 6)
+        self.assertEqual(lbl(self.thecopy), 7)
+
+    def test_numericlabeler(self):
+        m = self.m
+        lbl = NumericLabeler('x')
+        self.assertEqual(lbl(m.mycomp), 'x1')
+        self.assertEqual(lbl(m.mycomp), 'x2')
+        self.assertEqual(lbl(m.that), 'x3')
+        self.assertEqual(lbl(self.long1), 'x4')
+        self.assertEqual(lbl(m.myblock), 'x5')
+        self.assertEqual(lbl(m.myblock.mystreet), 'x6')
+        self.assertEqual(lbl(self.thecopy), 'x7')
+
+        lbl = NumericLabeler('xyz')
+        self.assertEqual(lbl(m.mycomp), 'xyz1')
+        self.assertEqual(lbl(m.mycomp), 'xyz2')
+        self.assertEqual(lbl(m.that), 'xyz3')
+        self.assertEqual(lbl(self.long1), 'xyz4')
+        self.assertEqual(lbl(m.myblock), 'xyz5')
+        self.assertEqual(lbl(m.myblock.mystreet), 'xyz6')
+        self.assertEqual(lbl(self.thecopy), 'xyz7')
+
+    def test_cnamelabeler(self):
+        m = self.m
+        lbl = CNameLabeler()
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.that), 'that')
+        self.assertEqual(lbl(self.long1), 'myverylongcomponentname')
+        self.assertEqual(lbl(m.myblock), 'myblock')
+        self.assertEqual(lbl(m.myblock.mystreet), 'myblock.mystreet')
+        self.assertEqual(lbl(self.thecopy), 'myblock.mystreet')
+        self.assertEqual(lbl(m.ind[3]), 'ind[3]')
+        self.assertEqual(lbl(m.ind[10]), 'ind[10]')
+        self.assertEqual(lbl(m.ind[1]), 'ind[1]')
+
+    def test_textlabeler(self):
+        m = self.m
+        lbl = TextLabeler()
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.that), 'that')
+        self.assertEqual(lbl(self.long1), 'myverylongcomponentname')
+        self.assertEqual(lbl(m.myblock), 'myblock')
+        self.assertEqual(lbl(m.myblock.mystreet), 'myblock_mystreet')
+        self.assertEqual(lbl(self.thecopy), 'myblock_mystreet')
+        self.assertEqual(lbl(m.ind[3]), 'ind(3)')
+        self.assertEqual(lbl(m.ind[10]), 'ind(10)')
+        self.assertEqual(lbl(m.ind[1]), 'ind(1)')
+
+    def test_alphanumerictextlabeler(self):
+        m = self.m
+        lbl = AlphaNumericTextLabeler()
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.that), 'that')
+        self.assertEqual(lbl(self.long1), 'myverylongcomponentname')
+        self.assertEqual(lbl(m.myblock), 'myblock')
+        self.assertEqual(lbl(m.myblock.mystreet), 'myblock_mystreet')
+        self.assertEqual(lbl(self.thecopy), 'myblock_mystreet')
+        self.assertEqual(lbl(m.ind[3]), 'ind_3_')
+        self.assertEqual(lbl(m.ind[10]), 'ind_10_')
+        self.assertEqual(lbl(m.ind[1]), 'ind_1_')
+
+    def test_namelabeler(self):
+        m = self.m
+        lbl = NameLabeler()
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.that), 'that')
+        self.assertEqual(lbl(self.long1), 'myverylongcomponentname')
+        self.assertEqual(lbl(m.myblock), 'myblock')
+        self.assertEqual(lbl(m.myblock.mystreet), 'myblock.mystreet')
+        self.assertEqual(lbl(self.thecopy), 'myblock.mystreet')
+        self.assertEqual(lbl(m.ind[3]), 'ind[3]')
+        self.assertEqual(lbl(m.ind[10]), 'ind[10]')
+        self.assertEqual(lbl(m.ind[1]), 'ind[1]')
+
+    def test_cuidlabeler(self):
+        m = self.m
+        lbl = CuidLabeler()
+        self.assertEqual(lbl(m.mycomp), ComponentUID(m.mycomp))
+        self.assertEqual(lbl(m.mycomp), ComponentUID(m.mycomp))
+        self.assertEqual(lbl(m.that), ComponentUID(m.that))
+        self.assertEqual(lbl(self.long1), ComponentUID(self.long1))
+        self.assertEqual(lbl(m.myblock), ComponentUID(m.myblock))
+        self.assertEqual(lbl(m.myblock.mystreet), ComponentUID(m.myblock.mystreet))
+        self.assertEqual(lbl(self.thecopy), ComponentUID(self.thecopy))
+        self.assertEqual(lbl(m.ind[3]), ComponentUID(m.ind[3]))
+        self.assertEqual(lbl(m.ind[10]), ComponentUID(m.ind[10]))
+        self.assertEqual(lbl(m.ind[1]), ComponentUID(m.ind[1]))
+
+    def test_shortnamelabeler(self):
+        m = self.m
+        lbl = ShortNameLabeler(20, '_')
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.mycomp), 'mycomp')
+        self.assertEqual(lbl(m.that), 'that')
+        self.assertEqual(lbl(self.long1), 'longcomponentname_1_')
+        self.assertEqual(lbl(self.long2), 'nentnamerighthere_2_')
+        self.assertEqual(lbl(self.long3), 'ngonebutdifferent_3_')
+        self.assertEqual(lbl(self.long4), 'longcomponentname_4_')
+        self.assertEqual(lbl(self.long5), 'gcomponentname_1__5_')
+        self.assertEqual(lbl(m.myblock), 'myblock')
+        self.assertEqual(lbl(m.myblock.mystreet), 'myblock_mystreet')
+        self.assertEqual(lbl(self.thecopy), 'myblock_mystreet')
+        self.assertEqual(lbl(m.ind[3]), 'ind_3_')
+        self.assertEqual(lbl(m.ind[10]), 'ind_10_')
+        self.assertEqual(lbl(m.ind[1]), 'ind_1_')
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/pyomo/solvers/tests/checks/test_GAMS.py b/pyomo/solvers/tests/checks/test_GAMS.py
--- a/pyomo/solvers/tests/checks/test_GAMS.py
+++ b/pyomo/solvers/tests/checks/test_GAMS.py
@@ -10,13 +10,10 @@
 
 
 import pyutilib.th as unittest
-import pyutilib.subprocess
 from pyutilib.misc import capture_output
 from pyomo.environ import *
-from six import StringIO
-import contextlib, sys, os, shutil
+import os, shutil
 from tempfile import mkdtemp
-import subprocess
 
 opt_py = SolverFactory('gams', solver_io='python')
 gamspy_available = opt_py.available(exception_flag=False)
@@ -209,6 +206,44 @@ def test_fixed_var_sign_gms(self):
             self.assertEqual(results.solver.termination_condition,
                              TerminationCondition.optimal)
 
+    @unittest.skipIf(not gamspy_available,
+                     "The 'gams' python bindings are not available")
+    def test_long_var_py(self):
+        with SolverFactory("gams", solver_io="python") as opt:
+
+            m = ConcreteModel()
+            x = m.a23456789012345678901234567890123456789012345678901234567890123 = Var()
+            y = m.b234567890123456789012345678901234567890123456789012345678901234 = Var()
+            z = m.c23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 = Var()
+            w = m.d01234567890 = Var()
+            m.c1 = Constraint(expr= x + y + z + w == 0)
+            m.c2 = Constraint(expr= x >= 10)
+            m.o = Objective(expr= x)
+
+            results = opt.solve(m)
+
+            self.assertEqual(results.solver.termination_condition,
+                             TerminationCondition.optimal)
+
+    @unittest.skipIf(not gamsgms_available,
+                     "The 'gams' executable is not available")
+    def test_long_var_gms(self):
+        with SolverFactory("gams", solver_io="gms") as opt:
+
+            m = ConcreteModel()
+            x = m.a23456789012345678901234567890123456789012345678901234567890123 = Var()
+            y = m.b234567890123456789012345678901234567890123456789012345678901234 = Var()
+            z = m.c23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 = Var()
+            w = m.d01234567890 = Var()
+            m.c1 = Constraint(expr= x + y + z + w == 0)
+            m.c2 = Constraint(expr= x >= 10)
+            m.o = Objective(expr= x)
+
+            results = opt.solve(m)
+
+            self.assertEqual(results.solver.termination_condition,
+                             TerminationCondition.optimal)
+
 class GAMSLogfileTestBase(unittest.TestCase):
     def setUp(self):
         """Set up model and temporary directory."""