Open
Description
Summary
There are many applications in which it is useful to generate a graph representation of a pyomo model using NetworkX. It would be nice to have a single common function/module in Pyomo with this functionality that can be utilized for multiple applications. However, the desired API is not clear. The purpose of this PEP is to discuss the API.
The problem
The problem is that nodes in a NetworkX graph must be hashable. This motivates two potential solutions:
- Create hashable wrappers for Pyomo components and use them as nodes in the NetworkX graph.
- Use a hashable object (probably an
int
) as nodes and provide some sort of mapping from nodes to Pyomo components.
Solution 1: Hashable Wrappers
What it looks like
class HashableWrapper(object):
def __init__(self, comp):
self.comp = comp
def __eq__(self, other):
if type(other) is _CompNode:
return self.comp is other.comp
return False
def __hash__(self):
return hash(id(self.comp))
def graph_from_pyomo(m: _BlockData,
include_objective: bool = True,
active: bool = True) -> nx.Graph:
graph = nx.Graph()
for v in ComponentSet(m.component_data_objects(pe.Var, descend_into=True)):
graph.add_node(HashableWrapper(v))
for con in OrderedSet(m.component_data_objects(pe.Constraint, descend_into=True, active=active)):
graph.add_node(HashableWrapper(con)) # This does not need the wrapper, but consistency is nice
for v in identify_variables(con.body, include_fixed=True):
graph.add_edge(HashableWrapper(con), HashableWrapper(v))
if include_objective:
for obj in ComponentSet(m.component_data_objects(pe.Objective, descend_into=True, active=active)):
graph.add_node(HashableWrapper(obj))
for v in identify_variables(obj.expr, include_fixed=True):
graph.add_edge(HashableWrapper(obj), HashableWrapper(v))
return graph
Example Usage
g = graph_from_pyomo(m)
for n1, n2 in graph.edges():
n1.comp.pprint()
for v in m.component_data_objects(pe.Var):
g.neighbors(HashableWrapper(v))
Pros
- Easy to use
- Only one object to worry about
Cons
- Needs a new object for every Pyomo component
Solution 2: Provide a mapping from nodes to Pyomo components
What it looks like
def graph_from_pyomo(m: _BlockData, include_objective: bool = True, active: bool = True) -> nx.Graph:
graph = nx.Graph()
ndx = 0
comp_map = dict()
rev_comp_map = ComponentMap()
for v in ComponentSet(m.component_data_objects(pe.Var, descend_into=True)):
graph.add_node(ndx)
comp_map[ndx] = v
rev_comp_map[v] = ndx
ndx += 1
for con in OrderedSet(m.component_data_objects(pe.Constraint, descend_into=True, active=active)):
graph.add_node(ndx)
comp_map[ndx] = con
rev_comp_map[con] = ndx
ndx += 1
for v in identify_variables(con.body, include_fixed=True):
graph.add_edge(rev_comp_map[con], rev_comp_map[v])
if include_objective:
for obj in ComponentSet(m.component_data_objects(pe.Objective, descend_into=True, active=active)):
graph.add_node(ndx)
comp_map[ndx] = obj
rev_comp_map[obj] = ndx
ndx += 1
for v in identify_variables(obj.expr, include_fixed=True):
graph.add_edge(rev_comp_map[obj], rev_comp_map[v])
return graph, comp_map, rev_comp_map
Example Usage
g, cm, rcm = graph_from_pyomo(m)
for n1, n2 in graph.edges():
cm[n1].pprint()
for v in m.component_data_objects(pe.Var):
g.neighbors(rcm[v])
Pros
- Very simple (I like simple)
- Lightweight solution
Cons
- Working with the resulting graph is slightly more cumbersome