From fe8b7f621022fa0446e2879a5add31a86fc1bb6e Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:46:18 -0600 Subject: [PATCH 01/10] Adding VarCollector enum --- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/enums.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index d92999aa931..6d8d22c85a9 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -38,7 +38,7 @@ from pyomo.core.base.component import name, Component, ModelComponentFactory from pyomo.core.base.componentuid import ComponentUID from pyomo.core.base.config import PyomoOptions -from pyomo.core.base.enums import SortComponents, TraversalStrategy +from pyomo.core.base.enums import SortComponents, TraversalStrategy, VarCollector from pyomo.core.base.label import ( CuidLabeler, CounterLabeler, diff --git a/pyomo/core/base/enums.py b/pyomo/core/base/enums.py index 9fe6e4f9f36..5eb90c9f69c 100644 --- a/pyomo/core/base/enums.py +++ b/pyomo/core/base/enums.py @@ -11,6 +11,7 @@ import enum import sys +from pyomo.common import enums if sys.version_info[:2] >= (3, 11): strictEnum = {'boundary': enum.STRICT} @@ -93,3 +94,8 @@ def sort_names(flag): @staticmethod def sort_indices(flag): return SortComponents.SORTED_INDICES in SortComponents(flag) + + +class VarCollector(enums.IntEnum): + FromVarComponents = 1 + FromExpressions = 2 From 4eaf340b553e5a5780e2db7a9e576a6a908afd0c Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:46:58 -0600 Subject: [PATCH 02/10] Rewriting relax_integer_vars transformation and adding tests --- pyomo/core/plugins/transform/discrete_vars.py | 198 ++++++++++++++---- .../tests/unit/test_xfrm_discrete_vars.py | 159 +++++++++++++- 2 files changed, 318 insertions(+), 39 deletions(-) diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index 89cd1804266..d11a8bef834 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -14,7 +14,15 @@ logger = logging.getLogger('pyomo.core') from pyomo.common import deprecated -from pyomo.core.base import Transformation, TransformationFactory, Var, Suffix, Reals +from pyomo.common.config import ConfigDict, ConfigValue, In +from pyomo.common.deprecation import deprecation_warning +from pyomo.core.base import ( + Transformation, TransformationFactory, Var, Suffix, Reals, Block, + ReverseTransformationToken, VarCollector, Constraint, Objective +) +from pyomo.core.util import target_list +from pyomo.gdp import Disjunct +from pyomo.util.vars_from_expressions import get_vars_from_components # @@ -25,12 +33,97 @@ 'core.relax_integer_vars', doc="Relax integer variables to continuous counterparts" ) class RelaxIntegerVars(Transformation): + CONFIG = ConfigDict('core.relax_integer_vars') + CONFIG.declare( + 'targets', + ConfigValue( + default=None, + domain=target_list, + description="target or list of targets that will be relaxed", + doc=""" + This specifies the list of components to relax. If None (default), the + entire model is transformed. Note that if the transformation is done + out of place, the list of targets should be attached to the model before + it is cloned, and the list will specify the targets on the cloned + instance.""", + ), + ) + CONFIG.declare( + 'reverse', + ConfigValue( + default=None, + description="The token returned by a (forward) call to this " + "transformation, if you wish to reverse the transformation.", + doc=""" + This argument should be the reverse transformation token + returned by a previous call to this transformation to transform + fixed disjunctive state in the given model. + If this argument is specified, this call to the transformation + will reverse what the transformation did in the call that returned + the token. Note that if there are intermediate changes to the model + in between the forward and the backward calls to the transformation, + the behavior could be unexpected. + """, + ), + ) + CONFIG.declare( + 'var_collector', + ConfigValue( + default=VarCollector.FromVarComponents, + domain=In(VarCollector), + description="The method for collection the Vars to relax. If " + "VarCollector.FromVarComponents (default), any Var component on " + "the active tree will be relaxed.", + doc=""" + This specifies the method for collecting the Var components to relax. + The default, VarCollector.FromVarComponents, assumes that all relevant + Vars are on the active tree passed to the transformation. If this is + true, then this is the most performant option. However, in more complex + cases where some Vars may not be in the active tree, specify + VarCollector.FromExpressions to relax all Vars that appear in expressions + in the active tree. + """, + ), + ) + CONFIG.declare( + 'transform_deactivated_blocks', + ConfigValue( + default=True, + description="Whether or not to search for Var components to relax on " + "deactivated Blocks. True by default", + doc=""" + This option controls whether the transformation relaxes Var components on + deactivated Blocks or not. It is True by default. + """, + ), + ) + CONFIG.declare( + 'undo', + ConfigValue( + default=False, + domain=bool, + description="[DEPRECATED]: Please use the 'reverse' argument to undo " + "the transformation.", + ), + ) def __init__(self): - super(RelaxIntegerVars, self).__init__() + super().__init__() def _apply_to(self, model, **kwds): - options = kwds.pop('options', {}) - if kwds.get('undo', options.get('undo', False)): + if not model.ctype in (Block, Disjunct): + raise ValueError( + "Transformation called on %s of type %s. 'model' " + "must be a ConcreteModel or Block." % (model.name, model.ctype) + ) + config = self.CONFIG(kwds.pop('options', {})) + config.set_value(kwds) + + if config.undo: + deprecation_warning( + "The 'undo' argument is deprecated. Please use the 'reverse' " + "argument to undo the transformation.", + version='6.9.3.dev0' + ) for v, d in model._relaxed_integer_vars[None].values(): bounds = v.bounds v.domain = d @@ -38,43 +131,72 @@ def _apply_to(self, model, **kwds): v.setub(bounds[1]) model.del_component("_relaxed_integer_vars") return - # True by default, you can specify False if you want - descend = kwds.get( - 'transform_deactivated_blocks', - options.get('transform_deactivated_blocks', True), - ) - active = None if descend else True - # Relax the model - relaxed_vars = {} - _base_model_vars = model.component_data_objects( - Var, active=active, descend_into=True - ) - for var in _base_model_vars: + active = None if config.transform_deactivated_blocks else True + targets = (model,) if config.targets is None else config.targets + + if config.reverse is None: + reverse_dict = {} + # Relax the model + reverse_token = ReverseTransformationToken( + self.__class__, model, targets, reverse_dict + ) + else: + # reverse the transformation + reverse_token = config.reverse + reverse_token.check_token_valid(self.__class__, model, targets) + reverse_dict = reverse_token.reverse_dict + for v, d in reverse_dict.values(): + lb, ub = v.bounds + v.domain = d + v.setlb(lb) + v.setub(ub) + return + + ### [ESJ 4/29/25]: This can go away when we remove 'undo' + model._relaxed_integer_vars = Suffix(direction=Suffix.LOCAL) + model._relaxed_integer_vars[None] = reverse_dict + ### + + for t in targets: + if isinstance(t, Block): + blocks = t.values() if t.is_indexed() else (t,) + for block in blocks: + if config.var_collector is VarCollector.FromVarComponents: + model_vars = block.component_data_objects( + Var, active=active, descend_into=True + ) + else: + model_vars = get_vars_from_components( + block, + ctype=(Constraint, Objective), + active=active, + descend_into=True + ) + for var in model_vars: + self._relax_var(var, reverse_dict) + elif t.ctype is Var: + self._relax_var(t, reverse_dict) + else: + raise ValueError( + "Target '%s' was not a Block or Var. It was of type " + "'%s' and cannot be transformed." % (t.name, type(t)) + ) + + return reverse_token + + def _relax_var(self, v, reverse_dict): + var_datas = v.values() if v.is_indexed() else (v,) + for var in var_datas: if not var.is_integer(): continue - # Note: some indexed components can only have their - # domain set on the parent component (the individual - # indices cannot be set independently) - _c = var.parent_component() - try: - lb, ub = var.bounds - _domain = var.domain - var.domain = Reals - var.setlb(lb) - var.setub(ub) - relaxed_vars[id(var)] = (var, _domain) - except: - if id(_c) in relaxed_vars: - continue - _domain = _c.domain - lb, ub = _c.bounds - _c.domain = Reals - _c.setlb(lb) - _c.setub(ub) - relaxed_vars[id(_c)] = (_c, _domain) - model._relaxed_integer_vars = Suffix(direction=Suffix.LOCAL) - model._relaxed_integer_vars[None] = relaxed_vars + lb, ub = var.bounds + _domain = var.domain + var.domain = Reals + var.setlb(lb) + var.setub(ub) + reverse_dict[id(var)] = (var, _domain) + @TransformationFactory.register( diff --git a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py index 07edce21771..7fc2bd9765a 100644 --- a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py +++ b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py @@ -13,6 +13,7 @@ import pyomo.common.unittest as unittest +from pyomo.core.base import VarCollector from pyomo.environ import ( ConcreteModel, Var, @@ -23,6 +24,8 @@ TransformationFactory, SolverFactory, Reals, + Block, + Integers, ) from pyomo.opt import check_available_solvers @@ -40,6 +43,17 @@ def _generateModel(): return model +def _make_hierarchical_model(): + m = ConcreteModel() + m.y = Var(domain=Binary) + m.b = Block() + m.b.x = Var([1, 2], bounds=(2, 45), domain=Integers) + m.b.y = Var(domain=Binary) + m.b.c = Constraint(expr=m.b.x[1]*m.y <= 23) + + return m + + class Test(unittest.TestCase): @unittest.skipIf(len(solvers) == 0, "LP/MIP solver not available") def test_solve_relax_transform(self): @@ -51,7 +65,7 @@ def test_solve_relax_transform(self): s.solve(m) self.assertEqual(len(m.dual), 0) - TransformationFactory('core.relax_discrete').apply_to(m) + TransformationFactory('core.relax_integer_vars').apply_to(m) self.assertIs(m.x.domain, Reals) self.assertEqual(m.x.lb, 0) self.assertEqual(m.x.ub, 1) @@ -60,6 +74,149 @@ def test_solve_relax_transform(self): self.assertAlmostEqual(m.dual[m.c1], -0.5, 4) self.assertAlmostEqual(m.dual[m.c2], -0.5, 4) + def test_reverse_relax_integer_vars(self): + m = _generateModel() + lp_relax = TransformationFactory('core.relax_integer_vars') + reverse = lp_relax.apply_to(m) + self.assertIs(m.x.domain, Reals) + self.assertIs(m.y.domain, Reals) + self.assertEqual(m.x.lb, 0) + self.assertEqual(m.x.ub, 1) + self.assertIsNone(m.y.lb) + self.assertIsNone(m.y.ub) + + lp_relax.apply_to(m, reverse=reverse) + self.assertIs(m.x.domain, Binary) + self.assertIs(m.y.domain, Reals) + self.assertEqual(m.x.lb, 0) + self.assertEqual(m.x.ub, 1) + self.assertIsNone(m.y.lb) + self.assertIsNone(m.y.ub) + + def test_relax_integer_vars_block_targets(self): + m = _make_hierarchical_model() + TransformationFactory('core.relax_integer_vars').apply_to( + m, targets=m.b + ) + for i in [1, 2]: + self.assertIs(m.b.x[i].domain, Reals) + self.assertEqual(m.b.x[i].lb, 2) + self.assertEqual(m.b.x[i].ub, 45) + self.assertIs(m.b.y.domain, Reals) + self.assertEqual(m.b.y.lb, 0) + self.assertEqual(m.b.y.ub, 1) + + self.assertIs(m.y.domain, Binary) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) + + def test_relax_integer_vars_var_data_targets(self): + m = _make_hierarchical_model() + TransformationFactory('core.relax_integer_vars').apply_to( + m, targets=[m.b.x[1], m.y] + ) + # transformed + self.assertIs(m.b.x[1].domain, Reals) + self.assertEqual(m.b.x[1].lb, 2) + self.assertEqual(m.b.x[1].ub, 45) + # not transformed + self.assertIs(m.b.x[2].domain, Integers) + self.assertEqual(m.b.x[2].lb, 2) + self.assertEqual(m.b.x[2].ub, 45) + # not transformed + self.assertIs(m.b.y.domain, Binary) + self.assertEqual(m.b.y.lb, 0) + self.assertEqual(m.b.y.ub, 1) + # transformed + self.assertIs(m.y.domain, Reals) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) + + def test_relax_integer_vars_indexed_var_targets(self): + m = _make_hierarchical_model() + TransformationFactory('core.relax_integer_vars').apply_to( + m, targets=m.b.x + ) + # transformed + for i in [1, 2]: + self.assertIs(m.b.x[i].domain, Reals) + self.assertEqual(m.b.x[i].lb, 2) + self.assertEqual(m.b.x[i].ub, 45) + # not transformed + self.assertIs(m.b.y.domain, Binary) + self.assertEqual(m.b.y.lb, 0) + self.assertEqual(m.b.y.ub, 1) + # not transformed + self.assertIs(m.y.domain, Binary) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) + + def test_relax_integer_vars_vars_from_expressions(self): + m = _make_hierarchical_model() + TransformationFactory('core.relax_integer_vars').apply_to( + m.b, var_collector=VarCollector.FromExpressions + ) + # transformed + self.assertIs(m.b.x[1].domain, Reals) + self.assertEqual(m.b.x[1].lb, 2) + self.assertEqual(m.b.x[1].ub, 45) + # not transformed + self.assertIs(m.b.x[2].domain, Integers) + self.assertEqual(m.b.x[2].lb, 2) + self.assertEqual(m.b.x[2].ub, 45) + # not transformed + self.assertIs(m.b.y.domain, Binary) + self.assertEqual(m.b.y.lb, 0) + self.assertEqual(m.b.y.ub, 1) + # transformed + self.assertIs(m.y.domain, Reals) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) + + def test_relax_integer_vars_ignore_deactivated_blocks(self): + m = _make_hierarchical_model() + m.b.deactivate() + TransformationFactory('core.relax_integer_vars').apply_to( + m, transform_deactivated_blocks=False + ) + # not transformed + for i in [1, 2]: + self.assertIs(m.b.x[i].domain, Integers) + self.assertEqual(m.b.x[i].lb, 2) + self.assertEqual(m.b.x[i].ub, 45) + # not transformed + self.assertIs(m.b.y.domain, Binary) + self.assertEqual(m.b.y.lb, 0) + self.assertEqual(m.b.y.ub, 1) + # transformed + self.assertIs(m.y.domain, Reals) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) + + m = _make_hierarchical_model() + m.obj = Objective(expr=m.b.x[2]) + m.b.deactivate() + TransformationFactory('core.relax_integer_vars').apply_to( + m, transform_deactivated_blocks=False, + var_collector=VarCollector.FromExpressions + ) + # not transformed + self.assertIs(m.b.x[1].domain, Integers) + self.assertEqual(m.b.x[1].lb, 2) + self.assertEqual(m.b.x[1].ub, 45) + # transformed + self.assertIs(m.b.x[2].domain, Reals) + self.assertEqual(m.b.x[2].lb, 2) + self.assertEqual(m.b.x[2].ub, 45) + # not transformed + self.assertIs(m.b.y.domain, Binary) + self.assertEqual(m.b.y.lb, 0) + self.assertEqual(m.b.y.ub, 1) + # not transformed + self.assertIs(m.y.domain, Binary) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) + @unittest.skipIf(len(solvers) == 0, "LP/MIP solver not available") def test_solve_fix_transform(self): s = SolverFactory(solvers[0]) From 3901747216de3448c2311b99a760d81adb2cd5d4 Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:47:59 -0600 Subject: [PATCH 03/10] black --- pyomo/core/plugins/transform/discrete_vars.py | 22 +++++++++++++------ .../tests/unit/test_xfrm_discrete_vars.py | 19 +++++++--------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index d11a8bef834..4510454f2d3 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -17,8 +17,16 @@ from pyomo.common.config import ConfigDict, ConfigValue, In from pyomo.common.deprecation import deprecation_warning from pyomo.core.base import ( - Transformation, TransformationFactory, Var, Suffix, Reals, Block, - ReverseTransformationToken, VarCollector, Constraint, Objective + Transformation, + TransformationFactory, + Var, + Suffix, + Reals, + Block, + ReverseTransformationToken, + VarCollector, + Constraint, + Objective, ) from pyomo.core.util import target_list from pyomo.gdp import Disjunct @@ -46,8 +54,8 @@ class RelaxIntegerVars(Transformation): out of place, the list of targets should be attached to the model before it is cloned, and the list will specify the targets on the cloned instance.""", - ), - ) + ), + ) CONFIG.declare( 'reverse', ConfigValue( @@ -106,6 +114,7 @@ class RelaxIntegerVars(Transformation): "the transformation.", ), ) + def __init__(self): super().__init__() @@ -122,7 +131,7 @@ def _apply_to(self, model, **kwds): deprecation_warning( "The 'undo' argument is deprecated. Please use the 'reverse' " "argument to undo the transformation.", - version='6.9.3.dev0' + version='6.9.3.dev0', ) for v, d in model._relaxed_integer_vars[None].values(): bounds = v.bounds @@ -171,7 +180,7 @@ def _apply_to(self, model, **kwds): block, ctype=(Constraint, Objective), active=active, - descend_into=True + descend_into=True, ) for var in model_vars: self._relax_var(var, reverse_dict) @@ -198,7 +207,6 @@ def _relax_var(self, v, reverse_dict): reverse_dict[id(var)] = (var, _domain) - @TransformationFactory.register( 'core.relax_discrete', doc="[DEPRECATED] Relax integer variables to continuous counterparts", diff --git a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py index 7fc2bd9765a..7de452ec370 100644 --- a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py +++ b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py @@ -49,7 +49,7 @@ def _make_hierarchical_model(): m.b = Block() m.b.x = Var([1, 2], bounds=(2, 45), domain=Integers) m.b.y = Var(domain=Binary) - m.b.c = Constraint(expr=m.b.x[1]*m.y <= 23) + m.b.c = Constraint(expr=m.b.x[1] * m.y <= 23) return m @@ -95,9 +95,7 @@ def test_reverse_relax_integer_vars(self): def test_relax_integer_vars_block_targets(self): m = _make_hierarchical_model() - TransformationFactory('core.relax_integer_vars').apply_to( - m, targets=m.b - ) + TransformationFactory('core.relax_integer_vars').apply_to(m, targets=m.b) for i in [1, 2]: self.assertIs(m.b.x[i].domain, Reals) self.assertEqual(m.b.x[i].lb, 2) @@ -134,9 +132,7 @@ def test_relax_integer_vars_var_data_targets(self): def test_relax_integer_vars_indexed_var_targets(self): m = _make_hierarchical_model() - TransformationFactory('core.relax_integer_vars').apply_to( - m, targets=m.b.x - ) + TransformationFactory('core.relax_integer_vars').apply_to(m, targets=m.b.x) # transformed for i in [1, 2]: self.assertIs(m.b.x[i].domain, Reals) @@ -197,8 +193,9 @@ def test_relax_integer_vars_ignore_deactivated_blocks(self): m.obj = Objective(expr=m.b.x[2]) m.b.deactivate() TransformationFactory('core.relax_integer_vars').apply_to( - m, transform_deactivated_blocks=False, - var_collector=VarCollector.FromExpressions + m, + transform_deactivated_blocks=False, + var_collector=VarCollector.FromExpressions, ) # not transformed self.assertIs(m.b.x[1].domain, Integers) @@ -215,8 +212,8 @@ def test_relax_integer_vars_ignore_deactivated_blocks(self): # not transformed self.assertIs(m.y.domain, Binary) self.assertEqual(m.y.lb, 0) - self.assertEqual(m.y.ub, 1) - + self.assertEqual(m.y.ub, 1) + @unittest.skipIf(len(solvers) == 0, "LP/MIP solver not available") def test_solve_fix_transform(self): s = SolverFactory(solvers[0]) From 189900675d1ecee3aa57a82559aa19c6cf9268fa Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Mon, 12 May 2025 12:14:32 -0600 Subject: [PATCH 04/10] Adding a test that we do the right thing with fixed variables --- .../tests/unit/test_xfrm_discrete_vars.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py index 7de452ec370..9dd2d67f460 100644 --- a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py +++ b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py @@ -26,6 +26,7 @@ Reals, Block, Integers, + value ) from pyomo.opt import check_available_solvers @@ -214,6 +215,46 @@ def test_relax_integer_vars_ignore_deactivated_blocks(self): self.assertEqual(m.y.lb, 0) self.assertEqual(m.y.ub, 1) + def test_relax_integer_vars_fixed_vars(self): + m = _make_hierarchical_model() + m.y.fix(0) + m.b.y.fix(1) + reverse = TransformationFactory('core.relax_integer_vars').apply_to(m) + + # change the domain, but don't unfix + self.assertIs(m.y.domain, Reals) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) + self.assertEqual(value(m.y), 0) + self.assertTrue(m.y.fixed) + + self.assertIs(m.b.y.domain, Reals) + self.assertEqual(m.b.y.lb, 0) + self.assertEqual(m.b.y.ub, 1) + self.assertEqual(value(m.b.y), 1) + self.assertTrue(m.b.y.fixed) + + # transformed + for i in [1, 2]: + self.assertIs(m.b.x[i].domain, Reals) + self.assertEqual(m.b.x[i].lb, 2) + self.assertEqual(m.b.x[i].ub, 45) + + # reverse and make sure fixed guys are still fixed + TransformationFactory('core.relax_integer_vars').apply_to(m, + reverse=reverse) + self.assertIs(m.y.domain, Binary) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) + self.assertEqual(value(m.y), 0) + self.assertTrue(m.y.fixed) + + self.assertIs(m.b.y.domain, Binary) + self.assertEqual(m.b.y.lb, 0) + self.assertEqual(m.b.y.ub, 1) + self.assertEqual(value(m.b.y), 1) + self.assertTrue(m.b.y.fixed) + @unittest.skipIf(len(solvers) == 0, "LP/MIP solver not available") def test_solve_fix_transform(self): s = SolverFactory(solvers[0]) From 1025b2e3f9087a87988001d9c00ac3e4d62e3f46 Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Mon, 12 May 2025 12:15:32 -0600 Subject: [PATCH 05/10] Black --- pyomo/core/tests/unit/test_xfrm_discrete_vars.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py index 9dd2d67f460..95dc42001bc 100644 --- a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py +++ b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py @@ -26,7 +26,7 @@ Reals, Block, Integers, - value + value, ) from pyomo.opt import check_available_solvers @@ -227,7 +227,7 @@ def test_relax_integer_vars_fixed_vars(self): self.assertEqual(m.y.ub, 1) self.assertEqual(value(m.y), 0) self.assertTrue(m.y.fixed) - + self.assertIs(m.b.y.domain, Reals) self.assertEqual(m.b.y.lb, 0) self.assertEqual(m.b.y.ub, 1) @@ -241,20 +241,19 @@ def test_relax_integer_vars_fixed_vars(self): self.assertEqual(m.b.x[i].ub, 45) # reverse and make sure fixed guys are still fixed - TransformationFactory('core.relax_integer_vars').apply_to(m, - reverse=reverse) + TransformationFactory('core.relax_integer_vars').apply_to(m, reverse=reverse) self.assertIs(m.y.domain, Binary) self.assertEqual(m.y.lb, 0) self.assertEqual(m.y.ub, 1) self.assertEqual(value(m.y), 0) self.assertTrue(m.y.fixed) - + self.assertIs(m.b.y.domain, Binary) self.assertEqual(m.b.y.lb, 0) self.assertEqual(m.b.y.ub, 1) self.assertEqual(value(m.b.y), 1) - self.assertTrue(m.b.y.fixed) - + self.assertTrue(m.b.y.fixed) + @unittest.skipIf(len(solvers) == 0, "LP/MIP solver not available") def test_solve_fix_transform(self): s = SolverFactory(solvers[0]) From 4cfda440cce74f86505265830c402db7999223df Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Mon, 12 May 2025 12:46:27 -0600 Subject: [PATCH 06/10] Adding domain to reverse transformation token argument --- pyomo/core/plugins/transform/discrete_vars.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index 4510454f2d3..129aec7f5db 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -14,7 +14,7 @@ logger = logging.getLogger('pyomo.core') from pyomo.common import deprecated -from pyomo.common.config import ConfigDict, ConfigValue, In +from pyomo.common.config import ConfigDict, ConfigValue, In, IsInstance from pyomo.common.deprecation import deprecation_warning from pyomo.core.base import ( Transformation, @@ -60,6 +60,7 @@ class RelaxIntegerVars(Transformation): 'reverse', ConfigValue( default=None, + domain=IsInstance(ReverseTransformationToken), description="The token returned by a (forward) call to this " "transformation, if you wish to reverse the transformation.", doc=""" From bf68bf5f21f9d0420acd526ec34c25b4a1c5d44e Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Mon, 12 May 2025 12:48:41 -0600 Subject: [PATCH 07/10] Clarifying docstring --- pyomo/core/plugins/transform/discrete_vars.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index 129aec7f5db..af312dd969b 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -86,11 +86,11 @@ class RelaxIntegerVars(Transformation): doc=""" This specifies the method for collecting the Var components to relax. The default, VarCollector.FromVarComponents, assumes that all relevant - Vars are on the active tree passed to the transformation. If this is - true, then this is the most performant option. However, in more complex - cases where some Vars may not be in the active tree, specify - VarCollector.FromExpressions to relax all Vars that appear in expressions - in the active tree. + Vars are on the active tree. If this is true, then this is the most + performant option. However, in more complex cases where some Vars may not + be in the active tree (e.g. some are on deactivated Blocks or come from + other models), specify VarCollector.FromExpressions to relax all Vars that + appear in expressions in the active tree. """, ), ) From 3aa02834e9fe40e53058734248ba7b9f471701fd Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Mon, 12 May 2025 13:24:45 -0600 Subject: [PATCH 08/10] Deprecating transform_deactivated_blocks --- pyomo/core/plugins/transform/discrete_vars.py | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index af312dd969b..4dc60911dd9 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -98,12 +98,8 @@ class RelaxIntegerVars(Transformation): 'transform_deactivated_blocks', ConfigValue( default=True, - description="Whether or not to search for Var components to relax on " - "deactivated Blocks. True by default", - doc=""" - This option controls whether the transformation relaxes Var components on - deactivated Blocks or not. It is True by default. - """, + description="[DEPRECATED]: Whether or not to search for Var components to " + "relax on deactivated Blocks. True by default", ), ) CONFIG.declare( @@ -142,7 +138,6 @@ def _apply_to(self, model, **kwds): model.del_component("_relaxed_integer_vars") return - active = None if config.transform_deactivated_blocks else True targets = (model,) if config.targets is None else config.targets if config.reverse is None: @@ -172,19 +167,7 @@ def _apply_to(self, model, **kwds): if isinstance(t, Block): blocks = t.values() if t.is_indexed() else (t,) for block in blocks: - if config.var_collector is VarCollector.FromVarComponents: - model_vars = block.component_data_objects( - Var, active=active, descend_into=True - ) - else: - model_vars = get_vars_from_components( - block, - ctype=(Constraint, Objective), - active=active, - descend_into=True, - ) - for var in model_vars: - self._relax_var(var, reverse_dict) + self._relax_block(block, config, reverse_dict) elif t.ctype is Var: self._relax_var(t, reverse_dict) else: @@ -194,6 +177,41 @@ def _apply_to(self, model, **kwds): ) return reverse_token + + + def _relax_block(self, block, config, reverse_dict): + self._relax_vars_from_block(block, config, reverse_dict) + + for b in block.component_data_objects( + Block, active=None, descend_into=True + ): + if not b.active: + if config.transform_deactivated_blocks: + deprecation_warning( + "The `transform_deactivated_blocks` arguments is deprecated. " + "Either specify deactivated Blocks as targets to activate them " + "if transforming them is the desired behavior." + ) + else: + continue + self._relax_vars_from_block(b, config, reverse_dict) + + + def _relax_vars_from_block(self, block, config, reverse_dict): + if config.var_collector is VarCollector.FromVarComponents: + model_vars = block.component_data_objects( + Var, descend_into=False + ) + else: + model_vars = get_vars_from_components( + block, + ctype=(Constraint, Objective), + descend_into=False, + ) + for var in model_vars: + if id(var) not in reverse_dict: + self._relax_var(var, reverse_dict) + def _relax_var(self, v, reverse_dict): var_datas = v.values() if v.is_indexed() else (v,) From 79f8aef385c5186a3c1d17e1afa37e546f6dda48 Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Mon, 12 May 2025 13:26:01 -0600 Subject: [PATCH 09/10] black --- pyomo/core/plugins/transform/discrete_vars.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index 4dc60911dd9..4696efe64d3 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -177,14 +177,11 @@ def _apply_to(self, model, **kwds): ) return reverse_token - def _relax_block(self, block, config, reverse_dict): self._relax_vars_from_block(block, config, reverse_dict) - - for b in block.component_data_objects( - Block, active=None, descend_into=True - ): + + for b in block.component_data_objects(Block, active=None, descend_into=True): if not b.active: if config.transform_deactivated_blocks: deprecation_warning( @@ -196,23 +193,17 @@ def _relax_block(self, block, config, reverse_dict): continue self._relax_vars_from_block(b, config, reverse_dict) - def _relax_vars_from_block(self, block, config, reverse_dict): if config.var_collector is VarCollector.FromVarComponents: - model_vars = block.component_data_objects( - Var, descend_into=False - ) + model_vars = block.component_data_objects(Var, descend_into=False) else: model_vars = get_vars_from_components( - block, - ctype=(Constraint, Objective), - descend_into=False, + block, ctype=(Constraint, Objective), descend_into=False ) for var in model_vars: if id(var) not in reverse_dict: self._relax_var(var, reverse_dict) - def _relax_var(self, v, reverse_dict): var_datas = v.values() if v.is_indexed() else (v,) for var in var_datas: From 66b03f2b5a4c653b090335d8dd6156c449453262 Mon Sep 17 00:00:00 2001 From: Emma Johnson <12833636+emma58@users.noreply.github.com> Date: Mon, 12 May 2025 14:15:31 -0600 Subject: [PATCH 10/10] Adding version to deprecation warning --- pyomo/core/plugins/transform/discrete_vars.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index 4696efe64d3..4c1d331f758 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -187,7 +187,8 @@ def _relax_block(self, block, config, reverse_dict): deprecation_warning( "The `transform_deactivated_blocks` arguments is deprecated. " "Either specify deactivated Blocks as targets to activate them " - "if transforming them is the desired behavior." + "if transforming them is the desired behavior.", + version='6.9.3.dev0', ) else: continue