Repository: Pyomo/pyomo

Issue:
FBBT of squares written as multiplications
## Summary

This isn't a bug in the sense that it's mathematically wrong, but it seems not ideal: fbbt computes different bounds on m.x*m.x than it does on m.x**2.

### Steps to reproduce the issue

```
from pyomo.environ import *
from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr

m = ConcreteModel()
m.x = Var([1,2], bounds=(-2,6))

print(compute_bounds_on_expr(m.x[1]*m.x[1] + m.x[2]*m.x[2]))
print(compute_bounds_on_expr(m.x[1]**2 + m.x[2]**2))
```
 outputs
```
(-24, 72)
(0, 72)
```

### Information on your system

Pyomo version: main branch
Python version: 3.6
Operating system: linux
How Pyomo was installed (PyPI, conda, source): source
Solver (if applicable):


### Additional information

<!-- If you have any additional information, please list it here. -->


<!-- We encourage you to try, as much as possible, to reduce your problem to the minimal example that still reproduces the issue. That would help us a lot in fixing it quickly and effectively!

If you want to ask a question (how to use Pyomo, what it can do, etc.), please refer to our [online documentation](https://github.com/Pyomo/pyomo#getting-help) on ways to get help. -->




Discussion:

Resolving patch:
diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py
--- a/pyomo/contrib/fbbt/fbbt.py
+++ b/pyomo/contrib/fbbt/fbbt.py
@@ -83,7 +83,10 @@ def _prop_bnds_leaf_to_root_ProductExpression(node, bnds_dict, feasibility_tol):
     arg1, arg2 = node.args
     lb1, ub1 = bnds_dict[arg1]
     lb2, ub2 = bnds_dict[arg2]
-    bnds_dict[node] = interval.mul(lb1, ub1, lb2, ub2)
+    if arg1 is arg2:
+        bnds_dict[node] = interval.power(lb1, ub1, 2, 2, feasibility_tol)
+    else:
+        bnds_dict[node] = interval.mul(lb1, ub1, lb2, ub2)
 
 
 def _prop_bnds_leaf_to_root_SumExpression(node, bnds_dict, feasibility_tol):
@@ -498,8 +501,12 @@ def _prop_bnds_root_to_leaf_ProductExpression(node, bnds_dict, feasibility_tol):
     lb0, ub0 = bnds_dict[node]
     lb1, ub1 = bnds_dict[arg1]
     lb2, ub2 = bnds_dict[arg2]
-    _lb1, _ub1 = interval.div(lb0, ub0, lb2, ub2, feasibility_tol)
-    _lb2, _ub2 = interval.div(lb0, ub0, lb1, ub1, feasibility_tol)
+    if arg1 is arg2:
+        _lb1, _ub1 = interval._inverse_power1(lb0, ub0, 2, 2, orig_xl=lb1, orig_xu=ub1, feasibility_tol=feasibility_tol)
+        _lb2, _ub2 = _lb1, _ub1
+    else:
+        _lb1, _ub1 = interval.div(lb0, ub0, lb2, ub2, feasibility_tol)
+        _lb2, _ub2 = interval.div(lb0, ub0, lb1, ub1, feasibility_tol)
     if _lb1 > lb1:
         lb1 = _lb1
     if _ub1 < ub1:


Test patch:
diff --git a/pyomo/contrib/fbbt/tests/test_fbbt.py b/pyomo/contrib/fbbt/tests/test_fbbt.py
--- a/pyomo/contrib/fbbt/tests/test_fbbt.py
+++ b/pyomo/contrib/fbbt/tests/test_fbbt.py
@@ -831,3 +831,23 @@ def test_linear_expression(self):
         fbbt(m.c)
         self.assertAlmostEqual(m.y.lb, -1.5)
         self.assertAlmostEqual(m.y.ub, -1)
+
+    def test_quadratic_as_product(self):
+        m = pyo.ConcreteModel()
+        m.x = pyo.Var([1, 2], bounds=(-2, 6))
+
+        e1 = m.x[1]*m.x[1] + m.x[2]*m.x[2]
+        e2 = m.x[1]**2 + m.x[2]**2
+
+        lb1, ub1 = compute_bounds_on_expr(e1)
+        lb2, ub2 = compute_bounds_on_expr(e2)
+
+        self.assertAlmostEqual(lb1, lb2)
+        self.assertAlmostEqual(ub1, ub2)
+
+        m.c = pyo.Constraint(expr=m.x[1]*m.x[1] + m.x[2]*m.x[2] == 0)
+        fbbt(m.c)
+        self.assertAlmostEqual(m.x[1].lb, 0)
+        self.assertAlmostEqual(m.x[1].ub, 0)
+        self.assertAlmostEqual(m.x[2].lb, 0)
+        self.assertAlmostEqual(m.x[2].ub, 0)