Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Pyomo Model Plot #2251

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions pyomo/util/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import networkx as nx
import pyomo.environ as pe
import plotly.graph_objects as go
Comment on lines +1 to +3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the attempt_import stuff around networkx and plotly here?

from pyomo.core.base.block import _BlockData
from pyomo.core.base.var import _GeneralVarData, ScalarVar
from pyomo.core.base.constraint import _GeneralConstraintData, ScalarConstraint
from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective
from pyomo.core.expr.visitor import identify_variables
from pyomo.common.collections import OrderedSet, ComponentSet
from typing import Optional
import textwrap


class _CompNode(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:
Comment on lines +27 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a short docstring?

graph = nx.Graph()

for v in ComponentSet(m.component_data_objects(pe.Var, descend_into=True)):
graph.add_node(_CompNode(v))

for con in OrderedSet(m.component_data_objects(pe.Constraint, descend_into=True, active=active)):
graph.add_node(con)
for v in identify_variables(con.body, include_fixed=True):
graph.add_edge(con, _CompNode(v))

if include_objective:
for obj in ComponentSet(m.component_data_objects(pe.Objective, descend_into=True, active=active)):
graph.add_node(_CompNode(obj))
for v in identify_variables(obj.expr, include_fixed=True):
graph.add_edge(_CompNode(obj), _CompNode(v))

return graph


def plot_pyomo_model(m: _BlockData,
include_objective: bool = True,
active: bool = True,
plot_title: Optional[str] = None,
bipartite_plot: bool = False,
show_plot: bool = True):
Comment on lines +49 to +54
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a short docstring?

graph = graph_from_pyomo(m, include_objective=include_objective, active=active)
if bipartite_plot:
left_nodes = [c for c in OrderedSet(m.component_data_objects(pe.Constraint, descend_into=True, active=active))]
left_nodes.extend(_CompNode(obj) for obj in ComponentSet(m.component_data_objects(pe.Objective, descend_into=True, active=active)))
pos_dict = nx.drawing.bipartite_layout(graph, nodes=left_nodes)
else:
pos_dict = nx.drawing.spring_layout(graph, seed=0)

edge_x = list()
edge_y = list()
for start_node, end_node in graph.edges():
x0, y0 = pos_dict[start_node]
x1, y1 = pos_dict[end_node]
edge_x.append(x0)
edge_x.append(x1)
edge_x.append(None)
edge_y.append(y0)
edge_y.append(y1)
edge_y.append(None)
edge_trace = go.Scatter(x=edge_x, y=edge_y, line=dict(width=0.5, color='#888'), hoverinfo='none', mode='lines')

node_x = list()
node_y = list()
node_text = list()
node_color = list()
for node in graph.nodes():
x, y = pos_dict[node]
node_x.append(x)
node_y.append(y)
if type(node) == _CompNode and type(node.comp) in {_GeneralVarData, ScalarVar}:
v: _GeneralVarData = node.comp
node_color.append('blue')
node_text.append(f'{str(v)}<br>lb: {str(v.lb)}<br>ub: {str(v.ub)}<br>'
f'value: {str(v.value)}<br>domain: {str(v.domain)}<br>'
f'fixed: {str(v.is_fixed())}')
elif type(node) in {ScalarConstraint, _GeneralConstraintData}:
c: _GeneralConstraintData = node
node_color.append('red')
body_text = '<br>'.join(textwrap.wrap(str(c.body), width=120, subsequent_indent=" "))
node_text.append(f'{str(c)}<br>lb: {str(c.lower)}<br>body: {body_text}<br>'
f'ub: {str(c.upper)}<br>active: {str(c.active)}')
elif type(node) == _CompNode and type(node.comp) in {ScalarObjective, _GeneralObjectiveData}:
c: _GeneralObjectiveData = node.comp
node_color.append('green')
expr_text = '<br>'.join(textwrap.wrap(str(c.expr), width=120, subsequent_indent=" "))
node_text.append(f'{str(c)}<br>expr: {expr_text}<br>'
f'active: {str(c.active)}')
else:
raise ValueError(f'Unexpected node type: {type(node)}')
node_trace = go.Scatter(x=node_x, y=node_y, mode='markers', hoverinfo='text', text=node_text, marker=dict(color=node_color, size=10))

fig = go.Figure(data=[edge_trace, node_trace])
if plot_title is not None:
fig.update_layout(title=dict(text=plot_title))
if show_plot: # this option is mostly for unit tests
fig.show()
26 changes: 26 additions & 0 deletions pyomo/util/tests/test_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pyomo.environ as pe
from pyomo.common import unittest
from pyomo.common.dependencies import attempt_import
nx, nx_available = attempt_import('networkx')
plotly, plotly_available = attempt_import('plotly')


@unittest.skipUnless(nx_available, 'plot_pyomo_model requires networkx')
@unittest.skipUnless(plotly_available, 'plot_pyomo_model requires plotly')
class TestPlotPyomoModel(unittest.TestCase):
def test_plot_pyomo_model(self):
"""
Unfortunately, this test only ensures the code runs without errors.
It does not test for correctness.
"""
m = pe.ConcreteModel()
m.x = pe.Var(bounds=(-1, 1))
m.y = pe.Var()
m.z = pe.Var()
m.obj = pe.Objective(expr=m.y**2 + m.z**2)
m.c1 = pe.Constraint(expr=m.y == 2*m.x + 1)
m.c2 = pe.Constraint(expr=m.z >= m.x)
from pyomo.util.graph import plot_pyomo_model
plot_pyomo_model(m, plot_title='test plot', bipartite_plot=False, show_plot=False)
plot_pyomo_model(m, plot_title='test plot', bipartite_plot=True, show_plot=False)

1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ def _print_deps(self, deplist):
'ipython', # contrib.viewer
'matplotlib',
'networkx', # network, incidence_analysis, community_detection
'plotly', # pyomo.util.graph.plot_pyomo_model
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this list in alphabetical order

'numpy',
'openpyxl', # dataportals
#'pathos', # requested for #963, but PR currently closed
Expand Down