Skip to content

Commit

Permalink
Merge pull request #2716 from Robbybp/igraph-plot
Browse files Browse the repository at this point in the history
[contrib] Add `plot` method to `IncidenceGraphInterface`
  • Loading branch information
mrmundt authored Feb 14, 2023
2 parents 48cafd2 + 4763aa4 commit e49b973
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 3 deletions.
80 changes: 79 additions & 1 deletion pyomo/contrib/incidence_analysis/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# ___________________________________________________________________________

import enum
import textwrap
from pyomo.core.base.block import Block
from pyomo.core.base.var import Var
from pyomo.core.base.constraint import Constraint
Expand All @@ -19,7 +20,7 @@
from pyomo.core.expr.current import EqualityExpression
from pyomo.util.subsystems import create_subsystem_block
from pyomo.common.collections import ComponentSet, ComponentMap
from pyomo.common.dependencies import scipy_available
from pyomo.common.dependencies import scipy_available, attempt_import
from pyomo.common.dependencies import networkx as nx
from pyomo.contrib.incidence_analysis.matching import maximum_matching
from pyomo.contrib.incidence_analysis.connected import (
Expand All @@ -41,6 +42,10 @@
from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP
import scipy as sp

plotly, plotly_available = attempt_import("plotly")
if plotly_available:
go = plotly.graph_objects


class IncidenceMatrixType(enum.Enum):
NONE = 0
Expand Down Expand Up @@ -577,3 +582,76 @@ def remove_nodes(self, nodes, constraints=None):
)
self.row_block_map = None
self.col_block_map = None

def plot(self, variables=None, constraints=None, title=None, show=True):
"""Plot the bipartite incidence graph of variables and constraints
"""
variables, constraints = self._validate_input(variables, constraints)
graph = self._extract_subgraph(variables, constraints)
M = len(constraints)

left_nodes = list(range(M))
pos_dict = nx.drawing.bipartite_layout(graph, nodes=left_nodes)

edge_x = []
edge_y = []
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 = []
node_y = []
node_text = []
node_color = []
for node in graph.nodes():
x, y = pos_dict[node]
node_x.append(x)
node_y.append(y)
if node < M:
# According to convention, we are a constraint node
c = constraints[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)}'
)
else:
# According to convention, we are a variable node
v = variables[node-M]
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())}'
)
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 title is not None:
fig.update_layout(title=dict(text=title))
if show:
fig.show()
28 changes: 26 additions & 2 deletions pyomo/contrib/incidence_analysis/tests/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
# ___________________________________________________________________________

import pyomo.environ as pyo
from pyomo.common.dependencies import networkx_available
from pyomo.common.dependencies import scipy_available
from pyomo.common.dependencies import (
networkx_available,
scipy_available,
attempt_import,
)
plotly, plotly_available = attempt_import("plotly")
from pyomo.common.collections import ComponentSet, ComponentMap
from pyomo.contrib.incidence_analysis.interface import (
IncidenceGraphInterface,
Expand Down Expand Up @@ -1474,6 +1478,26 @@ def test_subgraph_with_fewer_var_or_con(self):
matching = igraph.maximum_matching(variables, constraints)
self.assertEqual(len(matching), 1)

@unittest.skipUnless(plotly_available, "Plotly is not available")
def test_plot(self):
"""
Unfortunately, this test only ensures the code runs without errors.
It does not test for correctness.
"""
m = pyo.ConcreteModel()
m.x = pyo.Var(bounds=(-1, 1))
m.y = pyo.Var()
m.z = pyo.Var()
# NOTE: Objective will not be displayed
m.obj = pyo.Objective(expr=m.y**2 + m.z**2)
m.c1 = pyo.Constraint(expr=m.y == 2*m.x + 1)
m.c2 = pyo.Constraint(expr=m.z >= m.x)
m.y.fix()
igraph = IncidenceGraphInterface(
m, include_inequality=True, include_fixed=True
)
igraph.plot(title='test plot', show=False)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ def _print_deps(self, deplist):
'openpyxl', # dataportals
#'pathos', # requested for #963, but PR currently closed
'pint', # units
'plotly', # incidence_analysis
'python-louvain', # community_detection
'pyyaml', # core
'scipy',
Expand Down

0 comments on commit e49b973

Please sign in to comment.