Repository: Pyomo/pyomo

Issue:
`generate_standard_repn` silently fails to capture nonlinear expression
## Summary

In the example below, `generate_standard_repn` fails to record the nonlinear expression at all.

### Steps to reproduce the issue

```
from pyomo.environ import *
from pyomo.repn import generate_standard_repn

m = ConcreteModel()
m.x = Var()
m.y = Var()

expr = (m.x + m.y)*(m.x - m.y)*(m.x**2 + m.y**2)
repn = generate_standard_repn(expr)
print(repn)
```

### Error Message

Output from the script above:
```
constant:       0
linear vars:    []
linear var ids: []
linear coef:    []
quadratic vars:    []
quadratic var ids: []
quadratic coef:    []
nonlinear expr: None
nonlinear vars: []
```

### Information on your system

Pyomo version: main branch
Python version: 3.6.5
Operating system: linux
How Pyomo was installed (PyPI, conda, source): source
Solver (if applicable):


Discussion:
Hi, 

This expression is not `quadratic`, to solve this issue, you can set `quadratic` param at `False`:
```python
expr = (m.x + m.y)*(m.x - m.y)*(m.x**2 + m.y**2)
repn = generate_standard_repn(expr, quadratic=False)
```

Output is :  
```
nonlinear expr:
(x + y)*(x - y)*(x**2 + y**2)
nonlinear vars: ['x', 'y']
```

However, if the user does not indicate that the problem is not quadratic, the user has to get an error or a warning : 
- raise an Error which tells the user to set quadratic to False
- or have an output like below : 
```
nonlinear expr:
(x + y)*(x - y)*(x**2 + y**2)
nonlinear vars: ['x', 'y']

WARNING: The problem is not quadratic
```


Resolving patch:
diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py
--- a/pyomo/repn/standard_repn.py
+++ b/pyomo/repn/standard_repn.py
@@ -577,11 +577,14 @@ def _collect_prod(exp, multiplier, idMap, compute_values, verbose, quadratic):
     #
     if not lhs_nonl_None or not rhs_nonl_None:
         return Results(nonl=multiplier*exp)
+
+    # If the resulting expression has a polynomial degree greater than 2
+    # (1 if quadratic is False), then simply return this as a general
+    # nonlinear expression
     #
-    # If not collecting quadratic terms and both terms are linear, then simply return the nonlinear expression
-    #
-    if not quadratic and len(lhs.linear) > 0 and len(rhs.linear) > 0:
-        # NOTE: We treat a product of linear terms as nonlinear unless quadratic is True
+    if ( max(1 if lhs.linear else 0, 2 if quadratic and lhs.quadratic else 0) +
+         max(1 if rhs.linear else 0, 2 if quadratic and rhs.quadratic else 0)
+         > (2 if quadratic else 1) ):
         return Results(nonl=multiplier*exp)
 
     ans = Results()


Test patch:
diff --git a/pyomo/repn/tests/test_standard.py b/pyomo/repn/tests/test_standard.py
--- a/pyomo/repn/tests/test_standard.py
+++ b/pyomo/repn/tests/test_standard.py
@@ -2661,16 +2661,15 @@ def test_quadratic2(self):
         #
         self.assertTrue(len(rep.linear_vars) == 0)
         self.assertTrue(len(rep.linear_coefs) == 0)
-        self.assertTrue(len(rep.quadratic_vars) == 1)
-        self.assertTrue(len(rep.quadratic_coefs) == 1)
+        self.assertTrue(len(rep.quadratic_vars) == 0)
+        self.assertTrue(len(rep.quadratic_coefs) == 0)
         self.assertFalse(rep.nonlinear_expr is None)
         self.assertEqual(len(rep.nonlinear_vars), 3)
-        baseline = { bc_key:5 }
+        baseline = {}
         self.assertEqual(baseline, repn_to_dict(rep))
         s = pickle.dumps(rep)
         rep = pickle.loads(s)
-        bc_key_ = (id(rep.quadratic_vars[0][0]),id(rep.quadratic_vars[0][1])) if id(rep.quadratic_vars[0][0]) <= id(rep.quadratic_vars[0][1]) else (id(rep.quadratic_vars[0][1]),id(rep.quadratic_vars[0][0]))
-        baseline = { bc_key_:5 }
+        baseline = {}
         self.assertEqual(baseline, repn_to_dict(rep))
 
         # Do not collect quadratics
@@ -3928,19 +3927,19 @@ def test_product4(self):
 
         e = (1 + m.v + m.w + m.v**2)*(m.v + m.w + m.v**2)
         rep = generate_standard_repn(e, compute_values=True)
-        self.assertEqual(str(rep.to_expression()), "v + w + 2*v**2 + 2*(v*w) + w**2 + (v + w)*(v*v) + v*v*(v + w)")
+        self.assertEqual(str(rep.to_expression()), "(1 + v + w + v**2)*(v + w + v**2)")
         rep = generate_standard_repn(e, compute_values=True, quadratic=False)
         self.assertEqual(str(rep.to_expression()), "(1 + v + w + v**2)*(v + w + v**2)")
 
         e = (m.v + m.w + m.v**2)*(1 + m.v + m.w + m.v**2)
         rep = generate_standard_repn(e, compute_values=True)
-        self.assertEqual(str(rep.to_expression()), "v + w + 2*v**2 + 2*(v*w) + w**2 + (v + w)*(v*v) + v*v*(v + w)")
+        self.assertEqual(str(rep.to_expression()), "(v + w + v**2)*(1 + v + w + v**2)")
         rep = generate_standard_repn(e, compute_values=True, quadratic=False)
         self.assertEqual(str(rep.to_expression()), "(v + w + v**2)*(1 + v + w + v**2)")
 
         e = (1 + m.v + m.w + m.v**2)*(1 + m.v + m.w + m.v**2)
         rep = generate_standard_repn(e, compute_values=True)
-        self.assertEqual(str(rep.to_expression()), "1 + 2*v + 2*w + 3*v**2 + 2*(v*w) + w**2 + (v + w)*(v*v) + v*v*(v + w)")
+        self.assertEqual(str(rep.to_expression()), "(1 + v + w + v**2)*(1 + v + w + v**2)")
         rep = generate_standard_repn(e, compute_values=True, quadratic=False)
         self.assertEqual(str(rep.to_expression()), "(1 + v + w + v**2)*(1 + v + w + v**2)")
 
@@ -3955,6 +3954,17 @@ def test_product5(self):
         rep = generate_standard_repn(e, compute_values=True, quadratic=False)
         self.assertEqual(str(rep.to_expression()), "(1 + v)*(1 + v)")
 
+    def test_product6(self):
+        m = ConcreteModel()
+        m.x = Var()
+        m.y = Var()
+
+        e = (m.x + m.y) * (m.x - m.y) * (m.x ** 2 + m.y ** 2)
+        rep = generate_standard_repn(e)
+        self.assertEqual(str(rep.to_expression()), "(x + y)*(x - y)*(x**2 + y**2)")
+        self.assertTrue(rep.is_nonlinear())
+        self.assertFalse(rep.is_quadratic())
+
     def test_vars(self): 
         m = ConcreteModel()
         m.v = Var(initialize=2)