Repository: Pyomo/pyomo

Issue:
Pyomo AD fails with constant Expressions
## Summary

Pyomo AD raises an error when encountering `Expressions` with a constant value.

### Steps to reproduce the issue

```console
import pyomo.environ as pyo
from pyomo.core.expr.calculus.derivatives import Modes, differentiate

m = pyo.ConcreteModel()

m.x = pyo.Var(units=pyo.units.mol, initialize=0)

@m.Expression()
def y(blk):
    return 2

@m.Expression()
def product(blk):
    return blk.x * blk.y

diff_expr = differentiate(
                expr=m.product,
                wrt=m.x,
                mode=Modes.reverse_symbolic
            )
diff_expr.pprint()
```

### Error Message

```console
Exception has occurred: KeyError
"Component with id '1907001786672': 2.0"
  File "C:\Users\[REDACTED]\Repos\pyomo\pyomo\common\collections\component_map.py", line 71, in __getitem__
    return self._dict[id(obj)][1]
KeyError: 1907001786672

During handling of the above exception, another exception occurred:

  File "C:\Users\[REDACTED]\Repos\pyomo\pyomo\common\collections\component_map.py", line 73, in __getitem__
    raise KeyError("Component with id '%s': %s" % (id(obj), str(obj)))
  File "C:\Users\[REDACTED]\Repos\pyomo\pyomo\core\expr\calculus\diff_with_pyomo.py", line 331, in _diff_GeneralExpression
    der_dict[node.expr] += der_dict[node]
  File "C:\Users\[REDACTED]\Repos\pyomo\pyomo\core\expr\calculus\diff_with_pyomo.py", line 442, in _reverse_diff_helper
    _diff_GeneralExpression(e, val_dict, der_dict)
  File "C:\Users\[REDACTED]\Repos\pyomo\pyomo\core\expr\calculus\diff_with_pyomo.py", line 484, in reverse_sd
    return _reverse_diff_helper(expr, False)
  File "C:\Users\[REDACTED]\Repos\pyomo\pyomo\core\expr\calculus\derivatives.py", line 96, in differentiate
    res = reverse_sd(expr=expr)
  File "C:\Users\[REDACTED]\Desktop\diff_expr.py", line 16, in <module>
    diff_expr = differentiate(
KeyError: "Component with id '1907001786672': 2.0"
```

### Information on your system
Pyomo version: 6.6.2
Python version: 3.10.10 | packaged by Anaconda, Inc. | (main, Mar 21 2023, 18:39:17) [MSC v.1916 64 bit (AMD64)]
Operating system: Windows 10
How Pyomo was installed (PyPI, conda, source): From sources
Solver (if applicable): n/a



Discussion:
@dallan-keylogic  - please provide this information:

### Information on your system

Pyomo version:
Python version:
Operating system:
How Pyomo was installed (PyPI, conda, source):
Solver (if applicable):
@mrmundt Added to the main issue.
@michaelbynum , any idea when this is going to be fixed? I want to use AD for calculating excess enthalpy as part of the IDAES eNRTL models.
Previously, I was able to work around this issue by replacing every "0" in my code with a constant zero `Param` with appropriate units. Unfortunately, I ran into this issue again today because I want to use the eNRTL property package without ions (because I don't want to code up a separate NRTL property package), and the sum over an empty set evaluates to zero. My attempt to fix this by adding a dimensionless zero to the sum doesn't appear to be working.

```
        dimensionless_zero = Param(initialize=0.0, mutable=False, doc="Dimensionless zero to avoid issue with AD (Pyomo 6.6.2)")
        b.add_component(
            pname + "_dimensionless_zero",
            dimensionless_zero,
        )
        # Ionic Strength
        def rule_I(b):  # Eqn 62
            dimensionless_zero = getattr(b, pname + "_dimensionless_zero")
            return 0.5 * sum(
                b.mole_frac_phase_comp_true[pname, c]
                * b.params.get_component(c).config.charge ** 2
                for c in b.params.ion_set
            ) + dimensionless_zero # TODO get rid of this once Pyomo fixes their AD
```

> b.Liq_ionic_strength.expr
> <pyomo.core.expr.numvalue.NumericConstant object at 0x00000225F7B5E860>

It would be nice to have a fix for this.

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
@@ -328,7 +328,7 @@ def _diff_GeneralExpression(node, val_dict, der_dict):
     val_dict: ComponentMap
     der_dict: ComponentMap
     """
-    der_dict[node.expr] += der_dict[node]
+    der_dict[node.arg(0)] += der_dict[node]
 
 
 def _diff_ExternalFunctionExpression(node, val_dict, der_dict):


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
@@ -230,6 +230,17 @@ def e2(m, i):
         symbolic = reverse_sd(m.o.expr)
         self.assertAlmostEqual(derivs[m.x], pyo.value(symbolic[m.x]), tol)
 
+    def test_constant_named_expressions(self):
+        m = pyo.ConcreteModel()
+        m.x = pyo.Var(initialize=3)
+        m.e = pyo.Expression(expr=2)
+
+        e = m.x * m.e
+        derivs = reverse_ad(e)
+        symbolic = reverse_sd(e)
+        self.assertAlmostEqual(derivs[m.x], pyo.value(symbolic[m.x]), tol + 3)
+        self.assertAlmostEqual(derivs[m.x], approx_deriv(e, m.x), tol)
+
     def test_multiple_named_expressions(self):
         m = pyo.ConcreteModel()
         m.x = pyo.Var()