Source code for wnnet.graph

"""A module to create graphs from network data.

.. _Allowed_keywords:

The routines in this module allow for a number of optional keyword
arguments.  Not all keyword arguments are available for each routine,
however.  The documentation indicates which keyword arguments are
appropriate for a given routine.

The possible keyword arguments are:

.. _induced_nuc_xpath:

    ``induced_nuc_xpath`` (:obj:`str`): An XPath expression to select\
        the subset of nuclides in the graph.  The default is all species\
        in the network.

.. _induced_reac_xpath:

    ``induced_reac_xpath`` (:obj:`str`): An XPath expression to select\
        the subset of reactions in the graph.  The default is all reactions\
        in the network.

.. _flow_type:

    ``flow_type`` (:obj:`str`) A string giving the flow type to be\
        presented.  The possible values are `net`, which shows the forward\
        minus the reverse flow (or the opposite if the reverse flow is larger),\
        and `full`, which shows both the foward and reverse flows.  The\
        default is `net`.

.. _direction:

    ``direction`` (:obj:`str`) A string indicting the reaction directions\
        to show.  Allowed values are `forward`, `reverse`, and `both`\
        (the default, which shows both `forward` and `reverse`).

.. _reaction_color_tuples:

    ``reaction_color_tuples`` (:obj:`list`): A list of :obj:`tuple` objects\
        to select arc colors.  There is a tuple for each reaction type.\
        The first member of the tuple is an XPath expression to select the\
        reaction type while the second member is a string giving the color\
        for that reaction type.  The default is that all arcs are black.

.. _user_funcs:

    ``user_funcs`` (:obj:`dict`): A dictionary of user-defined\
        functions associated with a user_rate key.  The prototype for each\
        user rate function should be (*reaction*, *t_9*), where\
        *t_9* is the temperature in billions of Kelvin and *reaction*\
        is a `wnutils <https://wnutils.readthedocs.io>`_ reaction\
        instance.  Other data can be bound to the function.

.. _zone_user_funcs:

     ``zone_user_funcs`` (:obj:`dict`): A dictionary of user-defined\
        functions associated with a user_rate key.\
        The prototype for each\
        user rate function should be (*reaction*, *t_9*, *zone*), where\
        *t_9* is the temperature in billions of Kelvin and *reaction* and\
        *zone* are `wnutils <https://wnutils.readthedocs.io>`_ reaction and\
        zone instances.  Other data can be bound to the function.

.. _threshold:

    ``threshold`` (:obj:`float`):  The minimum flow (relative to the\
        maximum flow) to be shown on the graph.  The default is 0.01.

.. _scale:
        
    ``scale`` (:obj:`float`):  Scaling factor for the maximum weight arc.\
        The default is 10.

.. _state_scaling:
        
    ``state_scaling`` (:obj:`float`):  Scaling factor for isomeric states.\
        The default is 0.35.

.. _allow_isolated_species:
        
    ``allow_isolated_species`` (:obj:`bool`):  Boolean to choose whether\
        to allow isolated species (ones without incoming or outgoing arcs)\
        in the graph.  The default is *False*.

.. _title_func:

    ``title_func``: A\
       `function <https://docs.python.org/3/library/stdtypes.html#functions>`_\
        that applies the title to the graph.  The function must take\
        one :obj:`float` argument giving the maximum flow.\
        Other data can be bound to the function.  The function must return\
        a :obj:`str` giving the title.  The default is\
        :meth:`wnnet.graph.make_t9_rho_flow_string`.

.. _zone_title_func:
 
    ``zone_title_func``: A\
        `function <https://docs.python.org/3/library/stdtypes.html#functions>`_\
        that applies the title to the graph.  The function must take\
        three arguments.  The first is a :obj:`float` giving the maximum\
        flow.  The second is the zone object corresponding to\
        the graph while the third is the zone label.  Other data can \
        be bound to the function.  The function must return a :obj:`str` \
        giving the title.\
        The default is :meth:`wnnet.graph.make_time_t9_rho_flow_string`\
        for flow graphs and\
        :meth:`wnnet.graph.make_time_t9_rho_current_string` for integrated\
        current graphs.

.. _node_label_func:
        
    ``node_label_func``: A\
      `function  <https://docs.python.org/3/library/stdtypes.html#functions>`_ \
       that applies a label to each node in the graph.  The function\
       must take as argument a species name.  Other data can be bound to\
       the function.  The function must return a :obj:`str`\
       giving the label.\
       The default is :meth:`wnnet.graph.make_node_label`.  The *g_names*\
       argument for the default is bound by the calling routine.

.. _zone_node_label_func:

    ``zone_node_label_func``: A `function \
        <https://docs.python.org/3/library/stdtypes.html#functions>`_ \
        that applies a label to each node in the graph.  The function \
        must take as arguments a species name, a zone, and the zone label. \
        Other data can be bound to \
        the function.  The function must return a :obj:`str` \
        giving the label.  The default is \
        :meth:`wnnet.graph.make_zone_node_label`.

.. _scale_edge_weight_func:
        
    ``scale_edge_weight_func``: A `function \
        <https://docs.python.org/3/library/stdtypes.html#functions>`_ \
        that applies scales each edge weight in the graph.  The function \
        must take four arguments: a dictionary of edge data, the \
        maximum edge weight in the scope of the graph, a scale factor \
        by which to scale the weight (input as `scale`_ to this routine), \
        and a threshold for not including the edge in the graph \
        (input as `threshold`_ to this routine). \
        Other data can be bound to \
        the function.  The function must modify the weight in the \
        edge data and return a :obj:`bool` indicating whether to include \
        the edge in the graph (True) or not (False).\
        The default is :meth:`wnnet.graph.scale_edge_weight`.

.. _graph_attributes:
        
    ``graph_attributes`` (:obj:`dict`):  A dictionary of graphviz attributes\
        for the graph.  The default is *{"outputorder": "edgesfirst"}*.

.. _edge_attributes:
        
    ``edge_attributes`` (:obj:`dict`):  A dictionary of grapvhiz attributes\
        for the edges in the graph.  The default is *{"arrowsize": 0.2}*.

.. _node_attributes:
        
    ``node_attributes`` (:obj:`dict`):  A dictionary of graphviz\
        attributes for the nodes in the graph.  The default is\
        *{ "shape": "box", "fontsize": 16, "style": "filled",\
         "fillcolor": "white" }*.

.. _solar_species:

    ``solar_species`` (:obj:`list`):  A list of species to be\
        considered as the naturally occurring species.  The default is the list\
        returned from :meth:`wnnet.graph.get_solar_species`.

.. _solar_node_attributes:

    ``solar_node_attributes`` (:obj:`dict`):  A dictionary of graphviz\
        attributes to be applied to the solar species in the graph.\
        The default is *{"fillcolor": "yellow", "style": "filled"}*.

.. _special_node_attributes:

    ``special_node_attributes`` (:obj:`dict`):  A dictionary of graphviz\
        attributes to be applied to the special nodes in the graph.\
        The dictionary has as keys the names of the special nodes and as values\
        a dictionary of graphviz properties to be applied to the given special\
        node.

.. _special_edge_attributes:

    ``special_edge_attributes`` (:obj:`dict`):  A dictionary of graphviz\
        attributes to be applied to the special edges in the graph.\
        The dictionary has as keys a :obj:`tuple` *(u, v, reaction)*,\
        where *u* is the source of the directed edge (i.e., arc),\
        *v* is the target,\
        and *reaction* is the reaction from which the arc derives.  The\
        *reaction* tuple element serves as a key in the multi-digraph to\
        distinguish parallel arcs.  The values of the dictionary are each\
        a dictionary of graphviz properties to be applied to the given special\
        edge.

"""

