├── Output ├── errorLog │ ├── README.md │ └── .DS_Store ├── NodeLists │ ├── README.md │ └── .DS_Store ├── graphs_plots │ ├── README.md │ └── .DS_Store ├── graphs_graphml │ ├── noPCE │ │ ├── README.md │ │ └── .DS_Store │ ├── complete │ │ ├── README.md │ │ └── .DS_Store │ └── .DS_Store └── .DS_Store ├── .DS_Store ├── figures ├── dexpi2graph_gui.png ├── dexpi2graph_idea.png └── dexpi2graph_workflow.png ├── GUI_figs ├── dexpi2graphML_logo.ico ├── dexpi2graphML_logo.png └── AD_Logo_EN_600dpi_gui.png ├── dexpi2graph_python ├── dexpi2graphML-ad@TUDO.py └── functions.py ├── README.md └── LICENSE.txt /Output/errorLog/README.md: -------------------------------------------------------------------------------- 1 | Directory to store errorLog files! -------------------------------------------------------------------------------- /Output/NodeLists/README.md: -------------------------------------------------------------------------------- 1 | Directory to store P&ID node lists! 2 | -------------------------------------------------------------------------------- /Output/graphs_plots/README.md: -------------------------------------------------------------------------------- 1 | Directory to store P&ID plots! 2 | -------------------------------------------------------------------------------- /Output/graphs_graphml/noPCE/README.md: -------------------------------------------------------------------------------- 1 | Directory to store P&ID files! 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/.DS_Store -------------------------------------------------------------------------------- /Output/graphs_graphml/complete/README.md: -------------------------------------------------------------------------------- 1 | Directory to store P&ID files! 2 | -------------------------------------------------------------------------------- /Output/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/Output/.DS_Store -------------------------------------------------------------------------------- /Output/NodeLists/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/Output/NodeLists/.DS_Store -------------------------------------------------------------------------------- /Output/errorLog/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/Output/errorLog/.DS_Store -------------------------------------------------------------------------------- /figures/dexpi2graph_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/figures/dexpi2graph_gui.png -------------------------------------------------------------------------------- /figures/dexpi2graph_idea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/figures/dexpi2graph_idea.png -------------------------------------------------------------------------------- /GUI_figs/dexpi2graphML_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/GUI_figs/dexpi2graphML_logo.ico -------------------------------------------------------------------------------- /GUI_figs/dexpi2graphML_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/GUI_figs/dexpi2graphML_logo.png -------------------------------------------------------------------------------- /Output/graphs_graphml/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/Output/graphs_graphml/.DS_Store -------------------------------------------------------------------------------- /Output/graphs_plots/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/Output/graphs_plots/.DS_Store -------------------------------------------------------------------------------- /figures/dexpi2graph_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/figures/dexpi2graph_workflow.png -------------------------------------------------------------------------------- /GUI_figs/AD_Logo_EN_600dpi_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/GUI_figs/AD_Logo_EN_600dpi_gui.png -------------------------------------------------------------------------------- /Output/graphs_graphml/noPCE/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/Output/graphs_graphml/noPCE/.DS_Store -------------------------------------------------------------------------------- /Output/graphs_graphml/complete/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TUDoAD/DEXPI2graphML/HEAD/Output/graphs_graphml/complete/.DS_Store -------------------------------------------------------------------------------- /dexpi2graph_python/dexpi2graphML-ad@TUDO.py: -------------------------------------------------------------------------------- 1 | import PySimpleGUI as psg 2 | import os 3 | from PIL import Image, ImageTk 4 | import functions 5 | import subprocess 6 | import lxml 7 | import pandas as pd 8 | 9 | #psg.theme('Default') 10 | 11 | # Define window content 12 | col_left = [[psg.Text("Choose DEXPI - P&ID - folder...")], 13 | [psg.Input(key='path_dexpi'), psg.FolderBrowse()], 14 | [psg.Text("Processing Information / Console...")], 15 | [psg.Output(size=(60,30), key='_output_')], 16 | [psg.Button('Convert'), psg.Button('show graphML P&ID in Explorer'), psg.Button('show Plot in Explorer')], 17 | ] 18 | 19 | image_elem =psg.Image(size=(600, 450), key='plot_graph', visible=True, background_color ='white') 20 | list_elem = psg.Listbox(os.listdir('./Output/graphs_plots'), key='selected_plot', size=(50,5)) 21 | 22 | col_right =[[psg.Text('Plot')] , 23 | [image_elem], 24 | [list_elem], 25 | [psg.Button('P&ID-graph Plot'), psg.Button('show graphML'), psg.Button('show Error Log')]] 26 | 27 | col_bottom = [[psg.Image('./GUI_figs/AD_Logo_EN_600dpi_gui.png')], 28 | [psg.Text(' CC: Technische Universität Dortmund, AG Apparatedesign \n Author: Jonas Oeing')]] 29 | 30 | layout = [[psg.Column(col_left), psg.Column(col_right, element_justification='c')], 31 | [ psg.Column(col_bottom)]] 32 | 33 | 34 | # create window 35 | window = psg.Window('dexpi2graph - ad@TUDO', layout) 36 | 37 | # Display and interact with the Window using an Event Loop 38 | while True: 39 | event, values = window.read() 40 | list_elem.update(os.listdir('./Output/graphs_plots')) 41 | if event == 'Convert': 42 | 43 | if values['path_dexpi']=='': 44 | psg.popup('Enter path of the DEXPI folder!') 45 | 46 | else: 47 | print('open directory:', values['path_dexpi']) 48 | print('Start conversion of DEXPI files into graphML...') 49 | 50 | for file in os.listdir(values['path_dexpi']): 51 | savename=file[:-4] 52 | functions.Dexpi2graph(values['path_dexpi']+'/'+file, './Output/graphs_graphml/complete/', './Output/graphs_graphml/noPCE/', './Output/NodeLists/', './Output/errorLog/', savename) 53 | functions.plot_graph2('./Output/graphs_graphml/complete/'+file, './Output/graphs_plots/'+savename) 54 | list_elem.update(os.listdir('./Output/graphs_plots')) 55 | 56 | ### Kasten auswahl einfügen 57 | if event == 'show graphML P&ID in Explorer': 58 | Application = os.getcwd() 59 | Application = 'explorer "'+Application+'\Output\graphs_graphml\complete"' 60 | subprocess.Popen(Application) 61 | 62 | if event == 'P&ID-graph Plot': 63 | if values['selected_plot']==[]: 64 | psg.popup('Choose a P&ID-graph!') 65 | 66 | else: 67 | img = Image.open('./Output/graphs_plots/'+values['selected_plot'][0]) 68 | img.thumbnail((600, 500)) 69 | image_elem.update(data=ImageTk.PhotoImage(img), size=(600, 450)) 70 | 71 | if event == 'show Plot in Explorer': 72 | Application = os.getcwd() 73 | Application = 'explorer "'+Application+'\Output\graphs_plots"' 74 | subprocess.Popen(Application) 75 | 76 | if event == 'show graphML': 77 | if values['selected_plot']==[]: 78 | psg.popup('Choose a P&ID-graph!') 79 | 80 | else: 81 | window.FindElement('_output_').Update('') 82 | file = values['selected_plot'][0][:-4] 83 | xml_file = './Output/graphs_graphml/complete/'+file+'.xml' 84 | tree = lxml.etree.parse(xml_file) 85 | pretty = lxml.etree.tostring(tree, encoding="unicode", pretty_print=True) 86 | print(pretty) 87 | 88 | if event == 'show Error Log': 89 | if values['selected_plot']==[]: 90 | psg.popup('Choose a P&ID-graph!') 91 | 92 | else: 93 | window.FindElement('_output_').Update('') 94 | file = values['selected_plot'][0][:-4] 95 | error_file = './Output/errorLog/'+file+'_ErrorLog.xlsx' 96 | error_df = pd.read_excel(error_file) 97 | for i in range(0, len(error_df)): 98 | print(error_df['Warning'][i]) 99 | print(error_df['Node(s)'][i]) 100 | print('\n') 101 | 102 | 103 | # See if user wants to quit or window was closed 104 | if event == psg.WINDOW_CLOSED: 105 | break 106 | # Output a message to the window 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEXPI2graphML-Converter 2 | 3 | ## Publication using the DEXPI2graphML converter: https://doi.org/10.1016/j.dche.2022.100038 4 | Jonas Oeing, Wolfgang Welscher, Niclas Krink, Lars Jansen, Fabian Henke, Norbert Kockmann, 5 | Using artificial intelligence to support the drawing of piping and instrumentation diagrams using DEXPI standard, 6 | Digital Chemical Engineering, Volume 4, 2022, ISSN 2772-5081, 7 | 8 | ### Authors: 9 | Jonas Oeing, Tim Holtermann
10 | TU Dortmund University, 11 | Laboratory of Equipment Design 12 | 13 | ### Tool to convert DEXPI-P&ID-Flowsheets into GraphML-graphs, which can be processed in ML/DL-applications via python (e.g. pytorch, keras etc.) 14 | - DEXPI (specification [1]) provides machine-readable plant topology data 15 | - Availability for application in artificial intelligence 16 | - Conversion via a graphical user interface 17 | 18 |
19 | SelfHTML-Logo 20 |
21 | Figure 1. Idea of converting a DEXPI Piping- and Instrumentation Diagram (P&ID) into a graphML graph representation. 22 |
23 |
24 | 25 | ## Note: 26 | The skript was tested with P&ID-flowsheets (DEXPI-xml) created with the software PlantEngineer developed by X-Visual Technologies. 27 | It is important, that the connection of all MS Visio shapes are correctly connected by the user. Otherwise the tool will not achieve the 28 | desired output because of mission topology information of the DEXPI files. 29 | 30 | ## Install: 31 | The converter is available as an application for Python. 32 | A application for Windows 10 will be available soon. 33 | 34 | ### Python installation 35 | 1. Install Python (anaconda) from https://www.anaconda.com/products/individual 36 | 2. Load the following python libraries 37 | - [NetworkX](https://networkx.org/) (vers. 2.4) [2] 38 | - [Matplotlib](https://matplotlib.org/) (vers. 3.2.2) [3] 39 | - [Pandas](https://pandas.pydata.org/) (vers. 1.0.5) [4] 40 | - [NLTK](https://www.nltk.org/) (vers. 3.5) [5] 41 | - [Pillow](https://pillow.readthedocs.io/en/stable/) (vers. 7.2.0) [6] 42 | - [PySimpleGUI](https://pysimplegui.readthedocs.io/en/latest/) (vers. 4.56.0) [7] 43 | - [lxml](https://lxml.de/) (vers. 4.5.2) [8] 44 | - [openpyxl]() (vers. 3.0.9) [9] 45 | 3. Download the folder dexpi2graph_python. 46 | 4. Running the script *dexpi2graphML.py* starts the converter. 47 | 48 | ## How to Use: 49 | The *DEXPI2graphML converter* consists a graphical user interface (GUI) shown in Figure 2. 50 | On the upper left side you find a bar to browse the input folder, which containts the DEXPI-P&IDs you want to convert. 51 | On the left handside a console shows the output as well as the error log of the conversion process. 52 | On the left handside you will find a plot window which shows the converted plots. 53 | 54 |
55 | dexpi2graph GUI 56 |
57 | Figure 2. GUI of the DEXPI2graphML converter. 58 |
59 |
60 | 61 | ## Example P&ID files: 62 | The folder *DEXPI_examples* containts two different DEXPI P&IDs: 63 | - A laboratory distillation plant [P&ID_distillation_laboratory](./DEXPI_examples/distillation_laboratory.xml) [10] 64 | - Textbooks example distillation plant [P&ID_distillation](./DEXPI_examples/distillation_plant.xml) [11] 65 | 66 | 67 | ## References: 68 | [1] DEXPI Initiative, DEXPI specification 1.3, https://dexpi.org/specifications/, accessed on 25.08.2021
69 | [2] NetworkX developers, online documentation, https://networkx.org/, accessed on 09.03.2022
70 | [3] Matplotlib development team, online documentation, https://matplotlib.org/, accessed on 09.03.2022
71 | [4] Pandas development team, online documentation, https://pandas.pydata.org/, accessed on 09.03.2022
72 | [5] NLTK project, online documentation, https://www.nltk.org/, accessed on 09.03.2022
73 | [6] Alex Clark and contributors, online documentation, https://pillow.readthedocs.io/en/stable/, accessed on 09.03.2022
74 | [7] PySimpleGUI, online documentation, https://pysimplegui.readthedocs.io/en/latest/, accessed on 09.03.2022
75 | [8] lxml development team, online documentation, https://lxml.de/, accessed on 09.03.2022
76 | [9] openpyxl, online documentation, https://openpyxl.readthedocs.io, accessed on 23.02.2023
77 | [10] Oeing, J., Neuendorf, L.M., Bittorf, L., Krieger, W. and Kockmann, N. (2021), Flooding Prevention in Distillation and Extraction Columns with Aid of Machine Learning Approaches. Chem. Ing. Tech., 93: 1917-1929. https://doi.org/10.1002/cite.202100051
78 | [11] Baerns, M., Behr, A., Brehm, A., Gmehling, J., Hinrichsen, K.-O., Hofmann, H., & Onken, U., Palkovits, R., Renken, A. (2013). Technische Chemie. Wiley-VCH, Weinheim.
79 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /dexpi2graph_python/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Author: Tim Holtermann (TU Dortmund, AG Apparatedesign) 4 | ''' 5 | 6 | def plot_graph(Path_graph, Path_plot): 7 | import networkx as nx 8 | import matplotlib.pyplot as plt 9 | 10 | 11 | graph=nx.read_graphml(Path_graph) 12 | color=[] 13 | width=[] 14 | node_group=nx.get_node_attributes(graph, 'node_group') 15 | edge_class=nx.get_edge_attributes(graph, 'edge_class') 16 | edge_sub_class=nx.get_edge_attributes(graph, 'edge_sub_class') 17 | dict_group_color={"Vessel":'red', 'Column':'red', 'Pipe tee':'grey', 'Valves':'grey', 'Fittings':'grey', 'Pump':'blue', 18 | 'Filter':'yellow', 'Heat exchanger':'orange', 'Connector':'brown', 'MSR':'green'} 19 | 20 | #color of nodes 21 | for node in graph.nodes(): 22 | Found='No' 23 | for group in dict_group_color: 24 | if node_group[node]==group: 25 | color.append(dict_group_color[group]) 26 | Found='Yes' 27 | if Found=='No': 28 | color.append('white') 29 | 30 | #width of the connections 31 | for edge in graph.edges(): 32 | if edge_class[edge] in ['Signal line', 'Process connection line']: 33 | width.append(1) 34 | elif edge_class[edge]=='Piping' and edge_sub_class[edge]=='Main pipe': 35 | width.append(1) 36 | elif edge_class[edge]=='Piping' and edge_sub_class[edge]=='Secondary pipe': 37 | width.append(1) 38 | elif edge_class[edge]=='Heat transfer medium': 39 | width.append(1) 40 | else: 41 | width.append(10) 42 | 43 | plot_graph = plt.figure(figsize=(15,10))#leeres Bild erzeugt, worauf gleich gezeichnet wird 44 | nx.draw_kamada_kawai(graph, node_color=color, node_size=150, font_size=10, width=width, arrowsize=15, with_labels=True) 45 | plot_graph.savefig(Path_plot) 46 | 47 | def plot_graph2(Path_graph, Path_plot): 48 | import networkx as nx 49 | import matplotlib.pyplot as plt 50 | import math 51 | 52 | graph=nx.read_graphml(Path_graph) 53 | color=[] 54 | width=[] 55 | node_sizes=[] 56 | pos={} 57 | node_group=nx.get_node_attributes(graph, 'node_group') 58 | edge_class=nx.get_edge_attributes(graph, 'edge_class') 59 | edge_sub_class=nx.get_edge_attributes(graph, 'edge_sub_class') 60 | dict_group_color={"Vessel":'orange', 'Column':'orange', 'Pipe tee':'grey', 'Valves':'grey', 'Fittings':'grey', 'Pump':'blue', 61 | 'Filter':'yellow', 'Heat exchanger':'red', 'Connector':'brown', 'MSR':'green'} 62 | 63 | 64 | 65 | #color of nodes 66 | for node in graph.nodes(): 67 | Found='No' 68 | for group in dict_group_color: 69 | if node_group[node]==group: 70 | color.append(dict_group_color[group]) 71 | Found='Yes' 72 | if Found=='No': 73 | color.append('white') 74 | 75 | if node_group[node] in ['Filter', 'Vessel', 'Column', 'Pump','Heat exchanger', '...']: 76 | node_sizes.append(2000) 77 | else: 78 | node_sizes.append(500) 79 | 80 | pos[node]=(float(graph.nodes[node]['node_x']), float(graph.nodes[node]['node_y'])) 81 | 82 | 83 | 84 | #width of the connections 85 | for edge in graph.edges(): 86 | if edge_class[edge] in ['Signal line', 'Process connection line']: 87 | width.append(1) 88 | elif edge_class[edge]=='Piping' and edge_sub_class[edge]=='Main pipe': 89 | width.append(1) 90 | elif edge_class[edge]=='Piping' and edge_sub_class[edge]=='Secondary pipe': 91 | width.append(1) 92 | elif edge_class[edge]=='Heat transfer medium': 93 | width.append(1) 94 | else: 95 | width.append(10) 96 | 97 | 98 | 99 | ##alpha=1, 100 | plot_graph = plt.figure(figsize=(40,20))#leeres Bild erzeugt, worauf gleich gezeichnet wird 101 | #pos=nx.spring_layout(graph, k=3/math.sqrt(graph.order())) 102 | nx.draw(graph, pos=pos, node_color=color, node_shape='o', node_size=node_sizes, edgecolors='none', font_size=10, width=width, edge_color='grey', arrowsize=20, with_labels=True, font_weight='bold') 103 | plot_graph.savefig(Path_plot) 104 | 105 | def Dexpi2graph(DEXPI_path, save_path_graph, save_path_graph_no_MSR, path_IDlist, path_errorLog, savename): 106 | 107 | import pandas as pd 108 | import networkx as nx 109 | import xml.etree.ElementTree as ET 110 | import nltk 111 | import sys 112 | 113 | mytree = ET.parse(DEXPI_path)#load DEXPI-File 114 | myroot = mytree.getroot() 115 | ID_list=pd.DataFrame(columns=['ID', 'P&ID_name', 'neighbors'])#mapping DataFrame 116 | Error_log=pd.DataFrame(columns=['Warning', 'Node(s)']) 117 | graph=nx.DiGraph()# create directed graph 118 | 119 | print('########################################') 120 | print('Start to process DEXPI file: ', savename) 121 | print('########################################') 122 | print('Collecting Equipment...') 123 | #EQUIPMENT 124 | 125 | i=0 126 | 127 | for equipment in myroot.findall('Equipment'):#all equipments 128 | 129 | #predefine the optional attributes 130 | P1='Not available' 131 | unit_P1='Not available' 132 | unit_P2='Not available' 133 | P2='Not available' 134 | T1='Not available' 135 | unit_T1='Not available' 136 | unit_T2='Not available' 137 | T2='Not available' 138 | V='Not available' 139 | unit_V='Not available' 140 | I='Not available' 141 | CH='Not available' 142 | L='Not available' 143 | O='Not available' 144 | M='Not available' 145 | a='Not available' 146 | b='Not available' 147 | R='Not available' 148 | 149 | #read 150 | ID=equipment.get('ID') 151 | N=equipment.get('TagName') 152 | 153 | #read equipment coordinates. 154 | Position = equipment.findall('Position/Location') 155 | x=float(Position[0].get('X').replace(',', '.'))#Unfortunately the entry is a string and the number format may have to be changed. 156 | y=float(Position[0].get('Y').replace(',', '.')) 157 | 158 | #Read all equipment attributes 159 | for equipment_attributes in equipment.findall('GenericAttributes/GenericAttribute'): 160 | if equipment_attributes.get('Name')=='CLASS':#selecting the class 161 | C=equipment_attributes.get('Value') 162 | 163 | elif equipment_attributes.get('Name')=='SUB_CLASS':#selecting the sub class 164 | C_sub=equipment_attributes.get('Value') 165 | 166 | elif equipment_attributes.get('Name')=='VPE_PRESSURE_DESIGN_MAX':#selecting max design pressure 167 | #In general, entry is a string containing value and unit, so the string has to be tokenized 168 | if len(nltk.word_tokenize(equipment_attributes.get('Value')))==2:#Assure correct entry 169 | P1=float(nltk.word_tokenize(equipment_attributes.get('Value'))[0].replace(',', '.')) 170 | unit_P1=nltk.word_tokenize(equipment_attributes.get('Value'))[1] 171 | elif equipment_attributes.get('Value')=='': 172 | P1='' 173 | unit_P1='' 174 | else: 175 | sys.exit('Invalid entry in Equipment properties') 176 | 177 | elif equipment_attributes.get('Name')=='VPE_PRESSURE_DESIGN_MIN':#selecting min design pressure 178 | if len(nltk.word_tokenize(equipment_attributes.get('Value')))==2: 179 | P2=float(nltk.word_tokenize(equipment_attributes.get('Value'))[0].replace(',', '.'))#first token of dexpi string is selected 180 | unit_P2=nltk.word_tokenize(equipment_attributes.get('Value'))[1] 181 | elif equipment_attributes.get('Value')=='': 182 | P2='' 183 | unit_P2='' 184 | else: 185 | sys.exit('Invalid entry in Equipment properties') 186 | 187 | elif equipment_attributes.get('Name')=='VPE_TEMP_DESIGN_MAX':#selecting max design temperature 188 | if len(nltk.word_tokenize(equipment_attributes.get('Value')))==2: 189 | T1=float(nltk.word_tokenize(equipment_attributes.get('Value'))[0].replace(',', '.'))#first token of dexpi string is selected 190 | unit_T1=nltk.word_tokenize(equipment_attributes.get('Value'))[1] 191 | elif equipment_attributes.get('Value')=='': 192 | T1='' 193 | unit_T1='' 194 | else: 195 | sys.exit('Invalid entry in Equipment properties') 196 | 197 | elif equipment_attributes.get('Name')=='VPE_TEMP_DESIGN_MIN':#selecting min design temperature 198 | if len(nltk.word_tokenize(equipment_attributes.get('Value')))==2: 199 | T2=float(nltk.word_tokenize(equipment_attributes.get('Value'))[0].replace(',', '.'))#first token of dexpi string is selected 200 | unit_T2=nltk.word_tokenize(equipment_attributes.get('Value'))[1] 201 | elif equipment_attributes.get('Value')=='': 202 | T2='' 203 | unit_T2='' 204 | else: 205 | sys.exit('Invalid entry in Equipment properties') 206 | 207 | elif equipment_attributes.get('Name')=='VPE_MAT_PARTS_MEDIA_CONTACT':#selecting material in contact with medium 208 | M=equipment_attributes.get('Value') 209 | 210 | elif equipment_attributes.get('Name')=='VPE_TNK_VOL_BRUTTO':#selecting volume 211 | if len(nltk.word_tokenize(equipment_attributes.get('Value')))==2: 212 | V=nltk.word_tokenize(equipment_attributes.get('Value'))[0] 213 | unit_V=nltk.word_tokenize(equipment_attributes.get('Value'))[1] 214 | elif equipment_attributes.get('Value')=='': 215 | V='' 216 | unit_V='' 217 | else: 218 | sys.exit('Invalid entry in Equipment properties') 219 | 220 | elif equipment_attributes.get('Name')=='INSULATION':#selecting insulation (Yes/No) 221 | I=equipment_attributes.get('Value') 222 | 223 | elif equipment_attributes.get('Name')=='COOLING_HEATING_SYSTEM':#selecting cooling/heating system 224 | CH=equipment_attributes.get('Value') 225 | 226 | elif equipment_attributes.get('Name')=='FN_LOCATION':#selecting location 227 | L=equipment_attributes.get('Value')#outside 228 | 229 | #opportunity to determine the unit operation 230 | for unit in ['Vessel']: 231 | O='Vessel' 232 | for unit in ['Column']: 233 | O='Column' 234 | 235 | #Add equipment as node with attributes to graph 236 | graph.add_node(ID, node_ID=ID, node_name=N, node_class=C, node_sub_class=C_sub, node_material=M, 237 | node_unit_V=unit_V, node_P_max=P1, node_unit_P1=unit_P1, node_P_min=P2, node_unit_P2=unit_P2, 238 | node_T_max=T1, node_unit_T1=unit_T1, node_T_min=T2, node_unit_T2=unit_T2, node_volume=V, 239 | node_insulation=I, node_cool_heat_system=CH, node_x=x, node_y=y, node_location=L, node_request=R, 240 | node_a=a, node_b=b, node_operation=O)#create node with attributes 241 | 242 | #add to Dataframe 243 | ID_list.loc[i,'ID']=ID 244 | ID_list.loc[i,'class']=C 245 | ID_list.loc[i,'P&ID_name']=N 246 | i+=1 247 | 248 | 249 | #MSR 250 | print('Collecting Instrumentation Function...') 251 | 252 | for PIF in myroot.findall('ProcessInstrumentationFunction'):#all MSR 253 | 254 | #predefine the optional attributes 255 | P1='Not available' 256 | unit_P1='Not available' 257 | unit_P2='Not available' 258 | P2='Not available' 259 | T1='Not available' 260 | unit_T1='Not available' 261 | unit_T2='Not available' 262 | T2='Not available' 263 | V='Not available' 264 | unit_V='Not available' 265 | I='Not available' 266 | CH='Not available' 267 | L='Not available' 268 | O='Not available' 269 | M='Not available' 270 | a='Not available' 271 | b='Not available' 272 | R='Not available' 273 | 274 | #Read 275 | ID=PIF.get('ID') 276 | N=PIF.get('TagName') 277 | 278 | #Read equipment coordinates 279 | Position = PIF.findall('Position/Location') 280 | x=float(Position[0].get('X').replace(',', '.')) 281 | y=float(Position[0].get('Y').replace(',', '.')) 282 | 283 | #Read all attributes 284 | for PIF_attributes in PIF.findall('GenericAttributes/GenericAttribute'): 285 | 286 | if PIF_attributes.get('Name')=='CLASS':#selecting the class 287 | C=PIF_attributes.get('Value') 288 | 289 | if PIF_attributes.get('Name')=='PCE_CAT_FUNC':#selecting the type of request 290 | R=PIF_attributes.get('Value') 291 | 292 | elif PIF_attributes.get('Name')=='SUB_CLASS':#selecting the sub class 293 | C_sub=PIF_attributes.get('Value') 294 | 295 | elif PIF_attributes.get('Name')=='LOCATION':#selecting location 296 | L=PIF_attributes.get('Value') 297 | 298 | #Add MSR-unit as node with attributes to graph 299 | graph.add_node(ID, node_ID=ID, node_name=N, node_class=C, node_sub_class=C_sub, node_material=M, 300 | node_unit_V=unit_V, node_P_max=P1, node_unit_P1=unit_P1, node_P_min=P2, node_unit_P2=unit_P2, 301 | node_T_max=T1, node_unit_T1=unit_T1, node_T_min=T2, node_unit_T2=unit_T2, node_volume=V, 302 | node_insulation=I, node_cool_heat_system=CH, node_x=x, node_y=y, node_location=L, node_request=R, 303 | node_a=a, node_b=b, node_operation=O)#create node with attributes 304 | 305 | #add to Dataframe 306 | ID_list.loc[i,'ID']=ID 307 | ID_list.loc[i,'class']=C 308 | ID_list.loc[i,'P&ID_name']=N 309 | i+=1 310 | 311 | 312 | #PIPING COMPONENTS 313 | 314 | j=1 315 | k=1 316 | 317 | print('Collecting Piping Components...') 318 | 319 | for piping_component in myroot.findall('PipingNetworkSystem//PipingComponent'):#select all piping components 320 | 321 | #predefine optional attributes 322 | P1='Not available' 323 | unit_P1='Not available' 324 | unit_P2='Not available' 325 | P2='Not available' 326 | T1='Not available' 327 | unit_T1='Not available' 328 | unit_T2='Not available' 329 | T2='Not available' 330 | V='Not available' 331 | unit_V='Not available' 332 | I='Not available' 333 | CH='Not available' 334 | L='Not available' 335 | O='Not available' 336 | M='Not available' 337 | a='Not available' 338 | b='Not available' 339 | R='Not available' 340 | 341 | #Read 342 | ID=piping_component.get('ID') 343 | N=piping_component.get('TagName') 344 | 345 | #Read equipment coordinates 346 | Position = piping_component.findall('Position/Location') 347 | x=float(Position[0].get('X').replace(',', '.')) 348 | y=float(Position[0].get('Y').replace(',', '.')) 349 | 350 | #in case of a pipe tee 351 | if piping_component.get('ComponentClass')=='Pipe tee': 352 | 353 | C=piping_component.get('ComponentClass') 354 | N='Pipe_tee_'+str(j)#P&ID name does not exist, so name by consecutive numbers 355 | 356 | ID_list.loc[i,'ID']=ID 357 | ID_list.loc[i,'class']=C 358 | ID_list.loc[i,'P&ID_name']=N 359 | 360 | i+=1 361 | j+=1 362 | 363 | #in case of an in/-outlet 364 | elif piping_component.get('ComponentClass')=='Arrow' or piping_component.get('ComponentClass')=='Flow in pipe connector symbol' or piping_component.get('ComponentClass')=='Flow out pipe connector symbol': 365 | 366 | C=piping_component.get('ComponentClass') 367 | N='C_'+str(k)#P&ID name does not exist, so name by consecutive numbers 368 | 369 | #Read attributes 370 | for Attribute in piping_component.findall('GenericAttributes/GenericAttribute'): 371 | if Attribute.get('Name')=='PRODUCT': 372 | a=Attribute.get('Value') 373 | if Attribute.get('Name')=='DESCRIPT': 374 | b=Attribute.get('Value') 375 | 376 | ID_list.loc[i,'ID']=ID 377 | ID_list.loc[i,'class']=C 378 | ID_list.loc[i,'P&ID_name']=N 379 | 380 | i+=1 381 | k+=1 382 | 383 | #other piping components 384 | else: 385 | 386 | #Read attributes 387 | for piping_component_attribute in piping_component.findall('GenericAttributes/GenericAttribute'): 388 | if piping_component_attribute.get('Name')=='CLASS': 389 | C=piping_component_attribute.get('Value') 390 | elif piping_component_attribute.get('Name')=='SUB_CLASS': 391 | C_sub=piping_component_attribute.get('Value') 392 | 393 | #add to Dataframe 394 | ID_list.loc[i,'ID']=ID 395 | ID_list.loc[i,'class']=C 396 | ID_list.loc[i,'P&ID_name']=N 397 | i+=1 398 | 399 | #Add piping component node with attributes to graph 400 | graph.add_node(ID, node_ID=ID, node_name=N, node_class=C, node_sub_class=C_sub, node_material=M, 401 | node_unit_V=unit_V, node_P_max=P1, node_unit_P1=unit_P1, node_P_min=P2, node_unit_P2=unit_P2, 402 | node_T_max=T1, node_unit_T1=unit_T1, node_T_min=T2, node_unit_T2=unit_T2, node_volume=V, 403 | node_insulation=I, node_cool_heat_system=CH, node_x=x, node_y=y, node_location=L, node_request=R, 404 | node_a=a, node_b=b, node_operation=O)#create node with attributes 405 | 406 | 407 | #PIPING CONNECTIONS 408 | 409 | print('Process Piping Connections...') 410 | 411 | i=1 412 | k=1 413 | nodes_from_nothing=[] 414 | nodes_to_nothing=[] 415 | nodes_not_registrated=[] 416 | 417 | for PNS in myroot.findall('PipingNetworkSystem/PipingNetworkSegment'): 418 | 419 | #identificate connection 420 | for connection in PNS.findall('Connection'): 421 | FromID=connection.get('FromID') 422 | ToID=connection.get('ToID') 423 | 424 | #Connection only added if the node already exists 425 | if FromID in list(graph.nodes()) and ToID in list(graph.nodes()): 426 | 427 | #Connection only added if there is a start and end point 428 | if FromID!="" and ToID!="" and FromID!=None and ToID!=None: 429 | 430 | #predefine optional parameter 431 | M='Not available' 432 | D='Not available' 433 | C_pipe='Not available' 434 | Nu='Not available' 435 | 436 | #Read attributes 437 | for piping_attribute in PNS.findall('GenericAttributes/GenericAttribute'): 438 | 439 | if piping_attribute.get('Name')=='CLASS':#selecting the class 440 | C=piping_attribute.get('Value') 441 | 442 | elif piping_attribute.get('Name')=='SUB_CLASS':#selecting the sub class 443 | C_sub=piping_attribute.get('Value') 444 | 445 | elif piping_attribute.get('Name')=='VPE_MAT_MAIN_MATERIAL':#selecting the material 446 | M=piping_attribute.get('Value') 447 | 448 | elif piping_attribute.get('Name')=='NOMINAL_DIAMETER':#selecting Diameter 449 | D=piping_attribute.get('Value') 450 | 451 | elif piping_attribute.get('Name')=='MAT_INAME':#selecting pipe class 452 | C_pipe=piping_attribute.get('Value') 453 | 454 | elif piping_attribute.get('Name')=='PIPENO':#selecting Pipe number 455 | Nu=piping_attribute.get('Value') 456 | 457 | #Add piping connection as edge with attributes to graph 458 | graph.add_edge(FromID, ToID, edge_class=C, edge_sub_class=C_sub, edge_material=M, 459 | edge_diameter=D, edge_pipe_class=C_pipe, edge_number=Nu)#Adding connection as edge to graph 460 | 461 | #Notice invalid connections 462 | elif FromID=="" or FromID==None: 463 | nodes_from_nothing.append(ToID) 464 | elif ToID=="" or ToID==None: 465 | nodes_to_nothing.append(FromID) 466 | 467 | #Notice node(s) that was not registrated before 468 | else: 469 | if FromID not in list(graph.nodes()) and FromID!=None and FromID!='': 470 | nodes_not_registrated.append(FromID) 471 | elif ToID not in list(graph.nodes()) and ToID!=None and ToID!='': 472 | nodes_not_registrated.append(ToID) 473 | 474 | #Add warning(s) to the error log 475 | if nodes_from_nothing!=[]: 476 | Error_log.loc[k,'Warning']='There is at least one node without a source. Please make sure it is correct.' 477 | Error_log.loc[k,'Node(s)']=str(nodes_from_nothing) 478 | k+=1 479 | if nodes_to_nothing!=[]: 480 | Error_log.loc[k,'Warning']='There is at least one node without a destination. Please make sure it is correct.' 481 | Error_log.loc[k,'Node(s)']=str(nodes_to_nothing) 482 | k+=1 483 | if nodes_not_registrated!=[]: 484 | Error_log.loc[k,'Warning']='At least one exported edge contains a node that was not registrated before.' 485 | Error_log.loc[k,'Node(s)']=str(nodes_not_registrated) 486 | k+=1 487 | 488 | 489 | #MSR CONNECTIONS 490 | print('Process Instrumentation Connections...') 491 | 492 | for InfoFlow in myroot.findall('ProcessInstrumentationFunction/InformationFlow'): 493 | 494 | for connection in InfoFlow.findall('Connection'): 495 | FromID=connection.get('FromID') 496 | ToID=connection.get('ToID') 497 | 498 | #Connection only added if there is a start and end point 499 | if FromID and ToID!=None: 500 | 501 | #predefine optional parameter 502 | M='Not available' 503 | D='Not available' 504 | C_pipe='Not available' 505 | Nu='Not available' 506 | 507 | #Read attributes 508 | for attributes in InfoFlow.findall('GenericAttributes/GenericAttribute'): 509 | 510 | if attributes.get('Name')=='CLASS': 511 | C=attributes.get('Value') 512 | 513 | elif attributes.get('Name')=='SUB_CLASS':#selecting the sub class 514 | C_sub=attributes.get('Value') 515 | 516 | #Add MSR connection as edge to graph 517 | graph.add_edge(FromID, ToID, edge_class=C, edge_sub_class=C_sub, edge_material=M, 518 | edge_diameter=D, edge_pipe_class=C_pipe, edge_number=Nu)#Adding connection as edge to graph 519 | 520 | 521 | 522 | ################################################################################################################### 523 | ### PROCESS DATA ################################################################################################ 524 | ################################################################################################################### 525 | print('Clean collected data...') 526 | 527 | #REMOVE EMPTY NODES 528 | 529 | Empty_1='No' 530 | Empty_2='No' 531 | 532 | for node in graph.nodes(): 533 | if node == "": 534 | Empty_1='Yes' 535 | if node == None: 536 | Empty_2='Yes' 537 | 538 | if Empty_1=='Yes': 539 | graph.remove_node("") 540 | if Empty_2=='Yes': 541 | graph.remove_node(None) 542 | 543 | 544 | #REMOVE ISOLATED NODES 545 | 546 | node_class=nx.get_node_attributes(graph, 'node_class') 547 | node_name=nx.get_node_attributes(graph, 'node_name') 548 | nodes_isolated={} 549 | 550 | #Identificate every isolated node which is not an Agitator or orifice plate 551 | for node in graph.nodes(): 552 | if nx.is_isolate(graph, node) and node_class[node] not in ['Agitator', 'Orifice plate']: 553 | nodes_isolated[node]=node_name[node] 554 | 555 | #Showing a warning if necessary 556 | if nodes_isolated!={}: 557 | Error_log.loc[k,'Warning']='Isolated nodes were identificated and removed. Please make sure it is correct.' 558 | Error_log.loc[k,'Node(s)']=str(nodes_isolated) 559 | k+=1 560 | 561 | graph.remove_nodes_from(nodes_isolated.keys())#remove isolated nodes from graph 562 | 563 | 564 | #CONVERT NODES LIKE PIPING EQUIPMENT AND HOSE IN TO EDGES 565 | 566 | node_class=nx.get_node_attributes(graph, 'node_class') 567 | node_sub_class=nx.get_node_attributes(graph, 'node_sub_class') 568 | edge_class=nx.get_edge_attributes(graph, 'edge_class') 569 | remove=[] 570 | nodes_problem=[] 571 | pipe_attributes={'Piping with conduit':{'Insulation':'No', 'Heated/cooled':'No'}, 572 | 'Piping insulated':{'Insulation':'Yes', 'Heated/cooled':'No'}, 573 | 'Piping heated or cooled':{'Insulation':'No', 'Heated/cooled':'Yes'}, 574 | 'Piping, heating or cooled insulated':{'Insulation':'Yes', 'Heated/cooled':'Yes'}}#preparation for creating new edge with new attributes (dict to avoid repeating script) 575 | 576 | #Add following new attributes to the already existing nodes 577 | for edge in graph.edges(): 578 | if edge_class[edge]=='Piping': 579 | graph.add_edge(edge[0], edge[1], edge_insulation='No', edge_heated_cooled='No') 580 | else: 581 | graph.add_edge(edge[0], edge[1], edge_insulation='Not available', edge_heated_cooled='Not available') 582 | 583 | # Select all relevant nodes 584 | for node in graph.nodes(): 585 | if node_class[node] in ['Hose', 'Pipe equipment']: 586 | 587 | #checking the right connection format, save node and its neighbors 588 | if len(list((graph.in_edges(node))))==1 and len(list((graph.out_edges(node))))==1: 589 | remove.append(node) 590 | all_neighbors=list((nx.all_neighbors(graph, node))) 591 | FromID=all_neighbors[0] 592 | ToID=all_neighbors[1] 593 | 594 | #In case of a pipe equipment the attributes of the edge in front can be taken over 595 | if node_class[node]=='Pipe equipment': 596 | C=nx.get_edge_attributes(graph, 'edge_class')[(FromID, node)] 597 | C_sub=nx.get_edge_attributes(graph, 'edge_sub_class')[(FromID, node)] 598 | M=nx.get_edge_attributes(graph, 'edge_material')[(FromID, node)] 599 | D=nx.get_edge_attributes(graph, 'edge_diameter')[(FromID, node)] 600 | C_pipe=nx.get_edge_attributes(graph, 'edge_pipe_class')[(FromID, node)] 601 | Nu=nx.get_edge_attributes(graph, 'edge_number')[(FromID, node)] 602 | 603 | #the remaining additional attributes depend on the sub class of pipe equipment (saved in the dict above) 604 | for sub_class in pipe_attributes: 605 | if node_sub_class[node]==sub_class: 606 | graph.add_edge(FromID, ToID, edge_class=C, edge_sub_class=C_sub, edge_material=M, 607 | edge_diameter=D, edge_pipe_class=C_pipe, edge_number=Nu, 608 | edge_insulation=pipe_attributes[sub_class]['Insulation'], 609 | edge_heated_cooled=pipe_attributes[sub_class]['Heated/cooled']) 610 | 611 | #in case of a hose, attributes can not be taken from pipe in front cause of having own attriubtes 612 | elif node_class[node]=='Hose': 613 | #predefine optional parameter 614 | M='Not available' 615 | D='Not available' 616 | C_pipe='Not available' 617 | Nu='Not available' 618 | 619 | #Read hose attributes 620 | C=node_class[node] 621 | C_sub=node_sub_class[node] 622 | 623 | graph.add_edge(FromID, ToID, edge_class=C, edge_sub_class=C_sub, edge_material=M, 624 | edge_diameter=D, edge_pipe_class=C_pipe, edge_number=Nu, 625 | edge_insulation='No', 626 | edge_heated_cooled='No') 627 | 628 | #if the node is not connected in the expected way, a warning is shown 629 | else: 630 | nodes_problem.append(node) 631 | 632 | if nodes_problem!=[]: 633 | Error_log.loc[k,'Warning']='Problems in converting hose and piping components. At least one node is connected in an unexpected way.' 634 | Error_log.loc[k,'Node(s)']=nodes_problem 635 | k+=1 636 | 637 | graph.remove_nodes_from(remove)#remove old nodes for pipe equipment and nodes from graph 638 | 639 | 640 | #EQUIPMENT GROUPS 641 | print('Group collected Equipment and Attributes...') 642 | 643 | node_class=nx.get_node_attributes(graph, 'node_class') 644 | node_sub_class=nx.get_node_attributes(graph, 'node_sub_class') 645 | dict_group={'Vessel':['Vessel', 'Vessel with two Diameters', 'Spherical vessel', 'Vessel with dome', 'Vessel, general', 'Silo', 'Gas cylinder', 'Basin', 'Barrel', 'Tank', 'Vessel with agigator', 'Bag', 'Container'], 646 | 'Column':['Column'], 647 | 'Shaping machines':['Vertical shaping machine', 'Horizontal shaping machine'], 648 | 'Crushing/Grinding':['Crushing maschine', 'Mill'], 649 | 'Dryer':['Dryer'], 650 | 'Centrifuge':['Centrifuge'], 651 | 'Separator':['Separator'], 652 | 'Sieving':['Basket band and screening machine', 'Sieving machine'], 653 | 'Mixer/Kneader':['Kneader', 'Mixing pipe', 'Rotating mixer', 'Static mixer'], 654 | 'Valves/Fittings':['Steam trap', 'Flange', 'Orifice plate', 'Flap trap (form 2)', 'Angle check valve','Angle globe valve', 'Angle valve, general', 'Ball valve', 'Breather valve', 'Breather flap', 'butterfly valve', 'Butterfly valve', 'Check valve', 'Check valve angled', 'Check valve angled globe type', 'Check valve globe type', 'Diaphragm valve', 'Float valve', 'Gate valve', 'Globe valve', 'Plug cock', 'Plug valve', 'Pressure reducing valve', 'Safety valve', 'Safety angle valve', 'Safety valve, angled type', 'Swing check valve', 'Three-way ball valve', 'Three-way globe valve', 'Three-way valve, general', 'Valve (general)', 'Valve, angle ball type', 'Valve, angle globe type', 'Valve, angle type (general)', 'Valve, ball type', 'Valve, butterfly type (Form 1)', 'Valve, butterfly type (Form 2)', 'Valve, gate type', 'Valve, general', 'Valve, globe type', 'Valve, needle type', 'Valve, three-way ball type', 'Valve, three-way globe type', 'Valve, three-way type (general)', 'Airtight butterfly valve', 'Flame arrestor', 'Fire protection flap', 'Rupture disk'], 655 | 'Filter':['Filter', 'Band Filter', ' Ion exchange filter', 'Air filter', 'Biological filter', 'Filter press', 'Fluid filter', 'Gas filter', 'Liquid rotary filter'], 656 | 'Pump':['Fluid pump', 'Liquid pump', 'Liquid jet pump'], 657 | 'Compressor':['Compressor', 'Ejector compressor', 'Vacuum pump', 'Vakuum pump', 'Jet vacuum pump', 'Jet vakuum pump'], 658 | 'Heat exchanger':['Heat exchanger', 'Heat exchanger ', 'Heat exchanger, detailed', 'Spiral type heat exchanger','Heat exchanger', 'detailed, Tube bundle with U-tubes', 'Electric Heaters', 'Facility for heating or cooling'], 659 | 'Pipe tee':['Pipe tee'], 660 | 'Connector':['Arrow', 'Flow in pipe connector symbol', 'Flow out pipe connector symbol'], 661 | 'MSR':['PCE Request']}#groups 662 | dict_sub_group={'Vessel':{'Vessel (solid)':{'Silo':'General', 'Bag':'General', 'Container':'Container for solids'}, 663 | 'Vessel (gaseous)':{'Gas cylinder':'General'}, 664 | 'Vessel (liquid)':'All other'}, 665 | 'Valves/Fittings':{'Valve (safety)':{'Safety valve':['spring loaded', 'General'], 'Safety angle valve':['Spring loaded', 'General'], 'Safety valve, angled type':['General', 'spring loaded'], 'Flame arrestor':'all', 'Rupture disk':'all'}, 666 | 'Valve (operation)':'All other'}, 667 | 'Filter':{'Filter (gaseous)':{'Air filter':'General', 'Gas filter':'all'}, 668 | 'Filter (liquid)':'All other'}, 669 | 'Heat exchanger':{'Heat exchanger (electric)':{'Electric Heaters':'General', 'Facility for heating or cooling':'General'}, 670 | 'Heat exchanger (medium)':'All other'}}#sub groups 671 | 672 | #create following attributes 673 | for node in graph.nodes(): 674 | graph.add_node(node, node_group='Not available') 675 | graph.add_node(node, node_sub_group='Not available') 676 | 677 | 678 | #sorting into groups by the dict (node class has to match with one of the listed classes under a specific group name) 679 | for name_group in dict_group: 680 | for node in graph.nodes(): 681 | if node_class[node] in dict_group[name_group]: 682 | graph.add_node(node, node_group=name_group)#overwrite attribute 683 | 684 | 685 | node_group=nx.get_node_attributes(graph, 'node_group') 686 | no_sub_group=[]#List to collect the nodes which are not sorted after sorting 687 | 688 | #sorting into sub groups by the dict 689 | for node in graph.nodes(): 690 | grouped='No' 691 | for group in dict_sub_group:#regarding all groups 692 | if node_group[node]==group:#select nodes belongs to the regarded group 693 | for sub_group in dict_sub_group[group]:#regarding all sub groups 694 | 695 | if dict_sub_group[group][sub_group]=='All other':#everything else 696 | graph.add_node(node, node_sub_group=sub_group) 697 | grouped='Yes' 698 | break 699 | 700 | else: 701 | for Class in dict_sub_group[group][sub_group]: 702 | if Class==node_class[node]: 703 | sub_classes=dict_sub_group[group][sub_group][Class] 704 | 705 | #Case of only one sub class (entry as string) or the signal word "all" 706 | if type(sub_classes)==str: 707 | if sub_classes==node_sub_class[node] or sub_classes=='all': 708 | graph.add_node(node, node_sub_group=sub_group) 709 | grouped='Yes' 710 | break 711 | 712 | #Case of more than one sub classes (entry as list) 713 | elif type(sub_classes)==list: 714 | for Subclass in sub_classes: 715 | if Subclass==node_sub_class[node]: 716 | graph.add_node(node, node_sub_group=sub_group) 717 | grouped='Yes' 718 | break 719 | 720 | #To break out of all loops except the last to start the next sorting 721 | if grouped=='Yes': 722 | break 723 | if grouped=='Yes': 724 | break 725 | if grouped=='Yes': 726 | break 727 | 728 | #If no sub group was found for the node (Requirement is that a potential sub group must exist) 729 | if grouped=='No' and node_group[node] in dict_sub_group.keys(): 730 | no_sub_group.append(node) 731 | 732 | node_sub_group=nx.get_node_attributes(graph, 'node_sub_group') 733 | 734 | 735 | #ASSIGN AGITATORS TO VESSELS 736 | print('Assign agitators to connected equipment...') 737 | 738 | node_class=nx.get_node_attributes(graph, 'node_class') 739 | node_x=nx.get_node_attributes(graph, 'node_x') 740 | node_y=nx.get_node_attributes(graph, 'node_y') 741 | Position={} 742 | Agitators=[] 743 | 744 | #Create new attribute for every node 745 | for node in graph.nodes(): 746 | if node_sub_group[node]=='Vessel (liquid)': 747 | graph.add_node(node, node_agitator='No') 748 | else: 749 | graph.add_node(node, node_agitator='not available') 750 | 751 | #Select agitators and vessels 752 | for node in graph.nodes(): 753 | if node_class[node]=='Agitator': 754 | Agitators.append(node) 755 | for other_node in graph.nodes(): 756 | if node_group[other_node]=='Vessel': 757 | 758 | #Calculate distance by using coordinates 759 | x_agitator=node_x[node] 760 | x_other_node=node_x[other_node] 761 | y_agitator=node_y[node] 762 | y_other_node=node_y[other_node] 763 | Position_difference=abs(x_agitator-x_other_node)+abs(y_agitator-y_other_node)#Calculate Position difference 764 | Position[Position_difference]=other_node#save distance together with node 765 | 766 | #identificate nearest node to agitator and notice it in the attribute 767 | node_assign=Position[min(Position.keys())] 768 | graph.add_node(node_assign, node_agitator='Yes') 769 | 770 | graph.remove_nodes_from(Agitators) 771 | 772 | 773 | #EXCHANGE FLANGE WITH THE ORIFICE PLATE 774 | 775 | node_class=nx.get_node_attributes(graph, 'node_class') 776 | node_name=nx.get_node_attributes(graph, 'node_name') 777 | node_x=nx.get_node_attributes(graph, 'node_x') 778 | node_y=nx.get_node_attributes(graph, 'node_y') 779 | Position={} 780 | Orifice_plates=[] 781 | 782 | #Select orifice plates and flanges 783 | for node in graph.nodes(): 784 | if node_class[node]=='Orifice plate': 785 | Orifice_plates.append(node) 786 | for other_node in graph.nodes(): 787 | if node_class[other_node]=='Flange': 788 | 789 | #calculate distance by using coordinates 790 | x_flange=float(node_x[node]) 791 | x_other_node=float(node_x[other_node]) 792 | y_fange=float(node_y[node]) 793 | y_other_node=float(node_y[other_node]) 794 | Position_difference=abs(x_flange-x_other_node)+abs(y_fange-y_other_node) 795 | Position[Position_difference]=other_node#save distance togehter with node 796 | 797 | #identificate node with the smallest distance 798 | #reset the attributes 799 | #set the attributes of the orifice plate (Recent ID keeps the same until relabeling) 800 | node_assign=Position[min(Position.keys())] 801 | graph.add_node(node_assign, node_ID='', node_name='', node_class='', node_sub_class='', node_x='', node_y='', node_group='', node_sub_group='') 802 | graph.add_node(node_assign, node_ID=node, node_name=node_name[node], node_class=node_class[node], node_sub_class=node_sub_class[node], node_x=node_x[node], node_y=node_y[node], node_group=node_group[node], node_sub_group=node_sub_group[node]) 803 | 804 | graph.remove_nodes_from(Orifice_plates) 805 | 806 | 807 | ############################################################################################################ 808 | ############################### COMPLETED DEXPI DATA EXTRACTION ############################################### 809 | ############################################################################################################ 810 | print('Extract complete DEXPI data...') 811 | 812 | #AVOID NO-LABEL NODES 813 | print('--> avoid no-label nodes') 814 | 815 | node_name=nx.get_node_attributes(graph, 'node_name') 816 | no_label_nodes=[] 817 | double_names=[] 818 | 819 | #Identificate no-label nodes and give them the ID as name 820 | for node in graph.nodes(): 821 | if node_name[node]=='': 822 | node_name[node]=node 823 | no_label_nodes.append(node) 824 | 825 | #Warning 826 | if no_label_nodes!=[]: 827 | Error_log.loc[k,'Warning']='Warning: At least one node is not labeled. Label of such a node was exchanged with ID of DEXPI-Export. To avoid it, please add a label for the node in the P&ID.' 828 | Error_log.loc[k,'Node(s)']=str(no_label_nodes) 829 | k+=1 830 | 831 | 832 | #AVOID SAME NAME 833 | print('--> avoid same name') 834 | 835 | i=0 836 | 837 | #Identificate more time names 838 | for node_1 in graph.nodes(): 839 | i+=1 840 | j=0 841 | for node_2 in graph.nodes(): 842 | j+=1 843 | if node_name[node_1]==node_name[node_2] and i!=j and node_name[node_1] not in double_names:#avoid similarity because of same node and noticing a name for more than one time in the list 844 | double_names.append(node_1) 845 | 846 | #Identificate nodes with the more time names and give them consecutive numbers 847 | for name in double_names: 848 | i=1 849 | for node in graph.nodes(): 850 | if node_name[node]==name: 851 | node_name[node]=name+' ('+str(i)+')' 852 | i+=1 853 | 854 | #Warning 855 | if double_names!=[]: 856 | Error_log.loc[k,'Warning']='At least one label is used more than one time. That is not possible cause a clear assignment must be given. For this run the nodes are numbered. For next run, please use clear label.' 857 | Error_log.loc[k,'Node(s)']=str(double_names) 858 | k+=1 859 | 860 | 861 | #RELABELING 862 | print('Relabeling...') 863 | 864 | graph=nx.relabel_nodes(graph, node_name) 865 | 866 | node_name=nx.get_node_attributes(graph, 'node_name') 867 | node_class=nx.get_node_attributes(graph, 'node_class') 868 | node_sub_class=nx.get_node_attributes(graph, 'node_sub_class') 869 | node_group=nx.get_node_attributes(graph, 'node_group') 870 | node_sub_group=nx.get_node_attributes(graph, 'node_sub_group') 871 | node_ID=nx.get_node_attributes(graph, 'node_ID') 872 | 873 | 874 | 875 | #INVALID DRAWING (Example: Only inlets or outlets in valve) 876 | 877 | neighbors=[] 878 | all_neighbors=[] 879 | nodes_wrong_connection=[] 880 | 881 | #Notice in/out edges for every relevant nodes (except Vessel and MSR) 882 | for node in graph.nodes(): 883 | if node_group[node]!='Vessel' and 'PIF' not in node_ID[node]: 884 | all_neighbors=list(nx.all_neighbors(graph, node)) 885 | neighbors=list(nx.neighbors(graph, node)) 886 | 887 | #every node with at least two edges and having only inlets or only outlets means invalid drawing and is noticed 888 | if len(all_neighbors)>1:#there must be more than one edge 889 | if len(neighbors)==0 or len(all_neighbors)-len(neighbors)==0: 890 | nodes_wrong_connection.append(node) 891 | 892 | #Warning 893 | if nodes_wrong_connection!=[]: 894 | Error_log.loc[k, 'Warning']='At least one node is wrong connected in the P&ID. Please check P&ID for the next run' 895 | Error_log.loc[k, 'Node(s)']=nodes_wrong_connection 896 | k+=1 897 | 898 | ################################################################################################################### 899 | ### CREATE A GRAPH WITHOUT MSR AND SAFETY EQUIPMENT ############################################################### 900 | ################################################################################################################### 901 | 902 | graph_no_MSR=graph.copy()#copy graph 903 | print('create a 2nd graph without PCE and safety equipment...') 904 | 905 | #REMOVE SIGNAL LINES 906 | 907 | edge_class_MSR=nx.get_edge_attributes(graph_no_MSR, 'edge_class') 908 | 909 | edges=[] 910 | for edge in graph_no_MSR.edges(): 911 | if edge_class_MSR[edge]=='Signal line': 912 | edges.append(edge) 913 | 914 | graph_no_MSR.remove_edges_from(edges) 915 | 916 | 917 | #REMOVE MSR AND CONNECTED VALVES 918 | 919 | list_MSR=[] 920 | list_MSR_valve=[] 921 | node_ID_MSR=nx.get_node_attributes(graph_no_MSR, 'node_ID') 922 | node_group_MSR=nx.get_node_attributes(graph_no_MSR, 'node_group') 923 | 924 | for node in graph_no_MSR.nodes(): 925 | if 'PIF' in node_ID_MSR[node]: 926 | list_MSR.append(node) 927 | all_neighbors=list(nx.all_neighbors(graph_no_MSR, node)) 928 | if len(all_neighbors)==1: 929 | node_neighbor=all_neighbors[0] 930 | all_neighbors=list(nx.all_neighbors(graph_no_MSR, node_neighbor)) 931 | if len(all_neighbors)==2 and node_group_MSR[node_neighbor]=='Valves/Fittings': 932 | list_MSR_valve.append(node_neighbor) 933 | 934 | graph_no_MSR.remove_nodes_from(list_MSR) 935 | graph_no_MSR.remove_nodes_from(list_MSR_valve) 936 | 937 | ################################################################################################################### 938 | excel_ID_path=path_IDlist+savename+'.xlsx' 939 | ID_list.to_excel(excel_ID_path) #save ID list 940 | Error_log.to_excel(path_errorLog+savename+'_ErrorLog.xlsx') #save Error_log 941 | nx.write_graphml(graph, save_path_graph+savename+'.xml', encoding='utf-8', prettyprint=True, infer_numeric_types=False)#save the plot 942 | nx.write_graphml(graph_no_MSR, save_path_graph_no_MSR+savename+'_noMSR.xml', encoding='utf-8', prettyprint=True, infer_numeric_types=False) 943 | 944 | print('Conversion and storing of the DEXPI file ', savename, 'completed!') 945 | 946 | return [graph, graph_no_MSR, k] 947 | 948 | --------------------------------------------------------------------------------