Skip to content

Commit ca33341

Browse files
authored
Merge pull request #3586 from emma58/modernize-relax-integer-vars
Rewrite `core.relax_integer_vars` transformation
2 parents 17f9a75 + 14812d2 commit ca33341

File tree

4 files changed

+381
-40
lines changed

4 files changed

+381
-40
lines changed

pyomo/core/base/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from pyomo.core.base.component import name, Component, ModelComponentFactory
3939
from pyomo.core.base.componentuid import ComponentUID
4040
from pyomo.core.base.config import PyomoOptions
41-
from pyomo.core.base.enums import SortComponents, TraversalStrategy
41+
from pyomo.core.base.enums import SortComponents, TraversalStrategy, VarCollector
4242
from pyomo.core.base.label import (
4343
CuidLabeler,
4444
CounterLabeler,

pyomo/core/base/enums.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import enum
1313
import sys
14+
from pyomo.common import enums
1415

1516
if sys.version_info[:2] >= (3, 11):
1617
strictEnum = {'boundary': enum.STRICT}
@@ -93,3 +94,8 @@ def sort_names(flag):
9394
@staticmethod
9495
def sort_indices(flag):
9596
return SortComponents.SORTED_INDICES in SortComponents(flag)
97+
98+
99+
class VarCollector(enums.IntEnum):
100+
FromVarComponents = 1
101+
FromExpressions = 2

pyomo/core/plugins/transform/discrete_vars.py

Lines changed: 179 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,23 @@
1414
logger = logging.getLogger('pyomo.core')
1515

1616
from pyomo.common import deprecated
17-
from pyomo.core.base import Transformation, TransformationFactory, Var, Suffix, Reals
17+
from pyomo.common.config import ConfigDict, ConfigValue, In, IsInstance
18+
from pyomo.common.deprecation import deprecation_warning
19+
from pyomo.core.base import (
20+
Transformation,
21+
TransformationFactory,
22+
Var,
23+
Suffix,
24+
Reals,
25+
Block,
26+
ReverseTransformationToken,
27+
VarCollector,
28+
Constraint,
29+
Objective,
30+
)
31+
from pyomo.core.util import target_list
32+
from pyomo.gdp import Disjunct
33+
from pyomo.util.vars_from_expressions import get_vars_from_components
1834

1935

2036
#
@@ -25,56 +41,181 @@
2541
'core.relax_integer_vars', doc="Relax integer variables to continuous counterparts"
2642
)
2743
class RelaxIntegerVars(Transformation):
44+
CONFIG = ConfigDict('core.relax_integer_vars')
45+
CONFIG.declare(
46+
'targets',
47+
ConfigValue(
48+
default=None,
49+
domain=target_list,
50+
description="target or list of targets that will be relaxed",
51+
doc="""
52+
This specifies the list of components to relax. If None (default), the
53+
entire model is transformed. Note that if the transformation is done
54+
out of place, the list of targets should be attached to the model before
55+
it is cloned, and the list will specify the targets on the cloned
56+
instance.""",
57+
),
58+
)
59+
CONFIG.declare(
60+
'reverse',
61+
ConfigValue(
62+
default=None,
63+
domain=IsInstance(ReverseTransformationToken),
64+
description="The token returned by a (forward) call to this "
65+
"transformation, if you wish to reverse the transformation.",
66+
doc="""
67+
This argument should be the reverse transformation token
68+
returned by a previous call to this transformation to transform
69+
fixed disjunctive state in the given model.
70+
If this argument is specified, this call to the transformation
71+
will reverse what the transformation did in the call that returned
72+
the token. Note that if there are intermediate changes to the model
73+
in between the forward and the backward calls to the transformation,
74+
the behavior could be unexpected.
75+
""",
76+
),
77+
)
78+
CONFIG.declare(
79+
'var_collector',
80+
ConfigValue(
81+
default=VarCollector.FromVarComponents,
82+
domain=In(VarCollector),
83+
description="The method for collection the Vars to relax. If "
84+
"VarCollector.FromVarComponents (default), any Var component on "
85+
"the active tree will be relaxed.",
86+
doc="""
87+
This specifies the method for collecting the Var components to relax.
88+
The default, VarCollector.FromVarComponents, assumes that all relevant
89+
Vars are on the active tree. If this is true, then this is the most
90+
performant option. However, in more complex cases where some Vars may not
91+
be in the active tree (e.g. some are on deactivated Blocks or come from
92+
other models), specify VarCollector.FromExpressions to relax all Vars that
93+
appear in expressions in the active tree.
94+
""",
95+
),
96+
)
97+
CONFIG.declare(
98+
'transform_deactivated_blocks',
99+
ConfigValue(
100+
default=True,
101+
description="[DEPRECATED]: Whether or not to search for Var components to "
102+
"relax on deactivated Blocks. True by default",
103+
),
104+
)
105+
CONFIG.declare(
106+
'undo',
107+
ConfigValue(
108+
default=False,
109+
domain=bool,
110+
description="[DEPRECATED]: Please use the 'reverse' argument to undo "
111+
"the transformation.",
112+
),
113+
)
114+
28115
def __init__(self):
29-
super(RelaxIntegerVars, self).__init__()
116+
super().__init__()
30117

