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 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 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