Skip to content

Commit

Permalink
Merge pull request #3275 from Robbybp/scaling-uninitialized
Browse files Browse the repository at this point in the history
Handle uninitialized variable in `propagate_solution` of scaling transformation
  • Loading branch information
blnicho authored Aug 20, 2024
2 parents 40ad402 + beb31f6 commit b498f6b
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 7 deletions.
27 changes: 20 additions & 7 deletions pyomo/core/plugins/transform/scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

import logging
from pyomo.common.collections import ComponentMap
from pyomo.core.base import Block, Var, Constraint, Objective, Suffix, value
from pyomo.core.plugins.transform.hierarchy import Transformation
Expand All @@ -17,6 +18,8 @@
from pyomo.core.expr import replace_expressions
from pyomo.util.components import rename_components

logger = logging.getLogger("pyomo.core.plugins.transform.scaling")


@TransformationFactory.register(
'core.scale_model', doc="Scale model variables, constraints, and objectives."
Expand All @@ -26,7 +29,7 @@ class ScaleModel(Transformation):
Transformation to scale a model.
This plugin performs variable, constraint, and objective scaling on
a model based on the scaling factors in the suffix 'scaling_parameter'
a model based on the scaling factors in the suffix 'scaling_factor'
set for the variables, constraints, and/or objective. This is typically
done to scale the problem for improved numerical properties.
Expand All @@ -35,6 +38,10 @@ class ScaleModel(Transformation):
* :py:meth:`create_using <pyomo.core.plugins.transform.scaling.ScaleModel.create_using>`
* :py:meth:`propagate_solution <pyomo.core.plugins.transform.scaling.ScaleModel.propagate_solution>`
By default, scaling components are renamed with the prefix ``scaled_``. To disable
this behavior and scale variables in-place (or keep the same names in a new model),
use the ``rename=False`` argument to ``apply_to`` or ``create_using``.
Examples
--------
Expand Down Expand Up @@ -67,8 +74,6 @@ class ScaleModel(Transformation):
>>> print(value(scaled_model.scaled_obj))
101.0
.. todo:: Implement an option to change the variables names or not
"""

def __init__(self, **kwds):
Expand Down Expand Up @@ -308,10 +313,18 @@ def propagate_solution(self, scaled_model, original_model):
original_v = original_model.find_component(original_v_path)

for k in scaled_v:
original_v[k].set_value(
value(scaled_v[k]) / component_scaling_factor_map[scaled_v[k]],
skip_validation=True,
)
if scaled_v[k].value is None and original_v[k].value is not None:
logger.warning(
"Variable with value None in the scaled model is replacing"
f" value of variable {original_v[k].name} in the original"
f" model with None (was {original_v[k].value})."
)
original_v[k].set_value(None, skip_validation=True)
elif scaled_v[k].value is not None:
original_v[k].set_value(
value(scaled_v[k]) / component_scaling_factor_map[scaled_v[k]],
skip_validation=True,
)
if check_reduced_costs and scaled_v[k] in scaled_model.rc:
original_model.rc[original_v[k]] = (
scaled_model.rc[scaled_v[k]]
Expand Down
27 changes: 27 additions & 0 deletions pyomo/core/tests/transform/test_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
# ___________________________________________________________________________
#

import io
import pyomo.common.unittest as unittest
import pyomo.environ as pyo
from pyomo.opt.base.solvers import UnknownSolver
from pyomo.core.plugins.transform.scaling import ScaleModel, SuffixFinder
from pyomo.common.log import LoggingIntercept


class TestScaleModelTransformation(unittest.TestCase):
Expand Down Expand Up @@ -708,6 +710,31 @@ def test_get_float_scaling_factor_intermediate_level(self):
# v2 should get SF from highest level, ignoring b3 level
self.assertEqual(_finder.find(m.b1.b2.b3.v3), 0.3)

def test_propagate_solution_uninitialized_variable(self):
m = pyo.ConcreteModel()
m.x = pyo.Var([1, 2], initialize=1.0)
m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT)
m.scaling_factor[m.x[1]] = 10.0
m.scaling_factor[m.x[2]] = 10.0
scaled_model = pyo.TransformationFactory("core.scale_model").create_using(m)
scaled_model.scaled_x[1] = 20.0
scaled_model.scaled_x[2] = None

OUTPUT = io.StringIO()
with LoggingIntercept(OUTPUT, "pyomo.core.plugins.transform.scaling"):
pyo.TransformationFactory("core.scale_model").propagate_solution(
scaled_model, m
)
msg = (
"Variable with value None in the scaled model is replacing value of"
" variable x[2] in the original model with None (was 1.0).\n"
)
self.assertEqual(OUTPUT.getvalue(), msg)
self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8)
# Note that value of x[2] in original model *has* been overridden to None.
# In this case, a warning has been raised.
self.assertIs(m.x[2].value, None)


if __name__ == "__main__":
unittest.main()

0 comments on commit b498f6b

Please sign in to comment.