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 test_eval_numpy(self): m = ConcreteModel() m.p = Param([1,2], mutable=True) m.x = Var() data = np.array([[0,-1,2], [.1,.2,.3], [4,5,6]]) cMap = ComponentMap() cMap[m.p[1]] = data[0] cMap[m.p[2]] = data[1] cMap[m.x] = data[2] npe = NumpyEvaluator(cMap) result = npe.walk_expression(sin(m.x)) self.assertEqual(result[0], sin(4)) self.assertEqual(result[1], sin(5)) self.assertEqual(result[2], sin(6)) result = npe.walk_expression(abs(m.x * m.p[1] - m.p[2])) self.assertEqual(result[0], .1) self.assertEqual(result[1], -((-1*5)-.2)) self.assertEqual(result[2], (2*6-.3)) result = npe.walk_expression(atan(m.x)) self.assertEqual(result[0], atan(4)) self.assertEqual(result[1], atan(5)) self.assertEqual(result[2], atan(6)) result = npe.walk_expression(atanh(m.p[2])) self.assertEqual(result[0], atanh(.1)) self.assertEqual(result[1], atanh(.2)) self.assertEqual(result[2], atanh(.3))
def test_eval_numpy(self): m = ConcreteModel() m.p = Param([1,2], mutable=True) m.x = Var() data = np.array([[0,-1,2], [.1,.2,.3], [4,5,6]]) cMap = ComponentMap() cMap[m.p[1]] = data[0] cMap[m.p[2]] = data[1] cMap[m.x] = data[2] npe = NumpyEvaluator(cMap) result = npe.walk_expression(sin(m.x)) assert pytest.approx(result[0], rel=1e-12) == sin(4) assert pytest.approx(result[1], rel=1e-12) == sin(5) assert pytest.approx(result[2], rel=1e-12) == sin(6) result = npe.walk_expression(abs(m.x * m.p[1] - m.p[2])) assert pytest.approx(result[0], rel=1e-12) == .1 assert pytest.approx(result[1], rel=1e-12) == -((-1*5)-.2) assert pytest.approx(result[2], rel=1e-12) == (2*6-.3) result = npe.walk_expression(atan(m.x)) assert pytest.approx(result[0], rel=1e-12) == atan(4) assert pytest.approx(result[1], rel=1e-12) == atan(5) assert pytest.approx(result[2], rel=1e-12) == atan(6) result = npe.walk_expression(atanh(m.p[2])) assert pytest.approx(result[0], rel=1e-12) == atanh(.1) assert pytest.approx(result[1], rel=1e-12) == atanh(.2) assert pytest.approx(result[2], rel=1e-12) == atanh(.3)
def test_improved_bounds(self): m = ConcreteModel() m.x = Var(bounds=(0, 100), initialize=5) improved_bounds = ComponentMap() improved_bounds[m.x] = (10, 20) mc_expr = mc(m.x, improved_var_bounds=improved_bounds) self.assertEqual(mc_expr.lower(), 10) self.assertEqual(mc_expr.upper(), 20)
def disjunctive_bounds(scope): """Return all of the variable bounds defined at a disjunctive scope.""" possible_disjunct = scope while possible_disjunct is not None: try: return possible_disjunct._disj_var_bounds except AttributeError: # possible disjunct does not have attribute '_disj_var_bounds'. # Try again with the scope's parent block. possible_disjunct = possible_disjunct.parent_block() # Unable to find '_disj_var_bounds' attribute within search scope. return ComponentMap()
def test_eval_constant(self): m = ConcreteModel() m.p = Param([1,2], mutable=True) m.x = Var(initialize=0.25) cMap = ComponentMap() cMap[m.p[1]] = 2 cMap[m.p[2]] = 4 npe = NumpyEvaluator(cMap) expr = m.p[1] + m.p[2] + m.x + .5 self.assertEqual(npe.walk_expression(expr), 6.75) m.p[1] = 2 m.p[2] = 4 self.assertEqual(value(expr), 6.75)
def test_eval_constant(self): m = ConcreteModel() m.p = Param([1,2], mutable=True) m.x = Var(initialize=0.25) cMap = ComponentMap() cMap[m.p[1]] = 2 cMap[m.p[2]] = 4 npe = NumpyEvaluator(cMap) expr = m.p[1] + m.p[2] + m.x + .5 assert npe.walk_expression(expr) == pytest.approx(6.75, rel=1e-12) m.p[1] = 2 m.p[2] = 4 assert value(expr) == pytest.approx(6.75, rel=1e-12)
def __init__(self, sVar, **kwds): if not isinstance(sVar, Var): raise DAE_Error( "%s is not a variable. Can only take the derivative of a Var" "component." % sVar) if "wrt" in kwds and "withrespectto" in kwds: raise TypeError( "Cannot specify both 'wrt' and 'withrespectto keywords " "in a DerivativeVar") wrt = kwds.pop('wrt', None) wrt = kwds.pop('withrespectto', wrt) try: num_contset = len(sVar._contset) except AttributeError: # This dictionary keeps track of where the ContinuousSet appears # in the index. This implementation assumes that every element # in an indexing set has the same dimension. sVar._contset = ComponentMap() sVar._derivative = {} if sVar.dim() == 0: num_contset = 0 else: sidx_sets = list(sVar.index_set().subsets()) loc = 0 for i, s in enumerate(sidx_sets): if s.type() is ContinuousSet: sVar._contset[s] = loc _dim = s.dimen if _dim is None: raise DAE_Error( "The variable %s is indexed by a Set (%s) with a " "non-fixed dimension. A DerivativeVar may only be " "indexed by Sets with constant dimension" % (sVar, s.name)) elif _dim is UnknownSetDimen: raise DAE_Error( "The variable %s is indexed by a Set (%s) with an " "unknown dimension. A DerivativeVar may only be " "indexed by Sets with known constant dimension" % (sVar, s.name)) loc += s.dimen num_contset = len(sVar._contset) if num_contset == 0: raise DAE_Error( "The variable %s is not indexed by any ContinuousSets. A " "derivative may only be taken with respect to a continuous " "domain" % sVar) if wrt is None: # Check to be sure Var is indexed by single ContinuousSet and take # first deriv wrt that set if num_contset != 1: raise DAE_Error( "The variable %s is indexed by multiple ContinuousSets. " "The desired ContinuousSet must be specified using the " "keyword argument 'wrt'" % sVar) wrt = [ next(iterkeys(sVar._contset)), ] elif type(wrt) is ContinuousSet: if wrt not in sVar._contset: raise DAE_Error( "Invalid derivative: The variable %s is not indexed by " "the ContinuousSet %s" % (sVar, wrt)) wrt = [ wrt, ] elif type(wrt) is tuple or type(wrt) is list: for i in wrt: if type(i) is not ContinuousSet: raise DAE_Error( "Cannot take the derivative with respect to %s. " "Expected a ContinuousSet or a tuple of " "ContinuousSets" % i) if i not in sVar._contset: raise DAE_Error( "Invalid derivative: The variable %s is not indexed " "by the ContinuousSet %s" % (sVar, i)) wrt = list(wrt) else: raise DAE_Error( "Cannot take the derivative with respect to %s. " "Expected a ContinuousSet or a tuple of ContinuousSets" % i) wrtkey = [str(i) for i in wrt] wrtkey.sort() wrtkey = tuple(wrtkey) if wrtkey in sVar._derivative: raise DAE_Error( "Cannot create a new derivative variable for variable " "%s: derivative already defined as %s" % (sVar.name, sVar._derivative[wrtkey]().name)) sVar._derivative[wrtkey] = weakref.ref(self) self._sVar = sVar self._wrt = wrt kwds.setdefault('ctype', DerivativeVar) Var.__init__(self, sVar.index_set(), **kwds)
def generate_structured_model(self): """ Using the community map and the original model used to create this community map, we will create structured_model, which will be based on the original model but will place variables, constraints, and objectives into or outside of various blocks (communities) based on the community map. Returns ------- structured_model: Block a Pyomo model that reflects the nature of the community map """ # Initialize a new model (structured_model) which will contain variables and constraints in blocks based on # their respective communities within the CommunityMap structured_model = ConcreteModel() # Create N blocks (where N is the number of communities found within the model) structured_model.b = Block([0, len(self.community_map) - 1, 1]) # values given for (start, stop, step) # Initialize a ComponentMap that will map a variable from the model (for example, old_model.x1) used to # create the CommunityMap to a list of variables in various blocks that were created based on this # variable (for example, [structured_model.b[0].x1, structured_model.b[3].x1]) blocked_variable_map = ComponentMap() # Example key-value pair -> {original_model.x1 : [structured_model.b[0].x1, structured_model.b[3].x1]} # TODO - Consider changing structure of the next two for loops to be more efficient (maybe loop through # constraints and add variables as you go) (but note that disconnected variables would be # missed with this strategy) # First loop through community_map to add all the variables to structured_model before we add constraints # that use those variables for community_key, community in self.community_map.items(): _, variables_in_community = community # Loop through all of the variables (from the original model) in the given community for stored_variable in variables_in_community: # Construct a new_variable whose attributes are determined by querying the variable from the # original model new_variable = Var(domain=stored_variable.domain, bounds=stored_variable.bounds) # Add this new_variable to its block/community and name it using the string of the variable from the # original model structured_model.b[community_key].add_component( str(stored_variable), new_variable) # Since there could be multiple variables 'x1' (such as # structured_model.b[0].x1, structured_model.b[3].x1, etc), we need to create equality constraints # for all of the variables 'x1' within structured_model (this is the purpose of blocked_variable_map) # Here we update blocked_variable_map to keep track of what equality constraints need to be made variable_in_new_model = structured_model.find_component( new_variable) blocked_variable_map[ stored_variable] = blocked_variable_map.get( stored_variable, []) + [variable_in_new_model] # Now that we have all of our variables within the model, we will initialize a dictionary that used to # replace variables within constraints to other variables (in our case, this will convert variables from the # original model into variables from the new model (structured_model)) replace_variables_in_expression_map = dict() # Loop through community_map again, this time to add constraints (with replaced variables) for community_key, community in self.community_map.items(): constraints_in_community, _ = community # Loop through all of the constraints (from the original model) in the given community for stored_constraint in constraints_in_community: # Now, loop through all of the variables within the given constraint expression for variable_in_stored_constraint in identify_variables( stored_constraint.expr): # Loop through each of the "blocked" variables that a variable is mapped to and update # replace_variables_in_expression_map if a variable has a "blocked" form in the given community # What this means is that if we are looping through constraints in community 0, then it would be # best to change a variable x1 into b[0].x1 as opposed to b[2].x1 or b[5].x1 (assuming all of these # blocked versions of the variable x1 exist (which depends on the community map)) variable_in_current_block = False for blocked_variable in blocked_variable_map[ variable_in_stored_constraint]: if 'b[%d]' % community_key in str(blocked_variable): # Update replace_variables_in_expression_map accordingly replace_variables_in_expression_map[ id(variable_in_stored_constraint )] = blocked_variable variable_in_current_block = True if not variable_in_current_block: # Create a version of the given variable outside of blocks then add it to # replace_variables_in_expression_map new_variable = Var( domain=variable_in_stored_constraint.domain, bounds=variable_in_stored_constraint.bounds) # Add the new variable just as we did above (but now it is not in any blocks) structured_model.add_component( str(variable_in_stored_constraint), new_variable) # Update blocked_variable_map to keep track of what equality constraints need to be made variable_in_new_model = structured_model.find_component( new_variable) blocked_variable_map[ variable_in_stored_constraint] = blocked_variable_map.get( variable_in_stored_constraint, []) + [variable_in_new_model] # Update replace_variables_in_expression_map accordingly replace_variables_in_expression_map[ id(variable_in_stored_constraint )] = variable_in_new_model # TODO - Is there a better way to check whether something is actually an objective? (as done below) # Check to see whether 'stored_constraint' is actually an objective (since constraints and objectives # grouped together) if self.with_objective and isinstance( stored_constraint, (_GeneralObjectiveData, Objective)): # If the constraint is actually an objective, we add it to the block as an objective new_objective = Objective(expr=replace_expressions( stored_constraint.expr, replace_variables_in_expression_map)) structured_model.b[community_key].add_component( str(stored_constraint), new_objective) else: # Construct a constraint based on the expression within stored_constraint and the dict we have # created for the purpose of replacing the variables within the constraint expression new_constraint = Constraint(expr=replace_expressions( stored_constraint.expr, replace_variables_in_expression_map)) # Add this new constraint to the corresponding community/block with its name as the string of the # constraint from the original model structured_model.b[community_key].add_component( str(stored_constraint), new_constraint) # If with_objective was set to False, that means we might have missed an objective function within the # original model if not self.with_objective: # Construct a new dictionary for replacing the variables (replace_variables_in_objective_map) which will # be specific to the variables in the objective function, since there is the possibility that the # objective contains variables we have not yet seen (and thus not yet added to our new model) for objective_function in self.model.component_data_objects( ctype=Objective, active=self.use_only_active_components, descend_into=True): for variable_in_objective in identify_variables( objective_function): # Add all of the variables in the objective function (not within any blocks) # Check to make sure a form of the variable has not already been made outside of the blocks if structured_model.find_component( str(variable_in_objective)) is None: new_variable = Var(domain=variable_in_objective.domain, bounds=variable_in_objective.bounds) structured_model.add_component( str(variable_in_objective), new_variable) # Again we update blocked_variable_map to keep track of what # equality constraints need to be made variable_in_new_model = structured_model.find_component( new_variable) blocked_variable_map[ variable_in_objective] = blocked_variable_map.get( variable_in_objective, []) + [variable_in_new_model] # Update the dictionary that we will use to replace the variables replace_variables_in_expression_map[id( variable_in_objective)] = variable_in_new_model else: for version_of_variable in blocked_variable_map[ variable_in_objective]: if 'b[' not in str(version_of_variable): replace_variables_in_expression_map[ id(variable_in_objective )] = version_of_variable # Now we will construct a new objective function based on the one from the original model and then # add it to the new model just as we have done before new_objective = Objective(expr=replace_expressions( objective_function.expr, replace_variables_in_expression_map)) structured_model.add_component(str(objective_function), new_objective) # Now, we need to create equality constraints for all of the different "versions" of a variable (such # as x1, b[0].x1, b[2].x2, etc.) # Create a constraint list for the equality constraints structured_model.equality_constraint_list = ConstraintList( doc="Equality Constraints for the different " "forms of a given variable") # Loop through blocked_variable_map and create constraints accordingly for variable, duplicate_variables in blocked_variable_map.items(): # variable -> variable from the original model # duplicate_variables -> list of variables in the new model # Create a list of all the possible equality constraints that need to be made equalities_to_make = combinations(duplicate_variables, 2) # Loop through the list of two-variable tuples and create an equality constraint for those two variables for variable_1, variable_2 in equalities_to_make: structured_model.equality_constraint_list.add( expr=variable_1 == variable_2) # Return 'structured_model', which is essentially identical to the original model but now has all of the # variables, constraints, and objectives placed into blocks based on the nature of the CommunityMap return structured_model
def visualize_model_graph(self, type_of_graph='constraint', filename=None, pos=None): """ This function draws a graph of the communities for a Pyomo model. The type_of_graph parameter is used to create either a variable-node graph, constraint-node graph, or bipartite graph of the Pyomo model. Then, the nodes are colored based on the communities they are in - which is based on the community map (self.community_map). A filename can be provided to save the figure, otherwise the figure is illustrated with matplotlib. Parameters ---------- type_of_graph: str, optional a string that specifies the types of nodes drawn on the model graph, the default is 'constraint'. 'constraint' draws a graph with constraint nodes, 'variable' draws a graph with variable nodes, 'bipartite' draws a bipartite graph (with both constraint and variable nodes) filename: str, optional a string that specifies a path for the model graph illustration to be saved pos: dict, optional a dictionary that maps node keys to their positions on the illustration Returns ------- fig: matplotlib figure the figure for the model graph drawing pos: dict a dictionary that maps node keys to their positions on the illustration - can be used to create consistent layouts for graphs of a given model """ # Check that all arguments are of the correct type assert type_of_graph in ('bipartite', 'constraint', 'variable'), \ "Invalid graph type specified: 'type_of_graph=%s' - Valid values: " \ "'bipartite', 'constraint', 'variable'" % type_of_graph assert isinstance(filename, (type(None), str)), "Invalid value for filename: 'filename=%s' - filename " \ "must be a string" % filename # No assert statement for pos; the NetworkX function can handle issues with the pos argument # There is a possibility that the desired networkX graph of the model is already stored in the # CommunityMap object (because the networkX graph is required to create the CommunityMap object) if type_of_graph != self.type_of_community_map: # Use the generate_model_graph function to create a NetworkX graph of the given model (along with # number_component_map and constraint_variable_map, which will be used to help with drawing the graph) model_graph, number_component_map, constraint_variable_map = generate_model_graph( self.model, type_of_graph=type_of_graph, with_objective=self.with_objective, weighted_graph=self.weighted_graph, use_only_active_components=self.use_only_active_components) else: # This is the case where, as mentioned above, we can use the networkX graph that was made to create # the CommunityMap object model_graph, number_component_map, constraint_variable_map = self.graph, self.graph_node_mapping, \ self.constraint_variable_map # This line creates the "reverse" of the number_component_map above, since mapping the Pyomo # components to their nodes in the networkX graph is more convenient in this function component_number_map = ComponentMap( (comp, number) for number, comp in number_component_map.items()) # Create a deep copy of the community_map attribute to avoid destructively modifying it numbered_community_map = copy.deepcopy(self.community_map) # Now we will use the component_number_map to change the Pyomo modeling components in community_map into the # numbers that correspond to their nodes/edges in the NetworkX graph, model_graph for key in self.community_map: numbered_community_map[key] = ([ component_number_map[component] for component in self.community_map[key][0] ], [ component_number_map[component] for component in self.community_map[key][1] ]) # Based on type_of_graph, which specifies what Pyomo modeling components are to be drawn as nodes in the graph # illustration, we will now get the node list and the color list, which describes how to color nodes # according to their communities (which is based on community_map) if type_of_graph == 'bipartite': list_of_node_lists = [ list_of_nodes for list_tuple in numbered_community_map.values() for list_of_nodes in list_tuple ] # list_of_node_lists is (as it implies) a list of lists, so we will use the list comprehension # below to flatten the list and get our one-dimensional node list node_list = [ node for sublist in list_of_node_lists for node in sublist ] color_list = [] # Now, we will find the first community that a node appears in and color the node based on that community # In community_map, certain nodes may appear in multiple communities, and we have chosen to give preference # to the first community a node appears in for node in node_list: not_found = True for community_key in numbered_community_map: if not_found and node in ( numbered_community_map[community_key][0] + numbered_community_map[community_key][1]): color_list.append(community_key) not_found = False # Find top_nodes (one of the two "groups" of nodes in a bipartite graph), which will be used to # determine the graph layout if model_graph.number_of_nodes() > 0 and nx.is_connected( model_graph): # An index of 1 used because this tends to place constraint nodes on the left, which is # consistent with the else case top_nodes = nx.bipartite.sets(model_graph)[1] else: top_nodes = { node for node in model_graph.nodes() if node in constraint_variable_map } if pos is None: # The case where the user has not provided their own layout pos = nx.bipartite_layout(model_graph, top_nodes) else: # This covers the case that type_of_community_map is 'constraint' or 'variable' # Constraints are in the first list of the tuples in community map and variables are in the second list position = 0 if type_of_graph == 'constraint' else 1 list_of_node_lists = list(i[position] for i in numbered_community_map.values()) # list_of_node_lists is (as it implies) a list of lists, so we will use the list comprehension # below to flatten the list and get our one-dimensional node list node_list = [ node for sublist in list_of_node_lists for node in sublist ] # Now, we will find the first community that a node appears in and color the node based on # that community (in numbered_community_map, certain nodes may appear in multiple communities, # and we have chosen to give preference to the first community a node appears in) color_list = [] for node in node_list: not_found = True for community_key in numbered_community_map: if not_found and node in numbered_community_map[ community_key][position]: color_list.append(community_key) not_found = False # Note - there is no strong reason to choose spring layout; it just creates relatively clean graphs if pos is None: # The case where the user has not provided their own layout pos = nx.spring_layout(model_graph) # Define color_map color_map = plt.cm.get_cmap('viridis', len(numbered_community_map)) # Create the figure and draw the graph fig = plt.figure() nx.draw_networkx_nodes(model_graph, pos, nodelist=node_list, node_size=40, cmap=color_map, node_color=color_list) nx.draw_networkx_edges(model_graph, pos, alpha=0.5) # Make the main title graph_type = type_of_graph.capitalize() community_map_type = self.type_of_community_map.capitalize() main_graph_title = "%s graph - colored using %s community map" % ( graph_type, community_map_type) main_font_size = 14 plt.suptitle(main_graph_title, fontsize=main_font_size) # Define a dict that will be used for the graph subtitle subtitle_naming_dict = { 'bipartite': 'Nodes are variables and constraints & Edges are variables in a constraint', 'constraint': 'Nodes are constraints & Edges are common variables', 'variable': 'Nodes are variables & Edges are shared constraints' } # Make the subtitle subtitle_font_size = 11 plt.title(subtitle_naming_dict[type_of_graph], fontsize=subtitle_font_size) if filename is None: plt.show() else: plt.savefig(filename) plt.close() # Return the figure and pos, the position dictionary used for the graph layout return fig, pos
def create_ef_instance(scenario_tree, ef_instance_name="MASTER", verbose_output=False, generate_weighted_cvar=False, cvar_weight=None, risk_alpha=None, cc_indicator_var_name=None, cc_alpha=0.0): # # create the new and empty binding instance. # # scenario tree must be "linked" with a set of instances # to used this function scenario_instances = {} for scenario in scenario_tree.scenarios: if scenario._instance is None: raise ValueError("Cannot construct extensive form instance. " "The scenario tree does not appear to be linked " "to any Pyomo models. Missing model for scenario " "with name: %s" % (scenario.name)) scenario_instances[scenario.name] = scenario._instance binding_instance = ConcreteModel(name=ef_instance_name) root_node = scenario_tree.findRootNode() opt_sense = minimize \ if (scenario_tree._scenarios[0]._instance_objective.is_minimizing()) \ else maximize # # validate cvar options, if specified. # cvar_excess_vardatas = [] if generate_weighted_cvar: if (cvar_weight is None) or (cvar_weight < 0.0): raise RuntimeError( "Weight of CVaR term must be >= 0.0 - value supplied=" + str(cvar_weight)) if (risk_alpha is None) or (risk_alpha <= 0.0) or (risk_alpha >= 1.0): raise RuntimeError( "CVaR risk alpha must be between 0 and 1, exclusive - value supplied=" + str(risk_alpha)) if verbose_output: print("Writing CVaR weighted objective") print("CVaR term weight=" + str(cvar_weight)) print("CVaR alpha=" + str(risk_alpha)) print("") # create the eta and excess variable on a per-scenario basis, # in addition to the constraint relating to the two. cvar_eta_variable_name = "CVAR_ETA_" + str(root_node._name) cvar_eta_variable = Var() binding_instance.add_component(cvar_eta_variable_name, cvar_eta_variable) excess_var_domain = NonNegativeReals if (opt_sense == minimize) else \ NonPositiveReals compute_excess_constraint = \ binding_instance.COMPUTE_SCENARIO_EXCESS = \ ConstraintList() for scenario in scenario_tree._scenarios: cvar_excess_variable_name = "CVAR_EXCESS_" + scenario._name cvar_excess_variable = Var(domain=excess_var_domain) binding_instance.add_component(cvar_excess_variable_name, cvar_excess_variable) compute_excess_expression = cvar_excess_variable compute_excess_expression -= scenario._instance_cost_expression compute_excess_expression += cvar_eta_variable if opt_sense == maximize: compute_excess_expression *= -1 compute_excess_constraint.add( (0.0, compute_excess_expression, None)) cvar_excess_vardatas.append( (cvar_excess_variable, scenario._probability)) # the individual scenario instances are sub-blocks of the binding instance. for scenario in scenario_tree._scenarios: scenario_instance = scenario_instances[scenario._name] binding_instance.add_component(str(scenario._name), scenario_instance) # Now deactivate the scenario instance Objective since we are creating # a new master objective scenario._instance_objective.deactivate() # walk the scenario tree - create variables representing the # common values for all scenarios associated with that node, along # with equality constraints to enforce non-anticipativity. also # create expected cost variables for each node, to be computed via # constraints/objectives defined in a subsequent pass. master # variables are created for all nodes but those in the last # stage. expected cost variables are, for no particularly good # reason other than easy coding, created for nodes in all stages. if verbose_output: print("Creating variables for master binding instance") _cmap = binding_instance.MASTER_CONSTRAINT_MAP = ComponentMap() for stage in scenario_tree._stages[:-1]: # skip the leaf stage for tree_node in stage._tree_nodes: # create the master blending variable and constraints for this node master_blend_variable_name = \ "MASTER_BLEND_VAR_"+str(tree_node._name) master_blend_constraint_name = \ "MASTER_BLEND_CONSTRAINT_"+str(tree_node._name) # don't create master variables for derived # stage variables as they will not be used in # the problem, and their values would likely # never be consistent with what is stored on the # scenario variables master_variable_index = Set( initialize=sorted(tree_node._standard_variable_ids), ordered=True, name=master_blend_variable_name + "_index") binding_instance.add_component( master_blend_variable_name + "_index", master_variable_index) master_variable = Var(master_variable_index, name=master_blend_variable_name) binding_instance.add_component(master_blend_variable_name, master_variable) master_constraint = ConstraintList( name=master_blend_constraint_name) binding_instance.add_component(master_blend_constraint_name, master_constraint) tree_node_variable_datas = tree_node._variable_datas for variable_id in sorted(tree_node._standard_variable_ids): master_vardata = master_variable[variable_id] vardatas = tree_node_variable_datas[variable_id] # Don't blend fixed variables if not tree_node.is_variable_fixed(variable_id): for scenario_vardata, scenario_probability in vardatas: _cmap[scenario_vardata] = master_constraint.add( (master_vardata - scenario_vardata, 0.0)) if generate_weighted_cvar: cvar_cost_expression_name = "CVAR_COST_" + str(root_node._name) cvar_cost_expression = Expression(name=cvar_cost_expression_name) binding_instance.add_component(cvar_cost_expression_name, cvar_cost_expression) # create an expression to represent the expected cost at the root node binding_instance.EF_EXPECTED_COST = \ Expression(initialize=sum(scenario._probability * \ scenario._instance_cost_expression for scenario in scenario_tree._scenarios)) opt_expression = \ binding_instance.MASTER_OBJECTIVE_EXPRESSION = \ Expression(initialize=binding_instance.EF_EXPECTED_COST) if generate_weighted_cvar: cvar_cost_expression_name = "CVAR_COST_" + str(root_node._name) cvar_cost_expression = \ binding_instance.find_component(cvar_cost_expression_name) if cvar_weight == 0.0: # if the cvar weight is 0, then we're only # doing cvar - no mean. opt_expression.set_value(cvar_cost_expression) else: opt_expression.expr += cvar_weight * cvar_cost_expression binding_instance.MASTER = Objective(sense=opt_sense, expr=opt_expression) # CVaR requires the addition of a variable per scenario to # represent the cost excess, and a constraint to compute the cost # excess relative to eta. if generate_weighted_cvar: # add the constraint to compute the master CVaR variable value. iterate # over scenario instances to create the expected excess component first. cvar_cost_expression_name = "CVAR_COST_" + str(root_node._name) cvar_cost_expression = binding_instance.find_component( cvar_cost_expression_name) cvar_eta_variable_name = "CVAR_ETA_" + str(root_node._name) cvar_eta_variable = binding_instance.find_component( cvar_eta_variable_name) cost_expr = 1.0 for scenario_excess_vardata, scenario_probability in cvar_excess_vardatas: cost_expr += (scenario_probability * scenario_excess_vardata) cost_expr /= (1.0 - risk_alpha) cost_expr += cvar_eta_variable cvar_cost_expression.set_value(cost_expr) if cc_indicator_var_name is not None: if verbose_output is True: print("Creating chance constraint for indicator variable= " + cc_indicator_var_name) print("with alpha= " + str(cc_alpha)) if not isVariableNameIndexed(cc_indicator_var_name): cc_expression = 0 #?????? for scenario in scenario_tree._scenarios: scenario_instance = scenario_instances[scenario._name] scenario_probability = scenario._probability cc_var = scenario_instance.find_component( cc_indicator_var_name) cc_expression += scenario_probability * cc_var def makeCCRule(expression): def CCrule(model): return (1.0 - cc_alpha, cc_expression, None) return CCrule cc_constraint_name = "cc_" + cc_indicator_var_name cc_constraint = Constraint(name=cc_constraint_name, rule=makeCCRule(cc_expression)) binding_instance.add_component(cc_constraint_name, cc_constraint) else: print("multiple cc not yet supported.") variable_name, index_template = extractVariableNameAndIndex( cc_indicator_var_name) # verify that the root variable exists and grab it. # NOTE: we are using whatever scenario happens to laying around... it might be better to use the reference variable = scenario_instance.find_component(variable_name) if variable is None: raise RuntimeError("Unknown variable=" + variable_name + " referenced as the CC indicator variable.") # extract all "real", i.e., fully specified, indices matching the index template. match_indices = extractComponentIndices(variable, index_template) # there is a possibility that no indices match the input template. # if so, let the user know about it. if len(match_indices) == 0: raise RuntimeError("No indices match template=" + str(index_template) + " for variable=" + variable_name) # add the suffix to all variable values identified. for index in match_indices: variable_value = variable[index] cc_expression = 0 #?????? for scenario in scenario_tree._scenarios: scenario_instance = scenario_instances[scenario._name] scenario_probability = scenario._probability cc_var = scenario_instance.find_component( variable_name)[index] cc_expression += scenario_probability * cc_var def makeCCRule(expression): def CCrule(model): return (1.0 - cc_alpha, cc_expression, None) return CCrule indexasname = '' for c in str(index): if c not in ' ,': indexasname += c cc_constraint_name = "cc_" + variable_name + "_" + indexasname cc_constraint = Constraint(name=cc_constraint_name, rule=makeCCRule(cc_expression)) binding_instance.add_component(cc_constraint_name, cc_constraint) return binding_instance
def _perform_branch_and_bound(solve_data): solve_data.explored_nodes = 0 root_node = solve_data.working_model root_util_blk = root_node.GDPopt_utils config = solve_data.config # Map unfixed disjunct -> list of deactivated constraints root_util_blk.disjunct_to_nonlinear_constraints = ComponentMap() # Map relaxed disjunctions -> list of unfixed disjuncts root_util_blk.disjunction_to_unfixed_disjuncts = ComponentMap() # Preprocess the active disjunctions for disjunction in root_util_blk.disjunction_list: assert disjunction.active disjuncts_fixed_True = [] disjuncts_fixed_False = [] unfixed_disjuncts = [] # categorize the disjuncts in the disjunction for disjunct in disjunction.disjuncts: if disjunct.indicator_var.fixed: if disjunct.indicator_var.value == 1: disjuncts_fixed_True.append(disjunct) elif disjunct.indicator_var.value == 0: disjuncts_fixed_False.append(disjunct) else: pass # raise error for fractional value? else: unfixed_disjuncts.append(disjunct) # update disjunct lists for predetermined disjunctions if len(disjuncts_fixed_False) == len(disjunction.disjuncts) - 1: # all but one disjunct in the disjunction is fixed to False. # Remaining one must be true. If not already fixed to True, do so. if not disjuncts_fixed_True: disjuncts_fixed_True = unfixed_disjuncts unfixed_disjuncts = [] disjuncts_fixed_True[0].indicator_var.fix(1) elif disjuncts_fixed_True and disjunction.xor: assert len( disjuncts_fixed_True ) == 1, "XOR (only one True) violated: %s" % disjunction.name disjuncts_fixed_False.extend(unfixed_disjuncts) unfixed_disjuncts = [] # Make sure disjuncts fixed to False are properly deactivated. for disjunct in disjuncts_fixed_False: disjunct.deactivate() # Deactivate nonlinear constraints in unfixed disjuncts for disjunct in unfixed_disjuncts: nonlinear_constraints_in_disjunct = [ constr for constr in disjunct.component_data_objects(Constraint, active=True) if constr.body.polynomial_degree() not in _linear_degrees ] for constraint in nonlinear_constraints_in_disjunct: constraint.deactivate() if nonlinear_constraints_in_disjunct: # TODO might be worthwhile to log number of nonlinear constraints in each disjunction # for later branching purposes root_util_blk.disjunct_to_nonlinear_constraints[ disjunct] = nonlinear_constraints_in_disjunct root_util_blk.disjunction_to_unfixed_disjuncts[ disjunction] = unfixed_disjuncts pass # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation. # TODO is this still necessary? if not hasattr(root_node, 'BigM'): root_node.BigM = Suffix() # Set up the priority queue queue = solve_data.bb_queue = [] solve_data.created_nodes = 0 unbranched_disjunction_indices = [ i for i, disjunction in enumerate(root_util_blk.disjunction_list) if disjunction in root_util_blk.disjunction_to_unfixed_disjuncts ] sort_tuple = BBNodeData( obj_lb=float('-inf'), obj_ub=float('inf'), is_screened=False, is_evaluated=False, num_unbranched_disjunctions=len(unbranched_disjunction_indices), node_count=0, unbranched_disjunction_indices=unbranched_disjunction_indices, ) heappush(queue, (sort_tuple, root_node)) # Do the branch and bound while len(queue) > 0: # visit the top node on the heap # from pprint import pprint # pprint([( # x[0].node_count, x[0].obj_lb, x[0].obj_ub, x[0].num_unbranched_disjunctions # ) for x in sorted(queue)]) node_data, node_model = heappop(queue) config.logger.info("Nodes: %s LB %.10g Unbranched %s" % (solve_data.explored_nodes, node_data.obj_lb, node_data.num_unbranched_disjunctions)) # Check time limit elapsed = get_main_elapsed_time(solve_data.timing) if elapsed >= config.time_limit: config.logger.info('GDPopt-LBB unable to converge bounds ' 'before time limit of {} seconds. ' 'Elapsed: {} seconds'.format( config.time_limit, elapsed)) no_feasible_soln = float('inf') solve_data.LB = node_data.obj_lb if solve_data.objective_sense == minimize else -no_feasible_soln solve_data.UB = no_feasible_soln if solve_data.objective_sense == minimize else -node_data.obj_lb config.logger.info('Final bound values: LB: {} UB: {}'.format( solve_data.LB, solve_data.UB)) solve_data.results.solver.termination_condition = tc.maxTimeLimit return True # Handle current node if not node_data.is_screened: # Node has not been evaluated. solve_data.explored_nodes += 1 new_node_data = _prescreen_node(node_data, node_model, solve_data) heappush( queue, (new_node_data, node_model)) # replace with updated node data elif node_data.obj_lb < node_data.obj_ub - config.bound_tolerance and not node_data.is_evaluated: # Node has not been fully evaluated. # Note: infeasible and unbounded nodes will skip this condition, because of strict inequality new_node_data = _evaluate_node(node_data, node_model, solve_data) heappush( queue, (new_node_data, node_model)) # replace with updated node data elif node_data.num_unbranched_disjunctions == 0 or node_data.obj_lb == float( 'inf'): # We have reached a leaf node, or the best available node is infeasible. original_model = solve_data.original_model copy_var_list_values( from_list=node_model.GDPopt_utils.variable_list, to_list=original_model.GDPopt_utils.variable_list, config=config, ) solve_data.LB = node_data.obj_lb if solve_data.objective_sense == minimize else -node_data.obj_ub solve_data.UB = node_data.obj_ub if solve_data.objective_sense == minimize else -node_data.obj_lb solve_data.master_iteration = solve_data.explored_nodes if node_data.obj_lb == float('inf'): solve_data.results.solver.termination_condition = tc.infeasible elif node_data.obj_ub == float('-inf'): solve_data.results.solver.termination_condition = tc.unbounded else: solve_data.results.solver.termination_condition = tc.optimal return else: _branch_on_node(node_data, node_model, solve_data)
def generate_model_graph(model, type_of_graph, with_objective=True, weighted_graph=True, use_only_active_components=True): """ Creates a networkX graph of nodes and edges based on a Pyomo optimization model This function takes in a Pyomo optimization model, then creates a graphical representation of the model with specific features of the graph determined by the user (see Parameters below). (This function is designed to be called by detect_communities, but can be used solely for the purpose of creating model graphs as well.) Parameters ---------- model: Block a Pyomo model or block to be used for community detection type_of_graph: str a string that specifies the type of graph that is created from the model 'constraint' creates a graph based on constraint nodes, 'variable' creates a graph based on variable nodes, 'bipartite' creates a graph based on constraint and variable nodes (bipartite graph). with_objective: bool, optional a Boolean argument that specifies whether or not the objective function is included in the graph; the default is True weighted_graph: bool, optional a Boolean argument that specifies whether a weighted or unweighted graph is to be created from the Pyomo model; the default is True (type_of_graph='bipartite' creates an unweighted graph regardless of this parameter) use_only_active_components: bool, optional a Boolean argument that specifies whether inactive constraints/objectives are included in the networkX graph Returns ------- bipartite_model_graph/projected_model_graph: nx.Graph a NetworkX graph with nodes and edges based on the given Pyomo optimization model number_component_map: dict a dictionary that (deterministically) maps a number to a component in the model constraint_variable_map: dict a dictionary that maps a numbered constraint to a list of (numbered) variables that appear in the constraint """ # Start off by making a bipartite graph (regardless of the value of type_of_graph), then if # type_of_graph = 'variable' or 'constraint', we will "collapse" this bipartite graph into a variable node # or constraint node graph # Initialize the data structure needed to keep track of edges in the graph (this graph will be made # without edge weights, because edge weights are not useful for this bipartite graph) edge_set = set() bipartite_model_graph = nx.Graph() # Initialize NetworkX graph for the bipartite graph constraint_variable_map = {} # Initialize map of the variables in constraint equations # Make a dict of all the components we need for the NetworkX graph (since we cannot use the components directly # in the NetworkX graph) if with_objective: component_number_map = ComponentMap((component, number) for number, component in enumerate( model.component_data_objects(ctype=(Constraint, Var, Objective), active=use_only_active_components, descend_into=True, sort=SortComponents.deterministic))) else: component_number_map = ComponentMap((component, number) for number, component in enumerate( model.component_data_objects(ctype=(Constraint, Var), active=use_only_active_components, descend_into=True, sort=SortComponents.deterministic))) # Create the reverse of component_number_map, which will be used in detect_communities to convert the node numbers # to their corresponding Pyomo modeling components number_component_map = dict((number, comp) for comp, number in component_number_map.items()) # Add the components as nodes to the bipartite graph bipartite_model_graph.add_nodes_from([node_number for node_number in range(len(component_number_map))]) # Loop through all constraints in the Pyomo model to determine what edges need to be created for model_constraint in model.component_data_objects(ctype=Constraint, active=use_only_active_components, descend_into=True): numbered_constraint = component_number_map[model_constraint] # Create a list of the variable numbers that occur in the given constraint equation numbered_variables_in_constraint_equation = [component_number_map[constraint_variable] for constraint_variable in identify_variables(model_constraint.body)] # Update constraint_variable_map constraint_variable_map[numbered_constraint] = numbered_variables_in_constraint_equation # Create a list of all the edges that need to be created based on the variables in this constraint equation edges_between_nodes = [(numbered_constraint, numbered_variable_in_constraint) for numbered_variable_in_constraint in numbered_variables_in_constraint_equation] # Update edge_set based on the determined edges between nodes edge_set.update(edges_between_nodes) # This if statement will be executed if the user chooses to include the objective function as a node in # the model graph if with_objective: # Use a loop to account for the possibility of multiple objective functions for objective_function in model.component_data_objects(ctype=Objective, active=use_only_active_components, descend_into=True): numbered_objective = component_number_map[objective_function] # Create a list of the variable numbers that occur in the given objective function numbered_variables_in_objective = [component_number_map[objective_variable] for objective_variable in identify_variables(objective_function)] # Update constraint_variable_map constraint_variable_map[numbered_objective] = numbered_variables_in_objective # Create a list of all the edges that need to be created based on the variables in the objective function edges_between_nodes = [(numbered_objective, numbered_variable_in_objective) for numbered_variable_in_objective in numbered_variables_in_objective] # Update edge_set based on the determined edges between nodes edge_set.update(edges_between_nodes) # Add edges to bipartite_model_graph (the order in which edges are added can affect community detection, so # sorting prevents any unpredictable changes) bipartite_model_graph.add_edges_from(sorted(edge_set)) if type_of_graph == 'bipartite': # This is the case where the user wants a bipartite graph, which we made above # Log important information with the following logger function _event_log(model, bipartite_model_graph, set(constraint_variable_map), type_of_graph, with_objective) # Return the bipartite NetworkX graph, the dictionary of node numbers mapped to their respective Pyomo # components, and the map of constraints to the variables they contain return bipartite_model_graph, number_component_map, constraint_variable_map # At this point of the code, we will create the projected version of the bipartite # model graph (based on the specific value of type_of_graph) constraint_nodes = set(constraint_variable_map) if type_of_graph == 'constraint': graph_nodes = constraint_nodes else: variable_nodes = set(number_component_map) - constraint_nodes graph_nodes = variable_nodes if weighted_graph: projected_model_graph = nx.bipartite.weighted_projected_graph(bipartite_model_graph, graph_nodes) else: projected_model_graph = nx.bipartite.projected_graph(bipartite_model_graph, graph_nodes) # Log important information with the following logger function _event_log(model, projected_model_graph, set(constraint_variable_map), type_of_graph, with_objective) # Return the projected NetworkX graph, the dictionary of node numbers mapped to their respective Pyomo # components, and the map of constraints to the variables they contain return projected_model_graph, number_component_map, constraint_variable_map