diff --git a/dependencies.xml b/dependencies.xml index 6641e11c..49383fac 100644 --- a/dependencies.xml +++ b/dependencies.xml @@ -1,5 +1,6 @@
+ 0.3.5 1.0 diff --git a/src/NetworkPlot.py b/src/NetworkPlot.py new file mode 100644 index 00000000..b5e86b2d --- /dev/null +++ b/src/NetworkPlot.py @@ -0,0 +1,133 @@ +# Copyright 2020, Battelle Energy Alliance, LLC +# ALL RIGHTS RESERVED +""" + This module defines logic to create a resource utilization network + graph for HERON Simulations. +""" +import networkx as nx +import matplotlib as mpl +mpl.use('Agg') # Prevents the module from blocking while plotting +import matplotlib.pyplot as plt + + +class NetworkPlot: + """ + Represents a network graph visualization of the resources found + in a HERON system. + """ + + def __init__(self, components: list) -> None: + """ + Initialize the network plot. + @ In, components, list, the components defined by the input file. + @ Out, None + """ + self._components = components + self._resources = set() + self._producers_and_consumers = set() + self._capacities = {} + self._edges = [] + self._graph = nx.DiGraph() + self._find_nodes_and_edges() + self._plot_graph() + self._build_table() + + def _find_nodes_and_edges(self) -> None: + """ + Iterate over the components to determine nodes and their + associated directional edges. + @ In, None + @ Out, None + """ + for c in self._components: + self._producers_and_consumers.add(c.name) + + rsc_in = c.get_inputs() + for ri in rsc_in: + self._resources.add(ri) + self._graph.add_edge(ri, c.name) + self._edges.append((ri, c.name)) + + rsc_out = c.get_outputs() + for ro in rsc_out: + self._resources.add(ro) + self._graph.add_edge(c.name, ro) + self._edges.append((c.name, ro)) + + def _build_table(self) -> None: + """ + Table should have two major sections: economic info and optimization parameters + + Economic info: + - Cash flows (just the names?) + - Lifetime? + + Optimization settings: + - dispatch (fixed, independent, dependent) + - optimized, swept, or fixed? + - capacity (optimization bounds, sweep values, or fixed value) + + @ In, None + @ Out, None + """ + col_labels = ['Dispatchable?', 'Governed?'] + cell_text = [] + row_labels = [] + + for c in self._components: + row_labels.append(c.name) + cell_text.append([c.is_dispatchable(), c.is_governed()]) + + plt.table(cell_text, rowLabels=row_labels, colLabels=col_labels, loc='bottom') + + def _plot_graph(self) -> None: + """ + Plots and formats the graph + @ In, None + @ Out, None + """ + tech_options = { # TODO make this something that can be done dynamically + "node_size": 1000, + "node_color": "#FCEDDA", + "edgecolors": "#FCEDDA", + "linewidths": 1 + } + + resrc_options = { + "node_size": 1500, + "node_color": "#EE4E34", + "edgecolors": "#EE4E34", + "linewidths": 1 + } + + label_options = { + "font_size": 8, + "font_weight": "normal", + "font_color": "black", + } + + edge_options = { + 'edge_color': 'black', + "width": 1, + 'arrows': True, + 'arrowsize': 20 + } + + fig, ax = plt.subplots(figsize=(7,7)) + pos = nx.spring_layout(self._graph) + + nx.draw_networkx_nodes(self._graph, pos, nodelist=list(self._resources), **resrc_options) + nx.draw_networkx_nodes(self._graph, pos, nodelist=list(self._producers_and_consumers), **tech_options) + nx.draw_networkx_labels(self._graph, pos, **label_options) + nx.draw_networkx_edges(self._graph, pos, node_size=1500, **edge_options) + + ax.axis('off') + fig.set_facecolor('darkgrey') + + def save(self, filename: str) -> None: + """ + Save resource graph to file + @ In, filename, str, path to file + @ Out, None + """ + plt.savefig(filename) diff --git a/src/main.py b/src/main.py index 091cfc03..9ee42a86 100755 --- a/src/main.py +++ b/src/main.py @@ -20,6 +20,7 @@ from HERON.src.base import Base from HERON.src.Moped import MOPED from HERON.src.Herd import HERD +from HERON.src.NetworkPlot import NetworkPlot from ravenframework.MessageHandler import MessageHandler @@ -45,7 +46,7 @@ def __init__(self): 'suppressErrs': False,}) self.messageHandler = messageHandler - def read_input(self, name): + def read_input(self, name: str) -> None: """ Loads data from input @ In, name, str, name of file to read from @@ -68,7 +69,7 @@ def __repr__(self): """ return '' - def print_me(self, tabs=0, tab=' '): + def print_me(self, tabs=0, tab=' ') -> None: """ Prints info about self. @ In, tabs, int, number of tabs to insert @@ -86,6 +87,19 @@ def print_me(self, tabs=0, tab=' '): for source in self._sources: source.print_me(tabs=tabs+1, tab=tab) + def plot_resource_graph(self) -> None: + """ + Plots the resource graph of the HERON simulation using components + from the input file. + + @ In, None + @ Out, None + """ + if self._case.debug['enabled']: # TODO do this every time? + graph = NetworkPlot(self._components) + img_path = os.path.join(self._input_dir, 'network.png') + graph.save(img_path) + def create_raven_workflow(self, case=None): """ Loads, modifies, and writes a RAVEN template workflow based on the Case. @@ -154,13 +168,14 @@ def main(): sim.read_input(args.xml_input_file) # TODO expand to use arguments? # print details sim.print_me() + sim.plot_resource_graph() + if sim._case._workflow == 'standard': sim.create_raven_workflow() elif sim._case._workflow == 'MOPED': sim.run_moped_workflow() elif sim._case._workflow == 'DISPATCHES': sim.run_dispatches_workflow() - # TODO someday? sim.run() if __name__ == '__main__': diff --git a/tests/integration_tests/mechanics/debug_mode/tests b/tests/integration_tests/mechanics/debug_mode/tests index a50060aa..da93306f 100644 --- a/tests/integration_tests/mechanics/debug_mode/tests +++ b/tests/integration_tests/mechanics/debug_mode/tests @@ -16,6 +16,10 @@ type = Exists output = 'Debug_Run_o/dispatch_id0_y10_c0.png Debug_Run_o/dispatch_id0_y11_c0.png Debug_Run_o/dispatch_id1_y10_c0.png Debug_Run_o/dispatch_id1_y11_c0.png' [../] + [./debug_plot] + type = Exists + output = 'network.png' + [../] [../] []