import functools
import networkx as nx
import wnnet.flows as wf
import wnnet.graph_helper as gh


[docs] def get_solar_species(): """A method to return the naturally-occurring solar-system species. Returns: A :obj:`list`. A list containing :obj:`str` of the names of the naturally-occurring solar-system species. """ return gh.get_solar_species_list()
[docs] def make_time_t9_rho_flow_string(f_max, zone, zone_label): """The default title function for zone flow graphs. Args: ``f_max`` (:obj:`float`): The maximum flow in the scope of the graph. ``zone``: A `wnutils <https://wnutils.readthedocs.io>`_ zone object. ``zone_label`` (:obj:`str` or :obj:`tuple`): The label of the zone. Returns: A :obj:`str`. The title string. """ assert zone_label props = zone["properties"] time = float(props["time"]) t_9 = float(props["t9"]) rho = float(props["rho"]) return f"<time (s) = {gh.fman(time):.2f} x 10<sup>{gh.fexp(time):d}</sup>, \ T<sub>9</sub> = {t_9:.2f}, \ rho (g/cc) = {gh.fman(rho):.2f} x 10<sup>{gh.fexp(rho):d}</sup> (g/cc), \ max. flow = {gh.fman(f_max):.2f} x 10<sup>{gh.fexp(f_max):d}</sup> (s<sup>-1</sup>)>"
[docs] def make_time_t9_rho_current_string(f_max, zone, zone_label): """The default title function for zone integrated current graphs. Args: ``f_max`` (:obj:`float`): The maximum integrated current in the scope\ of the graph. ``zone``: A `wnutils <https://wnutils.readthedocs.io>`_ zone object. ``zone_label`` (:obj:`str` or :obj:`tuple`): The label of the zone. Returns: A :obj:`str`. The title string. """ assert zone_label props = zone["properties"] time = float(props["time"]) t_9 = float(props["t9"]) rho = float(props["rho"]) return f"<time (s) = {gh.fman(time):.2f} x 10<sup>{gh.fexp(time):d}</sup>, \ T<sub>9</sub> = {t_9:.2f}, \ rho (g/cc) = {gh.fman(rho):.2f} x 10<sup>{gh.fexp(rho):d}</sup> (g/cc), \ max. int. current = {gh.fman(f_max):.2f} x 10<sup>{gh.fexp(f_max):d}</sup>>"
[docs] def make_t9_rho_flow_string(f_max, t_9, rho): """The default title function for flow graphs. Args: ``f_max`` (:obj:`float`): The maximum flow in the scope of the graph. ``t_9`` (:obj:`float`): The temperature in 10\\ :sup:`9` K at which the flows are computed. This value is bound by the calling function. ``rho`` (:obj:`float`): The density in g/cc at which the flows are computed. This value is bound by the calling function. Returns: A :obj:`str`. The title string. """ return f"<T<sub>9</sub> = {t_9:.2f}, \ rho (g/cc) = {gh.fman(rho):.2f} x 10<sup>{gh.fexp(rho):d}</sup> (g/cc), \ max. flow = {gh.fman(f_max):.2f} x 10<sup>{gh.fexp(f_max):d}</sup> (s<sup>-1</sup>)>"
[docs] def make_node_label(name, g_names): """The default node label function. Args: ``name`` (:obj:`str`): The species name. ``g_names`` (:obj:`dict`): A dictionary of graphviz names.\ This dictionary is bound to the function by the calling routine. Returns: A :obj:`str`. The node label. """ return g_names[name]
[docs] def make_zone_node_label(name, zone, zone_label, g_names): """The default zone node label function. Args: ``name`` (:obj:`str`): The species name. ``zone``: A `wnutils <https://wnutils.readthedocs.io>`_ zone object. ``zone_label`` (:obj:`str` or :obj:`tuple`): The label of the zone. ``g_names`` (:obj:`dict`): A dictionary of graphviz names. This dictionary is bound to the function by the calling routine. Returns: A :obj:`str`. The node label. """ assert zone assert zone_label return make_node_label(name, g_names)
[docs] def scale_edge_weight(edge_data, f_max, scale, threshold): """The default edge weight scale function. Args: ``edge_data``: A dictionary of edge properties. ``f_max`` (:obj:`float`): The maximum edge weight. ``scale`` (:obj:`float`): The factor by which to scale the weight. ``threshold`` (:obj:`float`): The threshold for not including the edge. Returns: A :obj:`bool` indicating whether to include the edge (True) or not (False). """ if f_max == 0: return False if "penwidth" in edge_data: return True keep_edge = True _r = edge_data["weight"] / f_max if _r >= threshold: edge_data["penwidth"] = scale * _r else: keep_edge = False return keep_edge
[docs] def create_flow_graph(net, t_9, rho, mass_fractions, **kwargs): """A routine to create a flow graph for a given set of mass fractions at the input temperature and density. Args: ``net``: A wnnet network. ``t_9`` (:obj:`float`): The temperature in 10\\ :sup:`9` K at which to\ compute the flows. ``rho`` (:obj:`float`): The density in g/cc at which to compute the\ flows. ``mass_fractions`` (:obj:`float`): A\ `wnutils <https://wnutils.readthedocs.io>`_ dictionary of mass\ fractions. ``**kwargs``: The allowed optional\ :ref:`keyword <Allowed_keywords>` arguments for this routine are:\ `induced_nuc_xpath`_, `induced_reac_xpath`_, `flow_type`_,\ `user_funcs`_, `reaction_color_tuples`_, `threshold`_, `scale`_,\ `state_scaling`_, `allow_isolated_species`_, `title_func`_,\ `node_label_func`_, `scale_edge_weight_func`_, `graph_attributes`_,\ `edge_attributes`_, `node_attributes`_, `solar_species`_,\ `solar_node_attributes`_, `special_node_attributes`_,\ `special_edge_attributes`_. Returns: A `networkx multidigraph \ <https://networkx.org/documentation/stable/reference/classes/multidigraph.html>`_\ showing the flows. """ my_list = [ "induced_nuc_xpath", "induced_reac_xpath", "flow_type", "user_funcs", "reaction_color_tuples", "threshold", "scale", "state_scaling", "allow_isolated_species", "title_func", "node_label_func", "scale_edge_weight_func", "graph_attributes", "edge_attributes", "node_attributes", "solar_species", "solar_node_attributes", "special_node_attributes", "special_edge_attributes", ] my_args = gh.get_keywords(my_list, **kwargs) assert my_args["flow_type"] in ("net", "full") my_flows = wf.compute_flows( net, t_9, rho, mass_fractions, reac_xpath=my_args["induced_reac_xpath"], user_funcs=my_args["user_funcs"], ) # Get the subset of nuclides to view in the graph. Get anchors. subset_nuclides, anchors = gh.get_subset_and_anchors( net.get_nuclides(nuc_xpath=my_args["induced_nuc_xpath"]) ) # Title if not my_args["title_func"]: my_args["title_func"] = functools.partial( make_t9_rho_flow_string, t_9=t_9, rho=rho ) # Node label if not my_args["node_label_func"]: g_names = net.xml.get_graphviz_names(subset_nuclides) my_args["node_label_func"] = functools.partial( make_node_label, g_names=g_names ) # Scale edge weight if not my_args["scale_edge_weight_func"]: my_args["scale_edge_weight_func"] = scale_edge_weight return gh.create_flow_graph( net, my_flows, subset_nuclides, anchors, **my_args )
[docs] def create_zone_flow_graphs(net, zones, **kwargs): """A routine to create flow graphs for a set of zones. Args: ``net``: A wnnet network. ``zones``: A `wnutils <https://wnutils.readthedocs.io>`_\ dictionary of zones. ``**kwargs``: The allowed optional\ :ref:`keyword <Allowed_keywords>` arguments for this routine are:\ `induced_nuc_xpath`_, `induced_reac_xpath`_, `flow_type`_,\ `zone_user_funcs`_, `reaction_color_tuples`_, `threshold`_,\ `scale`_, `state_scaling`_, `allow_isolated_species`_,\ `zone_title_func`_, `zone_node_label_func`_,\ `scale_edge_weight_func`_,\ `graph_attributes`_, `edge_attributes`_, `node_attributes`_,\ `solar_species`_, `solar_node_attributes`_,\ `special_node_attributes`_, `special_edge_attributes`_. Returns: A :obj:`dict` of\ `networkx multidigraphs\ <https://networkx.org/documentation/stable/reference/classes/multidigraph.html>`_\ showing the flows. The keys are the zone labels. """ my_list = [ "induced_nuc_xpath", "induced_reac_xpath", "flow_type", "zone_user_funcs", "reaction_color_tuples", "threshold", "scale", "state_scaling", "allow_isolated_species", "zone_title_func", "zone_node_label_func", "scale_edge_weight_func", "graph_attributes", "edge_attributes", "node_attributes", "solar_species", "solar_node_attributes", "special_node_attributes", "special_edge_attributes", ] my_args = gh.get_keywords(my_list, **kwargs) result = {} my_flows = wf.compute_flows_for_zones( net, zones, reac_xpath=my_args["induced_reac_xpath"], user_funcs=my_args["zone_user_funcs"], ) subset_nuclides, anchors = gh.get_subset_and_anchors( net.get_nuclides(nuc_xpath=my_args["induced_nuc_xpath"]) ) g_names = net.xml.get_graphviz_names(subset_nuclides) if not my_args["scale_edge_weight_func"]: my_args["scale_edge_weight_func"] = scale_edge_weight # Loop on zones for key, value in zones.items(): # Title if not my_args["zone_title_func"]: my_args["title_func"] = functools.partial( make_time_t9_rho_flow_string, zone=value, zone_label=key ) else: my_args["title_func"] = functools.partial( my_args["zone_title_func"], zone=value, zone_label=key ) # Node label if not my_args["zone_node_label_func"]: my_args["node_label_func"] = functools.partial( make_zone_node_label, zone=value, zone_label=key, g_names=g_names, ) else: my_args["node_label_func"] = functools.partial( my_args["zone_node_label_func"], zone=value, zone_label=key ) # Create graph result[key] = gh.create_flow_graph( net, my_flows[key], subset_nuclides, anchors, **my_args ) return result
[docs] def create_network_graph(net, **kwargs): """A routine to create a network graph showing species and reactions\ among them. Args: ``net``: A wnnet network. ``**kwargs``: The allowed optional\ :ref:`keyword <Allowed_keywords>` arguments for this routine are:\ `induced_nuc_xpath`_, `induced_reac_xpath`_, `direction`_,\ `reaction_color_tuples`_, `threshold`_, `scale`_, `state_scaling`_,\ `allow_isolated_species`_, `node_label_func`_, `graph_attributes`_,\ `edge_attributes`_, `node_attributes`_, `solar_species`_,\ `solar_node_attributes`_, `special_node_attributes`_,\ `special_edge_attributes`_. Returns: A `networkx multidigraph\ <https://networkx.org/documentation/stable/reference/classes/multidigraph.html>`_\ showing the network; that is, the nuclides and reactions among them. """ my_list = [ "induced_nuc_xpath", "induced_reac_xpath", "direction", "reaction_color_tuples", "threshold", "scale", "state_scaling", "allow_isolated_species", "node_label_func", "graph_attributes", "node_attributes", "edge_attributes", "solar_species", "solar_node_attributes", "special_node_attributes", "special_edge_attributes", ] my_args = gh.get_keywords(my_list, **kwargs) assert my_args["direction"] in ("forward", "reverse", "both") nuclides = net.get_nuclides() # Get the subset of nuclides to view in the graph. Get anchors. val, anchors = gh.get_subset_and_anchors( net.get_nuclides(nuc_xpath=my_args["induced_nuc_xpath"]) ) d_g = nx.MultiDiGraph() # Add nodes. for nuc in nuclides: d_g.add_node(nuc) # Add arcs (reactions). gh.add_reactions_to_graph(net, d_g, my_args) # Apply attributes gh.apply_graph_attributes(d_g, my_args["graph_attributes"]) gh.apply_node_attributes(d_g, my_args["node_attributes"]) gh.apply_edge_attributes(d_g, my_args["edge_attributes"]) gh.apply_solar_node_attributes( d_g, my_args["solar_species"], my_args["solar_node_attributes"] ) gh.apply_special_node_attributes(d_g, my_args["special_node_attributes"]) gh.apply_special_edge_attributes(d_g, my_args["special_edge_attributes"]) # Remove isolated nodes if desired if not my_args["allow_isolated_species"]: gh.remove_isolated_nodes(d_g, my_args) # Restore anchors for anchor in anchors: if anchor not in d_g.nodes: d_g.add_node(anchor, style="invis") # Subgraph sub_graph = nx.subgraph(d_g, val) # Node label if my_args["node_label_func"]: _node_label_func = my_args["node_label_func"] else: g_names = net.xml.get_graphviz_names(list(sub_graph.nodes.keys())) _node_label_func = functools.partial(make_node_label, g_names=g_names) for node in sub_graph.nodes: sub_graph.nodes[node]["pos"] = gh.get_pos( net, node, my_args["state_scaling"] ) sub_graph.nodes[node]["label"] = _node_label_func(node) gh.color_edges(sub_graph, net, my_args["reaction_color_tuples"]) return sub_graph
[docs] def create_zone_integrated_current_graphs(net, zones, **kwargs): """A routine to create integrated currents graphs for a set of zones. Args: ``net``: A wnnet network. ``zones``: A `wnutils <https://wnutils.readthedocs.io>`_ dictionary\ of zones. ``**kwargs``: The allowed optional\ :ref:`keyword <Allowed_keywords>` arguments for this routine are:\ `induced_nuc_xpath`_, `induced_reac_xpath`_,\ `reaction_color_tuples`_, `threshold`_, `scale`_, `state_scaling`_,\ `allow_isolated_species`_, `zone_title_func`_,\ `zone_node_label_func`_,\ `scale_edge_weight_func`_, `graph_attributes`_, `edge_attributes`_,\ `node_attributes`_, `solar_species`_, `solar_node_attributes`_,\ `special_node_attributes`_, `special_edge_attributes`_. Returns: A :obj:`dict` of `networkx multidigraphs \ <https://networkx.org/documentation/stable/reference/classes/multidigraph.html>`_ \ showing the integrated currents. The keys are the zone labels. """ my_list = [ "induced_nuc_xpath", "induced_reac_xpath", "reaction_color_tuples", "threshold", "scale", "state_scaling", "allow_isolated_species", "zone_title_func", "zone_node_label_func", "scale_edge_weight_func", "graph_attributes", "edge_attributes", "node_attributes", "solar_species", "solar_node_attributes", "special_node_attributes", "special_edge_attributes", ] my_args = gh.get_keywords(my_list, **kwargs) result = {} subset_nuclides, anchors = gh.get_subset_and_anchors( net.get_nuclides(nuc_xpath=my_args["induced_nuc_xpath"]) ) g_names = net.xml.get_graphviz_names(list(net.get_nuclides().keys())) if not my_args["scale_edge_weight_func"]: my_args["scale_edge_weight_func"] = scale_edge_weight for key, value in zones.items(): # Title if not my_args["zone_title_func"]: my_args["title_func"] = functools.partial( make_time_t9_rho_current_string, zone=value, zone_label=key ) else: my_args["title_func"] = functools.partial( my_args["zone_title_func"], zone=value, zone_label=key ) # Node label if not my_args["zone_node_label_func"]: my_args["node_label_func"] = functools.partial( make_zone_node_label, zone=value, zone_label=key, g_names=g_names, ) else: my_args["node_label_func"] = functools.partial( my_args["zone_node_label_func"], zone=value, zone_label=key, g_names=g_names, ) result[key] = gh.create_integrated_current_graph( net, value, subset_nuclides, anchors, **my_args ) return result