Repository: Pyomo/pyomo

Issue:
Bug in scaling transformation when working with large models
Hello there,

I've found the following bug which breaks the scaling transformation for any model of non-trivial size.

### The bug

When scaling the model, the transformation plugin calls `c.set_value((lower_bound, constraint_body, upper_bound))` as shown below.

https://github.com/Pyomo/pyomo/blob/e6ed67e23e58ad8f0634c1b9a757037dbd3eaa79/pyomo/core/plugins/transform/scaling.py#L204

The bug happens when we have an equality (the lower and upper bounds are equal). So let's say the scaling transformation is calling `c.set_value((10.0, some_expression, 10.0))`.

Now let's look at the code inside `set_value(self, expr)` in `constraint.py`. If `expr` is a tuple of length 3 (which it is when using the scaling transformation) then the following block of code is called.

https://github.com/Pyomo/pyomo/blob/e6ed67e23e58ad8f0634c1b9a757037dbd3eaa79/pyomo/core/base/constraint.py#L436-L465

What's important is this block essentially does this:
```
self._lower = as_numeric(10.0)
self._upper = as_numeric(10.0)
```
Now, `as_numeric(obj)` returns an instance of `pyomo.core.expr.numvalue.NumericConstant`.  It uses a cache to save on memory so if it's called twice with the same value for `obj`, it returns the same instance. However, the cache is limited to `1024` elements, after which every call to `as_numeric()` returns a different instance of `NumericConstant`. (see [here](https://github.com/Pyomo/pyomo/blob/e6ed67e23e58ad8f0634c1b9a757037dbd3eaa79/pyomo/core/expr/numvalue.py#L396))

The issue is later in `set_value()` we have the following assertion.

https://github.com/Pyomo/pyomo/blob/e6ed67e23e58ad8f0634c1b9a757037dbd3eaa79/pyomo/core/base/constraint.py#L687-L692

If we've exceeded the cache, then `self._lower is self._upper` will be false, even if they have the same numerical value, as they are different instances of `NumericConstant`.

This results in the assertion failing which is the error I'm getting.

```
AssertionError
> ~\.conda\envs\base\lib\site-packages\pyomo\core\base\constraint.py(614)set_value()
-> assert self._lower is self._upper
```

### Solution

I've created a pull request to fix this. My patch is to make the transformation plugin call `set_value()` with a tuple of length `2` when dealing with an equality since in that case `set_value()` properly handles the equality.

Another possible solution is to change the assertion to `self._lower.value == self._upper.value` however I don't know if this is better.
Bug in scaling transformation when working with large models
Hello there,

I've found the following bug which breaks the scaling transformation for any model of non-trivial size.

### The bug

When scaling the model, the transformation plugin calls `c.set_value((lower_bound, constraint_body, upper_bound))` as shown below.

https://github.com/Pyomo/pyomo/blob/e6ed67e23e58ad8f0634c1b9a757037dbd3eaa79/pyomo/core/plugins/transform/scaling.py#L204

The bug happens when we have an equality (the lower and upper bounds are equal). So let's say the scaling transformation is calling `c.set_value((10.0, some_expression, 10.0))`.

Now let's look at the code inside `set_value(self, expr)` in `constraint.py`. If `expr` is a tuple of length 3 (which it is when using the scaling transformation) then the following block of code is called.

https://github.com/Pyomo/pyomo/blob/e6ed67e23e58ad8f0634c1b9a757037dbd3eaa79/pyomo/core/base/constraint.py#L436-L465

What's important is this block essentially does this:
```
self._lower = as_numeric(10.0)
self._upper = as_numeric(10.0)
```
Now, `as_numeric(obj)` returns an instance of `pyomo.core.expr.numvalue.NumericConstant`.  It uses a cache to save on memory so if it's called twice with the same value for `obj`, it returns the same instance. However, the cache is limited to `1024` elements, after which every call to `as_numeric()` returns a different instance of `NumericConstant`. (see [here](https://github.com/Pyomo/pyomo/blob/e6ed67e23e58ad8f0634c1b9a757037dbd3eaa79/pyomo/core/expr/numvalue.py#L396))

The issue is later in `set_value()` we have the following assertion.

https://github.com/Pyomo/pyomo/blob/e6ed67e23e58ad8f0634c1b9a757037dbd3eaa79/pyomo/core/base/constraint.py#L687-L692

If we've exceeded the cache, then `self._lower is self._upper` will be false, even if they have the same numerical value, as they are different instances of `NumericConstant`.

This results in the assertion failing which is the error I'm getting.

```
AssertionError
> ~\.conda\envs\base\lib\site-packages\pyomo\core\base\constraint.py(614)set_value()
-> assert self._lower is self._upper
```

### Solution

I've created a pull request to fix this. My patch is to make the transformation plugin call `set_value()` with a tuple of length `2` when dealing with an equality since in that case `set_value()` properly handles the equality.

Another possible solution is to change the assertion to `self._lower.value == self._upper.value` however I don't know if this is better.


Discussion:

Resolving patch:
diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py
--- a/pyomo/core/plugins/transform/scaling.py
+++ b/pyomo/core/plugins/transform/scaling.py
@@ -188,20 +188,25 @@ def _apply_to(self, model, rename=True):
                                                remove_named_expressions=True)
 
                     # scale the rhs
-                    if c._lower is not None:
-                        c._lower = c._lower * scaling_factor
-                    if c._upper is not None:
-                        c._upper = c._upper * scaling_factor
+                    lower = c.lower
+                    upper = c.upper
+                    if lower is not None:
+                        lower = lower * scaling_factor
+                    if upper is not None:
+                        upper = upper * scaling_factor
 
                     if scaling_factor < 0:
-                        c._lower, c._upper = c._upper, c._lower
+                        lower, upper = upper, lower
 
                     if scale_constraint_dual and c in model.dual:
                         dual_value = model.dual[c]
                         if dual_value is not None:
                             model.dual[c] = dual_value / scaling_factor
-
-                    c.set_value((c._lower, body, c._upper))
+                    
+                    if c.equality:
+                        c.set_value((lower, body))
+                    else:
+                        c.set_value((lower, body, upper))
 
                 elif isinstance(c, _ObjectiveData):
                     c.expr = scaling_factor * \


Test patch: