Repository: Pyomo/pyomo

Issue:
Default domain for mutable Params
Suppose I have:
```
from pyomo.environ import *

m = ConcreteModel()

m.x = Var(bounds=(0, 10))
m.param = Param(initialize=5, mutable=True)
m.cons = Constraint(expr=m.x >= m.param)
m.obj = Objective(expr=m.x)

m.param = '10' #oops

# Now the error we get doesn't tell us where or what the problem is.
SolverFactory('cplex').solve(m)
# TypeError: unsupported operand type(s) for -: 'str' and 'int'
```
This is dumb on my part, but I doubt I'll be the last. I think the question is what the default domain for Params should be. Right now it looks like it is Any. So I could have saved myself by doing:
```
m.param = Param(initialize=5, mutable=True, within=Reals)
```
and I would get a good error message when I tried to set `m.param` to a string. But my concern is that the `TypeError` certainly isn't what led me to that answer. And in addition, this seems inconsistent because the writer is clearly expecting a number, but, by default, Param doesn't validate that its value is a number.


Discussion:
I just ran into this for a case where a Param was being set to a Pyomo expression (instead of the value of that expression) and the error thrown when calling Ipopt was completely unhelpful. I agree with @emma58 that Reals might be a better default domain for Params since that is the implicit expectation in the writers.
I agree. Easy for users to burn themselves by loading in data to a Param assuming that it's going to be interpreted as a numeric value, but their data source encoded things as strings.

Do we have anyone using Params with non-numerics? I think most people would just go with `dict` for that case.
Note that the Book documents this "feature":
> A Param object can contain any value: string, floating point, etc. The default domain for parameters is `Any`, so by default no domain validation is performed. However, it is often valuable to specify the space of valid parameter values to provide checking of the input data. Similar to the `Set` component, there are two ways to validate parameter values. First, the domain of feasible parameter values can be specified using the within option:
> `model.Z = Param(within=Reals)`


Resolving patch:
diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py
--- a/pyomo/core/base/param.py
+++ b/pyomo/core/base/param.py
@@ -15,6 +15,7 @@
 import logging
 from weakref import ref as weakref_ref
 
+from pyomo.common.deprecation import deprecation_warning
 from pyomo.common.modeling import NoArgumentGiven
 from pyomo.common.timing import ConstructionTimer
 from pyomo.core.base.plugin import ModelComponentFactory
@@ -23,7 +24,7 @@
     UnindexedComponent_set
 from pyomo.core.base.misc import apply_indexed_rule, apply_parameterized_indexed_rule
 from pyomo.core.base.numvalue import NumericValue, native_types, value
-from pyomo.core.base.set_types import Any
+from pyomo.core.base.set_types import Any, Reals
 
 from six import iteritems, iterkeys, next, itervalues
 
@@ -41,6 +42,38 @@ def _raise_modifying_immutable_error(obj, index):
         "declare the parameter as mutable [i.e., Param(mutable=True)]"
         % (name,))
 
