Source code for tessif.transform.nxgrph

# tessif/transform/nxgrph.py
"""
:mod:`nxgrph` is a :mod:`tessif` interface transforming the dictionairy
representation (see :mod:`tessif.transform.es2mapping`) of an (optimized)
energy system simulation model into a :class:`networkx.DiGraph`.

This module follows a 2 way approach on constructing :class:`networkx.Graph`
like objects:

    1. Approach is to expose a class like structure
       (:class:`~tessif.transform.nxgrph.Graph`) needing an
       :class:`~tessif.transform.es2mapping.base.ESTransformer` object for
       construction. Allowing automated postprocessing.

    2. Approach is exposing two explicit functions to create nodes and
       edges assuming a Graph like object already exists (which obviously
       could have been just constructed). This emulates puplic API
       functionality and allows the use of the dict processing capabilities
       coming with this module seperatey.


Note
----
When using this module to perform `NetworkX operations
<https://networkx.github.io/documentation/stable/reference/algorithms/index.html>`_
on the energy system it might be required to temporariliy `relabel nodes to
integers
<https://mriduls-networkx.readthedocs.io/en/latest/reference/relabel.html>`_
because of `complex node labeling
<https://networkx.github.io/documentation/stable/reference/generated/networkx.drawing.nx_agraph.pygraphviz_layout.html>`_.

"""
import tessif.frused.defaults as conventions
from tessif.frused.namedtuples import Edge
from tessif.write import log

import networkx
from collections import defaultdict
import logging
import dcttools


logger = logging.getLogger(__name__)


