Repository: Pyomo/pyomo

Issue:
'quicksum' hides exceptions raised in generator from the user, leading to a wrong model
## Summary
If the generator passed to `quicksum` raises an exception at the first call to __next__, the exception handling logic of `quicksum` will hide the exception from the user and return an expression as `0`.  This behavior of `quicksum` may produce a wrong model unintentionally.

The example below shows that the user misspells `param.aaa` as `param.bbb` in the expression,  in fact it is a non-exsiting attribute of the class. However, the code can run fluently without any exception occurs.  

### Steps to reproduce the issue

```python
import pyomo.environ as pyo

class Param:
    def __init__(self) -> None:
        self.aaa = [1.1, 2.2, 3.3]

param = Param()

m = pyo.ConcreteModel()
m.i = pyo.RangeSet(0, 3 - 1)
m.x = pyo.Var(m.i)

m.expr = pyo.Expression(expr=pyo.quicksum(m.x[i] * param.aaa[i] for i in m.i))
m.expr.pprint()

m.expr2 = pyo.Expression(expr=pyo.quicksum(m.x[i] * param.bbb[i] for i in m.i))
m.expr2.pprint()
```
### Error Message
```console
expr : Size=1, Index=None
    Key  : Expression
    None : 1.1*x[0] + 2.2*x[1] + 3.3*x[2]
expr2 : Size=1, Index=None
    Key  : Expression
    None :        0.0
```


### Information on your system

Pyomo version: 6.4.1
Python version: 3.9.12
Operating system: Windows 11
How Pyomo was installed (PyPI, conda, source): conda




Discussion:

Resolving patch:
diff --git a/pyomo/core/util.py b/pyomo/core/util.py
--- a/pyomo/core/util.py
+++ b/pyomo/core/util.py
@@ -21,6 +21,8 @@
 from pyomo.core.base.var import Var
 from pyomo.core.base.expression import Expression
 from pyomo.core.base.component import _ComponentBase
+import logging
+logger = logging.getLogger(__name__)
 
 def prod(terms):
     """
@@ -66,6 +68,14 @@ def quicksum(args, start=0, linear=None):
     Returns:
         The value of the sum, which may be a Pyomo expression object.
     """
+
+    # Ensure that args is an iterator (this manages things like IndexedComponent_slice objects)
+    try:
+        args = iter(args)
+    except:
+        logger.error('The argument `args` to quicksum() is not iterable!')
+        raise
+
     #
     # If we're starting with a numeric value, then 
     # create a new nonlinear sum expression but 
@@ -76,14 +86,7 @@ def quicksum(args, start=0, linear=None):
             #
             # Get the first term, which we will test for linearity
             #
-            try:
-                first = next(args, None)
-            except:
-                try:
-                    args = args.__iter__()
-                    first = next(args, None)
-                except:
-                    raise RuntimeError("The argument to quicksum() is not iterable!")
+            first = next(args, None)
             if first is None:
                 return start
             #


Test patch:
diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py
--- a/pyomo/core/tests/unit/test_numeric_expr.py
+++ b/pyomo/core/tests/unit/test_numeric_expr.py
@@ -3677,6 +3677,32 @@ def test_summation7(self):
         e = quicksum(self.m.p[i]*self.m.q[i] for i in self.m.I)
         self.assertEqual( e(), 15 )
         self.assertIs(type(e), SumExpression)
+        
+    def test_quicksum_reject_noniterable(self):
+        with LoggingIntercept() as LOG:
+            with self.assertRaisesRegex(TypeError, "'int' object is not iterable"):
+                quicksum(1)
+        self.assertEqual(LOG.getvalue(), 'The argument `args` to quicksum() is not iterable!\n')
+
+    def test_quicksum_exception_exposure(self):
+        ex0 = Exception()
+        def f(): raise ex0
+
+        with self.assertRaises(Exception) as cm:
+            quicksum((f() for i in [1, 2, 3]), linear=None)
+        self.assertIs(cm.exception, ex0)
+
+        with self.assertRaises(Exception) as cm:
+            quicksum((f() for i in [1, 2, 3]), linear=True)
+        self.assertIs(cm.exception, ex0)
+
+        with self.assertRaises(Exception) as cm:
+            quicksum((f() for i in [1, 2, 3]), linear=False)
+        self.assertIs(cm.exception, ex0)
+
+        with self.assertRaises(Exception) as cm:
+            quicksum((f() for i in [1, 2, 3]), start=self.m.a[1])
+        self.assertIs(cm.exception, ex0)
 
 
 class TestCloneExpression(unittest.TestCase):