Source code for pyomo.gdp.plugins.partition_disjuncts

#  ___________________________________________________________________________
#
#  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.
#  ___________________________________________________________________________

"""
Between Steps (P-Split) reformulation for GDPs from [KMT21]_.

"""


from pyomo.common.config import (
    ConfigBlock,
    ConfigValue,
    document_kwargs_from_configdict,
)
from pyomo.common.modeling import unique_component_name
from pyomo.core import (
    Block,
    Constraint,
    Var,
    SortComponents,
    Transformation,
    TransformationFactory,
    TraversalStrategy,
    NonNegativeIntegers,
    value,
    ConcreteModel,
    Objective,
    ComponentMap,
    BooleanVar,
    LogicalConstraint,
    Connector,
    Expression,
    Suffix,
    Param,
    Set,
    SetOf,
    RangeSet,
    Reference,
    Binary,
    LogicalConstraintList,
    maximize,
)
from pyomo.core.base.external import ExternalFunction
from pyomo.network import Port
from pyomo.common.collections import ComponentSet
from pyomo.repn import generate_standard_repn
import pyomo.core.expr as EXPR
from pyomo.opt import SolverFactory
from pyomo.util.vars_from_expressions import get_vars_from_components

from pyomo.gdp import Disjunct, Disjunction, GDP_Error
from pyomo.gdp.util import (
    is_child_of,
    _to_dict,
    verify_successful_solve,
    NORMAL,
    clone_without_expression_components,
    _warn_for_active_disjunct,
    get_gdp_tree,
)
from pyomo.core.util import target_list
from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr
from weakref import ref as weakref_ref

from math import floor

import logging

logger = logging.getLogger('pyomo.gdp.partition_disjuncts')


def _generate_additively_separable_repn(nonlinear_part):
    if nonlinear_part.__class__ is not EXPR.SumExpression:
        # This isn't separable, so we just have the one expression
        return {
            'nonlinear_vars': [
                tuple(v for v in EXPR.identify_variables(nonlinear_part))
            ],
            'nonlinear_exprs': [nonlinear_part],
        }

    # else, it was a SumExpression, and we will break it into the summands,
    # recording which variables are there.
    nonlinear_decomp = {'nonlinear_vars': [], 'nonlinear_exprs': []}
    for summand in nonlinear_part.args:
        nonlinear_decomp['nonlinear_exprs'].append(summand)
        nonlinear_decomp['nonlinear_vars'].append(
            tuple(v for v in EXPR.identify_variables(summand))
        )

    return nonlinear_decomp


