├── 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 |
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 |
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 |
--------------------------------------------------------------------------------