Repository: Pyomo/pyomo

Issue:
Using relational Expression in Objective
The following model is sent to a solver without any warning or error:

    from pyomo.environ import *
    m = ConcreteModel()
    m.v = Var()
    m.e = Expression(expr=m.v**2 + 2 >= 10)
    m.obj = Objective(expr=m.e)

While the user should be able to construct this model, the writers should be checking `is_relational` on the Expression object before the model is sent to a solver.


Discussion:
I agree that the is_relational check probably needs to go into the writers either way. However, why not just disallow storing relational expressions in the Expression object as well? It seems to be a common mistake that users try to do:
```python
m.e = Expression(expr= m.x <= 1)
m.c = Constraint(expr= m.e)
```
@ghackebeil I completely agree, in fact I always assumed that Expressions could not hold relational expressions until today when @jsiirola told me otherwise. Does anyone have any strong use-cases for allowing relational expressions in Expression components?
In fact, even leaving Expression out of this, we still don't check if a relational expression was passed to Objective. The following is sent to a solver with no error:

    from pyomo.environ import *
    m = ConcreteModel()
    m.v = Var()
    m.obj = Objective(expr=m.v**2>=10)

It looks like a check for is_relational is missing just for the nl writer, for the linear case an error is thrown in `generate_canonical_repn`.
That's a good catch. I believe Objective is a subclass of Expression though, so disallowing it there would also cover this second case.

Since this works with the NL writer (as in, there is a legitimate way to express this using symbols recognized by the Ampl Solver Library, and we do that), I guess we have to ask ourselves if there are any solvers / users making use of this. If so, we might need to leave it in.
I am not in favor of disallowing relational (logical) expressions in Expression components.  One of the things we would like to do is add explicit support for logical expressions (first for GDP, then to support Constraint Programming modeling approaches). In those contexts I could imagine putting a logical expression in an Expression would be useful. 
@jsiirola: Are you in favor of at least temporarily disallowing it until someone actually adds this kind of functionality? I imagine the difference will be 1-2 lines of code in the Expression file.
@blnicho @jsiirola @ghackebeil - I'm going through old bug reports. Is this bug still present in the most recent Pyomo version?
@mrmundt Yes, this still exists, and I now disagree with my comment from 4 years ago (that was before we finished thinking through / implementing the LogicalExpression system).  We should definitely disallow relational expressions in `Expression` components.  This is actually part of a larger issue that relational expressions should be moved out of the Numeric expression hierarchy and into the Logical expression hierarchy.

Resolving patch:
diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py
--- a/pyomo/core/base/expression.py
+++ b/pyomo/core/base/expression.py
@@ -187,6 +187,11 @@ def set_value(self, expr):
             self._expr = None
             return
         expr = as_numeric(expr)
+        if not expr.is_numeric_type():
+            raise ValueError(
+                f"Cannot assign {expr.__class__.__name__} to "
+                f"'{self.name}': {self.__class__.__name__} components only "
+                "allow numeric expression types.")
         # In-place operators will leave self as an argument.  We need to
         # replace that with the current expression in order to avoid
         # loops in the expression tree.


Test patch:
diff --git a/pyomo/core/tests/unit/test_obj.py b/pyomo/core/tests/unit/test_obj.py
--- a/pyomo/core/tests/unit/test_obj.py
+++ b/pyomo/core/tests/unit/test_obj.py
@@ -45,6 +45,15 @@ def test_singleton_get_set_value(self):
         model.o.expr += 2
         self.assertEqual(model.o.expr(), 4)
 
+    def test_scalar_invalid_expr(self):
+        m = ConcreteModel()
+        m.x = Var()
+        with self.assertRaisesRegex(
+                ValueError, "Cannot assign InequalityExpression to 'obj': "
+                "ScalarObjective components only allow numeric expression "
+                "types."):
+            m.obj = Objective(expr=m.x <= 0)
+
     def test_empty_singleton(self):
         a = Objective()
         a.construct()
diff --git a/pyomo/core/tests/unit/test_pickle.py b/pyomo/core/tests/unit/test_pickle.py
--- a/pyomo/core/tests/unit/test_pickle.py
+++ b/pyomo/core/tests/unit/test_pickle.py
@@ -227,7 +227,7 @@ def test_pickle_concrete_model_constant_objective(self):
     def test_pickle_concrete_model_objective(self):
         model = ConcreteModel()
         model.x = Var()
-        model.A = Objective(expr=model.x <= 0)
+        model.A = Objective(expr=model.x**2)
         str = pickle.dumps(model)
         tmodel = pickle.loads(str)
         self.verifyModel(model, tmodel)