diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 8dcd22b292a..bc2ed2ad1ba 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -13,11 +13,13 @@ from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap, DefaultComponentMap +from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.log import is_debug_set from pyomo.common.modeling import unique_component_name from pyomo.core.base import Transformation, TransformationFactory from pyomo.core.base.external import ExternalFunction +from pyomo.core.util import target_list from pyomo.core import ( Any, Block, @@ -64,6 +66,34 @@ class GDP_to_MIP_Transformation(Transformation): """ Base class for transformations from GDP to MIP """ + CONFIG = ConfigDict() + CONFIG.declare( + 'targets', + ConfigValue( + default=None, + domain=target_list, + description="target or list of targets that will be relaxed", + doc=""" + + This specifies the target or list of targets to relax as either a + component or a list of components. 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( + 'one_indicator_for_two_term', + ConfigValue( + default=False, + domain=bool, + description="Whether or not to reduce to one indicator binary variable " + "in the transformed model for two-term Disjunctions", + doc=""" + TODO + """ + ), + ) def __init__(self, logger): """Initialize transformation object.""" @@ -102,10 +132,12 @@ def __init__(self, logger): self._generate_debug_messages = False self._transformation_blocks = {} self._algebraic_constraints = {} + self._indicator_var_expr = {} def _restore_state(self): self._transformation_blocks.clear() self._algebraic_constraints.clear() + self._indicator_var_expr = {} if hasattr(self, '_config'): del self._config @@ -121,6 +153,11 @@ def _process_arguments(self, instance, **kwds): self._config.set_value(kwds) self._generate_debug_messages = is_debug_set(self.logger) + def _get_indicator_var(self, disjunct): + if disjunct in self._indicator_var_expr: + return self._indicator_var_expr[disjunct] + return disjunct.binary_indicator_var + def _transform_logical_constraints(self, instance, targets): # transform any logical constraints that might be anywhere on the stuff # we're about to transform. We do this before we preprocess targets diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 854366c0cf0..bcf995a93d8 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -50,7 +50,6 @@ is_child_of, _warn_for_active_disjunct, ) -from pyomo.core.util import target_list from pyomo.util.vars_from_expressions import get_vars_from_components from weakref import ref as weakref_ref @@ -108,22 +107,7 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): corresponding OR or XOR constraint. """ - CONFIG = cfg.ConfigDict('gdp.hull') - CONFIG.declare( - 'targets', - cfg.ConfigValue( - default=None, - domain=target_list, - description="target or list of targets that will be relaxed", - doc=""" - - This specifies the target or list of targets to relax as either a - component or a list of components. 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 = GDP_to_MIP_Transformation.CONFIG() CONFIG.declare( 'perspective function', cfg.ConfigValue( @@ -309,6 +293,12 @@ def _transform_disjunctionData( # change their active status when we transform them, but we still need # this list after the fact. active_disjuncts = [disj for disj in obj.disjuncts if disj.active] + two_term = False + if self._config.one_indicator_for_two_term and len(active_disjuncts) == 2: + two_term = True + binary = active_disjuncts[0].binary_indicator_var + self._indicator_var_expr[active_disjuncts[0]] = binary + self._indicator_var_expr[active_disjuncts[1]] = 1 - binary # We put *all* transformed things on the parent Block of this # disjunction. We'll mark the disaggregated Vars as local, but beyond @@ -405,7 +395,7 @@ def _transform_disjunctionData( parent_local_var_list = self._get_local_var_list(parent_disjunct) or_expr = 0 for disjunct in obj.disjuncts: - or_expr += disjunct.indicator_var.get_associated_binary() + or_expr += self._get_indicator_var(disjunct) if disjunct.active: self._transform_disjunct( obj=disjunct, @@ -416,10 +406,11 @@ def _transform_disjunctionData( parent_disjunct_local_vars=local_vars_by_disjunct[parent_disjunct], disjunct_disaggregated_var_map=disjunct_disaggregated_var_map, ) - xorConstraint.add(index, (or_expr, 1)) - # map the DisjunctionData to its XOR constraint to mark it as - # transformed - obj._algebraic_constraint = weakref_ref(xorConstraint[index]) + if not two_term: + xorConstraint.add(index, (or_expr, 1)) + # map the DisjunctionData to its XOR constraint to mark it as + # transformed + obj._algebraic_constraint = weakref_ref(xorConstraint[index]) # Now add the reaggregation constraints for var in all_vars_to_disaggregate: @@ -439,7 +430,7 @@ def _transform_disjunctionData( parent_local_var_list.append(disaggregated_var) local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) var_free = 1 - sum( - disj.indicator_var.get_associated_binary() + self._get_indicator_var(disj) for disj in disjuncts_var_appears_in[var] ) self._declare_disaggregated_var_bounds( @@ -546,7 +537,7 @@ def _transform_disjunct( bigmConstraint=bigmConstraint, lb_idx='lb', ub_idx='ub', - var_free_indicator=obj.indicator_var.get_associated_binary(), + var_free_indicator=self._get_indicator_var(obj), ) # update the bigm constraint mappings data_dict = disaggregatedVar.parent_block().private_data() @@ -575,7 +566,7 @@ def _transform_disjunct( bigmConstraint=bigmConstraint, lb_idx='lb', ub_idx='ub', - var_free_indicator=obj.indicator_var.get_associated_binary(), + var_free_indicator=self._get_indicator_var(obj), ) # update the bigm constraint mappings data_dict = var.parent_block().private_data() @@ -688,7 +679,7 @@ def _transform_constraint( c.body, substitute=zero_substitute_map ) - y = disjunct.binary_indicator_var + y = self._get_indicator_var(disjunct) if NL: if mode == "LeeGrossmann": sub_expr = clone_without_expression_components(