Repository: Pyomo/pyomo

Issue:
Add `evaluate_fgh` to PythonCallbackFunction API
## Summary

ExternalFunctions through ASL offer calls to `evaluate_fgh` to provide derivative information. We should add this feature to the `PythonCallbackFunction` API as well.

### Rationale

The APIs should match for consistency across different types of ExternalFunctions.




Discussion:

Resolving patch:
diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py
--- a/pyomo/common/deprecation.py
+++ b/pyomo/common/deprecation.py
@@ -58,7 +58,7 @@ def _default_msg(obj, user_msg, version, remove_in):
     if version:
         comment.append('deprecated in %s' % (version,))
     if remove_in:
-        comment.append('will be removed in %s' % (remove_in))
+        comment.append('will be removed in (or after) %s' % (remove_in))
     if comment:
         return user_msg + "  (%s)" % (', '.join(comment),)
     else:
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
@@ -29,7 +29,7 @@
 from pyomo.core.base.numvalue import (
     NumericValue, native_types, value as expr_value
 )
-from pyomo.core.base.set_types import Any, Reals
+from pyomo.core.base.set import Any, GlobalSetBase, Reals
 from pyomo.core.base.units_container import units
 
 logger = logging.getLogger('pyomo.core')
@@ -46,6 +46,7 @@ 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.
 
@@ -53,40 +54,66 @@ class _ImplicitAny(Any.__class__):
     change of Param's implicit domain from Any to Reals.
 
     """
-    def __new__(cls, **kwds):
-        return super(_ImplicitAny, cls).__new__(cls)
+    def __new__(cls, **kwargs):
+        # Strip off owner / kwargs before calling base __new__
+        return super().__new__(cls)
 
-    def __init__(self, owner, **kwds):
-        super(_ImplicitAny, self).__init__(**kwds)
+    def __init__(self, owner, **kwargs):
         self._owner = weakref_ref(owner)
+        super().__init__(**kwargs)
         self._component = weakref_ref(self)
         self.construct()
 
     def __getstate__(self):
-        state = super(_ImplicitAny, self).__getstate__()
+        state = super().__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)
+        super().__setstate__(state)
         self._owner = None if _owner is None else weakref_ref(_owner)
 
     def __deepcopy__(self, memo):
-        return super(Any.__class__, self).__deepcopy__(memo)
+        # Note: we need to start super() at GlobalSetBase to actually
+        # copy this object
+        return super(GlobalSetBase, self).__deepcopy__(memo)
 
     def __contains__(self, val):
         if val not in Reals:
+            if self._owner is None or self._owner() is None:
+                name = 'Unknown'
+            else:
+                name = self._owner().name
             deprecation_warning(
+                f"Param '{name}' declared with an implicit domain of 'Any'. "
                 "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) "
+                "future.  If you really intend the domain of this Param"
                 "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,),
+                "specifying 'within=Any' to the Param constructor.",
                 version='5.6.9', remove_in='6.0')
         return True
 
+    # This should "mock up" a global set, so the "name" should always be
+    # the local name (without block scope)
+    def getname(self, fully_qualified=False, name_buffer=None, relative_to=None):
+        return super().getname(False, name_buffer, relative_to)
+
+    # The parent tracks the parent of the owner.  We can't set it
+    # directly here because the owner has not been assigned to a block
+    # when we create the _ImplicitAny
+    @property
+    def _parent(self):
+        if self._owner is None or self._owner() is None:
+            return None
+        return self._owner()._parent
+    # This is not settable.  However the base classes assume that it is,
+    # so we need to define the setter and just ignore the incoming value
+    @_parent.setter
+    def _parent(self, val):
+        pass
+
 
 class _ParamData(ComponentData, NumericValue):
     """
@@ -268,9 +295,6 @@ def __init__(self, *args, **kwd):
         _init = self._pop_from_kwargs(
             'Param', kwd, ('rule', 'initialize'), NOTSET)
         self.domain = self._pop_from_kwargs('Param', kwd, ('domain', 'within'))
-        if self.domain is None:
-            self.domain = _ImplicitAny(owner=self, name='Any')
-
         self._validate      = kwd.pop('validate', None )
         self._mutable       = kwd.pop('mutable', Param.DefaultMutable )
         self._default_val   = kwd.pop('default', Param.NoValue )
@@ -283,6 +307,8 @@ def __init__(self, *args, **kwd):
         kwd.setdefault('ctype', Param)
         IndexedComponent.__init__(self, *args, **kwd)
 
