# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# 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.
# ___________________________________________________________________________
from pyomo.common.collections import ComponentMap
from pyomo.core.base import (
Block,
Var,
Constraint,
Objective,
_ConstraintData,
_ObjectiveData,
Suffix,
value,
)
from pyomo.core.plugins.transform.hierarchy import Transformation
from pyomo.core.base import TransformationFactory
from pyomo.core.base.suffix import SuffixFinder
from pyomo.core.expr import replace_expressions
from pyomo.util.components import rename_components
[docs]@TransformationFactory.register(
'core.scale_model', doc="Scale model variables, constraints, and objectives."
)
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'
set for the variables, constraints, and/or objective. This is typically
done to scale the problem for improved numerical properties.
Supported transformation methods:
* :py:meth:`apply_to <pyomo.core.plugins.transform.scaling.ScaleModel.apply_to>`
* :py:meth:`create_using <pyomo.core.plugins.transform.scaling.ScaleModel.create_using>`
* :py:meth:`propagate_solution <pyomo.core.plugins.transform.scaling.ScaleModel.propagate_solution>`
Examples
--------
.. doctest::
>>> from pyomo.environ import *
>>> # create the model
>>> model = ConcreteModel()
>>> model.x = Var(bounds=(-5, 5), initialize=1.0)
>>> model.y = Var(bounds=(0, 1), initialize=1.0)
>>> model.obj = Objective(expr=1e8*model.x + 1e6*model.y)
>>> model.con = Constraint(expr=model.x + model.y == 1.0)
>>> # create the scaling factors
>>> model.scaling_factor = Suffix(direction=Suffix.EXPORT)
>>> model.scaling_factor[model.obj] = 1e-6 # scale the objective
>>> model.scaling_factor[model.con] = 2.0 # scale the constraint
>>> model.scaling_factor[model.x] = 0.2 # scale the x variable
>>> # transform the model
>>> scaled_model = TransformationFactory('core.scale_model').create_using(model)
>>> # print the value of the objective function to show scaling has occurred
>>> print(value(model.x))
1.0
>>> print(value(scaled_model.scaled_x))
0.2
>>> print(value(scaled_model.scaled_x.lb))
-1.0
>>> print(value(model.obj))
101000000.0
>>> print(value(scaled_model.scaled_obj))
101.0
.. todo:: Implement an option to change the variables names or not
"""
def __init__(self, **kwds):
kwds['name'] = "scale_model"
self._scaling_method = kwds.pop('scaling_method', 'user')
self._suffix_finder = None
super(ScaleModel, self).__init__(**kwds)
def _create_using(self, original_model, **kwds):
scaled_model = original_model.clone()
self._apply_to(scaled_model, **kwds)
return scaled_model
def _get_float_scaling_factor(self, component):
if self._suffix_finder is None:
self._suffix_finder = SuffixFinder('scaling_factor', 1.0)
return self._suffix_finder.find(component)
def _apply_to(self, model, rename=True):
# create a map of component to scaling factor
component_scaling_factor_map = ComponentMap()
self._suffix_finder = SuffixFinder('scaling_factor', 1.0)
# if the scaling_method is 'user', get the scaling parameters from the suffixes
if self._scaling_method == 'user':
# get the scaling factors
for c in model.component_data_objects(
ctype=(Var, Constraint, Objective), descend_into=True
):
component_scaling_factor_map[c] = self._suffix_finder.find(c)
else:
raise ValueError(
"ScaleModel transformation: unknown scaling_method found"
"-- supported values: 'user' "
)
if rename:
# rename all the Vars, Constraints, and Objectives
# from foo to scaled_foo
component_list = list(
model.component_objects(ctype=[Var, Constraint, Objective])
)
scaled_component_to_original_name_map = rename_components(
model=model, component_list=component_list, prefix='scaled_'
)
else:
scaled_component_to_original_name_map = ComponentMap(
[
(comp, comp.name)
for comp in model.component_objects(
ctype=[Var, Constraint, Objective]
)
]
)
# scale the variable bounds and values and build the variable
# substitution map for scaling vars in constraints
variable_substitution_map = ComponentMap()
already_scaled = set()
for variable in [
var for var in model.component_objects(ctype=Var, descend_into=True)
]:
if variable.is_reference():
# Skip any references - these should get picked up when
# handling the actual variable
continue
# set the bounds/value for the scaled variable
for k in variable:
v = variable[k]
if id(v) in already_scaled:
continue
already_scaled.add(id(v))
scaling_factor = component_scaling_factor_map[v]
variable_substitution_map[v] = v / scaling_factor
if v.lb is not None:
v.setlb(v.lb * scaling_factor)
if v.ub is not None:
v.setub(v.ub * scaling_factor)
if scaling_factor < 0:
temp = v.lb
v.setlb(v.ub)
v.setub(temp)
if v.value is not None:
# Since the value was OK in the unscaled space, it
# should be safe to assume it is still valid in the
# scaled space)
v.set_value(value(v) * scaling_factor, skip_validation=True)
# scale the objectives/constraints and perform the scaled variable substitution
scale_constraint_dual = False
if type(model.component('dual')) is Suffix:
scale_constraint_dual = True
# translate the variable_substitution_map (ComponentMap)
# to variable_substitution_dict (key: id() of component)
# ToDo: We should change replace_expressions to accept a ComponentMap as well
variable_substitution_dict = {
id(k): v for k, v in variable_substitution_map.items()
}
already_scaled = set()
for component in model.component_objects(
ctype=(Constraint, Objective), descend_into=True
):
if component.is_reference():
# Skip any references - these should get picked up when
# handling the actual component
continue
for k in component:
c = component[k]
if id(c) in already_scaled:
continue
already_scaled.add(id(c))
# perform the constraint/objective scaling and variable sub
scaling_factor = component_scaling_factor_map[c]
if isinstance(c, _ConstraintData):
body = scaling_factor * replace_expressions(
expr=c.body,
substitution_map=variable_substitution_dict,
descend_into_named_expressions=True,
remove_named_expressions=True,
)
# scale the rhs
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:
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
if c.equality:
c.set_value((lower, body))
else:
c.set_value((lower, body, upper))
elif isinstance(c, _ObjectiveData):
c.expr = scaling_factor * replace_expressions(
expr=c.expr,
substitution_map=variable_substitution_dict,
descend_into_named_expressions=True,
remove_named_expressions=True,
)
else:
raise NotImplementedError(
'Unknown object type found when applying scaling factors '
'in ScaleModel transformation - Internal Error'
)
model.component_scaling_factor_map = component_scaling_factor_map
model.scaled_component_to_original_name_map = (
scaled_component_to_original_name_map
)
# Now that we have scaled the model, deactivate the relevant
# scaling suffixes so that we don't accidentally (later)
# double-scale.
for s in self._suffix_finder.all_suffixes:
s.deactivate()
return model
[docs] def propagate_solution(self, scaled_model, original_model):
"""This method takes the solution in scaled_model and maps it back to
the original model.
It will also transform duals and reduced costs if the suffixes
'dual' and/or 'rc' are present. The :code:`scaled_model`
argument must be a model that was already scaled using this
transformation as it expects data from the transformation to
perform the back mapping.
Parameters
----------
scaled_model : Pyomo Model
The model that was previously scaled with this transformation
original_model : Pyomo Model
The original unscaled source model
"""
if not hasattr(scaled_model, 'component_scaling_factor_map'):
raise AttributeError(
'ScaleModel:propagate_solution called with scaled_model that does '
'not have a component_scaling_factor_map. It is possible this '
'method was called using a model that was not scaled with the '
'ScaleModel transformation'
)
if not hasattr(scaled_model, 'scaled_component_to_original_name_map'):
raise AttributeError(
'ScaleModel:propagate_solution called with scaled_model that does '
'not have a scaled_component_to_original_name_map. It is possible '
'this method was called using a model that was not scaled with '
'the ScaleModel transformation'
)
component_scaling_factor_map = scaled_model.component_scaling_factor_map
scaled_component_to_original_name_map = (
scaled_model.scaled_component_to_original_name_map
)
# transfer the variable values and reduced costs
check_reduced_costs = type(scaled_model.component('rc')) is Suffix
check_dual = (
type(scaled_model.component('dual')) is Suffix
and type(original_model.component('dual')) is Suffix
)
if check_reduced_costs or check_dual:
# get the objective scaling factor
scaled_objectives = list(
scaled_model.component_data_objects(
ctype=Objective, active=True, descend_into=True
)
)
if len(scaled_objectives) != 1:
raise NotImplementedError(
'ScaleModel.propagate_solution requires a single active '
'objective function, but %d objectives found.'
% (len(scaled_objectives))
)
else:
objective_scaling_factor = component_scaling_factor_map[
scaled_objectives[0]
]
for scaled_v in scaled_model.component_objects(ctype=Var, descend_into=True):
# get the unscaled_v from the original model
original_v_path = scaled_component_to_original_name_map[scaled_v]
# This will not work if decimal indices are present:
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 check_reduced_costs and scaled_v[k] in scaled_model.rc:
original_model.rc[original_v[k]] = (
scaled_model.rc[scaled_v[k]]
* component_scaling_factor_map[scaled_v[k]]
/ objective_scaling_factor
)
# transfer the duals
if check_dual:
for scaled_c in scaled_model.component_objects(
ctype=Constraint, descend_into=True
):
original_c = original_model.find_component(
scaled_component_to_original_name_map[scaled_c]
)
for k in scaled_c:
original_model.dual[original_c[k]] = (
scaled_model.dual[scaled_c[k]]
* component_scaling_factor_map[scaled_c[k]]
/ objective_scaling_factor
)