Repository: Pyomo/pyomo

Issue:
Strip/Restore Bounds Feature
It looks like at least in solving square problems with IPOPT it is useful to remove the bounds on variables before solving.  There are likely other situations where this would be helpful.  When solving an optimization problem with IPOPT you usually need to bring the bounds back.  A simple addition to the solve function that could send problems to the solver unbounded may suffice here. There are also strip_bounds and restore_bounds functions in the IDAES stuff that save the bounds then remove them in the Pyomo problem.

Anyway I think this would be a good addition to Pyomo.


Discussion:
You could implement this as a reversible transformation. I've been putting things like this under `pyomo.core.plugins.transform`.

Resolving patch:
diff --git a/pyomo/contrib/preprocessing/plugins/__init__.py b/pyomo/contrib/preprocessing/plugins/__init__.py
--- a/pyomo/contrib/preprocessing/plugins/__init__.py
+++ b/pyomo/contrib/preprocessing/plugins/__init__.py
@@ -4,3 +4,4 @@ def load():
     import pyomo.contrib.preprocessing.plugins.init_vars
     import pyomo.contrib.preprocessing.plugins.remove_zero_terms
     import pyomo.contrib.preprocessing.plugins.equality_propagate
+    import pyomo.contrib.preprocessing.plugins.strip_bounds
diff --git a/pyomo/contrib/preprocessing/plugins/strip_bounds.py b/pyomo/contrib/preprocessing/plugins/strip_bounds.py
new file mode 100644
--- /dev/null
+++ b/pyomo/contrib/preprocessing/plugins/strip_bounds.py
@@ -0,0 +1,74 @@
+"""Transformation to strip variable bounds from a model."""
+import textwrap
+
+from pyomo.core.base.var import Var
+from pyomo.core.plugins.transform.hierarchy import NonIsomorphicTransformation
+from pyomo.util.plugin import alias
+from pyomo.core.kernel.component_map import ComponentMap
+from pyomo.core.kernel.set_types import Reals
+
+
+class VariableBoundStripper(NonIsomorphicTransformation):
+    """Strips bounds from variables."""
+
+    alias('contrib.strip_var_bounds',
+          doc=textwrap.fill(textwrap.dedent(__doc__.strip())))
+
+    def __init__(self):
+        """Initialize the transformation."""
+        super(VariableBoundStripper, self).__init__()
+
+    def _apply_to(self, instance, strip_domains=True, reversible=False):
+        """Apply the transformation.
+
+        Args:
+            instance (Block): the block on which to strip variable bounds
+            strip_domains (bool, optional): strip the domain for discrete
+                variables as well
+            reversible (bool, optional): Whether the bound stripping will be
+                temporary. If so, store information for reversion.
+
+        Returns:
+            None
+
+        """
+        if reversible:
+            if any(hasattr(instance, map_name) for map_name in [
+                    '_tmp_var_bound_strip_lb',
+                    '_tmp_var_bound_strip_ub',
+                    '_tmp_var_bound_strip_domain']):
+                raise RuntimeError(
+                    'Variable stripping reversion component maps already '
+                    'exist. Did you already apply a temporary transformation '
+                    'without a subsequent reversion?')
+            # Component maps to store data for reversion.
+            instance._tmp_var_bound_strip_lb = ComponentMap()
+            instance._tmp_var_bound_strip_ub = ComponentMap()
+            instance._tmp_var_bound_strip_domain = ComponentMap()
+        for var in instance.component_data_objects(ctype=Var):
+            if strip_domains and not var.domain == Reals:
+                if reversible:
+                    instance._tmp_var_bound_strip_domain[var] = var.domain
+                var.domain = Reals
+            if var.has_lb():
+                if reversible:
+                    instance._tmp_var_bound_strip_lb[var] = var.lb
+                var.setlb(None)
+            if var.has_ub():
+                if reversible:
+                    instance._tmp_var_bound_strip_ub[var] = var.ub
+                var.setub(None)
+
+    def revert(self, instance):
+        """Revert variable bounds and domains changed by the transformation."""
+        for var in instance.component_data_objects(
+                ctype=Var, descend_into=True):
+            if var in instance._tmp_var_bound_strip_lb:
+                var.setlb(instance._tmp_var_bound_strip_lb[var])
+            if var in instance._tmp_var_bound_strip_ub:
+                var.setub(instance._tmp_var_bound_strip_ub[var])
+            if var in instance._tmp_var_bound_strip_domain:
+                var.domain = instance._tmp_var_bound_strip_domain[var]
+        del instance._tmp_var_bound_strip_lb
+        del instance._tmp_var_bound_strip_ub
+        del instance._tmp_var_bound_strip_domain


