floodmodeller-api 0.4.2.post1__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- floodmodeller_api/__init__.py +8 -9
- floodmodeller_api/_base.py +184 -176
- floodmodeller_api/backup.py +273 -273
- floodmodeller_api/dat.py +909 -831
- floodmodeller_api/diff.py +136 -119
- floodmodeller_api/ied.py +307 -306
- floodmodeller_api/ief.py +647 -637
- floodmodeller_api/ief_flags.py +253 -253
- floodmodeller_api/inp.py +266 -266
- floodmodeller_api/libs/libifcoremd.dll +0 -0
- floodmodeller_api/libs/libifcoremt.so.5 +0 -0
- floodmodeller_api/libs/libifport.so.5 +0 -0
- floodmodeller_api/{libmmd.dll → libs/libimf.so} +0 -0
- floodmodeller_api/libs/libintlc.so.5 +0 -0
- floodmodeller_api/libs/libmmd.dll +0 -0
- floodmodeller_api/libs/libsvml.so +0 -0
- floodmodeller_api/libs/libzzn_read.so +0 -0
- floodmodeller_api/libs/zzn_read.dll +0 -0
- floodmodeller_api/logs/__init__.py +2 -2
- floodmodeller_api/logs/lf.py +320 -312
- floodmodeller_api/logs/lf_helpers.py +354 -352
- floodmodeller_api/logs/lf_params.py +643 -529
- floodmodeller_api/mapping.py +84 -0
- floodmodeller_api/test/__init__.py +4 -4
- floodmodeller_api/test/conftest.py +9 -8
- floodmodeller_api/test/test_backup.py +117 -117
- floodmodeller_api/test/test_dat.py +221 -92
- floodmodeller_api/test/test_data/All Units 4_6.DAT +1081 -1081
- floodmodeller_api/test/test_data/All Units 4_6.feb +1081 -1081
- floodmodeller_api/test/test_data/BRIDGE.DAT +926 -926
- floodmodeller_api/test/test_data/Culvert_Inlet_Outlet.dat +36 -36
- floodmodeller_api/test/test_data/Culvert_Inlet_Outlet.feb +36 -36
- floodmodeller_api/test/test_data/DamBreakADI.xml +52 -52
- floodmodeller_api/test/test_data/DamBreakFAST.xml +58 -58
- floodmodeller_api/test/test_data/DamBreakFAST_dy.xml +53 -53
- floodmodeller_api/test/test_data/DamBreakTVD.xml +55 -55
- floodmodeller_api/test/test_data/DefenceBreach.xml +53 -53
- floodmodeller_api/test/test_data/DefenceBreachFAST.xml +60 -60
- floodmodeller_api/test/test_data/DefenceBreachFAST_dy.xml +55 -55
- floodmodeller_api/test/test_data/Domain1+2_QH.xml +76 -76
- floodmodeller_api/test/test_data/Domain1_H.xml +41 -41
- floodmodeller_api/test/test_data/Domain1_Q.xml +41 -41
- floodmodeller_api/test/test_data/Domain1_Q_FAST.xml +48 -48
- floodmodeller_api/test/test_data/Domain1_Q_FAST_dy.xml +48 -48
- floodmodeller_api/test/test_data/Domain1_Q_xml_expected.json +263 -0
- floodmodeller_api/test/test_data/Domain1_W.xml +41 -41
- floodmodeller_api/test/test_data/EX1.DAT +321 -321
- floodmodeller_api/test/test_data/EX1.ext +107 -107
- floodmodeller_api/test/test_data/EX1.feb +320 -320
- floodmodeller_api/test/test_data/EX1.gxy +107 -107
- floodmodeller_api/test/test_data/EX17.DAT +421 -422
- floodmodeller_api/test/test_data/EX17.ext +213 -213
- floodmodeller_api/test/test_data/EX17.feb +422 -422
- floodmodeller_api/test/test_data/EX18.DAT +375 -375
- floodmodeller_api/test/test_data/EX18_DAT_expected.json +3876 -0
- floodmodeller_api/test/test_data/EX2.DAT +302 -302
- floodmodeller_api/test/test_data/EX3.DAT +926 -926
- floodmodeller_api/test/test_data/EX3_DAT_expected.json +16235 -0
- floodmodeller_api/test/test_data/EX3_IEF_expected.json +61 -0
- floodmodeller_api/test/test_data/EX6.DAT +2084 -2084
- floodmodeller_api/test/test_data/EX6.ext +532 -532
- floodmodeller_api/test/test_data/EX6.feb +2084 -2084
- floodmodeller_api/test/test_data/EX6_DAT_expected.json +31647 -0
- floodmodeller_api/test/test_data/Event Data Example.DAT +336 -336
- floodmodeller_api/test/test_data/Event Data Example.ext +107 -107
- floodmodeller_api/test/test_data/Event Data Example.feb +336 -336
- floodmodeller_api/test/test_data/Linked1D2D.xml +52 -52
- floodmodeller_api/test/test_data/Linked1D2DFAST.xml +53 -53
- floodmodeller_api/test/test_data/Linked1D2DFAST_dy.xml +48 -48
- floodmodeller_api/test/test_data/Linked1D2D_xml_expected.json +313 -0
- floodmodeller_api/test/test_data/blockage.dat +50 -50
- floodmodeller_api/test/test_data/blockage.ext +45 -45
- floodmodeller_api/test/test_data/blockage.feb +9 -9
- floodmodeller_api/test/test_data/blockage.gxy +71 -71
- floodmodeller_api/test/test_data/defaultUnits.dat +127 -127
- floodmodeller_api/test/test_data/defaultUnits.ext +45 -45
- floodmodeller_api/test/test_data/defaultUnits.feb +9 -9
- floodmodeller_api/test/test_data/defaultUnits.fmpx +58 -58
- floodmodeller_api/test/test_data/defaultUnits.gxy +85 -85
- floodmodeller_api/test/test_data/ex3.ief +20 -20
- floodmodeller_api/test/test_data/ex3.lf1 +2800 -2800
- floodmodeller_api/test/test_data/ex4.DAT +1374 -1374
- floodmodeller_api/test/test_data/ex4_changed.DAT +1374 -1374
- floodmodeller_api/test/test_data/example1.inp +329 -329
- floodmodeller_api/test/test_data/example2.inp +158 -158
- floodmodeller_api/test/test_data/example3.inp +297 -297
- floodmodeller_api/test/test_data/example4.inp +388 -388
- floodmodeller_api/test/test_data/example5.inp +147 -147
- floodmodeller_api/test/test_data/example6.inp +154 -154
- floodmodeller_api/test/test_data/jump.dat +176 -176
- floodmodeller_api/test/test_data/network.dat +1374 -1374
- floodmodeller_api/test/test_data/network.ext +45 -45
- floodmodeller_api/test/test_data/network.exy +1 -1
- floodmodeller_api/test/test_data/network.feb +45 -45
- floodmodeller_api/test/test_data/network.ied +45 -45
- floodmodeller_api/test/test_data/network.ief +20 -20
- floodmodeller_api/test/test_data/network.inp +147 -147
- floodmodeller_api/test/test_data/network.pxy +57 -57
- floodmodeller_api/test/test_data/network.zzd +122 -122
- floodmodeller_api/test/test_data/network_dat_expected.json +21837 -0
- floodmodeller_api/test/test_data/network_from_tabularCSV.csv +87 -87
- floodmodeller_api/test/test_data/network_ied_expected.json +287 -0
- floodmodeller_api/test/test_data/rnweir.dat +9 -9
- floodmodeller_api/test/test_data/rnweir.ext +45 -45
- floodmodeller_api/test/test_data/rnweir.feb +9 -9
- floodmodeller_api/test/test_data/rnweir.gxy +45 -45
- floodmodeller_api/test/test_data/rnweir_default.dat +74 -74
- floodmodeller_api/test/test_data/rnweir_default.ext +45 -45
- floodmodeller_api/test/test_data/rnweir_default.feb +9 -9
- floodmodeller_api/test/test_data/rnweir_default.fmpx +58 -58
- floodmodeller_api/test/test_data/rnweir_default.gxy +53 -53
- floodmodeller_api/test/test_data/unit checks.dat +16 -16
- floodmodeller_api/test/test_ied.py +29 -29
- floodmodeller_api/test/test_ief.py +125 -24
- floodmodeller_api/test/test_inp.py +47 -48
- floodmodeller_api/test/test_json.py +114 -0
- floodmodeller_api/test/test_logs_lf.py +48 -51
- floodmodeller_api/test/test_tool.py +165 -152
- floodmodeller_api/test/test_toolbox_structure_log.py +234 -239
- floodmodeller_api/test/test_xml2d.py +151 -156
- floodmodeller_api/test/test_zzn.py +36 -34
- floodmodeller_api/to_from_json.py +218 -0
- floodmodeller_api/tool.py +332 -329
- floodmodeller_api/toolbox/__init__.py +5 -5
- floodmodeller_api/toolbox/example_tool.py +45 -45
- floodmodeller_api/toolbox/model_build/__init__.py +2 -2
- floodmodeller_api/toolbox/model_build/add_siltation_definition.py +100 -98
- floodmodeller_api/toolbox/model_build/structure_log/__init__.py +1 -1
- floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +287 -289
- floodmodeller_api/toolbox/model_build/structure_log_definition.py +76 -76
- floodmodeller_api/units/__init__.py +10 -10
- floodmodeller_api/units/_base.py +214 -212
- floodmodeller_api/units/boundaries.py +467 -467
- floodmodeller_api/units/comment.py +52 -55
- floodmodeller_api/units/conduits.py +382 -402
- floodmodeller_api/units/helpers.py +123 -131
- floodmodeller_api/units/iic.py +107 -101
- floodmodeller_api/units/losses.py +305 -306
- floodmodeller_api/units/sections.py +444 -446
- floodmodeller_api/units/structures.py +1690 -1683
- floodmodeller_api/units/units.py +93 -104
- floodmodeller_api/units/unsupported.py +44 -44
- floodmodeller_api/units/variables.py +87 -89
- floodmodeller_api/urban1d/__init__.py +11 -11
- floodmodeller_api/urban1d/_base.py +188 -179
- floodmodeller_api/urban1d/conduits.py +93 -85
- floodmodeller_api/urban1d/general_parameters.py +58 -58
- floodmodeller_api/urban1d/junctions.py +81 -79
- floodmodeller_api/urban1d/losses.py +81 -74
- floodmodeller_api/urban1d/outfalls.py +114 -110
- floodmodeller_api/urban1d/raingauges.py +111 -111
- floodmodeller_api/urban1d/subsections.py +92 -98
- floodmodeller_api/urban1d/xsections.py +147 -144
- floodmodeller_api/util.py +77 -21
- floodmodeller_api/validation/parameters.py +660 -660
- floodmodeller_api/validation/urban_parameters.py +388 -404
- floodmodeller_api/validation/validation.py +110 -108
- floodmodeller_api/version.py +1 -1
- floodmodeller_api/xml2d.py +688 -673
- floodmodeller_api/xml2d_template.py +37 -37
- floodmodeller_api/zzn.py +387 -363
- {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/LICENSE.txt +13 -13
- {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/METADATA +82 -82
- floodmodeller_api-0.4.3.dist-info/RECORD +179 -0
- {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/WHEEL +1 -1
- floodmodeller_api/libifcoremd.dll +0 -0
- floodmodeller_api/test/test_data/EX3.bmp +0 -0
- floodmodeller_api/test/test_data/test_output.csv +0 -87
- floodmodeller_api/zzn_read.dll +0 -0
- floodmodeller_api-0.4.2.post1.dist-info/RECORD +0 -164
- {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/entry_points.txt +0 -0
- {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/top_level.txt +0 -0
floodmodeller_api/xml2d.py
CHANGED
|
@@ -1,673 +1,688 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Flood Modeller Python API
|
|
3
|
-
Copyright (C)
|
|
4
|
-
|
|
5
|
-
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
|
|
6
|
-
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
7
|
-
|
|
8
|
-
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
9
|
-
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
10
|
-
|
|
11
|
-
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
|
|
12
|
-
|
|
13
|
-
If you have any query about this program or this License, please contact us at support@floodmodeller.com or write to the following
|
|
14
|
-
address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
from
|
|
24
|
-
from
|
|
25
|
-
|
|
26
|
-
from
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
from
|
|
30
|
-
|
|
31
|
-
from .
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return value
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
self.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
self.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
self.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
self.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
"""
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
#
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1
|
+
"""
|
|
2
|
+
Flood Modeller Python API
|
|
3
|
+
Copyright (C) 2024 Jacobs U.K. Limited
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
|
|
6
|
+
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
7
|
+
|
|
8
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
9
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
10
|
+
|
|
11
|
+
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
|
|
12
|
+
|
|
13
|
+
If you have any query about this program or this License, please contact us at support@floodmodeller.com or write to the following
|
|
14
|
+
address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import datetime as dt
|
|
20
|
+
import io
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
from copy import deepcopy
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from subprocess import DEVNULL, Popen
|
|
26
|
+
from typing import Callable
|
|
27
|
+
|
|
28
|
+
from lxml import etree
|
|
29
|
+
from tqdm import trange
|
|
30
|
+
|
|
31
|
+
from floodmodeller_api._base import FMFile
|
|
32
|
+
|
|
33
|
+
from .logs import error_2d_dict, lf_factory
|
|
34
|
+
from .xml2d_template import xml2d_template
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def value_from_string(value: str | list[str]):
|
|
38
|
+
try:
|
|
39
|
+
if isinstance(value, list):
|
|
40
|
+
return value
|
|
41
|
+
return float(value) if "." in value else int(value)
|
|
42
|
+
except ValueError:
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def categorical_sort(itm, order, ns):
|
|
47
|
+
try:
|
|
48
|
+
return order[itm.tag.replace(ns, "")]
|
|
49
|
+
except Exception:
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class XML2D(FMFile):
|
|
54
|
+
"""Reads and write Flood Modeller 2D XML format '.xml'
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
xml_filepath (str, optional): Full filepath to xml file.
|
|
58
|
+
|
|
59
|
+
Output:
|
|
60
|
+
Initiates 'XML' class object
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
TypeError: Raised if xml_filepath does not point to a .xml file
|
|
64
|
+
FileNotFoundError: Raised if xml_filepath points to a file which does not exist
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
_filetype: str = "XML2D"
|
|
68
|
+
_suffix: str = ".xml"
|
|
69
|
+
_xsd_loc: str = "http://schema.floodmodeller.com/6.2/2d.xsd"
|
|
70
|
+
OLD_FILE = 5
|
|
71
|
+
GOOD_EXIT_CODE = 100
|
|
72
|
+
|
|
73
|
+
def __init__(self, xml_filepath: str | Path | None = None, from_json: bool = False):
|
|
74
|
+
try:
|
|
75
|
+
if from_json:
|
|
76
|
+
return
|
|
77
|
+
if xml_filepath is not None:
|
|
78
|
+
FMFile.__init__(self, xml_filepath)
|
|
79
|
+
self._read()
|
|
80
|
+
self._log_path = self._filepath.with_suffix(".lf2")
|
|
81
|
+
else:
|
|
82
|
+
self._read(from_blank=True)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self._handle_exception(e, when="read")
|
|
86
|
+
|
|
87
|
+
def _read(self, from_blank=False):
|
|
88
|
+
# Read xml data
|
|
89
|
+
self._ns = "{https://www.floodmodeller.com}"
|
|
90
|
+
if from_blank:
|
|
91
|
+
self._xmltree = etree.parse(io.StringIO(xml2d_template))
|
|
92
|
+
else:
|
|
93
|
+
self._xmltree = etree.parse(self._filepath)
|
|
94
|
+
self._xsd = etree.parse(self._xsd_loc)
|
|
95
|
+
self._xsdschema = etree.XMLSchema(self._xsd)
|
|
96
|
+
self._get_multi_value_keys()
|
|
97
|
+
|
|
98
|
+
self._create_dict()
|
|
99
|
+
for key, data in self._data.items():
|
|
100
|
+
if key == "domain":
|
|
101
|
+
self.domains = {domain["domain_id"]: domain for domain in data}
|
|
102
|
+
else:
|
|
103
|
+
setattr(self, key, data)
|
|
104
|
+
for attr in [
|
|
105
|
+
"name",
|
|
106
|
+
"link1d",
|
|
107
|
+
"logfile",
|
|
108
|
+
"domains",
|
|
109
|
+
"restart_options",
|
|
110
|
+
"advanced_options",
|
|
111
|
+
"processor",
|
|
112
|
+
"unit_system",
|
|
113
|
+
"description",
|
|
114
|
+
]:
|
|
115
|
+
if attr not in self.__dict__:
|
|
116
|
+
setattr(self, attr, None)
|
|
117
|
+
|
|
118
|
+
def _create_dict(self):
|
|
119
|
+
"""Iterate through XML Tree to add all elements as class attributes"""
|
|
120
|
+
xml_dict = {}
|
|
121
|
+
root = self._xmltree.getroot()
|
|
122
|
+
|
|
123
|
+
xml_dict.update({"name": root.attrib["name"]})
|
|
124
|
+
|
|
125
|
+
xml_dict = self._recursive_elements_to_dict(xml_dict, root)
|
|
126
|
+
self._raw_data = xml_dict
|
|
127
|
+
self._data = deepcopy(self._raw_data)
|
|
128
|
+
|
|
129
|
+
def _recursive_elements_to_dict(self, xml_dict, tree):
|
|
130
|
+
# Some elements can have multiple instances e.g. domains.
|
|
131
|
+
# In these cases we need to have the id of that instance as a new key on the domain
|
|
132
|
+
# e.g. xml.domains[domain_id]["computational_area"]... etc
|
|
133
|
+
|
|
134
|
+
for child in tree:
|
|
135
|
+
if isinstance(child, etree._Comment):
|
|
136
|
+
continue # Skips comments in xml
|
|
137
|
+
child_key = child.tag.replace(self._ns, "")
|
|
138
|
+
if child_key in self._multi_value_keys:
|
|
139
|
+
if child_key in xml_dict:
|
|
140
|
+
xml_dict[child_key].append({})
|
|
141
|
+
else:
|
|
142
|
+
xml_dict[child_key] = [{}]
|
|
143
|
+
child_dict = xml_dict[child_key][-1]
|
|
144
|
+
else:
|
|
145
|
+
xml_dict[child_key] = {} # Create new key for element
|
|
146
|
+
child_dict = xml_dict[child_key]
|
|
147
|
+
value = "" if child.text is None else child.text.strip()
|
|
148
|
+
if "\n" in value:
|
|
149
|
+
value = value.split("\n") # Only used for output variables
|
|
150
|
+
if len(child.attrib) != 0:
|
|
151
|
+
child_dict.update(child.attrib)
|
|
152
|
+
if value != "":
|
|
153
|
+
child_dict.update({"value": value_from_string(value)})
|
|
154
|
+
|
|
155
|
+
self._recursive_elements_to_dict(child_dict, child)
|
|
156
|
+
|
|
157
|
+
elif value == "":
|
|
158
|
+
self._recursive_elements_to_dict(child_dict, child)
|
|
159
|
+
|
|
160
|
+
elif child_key in self._multi_value_keys:
|
|
161
|
+
xml_dict[child_key] = xml_dict[child_key][:-1] # remove unused dict
|
|
162
|
+
xml_dict[child_key].append(value_from_string(value))
|
|
163
|
+
else:
|
|
164
|
+
xml_dict[child_key] = value_from_string(value)
|
|
165
|
+
|
|
166
|
+
return xml_dict
|
|
167
|
+
|
|
168
|
+
def _recursive_reorder_xml(self, parent="ROOT"):
|
|
169
|
+
if parent == "ROOT":
|
|
170
|
+
parent = self._xmltree.getroot()
|
|
171
|
+
parent[:] = self._sort_from_schema(parent)
|
|
172
|
+
|
|
173
|
+
for child in parent:
|
|
174
|
+
if not isinstance(child, etree._Comment):
|
|
175
|
+
self._recursive_reorder_xml(child)
|
|
176
|
+
|
|
177
|
+
def _sort_from_schema(self, parent):
|
|
178
|
+
# find element in schema
|
|
179
|
+
parent_name = parent.tag.replace(self._ns, "")
|
|
180
|
+
schema_elem = self._xsd.find(
|
|
181
|
+
f".//{{http://www.w3.org/2001/XMLSchema}}*[@name='{parent_name}']",
|
|
182
|
+
)
|
|
183
|
+
if "type" in schema_elem.attrib:
|
|
184
|
+
schema_elem = self._xsd.find(
|
|
185
|
+
f".//{{http://www.w3.org/2001/XMLSchema}}*[@name='{schema_elem.attrib['type']}']",
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
schema_elem = schema_elem.find("{http://www.w3.org/2001/XMLSchema}complexType")
|
|
189
|
+
if schema_elem is None:
|
|
190
|
+
return parent.getchildren()
|
|
191
|
+
|
|
192
|
+
seq = schema_elem.find("{http://www.w3.org/2001/XMLSchema}sequence")
|
|
193
|
+
if seq is None:
|
|
194
|
+
return parent.getchildren()
|
|
195
|
+
|
|
196
|
+
categorical_order = {sub_element.attrib["name"]: idx for idx, sub_element in enumerate(seq)}
|
|
197
|
+
return sorted(
|
|
198
|
+
parent.getchildren(),
|
|
199
|
+
key=lambda x: categorical_sort(x, categorical_order, self._ns),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _validate(self):
|
|
203
|
+
try:
|
|
204
|
+
self._xsdschema.assert_(self._xmltree)
|
|
205
|
+
except AssertionError as err:
|
|
206
|
+
msg = (
|
|
207
|
+
f"XML Validation Error for {repr(self)}:\n"
|
|
208
|
+
f" {err.args[0].replace(self._ns, '')}"
|
|
209
|
+
)
|
|
210
|
+
raise ValueError(msg) from err
|
|
211
|
+
|
|
212
|
+
def _recursive_update_xml( # noqa: C901, PLR0912
|
|
213
|
+
self,
|
|
214
|
+
new_dict,
|
|
215
|
+
orig_dict,
|
|
216
|
+
parent_key,
|
|
217
|
+
list_idx=None,
|
|
218
|
+
):
|
|
219
|
+
# TODO: Handle removing params
|
|
220
|
+
|
|
221
|
+
for key, item in new_dict.items():
|
|
222
|
+
if key in self._multi_value_keys and not isinstance(item, list):
|
|
223
|
+
raise Exception(f"Element: '{key}' must be added as list")
|
|
224
|
+
if parent_key == "ROOT":
|
|
225
|
+
parent = self._xmltree.getroot()
|
|
226
|
+
else:
|
|
227
|
+
parent = self._xmltree.findall(f".//{self._ns}{parent_key}")[list_idx or 0]
|
|
228
|
+
|
|
229
|
+
if key not in orig_dict:
|
|
230
|
+
# New key added, add recursively
|
|
231
|
+
self._recursive_add_element(parent=parent, add_item=item, add_key=key)
|
|
232
|
+
|
|
233
|
+
elif isinstance(item, dict):
|
|
234
|
+
self._recursive_update_xml(item, orig_dict[key], key, list_idx)
|
|
235
|
+
elif isinstance(item, list) and isinstance(item[0], dict):
|
|
236
|
+
for i, _item in enumerate(item):
|
|
237
|
+
if isinstance(_item, dict):
|
|
238
|
+
try:
|
|
239
|
+
self._recursive_update_xml(_item, orig_dict[key][i], key, list_idx=i)
|
|
240
|
+
except IndexError:
|
|
241
|
+
# New thing added, Add it all recursively
|
|
242
|
+
self._recursive_add_element(
|
|
243
|
+
parent=parent,
|
|
244
|
+
add_item=_item,
|
|
245
|
+
add_key=key,
|
|
246
|
+
from_list=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
else:
|
|
250
|
+
if parent_key == "ROOT":
|
|
251
|
+
item = getattr(self, key)
|
|
252
|
+
try:
|
|
253
|
+
if item != orig_dict[key]:
|
|
254
|
+
if key == "value":
|
|
255
|
+
# Value has been updated
|
|
256
|
+
parent.text = str(item)
|
|
257
|
+
else:
|
|
258
|
+
# Attribute has been updated
|
|
259
|
+
elems = parent.findall(f"{self._ns}{key}")
|
|
260
|
+
if isinstance(item, list) and key != "variables":
|
|
261
|
+
# Handle multiple similar elements
|
|
262
|
+
if len(elems) < len(item):
|
|
263
|
+
while len(elems) < len(item):
|
|
264
|
+
elems.append(etree.SubElement(parent, f"{self._ns}{key}"))
|
|
265
|
+
elif len(elems) > len(item):
|
|
266
|
+
while len(elems) > len(item):
|
|
267
|
+
parent.remove(elems.pop())
|
|
268
|
+
|
|
269
|
+
for i in range(len(elems)):
|
|
270
|
+
elems[i].text = item[i]
|
|
271
|
+
|
|
272
|
+
elif len(elems) == 1:
|
|
273
|
+
elem = elems[0]
|
|
274
|
+
if isinstance(item, list):
|
|
275
|
+
elem.text = "\n".join(item)
|
|
276
|
+
else:
|
|
277
|
+
elem.text = str(item)
|
|
278
|
+
|
|
279
|
+
else:
|
|
280
|
+
parent.set(key, str(item))
|
|
281
|
+
except KeyError:
|
|
282
|
+
# New value/attribute added
|
|
283
|
+
self._recursive_add_element(parent=parent, add_item=item, add_key=key)
|
|
284
|
+
|
|
285
|
+
def _recursive_add_element( # noqa: C901, PLR0912
|
|
286
|
+
self,
|
|
287
|
+
parent,
|
|
288
|
+
add_item,
|
|
289
|
+
add_key,
|
|
290
|
+
from_list=False,
|
|
291
|
+
):
|
|
292
|
+
if add_key in self._multi_value_keys and not isinstance(add_item, list) and not from_list:
|
|
293
|
+
raise Exception(f"Element: '{add_key}' must be added as list")
|
|
294
|
+
if isinstance(add_item, dict):
|
|
295
|
+
new_element = etree.SubElement(parent, f"{self._ns}{add_key}")
|
|
296
|
+
for key, item in add_item.items():
|
|
297
|
+
self._recursive_add_element(parent=new_element, add_item=item, add_key=key)
|
|
298
|
+
elif isinstance(add_item, list):
|
|
299
|
+
if add_key == "variables":
|
|
300
|
+
# Variables is special case where we have list but add to one element
|
|
301
|
+
new_element = etree.SubElement(parent, f"{self._ns}{add_key}")
|
|
302
|
+
new_element.text = "\n".join(add_item)
|
|
303
|
+
else:
|
|
304
|
+
for item in add_item:
|
|
305
|
+
self._recursive_add_element(
|
|
306
|
+
parent=parent,
|
|
307
|
+
add_item=item,
|
|
308
|
+
add_key=add_key,
|
|
309
|
+
from_list=True,
|
|
310
|
+
)
|
|
311
|
+
elif add_key == "value": # Value has been added
|
|
312
|
+
parent.text = str(add_item)
|
|
313
|
+
else: # Attribute or element added
|
|
314
|
+
# Check schema to see if we should use parent.set for attribute
|
|
315
|
+
# or etree.subelement() and set text
|
|
316
|
+
schema_elem = self._xsd.findall(
|
|
317
|
+
f".//{{http://www.w3.org/2001/XMLSchema}}*[@name='{add_key}']",
|
|
318
|
+
)
|
|
319
|
+
if len(schema_elem) == 1:
|
|
320
|
+
schema_elem = schema_elem[0]
|
|
321
|
+
else:
|
|
322
|
+
# This is just here for when there's multiple schema elements with same
|
|
323
|
+
# name, e.g. 'frequency'
|
|
324
|
+
parent_schema_elem = self._xsd.find(
|
|
325
|
+
f".//{{http://www.w3.org/2001/XMLSchema}}*[@name='{parent.tag.replace(self._ns, '')}']",
|
|
326
|
+
)
|
|
327
|
+
if "type" in parent_schema_elem.attrib:
|
|
328
|
+
parent_schema_elem = self._xsd.find(
|
|
329
|
+
f".//{{http://www.w3.org/2001/XMLSchema}}*[@name='{parent_schema_elem.attrib['type']}']",
|
|
330
|
+
)
|
|
331
|
+
schema_elem = parent_schema_elem.find(
|
|
332
|
+
f".//{{http://www.w3.org/2001/XMLSchema}}*[@name='{add_key}']",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if schema_elem.tag.endswith("attribute"):
|
|
336
|
+
parent.set(add_key, str(add_item))
|
|
337
|
+
|
|
338
|
+
else:
|
|
339
|
+
new_element = etree.SubElement(parent, f"{self._ns}{add_key}")
|
|
340
|
+
new_element.text = str(add_item)
|
|
341
|
+
|
|
342
|
+
def _recursive_remove_data_xml(self, new_dict, parent, list_idx=None):
|
|
343
|
+
# This method will recursively work through the original dictionary and remove any
|
|
344
|
+
# items that are not in the new_dictionary and need to be removed.
|
|
345
|
+
list_idx = 0
|
|
346
|
+
list_idx_key = ""
|
|
347
|
+
for elem in parent:
|
|
348
|
+
if isinstance(elem, etree._Comment):
|
|
349
|
+
continue # Skips comments in xml
|
|
350
|
+
# Check each element is in the new_dict somewhere, delete if not
|
|
351
|
+
elem_key = elem.tag.replace(self._ns, "")
|
|
352
|
+
if elem_key in self._multi_value_keys:
|
|
353
|
+
if list_idx_key != elem_key:
|
|
354
|
+
list_idx_key = elem_key
|
|
355
|
+
list_idx = 0
|
|
356
|
+
try:
|
|
357
|
+
self._recursive_remove_data_xml(new_dict[elem_key][list_idx], elem)
|
|
358
|
+
list_idx += 1
|
|
359
|
+
except (IndexError, KeyError):
|
|
360
|
+
parent.remove(elem)
|
|
361
|
+
|
|
362
|
+
elif elem_key in new_dict:
|
|
363
|
+
self._recursive_remove_data_xml(new_dict[elem_key], elem)
|
|
364
|
+
|
|
365
|
+
else:
|
|
366
|
+
parent.remove(elem)
|
|
367
|
+
|
|
368
|
+
def _update_dict(self):
|
|
369
|
+
self._data = {}
|
|
370
|
+
for attr in [
|
|
371
|
+
"name",
|
|
372
|
+
"link1d",
|
|
373
|
+
"logfile",
|
|
374
|
+
"domains",
|
|
375
|
+
"restart_options",
|
|
376
|
+
"advanced_options",
|
|
377
|
+
"processor",
|
|
378
|
+
"unit_system",
|
|
379
|
+
"description",
|
|
380
|
+
]:
|
|
381
|
+
if getattr(self, attr) is not None:
|
|
382
|
+
if attr == "domains":
|
|
383
|
+
self._data["domain"] = [domain for _, domain in self.domains.items()]
|
|
384
|
+
else:
|
|
385
|
+
try:
|
|
386
|
+
self._data[attr] = getattr(self, attr)
|
|
387
|
+
except AttributeError:
|
|
388
|
+
self._data[attr] = None
|
|
389
|
+
|
|
390
|
+
def _write(self) -> str:
|
|
391
|
+
orig_xml_tree = deepcopy(self._xmltree)
|
|
392
|
+
try:
|
|
393
|
+
self._update_dict()
|
|
394
|
+
self._recursive_update_xml(self._data, self._raw_data, "ROOT")
|
|
395
|
+
self._recursive_remove_data_xml(self._data, self._xmltree.getroot())
|
|
396
|
+
etree.indent(self._xmltree, space=" ")
|
|
397
|
+
try:
|
|
398
|
+
self._validate()
|
|
399
|
+
except Exception:
|
|
400
|
+
self._recursive_reorder_xml()
|
|
401
|
+
self._validate()
|
|
402
|
+
|
|
403
|
+
self._raw_data = deepcopy(self._data) # reset raw data to equal data
|
|
404
|
+
|
|
405
|
+
return f'<?xml version="1.0" standalone="yes"?>\n{etree.tostring(self._xmltree.getroot()).decode()}'
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
self._xmltree = orig_xml_tree
|
|
409
|
+
self._handle_exception(e, when="write")
|
|
410
|
+
|
|
411
|
+
def _get_multi_value_keys(self):
|
|
412
|
+
self._multi_value_keys = []
|
|
413
|
+
root = self._xsd.getroot()
|
|
414
|
+
for elem in root.findall(".//{http://www.w3.org/2001/XMLSchema}element"):
|
|
415
|
+
if elem.attrib.get("maxOccurs") not in (None, "0", "1"):
|
|
416
|
+
self._multi_value_keys.append(elem.attrib["name"])
|
|
417
|
+
self._multi_value_keys = set(self._multi_value_keys)
|
|
418
|
+
|
|
419
|
+
def diff(self, other: XML2D, force_print: bool = False) -> None:
|
|
420
|
+
"""Compares the XML2D class against another XML2D class to check whether they are
|
|
421
|
+
equivalent, or if not, what the differences are. Two instances of a XML2D class are
|
|
422
|
+
deemed equivalent if all of their attributes are equal except for the filepath and
|
|
423
|
+
raw data. For example, two XML2D files from different filepaths that had the same
|
|
424
|
+
data except maybe some differences in decimal places and some default parameters
|
|
425
|
+
ommitted, would be classed as equaivalent as they would produce the same XML2D instance
|
|
426
|
+
and write the exact same data.
|
|
427
|
+
|
|
428
|
+
The result is printed to the console. If you need to access the returned data, use
|
|
429
|
+
the method ``XML2D._get_diff()``
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
other (floodmodeller_api.XML2D): Other instance of a XML2D class
|
|
433
|
+
force_print (bool): Forces the API to print every difference found, rather than
|
|
434
|
+
just the first 25 differences. Defaults to False.
|
|
435
|
+
"""
|
|
436
|
+
self._diff(other, force_print=force_print)
|
|
437
|
+
|
|
438
|
+
def update(self) -> None:
|
|
439
|
+
"""Updates the existing XML based on any altered attributes"""
|
|
440
|
+
self._update()
|
|
441
|
+
|
|
442
|
+
# Update XML dict and tree
|
|
443
|
+
self._read()
|
|
444
|
+
|
|
445
|
+
def save(self, filepath: str | Path | None):
|
|
446
|
+
"""Saves the XML to the given location, if pointing to an existing file it will be overwritten.
|
|
447
|
+
Once saved, the XML() class will continue working from the saved location, therefore any further calls to XML.update() will
|
|
448
|
+
update in the latest saved location rather than the original source XML used to construct the class
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
filepath (str): Filepath to new save location including the name and '.xml' extension
|
|
452
|
+
|
|
453
|
+
Raises:
|
|
454
|
+
TypeError: Raised if given filepath doesn't point to a file suffixed '.xml'
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
self._save(filepath)
|
|
458
|
+
|
|
459
|
+
# Update XML dict and tree
|
|
460
|
+
self._read()
|
|
461
|
+
self._log_path = self._filepath.with_suffix(".lf2")
|
|
462
|
+
|
|
463
|
+
def simulate( # noqa: C901, PLR0912, PLR0913
|
|
464
|
+
self,
|
|
465
|
+
method: str = "WAIT",
|
|
466
|
+
raise_on_failure: bool = True,
|
|
467
|
+
precision: str = "DEFAULT",
|
|
468
|
+
enginespath: str = "",
|
|
469
|
+
console_output: str = "simple",
|
|
470
|
+
range_function: Callable = trange,
|
|
471
|
+
range_settings: dict | None = None,
|
|
472
|
+
) -> Popen | None:
|
|
473
|
+
"""Simulate the XML2D file directly as a subprocess.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
method (str, optional): {'WAIT'} | 'RETURN_PROCESS'
|
|
477
|
+
'WAIT' - The function waits for the simulation to complete before continuing (This is default)
|
|
478
|
+
'RETURN_PROCESS' - The function sets the simulation running in background and immediately continues, whilst returning the process object.
|
|
479
|
+
Defaults to 'WAIT'.
|
|
480
|
+
raise_on_failure (bool, optional): If True, an exception will be raised if the simulation fails to complete without errors.
|
|
481
|
+
If set to False, then the script will continue to run even if the simulation fails. If 'method' is set to 'RETURN_PROCESS'
|
|
482
|
+
then this argument is ignored. Defaults to True.
|
|
483
|
+
precision (str, optional): {'DEFAULT'} | 'SINGLE' | 'DOUBLE'
|
|
484
|
+
Define which engine to use for simulation, if set to 'DEFAULT' it will use the precision specified in the IEF. Alternatively,
|
|
485
|
+
this can be overwritten using 'SINGLE' or 'DOUBLE'.
|
|
486
|
+
enginespath (str, optional): {''} | '/absolute/path/to/engine/executables'
|
|
487
|
+
Define where the engine executables are located. This replaces the default location (usual installation folder) if set to
|
|
488
|
+
anything other than ''.
|
|
489
|
+
console_output (str, optional): {'simple'} | 'standard' | 'detailed'
|
|
490
|
+
'simple' - A simple progress bar for the simulation is presented in the console
|
|
491
|
+
'standard' - The standard Flood Modeller 2D output is presented in the console
|
|
492
|
+
'detailed' - The most detailed Flood Modeller 2D output is presented in the console
|
|
493
|
+
Defaults to 'WAIT'.
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
Raises:
|
|
497
|
+
UserWarning: Raised if ief filepath not already specified
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
subprocess.Popen(): If method == 'RETURN_PROCESS', the Popen() instance of the process is returned.
|
|
501
|
+
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
# TODO:
|
|
505
|
+
# - Clean up the lf code?
|
|
506
|
+
# - Remove or sort out get results
|
|
507
|
+
|
|
508
|
+
self.range_function = range_function
|
|
509
|
+
self.range_settings = range_settings if range_settings else {}
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
if self._filepath is None:
|
|
513
|
+
raise UserWarning(
|
|
514
|
+
"xml2D must be saved to a specific filepath before simulate() can be called.",
|
|
515
|
+
)
|
|
516
|
+
if precision.upper() == "DEFAULT":
|
|
517
|
+
precision = "SINGLE" # defaults to single precision
|
|
518
|
+
for _, domain in self.domains.items():
|
|
519
|
+
if domain["run_data"].get("double_precision") == "required":
|
|
520
|
+
precision = "DOUBLE"
|
|
521
|
+
break
|
|
522
|
+
|
|
523
|
+
if enginespath == "":
|
|
524
|
+
# Default location
|
|
525
|
+
_enginespath = r"C:\Program Files\Flood Modeller\bin"
|
|
526
|
+
else:
|
|
527
|
+
_enginespath = enginespath
|
|
528
|
+
if not Path(_enginespath).exists():
|
|
529
|
+
raise Exception(
|
|
530
|
+
f"Flood Modeller non-default engine path not found! {str(_enginespath)}",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# checking if all schemes used are fast, if so will use FAST.exe
|
|
534
|
+
# TODO: Add in option to choose to use or not to use if you can
|
|
535
|
+
is_fast = True
|
|
536
|
+
for _, domain in self.domains.items():
|
|
537
|
+
if domain["run_data"]["scheme"] != "FAST":
|
|
538
|
+
is_fast = False
|
|
539
|
+
break
|
|
540
|
+
|
|
541
|
+
if is_fast is True:
|
|
542
|
+
isis2d_fp = str(Path(_enginespath, "FAST.exe"))
|
|
543
|
+
elif precision.upper() == "SINGLE":
|
|
544
|
+
isis2d_fp = str(Path(_enginespath, "ISIS2d.exe"))
|
|
545
|
+
else:
|
|
546
|
+
isis2d_fp = str(Path(_enginespath, "ISIS2d_DP.exe"))
|
|
547
|
+
|
|
548
|
+
if not Path(isis2d_fp).exists():
|
|
549
|
+
raise Exception(f"Flood Modeller engine not found! Expected location: {isis2d_fp}")
|
|
550
|
+
|
|
551
|
+
console_output = console_output.lower()
|
|
552
|
+
run_command = (
|
|
553
|
+
f'"{isis2d_fp}" {"-q" if console_output != "detailed" else ""} "{self._filepath}"'
|
|
554
|
+
)
|
|
555
|
+
stdout = DEVNULL if console_output == "simple" else None
|
|
556
|
+
|
|
557
|
+
if method.upper() == "WAIT":
|
|
558
|
+
print("Executing simulation ... ")
|
|
559
|
+
# execute simulation
|
|
560
|
+
process = Popen(run_command, cwd=os.path.dirname(self._filepath), stdout=stdout)
|
|
561
|
+
|
|
562
|
+
# progress bar based on log files:
|
|
563
|
+
if console_output == "simple":
|
|
564
|
+
self._init_log_file()
|
|
565
|
+
self._update_progress_bar(process)
|
|
566
|
+
|
|
567
|
+
while process.poll() is None:
|
|
568
|
+
# process is still running
|
|
569
|
+
time.sleep(1)
|
|
570
|
+
|
|
571
|
+
exitcode = process.returncode
|
|
572
|
+
self._interpret_exit_code(exitcode, raise_on_failure)
|
|
573
|
+
|
|
574
|
+
elif method.upper() == "RETURN_PROCESS":
|
|
575
|
+
print("Executing simulation ...")
|
|
576
|
+
# execute simulation
|
|
577
|
+
return Popen(run_command, cwd=os.path.dirname(self._filepath), stdout=stdout)
|
|
578
|
+
|
|
579
|
+
return None
|
|
580
|
+
|
|
581
|
+
except Exception as e:
|
|
582
|
+
self._handle_exception(e, when="simulate")
|
|
583
|
+
|
|
584
|
+
def get_log(self):
|
|
585
|
+
"""If log files for the simulation exist, this function returns them as a LF2 class object
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
floodmodeller_api.LF2 class object
|
|
589
|
+
"""
|
|
590
|
+
if not self._log_path.exists():
|
|
591
|
+
raise FileNotFoundError("Log file (LF2) not found")
|
|
592
|
+
|
|
593
|
+
return lf_factory(self._log_path, "lf2", False)
|
|
594
|
+
|
|
595
|
+
def _init_log_file(self):
|
|
596
|
+
"""Checks for a new log file, waiting for its creation if necessary"""
|
|
597
|
+
# wait for log file to exist
|
|
598
|
+
log_file_exists = False
|
|
599
|
+
max_time = time.time() + 10
|
|
600
|
+
|
|
601
|
+
while not log_file_exists:
|
|
602
|
+
time.sleep(0.1)
|
|
603
|
+
log_file_exists = self._log_path.is_file()
|
|
604
|
+
|
|
605
|
+
# timeout
|
|
606
|
+
if time.time() > max_time:
|
|
607
|
+
self._no_log_file("log file is expected but not detected")
|
|
608
|
+
self._lf = None
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
# wait for new log file
|
|
612
|
+
old_log_file = True
|
|
613
|
+
max_time = time.time() + 10
|
|
614
|
+
|
|
615
|
+
while old_log_file:
|
|
616
|
+
time.sleep(0.1)
|
|
617
|
+
|
|
618
|
+
# difference between now and when log file was last modified
|
|
619
|
+
last_modified_timestamp = self._log_path.stat().st_mtime
|
|
620
|
+
last_modified = dt.datetime.fromtimestamp(last_modified_timestamp)
|
|
621
|
+
time_diff_sec = (dt.datetime.now() - last_modified).total_seconds()
|
|
622
|
+
|
|
623
|
+
# it's old if it's over self.OLD_FILE seconds old (TODO: is this robust?)
|
|
624
|
+
old_log_file = time_diff_sec > self.OLD_FILE
|
|
625
|
+
|
|
626
|
+
# timeout
|
|
627
|
+
if time.time() > max_time:
|
|
628
|
+
self._no_log_file("log file is from previous run")
|
|
629
|
+
self._lf = None
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
# create LF instance
|
|
633
|
+
self._lf = lf_factory(self._log_path, "lf2", False)
|
|
634
|
+
|
|
635
|
+
def _no_log_file(self, reason):
|
|
636
|
+
"""Warning that there will be no progress bar"""
|
|
637
|
+
|
|
638
|
+
print("No progress bar as " + reason + ". Simulation will continue as usual.")
|
|
639
|
+
|
|
640
|
+
def _update_progress_bar(self, process: Popen):
|
|
641
|
+
"""Updates progress bar based on log file"""
|
|
642
|
+
|
|
643
|
+
# only if there is a log file
|
|
644
|
+
if self._lf is None:
|
|
645
|
+
return
|
|
646
|
+
|
|
647
|
+
# tqdm progress bar
|
|
648
|
+
for i in self.range_function(100, **self.range_settings):
|
|
649
|
+
# Process still running
|
|
650
|
+
while process.poll() is None:
|
|
651
|
+
time.sleep(0.1)
|
|
652
|
+
|
|
653
|
+
# Find progress
|
|
654
|
+
self._lf.read(suppress_final_step=True)
|
|
655
|
+
progress = self._lf.report_progress()
|
|
656
|
+
|
|
657
|
+
# Reached i% progress => move onto waiting for (i+1)%
|
|
658
|
+
if progress > i:
|
|
659
|
+
break
|
|
660
|
+
|
|
661
|
+
# Process stopped
|
|
662
|
+
if process.poll() is not None:
|
|
663
|
+
# Find final progress
|
|
664
|
+
self._lf.read(suppress_final_step=True)
|
|
665
|
+
progress = self._lf.report_progress()
|
|
666
|
+
|
|
667
|
+
if progress > i:
|
|
668
|
+
pass # stopped because it completed
|
|
669
|
+
else:
|
|
670
|
+
break # stopped for another reason
|
|
671
|
+
|
|
672
|
+
def _interpret_exit_code(self, exitcode: int, raise_on_failure: bool):
|
|
673
|
+
"""This function will interpret the exit code and tell us if this is good or bad
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
exitcode - this is the exitcode from the simulation
|
|
677
|
+
|
|
678
|
+
Return:
|
|
679
|
+
String that explains the exitcode - this might be too much!
|
|
680
|
+
"""
|
|
681
|
+
try:
|
|
682
|
+
msg = f"Exit with {exitcode}: {error_2d_dict[exitcode]}"
|
|
683
|
+
except Exception:
|
|
684
|
+
msg = f"Exit with {exitcode}: Unknown error occurred!"
|
|
685
|
+
|
|
686
|
+
if raise_on_failure and exitcode != self.GOOD_EXIT_CODE:
|
|
687
|
+
raise Exception(msg)
|
|
688
|
+
print(msg)
|