Error loading file viewer.
How to visualize OEMOF models with Plotly Dash
The snippet can be accessed without any authentication.
Authored by
Andreas Wunsch
This notebook shows an example of how interactive OEMOF system graphs can be created with Plotly-Dash for defined energy systems.
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### General description\n",
"\n",
"This notebook shows an example of how interactive [OEMOF](https://oemof.org/) system graphs can be created with [Plotly-Dash](https://dash.plotly.com/) for defined energy systems.\n",
"\n",
"Three functions are defined for this purpose: \n",
"\n",
"1. the `make_network` function creates a directed graph (`DiGraph`) from the `oemof` energy system. The nodes of the graph represent the components of the energy system, and the edges represent the connections between these components. Based on the type of the components (source, sink, converter, bus, generic memory), colors and shapes are assigned to the nodes.\n",
"\n",
"2. the `make_cytoscape_elements` function converts the NetworkX graph into Cytoscape elements that can be used for visualization with Dash. Each node in the graph receives a dictionary with its properties such as ID, label, color and shape. The edges of the graph are also converted into corresponding Cytoscape elements.\n",
"\n",
"3. the `shownetwork` function initializes a dash application to visualize the network with Cytoscape. This function uses `make_cytoscape_elements` to create the Cytoscape elements and defines the layout and style definitions for the nodes and edges, including custom shapes for sources and sinks. Here we use the pre-defined OEMOF symbols. The application also includes a routine to make every node clickable (to show parameterization), and a button to export the visualization as an image.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Installation requirements\n",
"\n",
"oemof.solph > 0.5 \n",
"dash \n",
"dash_cytoscape \n",
"\n",
"we tested this notebok with: \n",
" \n",
"oemof.solph==0.5.5 \n",
"dash==2.18.1 \n",
"dash_cytoscape==1.0.2 "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Authors: \n",
"Tobias Hörter, \n",
"Andreas Wunsch\n",
"November, 2024\n",
"\n",
"#### Contact: \n",
"andreas.wunsch@iosb.fraunhofer.de \n",
"tobias.hoerter@iosb.fraunhofer.de\n",
"\n",
"Fraunhofer Institute of Optronics, System Technologies and Image Exploitation IOSB, \n",
"Department Systems for Measurement, Control and Diagnosis (MRD) \n",
"Fraunhoferstr. 1, 76131 Karlsruhe, GERMANY \n",
"[https://www.iosb.fraunhofer.de/en](https://www.iosb.fraunhofer.de/en/competences/system-technology/systems-measurement-control-diagnosis.html) \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### License\n",
"[GPLv3](https://www.gnu.org/licenses/gpl-3.0.de.html)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from oemof.solph import (\n",
" EnergySystem,\n",
" Bus,\n",
" Flow,\n",
")\n",
"from oemof.solph.components import (\n",
" Sink,\n",
" Source,\n",
" Converter,\n",
" GenericStorage,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"First create a dummy oemof energysystem without datetime index (not necessary):"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"energysystem = EnergySystem()\n",
"\n",
"bgas = Bus(label=\"natural_gas\")\n",
"bel = Bus(label=\"electricity\")\n",
"grid_sink = Sink(label=\"excess_bel\", inputs={bel: Flow()})\n",
"gas = Source(label=\"rgas\",outputs={bgas: Flow()})\n",
"wind = Source(label=\"wind\",outputs={bel: Flow()})\n",
"pv = Source(label=\"pv\",outputs={bel: Flow()})\n",
"demand = Sink(label=\"demand\",inputs={bel: Flow()},)\n",
"PowerPlant = Converter(label=\"pp_gas\", inputs={bgas: Flow()}, outputs={bel: Flow()})\n",
"storage = GenericStorage(label=\"storage\",inputs={bel: Flow()},outputs={bel: Flow()})\n",
"\n",
"energysystem.add(bgas, bel, grid_sink, gas, wind, pv, demand, PowerPlant, storage)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's define the functions described above to visualize the energysystem with Dash:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"import dash\n",
"from dash import html\n",
"import dash_cytoscape as cyto\n",
"import networkx as nx\n",
"from dash.dependencies import Input, Output\n",
"from oemof import solph\n",
"from IPython import get_ipython\n",
"import socket\n",
"\n",
"def make_network(energysystem):\n",
" # construct directed graph from oemof energysystem\n",
"\n",
" def get_parameters(component):\n",
" parameters = {}\n",
" \n",
" if isinstance(component, solph.components.GenericStorage) or isinstance(component, solph.components.Converter) or isinstance(component,solph.components.GenericCHP):\n",
" for attr, value in vars(component).items():\n",
" if not attr.startswith('_'): # Skip private attributes that are not relevant for the system\n",
" if isinstance(value, list):\n",
" value = '; '.join(map(str, value))\n",
" value = str(value) # Value needs to be a string if it is a list\n",
" parameters[attr] = value\n",
"\n",
"\n",
" for input, flow in component.inputs.items():\n",
" nominal_value_in = getattr(flow, 'nominal_value', 'N/A')\n",
" nominal_value_in=str(nominal_value_in)\n",
" variable_costs_in = getattr(flow, 'variable_costs', 'N/A')\n",
" variable_costs_in =str(variable_costs_in)\n",
"\n",
" parameters['nominal_value_in'] = nominal_value_in\n",
" parameters['variable_costs_input'] = variable_costs_in\n",
"\n",
" for output, flow in component.outputs.items():\n",
" nominal_value_out = getattr(flow, 'nominal_value', 'N/A')\n",
" variable_costs_out = getattr(flow, 'variable_costs', 'N/A')\n",
" variable_costs_out =str(variable_costs_out)\n",
" \n",
" nominal_value_out= str(nominal_value_out)\n",
" parameters['nominal_value_out'] = nominal_value_out\n",
" parameters['variable_costs_out'] = variable_costs_out\n",
" return parameters\n",
"\n",
" if isinstance(component, solph.components.Source): # For sources the relevant properties are in the output flow(sources have no input flow)\n",
" for output, flow in component.outputs.items():\n",
" nominal_value = getattr(flow, 'nominal_value', 'N/A')\n",
" nominal_value= str(nominal_value)\n",
" variable_costs = getattr(flow, 'variable_costs', 'N/A')\n",
" variable_costs = str(variable_costs)\n",
" parameters['nominal_value'] = nominal_value\n",
" parameters['variable_costs'] = variable_costs\n",
"\n",
" if isinstance(component, solph.components.Sink): # For sinks relevant properties are in the input flow\n",
" for input, flow in component.inputs.items():\n",
" nominal_value = getattr(flow, 'nominal_value', 'No nominal value')\n",
" nominal_value=str(nominal_value)\n",
" variable_costs = getattr(flow, 'variable_costs', 'N/A')\n",
" variable_costs = str(variable_costs)#'; '.join(map(str, variable_costs))\n",
" parameters['nominal_value'] = nominal_value\n",
" parameters['variable_costs'] = variable_costs\n",
"\n",
" return parameters\n",
"\n",
" G = nx.DiGraph() # Directed Graph using networkx\n",
" \n",
" for component in energysystem.nodes:\n",
" parameters = get_parameters(component)\n",
" G.add_node(component.label) # Use the real Oemof label for nodes\n",
"\n",
" if isinstance(component, solph.components.Source):\n",
" G.nodes[component.label]['type'] = 'source'\n",
" G.nodes[component.label]['parameters']= parameters\n",
" G.nodes[component.label]['color'] = 'yellow'\n",
" G.nodes[component.label]['shape'] = 'custom-source'\n",
" elif isinstance(component, solph.components.Sink):\n",
" G.nodes[component.label]['type'] = 'sink'\n",
" G.nodes[component.label]['parameters']= parameters\n",
" G.nodes[component.label]['color'] = 'green'\n",
" G.nodes[component.label]['shape'] = 'custom-sink'\n",
" elif isinstance(component, solph.components.Converter) or isinstance(component,solph.components.GenericCHP):\n",
" G.nodes[component.label]['type'] = 'converter'\n",
" G.nodes[component.label]['parameters'] = parameters \n",
" G.nodes[component.label]['color'] = 'gray'\n",
" G.nodes[component.label]['shape'] = 'rectangle'\n",
" elif isinstance(component, solph.buses.Bus):\n",
" G.nodes[component.label]['color'] = 'red'\n",
" G.nodes[component.label]['shape'] = 'ellipse' \n",
" elif isinstance(component, solph.components.GenericStorage):\n",
" G.nodes[component.label]['type'] = 'storage'\n",
" G.nodes[component.label]['parameters']=parameters\n",
" G.nodes[component.label]['color'] = 'black'\n",
" G.nodes[component.label]['shape'] = 'round-rectangle'\n",
" else:\n",
" G.nodes[component.label]['color'] = 'light-blue'\n",
" G.nodes[component.label]['shape'] = 'round-rectangle'\n",
" \n",
" for input_component in component.inputs:\n",
" G.add_edge(input_component.label, component.label)\n",
" for output_component in component.outputs:\n",
" G.add_edge(component.label, output_component.label)\n",
" \n",
" return G\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"def make_cytoscape_elements(G):\n",
" nodes = [{'data': {'id': node,\n",
" 'label': node,\n",
" 'type' : G.nodes[node].get('type',{}),\n",
" 'color': G.nodes[node]['color'],\n",
" 'shape': G.nodes[node]['shape'],\n",
" 'parameters': G.nodes[node].get('parameters', {})}}\n",
" for node in G.nodes()]\n",
" edges = [{'data': {'source': u, 'target': v}} for u, v in G.edges()]\n",
" return nodes + edges"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def shownetwork(network):\n",
" app = dash.Dash(__name__, suppress_callback_exceptions=True)\n",
" \n",
" elements = make_cytoscape_elements(network)\n",
" cyto.load_extra_layouts() \n",
" app.layout = html.Div([\n",
" cyto.Cytoscape(\n",
" id='cytoscape',\n",
" layout={'name': 'klay'}, # klay means horizontal layout. Change to e.g. 'dagre' for vertical layout\n",
" style={'width': '100%', 'height': '500px'},\n",
" elements=elements,\n",
" stylesheet=[\n",
" {\n",
" 'selector': 'node',\n",
" 'style': {\n",
" 'content': 'data(label)',\n",
" 'background-color': 'data(color)',\n",
" 'font-size': 10,\n",
" 'color': 'white',\n",
" 'text-valign': 'center',\n",
" 'text-halign': 'center',\n",
" 'width': '90px',\n",
" 'height': '70px',\n",
" 'shape': 'data(shape)',\n",
" 'text-outline-color': 'black',\n",
" 'text-outline-width': 0.5,\n",
" }\n",
" },\n",
" {\n",
" 'selector': '[shape = \"custom-source\"]',\n",
" 'style': {\n",
" 'shape': 'polygon',\n",
" 'shape-polygon-points': '-0.5 0.5, 0.5 0.5, 1 -0.5, -1 -0.5',\n",
" }\n",
" },\n",
" {\n",
" 'selector': '[shape = \"custom-sink\"]',\n",
" 'style': {\n",
" 'shape': 'polygon',\n",
" 'shape-polygon-points': '-0.5 -0.5, 0.5 -0.5, 1 0.5, -1 0.5',\n",
" }\n",
" },\n",
" {\n",
" 'selector': 'edge',\n",
" 'style': {\n",
" 'curve-style': 'straight',\n",
" 'width': 2,\n",
" 'line-color': 'gray',\n",
" 'target-arrow-color': 'gray',\n",
" 'target-arrow-shape': 'triangle',\n",
" 'arrow-scale': 2,\n",
" }\n",
" }\n",
" ]\n",
" ),\n",
" html.Div(id='node-data', style={'white-space': 'pre-line', 'color': 'white', 'background-color': '#2D3033', 'padding': '10px'}),\n",
" html.Button(\"Export as Image\", id=\"btn-image\", n_clicks=0)\n",
" ])\n",
"\n",
"\n",
" \n",
" textcolor='white' #(change textcolor depending on preferance)\n",
"\n",
" #implement routine to make results clickable\n",
" @app.callback(\n",
" Output('node-data', 'children'),\n",
" Input('cytoscape', 'tapNodeData')\n",
" )\n",
" def display_node_data(data): #https://dash.plotly.com/cytoscape/events\n",
" if data:\n",
" parameters = data.get('parameters', {})\n",
" components = []\n",
" components.append(html.H4(f\"Node {data['id']} Parameters:\", style={'color': textcolor}))\n",
"\n",
" if data.get('type')=='source' or data.get('type')=='sink':\n",
" for k,v in parameters.items():\n",
" components.append(html.P(f\"{k}: {v}\", style={'color': textcolor}))\n",
"\n",
" if data.get('type') == 'storage':\n",
" for k,v in parameters.items():\n",
" components.append(html.P(f\"{k}: {v}\", style={'color': textcolor}))\n",
" if data.get('type') == 'converter' and parameters:\n",
" for k, v in parameters.items():\n",
" components.append(html.P(f\"{k}: {v}\", style={'color': textcolor}))\n",
" \n",
" return html.Div(components)\n",
"\n",
" return html.P(\"Click on a node to see its parameters.\", style={'color': textcolor})\n",
"\n",
" app.clientside_callback(\n",
" \"\"\"\n",
" function(n_clicks) {\n",
" if (n_clicks > 0) {\n",
" var cy = window.cy;\n",
" if (cy) {\n",
" var png64 = cy.png({scale: 3, full: true});\n",
" var a = document.createElement('a');\n",
" a.href = png64;\n",
" a.download = 'Oemof_model.png';\n",
" a.click();\n",
" }\n",
" }\n",
" return 'Export as Image';\n",
" }\n",
" \"\"\",\n",
" Output('btn-image', 'children'),\n",
" Input('btn-image', 'n_clicks')\n",
" )\n",
"\n",
" def is_notebook(): # allow both the use within jupyter notebooks, as wells as within .py files \n",
" try:\n",
" shell = get_ipython().__class__.__name__\n",
" if shell == 'ZMQInteractiveShell':\n",
" return True\n",
" elif shell == 'TerminalInteractiveShell':\n",
" return False\n",
" else:\n",
" return False\n",
" except NameError:\n",
" return False,\n",
" \n",
"\n",
" #-----automatically run app on a free port-----#\n",
" # for successful visualization plotly dash needs a free port, which means you cannot run this routine in two different notebooks on the same port.\n",
" # In the following we search for a free port within a certain range: \n",
" def is_port_in_use(port):\n",
" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n",
" return s.connect_ex(('localhost', port)) == 0\n",
"\n",
" def find_free_port(start_port=8050, end_port=8100):\n",
" for port in range(start_port, end_port):\n",
" if not is_port_in_use(port):\n",
" return port\n",
" raise Exception(\"No free port found\")\n",
"\n",
" # Example usage\n",
" port = find_free_port(8050, 8100) #range can be increased/decreased...\n",
"\n",
" if is_notebook(): #directly displayed in jupyter\n",
" app.run_server(mode='inline', debug=True,port=port)\n",
" else: #gives a link for the port\n",
" app.run_server(debug=True,port=port)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Call the functions to visualize the energysystem. You can modify the visualiztation via drag and drop!"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
" <iframe\n",
" width=\"100%\"\n",
" height=\"650\"\n",
" src=\"http://127.0.0.1:8050/\"\n",
" frameborder=\"0\"\n",
" allowfullscreen\n",
" \n",
" ></iframe>\n",
" "
],
"text/plain": [
"<IPython.lib.display.IFrame at 0x27076640b80>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"network = make_network(energysystem)\n",
"shownetwork(network)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.11"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Please register or sign in to comment