From e6c45097f4b2fdb7280f33e113888fcd4dfda382 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Keck <Jean-Baptiste.Keck@imag.fr> Date: Fri, 17 Apr 2020 14:02:14 +0200 Subject: [PATCH] basic graph visu support --- hysop/core/graph/computational_graph.py | 43 ++++- hysop/core/graph/graph.py | 147 ++++++++++++------ hysop/core/graph/tests/test_graph.py | 2 +- hysop/numerics/splitting/test/test_strang.py | 2 - hysop/operator/tests/test_fd_derivative.py | 1 - .../tests/test_spectral_derivative.py | 1 - .../particles_above_salt_bc.py | 2 +- .../particles_above_salt_bc_3d.py | 2 +- .../particles_above_salt_periodic.py | 2 +- .../particles_above_salt_symmetrized.py | 2 +- .../sediment_deposit/sediment_deposit.py | 2 +- .../sediment_deposit_levelset.py | 2 +- 12 files changed, 143 insertions(+), 65 deletions(-) diff --git a/hysop/core/graph/computational_graph.py b/hysop/core/graph/computational_graph.py index d95fde0e6..eda63ad23 100644 --- a/hysop/core/graph/computational_graph.py +++ b/hysop/core/graph/computational_graph.py @@ -702,16 +702,51 @@ class ComputationalGraph(ComputationalGraphNode): print self.variable_report() print self.operator_report() - @debug - @graph_built - def display(self, visu_rank=0, vertex_font_size=10, edge_font_size=16): + def display(self, visu_rank=0, show_buttons=False): """ Display the reduced computational graph. """ from hysop import main_rank if (visu_rank is None) or (main_rank != visu_rank): return - raise NotImplementedError('This feature has not been implemented yet.') + + net = self.to_pyvis() + + import tempfile + with tempfile.NamedTemporaryFile(suffix='.html') as f: + net.show(f.name) + + def to_file(self, path, io_rank=0, show_buttons=False): + """ + Generate an interactive computational graph in an html file. + """ + from hysop import main_rank + if (io_rank is None) or (main_rank != io_rank): + return + + net = self.to_pyvis() + net.write_html(path) + + @graph_built + def to_pyvis(self): + """ + Convert the graph to a pyvis network for vizualization. + """ + try: + import pyvis + except ImportError: + msg='\nFATAL ERROR: Graph vizualization requires the pyvis module.\n' + print(msg) + raise + + graph = self.reduced_graph + network = pyvis.network.Network() + for node in graph: + node_id = int(node) + network.add_node(node_id, label=node.label, title=node.title, + color=node.color) + return network + @debug @graph_built diff --git a/hysop/core/graph/graph.py b/hysop/core/graph/graph.py index 6c2f37aff..1d8dadb31 100644 --- a/hysop/core/graph/graph.py +++ b/hysop/core/graph/graph.py @@ -1,5 +1,6 @@ import inspect, networkx from hysop import dprint +from hysop.constants import MemoryOrdering from hysop.tools.types import check_instance, first_not_None from hysop.tools.decorators import not_implemented, debug, wraps, profile @@ -25,9 +26,23 @@ def new_edge(graph, u, v, *args, **kwds): # /!\ We have to use networkx 2.2 which has a different interface for attributes graph.add_edge(u, v, object=EdgeAttributes(*args, **kwds)) return (u,v) + +def generate_vertex_colors(): + import matplotlib + from matplotlib import cm + c0 = cm.get_cmap('tab20c').colors + c1 = cm.get_cmap('tab20b').colors + colors = [] + for i in range(4): + colors += c0[i::4] + c1[i::4] + colors = tuple(map(matplotlib.colors.to_hex, colors)) + return colors class VertexAttributes(object): """Simple class to hold vertex data.""" + + colors = generate_vertex_colors() + def __init__(self, graph, operator=None): if not hasattr(graph, '_hysop_node_counter'): graph._hysop_node_counter = 0 @@ -61,6 +76,7 @@ class VertexAttributes(object): self.output_states = output_states return self + # hashing for networkx def hash(self): return self.node_id def __eq__(self, other): @@ -68,6 +84,87 @@ class VertexAttributes(object): def __int__(self): return self.node_id + # pyvis attributes for display + @property + def label(self): + return self.operator.pretty_name + @property + def title(self): + return self.node_info().replace('\n','<br>') + + @property + def color(self): + cq = self.command_queue + if (cq is None): + return None + assert isinstance(cq, int) and cq >= 0 + colors = self.colors + ncolors = len(colors) + return colors[cq%ncolors] + + def node_info(self): + op = self.operator + istates = self.input_states + ostates = self.output_states + + ifields = op.input_fields + ofields = op.output_fields + iparams = op.input_params + oparams = op.output_params + + memorder2str = { + MemoryOrdering.C_CONTIGUOUS: 'C', + MemoryOrdering.F_CONTIGUOUS: 'F', + } + + def ifinfo(field, topo): + info = (field.name, topo.id) + if istates: + assert field in istates + istate = istates[field] + assert (istate is not None) + info+=(memorder2str[istate.memory_order],) + info+=(str(istate.tstate),) + return ', '.join(map(str,info)) + def ofinfo(field, topo): + info = (field.name, topo.id) + if ostates: + assert field in ostates + ostate = ostates[field] + assert (ostate is not None) + info+=(memorder2str[ostate.memory_order],) + info+=(str(ostate.tstate),) + return ', '.join(map(str,info)) + def ipinfo(param): + return param.name + def opinfo(param): + return param.name + + prefix='  <b>' + suffix='</b>  ' + sep = '\n'+' '*14 + + ss = '<h2>Operator {}</h2>{}{}{}{}{}\n{}'.format(op.name, + '{p}Rank:{s}{}\n\n'.format(self.op_ordering, p=prefix, s=suffix) + if self.op_ordering else '', + '{p}Pin:{s}{}\n'.format(sep.join(ipinfo(param) + for param in iparams.values()), p=prefix, s=suffix+'  ') + if iparams else '', + '{p}Fin:{s}{}\n'.format(sep.join([ifinfo(f,topo) + for (f,topo) in ifields.iteritems()]), p=prefix, s=suffix+'  ') + if ifields else '', + '{p}Pout:{s}{}\n'.format(sep.join([opinfo(param) + for param in oparams.values()]), p=prefix, s=suffix) + if oparams else '', + '{p}Fout:{s}{}\n'.format(sep.join([ofinfo(f,topo) + for (f,topo) in ofields.iteritems()]), p=prefix, s=suffix) + if ofields else '', + '{p}Type:{s} {}'.format( + sep.join(map(lambda x: x.__name__, type(op).__mro__[:-2])), + p=prefix, s=suffix)) + return ss + + class EdgeAttributes(object): """Simple class to hold edge data.""" def __init__(self, variable=None, topology=None): @@ -242,53 +339,3 @@ def op_apply(f): return return ret return apply - -def _op_info(op, istates=None, ostates=None, jmp=False): - ifields = op.input_fields - ofields = op.output_fields - iparams = op.input_params - oparams = op.output_params - - memorder2str = { - MemoryOrdering.C_CONTIGUOUS: 'C', - MemoryOrdering.F_CONTIGUOUS: 'F', - } - - def ifinfo(field, topo): - info = (field.name, topo.id) - if istates: - assert field in istates - istate = istates[field] - assert (istate is not None) - info+=(memorder2str[istate.memory_order],) - info+=(str(istate.tstate),) - return info - def ofinfo(field, topo): - info = (field.name, topo.id) - if ostates: - assert field in ostates - ostate = ostates[field] - assert (ostate is not None) - info+=(memorder2str[ostate.memory_order],) - info+=(str(ostate.tstate),) - return info - def ipinfo(param): - return param.name - def opinfo(param): - return param.name - - ss = 'Operator {} => \n {}{}{}{}\n {}'.format(op.name, - 'Pin:{}\n '.format([ ipinfo(param) for param in iparams.values() ]) - if iparams else '', - 'Fin:{}\n '.format([ ifinfo(f,topo) for (f,topo) in ifields.iteritems() ]) - if ifields else '', - 'Pout:{}\n '.format([ opinfo(param) for param in oparams.values() ]) - if oparams else '', - 'Fout:{}\n '.format([ ofinfo(f,topo) for (f,topo) in ofields.iteritems() ]) - if ofields else '', - op.__class__) - if jmp: - return ss - else: - return ss.replace('\n',' ') - diff --git a/hysop/core/graph/tests/test_graph.py b/hysop/core/graph/tests/test_graph.py index a359a69b1..b91380728 100644 --- a/hysop/core/graph/tests/test_graph.py +++ b/hysop/core/graph/tests/test_graph.py @@ -101,4 +101,4 @@ class TestGraph(object): if __name__ == '__main__': test = TestGraph() - test.test_graph_build(display=False) + test.test_graph_build(display=True) diff --git a/hysop/numerics/splitting/test/test_strang.py b/hysop/numerics/splitting/test/test_strang.py index ac6048a00..b6c543fb0 100644 --- a/hysop/numerics/splitting/test/test_strang.py +++ b/hysop/numerics/splitting/test/test_strang.py @@ -81,8 +81,6 @@ class TestStrang(object): problem.insert(splitting) problem.insert(poisson) problem.build() - - problem.display() problem.finalize() def test_strang_2d(self, n=33): diff --git a/hysop/operator/tests/test_fd_derivative.py b/hysop/operator/tests/test_fd_derivative.py index 12f7f188d..036e75cc0 100644 --- a/hysop/operator/tests/test_fd_derivative.py +++ b/hysop/operator/tests/test_fd_derivative.py @@ -187,7 +187,6 @@ class TestFiniteDifferencesDerivative(object): for impl in implementations: for op in iter_impl(impl): op.build(outputs_are_inputs=True) - #op.display() dF = op.get_input_discrete_field(F) dgradF = op.get_output_discrete_field(gradF) diff --git a/hysop/operator/tests/test_spectral_derivative.py b/hysop/operator/tests/test_spectral_derivative.py index fbed428d1..a02ae7c9a 100644 --- a/hysop/operator/tests/test_spectral_derivative.py +++ b/hysop/operator/tests/test_spectral_derivative.py @@ -223,7 +223,6 @@ class TestSpectralDerivative(object): for impl in implementations: for op in iter_impl(impl): op.build(outputs_are_inputs=False) - # op.display() Fd = op.get_input_discrete_field(F) dFd = op.get_output_discrete_field(dF) diff --git a/hysop_examples/examples/particles_above_salt/particles_above_salt_bc.py b/hysop_examples/examples/particles_above_salt/particles_above_salt_bc.py index fecea38c9..cc3c345fc 100644 --- a/hysop_examples/examples/particles_above_salt/particles_above_salt_bc.py +++ b/hysop_examples/examples/particles_above_salt/particles_above_salt_bc.py @@ -296,7 +296,7 @@ def compute(args): # If a visu_rank was provided, and show_graph was set, # display the graph on the given process rank. if args.display_graph: - problem.display() + problem.display(args.visu_rank) # Create a simulation # (do not forget to specify the t and dt parameters here) diff --git a/hysop_examples/examples/particles_above_salt/particles_above_salt_bc_3d.py b/hysop_examples/examples/particles_above_salt/particles_above_salt_bc_3d.py index a54c41633..00b82718e 100644 --- a/hysop_examples/examples/particles_above_salt/particles_above_salt_bc_3d.py +++ b/hysop_examples/examples/particles_above_salt/particles_above_salt_bc_3d.py @@ -311,7 +311,7 @@ def compute(args): # If a visu_rank was provided, and show_graph was set, # display the graph on the given process rank. if args.display_graph: - problem.display() + problem.display(args.visu_rank) # Create a simulation # (do not forget to specify the t and dt parameters here) diff --git a/hysop_examples/examples/particles_above_salt/particles_above_salt_periodic.py b/hysop_examples/examples/particles_above_salt/particles_above_salt_periodic.py index fda6b5c66..c4a635967 100644 --- a/hysop_examples/examples/particles_above_salt/particles_above_salt_periodic.py +++ b/hysop_examples/examples/particles_above_salt/particles_above_salt_periodic.py @@ -305,7 +305,7 @@ def compute(args): # If a visu_rank was provided, and show_graph was set, # display the graph on the given process rank. if args.display_graph: - problem.display() + problem.display(args.visu_rank) # Create a simulation # (do not forget to specify the t and dt parameters here) diff --git a/hysop_examples/examples/particles_above_salt/particles_above_salt_symmetrized.py b/hysop_examples/examples/particles_above_salt/particles_above_salt_symmetrized.py index 42587f275..0fcfdcb60 100644 --- a/hysop_examples/examples/particles_above_salt/particles_above_salt_symmetrized.py +++ b/hysop_examples/examples/particles_above_salt/particles_above_salt_symmetrized.py @@ -293,7 +293,7 @@ def compute(args): # If a visu_rank was provided, and show_graph was set, # display the graph on the given process rank. if args.display_graph: - problem.display() + problem.display(args.visu_rank) # Create a simulation # (do not forget to specify the t and dt parameters here) diff --git a/hysop_examples/examples/sediment_deposit/sediment_deposit.py b/hysop_examples/examples/sediment_deposit/sediment_deposit.py index 1cd16b714..8bae7c71c 100644 --- a/hysop_examples/examples/sediment_deposit/sediment_deposit.py +++ b/hysop_examples/examples/sediment_deposit/sediment_deposit.py @@ -307,7 +307,7 @@ def compute(args): # If a visu_rank was provided, and show_graph was set, # display the graph on the given process rank. if args.display_graph: - problem.display() + problem.display(args.visu_rank) # Create a simulation # (do not forget to specify the t and dt parameters here) diff --git a/hysop_examples/examples/sediment_deposit/sediment_deposit_levelset.py b/hysop_examples/examples/sediment_deposit/sediment_deposit_levelset.py index 4b8622f08..9adbca147 100644 --- a/hysop_examples/examples/sediment_deposit/sediment_deposit_levelset.py +++ b/hysop_examples/examples/sediment_deposit/sediment_deposit_levelset.py @@ -368,7 +368,7 @@ def compute(args): # If a visu_rank was provided, and show_graph was set, # display the graph on the given process rank. if args.display_graph: - problem.display() + problem.display(args.visu_rank) # Create a simulation # (do not forget to specify the t and dt parameters here) -- GitLab