def _transform_constraint(self, obj, disjunct, bigMargs, arg_list, suffix_list): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() bigm_src = transBlock.bigm_src constraintMap = self._get_constraint_map_dict(transBlock) disjunctionRelaxationBlock = transBlock.parent_block() # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name cons_name = obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER) name = unique_component_name(transBlock, cons_name) if obj.is_indexed(): try: newConstraint = Constraint(obj.index_set(), disjunctionRelaxationBlock.lbub) # HACK: We get burned by #191 here... When #1319 is merged we # can revist this and I think stop catching the AttributeError. except (TypeError, AttributeError): # The original constraint may have been indexed by a # non-concrete set (like an Any). We will give up on # strict index verification and just blindly proceed. newConstraint = Constraint(Any) else: newConstraint = Constraint(disjunctionRelaxationBlock.lbub) transBlock.add_component(name, newConstraint) # add mapping of original constraint to transformed constraint constraintMap['srcConstraints'][newConstraint] = obj constraintMap['transformedConstraints'][obj] = newConstraint for i in sorted(iterkeys(obj)): c = obj[i] if not c.active: continue # first, we see if an M value was specified in the arguments. # (This returns None if not) M = self._get_M_from_args(c, bigMargs, arg_list, bigm_src) if __debug__ and logger.isEnabledFor(logging.DEBUG): _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint %s " "from the BigM argument is %s." % (cons_name, str(M))) # if we didn't get something from args, try suffixes: if M is None: M = self._get_M_from_suffixes(c, suffix_list, bigm_src) if __debug__ and logger.isEnabledFor(logging.DEBUG): _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint %s " "after checking suffixes is %s." % (cons_name, str(M))) if not isinstance(M, (tuple, list)): if M is None: M = (None, None) else: try: M = (-M, M) except: logger.error("Error converting scalar M-value %s " "to (-M,M). Is %s not a numeric type?" % (M, type(M))) raise if len(M) != 2: raise GDP_Error("Big-M %s for constraint %s is not of " "length two. " "Expected either a single value or " "tuple or list of length two for M." % (str(M), name)) if c.lower is not None and M[0] is None: M = (self._estimate_M(c.body, name)[0] - c.lower, M[1]) bigm_src[c] = M if c.upper is not None and M[1] is None: M = (M[0], self._estimate_M(c.body, name)[1] - c.upper) bigm_src[c] = M if __debug__ and logger.isEnabledFor(logging.DEBUG): _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint %s " "after estimating (if needed) is %s." % (cons_name, str(M))) # Handle indices for both SimpleConstraint and IndexedConstraint if i.__class__ is tuple: i_lb = i + ('lb',) i_ub = i + ('ub',) elif obj.is_indexed(): i_lb = (i, 'lb',) i_ub = (i, 'ub',) else: i_lb = 'lb' i_ub = 'ub' if c.lower is not None: if M[0] is None: raise GDP_Error("Cannot relax disjunctive constraint %s " "because M is not defined." % name) M_expr = M[0] * (1 - disjunct.indicator_var) newConstraint.add(i_lb, c.lower <= c. body - M_expr) if c.upper is not None: if M[1] is None: raise GDP_Error("Cannot relax disjunctive constraint %s " "because M is not defined." % name) M_expr = M[1] * (1 - disjunct.indicator_var) newConstraint.add(i_ub, c.body - M_expr <= c.upper) # deactivate because we relaxed c.deactivate()
def _transform_constraint(self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() bigm_src = transBlock.bigm_src constraintMap = self._get_constraint_map_dict(transBlock) disjunctionRelaxationBlock = transBlock.parent_block() # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name cons_name = obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER) name = unique_component_name(transBlock, cons_name) if obj.is_indexed(): newConstraint = Constraint(obj.index_set(), disjunctionRelaxationBlock.lbub) # we map the container of the original to the container of the # transformed constraint. Don't do this if obj is a SimpleConstraint # because we will treat that like a _ConstraintData and map to a # list of transformed _ConstraintDatas constraintMap['transformedConstraints'][obj] = newConstraint else: newConstraint = Constraint(disjunctionRelaxationBlock.lbub) transBlock.add_component(name, newConstraint) # add mapping of transformed constraint to original constraint constraintMap['srcConstraints'][newConstraint] = obj for i in sorted(obj.keys()): c = obj[i] if not c.active: continue lower = (None, None, None) upper = (None, None, None) # first, we see if an M value was specified in the arguments. # (This returns None if not) lower, upper = self._get_M_from_args(c, bigMargs, arg_list, lower, upper) M = (lower[0], upper[0]) if self._generate_debug_messages: _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint '%s' " "from the BigM argument is %s." % (cons_name, str(M))) # if we didn't get something we need from args, try suffixes: if (M[0] is None and c.lower is not None) or \ (M[1] is None and c.upper is not None): # first get anything parent to c but below disjunct suffix_list = self._get_bigm_suffix_list(c.parent_block(), stopping_block=disjunct) # prepend that to what we already collected for the disjunct. suffix_list.extend(disjunct_suffix_list) lower, upper = self._update_M_from_suffixes(c, suffix_list, lower, upper) M = (lower[0], upper[0]) if self._generate_debug_messages: _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint '%s' " "after checking suffixes is %s." % (cons_name, str(M))) if c.lower is not None and M[0] is None: M = (self._estimate_M(c.body, name)[0] - c.lower, M[1]) lower = (M[0], None, None) if c.upper is not None and M[1] is None: M = (M[0], self._estimate_M(c.body, name)[1] - c.upper) upper = (M[1], None, None) if self._generate_debug_messages: _name = obj.getname( fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug("GDP(BigM): The value for M for constraint '%s' " "after estimating (if needed) is %s." % (cons_name, str(M))) # save the source information bigm_src[c] = (lower, upper) # Handle indices for both SimpleConstraint and IndexedConstraint if i.__class__ is tuple: i_lb = i + ('lb',) i_ub = i + ('ub',) elif obj.is_indexed(): i_lb = (i, 'lb',) i_ub = (i, 'ub',) else: i_lb = 'lb' i_ub = 'ub' if c.lower is not None: if M[0] is None: raise GDP_Error("Cannot relax disjunctive constraint '%s' " "because M is not defined." % name) M_expr = M[0] * (1 - disjunct.indicator_var) newConstraint.add(i_lb, c.lower <= c. body - M_expr) constraintMap[ 'transformedConstraints'][c] = [newConstraint[i_lb]] constraintMap['srcConstraints'][newConstraint[i_lb]] = c if c.upper is not None: if M[1] is None: raise GDP_Error("Cannot relax disjunctive constraint '%s' " "because M is not defined." % name) M_expr = M[1] * (1 - disjunct.indicator_var) newConstraint.add(i_ub, c.body - M_expr <= c.upper) transformed = constraintMap['transformedConstraints'].get(c) if transformed is not None: constraintMap['transformedConstraints'][ c].append(newConstraint[i_ub]) else: constraintMap[ 'transformedConstraints'][c] = [newConstraint[i_ub]] constraintMap['srcConstraints'][newConstraint[i_ub]] = c # deactivate because we relaxed c.deactivate()
def _transform_disjunct(self, obj, transBlock, varSet, localVars): if hasattr(obj, "_gdp_transformation_info"): infodict = obj._gdp_transformation_info # If the user has something with our name that is not a dict, we # scream. If they have a dict with this name then we are just going # to use it... if type(infodict) is not dict: raise GDP_Error( "Disjunct %s contains an attribute named " "_gdp_transformation_info. The transformation requires " "that it can create this attribute!" % obj.name) else: infodict = obj._gdp_transformation_info = {} # deactivated means either we've already transformed or user deactivated if not obj.active: if obj.indicator_var.is_fixed(): if value(obj.indicator_var) == 0: # 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." % (obj.name, value(obj.indicator_var))) if not infodict.get('relaxed', False): 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." % (obj.name, )) if 'chull' in infodict: # we've transformed it (with CHull), so don't do it again. return # add reference to original disjunct to info dict on # transformation block relaxedDisjuncts = transBlock.relaxedDisjuncts relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)] relaxationBlockInfo = relaxationBlock._gdp_transformation_info = { 'src': obj, 'srcVars': ComponentMap(), 'srcConstraints': ComponentMap(), 'boundConstraintToSrcVar': ComponentMap(), } infodict['chull'] = chull = { 'relaxationBlock': relaxationBlock, 'relaxedConstraints': ComponentMap(), 'disaggregatedVars': ComponentMap(), 'bigmConstraints': ComponentMap(), } # if this is a disjunctData from an indexed disjunct, we are # going to want to check at the end that the container is # deactivated if everything in it is. So we save it in our # dictionary of things to check if it isn't there already. disjParent = obj.parent_component() if disjParent.is_indexed() and \ disjParent not in transBlock.disjContainers: transBlock.disjContainers.add(disjParent) # add the disaggregated variables and their bigm constraints # to the relaxationBlock for var in varSet: lb = var.lb ub = var.ub if lb is None or ub is None: raise GDP_Error("Variables that appear in disjuncts must be " "bounded in order to use the chull " "transformation! Missing bound for %s." % (var.name)) disaggregatedVar = Var(within=Reals, bounds=(min(0, lb), max(0, ub)), initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we # get a unique name disaggregatedVarName = unique_component_name( relaxationBlock, var.local_name) relaxationBlock.add_component(disaggregatedVarName, disaggregatedVar) chull['disaggregatedVars'][var] = disaggregatedVar relaxationBlockInfo['srcVars'][disaggregatedVar] = var bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(disaggregatedVarName + "_bounds", bigmConstraint) if lb: bigmConstraint.add('lb', obj.indicator_var * lb <= disaggregatedVar) if ub: bigmConstraint.add('ub', disaggregatedVar <= obj.indicator_var * ub) chull['bigmConstraints'][var] = bigmConstraint relaxationBlockInfo['boundConstraintToSrcVar'][ bigmConstraint] = var for var in localVars: lb = var.lb ub = var.ub if lb is None or ub is None: raise GDP_Error("Variables that appear in disjuncts must be " "bounded in order to use the chull " "transformation! Missing bound for %s." % (var.name)) if value(lb) > 0: var.setlb(0) if value(ub) < 0: var.setub(0) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we # get a unique name conName = unique_component_name(relaxationBlock, var.local_name + "_bounds") bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) bigmConstraint.add('lb', obj.indicator_var * lb <= var) bigmConstraint.add('ub', var <= obj.indicator_var * ub) chull['bigmConstraints'][var] = bigmConstraint relaxationBlockInfo['boundConstraintToSrcVar'][ bigmConstraint] = var var_substitute_map = dict( (id(v), newV) for v, newV in iteritems(chull['disaggregatedVars'])) zero_substitute_map = dict( (id(v), NumericConstant(0)) for v, newV in iteritems(chull['disaggregatedVars'])) zero_substitute_map.update( (id(v), NumericConstant(0)) for v in localVars) # Transform each component within this disjunct self._transform_block_components(obj, obj, infodict, var_substitute_map, zero_substitute_map) # deactivate disjunct so we know we've relaxed it obj._deactivate_without_fixing_indicator() infodict['relaxed'] = True
def _xform_constraint(self, obj, disjunct, infodict, var_substitute_map, zero_substitute_map): # we will put a new transformed constraint on the relaxation block. relaxationBlock = infodict['chull']['relaxationBlock'] transBlock = relaxationBlock.parent_block() varMap = infodict['chull']['disaggregatedVars'] # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name name = unique_component_name(relaxationBlock, obj.name) if obj.is_indexed(): try: newConstraint = Constraint(obj.index_set(), transBlock.lbub) except: # The original constraint may have been indexed by a # non-concrete set (like an Any). We will give up on # strict index verification and just blindly proceed. newConstraint = Constraint(Any) else: newConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(name, newConstraint) # add mapping of original constraint to transformed constraint # in transformation info dictionary infodict['chull']['relaxedConstraints'][obj] = newConstraint # add mapping of transformed constraint back to original constraint (we # know that the info dict is already created because this only got # called if we were transforming a disjunct...) relaxationBlock._gdp_transformation_info['srcConstraints'][ newConstraint] = obj for i in sorted(iterkeys(obj)): c = obj[i] if not c.active: continue NL = c.body.polynomial_degree() not in (0, 1) EPS = self._config.EPS mode = self._config.perspective_function # We need to evaluate the expression at the origin *before* # we substitute the expression variables with the # disaggregated variables if not NL or mode == "FurmanSawayaGrossmann": h_0 = clone_without_expression_components( c.body, substitute=zero_substitute_map) y = disjunct.indicator_var if NL: if mode == "LeeGrossmann": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs / y) for var, subs in iteritems(var_substitute_map))) expr = sub_expr * y elif mode == "GrossmannLee": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs / (y + EPS)) for var, subs in iteritems(var_substitute_map))) expr = (y + EPS) * sub_expr elif mode == "FurmanSawayaGrossmann": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs / ((1 - EPS) * y + EPS)) for var, subs in iteritems(var_substitute_map))) expr = ( (1 - EPS) * y + EPS) * sub_expr - EPS * h_0 * (1 - y) else: raise RuntimeError("Unknown NL CHull mode") else: expr = clone_without_expression_components( c.body, substitute=var_substitute_map) if c.equality: if NL: newConsExpr = expr == c.lower * y else: v = list(EXPR.identify_variables(expr)) if len(v) == 1 and not c.lower: # Setting a variable to 0 in a disjunct is # *very* common. We should recognize that in # that structure, the disaggregated variable # will also be fixed to 0. v[0].fix(0) continue newConsExpr = expr - (1 - y) * h_0 == c.lower * y if obj.is_indexed(): newConstraint.add((i, 'eq'), newConsExpr) else: newConstraint.add('eq', newConsExpr) continue if c.lower is not None: # TODO: At the moment there is no reason for this to be in both # lower and upper... I think there could be though if I say what # the new constraint is going to be or something. if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug( "GDP(cHull): Transforming constraint " + "'%s'", c.name) if NL: newConsExpr = expr >= c.lower * y else: newConsExpr = expr - (1 - y) * h_0 >= c.lower * y if obj.is_indexed(): newConstraint.add((i, 'lb'), newConsExpr) else: newConstraint.add('lb', newConsExpr) if c.upper is not None: if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug( "GDP(cHull): Transforming constraint " + "'%s'", c.name) if NL: newConsExpr = expr <= c.upper * y else: newConsExpr = expr - (1 - y) * h_0 <= c.upper * y if obj.is_indexed(): newConstraint.add((i, 'ub'), newConsExpr) else: newConstraint.add('ub', newConsExpr)
def _transform_constraint(self, obj, disjunct, var_substitute_map, zero_substitute_map): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() transBlock = relaxationBlock.parent_block() varMap = relaxationBlock._disaggregatedVarMap['disaggregatedVar'] constraintMap = relaxationBlock._constraintMap # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name name = unique_component_name( relaxationBlock, obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER)) if obj.is_indexed(): newConstraint = Constraint(obj.index_set(), transBlock.lbub) else: newConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(name, newConstraint) # map the containers: # add mapping of original constraint to transformed constraint if obj.is_indexed(): constraintMap['transformedConstraints'][obj] = newConstraint # add mapping of transformed constraint container back to original # constraint container (or SimpleConstraint) constraintMap['srcConstraints'][newConstraint] = obj for i in sorted(iterkeys(obj)): c = obj[i] if not c.active: continue NL = c.body.polynomial_degree() not in (0, 1) EPS = self._config.EPS mode = self._config.perspective_function # We need to evaluate the expression at the origin *before* # we substitute the expression variables with the # disaggregated variables if not NL or mode == "FurmanSawayaGrossmann": h_0 = clone_without_expression_components( c.body, substitute=zero_substitute_map) y = disjunct.indicator_var if NL: if mode == "LeeGrossmann": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs / y) for var, subs in iteritems(var_substitute_map))) expr = sub_expr * y elif mode == "GrossmannLee": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs / (y + EPS)) for var, subs in iteritems(var_substitute_map))) expr = (y + EPS) * sub_expr elif mode == "FurmanSawayaGrossmann": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs / ((1 - EPS) * y + EPS)) for var, subs in iteritems(var_substitute_map))) expr = ( (1 - EPS) * y + EPS) * sub_expr - EPS * h_0 * (1 - y) else: raise RuntimeError("Unknown NL Hull mode") else: expr = clone_without_expression_components( c.body, substitute=var_substitute_map) if c.equality: if NL: # ESJ TODO: This can't happen right? This is the only # obvious case where someone has messed up, but this has to # be nonconvex, right? Shouldn't we tell them? newConsExpr = expr == c.lower * y else: v = list(EXPR.identify_variables(expr)) if len(v) == 1 and not c.lower: # Setting a variable to 0 in a disjunct is # *very* common. We should recognize that in # that structure, the disaggregated variable # will also be fixed to 0. v[0].fix(0) # ESJ: If you ask where the transformed constraint is, # the answer is nowhere. Really, it is in the bounds of # this variable, so I'm going to return # it. Alternatively we could return an empty list, but I # think I like this better. constraintMap['transformedConstraints'][c] = [v[0]] # Reverse map also (this is strange) constraintMap['srcConstraints'][v[0]] = c continue newConsExpr = expr - (1 - y) * h_0 == c.lower * y if obj.is_indexed(): newConstraint.add((i, 'eq'), newConsExpr) # map the _ConstraintDatas (we mapped the container above) constraintMap['transformedConstraints'][c] = [ newConstraint[i, 'eq'] ] constraintMap['srcConstraints'][newConstraint[i, 'eq']] = c else: newConstraint.add('eq', newConsExpr) # map to the _ConstraintData (And yes, for # SimpleConstraints, this is overwriting the map to the # container we made above, and that is what I want to # happen. SimpleConstraints will map to lists. For # IndexedConstraints, we can map the container to the # container, but more importantly, we are mapping the # _ConstraintDatas to each other above) constraintMap['transformedConstraints'][c] = [ newConstraint['eq'] ] constraintMap['srcConstraints'][newConstraint['eq']] = c continue if c.lower is not None: if __debug__ and logger.isEnabledFor(logging.DEBUG): _name = c.getname(fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug( "GDP(Hull): Transforming constraint " + "'%s'", _name) if NL: newConsExpr = expr >= c.lower * y else: newConsExpr = expr - (1 - y) * h_0 >= c.lower * y if obj.is_indexed(): newConstraint.add((i, 'lb'), newConsExpr) constraintMap['transformedConstraints'][c] = [ newConstraint[i, 'lb'] ] constraintMap['srcConstraints'][newConstraint[i, 'lb']] = c else: newConstraint.add('lb', newConsExpr) constraintMap['transformedConstraints'][c] = [ newConstraint['lb'] ] constraintMap['srcConstraints'][newConstraint['lb']] = c if c.upper is not None: if __debug__ and logger.isEnabledFor(logging.DEBUG): _name = c.getname(fully_qualified=True, name_buffer=NAME_BUFFER) logger.debug( "GDP(Hull): Transforming constraint " + "'%s'", _name) if NL: newConsExpr = expr <= c.upper * y else: newConsExpr = expr - (1 - y) * h_0 <= c.upper * y if obj.is_indexed(): newConstraint.add((i, 'ub'), newConsExpr) # map (have to account for fact we might have created list # above transformed = constraintMap['transformedConstraints'].get( c) if transformed is not None: transformed.append(newConstraint[i, 'ub']) else: constraintMap['transformedConstraints'][c] = [ newConstraint[i, 'ub'] ] constraintMap['srcConstraints'][newConstraint[i, 'ub']] = c else: newConstraint.add('ub', newConsExpr) transformed = constraintMap['transformedConstraints'].get( c) if transformed is not None: transformed.append(newConstraint['ub']) else: constraintMap['transformedConstraints'][c] = [ newConstraint['ub'] ] constraintMap['srcConstraints'][newConstraint['ub']] = c # deactivate now that we have transformed obj.deactivate()
def form_linearized_objective_constraints(instance_name, instance, scenario_tree, linearize_nonbinary_penalty_terms, breakpoint_strategy, tolerance): # keep track and return what was added to the instance, so # it can be cleaned up if necessary. new_instance_attributes = [] linearization_index_set_name = "PH_LINEARIZATION_INDEX_SET" linearization_index_set = instance.find_component(linearization_index_set_name) if linearization_index_set is None: linearization_index_set = Set(initialize=range(0, linearize_nonbinary_penalty_terms*2), dimen=1, name=linearization_index_set_name) instance.add_component(linearization_index_set_name, linearization_index_set) scenario = scenario_tree.get_scenario(instance_name) nodeid_to_vardata_map = instance._ScenarioTreeSymbolMap.bySymbol for tree_node in scenario._node_list[:-1]: xbar_dict = tree_node._xbars # if linearizing, then we have previously defined a variable # associated with the result of the linearized approximation # of the penalty term - this is simply added to the objective # function. linearized_cost_variable_name = "PHQUADPENALTY_"+str(tree_node._name) linearized_cost_variable = instance.find_component(linearized_cost_variable_name) # grab the linearization constraint associated with the # linearized cost variable, if it exists. otherwise, create it # - but an empty variety. the constraints are stage-specific - # we could index by constraint, but we don't know if that is # really worth the additional effort. linearization_constraint_name = "PH_LINEARIZATION_"+str(tree_node._name) linearization_constraint = instance.find_component(linearization_constraint_name) if linearization_constraint is not None: # clear whatever constraint components are there - there # may be fewer breakpoints, due to tolerances, and we # don't want to the old pieces laying around. linearization_constraint.clear() else: # this is the first time the constraint is being added - # add it to the list of PH-specific constraints for this # instance. new_instance_attributes.append(linearization_constraint_name) nodal_index_set_name = "PHINDEX_"+str(tree_node._name) nodal_index_set = instance.find_component(nodal_index_set_name) assert nodal_index_set is not None linearization_constraint = \ Constraint(nodal_index_set, linearization_index_set, name=linearization_constraint_name) linearization_constraint.construct() instance.add_component(linearization_constraint_name, linearization_constraint) for variable_id in tree_node._variable_ids: # don't add weight terms for derived variables at the tree # node. if variable_id in tree_node._derived_variable_ids: continue if variable_id not in tree_node._minimums: variable_name, index = tree_node._variable_ids[variable_id] raise RuntimeError("No minimum value statistic found for variable=%s " "on tree node=%s; cannot form linearized PH objective" % (variable_name+indexToString(index), tree_node._name)) if variable_id not in tree_node._maximums: variable_name, index = tree_node._variable_ids[variable_id] raise RuntimeError("No maximums value statistic found for " "variable=%s on tree node=%s; cannot " "form linearized PH objective" % (variable_name+indexToString(index), tree_node._name)) xbar = xbar_dict[variable_id] node_min = tree_node._minimums[variable_id] node_max = tree_node._maximums[variable_id] instance_vardata = nodeid_to_vardata_map[variable_id] if (instance_vardata.stale is False) and (instance_vardata.fixed is False): # binaries have already been dealt with in the process of PH objective function formation. if isinstance(instance_vardata.domain, BooleanSet) is False: x = instance_vardata if x.lb is None or x.ub is None: msg = "Missing bound for variable '%s'\n" \ 'Both lower and upper bounds required when' \ ' piece-wise approximating quadratic ' \ 'penalty terms' raise ValueError(msg % instance_vardata.name) lb = value(x.lb) ub = value(x.ub) # compute the breakpoint sequence according to the specified strategy. try: strategy = (compute_uniform_breakpoints, compute_uniform_between_nodestat_breakpoints, compute_uniform_between_woodruff_breakpoints, compute_exponential_from_mean_breakpoints, )[ breakpoint_strategy ] args = ( lb, node_min, xbar, node_max, ub, \ linearize_nonbinary_penalty_terms, tolerance ) breakpoints = strategy( *args ) except ValueError: e = sys.exc_info()[1] msg = 'A breakpoint distribution strategy (%s) ' \ 'is currently not supported within PH!' raise ValueError(msg % breakpoint_strategy) for i in xrange(len(breakpoints)-1): this_lb = breakpoints[i] this_ub = breakpoints[i+1] segment_tuple = create_piecewise_constraint_tuple(this_lb, this_ub, x, xbar, linearized_cost_variable[variable_id], tolerance) linearization_constraint.add((variable_id,i), segment_tuple) return new_instance_attributes
def _transform_disjunct(self, obj, transBlock, varSet, localVars): # deactivated should only come from the user if not obj.active: if obj.indicator_var.is_fixed(): if value(obj.indicator_var) == 0: # 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." % (obj.name, value(obj.indicator_var))) if obj._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 0.)" % (obj.name, )) if obj._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." % obj.name) # create a relaxation block for this disjunct relaxedDisjuncts = transBlock.relaxedDisjuncts relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)] relaxationBlock.localVarReferences = Block() # Put the disaggregated variables all on their own block so that we can # isolate the name collisions and still have complete control over the # names on this block. (This is for peace of mind now, but will matter # in the future for adding the binaries corresponding to Boolean # indicator vars.) relaxationBlock.disaggregatedVars = Block() # add the map that will link back and forth between transformed # constraints and their originals. relaxationBlock._constraintMap = { 'srcConstraints': ComponentMap(), 'transformedConstraints': ComponentMap() } # Map between disaggregated variables for this disjunct and their # originals relaxationBlock._disaggregatedVarMap = { 'srcVar': ComponentMap(), 'disaggregatedVar': ComponentMap(), } # Map between disaggregated variables and their lb*indicator <= var <= # ub*indicator constraints relaxationBlock._bigMConstraintMap = ComponentMap() # add mappings to source disjunct (so we'll know we've relaxed) obj._transformation_block = weakref_ref(relaxationBlock) relaxationBlock._srcDisjunct = weakref_ref(obj) # add Suffix to the relaxation block that disaggregated variables are # local (in case this is nested in another Disjunct) local_var_set = None parent_disjunct = obj.parent_block() while parent_disjunct is not None: if parent_disjunct.ctype is Disjunct: break parent_disjunct = parent_disjunct.parent_block() if parent_disjunct is not None: localVarSuffix = relaxationBlock.LocalVars = Suffix( direction=Suffix.LOCAL) local_var_set = localVarSuffix[parent_disjunct] = ComponentSet() # add the disaggregated variables and their bigm constraints # to the relaxationBlock for var in varSet: lb = var.lb ub = var.ub if lb is None or ub is None: raise GDP_Error("Variables that appear in disjuncts must be " "bounded in order to use the hull " "transformation! Missing bound for %s." % (var.name)) disaggregatedVar = Var(within=Reals, bounds=(min(0, lb), max(0, ub)), initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we # get a unique name disaggregatedVarName = unique_component_name( relaxationBlock.disaggregatedVars, var.getname(fully_qualified=False, name_buffer=NAME_BUFFER), ) relaxationBlock.disaggregatedVars.add_component( disaggregatedVarName, disaggregatedVar) # mark this as local because we won't re-disaggregate if this is a # nested disjunction if local_var_set is not None: local_var_set.add(disaggregatedVar) # store the mappings from variables to their disaggregated selves on # the transformation block. relaxationBlock._disaggregatedVarMap['disaggregatedVar'][ var] = disaggregatedVar relaxationBlock._disaggregatedVarMap['srcVar'][ disaggregatedVar] = var bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(disaggregatedVarName + "_bounds", bigmConstraint) if lb: bigmConstraint.add('lb', obj.indicator_var * lb <= disaggregatedVar) if ub: bigmConstraint.add('ub', disaggregatedVar <= obj.indicator_var * ub) relaxationBlock._bigMConstraintMap[ disaggregatedVar] = bigmConstraint for var in localVars: lb = var.lb ub = var.ub if lb is None or ub is None: raise GDP_Error("Variables that appear in disjuncts must be " "bounded in order to use the hull " "transformation! Missing bound for %s." % (var.name)) if value(lb) > 0: var.setlb(0) if value(ub) < 0: var.setub(0) # map it to itself relaxationBlock._disaggregatedVarMap['disaggregatedVar'][var] = var relaxationBlock._disaggregatedVarMap['srcVar'][var] = var # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we # get a unique name conName = unique_component_name( relaxationBlock, var.getname(fully_qualified=False, name_buffer=NAME_BUFFER) + \ "_bounds") bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) if lb: bigmConstraint.add('lb', obj.indicator_var * lb <= var) if ub: bigmConstraint.add('ub', var <= obj.indicator_var * ub) relaxationBlock._bigMConstraintMap[var] = bigmConstraint var_substitute_map = dict((id(v), newV) for v, newV in iteritems( relaxationBlock._disaggregatedVarMap['disaggregatedVar'])) zero_substitute_map = dict((id(v), ZeroConstant) for v, newV in \ iteritems( relaxationBlock._disaggregatedVarMap[ 'disaggregatedVar'])) zero_substitute_map.update((id(v), ZeroConstant) for v in localVars) # Transform each component within this disjunct self._transform_block_components(obj, obj, var_substitute_map, zero_substitute_map) # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator()
def _xform_constraint(self, obj, disjunct, infodict, bigMargs, suffix_list): # add constraint to the transformation block, we'll transform it there. relaxationBlock = infodict['bigm']['relaxationBlock'] transBlock = relaxationBlock.parent_block() # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name name = unique_component_name(relaxationBlock, obj.name) if obj.is_indexed(): try: newConstraint = Constraint(obj.index_set(), transBlock.lbub) except TypeError: # The original constraint may have been indexed by a # non-concrete set (like an Any). We will give up on # strict index verification and just blindly proceed. newConstraint = Constraint(Any) else: newConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(name, newConstraint) # add mapping of original constraint to transformed constraint # in transformation info dictionary infodict['bigm']['relaxedConstraints'][obj] = newConstraint # add mapping of transformed constraint back to original constraint (we # know that the info dict is already created because this only got # called if we were transforming a disjunct...) relaxationBlock._gdp_transformation_info['srcConstraints'][ newConstraint] = obj for i in sorted(iterkeys(obj)): c = obj[i] if not c.active: continue # first, we see if an M value was specified in the arguments. # (This returns None if not) M = self._get_M_from_args(c, bigMargs) if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug("GDP(BigM): The value for M for constraint %s " "from the BigM argument is %s." % (obj.name, str(M))) # if we didn't get something from args, try suffixes: if M is None: M = self._get_M_from_suffixes(c, suffix_list) if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug("GDP(BigM): The value for M for constraint %s " "after checking suffixes is %s." % (obj.name, str(M))) if not isinstance(M, (tuple, list)): if M is None: M = (None, None) else: try: M = (-M, M) except: logger.error("Error converting scalar M-value %s " "to (-M,M). Is %s not a numeric type?" % (M, type(M))) raise if len(M) != 2: raise GDP_Error("Big-M %s for constraint %s is not of " "length two. " "Expected either a single value or " "tuple or list of length two for M." % (str(M), name)) if c.lower is not None and M[0] is None: M = (self._estimate_M(c.body, name)[0] - c.lower, M[1]) if c.upper is not None and M[1] is None: M = (M[0], self._estimate_M(c.body, name)[1] - c.upper) if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug("GDP(BigM): The value for M for constraint %s " "after estimating (if needed) is %s." % (obj.name, str(M))) # Handle indices for both SimpleConstraint and IndexedConstraint if i.__class__ is tuple: i_lb = i + ('lb', ) i_ub = i + ('ub', ) elif obj.is_indexed(): i_lb = ( i, 'lb', ) i_ub = ( i, 'ub', ) else: i_lb = 'lb' i_ub = 'ub' if c.lower is not None: if M[0] is None: raise GDP_Error("Cannot relax disjunctive constraint %s " "because M is not defined." % name) M_expr = M[0] * (1 - disjunct.indicator_var) newConstraint.add(i_lb, c.lower <= c.body - M_expr) if c.upper is not None: if M[1] is None: raise GDP_Error("Cannot relax disjunctive constraint %s " "because M is not defined." % name) M_expr = M[1] * (1 - disjunct.indicator_var) newConstraint.add(i_ub, c.body - M_expr <= c.upper)
def _xform_constraint(self, obj, disjunct, infodict, bigMargs, suffix_list): # add constraint to the transformation block, we'll transform it there. relaxationBlock = infodict['bigm']['relaxationBlock'] transBlock = relaxationBlock.parent_block() # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name name = unique_component_name(relaxationBlock, obj.name) if obj.is_indexed(): try: newConstraint = Constraint(obj.index_set(), transBlock.lbub) except TypeError: # The original constraint may have been indexed by a # non-concrete set (like an Any). We will give up on # strict index verification and just blindly proceed. newConstraint = Constraint(Any) else: newConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(name, newConstraint) # add mapping of original constraint to transformed constraint # in transformation info dictionary infodict['bigm']['relaxedConstraints'][obj] = newConstraint # add mapping of transformed constraint back to original constraint (we # know that the info dict is already created because this only got # called if we were transforming a disjunct...) relaxationBlock._gdp_transformation_info['srcConstraints'][ newConstraint] = obj for i in sorted(iterkeys(obj)): c = obj[i] if not c.active: continue # first, we see if an M value was specified in the arguments. # (This returns None if not) M = self._get_M_from_args(c, bigMargs) if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug("GDP(BigM): The value for M for constraint %s " "from the BigM argument is %s." % (obj.name, str(M))) # if we didn't get something from args, try suffixes: if M is None: M = self._get_M_from_suffixes(c, suffix_list) if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug("GDP(BigM): The value for M for constraint %s " "after checking suffixes is %s." % (obj.name, str(M))) if not isinstance(M, (tuple, list)): if M is None: M = (None, None) else: try: M = (-M, M) except: logger.error("Error converting scalar M-value %s " "to (-M,M). Is %s not a numeric type?" % (M, type(M))) raise if len(M) != 2: raise GDP_Error("Big-M %s for constraint %s is not of " "length two. " "Expected either a single value or " "tuple or list of length two for M." % (str(M), name)) if c.lower is not None and M[0] is None: M = (self._estimate_M(c.body, name)[0] - c.lower, M[1]) if c.upper is not None and M[1] is None: M = (M[0], self._estimate_M(c.body, name)[1] - c.upper) if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug("GDP(BigM): The value for M for constraint %s " "after estimating (if needed) is %s." % (obj.name, str(M))) # Handle indices for both SimpleConstraint and IndexedConstraint if i.__class__ is tuple: i_lb = i + ('lb',) i_ub = i + ('ub',) elif obj.is_indexed(): i_lb = (i, 'lb',) i_ub = (i, 'ub',) else: i_lb = 'lb' i_ub = 'ub' if c.lower is not None: if M[0] is None: raise GDP_Error("Cannot relax disjunctive constraint %s " "because M is not defined." % name) M_expr = M[0] * (1 - disjunct.indicator_var) newConstraint.add(i_lb, c.lower <= c. body - M_expr) if c.upper is not None: if M[1] is None: raise GDP_Error("Cannot relax disjunctive constraint %s " "because M is not defined." % name) M_expr = M[1] * (1 - disjunct.indicator_var) newConstraint.add(i_ub, c.body - M_expr <= c.upper)
def _xform_constraint(self, obj, disjunct, infodict, var_substitute_map, zero_substitute_map): # we will put a new transformed constraint on the relaxation block. relaxationBlock = infodict['chull']['relaxationBlock'] transBlock = relaxationBlock.parent_block() varMap = infodict['chull']['disaggregatedVars'] # Though rare, it is possible to get naming conflicts here # since constraints from all blocks are getting moved onto the # same block. So we get a unique name name = unique_component_name(relaxationBlock, obj.name) if obj.is_indexed(): try: newConstraint = Constraint(obj.index_set(), transBlock.lbub) except: # The original constraint may have been indexed by a # non-concrete set (like an Any). We will give up on # strict index verification and just blindly proceed. newConstraint = Constraint(Any) else: newConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(name, newConstraint) # add mapping of original constraint to transformed constraint # in transformation info dictionary infodict['chull']['relaxedConstraints'][obj] = newConstraint # add mapping of transformed constraint back to original constraint (we # know that the info dict is already created because this only got # called if we were transforming a disjunct...) relaxationBlock._gdp_transformation_info['srcConstraints'][ newConstraint] = obj for i in sorted(iterkeys(obj)): c = obj[i] if not c.active: continue NL = c.body.polynomial_degree() not in (0,1) EPS = self._config.EPS mode = self._config.perspective_function # We need to evaluate the expression at the origin *before* # we substitute the expression variables with the # disaggregated variables if not NL or mode == "FurmanSawayaGrossmann": h_0 = clone_without_expression_components( c.body, substitute=zero_substitute_map) y = disjunct.indicator_var if NL: if mode == "LeeGrossmann": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs/y) for var, subs in iteritems(var_substitute_map) ) ) expr = sub_expr * y elif mode == "GrossmannLee": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs/(y + EPS)) for var, subs in iteritems(var_substitute_map) ) ) expr = (y + EPS) * sub_expr elif mode == "FurmanSawayaGrossmann": sub_expr = clone_without_expression_components( c.body, substitute=dict( (var, subs/((1 - EPS)*y + EPS)) for var, subs in iteritems(var_substitute_map) ) ) expr = ((1-EPS)*y + EPS)*sub_expr - EPS*h_0*(1-y) else: raise RuntimeError("Unknown NL CHull mode") else: expr = clone_without_expression_components( c.body, substitute=var_substitute_map) if c.equality: if NL: newConsExpr = expr == c.lower*y else: v = list(EXPR.identify_variables(expr)) if len(v) == 1 and not c.lower: # Setting a variable to 0 in a disjunct is # *very* common. We should recognize that in # that structure, the disaggregated variable # will also be fixed to 0. v[0].fix(0) continue newConsExpr = expr - (1-y)*h_0 == c.lower*y if obj.is_indexed(): newConstraint.add((i, 'eq'), newConsExpr) else: newConstraint.add('eq', newConsExpr) continue if c.lower is not None: # TODO: At the moment there is no reason for this to be in both # lower and upper... I think there could be though if I say what # the new constraint is going to be or something. if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug("GDP(cHull): Transforming constraint " + "'%s'", c.name) if NL: newConsExpr = expr >= c.lower*y else: newConsExpr = expr - (1-y)*h_0 >= c.lower*y if obj.is_indexed(): newConstraint.add((i, 'lb'), newConsExpr) else: newConstraint.add('lb', newConsExpr) if c.upper is not None: if __debug__ and logger.isEnabledFor(logging.DEBUG): logger.debug("GDP(cHull): Transforming constraint " + "'%s'", c.name) if NL: newConsExpr = expr <= c.upper*y else: newConsExpr = expr - (1-y)*h_0 <= c.upper*y if obj.is_indexed(): newConstraint.add((i, 'ub'), newConsExpr) else: newConstraint.add('ub', newConsExpr)
def _transform_disjunct(self, obj, transBlock, varSet, localVars): if hasattr(obj, "_gdp_transformation_info"): infodict = obj._gdp_transformation_info # If the user has something with our name that is not a dict, we # scream. If they have a dict with this name then we are just going # to use it... if type(infodict) is not dict: raise GDP_Error( "Disjunct %s contains an attribute named " "_gdp_transformation_info. The transformation requires " "that it can create this attribute!" % obj.name) else: infodict = obj._gdp_transformation_info = {} # deactivated means either we've already transformed or user deactivated if not obj.active: if obj.indicator_var.is_fixed(): if value(obj.indicator_var) == 0: # 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." % ( obj.name, value(obj.indicator_var) )) if not infodict.get('relaxed', False): 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." % ( obj.name, )) if 'chull' in infodict: # we've transformed it (with CHull), so don't do it again. return # add reference to original disjunct to info dict on # transformation block relaxedDisjuncts = transBlock.relaxedDisjuncts relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)] relaxationBlockInfo = relaxationBlock._gdp_transformation_info = { 'src': obj, 'srcVars': ComponentMap(), 'srcConstraints': ComponentMap(), 'boundConstraintToSrcVar': ComponentMap(), } infodict['chull'] = chull = { 'relaxationBlock': relaxationBlock, 'relaxedConstraints': ComponentMap(), 'disaggregatedVars': ComponentMap(), 'bigmConstraints': ComponentMap(), } # if this is a disjunctData from an indexed disjunct, we are # going to want to check at the end that the container is # deactivated if everything in it is. So we save it in our # dictionary of things to check if it isn't there already. disjParent = obj.parent_component() if disjParent.is_indexed() and \ disjParent not in transBlock.disjContainers: transBlock.disjContainers.add(disjParent) # add the disaggregated variables and their bigm constraints # to the relaxationBlock for var in varSet: lb = var.lb ub = var.ub if lb is None or ub is None: raise GDP_Error("Variables that appear in disjuncts must be " "bounded in order to use the chull " "transformation! Missing bound for %s." % (var.name)) disaggregatedVar = Var(within=Reals, bounds=(min(0, lb), max(0, ub)), initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we # get a unique name disaggregatedVarName = unique_component_name( relaxationBlock, var.local_name) relaxationBlock.add_component( disaggregatedVarName, disaggregatedVar) chull['disaggregatedVars'][var] = disaggregatedVar relaxationBlockInfo['srcVars'][disaggregatedVar] = var bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component( disaggregatedVarName + "_bounds", bigmConstraint) if lb: bigmConstraint.add( 'lb', obj.indicator_var*lb <= disaggregatedVar) if ub: bigmConstraint.add( 'ub', disaggregatedVar <= obj.indicator_var*ub) chull['bigmConstraints'][var] = bigmConstraint relaxationBlockInfo['boundConstraintToSrcVar'][bigmConstraint] = var for var in localVars: lb = var.lb ub = var.ub if lb is None or ub is None: raise GDP_Error("Variables that appear in disjuncts must be " "bounded in order to use the chull " "transformation! Missing bound for %s." % (var.name)) if value(lb) > 0: var.setlb(0) if value(ub) < 0: var.setub(0) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we # get a unique name conName = unique_component_name( relaxationBlock, var.local_name+"_bounds") bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) bigmConstraint.add('lb', obj.indicator_var*lb <= var) bigmConstraint.add('ub', var <= obj.indicator_var*ub) chull['bigmConstraints'][var] = bigmConstraint relaxationBlockInfo['boundConstraintToSrcVar'][bigmConstraint] = var var_substitute_map = dict((id(v), newV) for v, newV in iteritems(chull['disaggregatedVars'])) zero_substitute_map = dict((id(v), ZeroConstant) for v, newV in iteritems(chull['disaggregatedVars'])) zero_substitute_map.update((id(v), ZeroConstant) for v in localVars) # Transform each component within this disjunct self._transform_block_components(obj, obj, infodict, var_substitute_map, zero_substitute_map) # deactivate disjunct so we know we've relaxed it obj._deactivate_without_fixing_indicator() infodict['relaxed'] = True