diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 0835e5fd060..d449d479475 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -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 @@ -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." @@ -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. @@ -35,6 +38,10 @@ class ScaleModel(Transformation): * :py:meth:`create_using ` * :py:meth:`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 -------- @@ -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): @@ -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]] diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index 7168f6bb707..94798535e9c 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -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): @@ -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()