[docs] def arbitrary_partition(disjunction, P): """Returns a valid partition into P sets of the variables that appear in algebraic additively separable constraints in the Disjuncts in 'disjunction'. Note that this method may return an invalid partition if the constraints are not additively separable! Arguments: ---------- disjunction : DisjunctionData A Disjunction object for which the variable partition will be created. P : int the number of partitions """ # collect variables v_set = ComponentSet() for disj in disjunction.disjuncts: v_set.update( get_vars_from_components(disj, Constraint, descend_into=Block, active=True) ) # assign them to partitions partitions = [ComponentSet() for i in range(P)] for i, v in enumerate(v_set): partitions[i % P].add(v) return partitions
[docs] def compute_optimal_bounds(expr, global_constraints, opt): """Returns a tuple (LB, UB) where LB and UB are the results of minimizing and maximizing expr over the variable bounds and the constraints on the global_constraints block. Note that if expr is nonlinear, even if one of the min and max problems is convex, the other won't be! Arguments: ---------- expr : ExpressionBase The subexpression whose bounds we will return global_constraints : BlockData A Block which contains the global Constraints and Vars of the original model opt : SolverBase A configured Solver object to use to minimize and maximize expr over the set defined by global_constraints. Note that if expr is nonlinear, opt will need to be capable of optimizing nonconvex problems. """ if opt is None: raise GDP_Error( "No solver was specified to optimize the " "subproblems for computing expression bounds! " "Please specify a configured solver in the " "'compute_bounds_solver' argument if using " "'compute_optimal_bounds.'" ) # add temporary objective and calculate bounds obj = Objective(expr=expr) global_constraints.add_component( unique_component_name(global_constraints, "tmp_obj"), obj ) # Solve first minimizing, to get a lower bound results = opt.solve(global_constraints) if verify_successful_solve(results) is not NORMAL: logger.warning( "Problem to find lower bound for expression %s" "did not solve normally.\n\n%s" % (expr, results) ) LB = None else: # This has some risks, if you're using a solver the gives a lower bound, # getting that would be better. But this is why this is a callback. LB = value(obj.expr) # Now solve maximizing, to get an upper bound obj.sense = maximize results = opt.solve(global_constraints) if verify_successful_solve(results) is not NORMAL: logger.warning( "Problem to find upper bound for expression %s" "did not solve normally.\n\n%s" % (expr, results) ) UB = None else: UB = value(obj.expr) # clean up global_constraints.del_component(obj) del obj return (LB, UB)
[docs] def compute_fbbt_bounds(expr, global_constraints, opt): """ Calls fbbt on expr and returns the lower and upper bounds on the expression based on the bounds of the Vars that appear in the expression. Ignores the global_constraints and opt arguments. """ return compute_bounds_on_expr(expr)
[docs] @TransformationFactory.register( 'gdp.partition_disjuncts', doc="Reformulates a convex disjunctive model " "into a new GDP by splitting additively " "separable constraints on P sets of variables", ) @document_kwargs_from_configdict('CONFIG') class PartitionDisjuncts_Transformation(Transformation): """ Transform disjunctive model to equivalent disjunctive model (with potentially tighter hull relaxation) by taking the "P-split" formulation from Kronqvist et al. 2021 [KMT21]_. In each Disjunct, convex and additively separable constraints are split into separate constraints by introducing auxiliary variables that upperbound the subexpressions created by the split. Increasing the number of partitions can result in tighter hull relaxations, but at the cost of larger model sizes. The transformation will create a new Block with a unique name beginning "_pyomo_gdp_partition_disjuncts_reformulation". The Block will have new Disjunct objects, each corresponding to one of the Disjuncts being transformed. These will have the transformed constraints on them, and be in new Disjunctions, each corresponding to one of the originals. In addition, the auxiliary variables and the partitioned constraints will be declared on this Block, as well as LogicalConstraints linking the original indicator_vars with the ones of the transformed Disjuncts. All original GDP components that were transformed will be deactivated. References ---------- See [KMT21]_. """ CONFIG = ConfigBlock("gdp.partition_disjuncts") CONFIG.declare( 'targets', ConfigValue( default=None, domain=target_list, description="""target or list of targets that will be relaxed""", doc=""" 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( 'variable_partitions', ConfigValue( default=None, domain=_to_dict, description="""Set of sets of variables which define valid partitions (i.e., the constraints are additively separable across these partitions). These can be specified globally (for all active Disjunctions), or by Disjunction.""", doc=""" Specified variable partitions, either globally or per Disjunction. Expects either a set of disjoint ComponentSets whose union is all the variables that appear in all Disjunctions or a mapping from each active Disjunction to a set of disjoint ComponentSets whose union is the set of variables that appear in that Disjunction. In either case, if any constraints in the Disjunction are only partially additively separable, these sets must be a valid partition so that these constraints are additively separable with respect to this partition. To specify a default partition for Disjunctions that do not appear as keys in the map, map the partition to 'None.' Last, note that in the case of constraints containing partially additively separable functions, it is required that the user specify the variable partition(s). """, ), ) CONFIG.declare( 'num_partitions', ConfigValue( default=None, domain=_to_dict, description="""Number of partitions of variables, if variable_partitions is not specified. Can be specified separately for specific Disjunctions if desired.""", doc=""" Either a single value so that all Disjunctions will have variables partitioned into P sets, or a map of Disjunctions to a value of P for each active Disjunction. Mapping None to a value of P will specify the default value of P to use if the value for a given Disjunction is not explicitly specified. Note that if any constraints contain partially additively separable functions, the partitions for the Disjunctions with these Constraints must be specified in the variable_partitions argument. """, ), ) CONFIG.declare( 'variable_partitioning_method', ConfigValue( default=arbitrary_partition, domain=_to_dict, description="""Method to partition the variables. By default, the partitioning will be done arbitrarily.""", doc=""" A function which takes a Disjunction object and a number P and return a valid partitioning of the variables that appear in the disjunction into P partitions. Note that you must give a value for 'P' if you are using this method to calculate partitions. Note that if any constraints contain partially additively separable functions, the partitions for the Disjunctions cannot be calculated automatically. Please specify the partitions for the Disjunctions with these Constraints in the variable_partitions argument. """, ), ) CONFIG.declare( 'assume_fixed_vars_permanent', ConfigValue( default=False, domain=bool, description="""Boolean indicating whether or not to transform so that the transformed model will still be valid when fixed Vars are unfixed.""", doc=""" If True, the transformation will create a correct model even if fixed variables are later unfixed. That is, bounds will be calculated based on fixed variables' bounds, not their values. However, if fixed variables will never be unfixed, a possibly tighter model will result, and fixed variables need not have bounds. Note that this has no effect on fixed BooleanVars, including the indicator variables of Disjuncts. The transformation is always correct whether or not these remain fixed. """, ), ) CONFIG.declare( 'compute_bounds_method', ConfigValue( default=compute_fbbt_bounds, description="""Function that takes an expression, a Block containing the global constraints of the original problem, and a configured solver, and returns both a lower and upper bound for the expression.""", doc=""" Callback for computing bounds on expressions, in order to bound the auxiliary variables created by the transformation. Some pre-implemented options include * compute_fbbt_bounds (the default), and * compute_optimal_bounds or you can write your own callback which accepts an Expression object, a model containing the variables and global constraints of the original instance, and a configured solver and returns a tuple (LB, UB) where either element can be None if no valid bound could be found. """, ), ) CONFIG.declare( 'compute_bounds_solver', ConfigValue( default=None, description="""Solver object to pass to compute_bounds_method. This is required if you are using 'compute_optimal_bounds'.""", doc=""" Configured solver object for use in the compute_bounds_method. In particular, if compute_bounds_method is 'compute_optimal_bounds', this will be used to solve the subproblems, so needs to handle non-convex problems if any Disjunctions contain nonlinear constraints. """, ), )
[docs] def __init__(self): super(PartitionDisjuncts_Transformation, self).__init__() self.handlers = { Constraint: self._transform_constraint, Var: False, # these will be already dealt with--we add # references to them before we call handlers. BooleanVar: False, Connector: False, Expression: False, Suffix: False, Param: False, Set: False, SetOf: False, RangeSet: False, Disjunct: self._warn_for_active_disjunct, Block: False, ExternalFunction: False, Port: False, # not Arcs, because those are deactivated after # the network.expand_arcs transformation }
def _apply_to(self, instance, **kwds): if not instance.ctype in (Block, Disjunct): raise GDP_Error( "Transformation called on %s of type %s. 'instance'" " must be a ConcreteModel, Block, or Disjunct (in " "the case of nested disjunctions)." % (instance.name, instance.ctype) ) try: self._config = self.CONFIG(kwds.pop('options', {})) self._config.set_value(kwds) self._transformation_blocks = {} if not self._config.assume_fixed_vars_permanent: fixed_vars = ComponentMap() for v in get_vars_from_components( instance, Constraint, include_fixed=True, active=True, descend_into=(Block, Disjunct), ): if v.fixed: fixed_vars[v] = value(v) v.fixed = False self._apply_to_impl(instance) finally: # restore fixed variables if not self._config.assume_fixed_vars_permanent: for v, val in fixed_vars.items(): v.fix(val) del self._config del self._transformation_blocks def _apply_to_impl(self, instance): self.variable_partitions = ( self._config.variable_partitions if self._config.variable_partitions is not None else {} ) self.partitioning_method = self._config.variable_partitioning_method # create a model to store the global constraints on that we will pass to # the compute_bounds_method, for if it wants them. We're making it a # separate model because we don't need it again global_constraints = ConcreteModel() for cons in instance.component_objects( Constraint, active=True, descend_into=Block, sort=SortComponents.deterministic, ): global_constraints.add_component( unique_component_name( global_constraints, cons.getname(fully_qualified=True) ), Reference(cons), ) for var in instance.component_objects( Var, descend_into=(Block, Disjunct), sort=SortComponents.deterministic ): global_constraints.add_component( unique_component_name( global_constraints, var.getname(fully_qualified=True) ), Reference(var), ) self._global_constraints = global_constraints # we can support targets as usual. targets = self._config.targets knownBlocks = {} if targets is None: targets = (instance,) # Disjunctions in targets will transform their Disjuncts which will in # turn transform all the GDP components declared on themselves. So we # only need to list root nodes of the GDP tree as targets, and # everything will be transformed (and in the correct order) targets = self._preprocess_targets(targets, instance, knownBlocks) for t in targets: if t.ctype is Disjunction: # After preprocessing, we know that this is not indexed. self._transform_disjunctionData(t, t.index()) else: # We know this is a DisjunctData after preprocessing self._transform_blockData(t) def _preprocess_targets(self, targets, instance, knownBlocks): gdp_tree = get_gdp_tree(targets, instance, knownBlocks) preprocessed_targets = [] # We need only transform root nodes of the tree--the rest will be # transformed recursively from there. (It's also possible to do a # topological sort here and just make sure we don't ask for nested # Disjuncts after their Disjunctions, but that's more work than is # necessary.) for node in gdp_tree.vertices: if gdp_tree.in_degree(node) == 0: preprocessed_targets.append(node) return preprocessed_targets def _get_transformation_block(self, block): if self._transformation_blocks.get(block) is not None: return self._transformation_blocks[block] # create a transformation block on which we will create the reformulated # GDP... self._transformation_blocks[block] = transformation_block = Block() block.add_component( unique_component_name( block, '_pyomo_gdp_partition_disjuncts_reformulation' ), transformation_block, ) transformation_block.indicator_var_equalities = LogicalConstraint( NonNegativeIntegers ) return transformation_block def _transform_blockData(self, obj): # compute the list of Disjunctions to transform *once*, then do it. Else # we will pick up the Disjunctions we create! to_transform = [] # Transform every (active) disjunction in the block. Don't descend into # Disjuncts because we'll transform what's on them recursively. for disjunction in obj.component_data_objects( Disjunction, active=True, sort=SortComponents.deterministic, descend_into=Block, ): to_transform.append(disjunction) for disjunction in to_transform: self._transform_disjunctionData(disjunction, disjunction.index()) def _transform_disjunctionData( self, obj, idx, transBlock=None, transformed_parent_disjunct=None ): if not obj.active: return # Just because it's unlikely this is what someone meant to do... if len(obj.disjuncts) == 0: raise GDP_Error( "Disjunction '%s' is empty. This is " "likely indicative of a modeling error." % obj.getname(fully_qualified=True) ) if transBlock is None and transformed_parent_disjunct is not None: transBlock = self._get_transformation_block(transformed_parent_disjunct) if transBlock is None: transBlock = self._get_transformation_block(obj.parent_block()) variable_partitions = self.variable_partitions partition_method = self.partitioning_method # was the partition specified for the disjunct? partition = variable_partitions.get(obj) if partition is None: # was there a default partition? partition = variable_partitions.get(None) if partition is None: # If not, see what method to use to calculate one method = partition_method.get(obj) if method is None: # was there a default method? method = partition_method.get(None) # if all else fails, set it to our default method = method if method is not None else arbitrary_partition # now figure out P if self._config.num_partitions is None: # This will just end in failure below. (We're checking here # because we don't need a value of P if the partitions were # specified for every Disjunction.) P = None else: P = self._config.num_partitions.get(obj) if P is None: P = self._config.num_partitions.get(None) if P is None: raise GDP_Error( "No value for P was given for disjunction " "%s! Please specify a value of P " "(number of " "partitions), if you do not specify the " "partitions directly." % obj.name ) # it's this method's job to scream if it can't handle what's # here, we can only assume it worked for now, since it's a # callback. partition = method(obj, P) # these have to be ComponentSets partition = [ComponentSet(var_list) for var_list in partition] transformed_disjuncts = [] for disjunct in obj.disjuncts: transformed_disjunct = self._transform_disjunct( disjunct, partition, transBlock ) if transformed_disjunct is not None: transformed_disjuncts.append(transformed_disjunct) # These require transformation, but that's okay because we are # going to a GDP transBlock.indicator_var_equalities[ len(transBlock.indicator_var_equalities) ] = disjunct.indicator_var.equivalent_to( transformed_disjunct.indicator_var ) # make a new disjunction with the transformed guys transformed_disjunction = Disjunction( expr=[disj for disj in transformed_disjuncts] ) transBlock.add_component( unique_component_name(transBlock, obj.getname(fully_qualified=True)), transformed_disjunction, ) obj._algebraic_constraint = weakref_ref(transformed_disjunction) obj.deactivate() def _get_leq_constraints(self, cons): constraints = [] if cons.lower is not None: constraints.append((-cons.body, -cons.lower)) if cons.upper is not None: constraints.append((cons.body, cons.upper)) return constraints def _transform_disjunct(self, disjunct, partition, transBlock): # deactivated -> either we've already transformed or user deactivated if not disjunct.active: if disjunct.indicator_var.is_fixed(): if not value(disjunct.indicator_var): # The user cleanly deactivated the disjunct: there # is nothing for us to do here. return else: raise GDP_Error( "The disjunct '%s' is deactivated, but the " "indicator_var is fixed to %s. This makes no sense." % (disjunct.name, value(disjunct.indicator_var)) ) if disjunct._transformation_block is None: raise GDP_Error( "The disjunct '%s' is deactivated, but the " "indicator_var is not fixed and the disjunct does not " "appear to have been relaxed. This makes no sense. " "(If the intent is to deactivate the disjunct, fix its " "indicator_var to False.)" % (disjunct.name,) ) if disjunct._transformation_block is not None: # we've transformed it, which means this is the second time it's # appearing in a Disjunction raise GDP_Error( "The disjunct '%s' has been transformed, but a disjunction " "it appears in has not. Putting the same disjunct in " "multiple disjunctions is not supported." % disjunct.name ) transformed_disjunct = Disjunct() disjunct._transformation_block = weakref_ref(transformed_disjunct) transBlock.add_component( unique_component_name(transBlock, disjunct.getname(fully_qualified=True)), transformed_disjunct, ) # If the original has an indicator_var fixed to something, fix this one # too. if disjunct.indicator_var.fixed: transformed_disjunct.indicator_var.fix(value(disjunct.indicator_var)) # need to transform inner Disjunctions first (before we complain about # active Disjuncts) for disjunction in disjunct.component_data_objects( Disjunction, active=True, sort=SortComponents.deterministic, descend_into=Block, ): self._transform_disjunctionData( disjunction, disjunction.index(), None, transformed_disjunct ) # create references to any variables declared here on the transformed # Disjunct (this will include the indicator_var) NOTE that we will not # have to do this when #1032 is implemented for the writers. But right # now, we are going to deactivate this and hide it from the active # subtree, so we need to be safe. for var in disjunct.component_objects(Var, descend_into=Block, active=None): transformed_disjunct.add_component( unique_component_name( transformed_disjunct, var.getname(fully_qualified=True) ), Reference(var), ) # Since this transformation is GDP -> GDP and it is based on # partitioning algebraic expressions, we will copy over # LogicalConstraints that may be on the Disjuncts, without transforming # them. This is consistent with our handling of nested Disjunctions, # which also remain nested, though their algebraic constraints may be # transformed. Note that we are not using References because when asked # who their parent block is, we would like these constraints to answer # that it is the transformed Disjunct. logical_constraints = LogicalConstraintList() transformed_disjunct.add_component( unique_component_name(transformed_disjunct, 'logical_constraints'), logical_constraints, ) for cons in disjunct.component_data_objects( LogicalConstraint, descend_into=Block, active=None ): # Add a copy of it on the new Disjunct logical_constraints.add(cons.expr) # deactivate to mark as transformed (so we don't hit it in the loop # below) cons.deactivate() # transform everything else for obj in disjunct.component_data_objects( active=True, sort=SortComponents.deterministic, descend_into=Block ): handler = self.handlers.get(obj.ctype, None) if not handler: if handler is None: raise GDP_Error( "No partition_disjuncts transformation handler " "registered " "for modeling components of type %s. If your " "disjuncts contain non-GDP Pyomo components that " "require transformation, please transform them first." % obj.ctype ) continue # we are really only transforming constraints and checking for # anything nutty (active Disjuncts, etc) here, so pass through what # is necessary for transforming Constraints handler(obj, disjunct, transformed_disjunct, transBlock, partition) disjunct._deactivate_without_fixing_indicator() return transformed_disjunct def _transform_constraint( self, cons, disjunct, transformed_disjunct, transBlock, partition ): instance = disjunct.model() cons_name = cons.getname(fully_qualified=True) # create place on transformed Disjunct for the new constraint and # for the auxiliary variables transformed_constraint = Constraint(NonNegativeIntegers) transformed_disjunct.add_component( unique_component_name(transformed_disjunct, cons_name), transformed_constraint, ) aux_vars = Var(NonNegativeIntegers, dense=False) transformed_disjunct.add_component( unique_component_name(transformed_disjunct, cons_name + "_aux_vars"), aux_vars, ) # create a place on the transBlock for the split constraints split_constraints = Constraint(NonNegativeIntegers) transBlock.add_component( unique_component_name(transBlock, cons_name + "_split_constraints"), split_constraints, ) # this is a list which might have two constraints in it if we had # both a lower and upper value. leq_constraints = self._get_leq_constraints(cons) for body, rhs in leq_constraints: repn = generate_standard_repn(body, compute_values=True) nonlinear_repn = None if repn.nonlinear_expr is not None: nonlinear_repn = _generate_additively_separable_repn( repn.nonlinear_expr ) split_exprs = [] split_aux_vars = [] vars_not_accounted_for = ComponentSet( v for v in EXPR.identify_variables(body, include_fixed=False) ) vars_accounted_for = ComponentSet() for idx, var_list in enumerate(partition): # we are going to recreate the piece of the expression # involving the vars in var_list split_exprs.append(0) expr = split_exprs[-1] for i, v in enumerate(repn.linear_vars): if v in var_list: expr += repn.linear_coefs[i] * v vars_accounted_for.add(v) for i, (v1, v2) in enumerate(repn.quadratic_vars): if v1 in var_list: if v2 not in var_list: raise GDP_Error( "Variables '%s' and '%s' are " "multiplied in Constraint '%s', " "but they are in different " "partitions! Please ensure that " "all the constraints in the " "disjunction are " "additively separable with " "respect to the specified " "partition." % (v1.name, v2.name, cons.name) ) expr += repn.quadratic_coefs[i] * v1 * v2 vars_accounted_for.add(v1) vars_accounted_for.add(v2) if nonlinear_repn is not None: for i, expr_var_set in enumerate(nonlinear_repn['nonlinear_vars']): # check if v_list is a subset of var_list. If it is # not and there is no intersection, we move on. If # it is not and there is an intersection, we raise # an error: It's not a valid partition. If it is, # then we add this piece of the expression. # subset? if all(v in var_list for v in list(expr_var_set)): expr += nonlinear_repn['nonlinear_exprs'][i] for var in expr_var_set: vars_accounted_for.add(var) # intersection? elif len(ComponentSet(expr_var_set) & var_list) != 0: raise GDP_Error( "Variables which appear in the " "expression %s are in different " "partitions, but this " "expression doesn't appear " "additively separable. Please " "expand it if it is additively " "separable or, more likely, " "ensure that all the " "constraints in the disjunction " "are additively separable with " "respect to the specified " "partition. If you did not " "specify a partition, only " "a value of P, note that to " "automatically partition the " "variables, we assume all the " "expressions are additively " "separable." % nonlinear_repn['nonlinear_exprs'][i] ) expr_lb, expr_ub = self._config.compute_bounds_method( expr, self._global_constraints, self._config.compute_bounds_solver ) if expr_lb is None or expr_ub is None: raise GDP_Error( "Expression %s from constraint '%s' " "is unbounded! Please ensure all " "variables that appear " "in the constraint are bounded or " "specify compute_bounds_method=" "compute_optimal_bounds" " if the expression is bounded by the " "global constraints." % (expr, cons.name) ) # if the expression was empty wrt the partition, we don't # need to bother with any of this. The aux_var doesn't need # to exist because it would be 0. if type(expr) is not int or expr != 0: aux_var = aux_vars[len(aux_vars)] aux_var.setlb(expr_lb) aux_var.setub(expr_ub) split_aux_vars.append(aux_var) split_constraints[len(split_constraints)] = expr <= aux_var if len(vars_accounted_for) < len(vars_not_accounted_for): orphans = vars_not_accounted_for - vars_accounted_for orphan_string = "" for v in orphans: orphan_string += "'%s', " % v.name orphan_string = orphan_string[:-2] raise GDP_Error( "Partition specified for disjunction " "containing Disjunct '%s' does not " "include all the variables that appear " "in the disjunction. The following " "variables are not assigned to any part " "of the partition: %s" % (disjunct.name, orphan_string) ) transformed_constraint[len(transformed_constraint)] = ( sum(v for v in split_aux_vars) <= rhs - repn.constant ) # deactivate the constraint since we've transformed it cons.deactivate() def _warn_for_active_disjunct( self, disjunct, parent_disjunct, transformed_parent_disjunct, transBlock, partition, ): _warn_for_active_disjunct(disjunct, parent_disjunct)