Repository: Pyomo/pyomo

Issue:
When there are brackets in the expression, such as (a+b)/2, there will be a bug during the parsing process using cplex
## Summary

when I find the lp files are not same when call pe.SolverFactory("cplex") and pe.SolverFactory("cplex_direct"), I try to use pe.SolverFactory("cplex_persistent") to generate the model lp file,

### Steps to reproduce the issue

```
# example.py
import pyomo.environ as pe 
import pandas as pd
import numpy as np
model = pe.ConcreteModel(name="test")
model.aSet = pe.RangeSet(5)
model.a = pe.Var(model.aSet, domain=pe.Binary)
model.x = pe.Var(model.aSet, domain=pe.NonNegativeReals, bounds=(0,1000))
coff = [2,3,4,5,6]
profit = sum([coff[i]*model.a[i+1]*model.x[i+1] for i in range(5)]) /2 + 4500
model.profit = pe.Objective(expr=profit, sense=pe.minimize)
model.c1 = pe.Constraint(expr=model.x[1]+model.x[2]<=1500)

model.write("a1.lp",io_options={"symbolic_solver_labels": True})

# case 1 
opt = pe.SolverFactory('cplex_persistent')
opt.set_instance(model,symbolic_solver_labels=True)
opt.write("a2.lp")

```

### Error Message
#### a1.lp
```
min 
profit:
+ [
+2 a(1) * x(1)
+3 a(2) * x(2)
+4 a(3) * x(3)
+5 a(4) * x(4)
+6 a(5) * x(5)
] / 2
+4500 ONE_VAR_CONSTANT
...
```
#### a2.lp
```
Minimize
 obj: 0 a(1) + 0 a(2) + 0 a(3) + 0 a(4) + 0 a(5) + 0 x(3) + 0 x(4) + 0 x(5)
      + [ 4 a(1) * x(1) + 6 a(2) * x(2) + 8 a(3) * x(3) + 10 a(4) * x(4)
      + 12 a(5) * x(5) ] / 2 + 4500
```
the coff of a(1) * x(1) are: 2 , 4 

### Information on your system

Pyomo version: 6.6.2
Python version: 3.10.12
Operating system:
How Pyomo was installed (PyPI, conda, source): pip
Solver (if applicable):
CPLEX(R) Interactive Optimizer 22.1.1.0






Discussion:

Resolving patch:
diff --git a/pyomo/solvers/plugins/solvers/cplex_direct.py b/pyomo/solvers/plugins/solvers/cplex_direct.py
--- a/pyomo/solvers/plugins/solvers/cplex_direct.py
+++ b/pyomo/solvers/plugins/solvers/cplex_direct.py
@@ -596,8 +596,13 @@ def _set_objective(self, obj):
         cplex_expr, referenced_vars = self._get_expr_from_pyomo_expr(
             obj.expr, self._max_obj_degree
         )
-        for i in range(len(cplex_expr.q_coefficients)):
-            cplex_expr.q_coefficients[i] *= 2
+        # CPLEX actually uses x'Qx/2 in the objective, as the
+        # off-diagonal entries appear in both the lower triangle and the
+        # upper triangle (i.e., c*x1*x2 and c*x2*x1).  However, since
+        # the diagonal entries only appear once, we need to double them.
+        for i, v1 in enumerate(cplex_expr.q_variables1):
+            if v1 == cplex_expr.q_variables2[i]:
+                cplex_expr.q_coefficients[i] *= 2
 
         for var in referenced_vars:
             self._referenced_variables[var] += 1