[docs]@log.timings def create_nodes(graph, nodes, defaults={}, **kwargs): """ Takes a nodelist of string uids as positional argument. All other arguments are aggregated into kwargs and end up as node attributes which are accesible via :attr:`networkx.Graph.nodes(data='attribute') <networkx.Graph.nodes>` Parameters ---------- graph : :class:`networkx.Graph` like object Graph object the nodes are created for and added to. nodes: :class:`~collections.abc.Iterable` Iterable of node uids as strings as in:: ['1', '2', '3'] or:: [str(node.uid) for node in energy_sytem.nodes] defaults : dict, default={} In case a dict is provided via :paramref:`~create_nodes.kwargs` and not every node is present in this dict, the keyword argument will be looked for in :paramref:`~create_nodes.defaults`. kwargs: value, dict Node attributes as keyword arguments to pass to the created nodes. Using a node dict as in ``{node: attribute}`` allows different attributes for each node. All keyword arguments can be single value arguments or ``{node_uid: value}`` dictionairies. :paramref:`~create_nodes.defaults` are used for those nodes not present in the dictionairy. Otherwise value will be set to ``None``. Note ---- To pass a bunch of keyword arguments directly use :meth:`networkx.Graph.add_node` to supply them directly as in >>> import networkx >>> grph = networkx.Graph() >>> kwargs = {'arg_1': 'value_1', 'arg_2': 'value_2'} >>> grph.add_node('node_1', **kwargs) >>> print(grph.nodes(data=True)) [('node_1', {'arg_1': 'value_1', 'arg_2': 'value_2'})] Return ------ node_attr : dict A dictionairy holding the processed and passed node attributes. Examples -------- Use a dict to populate each node seperatey. Use a single value for uniform value setting (logging stuff is done to enable tessif internal doctesting and can be ignored here): >>> from tessif.transform import nxgrph >>> import networkx as nx >>> import pprint >>> nodes = ['1', '2', '3'] >>> grph = nx.DiGraph(name='my_graph') >>> node_attributes = nxgrph.create_nodes( ... grph, nodes, ... d=dict(zip(nodes, [10, 20, 30])), ... i=1) >>> pprint.pprint(node_attributes) {'1': {'d': 10, 'i': 1}, '2': {'d': 20, 'i': 1}, '3': {'d': 30, 'i': 1}} """ # Generate a node_attr dictionairy: {node: {attr: value}} # Use defaultdict adding a new dict entry if key not present node_attr = defaultdict(dict) # aggregate kwargs into a nested dict as in {node: {attr: parameter}} node_attr = dcttools.maggregate( tlkys=nodes, nstd_dcts=[node_attr, ], dcts=[defaults, ], **kwargs) # create nodes, return empty dict as attribute if no attribute present for node in nodes: graph.add_node(node, **node_attr.get(node, {})) return node_attr
[docs]@log.timings def create_edges(graph, edges, defaults={}, **kwargs): """ Convenience function to populate the Graph-object with edges. Takes an iterable of edge tuple uids as positional argument. All other arguments are aggregated into kwargs and end up as edge attributes which are accesible via :attr:`networkx.Graph.edges(data='attribute') <networkx.Graph.edges>` Parameters ---------- graph : :class:`networkx.Graph` like object Graph object the nodes are created for and added to. edges: :class:`~collections.abc.Iterable` Iterable of edge uids as strings as in:: [('1', '2'), ('2', '3'), ('3', '1')] or:: [(str(inflow.uid), str(node.uid)) for node in esystem.nodes for inflow in node.inputs.keys()] defaults : dict, default={} In case a dict is provided via :paramref:`~create_edges.kwargs` and not every node is present in this dict, the keyword argument will be looked for in :paramref:`~create_edges.defaults`. kwargs: value, dict Edge attributes as keyword arguments to pass to the created nodes. Using a node dict as in ``{('node_from_uid', 'node_to_uid'): value}`` allows different attributes for each node. All keyword arguments can be single value arguments or ``{('node_from_uid', 'node_to_uid'): value}`` dictionairies. :paramref:`~create_edges.defaults` are used for those edges not present in the dictionairy. Otherwise value will set to ``None``. Return ------ edge_attr : dict A dictionairy holding the processed and passed edge attributes. Examples -------- Use a dict to populate each edge seperatey. Use a single value for uniform value setting (logging stuff is done to enable tessif internal doctesting and can be ignored here): >>> from tessif.transform import nxgrph >>> import networkx as nx >>> import pprint >>> edges = [('1', '2'), ('2', '3')] >>> grph = nx.DiGraph(name='my_graph') >>> edge_attributes = nxgrph.create_edges( ... grph, edges, ... d=dict(zip(edges, [10, 20])), ... v=1) >>> pprint.pprint(edge_attributes) {Edge(source='1', target='2'): {'d': 10, 'v': 1}, Edge(source='2', target='3'): {'d': 20, 'v': 1}} """ # Use namedtuplpes, cause idiomatic python: # transform edge iterable into a namedtuple Edge list: Edges = [Edge(*edge) for edge in edges] # generate an edge_attr dictionairy: {edge: {attr: value}} # Use default dict adding a new dict entry if key not present edge_attr = defaultdict(dict) # aggregate kwargs into a nested dict as in {edge: {attr: parameter}} edge_attr = dcttools.maggregate( tlkys=Edges, nstd_dcts=[edge_attr, ], dcts=[defaults, ], **kwargs) # Iterate through Edges to specify each edge individually: for edge in Edges: # Create edges, return empty dict if no edge attributes present graph.add_edge(edge.source, edge.target, **edge_attr.get(edge, {})) return edge_attr
[docs]class Graph(networkx.DiGraph): """ Graph object holding relevant energy system data as node and edge attributes. Convenience wrapper for creating a :class:`networkx.DiGraph` Designed to be used with a :class:`~tessif.transform.es2mapping.base.ESTransformer` object. For more flexibility and control use :func:`create_nodes` and :func:`create_edges`. Parameters ---------- es_transformer : :class:`~tessif.transform.es2mapping.base.ESTransformer` Energy system to dictionairy transformer object returning its data as a 2 layer nested dict in the form of ``{attribute: {node/edge: parameter}}`` if accessed for :func:`~tessif.transform.es2mapping.base.ESTransformer.node_data` / :func:`~tessif.transform.es2mapping.base.ESTransformer.edge_data` respectively. As well es a default dictionairy for node and edge attributes if accessed for :attr:`~tessif.transform.es2mapping.base.ESTransformer.defaults` **kwargs : key word arguments kwargs are passed to :class:`networkx.DiGraph` Examples -------- Use the :class:`Example Resultier <tessif.transform.es2mapping.base.XmplResultier>` to demonstrate behaviour: >>> from tessif.transform import nxgrph >>> from tessif.transform.es2mapping.base import XmplResultier >>> import pprint >>> grph = nxgrph.Graph(XmplResultier()) >>> pprint.pprint(list(grph.nodes(data=True))) [('1', {'attr_xmpl': 'red'}), ('2', {'attr_xmpl': 'red'}), ('3', {'attr_xmpl': 'red'})] >>> pprint.pprint(list(grph.edges(data=True))) [('1', '2', {'attr_xmpl': 3}), ('2', '3', {'attr_xmpl': 5}), ('3', '1', {'attr_xmpl': 4})] Use the oemof minimum working example together with the AllResultier: >>> from tessif.transform.es2mapping.omf import AllResultier >>> import tessif.examples.data.omf.py_hard as omf_examples >>> grph = nxgrph.Graph(AllResultier(omf_examples.create_mwe())) >>> pprint.pprint(list(grph.edges(data='specific_flow_costs'))) [('Power Line', 'Demand', 0), ('Renewable', 'Power Line', 9), ('CBET', 'Transformer', 0), ('CBE', 'CBET', 0), ('Transformer', 'Power Line', 10)] """ @log.timings def __init__(self, es_transformer, **kwargs): # call super constructor (netowrkx.DiGraph()) super().__init__(**kwargs) fltr = conventions.nxgrph_visualize_tags.node xcptns = conventions.nxgrph_visualize_xcptns.node node_attr, defaults = dcttools.kfrep( dcts=dcttools.kfltr( dcts=[es_transformer.node_data(), es_transformer.defaults], fltr=fltr), fnd=fltr, xcptns=xcptns) # Create a grpah create_nodes( graph=self, nodes=es_transformer.nodes, defaults=defaults, **node_attr) fltr = conventions.nxgrph_visualize_tags.edge xcptns = conventions.nxgrph_visualize_xcptns.edge edge_attr, defaults = dcttools.kfrep( dcts=dcttools.kfltr( dcts=[es_transformer.edge_data(), es_transformer.defaults], fltr=fltr), fnd=fltr, xcptns=xcptns) create_edges( graph=self, edges=es_transformer.edges, defaults=defaults, **edge_attr) logger.info("Successfully created an energy system graph with " + "{:.0f} nodes and {:.0f} edges".format( self.number_of_nodes(), self.number_of_edges())) def __repr__(self): return '<%s.%s object at %s>' % ( self.__class__.__module__, self.__class__.__name__, hex(id(self)) ) def __str__(self): return self.__repr__()