Skip to content

Commit 497336a

Browse files
authored
Merge pull request #2619 from andrewlee94/scaling_transform
Adding support for local suffixes in scaling transformation
2 parents 9cc86f1 + c2ca8b8 commit 497336a

File tree

5 files changed

+454
-6
lines changed

5 files changed

+454
-6
lines changed

doc/OnlineDocs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ with a diverse set of optimization capabilities.
1818
solving_pyomo_models.rst
1919
working_models.rst
2020
working_abstractmodels/index.rst
21+
model_transformations/index.rst
2122
modeling_extensions/index.rst
2223
tutorial_examples.rst
2324
model_debugging/index.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Model Transformations
2+
=====================
3+
4+
.. toctree::
5+
:maxdepth: 1
6+
7+
scaling.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
Model Scaling Transformation
2+
============================
3+
4+
Good scaling of models can greatly improve the numerical properties of a problem and thus increase reliability and convergence. The ``core.scale_model`` transformation allows users to separate scaling of a model from the declaration of the model variables and constraints which allows for models to be written in more natural forms and to be scaled and rescaled as required without having to rewrite the model code.
5+
6+
.. autoclass:: pyomo.core.plugins.transform.scaling.ScaleModel
7+
:members:
8+
9+
Setting Scaling Factors
10+
-----------------------
11+
12+
Scaling factors for components in a model are declared using :ref:`Suffixes`, as shown in the example above. In order to define a scaling factor for a component, a ``Suffix`` named ``scaling_factor`` must first be created to hold the scaling factor(s). Scaling factor suffixes can be declared at any level of the model hierarchy, but scaling factors declared on the higher-level ``models`` or ``Blocks`` take precedence over those declared at lower levels.
13+
14+
Scaling suffixes are dict-like where each key is a Pyomo component and the value is the scaling factor to be applied to that component.
15+
16+
In the case of indexed components, scaling factors can either be declared for an individual index or for the indexed component as a whole (with scaling factors for individual indices taking precedence over overall scaling factors).
17+
18+
.. note::
19+
20+
In the case that a scaling factor is declared for a component on at multiple levels of the hierarchy, the highest level scaling factor will be applied.
21+
22+
.. note::
23+
24+
It is also possible (but not encouraged) to define a "default" scaling factor to be applied to any component for which a specific scaling factor has not been declared by setting a entry in a Suffix with a key of ``None``. In this case, the default value declared closest to the component to be scaled will be used (i.e., the first default value found when walking up the model hierarchy).
25+
26+
Applying Model Scaling
27+
----------------------
28+
29+
The ``core.scale_model`` transformation provides two approaches for creating a scaled model.
30+
31+
In-Place Scaling
32+
****************
33+
34+
The ``apply_to(model)`` method can be used to apply scaling directly to an existing model. When using this method, all the variables, constraints and objectives within the target model are replaced with new scaled components and the appropriate scaling factors applied. The model can then be sent to a solver as usual, however the results will be in terms of the scaled components and must be un-scaled by the user.
35+
36+
Creating a New Scaled Model
37+
***************************
38+
39+
Alternatively, the ``create_using(model)`` method can be used to create a new, scaled version of the model which can be solved. In this case, a clone of the original model is generated with the variables, constraints and objectives replaced by scaled equivalents. Users can then send the scaled model to a solver after which the ``propagate_solution`` method can be used to map the scaled solution back onto the original model for further analysis.
40+
41+
The advantage of this approach is that the original model is maintained separately from the scaled model, which facilitates rescaling and other manipulation of the original model after a solution has been found. The disadvantage of this approach is that cloning the model may result in memory issues when dealing with larger models.

pyomo/core/plugins/transform/scaling.py

+84-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
# ___________________________________________________________________________
1111