Test patch:
diff --git a/pyomo/solvers/tests/mip/test_qp.py b/pyomo/solvers/tests/mip/test_qp.py
new file mode 100644
--- /dev/null
+++ b/pyomo/solvers/tests/mip/test_qp.py
@@ -0,0 +1,194 @@
+#  ___________________________________________________________________________
+#
+#  Pyomo: Python Optimization Modeling Objects
+#  Copyright (c) 2008-2022
+#  National Technology and Engineering Solutions of Sandia, LLC
+#  Under the terms of Contract DE-NA0003525 with National Technology and
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+#  rights in this software.
+#  This software is distributed under the 3-clause BSD License.
+#  ___________________________________________________________________________
+#
+
+import pyomo.common.unittest as unittest
+
+from pyomo.environ import ConcreteModel, Var, Objective, ConstraintList, SolverFactory
+
+gurobi_lp = SolverFactory('gurobi', solver_io='lp')
+gurobi_nl = SolverFactory('gurobi', solver_io='nl')
+gurobi_direct = SolverFactory('gurobi_direct')
+gurobi_persistent = SolverFactory('gurobi_persistent')
+gurobi_appsi = SolverFactory('appsi_gurobi')
+
+cplex_lp = SolverFactory('cplex', solver_io='lp')
+cplex_nl = SolverFactory('cplex', solver_io='nl')
+cplex_direct = SolverFactory('cplex_direct')
+cplex_persistent = SolverFactory('cplex_persistent')
+cplex_appsi = SolverFactory('appsi_cplex')
+
+xpress_lp = SolverFactory('xpress', solver_io='lp')
+xpress_nl = SolverFactory('xpress', solver_io='nl')
+xpress_direct = SolverFactory('xpress_direct')
+xpress_persistent = SolverFactory('xpress_persistent')
+xpress_appsi = SolverFactory('appsi_xpress')
+
+
+class TestQuadraticModels(unittest.TestCase):
+    def _qp_model(self):
+        m = ConcreteModel(name="test")
+        m.x = Var([0, 1, 2])
+        m.obj = Objective(
+            expr=m.x[0]
+            + 10 * m.x[1]
+            + 100 * m.x[2]
+            + 1000 * m.x[1] * m.x[2]
+            + 10000 * m.x[0] ** 2
+            + 10000 * m.x[1] ** 2
+            + 100000 * m.x[2] ** 2
+        )
+        m.c = ConstraintList()
+        m.c.add(m.x[0] == 1)
+        m.c.add(m.x[1] == 2)
+        m.c.add(m.x[2] == 4)
+        return m
+
+    @unittest.skipUnless(
+        gurobi_lp.available(exception_flag=False), "needs Gurobi LP interface"
+    )
+    def test_qp_objective_gurobi_lp(self):
+        m = self._qp_model()
+        results = gurobi_lp.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        gurobi_nl.available(exception_flag=False), "needs Gurobi NL interface"
+    )
+    def test_qp_objective_gurobi_nl(self):
+        m = self._qp_model()
+        results = gurobi_nl.solve(m)
+        # TODO: the NL interface should set either the Upper or Lower
+        # bound for feasible solutions!
+        #
+        # self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+        self.assertIn(str(int(m.obj())), results['Solver'][0]['Message'])
+
+    @unittest.skipUnless(
+        gurobi_appsi.available(exception_flag=False), "needs Gurobi APPSI interface"
+    )
+    def test_qp_objective_gurobi_appsi(self):
+        m = self._qp_model()
+        gurobi_appsi.set_instance(m)
+        results = gurobi_appsi.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        gurobi_direct.available(exception_flag=False), "needs Gurobi Direct interface"
+    )
+    def test_qp_objective_gurobi_direct(self):
+        m = self._qp_model()
+        results = gurobi_direct.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        gurobi_persistent.available(exception_flag=False),
+        "needs Gurobi Persistent interface",
+    )
+    def test_qp_objective_gurobi_persistent(self):
+        m = self._qp_model()
+        gurobi_persistent.set_instance(m)
+        results = gurobi_persistent.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        cplex_lp.available(exception_flag=False), "needs Cplex LP interface"
+    )
+    def test_qp_objective_cplex_lp(self):
+        m = self._qp_model()
+        results = cplex_lp.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        cplex_nl.available(exception_flag=False), "needs Cplex NL interface"
+    )
+    def test_qp_objective_cplex_nl(self):
+        m = self._qp_model()
+        results = cplex_nl.solve(m)
+        # TODO: the NL interface should set either the Upper or Lower
+        # bound for feasible solutions!
+        #
+        # self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+        self.assertIn(str(int(m.obj())), results['Solver'][0]['Message'])
+
+    @unittest.skipUnless(
+        cplex_direct.available(exception_flag=False), "needs Cplex Direct interface"
+    )
+    def test_qp_objective_cplex_direct(self):
+        m = self._qp_model()
+        results = cplex_direct.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        cplex_persistent.available(exception_flag=False),
+        "needs Cplex Persistent interface",
+    )
+    def test_qp_objective_cplex_persistent(self):
+        m = self._qp_model()
+        cplex_persistent.set_instance(m)
+        results = cplex_persistent.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        cplex_appsi.available(exception_flag=False), "needs Cplex APPSI interface"
+    )
+    def test_qp_objective_cplex_appsi(self):
+        m = self._qp_model()
+        cplex_appsi.set_instance(m)
+        results = cplex_appsi.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        xpress_lp.available(exception_flag=False), "needs Xpress LP interface"
+    )
+    def test_qp_objective_xpress_lp(self):
+        m = self._qp_model()
+        results = xpress_lp.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        xpress_nl.available(exception_flag=False), "needs Xpress NL interface"
+    )
+    def test_qp_objective_xpress_nl(self):
+        m = self._qp_model()
+        results = xpress_nl.solve(m)
+        # TODO: the NL interface should set either the Upper or Lower
+        # bound for feasible solutions!
+        #
+        # self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+        self.assertIn(str(int(m.obj())), results['Solver'][0]['Message'])
+
+    @unittest.skipUnless(
+        xpress_direct.available(exception_flag=False), "needs Xpress Direct interface"
+    )
+    def test_qp_objective_xpress_direct(self):
+        m = self._qp_model()
+        results = xpress_direct.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        xpress_persistent.available(exception_flag=False),
+        "needs Xpress Persistent interface",
+    )
+    def test_qp_objective_xpress_persistent(self):
+        m = self._qp_model()
+        xpress_persistent.set_instance(m)
+        results = xpress_persistent.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])
+
+    @unittest.skipUnless(
+        xpress_appsi.available(exception_flag=False), "needs Xpress APPSI interface"
+    )
+    def test_qp_objective_xpress_appsi(self):
+        m = self._qp_model()
+        xpress_appsi.set_instance(m)
+        results = xpress_appsi.solve(m)
+        self.assertEqual(m.obj(), results['Problem'][0]['Upper bound'])