Test patch:
diff --git a/pyomo/contrib/preprocessing/tests/test_strip_bounds.py b/pyomo/contrib/preprocessing/tests/test_strip_bounds.py
new file mode 100644
--- /dev/null
+++ b/pyomo/contrib/preprocessing/tests/test_strip_bounds.py
@@ -0,0 +1,111 @@
+"""Tests stripping of variable bounds."""
+import pyutilib.th as unittest
+
+from pyomo.environ import (Binary, ConcreteModel, Integers, NonNegativeReals,
+                           PositiveReals, Reals, TransformationFactory, Var)
+
+
+class TestStripBounds(unittest.TestCase):
+    """Tests stripping of bounds."""
+
+    def shortDescription(self):
+        """Suppress one-line description at high verbosity."""
+        return None
+
+    def test_strip_bounds_maps_exist(self):
+        """Tests if component maps for reversion already exist."""
+        m = ConcreteModel()
+        m.v0 = Var(bounds=(2, 4))
+        m.v1 = Var(domain=NonNegativeReals)
+        m.v2 = Var(domain=PositiveReals)
+        m.v3 = Var(bounds=(-1, 1))
+        m.v4 = Var(domain=Binary)
+        m.v5 = Var(domain=Integers, bounds=(15, 16))
+
+        xfrm = TransformationFactory('contrib.strip_var_bounds')
+        xfrm.apply_to(m, reversible=True)
+        # At this point, component maps for reversion already exist.
+        with self.assertRaises(RuntimeError):
+            xfrm.apply_to(m, reversible=True)
+
+    def test_strip_bounds(self):
+        """Test bound stripping and restoration."""
+        m = ConcreteModel()
+        m.v0 = Var(bounds=(2, 4))
+        m.v1 = Var(domain=NonNegativeReals)
+        m.v2 = Var(domain=PositiveReals)
+        m.v3 = Var(bounds=(-1, 1))
+        m.v4 = Var(domain=Binary)
+        m.v5 = Var(domain=Integers, bounds=(15, 16))
+
+        xfrm = TransformationFactory('contrib.strip_var_bounds')
+        xfrm.apply_to(m, reversible=True)
+        self.assertEqual(m.v0.bounds, (None, None))
+        self.assertEqual(m.v1.bounds, (None, None))
+        self.assertEqual(m.v2.bounds, (None, None))
+        self.assertEqual(m.v3.bounds, (None, None))
+        self.assertEqual(m.v4.bounds, (None, None))
+        self.assertEqual(m.v5.bounds, (None, None))
+        self.assertEqual(m.v0.domain, Reals)
+        self.assertEqual(m.v1.domain, Reals)
+        self.assertEqual(m.v2.domain, Reals)
+        self.assertEqual(m.v3.domain, Reals)
+        self.assertEqual(m.v4.domain, Reals)
+        self.assertEqual(m.v5.domain, Reals)
+
+        xfrm.revert(m)
+        self.assertEqual(m.v0.bounds, (2, 4))
+        self.assertEqual(m.v1.bounds, (0, None))
+        self.assertEqual(m.v2.bounds, (0, None))
+        self.assertEqual(m.v3.bounds, (-1, 1))
+        self.assertEqual(m.v4.bounds, (0, 1))
+        self.assertEqual(m.v5.bounds, (15, 16))
+        self.assertEqual(m.v0.domain, Reals)
+        self.assertEqual(m.v1.domain, NonNegativeReals)
+        self.assertEqual(m.v2.domain, PositiveReals)
+        self.assertEqual(m.v3.domain, Reals)
+        self.assertEqual(m.v4.domain, Binary)
+        self.assertEqual(m.v5.domain, Integers)
+
+    def test_no_strip_domain(self):
+        """Test bounds stripping without domain change."""
+        m = ConcreteModel()
+        m.v0 = Var(bounds=(2, 4))
+        m.v1 = Var(domain=NonNegativeReals)
+        m.v2 = Var(domain=PositiveReals)
+        m.v3 = Var(bounds=(-1, 1))
+        m.v4 = Var(domain=Binary)
+        m.v5 = Var(domain=Integers, bounds=(15, 16))
+
+        xfrm = TransformationFactory('contrib.strip_var_bounds')
+        xfrm.apply_to(m, strip_domains=False, reversible=True)
+        self.assertEqual(m.v0.bounds, (None, None))
+        self.assertEqual(m.v1.bounds, (0, None))
+        self.assertEqual(m.v2.bounds, (0, None))
+        self.assertEqual(m.v3.bounds, (None, None))
+        self.assertEqual(m.v4.bounds, (0, 1))
+        self.assertEqual(m.v5.bounds, (None, None))
+        self.assertEqual(m.v0.domain, Reals)
+        self.assertEqual(m.v1.domain, NonNegativeReals)
+        self.assertEqual(m.v2.domain, PositiveReals)
+        self.assertEqual(m.v3.domain, Reals)
+        self.assertEqual(m.v4.domain, Binary)
+        self.assertEqual(m.v5.domain, Integers)
+
+        xfrm.revert(m)
+        self.assertEqual(m.v0.bounds, (2, 4))
+        self.assertEqual(m.v1.bounds, (0, None))
+        self.assertEqual(m.v2.bounds, (0, None))
+        self.assertEqual(m.v3.bounds, (-1, 1))
+        self.assertEqual(m.v4.bounds, (0, 1))
+        self.assertEqual(m.v5.bounds, (15, 16))
+        self.assertEqual(m.v0.domain, Reals)
+        self.assertEqual(m.v1.domain, NonNegativeReals)
+        self.assertEqual(m.v2.domain, PositiveReals)
+        self.assertEqual(m.v3.domain, Reals)
+        self.assertEqual(m.v4.domain, Binary)
+        self.assertEqual(m.v5.domain, Integers)
+
+
+if __name__ == '__main__':
+    unittest.main()