31118
def _apply_to(self, model, **kwds):
32-
options = kwds.pop('options', {})
33-
if kwds.get('undo', options.get('undo', False)):
119+
if not model.ctype in (Block, Disjunct):
120+
raise ValueError(
121+
"Transformation called on %s of type %s. 'model' "
122+
"must be a ConcreteModel or Block." % (model.name, model.ctype)
123+
)
124+
config = self.CONFIG(kwds.pop('options', {}))
125+
config.set_value(kwds)
126+
127+
if config.undo:
128+
deprecation_warning(
129+
"The 'undo' argument is deprecated. Please use the 'reverse' "
130+
"argument to undo the transformation.",
131+
version='6.9.3.dev0',
132+
)
34133
for v, d in model._relaxed_integer_vars[None].values():
35134
bounds = v.bounds
36135
v.domain = d
37136
v.setlb(bounds[0])
38137
v.setub(bounds[1])
39138
model.del_component("_relaxed_integer_vars")
40139
return
41-
# True by default, you can specify False if you want
42-
descend = kwds.get(
43-
'transform_deactivated_blocks',
44-
options.get('transform_deactivated_blocks', True),
45-
)
46-
active = None if descend else True
47140

48-
# Relax the model
49-
relaxed_vars = {}
50-
_base_model_vars = model.component_data_objects(
51-
Var, active=active, descend_into=True
52-
)
53-
for var in _base_model_vars:
141+
targets = (model,) if config.targets is None else config.targets
142+
143+
if config.reverse is None:
144+
reverse_dict = {}
145+
# Relax the model
146+
reverse_token = ReverseTransformationToken(
147+
self.__class__, model, targets, reverse_dict
148+
)
149+
else:
150+
# reverse the transformation
151+
reverse_token = config.reverse
152+
reverse_token.check_token_valid(self.__class__, model, targets)
153+
reverse_dict = reverse_token.reverse_dict
154+
for v, d in reverse_dict.values():
155+
lb, ub = v.bounds
156+
v.domain = d
157+
v.setlb(lb)
158+
v.setub(ub)
159+
return
160+
161+
### [ESJ 4/29/25]: This can go away when we remove 'undo'
162+
model._relaxed_integer_vars = Suffix(direction=Suffix.LOCAL)
163+
model._relaxed_integer_vars[None] = reverse_dict
164+
###
165+
166+
for t in targets:
167+
if isinstance(t, Block):
168+
blocks = t.values() if t.is_indexed() else (t,)
169+
for block in blocks:
170+
self._relax_block(block, config, reverse_dict)
171+
elif t.ctype is Var:
172+
self._relax_var(t, reverse_dict)
173+
else:
174+
raise ValueError(
175+
"Target '%s' was not a Block or Var. It was of type "
176+
"'%s' and cannot be transformed." % (t.name, type(t))
177+
)
178+
179+
return reverse_token
180+
181+
def _relax_block(self, block, config, reverse_dict):
182+
self._relax_vars_from_block(block, config, reverse_dict)
183+
184+
for b in block.component_data_objects(Block, active=None, descend_into=True):
185+
if not b.active:
186+
if config.transform_deactivated_blocks:
187+
deprecation_warning(
188+
"The `transform_deactivated_blocks` arguments is deprecated. "
189+
"Either specify deactivated Blocks as targets to activate them "
190+
"if transforming them is the desired behavior.",
191+
version='6.9.3.dev0',
192+
)
193+
else:
194+
continue
195+
self._relax_vars_from_block(b, config, reverse_dict)
196+
197+
def _relax_vars_from_block(self, block, config, reverse_dict):
198+
if config.var_collector is VarCollector.FromVarComponents:
199+
model_vars = block.component_data_objects(Var, descend_into=False)
200+
else:
201+
model_vars = get_vars_from_components(
202+
block, ctype=(Constraint, Objective), descend_into=False
203+
)
204+
for var in model_vars:
205+
if id(var) not in reverse_dict:
206+
self._relax_var(var, reverse_dict)
207+
208+
def _relax_var(self, v, reverse_dict):
209+
var_datas = v.values() if v.is_indexed() else (v,)
210+
for var in var_datas:
54211
if not var.is_integer():
55212
continue
56-
# Note: some indexed components can only have their
57-
# domain set on the parent component (the individual
58-
# indices cannot be set independently)
59-
_c = var.parent_component()
60-
try:
61-
lb, ub = var.bounds
62-
_domain = var.domain
63-
var.domain = Reals
64-
var.setlb(lb)
65-
var.setub(ub)
66-
relaxed_vars[id(var)] = (var, _domain)
67-
except:
68-
if id(_c) in relaxed_vars:
69-
continue
70-
_domain = _c.domain
71-
lb, ub = _c.bounds
72-
_c.domain = Reals
73-
_c.setlb(lb)
74-
_c.setub(ub)
75-
relaxed_vars[id(_c)] = (_c, _domain)
76-
model._relaxed_integer_vars = Suffix(direction=Suffix.LOCAL)
77-
model._relaxed_integer_vars[None] = relaxed_vars
213+
lb, ub = var.bounds
214+
_domain = var.domain
215+
var.domain = Reals
216+
var.setlb(lb)
217+
var.setub(ub)
218+
reverse_dict[id(var)] = (var, _domain)
78219

79220

80221
@TransformationFactory.register(

0 commit comments

Comments
 (0)