Repository: Pyomo/pyomo

Issue:
differentiate cannot handle NPV expression types
This code:
```
from pyomo.environ import *

m = ConcreteModel()
m.x = Var()
m.x = 3
m.p = Param(initialize=3, mutable=True)

m.c = Constraint(expr=m.x - m.p <= 0)

print(differentiate(m.c.body, wrt_list=[m.x]))
```
gives the error
```
pyomo.core.expr.calculus.diff_with_pyomo.DifferentiationException: Unsupported expression type for differentiation: <class 'pyomo.core.expr.numeric_expr.NPV_NegationExpression'>
```
(It is fine if `m.p` is not mutable.)


Discussion:
I can fix this real quick.
Yay! (But no urgency--I found it doing something silly on my end.)

Resolving patch:
diff --git a/pyomo/core/expr/calculus/diff_with_pyomo.py b/pyomo/core/expr/calculus/diff_with_pyomo.py
--- a/pyomo/core/expr/calculus/diff_with_pyomo.py
+++ b/pyomo/core/expr/calculus/diff_with_pyomo.py
@@ -53,6 +53,15 @@ def _diff_SumExpression(node, val_dict, der_dict):
         der_dict[arg] += der
 
 
+def _diff_LinearExpression(node, val_dict, der_dict):
+    der = der_dict[node]
+    for ndx, v in enumerate(node.linear_vars):
+        coef = node.linear_coefs[ndx]
+        der_dict[v] += der * val_dict[coef]
+        der_dict[coef] += der * val_dict[v]
+
+    der_dict[node.constant] += der
+
 def _diff_PowExpression(node, val_dict, der_dict):
     """
 
@@ -325,6 +334,16 @@ def _diff_ExternalFunctionExpression(node, val_dict, der_dict):
 _diff_map[_expr.NegationExpression] = _diff_NegationExpression
 _diff_map[_expr.UnaryFunctionExpression] = _diff_UnaryFunctionExpression
 _diff_map[_expr.ExternalFunctionExpression] = _diff_ExternalFunctionExpression
+_diff_map[_expr.LinearExpression] = _diff_LinearExpression
+
+_diff_map[_expr.NPV_ProductExpression] = _diff_ProductExpression
+_diff_map[_expr.NPV_DivisionExpression] = _diff_DivisionExpression
+_diff_map[_expr.NPV_ReciprocalExpression] = _diff_ReciprocalExpression
+_diff_map[_expr.NPV_PowExpression] = _diff_PowExpression
+_diff_map[_expr.NPV_SumExpression] = _diff_SumExpression
+_diff_map[_expr.NPV_NegationExpression] = _diff_NegationExpression
+_diff_map[_expr.NPV_UnaryFunctionExpression] = _diff_UnaryFunctionExpression
+_diff_map[_expr.NPV_ExternalFunctionExpression] = _diff_ExternalFunctionExpression
 
 
 class _NamedExpressionCollector(ExpressionValueVisitor):
@@ -393,6 +412,18 @@ def visiting_potential_leaf(self, node):
                 self.der_dict[node] = 0
             return True, node
 
+        if node.__class__ is _expr.LinearExpression:
+            for v in node.linear_vars + node.linear_coefs + [node.constant]:
+                val = value(v)
+                self.val_dict[v] = val
+                if v not in self.der_dict:
+                    self.der_dict[v] = 0
+            val = value(node)
+            self.val_dict[node] = val
+            if node not in self.der_dict:
+                self.der_dict[node] = 0
+            return True, val
+
         if not node.is_expression_type():
             val = value(node)
             self.val_dict[node] = val
@@ -488,6 +519,18 @@ def visiting_potential_leaf(self, node):
                 self.der_dict[node] = 0
             return True, node
 
+        if node.__class__ is _expr.LinearExpression:
+            for v in node.linear_vars + node.linear_coefs + [node.constant]:
+                val = v
+                self.val_dict[v] = val
+                if v not in self.der_dict:
+                    self.der_dict[v] = 0
+            val = node
+            self.val_dict[node] = val
+            if node not in self.der_dict:
+                self.der_dict[node] = 0
+            return True, val
+
         if not node.is_expression_type():
             val = node
             self.val_dict[node] = val


Test patch:
diff --git a/pyomo/core/tests/unit/test_derivs.py b/pyomo/core/tests/unit/test_derivs.py
--- a/pyomo/core/tests/unit/test_derivs.py
+++ b/pyomo/core/tests/unit/test_derivs.py
@@ -2,6 +2,7 @@
 import pyomo.environ as pe
 from pyomo.core.expr.calculus.diff_with_pyomo import reverse_ad, reverse_sd
 from pyomo.common.getGSL import find_GSL
+from pyomo.core.expr.numeric_expr import LinearExpression
 
 
 tol = 6
@@ -220,3 +221,25 @@ def test_external(self):
         derivs = reverse_ad(e)
         self.assertAlmostEqual(derivs[m.x], approx_deriv(e, m.x), tol)
         self.assertAlmostEqual(derivs[m.y], approx_deriv(e, m.y), tol)
+
+    def test_linear_expression(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var(initialize=2.0)
+        m.y = pe.Var(initialize=3.0)
+        m.p = pe.Param(initialize=2.5, mutable=True)
+        e = LinearExpression(constant=m.p, linear_vars=[m.x, m.y], linear_coefs=[1.8, m.p])
+        e = pe.log(e)
+        derivs = reverse_ad(e)
+        symbolic = reverse_sd(e)
+        for v in [m.x, m.y, m.p]:
+            self.assertAlmostEqual(derivs[v], pe.value(symbolic[v]), tol)
+            self.assertAlmostEqual(derivs[v], approx_deriv(e, v), tol)
+
+    def test_NPV(self):
+        m = pe.ConcreteModel()
+        m.p = pe.Param(initialize=2.0, mutable=True)
+        e = pe.log(m.p)
+        derivs = reverse_ad(e)
+        symbolic = reverse_sd(e)
+        self.assertAlmostEqual(derivs[m.p], pe.value(symbolic[m.p]), tol)
+        self.assertAlmostEqual(derivs[m.p], approx_deriv(e, m.p), tol)