def _bilinear_expressions(model): # TODO for now, we look for only expressions where the bilinearities are # exposed on the root level SumExpression, and thus accessible via # generate_standard_repn. This will not detect exp(x*y). We require a # factorization transformation to be applied beforehand in order to pick # these constraints up. pass # Bilinear map will be stored in the format: # x --> (y --> [constr1, constr2, ...], z --> [constr2, constr3]) bilinear_map = ComponentMap() for constr in model.component_data_objects(Constraint, active=True, descend_into=(Block, Disjunct)): if constr.body.polynomial_degree() in (1, 0): continue # Skip trivial and linear constraints repn = generate_standard_repn(constr.body) for pair in repn.quadratic_vars: v1, v2 = pair v1_pairs = bilinear_map.get(v1, ComponentMap()) if v2 in v1_pairs: # bilinear term has been found before. Simply add constraint to # the set associated with the bilinear term. v1_pairs[v2].add(constr) else: # We encounter the bilinear term for the first time. bilinear_map[v1] = v1_pairs bilinear_map[v2] = bilinear_map.get(v2, ComponentMap()) constraints_with_bilinear_pair = ComponentSet([constr]) bilinear_map[v1][v2] = constraints_with_bilinear_pair bilinear_map[v2][v1] = constraints_with_bilinear_pair return bilinear_map
def _build_equality_set(model): """Construct an equality set map. Maps all variables to the set of variables that are linked to them by equality. Mapping takes place using id(). That is, if you have x = y, then you would have id(x) -> ComponentSet([x, y]) and id(y) -> ComponentSet([x, y]) in the mapping. """ # Map of variables to their equality set (ComponentSet) eq_var_map = ComponentMap() # Loop through all the active constraints in the model for constraint in model.component_data_objects(ctype=Constraint, active=True, descend_into=True): eq_linked_vars = _get_equality_linked_variables(constraint) if not eq_linked_vars: continue # if we get an empty tuple, skip to next constraint. v1, v2 = eq_linked_vars set1 = eq_var_map.get(v1, ComponentSet((v1, v2))) set2 = eq_var_map.get(v2, (v2, )) # if set1 and set2 are equivalent, skip to next constraint. if set1 is set2: continue # add all elements of set2 to set 1 set1.update(set2) # Update all elements to point to set 1 for v in set1: eq_var_map[v] = set1 return eq_var_map
def test_getsetdelitem(self): cmap = ComponentMap() for c, val in self._components: self.assertTrue(c not in cmap) for c, val in self._components: cmap[c] = val self.assertEqual(cmap[c], val) self.assertEqual(cmap.get(c), val) del cmap[c] with self.assertRaises(KeyError): cmap[c] with self.assertRaises(KeyError): del cmap[c] self.assertEqual(cmap.get(c), None)
def _build_equality_set(m): """Construct an equality set map. Maps all variables to the set of variables that are linked to them by equality. Mapping takes place using id(). That is, if you have x = y, then you would have id(x) -> ComponentSet([x, y]) and id(y) -> ComponentSet([x, y]) in the mapping. """ #: dict: map of var UID to the set of all equality-linked var UIDs eq_var_map = ComponentMap() relevant_vars = ComponentSet() for constr in m.component_data_objects(ctype=Constraint, active=True, descend_into=True): # Check to make sure the constraint is of form v1 - v2 == 0 if (value(constr.lower) == 0 and value(constr.upper) == 0 and constr.body.polynomial_degree() == 1): repn = generate_standard_repn(constr.body) # only take the variables with nonzero coefficients vars_ = [v for i, v in enumerate(repn.linear_vars) if repn.linear_coefs[i]] if (len(vars_) == 2 and repn.constant == 0 and sorted(l for l in repn.linear_coefs if l) == [-1, 1]): # this is an a == b constraint. v1 = vars_[0] v2 = vars_[1] set1 = eq_var_map.get(v1, ComponentSet([v1])) set2 = eq_var_map.get(v2, ComponentSet([v2])) relevant_vars.update([v1, v2]) set1.update(set2) # set1 is now the union for v in set1: eq_var_map[v] = set1 return eq_var_map, relevant_vars
def _map_variable_stages(model): variable_stage_annotation = locate_annotations(model, VariableStageAnnotation, max_allowed=1) if len(variable_stage_annotation) == 0: raise ValueError("Reference model is missing variable stage " "annotation: %s" % (VariableStageAnnotation.__name__)) else: assert len(variable_stage_annotation) == 1 variable_stage_annotation = variable_stage_annotation[0][1] variable_stage_assignments = ComponentMap( variable_stage_annotation.expand_entries()) if len(variable_stage_assignments) == 0: raise ValueError("At least one variable stage assignment " "is required.") min_stagenumber = min(variable_stage_assignments.values(), key=lambda x: x[0])[0] max_stagenumber = max(variable_stage_assignments.values(), key=lambda x: x[0])[0] if max_stagenumber > 2: for var, (stagenum, derived) in \ variable_stage_assignments.items(): if stagenum > 2: raise ValueError( "Embedded stochastic programs must be two-stage " "(for now), but variable with name '%s' has been " "annotated with stage number: %s" % (var.name, stagenum)) stage_to_variables_map = {} stage_to_variables_map[1] = [] stage_to_variables_map[2] = [] for var in model.component_data_objects( Var, active=True, descend_into=True, sort=SortComponents.alphabetizeComponentAndIndex): stagenumber, derived = \ variable_stage_assignments.get(var, (2, False)) if (stagenumber != 1) and (stagenumber != 2): raise ValueError("Invalid stage annotation for variable with " "name '%s'. Stage assignment must be 1 or 2. " "Current value: %s" % (var.name, stagenumber)) if (stagenumber == 1): stage_to_variables_map[1].append((var, derived)) else: assert stagenumber == 2 stage_to_variables_map[2].append((var, derived)) variable_to_stage_map = ComponentMap() for stagenum, stagevars in stage_to_variables_map.items(): for var, derived in stagevars: variable_to_stage_map[var] = (stagenum, derived) return (stage_to_variables_map, variable_to_stage_map, variable_stage_assignments)
def determine_valid_values(block, discr_var_to_constrs_map, config): """Calculate valid values for each effectively discrete variable. We need the set of possible values for the effectively discrete variable in order to do the reformulations. Right now, we select a naive approach where we look for variables in the discreteness-inducing constraints. We then adjust their values and see if things are stil feasible. Based on their coefficient values, we can infer a set of allowable values for the effectively discrete variable. Args: block: The model or a disjunct on the model. """ possible_values = ComponentMap() for eff_discr_var, constrs in discr_var_to_constrs_map.items(): # get the superset of possible values by looking through the # constraints for constr in constrs: repn = generate_standard_repn(constr.body) var_coef = sum(coef for i, coef in enumerate(repn.linear_coefs) if repn.linear_vars[i] is eff_discr_var) const = -(repn.constant - constr.upper) / var_coef possible_vals = set((const, )) for i, var in enumerate(repn.linear_vars): if var is eff_discr_var: continue coef = -repn.linear_coefs[i] / var_coef if var.is_binary(): var_values = (0, coef) elif var.is_integer(): var_values = [v * coef for v in range(var.lb, var.ub + 1)] else: raise ValueError( '%s has unacceptable variable domain: %s' % (var.name, var.domain)) possible_vals = set( (v1 + v2 for v1 in possible_vals for v2 in var_values)) old_possible_vals = possible_values.get(eff_discr_var, None) if old_possible_vals is not None: possible_values[ eff_discr_var] = old_possible_vals & possible_vals else: possible_values[eff_discr_var] = possible_vals possible_values = prune_possible_values(block, possible_values, config) return possible_values
def detect_effectively_discrete_vars(block, equality_tolerance): """Detect effectively discrete variables. These continuous variables are the sum of discrete variables. """ # Map of effectively_discrete var --> inducing constraints effectively_discrete = ComponentMap() for constr in block.component_data_objects(Constraint, active=True): if constr.lower is None or constr.upper is None: continue # skip inequality constraints if fabs(value(constr.lower) - value(constr.upper)) > equality_tolerance: continue # not equality constriant. Skip. if constr.body.polynomial_degree() not in (1, 0): continue # skip nonlinear expressions repn = generate_standard_repn(constr.body) if len(repn.linear_vars) < 2: # TODO should this be < 2 or < 1? # TODO we should make sure that trivial equality relations are # preprocessed before this, or we will end up reformulating # expressions that we do not need to here. continue non_discrete_vars = list(v for v in repn.linear_vars if v.is_continuous()) if len(non_discrete_vars) == 1: # We know that this is an effectively discrete continuous # variable. Add it to our identified variable list. var = non_discrete_vars[0] inducing_constraints = effectively_discrete.get(var, []) inducing_constraints.append(constr) effectively_discrete[var] = inducing_constraints # TODO we should eventually also look at cases where all other # non_discrete_vars are effectively_discrete_vars return effectively_discrete
class _PortData(ComponentData): """ This class defines the data for a single Port Attributes ---------- vars:`dict` A dictionary mapping added names to variables """ __slots__ = ('vars', '_arcs', '_sources', '_dests', '_rules', '_splitfracs') def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: # - ComponentData # - NumericValue self._component = weakref_ref(component) if (component is not None) \ else None self.vars = {} self._arcs = [] self._sources = [] self._dests = [] self._rules = {} self._splitfracs = ComponentMap() def __getstate__(self): state = super(_PortData, self).__getstate__() for i in _PortData.__slots__: state[i] = getattr(self, i) # Remove/resolve weak references for i in ('_arcs', '_sources', '_dests'): state[i] = [ref() for ref in state[i]] return state def __setstate__(self, state): state['_arcs'] = [weakref_ref(i) for i in state['_arcs']] state['_sources'] = [weakref_ref(i) for i in state['_sources']] state['_dests'] = [weakref_ref(i) for i in state['_dests']] super(_PortData, self).__setstate__(state) # Note: None of the slots on this class need to be edited, so we # don't need to implement a specialized __setstate__ method, and # can quietly rely on the super() class's implementation. def __getattr__(self, name): """Returns `self.vars[name]` if it exists""" if name in self.vars: return self.vars[name] # Since the base classes don't support getattr, we can just # throw the "normal" AttributeError raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) def arcs(self, active=None): """A list of Arcs in which this Port is a member""" return self._collect_ports(active, self._arcs) def sources(self, active=None): """A list of Arcs in which this Port is a destination""" return self._collect_ports(active, self._sources) def dests(self, active=None): """A list of Arcs in which this Port is a source""" return self._collect_ports(active, self._dests) def _collect_ports(self, active, port_list): # need to call the weakrefs if active is None: return [_a() for _a in port_list] tmp = [] for _a in port_list: a = _a() if a.active == active: tmp.append(a) return tmp def set_value(self, value): """Cannot specify the value of a port""" raise ValueError("Cannot specify the value of a port: '%s'" % self.name) def polynomial_degree(self): """Returns the maximum polynomial degree of all port members""" ans = 0 for v in self.iter_vars(): tmp = v.polynomial_degree() if tmp is None: return None ans = max(ans, tmp) return ans def is_fixed(self): """Return True if all vars/expressions in the Port are fixed""" return all(v.is_fixed() for v in self.iter_vars()) def is_potentially_variable(self): """Return True as ports may (should!) contain variables""" return True def is_binary(self): """Return True if all variables in the Port are binary""" return len(self) and all(v.is_binary() for v in self.iter_vars(expr_vars=True)) def is_integer(self): """Return True if all variables in the Port are integer""" return len(self) and all(v.is_integer() for v in self.iter_vars(expr_vars=True)) def is_continuous(self): """Return True if all variables in the Port are continuous""" return len(self) and all(v.is_continuous() for v in self.iter_vars(expr_vars=True)) def add(self, var, name=None, rule=None, **kwds): """ Add `var` to this Port, casting it to a Pyomo numeric if necessary Arguments --------- var A variable or some `NumericValue` like an expression name: `str` Name to associate with this member of the Port rule: `function` Function implementing the desired expansion procedure for this member. `Port.Equality` by default, other options include `Port.Extensive`. Customs are allowed. kwds Keyword arguments that will be passed to rule """ if var is not None: try: # indexed components are ok, but as_numeric will error on them # make sure they have this attribute var.is_indexed() except AttributeError: var = as_numeric(var) if name is None: name = var.local_name if name in self.vars and self.vars[name] is not None: # don't throw warning if replacing an implicit (None) var logger.warning("Implicitly replacing variable '%s' in Port '%s'.\n" "To avoid this warning, use Port.remove() first." % (name, self.name)) self.vars[name] = var if rule is None: rule = Port.Equality if rule is Port.Extensive: # avoid name collisions if (name.endswith("_split") or name.endswith("_equality") or name == "splitfrac"): raise ValueError( "Extensive variable '%s' on Port '%s' may not end " "with '_split' or '_equality'" % (name, self.name)) self._rules[name] = (rule, kwds) def remove(self, name): """Remove this member from the port""" if name not in self.vars: raise ValueError("Cannot remove member '%s' not in Port '%s'" % (name, self.name)) self.vars.pop(name) self._rules.pop(name) def rule_for(self, name): """Return the rule associated with the given port member""" return self._rules[name][0] def is_equality(self, name): """Return True if the rule for this port member is Port.Equality""" return self.rule_for(name) is Port.Equality def is_extensive(self, name): """Return True if the rule for this port member is Port.Extensive""" return self.rule_for(name) is Port.Extensive def fix(self): """ Fix all variables in the port at their current values. For expressions, fix every variable in the expression. """ for v in self.iter_vars(expr_vars=True, fixed=False): v.fix() def unfix(self): """ Unfix all variables in the port. For expressions, unfix every variable in the expression. """ for v in self.iter_vars(expr_vars=True, fixed=True): v.unfix() free = unfix def iter_vars(self, expr_vars=False, fixed=None, names=False): """ Iterate through every member of the port, going through the indices of indexed members. Arguments --------- expr_vars: `bool` If True, call `identify_variables` on expression type members fixed: `bool` Only include variables/expressions with this type of fixed names: `bool` If True, yield (name, index, var/expr) tuples """ for name, mem in self.vars.items(): if not mem.is_indexed(): itr = {None: mem} else: itr = mem for idx, v in itr.items(): if fixed is not None and v.is_fixed() != fixed: continue if expr_vars and v.is_expression_type(): for var in identify_variables(v): if fixed is not None and var.is_fixed() != fixed: continue if names: yield name, idx, var else: yield var else: if names: yield name, idx, v else: yield v def set_split_fraction(self, arc, val, fix=True): """ Set the split fraction value to be used for an arc during arc expansion when using `Port.Extensive`. """ if arc not in self.dests(): raise ValueError("Port '%s' is not a source of Arc '%s', cannot " "set split fraction" % (self.name, arc.name)) self._splitfracs[arc] = (val, fix) def get_split_fraction(self, arc): """ Returns a tuple (val, fix) for the split fraction of this arc that was set via `set_split_fraction` if it exists, and otherwise None. """ res = self._splitfracs.get(arc, None) if res is None: return None else: return res
def _transform_disjunctionData(self, obj, index, transBlock=None): if not obj.active: return # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: raise GDP_Error("Cannot do hull reformulation for " "Disjunction '%s' with OR constraint. " "Must be an XOR!" % obj.name) if transBlock is None: # It's possible that we have already created a transformation block # for another disjunctionData from this same container. If that's # the case, let's use the same transformation block. (Else it will # be really confusing that the XOR constraint goes to that old block # but we create a new one here.) if obj.parent_component()._algebraic_constraint is not None: transBlock = obj.parent_component()._algebraic_constraint().\ parent_block() else: transBlock = self._add_transformation_block(obj.parent_block()) parent_component = obj.parent_component() orConstraint = self._add_xor_constraint(parent_component, transBlock) disaggregationConstraint = transBlock.disaggregationConstraints disaggregationConstraintMap = transBlock._disaggregationConstraintMap # 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, name_buffer=NAME_BUFFER)) # We first go through and collect all the variables that we # are going to disaggregate. varOrder_set = ComponentSet() varOrder = [] varsByDisjunct = ComponentMap() localVarsByDisjunct = ComponentMap() include_fixed_vars = not self._config.assume_fixed_vars_permanent for disjunct in obj.disjuncts: disjunctVars = varsByDisjunct[disjunct] = ComponentSet() for cons in disjunct.component_data_objects( Constraint, active=True, sort=SortComponents.deterministic, descend_into=Block): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future # and we are mathematically wrong if we don't transform these # correctly and someone later unfixes them and keeps playing # with their transformed model. However, the user may have set # assume_fixed_vars_permanent to True in which case we will skip # them for var in EXPR.identify_variables( cons.body, include_fixed=include_fixed_vars): # Note the use of a list so that we will # eventually disaggregate the vars in a # deterministic order (the order that we found # them) disjunctVars.add(var) if not var in varOrder_set: varOrder.append(var) varOrder_set.add(var) # check for LocalVars Suffix localVarsByDisjunct = self._get_local_var_suffixes( disjunct, localVarsByDisjunct) # We will disaggregate all variables which are not explicitly declared # as being local. Note however, that we do declare our own disaggregated # variables as local, so they will not be re-disaggregated. varSet = [] # Note that variables are local with respect to a Disjunct. We deal with # them here to do some error checking (if something is obviously not # local since it is used in multiple Disjuncts in this Disjunction) and # also to get a deterministic order in which to process them when we # transform the Disjuncts: Values of localVarsByDisjunct are # ComponentSets, so we need this for determinism (we iterate through the # localVars of a Disjunct later) localVars = ComponentMap() for var in varOrder: disjuncts = [d for d in varsByDisjunct if var in varsByDisjunct[d]] # clearly not local if used in more than one disjunct if len(disjuncts) > 1: if self._generate_debug_messages: logger.debug("Assuming '%s' is not a local var since it is" "used in multiple disjuncts." % var.getname(fully_qualified=True, name_buffer=NAME_BUFFER)) varSet.append(var) # disjuncts is a list of length 1 elif localVarsByDisjunct.get(disjuncts[0]) is not None: if var in localVarsByDisjunct[disjuncts[0]]: localVars_thisDisjunct = localVars.get(disjuncts[0]) if localVars_thisDisjunct is not None: localVars[disjuncts[0]].append(var) else: localVars[disjuncts[0]] = [var] else: # It's not local to this Disjunct varSet.append(var) else: # We don't even have have any local vars for this Disjunct. varSet.append(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var self._transform_disjunct(disjunct, transBlock, varSet, localVars.get(disjunct, [])) orConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(orConstraint[index]) # add the reaggregation constraints for i, var in enumerate(varSet): disaggregatedExpr = 0 for disjunct in obj.disjuncts: if disjunct._transformation_block is None: # Because we called _transform_disjunct in the loop above, # we know that if this isn't transformed it is because it # was cleanly deactivated, and we can just skip it. continue disaggregatedVar = disjunct._transformation_block().\ _disaggregatedVarMap['disaggregatedVar'][var] disaggregatedExpr += disaggregatedVar disaggregationConstraint.add((i, index), var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction if disaggregationConstraintMap.get(var) is not None: disaggregationConstraintMap[var][ obj] = disaggregationConstraint[(i, index)] else: thismap = disaggregationConstraintMap[var] = ComponentMap() thismap[obj] = disaggregationConstraint[(i, index)] # deactivate for the writers obj.deactivate()