+class _ImplicitAny(Any.__class__):
+    """An Any that issues a deprecation warning for non-Real values.
+
+    This is a helper class to implement the deprecation warnings for the
+    change of Param's implicit domain from Any to Reals.
+
+    """
+    def __init__(self, owner, **kwds):
+        super(_ImplicitAny, self).__init__(**kwds)
+        self._owner = weakref_ref(owner)
+
+    def __getstate__(self):
+        state = super(_ImplicitAny, self).__getstate__()
+        state['_owner'] = None if self._owner is None else self._owner()
+        return state
+
+    def __setstate__(self, state):
+        _owner = state.pop('_owner')
+        super(_ImplicitAny, self).__setstate__(state)
+        self._owner = None if _owner is None else weakref_ref(_owner)
+
+    def __contains__(self, val):
+        if val not in Reals:
+            deprecation_warning(
+                "The default domain for Param objects is 'Any'.  However, "
+                "we will be changing that default to 'Reals' in the "
+                "future.  If you really intend the domain of this Param (%s) "
+                "to be 'Any', you can suppress this warning by explicitly "
+                "specifying 'within=Any' to the Param constructor."
+                % ('Unknown' if self._owner is None else self._owner().name,),
+                version='TBD', remove_in='6.0')
+        return True
 
 class _NotValid(object):
     """A dummy type that is pickle-safe that we can use as the default
@@ -213,7 +246,7 @@ def __init__(self, *args, **kwd):
         self._rule          = kwd.pop('rule', _NotValid )
         self._rule          = kwd.pop('initialize', self._rule )
         self._validate      = kwd.pop('validate', None )
-        self.domain         = kwd.pop('domain', Any )
+        self.domain         = kwd.pop('domain', None )
         self.domain         = kwd.pop('within', self.domain )
         self._mutable       = kwd.pop('mutable', Param.DefaultMutable )
         self._default_val   = kwd.pop('default', _NotValid )
@@ -224,7 +257,7 @@ def __init__(self, *args, **kwd):
                 "The 'repn' keyword is not a validate keyword argument for Param")
         #
         if self.domain is None:
-            self.domain = Any
+            self.domain = _ImplicitAny(owner=self, name='Any')
         #
         kwd.setdefault('ctype', Param)
         IndexedComponent.__init__(self, *args, **kwd)
diff --git a/pyomo/pysp/scenariotree/tree_structure_model.py b/pyomo/pysp/scenariotree/tree_structure_model.py
--- a/pyomo/pysp/scenariotree/tree_structure_model.py
+++ b/pyomo/pysp/scenariotree/tree_structure_model.py
@@ -22,7 +22,9 @@
     has_networkx = False
 
 def CreateAbstractScenarioTreeModel():
-    from pyomo.core import (AbstractModel, Set, Param, Boolean)
+    from pyomo.core import (
+        AbstractModel, Set, Param, Boolean, Any, UnitInterval,
+    )
 
     model = AbstractModel()
 
@@ -40,6 +42,7 @@ def CreateAbstractScenarioTreeModel():
                          initialize=[],
                          ordered=True)
     model.ConditionalProbability = Param(model.Nodes,
+                                         within=UnitInterval,
                                          mutable=True)
 
     model.Scenarios = Set(ordered=True)
@@ -56,14 +59,17 @@ def CreateAbstractScenarioTreeModel():
                               ordered=True)
 
     model.StageCost = Param(model.Stages,
+                            within=Any,
                             mutable=True,
                             default=None)
     model.NodeCost = Param(model.Nodes,
+                           within=Any,
                            mutable=True,
                            default=None)
 
     # DEPRECATED
     model.StageCostVariable = Param(model.Stages,
+                                    within=Any,
                                     mutable=True)
 
     # it is often the case that a subset of the stage variables are strictly "derived"


Test patch:
diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py
--- a/pyomo/core/tests/unit/test_param.py
+++ b/pyomo/core/tests/unit/test_param.py
@@ -26,6 +26,7 @@
 import pyutilib.th as unittest
 
 from pyomo.environ import *
+from pyomo.common.log import LoggingIntercept
 from pyomo.core.base.param import _NotValid
 
 from six import iteritems, itervalues, StringIO
@@ -1345,6 +1346,26 @@ def test_nonnumeric(self):
       a : b
             """.strip())
 
+    def test_domain_deprecation(self):
+        m = ConcreteModel()
+        log = StringIO()
+        with LoggingIntercept(log, 'pyomo.core'):
+            m.p = Param(mutable=True)
+            m.p = 10
+        self.assertEqual(log.getvalue(), "")
+        self.assertEqual(value(m.p), 10)
+
+        with LoggingIntercept(log, 'pyomo.core'):
+            m.p = 'a'
+        self.assertIn(
+            "DEPRECATED: The default domain for Param objects is 'Any'",
+            log.getvalue())
+        self.assertIn(
+            "domain of this Param (p) to be 'Any'",
+            log.getvalue())
+        self.assertEqual(value(m.p), 'a')
+
+
 def createNonIndexedParamMethod(func, init_xy, new_xy, tol=1e-10):
 
     def testMethod(self):