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

[contrib] Add plot method to IncidenceGraphInterface #2716

Merged
merged 6 commits into from
Feb 14, 2023
Merged
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
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
"""
Comment on lines +586 to +588
Copy link
Contributor

Choose a reason for hiding this comment

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

Whoop!

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