floodmodeller-api 0.4.2__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 -838
- floodmodeller_api/diff.py +136 -119
- floodmodeller_api/ied.py +307 -311
- floodmodeller_api/ief.py +647 -646
- floodmodeller_api/ief_flags.py +253 -253
- floodmodeller_api/inp.py +266 -268
- 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 -314
- floodmodeller_api/logs/lf_helpers.py +354 -346
- 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 -154
- 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 -330
- 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 -94
- 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 -72
- floodmodeller_api/units/__init__.py +10 -10
- floodmodeller_api/units/_base.py +214 -209
- floodmodeller_api/units/boundaries.py +467 -469
- floodmodeller_api/units/comment.py +52 -55
- floodmodeller_api/units/conduits.py +382 -403
- floodmodeller_api/units/helpers.py +123 -132
- floodmodeller_api/units/iic.py +107 -101
- floodmodeller_api/units/losses.py +305 -308
- floodmodeller_api/units/sections.py +444 -445
- floodmodeller_api/units/structures.py +1690 -1684
- floodmodeller_api/units/units.py +93 -102
- 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 -177
- 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 -107
- floodmodeller_api/urban1d/raingauges.py +111 -108
- floodmodeller_api/urban1d/subsections.py +92 -93
- floodmodeller_api/urban1d/xsections.py +147 -141
- 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 -112
- floodmodeller_api/version.py +1 -1
- floodmodeller_api/xml2d.py +688 -684
- floodmodeller_api/xml2d_template.py +37 -37
- floodmodeller_api/zzn.py +387 -365
- {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/LICENSE.txt +13 -13
- {floodmodeller_api-0.4.2.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.dist-info → floodmodeller_api-0.4.3.dist-info}/WHEEL +1 -1
- floodmodeller_api-0.4.3.dist-info/entry_points.txt +3 -0
- 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.data/scripts/fmapi-add_siltation.bat +0 -2
- floodmodeller_api-0.4.2.data/scripts/fmapi-add_siltation.py +0 -3
- floodmodeller_api-0.4.2.data/scripts/fmapi-structure_log.bat +0 -2
- floodmodeller_api-0.4.2.data/scripts/fmapi-structure_log.py +0 -3
- floodmodeller_api-0.4.2.data/scripts/fmapi-toolbox.bat +0 -2
- floodmodeller_api-0.4.2.data/scripts/fmapi-toolbox.py +0 -41
- floodmodeller_api-0.4.2.dist-info/RECORD +0 -169
- {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/top_level.txt +0 -0
floodmodeller_api/ief.py
CHANGED
|
@@ -1,646 +1,647 @@
|
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
from
|
|
29
|
-
|
|
30
|
-
from .
|
|
31
|
-
from .
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
90
|
-
if
|
|
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
|
-
if
|
|
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
|
-
self.
|
|
225
|
-
|
|
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
|
-
def
|
|
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
|
-
the
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
if
|
|
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
|
-
|
|
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 os
|
|
21
|
+
import subprocess
|
|
22
|
+
import time
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from subprocess import Popen
|
|
25
|
+
from typing import Callable
|
|
26
|
+
|
|
27
|
+
import pandas as pd
|
|
28
|
+
from tqdm import trange
|
|
29
|
+
|
|
30
|
+
from ._base import FMFile
|
|
31
|
+
from .ief_flags import flags
|
|
32
|
+
from .logs import lf_factory
|
|
33
|
+
from .zzn import ZZN
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class IEF(FMFile):
|
|
37
|
+
"""Reads and write Flood Modeller event file format '.ief'
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
ief_filepath (str, optional): Full filepath to ief file. If not specified, a new IEF class will be created.. Defaults to None.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
TypeError: Raised if ief_filepath not pointing to valide IEF file
|
|
44
|
+
FileNotFoundError: Raised if ief_filepath points to a non-existent location
|
|
45
|
+
|
|
46
|
+
Output:
|
|
47
|
+
Initiates 'IEF' class object
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_filetype: str = "IEF"
|
|
51
|
+
_suffix: str = ".ief"
|
|
52
|
+
OLD_FILE = 5
|
|
53
|
+
ERROR_MAX = 2000
|
|
54
|
+
WARNING_MAX = 3000
|
|
55
|
+
LOG_TIMEOUT = 10
|
|
56
|
+
|
|
57
|
+
def __init__(self, ief_filepath: str | Path | None = None, from_json: bool = False):
|
|
58
|
+
try:
|
|
59
|
+
if from_json:
|
|
60
|
+
return
|
|
61
|
+
if ief_filepath is not None:
|
|
62
|
+
FMFile.__init__(self, ief_filepath)
|
|
63
|
+
|
|
64
|
+
self._read()
|
|
65
|
+
|
|
66
|
+
else:
|
|
67
|
+
self._create_from_blank()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
self._handle_exception(e, when="read")
|
|
70
|
+
|
|
71
|
+
def _read(self):
|
|
72
|
+
# Read IEF data
|
|
73
|
+
with open(self._filepath) as ief_file:
|
|
74
|
+
raw_data = [line.rstrip("\n") for line in ief_file.readlines()]
|
|
75
|
+
# Clean data and add as class properties
|
|
76
|
+
# Create a list to store the properties which are to be saved in IEF, so as to ignore any temp properties.
|
|
77
|
+
prev_comment = None
|
|
78
|
+
self._ief_properties = []
|
|
79
|
+
for line in raw_data:
|
|
80
|
+
# Handle any comments here (prefixed with ;)
|
|
81
|
+
if line.lstrip().startswith(";"):
|
|
82
|
+
self._ief_properties.append(line) # Add comment in raw state
|
|
83
|
+
prev_comment = line.strip(";")
|
|
84
|
+
|
|
85
|
+
elif "=" in line:
|
|
86
|
+
# Using strip() method to remove any leading/trailing whitespace
|
|
87
|
+
prop, value = (itm.strip() for itm in line.split("=", 1))
|
|
88
|
+
# Handle 'EventData' properties so that multiple can be set
|
|
89
|
+
if prop.upper() == "EVENTDATA":
|
|
90
|
+
if prev_comment is None:
|
|
91
|
+
try:
|
|
92
|
+
event_data_title = Path(value).stem
|
|
93
|
+
except Exception:
|
|
94
|
+
event_data_title = value
|
|
95
|
+
else:
|
|
96
|
+
event_data_title = prev_comment
|
|
97
|
+
if hasattr(self, "EventData"):
|
|
98
|
+
# Append event data to list so multiple can be specified
|
|
99
|
+
self.EventData[event_data_title] = value
|
|
100
|
+
else:
|
|
101
|
+
self.EventData = {event_data_title: value}
|
|
102
|
+
self._ief_properties.append("EventData")
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
# Sets the property and value as class properties so they can be edited.
|
|
106
|
+
setattr(self, prop, value)
|
|
107
|
+
self._ief_properties.append(prop)
|
|
108
|
+
prev_comment = None
|
|
109
|
+
else:
|
|
110
|
+
# This should add the [] bound headers
|
|
111
|
+
self._ief_properties.append(line)
|
|
112
|
+
prev_comment = None
|
|
113
|
+
del raw_data
|
|
114
|
+
|
|
115
|
+
def _write(self) -> str:
|
|
116
|
+
"""Returns string representation of the current IEF data
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
str: Full string representation of IEF in its most recent state (including changes not yet saved to disk)
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
# update _ief_properties
|
|
123
|
+
self._update_ief_properties()
|
|
124
|
+
|
|
125
|
+
ief_string = ""
|
|
126
|
+
event = 0 # Used as a counter for multiple eventdata files
|
|
127
|
+
for idx, prop in enumerate(self._ief_properties):
|
|
128
|
+
if prop.startswith("["):
|
|
129
|
+
# writes the [] bound headers to ief string
|
|
130
|
+
ief_string += prop + "\n"
|
|
131
|
+
elif prop.lstrip().startswith(";"):
|
|
132
|
+
if self._ief_properties[idx + 1].lower() != "eventdata":
|
|
133
|
+
# Only write comment if not preceding event data
|
|
134
|
+
ief_string += prop + "\n"
|
|
135
|
+
elif prop.lower() == "eventdata":
|
|
136
|
+
event_data = getattr(self, prop)
|
|
137
|
+
# Add multiple EventData if present
|
|
138
|
+
for event_idx, key in enumerate(event_data):
|
|
139
|
+
if event_idx == event:
|
|
140
|
+
ief_string += f";{key}\n{prop}={str(event_data[key])}\n"
|
|
141
|
+
break
|
|
142
|
+
event += 1
|
|
143
|
+
|
|
144
|
+
else:
|
|
145
|
+
# writes property and value to ief string
|
|
146
|
+
ief_string += f"{prop}={str(getattr(self, prop))}\n"
|
|
147
|
+
return ief_string
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
self._handle_exception(e, when="write")
|
|
151
|
+
|
|
152
|
+
def _create_from_blank(self):
|
|
153
|
+
# No filepath specified, create new 'blank' IEF in memory
|
|
154
|
+
blank_ief = [
|
|
155
|
+
"[ISIS Event Header]",
|
|
156
|
+
'Title=""',
|
|
157
|
+
'Datafile=""',
|
|
158
|
+
'Results=""',
|
|
159
|
+
"[ISIS Event Details]",
|
|
160
|
+
"RunType=Steady",
|
|
161
|
+
"Start=0",
|
|
162
|
+
"ICsFrom=1",
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
# Create a list to store the properties which are to be saved in IEF, so as to ignore any temp properties.
|
|
166
|
+
self._ief_properties = []
|
|
167
|
+
for line in blank_ief:
|
|
168
|
+
if "=" in line:
|
|
169
|
+
prop, value = line.split("=")
|
|
170
|
+
# Sets the property and value as class properties so they can be edited.
|
|
171
|
+
setattr(self, prop, value)
|
|
172
|
+
self._ief_properties.append(prop)
|
|
173
|
+
else:
|
|
174
|
+
# This should add the [] bound headers
|
|
175
|
+
self._ief_properties.append(line)
|
|
176
|
+
del blank_ief
|
|
177
|
+
|
|
178
|
+
def _update_ief_properties(self): # noqa: C901
|
|
179
|
+
"""Updates the list of properties included in the IEF file"""
|
|
180
|
+
# Add new properties
|
|
181
|
+
for prop, val in self.__dict__.copy().items():
|
|
182
|
+
if (prop not in self._ief_properties) and (not prop.startswith("_")) and prop != "file":
|
|
183
|
+
# Check if valid flag
|
|
184
|
+
if prop.upper() not in flags:
|
|
185
|
+
print(
|
|
186
|
+
f"Warning: '{prop}' is not a valid IEF flag, it will be ommited from the IEF\n",
|
|
187
|
+
)
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
if prop.upper() == "EVENTDATA" and prop != "EventData":
|
|
191
|
+
# (1) This will be triggered in special case where eventdata has been added with different case, but case
|
|
192
|
+
# needs to be kept as 'EventData', to allow dealing wiht multiple IEDs
|
|
193
|
+
# (2) In case of EventData being added with correct case where it doesn't already
|
|
194
|
+
# exist, this stops it being deleted
|
|
195
|
+
# Add new values to EventData flag
|
|
196
|
+
delattr(self, prop)
|
|
197
|
+
self.EventData = val
|
|
198
|
+
prop = "EventData"
|
|
199
|
+
|
|
200
|
+
# Check ief group header
|
|
201
|
+
group = f"[{flags[prop.upper()]}]"
|
|
202
|
+
if group in self._ief_properties:
|
|
203
|
+
# If group already exists, add property to end of group
|
|
204
|
+
group_idx = False
|
|
205
|
+
# defaults to inserting in last place
|
|
206
|
+
insert_index = len(self._ief_properties)
|
|
207
|
+
for idx, item in enumerate(self._ief_properties):
|
|
208
|
+
if group_idx is True and item.startswith("["):
|
|
209
|
+
insert_index = idx
|
|
210
|
+
break
|
|
211
|
+
if item == group:
|
|
212
|
+
group_idx = True
|
|
213
|
+
|
|
214
|
+
self._ief_properties.insert(insert_index, prop)
|
|
215
|
+
else:
|
|
216
|
+
# Add group header to the end of list
|
|
217
|
+
self._ief_properties.append(group)
|
|
218
|
+
# Add property to end of list
|
|
219
|
+
self._ief_properties.append(prop)
|
|
220
|
+
|
|
221
|
+
# Remove any deleted properties
|
|
222
|
+
self._ief_properties = [
|
|
223
|
+
line
|
|
224
|
+
for line in self._ief_properties
|
|
225
|
+
if (line.startswith("[") or (line in dir(self)) or line.lstrip().startswith(";"))
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
# Rearrange order of Flow Time Profiles group if present * Currently assuming all relevent flags included
|
|
229
|
+
if "[Flow Time Profiles]" in self._ief_properties:
|
|
230
|
+
self._update_flowtimeprofile_info()
|
|
231
|
+
|
|
232
|
+
# Ensure number of EventData entries is equal to length of EventData attribute
|
|
233
|
+
if hasattr(self, "EventData"):
|
|
234
|
+
self._update_eventdata_info()
|
|
235
|
+
|
|
236
|
+
def _update_eventdata_info(self): # noqa: C901
|
|
237
|
+
if not isinstance(self.EventData, dict):
|
|
238
|
+
# If attribute not a dict, adds the value as a single entry in list
|
|
239
|
+
raise AttributeError(
|
|
240
|
+
"The 'EventData' attribute should be a dictionary with keys defining the event"
|
|
241
|
+
" names and values referencing the IED files",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Number of 'EventData' flags in ief
|
|
245
|
+
event_properties = self._ief_properties.count("EventData")
|
|
246
|
+
# Number of event data specified in class
|
|
247
|
+
events = len(self.EventData)
|
|
248
|
+
if event_properties < events:
|
|
249
|
+
# Need to add additional event properties to IEF to match number of events specified
|
|
250
|
+
to_add = events - event_properties
|
|
251
|
+
# Used for if no existing eventdata exists
|
|
252
|
+
insert_index = len(self._ief_properties)
|
|
253
|
+
for idx, itm in enumerate(reversed(self._ief_properties)):
|
|
254
|
+
if itm in ("EventData", "[ISIS Event Details]"):
|
|
255
|
+
insert_index = len(self._ief_properties) - idx
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
for _ in range(to_add):
|
|
259
|
+
# Add in required number of extra EventData after last one.
|
|
260
|
+
self._ief_properties.insert(insert_index, "EventData")
|
|
261
|
+
|
|
262
|
+
elif event_properties > events:
|
|
263
|
+
# Need to remove some event properties from IEF to match number of events specified
|
|
264
|
+
to_remove = event_properties - events
|
|
265
|
+
removed = 0 # Counter for number removed
|
|
266
|
+
num_props = len(self._ief_properties)
|
|
267
|
+
for idx, itm in enumerate(reversed(self._ief_properties)):
|
|
268
|
+
if itm == "EventData":
|
|
269
|
+
del self._ief_properties[num_props - 1 - idx]
|
|
270
|
+
# Also remove event data title comment if present
|
|
271
|
+
if self._ief_properties[num_props - 2 - idx].lstrip().startswith(";"):
|
|
272
|
+
del self._ief_properties[num_props - 2 - idx]
|
|
273
|
+
removed += 1
|
|
274
|
+
if removed == to_remove:
|
|
275
|
+
break
|
|
276
|
+
|
|
277
|
+
def _update_flowtimeprofile_info(self):
|
|
278
|
+
end_index = None
|
|
279
|
+
start_index = self._ief_properties.index("[Flow Time Profiles]")
|
|
280
|
+
for idx, item in enumerate(self._ief_properties[start_index:]):
|
|
281
|
+
if idx != 0 and item.startswith("["):
|
|
282
|
+
end_index = idx + start_index
|
|
283
|
+
break
|
|
284
|
+
flow_time_list = self._ief_properties[start_index:end_index]
|
|
285
|
+
flow_time_list = [
|
|
286
|
+
"[Flow Time Profiles]",
|
|
287
|
+
"NoOfFlowTimeProfiles",
|
|
288
|
+
"NoOfFlowTimeSeries",
|
|
289
|
+
] + [i for i in flow_time_list if i.lower().startswith("flowtimeprofile")]
|
|
290
|
+
|
|
291
|
+
# sort list to ensure the flow time profiles are in order
|
|
292
|
+
def flow_sort(itm):
|
|
293
|
+
try:
|
|
294
|
+
num = int(itm.upper().replace("FLOWTIMEPROFILE", ""))
|
|
295
|
+
return (1, num)
|
|
296
|
+
except ValueError:
|
|
297
|
+
return (0, itm)
|
|
298
|
+
|
|
299
|
+
flow_time_list[3:] = sorted(flow_time_list[3:], key=flow_sort)
|
|
300
|
+
|
|
301
|
+
# Replace existing slice of ief properties with new reordered slice
|
|
302
|
+
self._ief_properties[start_index:end_index] = flow_time_list
|
|
303
|
+
|
|
304
|
+
# Update NoOfFlowTimeSeries
|
|
305
|
+
self.NoOfFlowTimeProfiles = str(len(flow_time_list[3:]))
|
|
306
|
+
|
|
307
|
+
def __getattr__(self, name):
|
|
308
|
+
for attr in self.__dict__.copy():
|
|
309
|
+
if name.lower() == attr.lower():
|
|
310
|
+
return self.__dict__[attr]
|
|
311
|
+
return self.__getattribute__(name)
|
|
312
|
+
|
|
313
|
+
def __setattr__(self, name, value):
|
|
314
|
+
existing_attr_updated = False
|
|
315
|
+
for attr in self.__dict__.copy():
|
|
316
|
+
if name.lower() == attr.lower():
|
|
317
|
+
self.__dict__[attr] = value
|
|
318
|
+
existing_attr_updated = True
|
|
319
|
+
|
|
320
|
+
if not existing_attr_updated:
|
|
321
|
+
self.__dict__[name] = value
|
|
322
|
+
|
|
323
|
+
def __delattr__(self, name):
|
|
324
|
+
existing_attr_deleted = False
|
|
325
|
+
for attr in self.__dict__.copy():
|
|
326
|
+
if name.lower() == attr.lower():
|
|
327
|
+
super().__delattr__(attr)
|
|
328
|
+
existing_attr_deleted = True
|
|
329
|
+
|
|
330
|
+
if not existing_attr_deleted:
|
|
331
|
+
super().__delattr__(name)
|
|
332
|
+
|
|
333
|
+
def diff(self, other: IEF, force_print: bool = False) -> None:
|
|
334
|
+
"""Compares the IEF class against another IEF class to check whether they are
|
|
335
|
+
equivalent, or if not, what the differences are. Two instances of an IEF class are
|
|
336
|
+
deemed equivalent if all of their attributes are equal except for the filepath and
|
|
337
|
+
raw data.
|
|
338
|
+
|
|
339
|
+
The result is printed to the console. If you need to access the returned data, use
|
|
340
|
+
the method ``IEF._get_diff()``
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
other (floodmodeller_api.IEF): Other instance of an IEF class
|
|
344
|
+
force_print (bool): Forces the API to print every difference found, rather than
|
|
345
|
+
just the first 25 differences. Defaults to False.
|
|
346
|
+
"""
|
|
347
|
+
self._diff(other, force_print=force_print)
|
|
348
|
+
|
|
349
|
+
def update(self) -> None:
|
|
350
|
+
"""Updates the existing IEF based on any altered attributes"""
|
|
351
|
+
self._update()
|
|
352
|
+
|
|
353
|
+
def save(self, filepath: str | Path) -> None:
|
|
354
|
+
"""Saves the IEF to the given location, if pointing to an existing file it will be overwritten.
|
|
355
|
+
Once saved, the IEF() class will continue working from the saved location, therefore any further calls to IEF.update() will update in the latest saved location
|
|
356
|
+
rather than the original source IEF used to construct the class
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
filepath (string): Full filepath to new location for ief file (including '.ief' extension)
|
|
360
|
+
"""
|
|
361
|
+
self._save(filepath)
|
|
362
|
+
|
|
363
|
+
def simulate( # noqa: C901, PLR0912, PLR0913
|
|
364
|
+
self,
|
|
365
|
+
method: str = "WAIT",
|
|
366
|
+
raise_on_failure: bool = True,
|
|
367
|
+
precision: str = "DEFAULT",
|
|
368
|
+
enginespath: str = "",
|
|
369
|
+
range_function: Callable = trange,
|
|
370
|
+
range_settings: dict | None = None,
|
|
371
|
+
) -> subprocess.Popen | None:
|
|
372
|
+
"""Simulate the IEF file directly as a subprocess
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
method (str, optional): {'WAIT'} | 'RETURN_PROCESS'
|
|
376
|
+
'WAIT' - The function waits for the simulation to complete before continuing (This is default)
|
|
377
|
+
'RETURN_PROCESS' - The function sets the simulation running in background and immediately continues, whilst returning the process object.
|
|
378
|
+
Defaults to 'WAIT'.
|
|
379
|
+
raise_on_failure (bool, optional): If True, an exception will be raised if the simulation fails to complete without errors.
|
|
380
|
+
If set to False, then the script will continue to run even if the simulation fails. If 'method' is set to 'RETURN_PROCESS'
|
|
381
|
+
then this argument is ignored. Defaults to True.
|
|
382
|
+
precision (str, optional): {'DEFAULT'} | 'SINGLE' | 'DOUBLE'
|
|
383
|
+
Define which engine to use for simulation, if set to 'DEFAULT' it will use the precision specified in the IEF. Alternatively,
|
|
384
|
+
this can be overwritten using 'SINGLE' or 'DOUBLE'.
|
|
385
|
+
enginespath (str, optional): {''} | '/absolute/path/to/engine/executables'
|
|
386
|
+
Define where the engine executables are located. This replaces the default location (usual installation folder) if set to
|
|
387
|
+
anything other than ''.
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
UserWarning: Raised if ief filepath not already specified
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
subprocess.Popen(): If method == 'RETURN_PROCESS', the Popen() instance of the process is returned.
|
|
394
|
+
"""
|
|
395
|
+
try:
|
|
396
|
+
self._range_function = range_function
|
|
397
|
+
self._range_settings = range_settings if range_settings else {}
|
|
398
|
+
if self._filepath is None:
|
|
399
|
+
raise UserWarning(
|
|
400
|
+
"IEF must be saved to a specific filepath before simulate() can be called.",
|
|
401
|
+
)
|
|
402
|
+
if precision.upper() == "DEFAULT":
|
|
403
|
+
precision = "SINGLE" # Defaults to single...
|
|
404
|
+
for attr in dir(self):
|
|
405
|
+
if (
|
|
406
|
+
attr.upper() == "LAUNCHDOUBLEPRECISIONVERSION" # Unless DP specified
|
|
407
|
+
and int(getattr(self, attr)) == 1
|
|
408
|
+
):
|
|
409
|
+
precision = "DOUBLE"
|
|
410
|
+
break
|
|
411
|
+
|
|
412
|
+
if enginespath == "":
|
|
413
|
+
_enginespath = r"C:\Program Files\Flood Modeller\bin" # Default location
|
|
414
|
+
else:
|
|
415
|
+
_enginespath = enginespath
|
|
416
|
+
if not Path(_enginespath).exists():
|
|
417
|
+
raise Exception(
|
|
418
|
+
f"Flood Modeller non-default engine path not found! {str(_enginespath)}",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
if precision.upper() == "SINGLE":
|
|
422
|
+
isis32_fp = str(Path(_enginespath, "ISISf32.exe"))
|
|
423
|
+
else:
|
|
424
|
+
isis32_fp = str(Path(_enginespath, "ISISf32_DoubleP.exe"))
|
|
425
|
+
|
|
426
|
+
if not Path(isis32_fp).exists():
|
|
427
|
+
raise Exception(f"Flood Modeller engine not found! Expected location: {isis32_fp}")
|
|
428
|
+
|
|
429
|
+
run_command = f'"{isis32_fp}" -sd "{self._filepath}"'
|
|
430
|
+
|
|
431
|
+
if method.upper() == "WAIT":
|
|
432
|
+
print("Executing simulation...")
|
|
433
|
+
# execute simulation
|
|
434
|
+
process = Popen(run_command, cwd=os.path.dirname(self._filepath))
|
|
435
|
+
|
|
436
|
+
# progress bar based on log files
|
|
437
|
+
self._init_log_file()
|
|
438
|
+
self._update_progress_bar(process)
|
|
439
|
+
|
|
440
|
+
while process.poll() is None:
|
|
441
|
+
# Process still running
|
|
442
|
+
time.sleep(1)
|
|
443
|
+
|
|
444
|
+
result, summary = self._summarise_exy()
|
|
445
|
+
|
|
446
|
+
if result == 1 and raise_on_failure:
|
|
447
|
+
raise RuntimeError(summary)
|
|
448
|
+
print(summary)
|
|
449
|
+
|
|
450
|
+
elif method.upper() == "RETURN_PROCESS":
|
|
451
|
+
print("Executing simulation...")
|
|
452
|
+
# execute simulation
|
|
453
|
+
return Popen(run_command, cwd=os.path.dirname(self._filepath))
|
|
454
|
+
|
|
455
|
+
return None
|
|
456
|
+
|
|
457
|
+
except Exception as e:
|
|
458
|
+
self._handle_exception(e, when="simulate")
|
|
459
|
+
|
|
460
|
+
def _get_result_filepath(self, suffix):
|
|
461
|
+
if hasattr(self, "Results"):
|
|
462
|
+
path = Path(self.Results).with_suffix("." + suffix)
|
|
463
|
+
if not path.is_absolute():
|
|
464
|
+
# set cwd to ief location and resolve path
|
|
465
|
+
path = Path(self._filepath.parent, path).resolve()
|
|
466
|
+
|
|
467
|
+
else:
|
|
468
|
+
path = self._filepath.with_suffix("." + suffix)
|
|
469
|
+
|
|
470
|
+
return path
|
|
471
|
+
|
|
472
|
+
def get_results(self) -> ZZN:
|
|
473
|
+
"""If results for the simulation exist, this function returns them as a ZZN class object
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
floodmodeller_api.ZZN class object
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
# Get zzn location
|
|
480
|
+
result_path = self._get_result_filepath(suffix="zzn")
|
|
481
|
+
|
|
482
|
+
if not result_path.exists():
|
|
483
|
+
raise FileNotFoundError("Simulation results file (zzn) not found")
|
|
484
|
+
|
|
485
|
+
return ZZN(result_path)
|
|
486
|
+
|
|
487
|
+
def get_log(self):
|
|
488
|
+
"""If log files for the simulation exist, this function returns them as a LF1 class object
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
floodmodeller_api.LF1 class object
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
suffix, steady = self._determine_lf_type()
|
|
495
|
+
|
|
496
|
+
# Get lf location
|
|
497
|
+
lf_path = self._get_result_filepath(suffix)
|
|
498
|
+
|
|
499
|
+
if not lf_path.exists():
|
|
500
|
+
raise FileNotFoundError("Log file (" + suffix + ") not found")
|
|
501
|
+
|
|
502
|
+
return lf_factory(lf_path, suffix, steady)
|
|
503
|
+
|
|
504
|
+
def _determine_lf_type(self): # (str, bool) or (None, None):
|
|
505
|
+
"""Determine the log file type"""
|
|
506
|
+
|
|
507
|
+
if self.RunType == "Unsteady":
|
|
508
|
+
suffix = "lf1"
|
|
509
|
+
steady = False
|
|
510
|
+
|
|
511
|
+
elif self.RunType == "Steady":
|
|
512
|
+
suffix = "lf1"
|
|
513
|
+
steady = True
|
|
514
|
+
|
|
515
|
+
else:
|
|
516
|
+
raise ValueError(f'Unexpected run type "{self.RunType}"')
|
|
517
|
+
|
|
518
|
+
return suffix, steady
|
|
519
|
+
|
|
520
|
+
def _init_log_file(self):
|
|
521
|
+
"""Checks for a new log file, waiting for its creation if necessary"""
|
|
522
|
+
|
|
523
|
+
# determine log file type based on self.RunType
|
|
524
|
+
try:
|
|
525
|
+
suffix, steady = self._determine_lf_type()
|
|
526
|
+
except ValueError:
|
|
527
|
+
self._no_log_file(f'run type "{self.RunType}" not supported')
|
|
528
|
+
self._lf = None
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
# ensure progress bar is supported for that type
|
|
532
|
+
if not (suffix == "lf1" and steady is False):
|
|
533
|
+
self._no_log_file("only 1D unsteady runs are supported")
|
|
534
|
+
self._lf = None
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
# find what log filepath should be
|
|
538
|
+
lf_filepath = self._get_result_filepath(suffix)
|
|
539
|
+
|
|
540
|
+
# wait for log file to exist
|
|
541
|
+
log_file_exists = False
|
|
542
|
+
max_time = time.time() + self.LOG_TIMEOUT
|
|
543
|
+
|
|
544
|
+
while not log_file_exists:
|
|
545
|
+
time.sleep(0.1)
|
|
546
|
+
|
|
547
|
+
log_file_exists = lf_filepath.is_file()
|
|
548
|
+
|
|
549
|
+
# timeout
|
|
550
|
+
if time.time() > max_time:
|
|
551
|
+
self._no_log_file("log file is expected but not detected")
|
|
552
|
+
self._lf = None
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
# wait for new log file
|
|
556
|
+
old_log_file = True
|
|
557
|
+
max_time = time.time() + self.LOG_TIMEOUT
|
|
558
|
+
|
|
559
|
+
while old_log_file:
|
|
560
|
+
time.sleep(0.1)
|
|
561
|
+
|
|
562
|
+
# difference between now and when log file was last modified
|
|
563
|
+
last_modified_timestamp = lf_filepath.stat().st_mtime
|
|
564
|
+
last_modified = dt.datetime.fromtimestamp(last_modified_timestamp)
|
|
565
|
+
time_diff_sec = (dt.datetime.now() - last_modified).total_seconds()
|
|
566
|
+
|
|
567
|
+
# it's old if it's over self.OLD_FILE seconds old (TODO: is this robust?)
|
|
568
|
+
old_log_file = time_diff_sec > self.OLD_FILE
|
|
569
|
+
|
|
570
|
+
# timeout
|
|
571
|
+
if time.time() > max_time:
|
|
572
|
+
self._no_log_file("log file is from previous run")
|
|
573
|
+
self._lf = None
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
# create LF instance
|
|
577
|
+
self._lf = lf_factory(lf_filepath, suffix, steady)
|
|
578
|
+
|
|
579
|
+
def _no_log_file(self, reason):
|
|
580
|
+
"""Warning that there will be no progress bar"""
|
|
581
|
+
|
|
582
|
+
print("No progress bar as " + reason + ". Simulation will continue as usual.")
|
|
583
|
+
|
|
584
|
+
def _update_progress_bar(self, process: Popen):
|
|
585
|
+
"""Updates progress bar based on log file"""
|
|
586
|
+
|
|
587
|
+
# only if there is a log file
|
|
588
|
+
if self._lf is None:
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
# tqdm progress bar
|
|
592
|
+
for i in self._range_function(100, **self._range_settings):
|
|
593
|
+
# Process still running
|
|
594
|
+
while process.poll() is None:
|
|
595
|
+
time.sleep(0.1)
|
|
596
|
+
|
|
597
|
+
# Find progress
|
|
598
|
+
self._lf.read(suppress_final_step=True)
|
|
599
|
+
progress = self._lf.report_progress()
|
|
600
|
+
|
|
601
|
+
# Reached i% progress => move onto waiting for (i+1)%
|
|
602
|
+
if progress > i:
|
|
603
|
+
break
|
|
604
|
+
|
|
605
|
+
# Process stopped
|
|
606
|
+
if process.poll() is not None:
|
|
607
|
+
# Find final progress
|
|
608
|
+
self._lf.read(suppress_final_step=True)
|
|
609
|
+
progress = self._lf.report_progress()
|
|
610
|
+
|
|
611
|
+
if progress > i:
|
|
612
|
+
pass # stopped because it completed
|
|
613
|
+
else:
|
|
614
|
+
break # stopped for another reason
|
|
615
|
+
|
|
616
|
+
def _summarise_exy(self):
|
|
617
|
+
"""Reads and summarises associated exy file if available"""
|
|
618
|
+
|
|
619
|
+
# Get results location
|
|
620
|
+
if hasattr(self, "Results"):
|
|
621
|
+
exy_path = Path(self.Results).with_suffix(".exy")
|
|
622
|
+
if not exy_path.is_absolute():
|
|
623
|
+
# set cwd to ief location and resolve path
|
|
624
|
+
exy_path = Path(self._filepath.parent, exy_path).resolve()
|
|
625
|
+
|
|
626
|
+
else:
|
|
627
|
+
exy_path = self._filepath.with_suffix(".exy")
|
|
628
|
+
|
|
629
|
+
if not exy_path.exists():
|
|
630
|
+
raise FileNotFoundError("Simulation results error log (.exy) not found")
|
|
631
|
+
|
|
632
|
+
exy_data = pd.read_csv(exy_path, names=["node", "timestep", "severity", "code", "summary"])
|
|
633
|
+
exy_data["type"] = exy_data["code"].apply(
|
|
634
|
+
lambda x: (
|
|
635
|
+
"Error" if x < self.ERROR_MAX else ("Warning" if x < self.WARNING_MAX else "Note")
|
|
636
|
+
),
|
|
637
|
+
)
|
|
638
|
+
errors = len(exy_data[exy_data["type"] == "Error"])
|
|
639
|
+
warnings = len(exy_data[exy_data["type"] == "Warning"])
|
|
640
|
+
notes = len(exy_data[exy_data["type"] == "Note"])
|
|
641
|
+
|
|
642
|
+
details = f"({errors} Error(s), {warnings} Warning(s), {notes} Note(s) ) - Check ZZD for more details."
|
|
643
|
+
|
|
644
|
+
if errors > 0:
|
|
645
|
+
return 1, f"Simulation Failed! - {details}"
|
|
646
|
+
|
|
647
|
+
return 0, f"Simulation Completed! - {details}"
|