1212
from pyomo.common.collections import ComponentMap
13-
from pyomo.core.base import Var, Constraint, Objective, _ConstraintData, _ObjectiveData, Suffix, value
13+
from pyomo.core.base import (
14+
Block, Var, Constraint, Objective, _ConstraintData, _ObjectiveData,
15+
Suffix, value,
16+
)
1417
from pyomo.core.plugins.transform.hierarchy import Transformation
1518
from pyomo.core.base import TransformationFactory
1619
from pyomo.core.expr.current import replace_expressions
@@ -81,16 +84,91 @@ def _create_using(self, original_model, **kwds):
8184
self._apply_to(scaled_model, **kwds)
8285
return scaled_model
8386

87+
def _suffix_finder(self, component_data, suffix_name, root=None):
88+
"""Find suffix value for a given component data object in model tree
89+
90+
Suffixes are searched by traversing the model hierarchy in three passes:
91+
92+
1. Search for a Suffix matching the specific component_data,
93+
starting at the `root` and descending down the tree to
94+
the component_data. Return the first match found.
95+
2. Search for a Suffix matching the component_data's container,
96+
starting at the `root` and descending down the tree to
97+
the component_data. Return the first match found.
98+
3. Search for a Suffix with key `None`, starting from the
99+
component_data and working up the tree to the `root`.
100+
Return the first match found.
101+
4. Return None
102+
103+
Parameters
104+
----------
105+
component_data: ComponentDataBase
106+
107+
Component or component data object to find suffix value for.
108+
109+
suffix_name: str
110+
111+
Name of Suffix to search for.
112+
113+
root: BlockData
114+
115+
When searching up the block hierarchy, stop at this
116+
BlockData instead of traversing all the way to the
117+
`component_data.model()` Block. If the `component_data` is
118+
not in the subtree defined by `root`, then the search will
119+
proceed up to `component_data.model()`.
120+
121+
Returns
122+
-------
123+
The value for Suffix associated with component data if found, else None.
124+
125+
"""
126+
# Prototype for Suffix finder
127+
128+
# We want to *include* the root (if it is not None), so if
129+
# it is not None, we want to stop as soon as we get to its
130+
# parent.
131+
if root is not None:
132+
if root.ctype is not Block and not issubclass(root.ctype, Block):
133+
raise ValueError("_find_suffix: root must be a BlockData "
134+
f"(found {root.ctype.__name__}: {root})")
135+
if root.is_indexed():
136+
raise ValueError(
137+
"_find_suffix: root must be a BlockData "
138+
f"(found {type(root).__name__}: {root})")
139+
root = root.parent_block()
140+
# Walk parent tree and search for suffixes
141+
parent = component_data.parent_block()
142+
suffixes = []
143+
while parent is not root:
144+
s = parent.component(suffix_name)
145+
if s is not None and s.ctype is Suffix:
146+
suffixes.append(s)
147+
parent = parent.parent_block()
148+
# Pass 1: look for the component_data, working root to leaf
149+
for s in reversed(suffixes):
150+
if component_data in s:
151+
return s[component_data]
152+
# Pass 2: look for the component container, working root to leaf
153+
parent_comp = component_data.parent_component()
154+
if parent_comp is not component_data:
155+
for s in reversed(suffixes):
156+
if parent_comp in s:
157+
return s[parent_comp]
158+
# Pass 3: look for None, working leaf to root
159+
for s in suffixes:
160+
if None in s:
161+
return s[None]
162+
return None
163+
84164
def _get_float_scaling_factor(self, instance, component_data):
85-
scaling_factor = None
86-
if component_data in instance.scaling_factor:
87-
scaling_factor = instance.scaling_factor[component_data]
88-
elif component_data.parent_component() in instance.scaling_factor:
89-
scaling_factor = instance.scaling_factor[component_data.parent_component()]
165+
scaling_factor = self._suffix_finder(component_data, "scaling_factor")
90166

167+
# If still no scaling factor, return 1.0
91168
if scaling_factor is None:
92169
return 1.0
93170

171+
# Make sure scaling factor is a float
94172
try:
95173
scaling_factor = float(scaling_factor)
96174
except ValueError:

0 commit comments

Comments
 (0)