+        if self.domain is None:
+            self.domain = _ImplicitAny(owner=self, name='Any')
         # After IndexedComponent.__init__ so we can call is_indexed().
         self._rule = Initializer(_init,
                                  treat_sequences_as_mappings=self.is_indexed(),


Test patch:
diff --git a/pyomo/common/tests/test_deprecated.py b/pyomo/common/tests/test_deprecated.py
--- a/pyomo/common/tests/test_deprecated.py
+++ b/pyomo/common/tests/test_deprecated.py
@@ -38,7 +38,7 @@ def test_deprecation_warning(self):
 
         self.assertIn('DEPRECATED: This has been deprecated',
                       DEP_OUT.getvalue())
-        self.assertIn('(deprecated in 1.2, will be removed in 3.4)',
+        self.assertIn('(deprecated in 1.2, will be removed in (or after) 3.4)',
                       DEP_OUT.getvalue().replace('\n',' '))
 
         DEP_OUT = StringIO()
@@ -47,7 +47,7 @@ def test_deprecation_warning(self):
 
         self.assertIn('DEPRECATED: custom message here',
                       DEP_OUT.getvalue())
-        self.assertIn('(deprecated in 1.2, will be removed in 3.4)',
+        self.assertIn('(deprecated in 1.2, will be removed in (or after) 3.4)',
                       DEP_OUT.getvalue().replace('\n',' '))
 
 
@@ -305,7 +305,7 @@ def bar(self):
         self.assertIn(
             '.. deprecated:: 1.2\n   This function has been deprecated',
             foo.bar.__doc__)
-        self.assertIn('(will be removed in 3.4)',
+        self.assertIn('(will be removed in (or after) 3.4)',
                       foo.bar.__doc__.replace('\n',' '))
 
         # Test the default argument
@@ -320,8 +320,8 @@ def bar(self):
         # Test that the deprecation warning was logged
         self.assertIn('DEPRECATED: This function has been deprecated',
                       DEP_OUT.getvalue())
-        self.assertIn('(deprecated in 1.2, will be removed in 3.4)',
-                      DEP_OUT.getvalue())
+        self.assertIn('(deprecated in 1.2, will be removed in (or after) 3.4)',
+                      DEP_OUT.getvalue().replace('\n', ' '))
 
 
 class Bar(object):
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
@@ -24,13 +24,13 @@
 
 import pyomo.common.unittest as unittest
 
-from pyomo.environ import (Set, RangeSet, Param, ConcreteModel,
-                           AbstractModel, Constraint, Var,
-                           NonNegativeIntegers, Integers,
-                           NonNegativeReals, Boolean, Reals, Any, display,
-                           value, set_options, sin, cos, tan, log, log10,
-                           exp, sqrt, ceil, floor, asin, acos, atan, sinh,
-                           cosh, tanh, asinh, acosh, atanh)
+from pyomo.environ import (
+    Set, RangeSet, Param, ConcreteModel, AbstractModel, Block, Constraint, Var,
+    NonNegativeIntegers, Integers, NonNegativeReals, Boolean, Reals, Any,
+    display, value, set_options,
+    sin, cos, tan, log, log10, exp, sqrt, ceil, floor, asin, acos, atan,
+    sinh, cosh, tanh, asinh, acosh, atanh,
+)
 from pyomo.common.errors import PyomoException
 from pyomo.common.log import LoggingIntercept
 from pyomo.common.tempfiles import TempfileManager
@@ -1411,13 +1411,29 @@ def test_domain_deprecation(self):
         with LoggingIntercept(log, 'pyomo.core'):
             m.p = 'a'
         self.assertIn(
-            "DEPRECATED: The default domain for Param objects is 'Any'",
-            log.getvalue())
+            "The default domain for Param objects is 'Any'",
+            log.getvalue().replace('\n', ' '))
         self.assertIn(
-            "domain of this Param (p) to be 'Any'",
-            log.getvalue())
+            "DEPRECATED: Param 'p' declared with an implicit domain of 'Any'",
+            log.getvalue().replace('\n', ' '))
         self.assertEqual(value(m.p), 'a')
 
+        m.b = Block()
+        m.b.q = Param()
+        buf = StringIO()
+        m.b.q.pprint(ostream=buf)
+        self.assertEqual(
+            buf.getvalue().strip(),
+            """
+q : Size=0, Index=None, Domain=Any, Default=None, Mutable=False
+    Key : Value
+            """.strip())
+
+        i = m.clone()
+        self.assertIsNot(m.p.domain, i.p.domain)
+        self.assertIs(m.p.domain._owner(), m.p)
+        self.assertIs(i.p.domain._owner(), i.p)
+
     @unittest.skipUnless(pint_available, "units test requires pint module")
     def test_set_value_units(self):
         m = ConcreteModel()