floodmodeller-api 0.5.0.post1__py3-none-any.whl → 0.5.2__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 +11 -1
- floodmodeller_api/_base.py +55 -36
- floodmodeller_api/backup.py +15 -12
- floodmodeller_api/dat.py +191 -121
- floodmodeller_api/diff.py +4 -4
- floodmodeller_api/hydrology_plus/hydrology_plus_export.py +15 -14
- floodmodeller_api/ied.py +8 -10
- floodmodeller_api/ief.py +56 -42
- floodmodeller_api/ief_flags.py +1 -1
- floodmodeller_api/inp.py +7 -10
- floodmodeller_api/logs/lf.py +25 -26
- floodmodeller_api/logs/lf_helpers.py +20 -20
- floodmodeller_api/logs/lf_params.py +1 -5
- floodmodeller_api/mapping.py +11 -2
- floodmodeller_api/test/__init__.py +2 -2
- floodmodeller_api/test/conftest.py +2 -3
- floodmodeller_api/test/test_backup.py +2 -2
- floodmodeller_api/test/test_conveyance.py +13 -7
- floodmodeller_api/test/test_dat.py +168 -20
- floodmodeller_api/test/test_data/EX18_DAT_expected.json +164 -144
- floodmodeller_api/test/test_data/EX3_DAT_expected.json +6 -2
- floodmodeller_api/test/test_data/EX6_DAT_expected.json +12 -46
- floodmodeller_api/test/test_data/encoding_test_cp1252.dat +1081 -0
- floodmodeller_api/test/test_data/encoding_test_utf8.dat +1081 -0
- floodmodeller_api/test/test_data/integrated_bridge/AR_NoSP_NoBl_2O_NO_OneFRC.ied +33 -0
- floodmodeller_api/test/test_data/integrated_bridge/AR_vSP_25pc_1O.ied +32 -0
- floodmodeller_api/test/test_data/integrated_bridge/PL_vSP_25pc_1O.ied +34 -0
- floodmodeller_api/test/test_data/integrated_bridge/SBTwoFRCsStaggered.IED +32 -0
- floodmodeller_api/test/test_data/integrated_bridge/US_NoSP_NoBl_OR_RN.ied +28 -0
- floodmodeller_api/test/test_data/integrated_bridge/US_SP_NoBl_OR_frc_PT2-5_RN.ied +34 -0
- floodmodeller_api/test/test_data/integrated_bridge/US_fSP_NoBl_1O.ied +30 -0
- floodmodeller_api/test/test_data/integrated_bridge/US_nSP_NoBl_1O.ied +49 -0
- floodmodeller_api/test/test_data/integrated_bridge/US_vSP_NoBl_2O_Para.ied +35 -0
- floodmodeller_api/test/test_data/integrated_bridge.dat +40 -0
- floodmodeller_api/test/test_data/network.ied +2 -2
- floodmodeller_api/test/test_data/network_dat_expected.json +141 -243
- floodmodeller_api/test/test_data/network_ied_expected.json +2 -2
- floodmodeller_api/test/test_data/network_with_comments.ied +2 -2
- floodmodeller_api/test/test_data/structure_logs/EX17_expected.csv +4 -0
- floodmodeller_api/test/test_data/structure_logs/EX17_expected.json +69 -0
- floodmodeller_api/test/test_data/structure_logs/EX18_expected.csv +20 -0
- floodmodeller_api/test/test_data/structure_logs/EX18_expected.json +292 -0
- floodmodeller_api/test/test_data/structure_logs/EX6_expected.csv +4 -0
- floodmodeller_api/test/test_data/structure_logs/EX6_expected.json +35 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_flow.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_fr.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_mode.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_stage.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_state.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_velocity.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_left_fp_h.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_left_fp_mode.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_link_inflow.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_max.csv +87 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_right_fp_h.csv +182 -0
- floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_right_fp_mode.csv +182 -0
- floodmodeller_api/test/test_flowtimeprofile.py +2 -2
- floodmodeller_api/test/test_hydrology_plus_export.py +4 -2
- floodmodeller_api/test/test_ied.py +3 -3
- floodmodeller_api/test/test_ief.py +12 -4
- floodmodeller_api/test/test_inp.py +2 -2
- floodmodeller_api/test/test_integrated_bridge.py +159 -0
- floodmodeller_api/test/test_json.py +14 -13
- floodmodeller_api/test/test_logs_lf.py +50 -29
- floodmodeller_api/test/test_read_file.py +1 -0
- floodmodeller_api/test/test_river.py +12 -12
- floodmodeller_api/test/test_tool.py +8 -5
- floodmodeller_api/test/test_toolbox_structure_log.py +148 -158
- floodmodeller_api/test/test_xml2d.py +14 -16
- floodmodeller_api/test/test_zz.py +143 -0
- floodmodeller_api/to_from_json.py +9 -9
- floodmodeller_api/tool.py +15 -11
- floodmodeller_api/toolbox/example_tool.py +5 -1
- floodmodeller_api/toolbox/model_build/add_siltation_definition.py +13 -9
- floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +500 -194
- floodmodeller_api/toolbox/model_build/structure_log_definition.py +5 -1
- floodmodeller_api/units/__init__.py +15 -0
- floodmodeller_api/units/_base.py +87 -20
- floodmodeller_api/units/_helpers.py +343 -0
- floodmodeller_api/units/boundaries.py +59 -71
- floodmodeller_api/units/comment.py +1 -1
- floodmodeller_api/units/conduits.py +57 -54
- floodmodeller_api/units/connectors.py +112 -0
- floodmodeller_api/units/controls.py +107 -0
- floodmodeller_api/units/conveyance.py +1 -1
- floodmodeller_api/units/iic.py +2 -9
- floodmodeller_api/units/losses.py +44 -45
- floodmodeller_api/units/sections.py +52 -51
- floodmodeller_api/units/structures.py +361 -531
- floodmodeller_api/units/units.py +27 -26
- floodmodeller_api/units/unsupported.py +5 -7
- floodmodeller_api/units/variables.py +2 -2
- floodmodeller_api/urban1d/_base.py +13 -17
- floodmodeller_api/urban1d/conduits.py +11 -21
- floodmodeller_api/urban1d/general_parameters.py +1 -1
- floodmodeller_api/urban1d/junctions.py +7 -11
- floodmodeller_api/urban1d/losses.py +13 -17
- floodmodeller_api/urban1d/outfalls.py +18 -22
- floodmodeller_api/urban1d/raingauges.py +5 -10
- floodmodeller_api/urban1d/subsections.py +5 -4
- floodmodeller_api/urban1d/xsections.py +14 -17
- floodmodeller_api/util.py +23 -6
- floodmodeller_api/validation/parameters.py +7 -3
- floodmodeller_api/validation/urban_parameters.py +1 -4
- floodmodeller_api/validation/validation.py +11 -5
- floodmodeller_api/version.py +1 -1
- floodmodeller_api/xml2d.py +27 -31
- floodmodeller_api/xml2d_template.py +1 -1
- floodmodeller_api/zz.py +539 -0
- {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/LICENSE.txt +1 -1
- {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/METADATA +30 -16
- {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/RECORD +116 -83
- {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/WHEEL +1 -1
- floodmodeller_api/test/test_zzn.py +0 -36
- floodmodeller_api/units/helpers.py +0 -123
- floodmodeller_api/zzn.py +0 -414
- /floodmodeller_api/test/test_data/{network_from_tabularCSV.csv → tabular_csv_outputs/network_zzn_max.csv} +0 -0
- {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/entry_points.txt +0 -0
- {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/top_level.txt +0 -0
floodmodeller_api/ief.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Flood Modeller Python API
|
|
3
|
-
Copyright (C)
|
|
3
|
+
Copyright (C) 2025 Jacobs U.K. Limited
|
|
4
4
|
|
|
5
5
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
|
|
6
6
|
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
@@ -17,7 +17,7 @@ address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import csv
|
|
20
|
-
import
|
|
20
|
+
import logging
|
|
21
21
|
import subprocess
|
|
22
22
|
import time
|
|
23
23
|
from io import StringIO
|
|
@@ -33,19 +33,28 @@ from .diff import check_item_with_dataframe_equal
|
|
|
33
33
|
from .ief_flags import flags
|
|
34
34
|
from .logs import LF1, create_lf
|
|
35
35
|
from .to_from_json import Jsonable
|
|
36
|
-
from .util import handle_exception
|
|
37
|
-
from .
|
|
36
|
+
from .util import handle_exception, is_windows
|
|
37
|
+
from .zz import ZZN
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def try_converting(value: str) -> str | int | float:
|
|
41
41
|
"""Attempt to parse value as float or int if valid, else return the original string"""
|
|
42
42
|
try:
|
|
43
43
|
return int(value)
|
|
44
44
|
except ValueError:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
return float(value)
|
|
49
|
+
except ValueError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
if not is_windows() and "\\" in value:
|
|
53
|
+
# backslashes aren't valid in paths
|
|
54
|
+
logging.info("Changing '\\' in '%s' to '/' to be valid in Linux.", value)
|
|
55
|
+
return value.replace("\\", "/")
|
|
56
|
+
|
|
57
|
+
return value
|
|
49
58
|
|
|
50
59
|
|
|
51
60
|
class IEF(FMFile):
|
|
@@ -75,14 +84,14 @@ class IEF(FMFile):
|
|
|
75
84
|
if ief_filepath is not None:
|
|
76
85
|
FMFile.__init__(self, ief_filepath)
|
|
77
86
|
self._read()
|
|
78
|
-
self._log_path = self.
|
|
87
|
+
self._log_path = self._filepath.with_suffix(".lf1")
|
|
79
88
|
else:
|
|
80
89
|
self._create_from_blank()
|
|
81
90
|
|
|
82
91
|
def _read(self):
|
|
83
92
|
# Read IEF data
|
|
84
|
-
with open(self._filepath) as ief_file:
|
|
85
|
-
raw_data = [line.rstrip("\n") for line in ief_file
|
|
93
|
+
with open(self._filepath, encoding=self.ENCODING) as ief_file:
|
|
94
|
+
raw_data = [line.rstrip("\n") for line in ief_file]
|
|
86
95
|
# Clean data and add as class properties
|
|
87
96
|
# Create a list to store the properties which are to be saved in IEF, so as to ignore any temp properties.
|
|
88
97
|
prev_comment = None
|
|
@@ -117,7 +126,7 @@ class IEF(FMFile):
|
|
|
117
126
|
self._ief_properties.append(prop)
|
|
118
127
|
else:
|
|
119
128
|
# Sets the property and value as class properties so they can be edited.
|
|
120
|
-
setattr(self, prop,
|
|
129
|
+
setattr(self, prop, try_converting(value))
|
|
121
130
|
self._ief_properties.append(prop)
|
|
122
131
|
prev_comment = None
|
|
123
132
|
else:
|
|
@@ -170,7 +179,7 @@ class IEF(FMFile):
|
|
|
170
179
|
# Add multiple EventData if present
|
|
171
180
|
for idx, key in enumerate(event_data):
|
|
172
181
|
if idx == event_index:
|
|
173
|
-
ief_string += f";{key}\nEventData{eq}{
|
|
182
|
+
ief_string += f";{key}\nEventData{eq}{event_data[key]!s}\n"
|
|
174
183
|
break
|
|
175
184
|
event_index += 1
|
|
176
185
|
|
|
@@ -181,7 +190,7 @@ class IEF(FMFile):
|
|
|
181
190
|
|
|
182
191
|
else:
|
|
183
192
|
# writes property and value to ief string
|
|
184
|
-
ief_string += f"{prop}{eq}{
|
|
193
|
+
ief_string += f"{prop}{eq}{getattr(self, prop)!s}\n"
|
|
185
194
|
|
|
186
195
|
return ief_string
|
|
187
196
|
|
|
@@ -209,7 +218,7 @@ class IEF(FMFile):
|
|
|
209
218
|
if "=" in line:
|
|
210
219
|
prop, value = line.split("=")
|
|
211
220
|
# Sets the property and value as class properties so they can be edited.
|
|
212
|
-
setattr(self, prop,
|
|
221
|
+
setattr(self, prop, try_converting(value))
|
|
213
222
|
self._ief_properties.append(prop)
|
|
214
223
|
else:
|
|
215
224
|
# This should add the [] bound headers
|
|
@@ -227,8 +236,9 @@ class IEF(FMFile):
|
|
|
227
236
|
):
|
|
228
237
|
# Check if valid flag
|
|
229
238
|
if prop.upper() not in flags:
|
|
230
|
-
|
|
231
|
-
|
|
239
|
+
logging.warning(
|
|
240
|
+
"'%s' is not a valid IEF flag, it will be ommited from the IEF\n",
|
|
241
|
+
prop,
|
|
232
242
|
)
|
|
233
243
|
continue
|
|
234
244
|
|
|
@@ -279,10 +289,11 @@ class IEF(FMFile):
|
|
|
279
289
|
def _update_eventdata_info(self): # noqa: C901
|
|
280
290
|
if not isinstance(self.eventdata, dict):
|
|
281
291
|
# If attribute not a dict, adds the value as a single entry in list
|
|
282
|
-
|
|
292
|
+
msg = (
|
|
283
293
|
"The 'EventData' attribute should be a dictionary with keys defining the event"
|
|
284
|
-
" names and values referencing the IED files"
|
|
294
|
+
" names and values referencing the IED files"
|
|
285
295
|
)
|
|
296
|
+
raise AttributeError(msg)
|
|
286
297
|
|
|
287
298
|
# Number of 'EventData' flags in ief
|
|
288
299
|
event_properties = self._ief_properties.count("EventData")
|
|
@@ -328,11 +339,12 @@ class IEF(FMFile):
|
|
|
328
339
|
try:
|
|
329
340
|
self.NoOfFlowTimeSeries = sum([ftp.count_series() for ftp in self.flowtimeprofiles])
|
|
330
341
|
except FileNotFoundError as err:
|
|
331
|
-
|
|
342
|
+
msg = (
|
|
332
343
|
"Failed to read csv referenced in flowtimeprofile, file either does not exist or is"
|
|
333
344
|
"unable to be found due to relative path from IEF file. NoOfFlowTimeSeries has not"
|
|
334
|
-
"been updated."
|
|
335
|
-
)
|
|
345
|
+
"been updated."
|
|
346
|
+
)
|
|
347
|
+
raise UserWarning(msg) from err
|
|
336
348
|
|
|
337
349
|
end_index = None
|
|
338
350
|
start_index = (
|
|
@@ -434,6 +446,7 @@ class IEF(FMFile):
|
|
|
434
446
|
filepath (string): Full filepath to new location for ief file (including '.ief' extension)
|
|
435
447
|
"""
|
|
436
448
|
self._save(filepath)
|
|
449
|
+
self._log_path = self._filepath.with_suffix(".lf1")
|
|
437
450
|
|
|
438
451
|
@handle_exception(when="simulate")
|
|
439
452
|
def simulate( # noqa: C901, PLR0912, PLR0913
|
|
@@ -471,9 +484,8 @@ class IEF(FMFile):
|
|
|
471
484
|
self._range_function = range_function
|
|
472
485
|
self._range_settings = range_settings if range_settings else {}
|
|
473
486
|
if self._filepath is None:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
)
|
|
487
|
+
msg = "IEF must be saved to a specific filepath before simulate() can be called."
|
|
488
|
+
raise UserWarning(msg)
|
|
477
489
|
if precision.upper() == "DEFAULT":
|
|
478
490
|
precision = "SINGLE" # Defaults to single...
|
|
479
491
|
for attr in dir(self):
|
|
@@ -489,9 +501,8 @@ class IEF(FMFile):
|
|
|
489
501
|
else:
|
|
490
502
|
_enginespath = enginespath
|
|
491
503
|
if not Path(_enginespath).exists():
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
)
|
|
504
|
+
msg = f"Flood Modeller non-default engine path not found! {_enginespath!s}"
|
|
505
|
+
raise Exception(msg)
|
|
495
506
|
|
|
496
507
|
if precision.upper() == "SINGLE":
|
|
497
508
|
isis32_fp = str(Path(_enginespath, "ISISf32.exe"))
|
|
@@ -499,14 +510,15 @@ class IEF(FMFile):
|
|
|
499
510
|
isis32_fp = str(Path(_enginespath, "ISISf32_DoubleP.exe"))
|
|
500
511
|
|
|
501
512
|
if not Path(isis32_fp).exists():
|
|
502
|
-
|
|
513
|
+
msg = f"Flood Modeller engine not found! Expected location: {isis32_fp}"
|
|
514
|
+
raise Exception(msg)
|
|
503
515
|
|
|
504
|
-
run_command = f'"{isis32_fp}" -sd "{self._filepath}"'
|
|
516
|
+
run_command = f'"{isis32_fp}" -sd "{self._filepath.resolve()}"'
|
|
505
517
|
|
|
506
518
|
if method.upper() == "WAIT":
|
|
507
|
-
|
|
519
|
+
logging.info("Executing simulation...")
|
|
508
520
|
# execute simulation
|
|
509
|
-
process = Popen(run_command, cwd=
|
|
521
|
+
process = Popen(run_command, cwd=Path(self._filepath).parent)
|
|
510
522
|
|
|
511
523
|
# progress bar based on log files
|
|
512
524
|
steady = self.RunType == "Steady"
|
|
@@ -521,12 +533,12 @@ class IEF(FMFile):
|
|
|
521
533
|
|
|
522
534
|
if result == 1 and raise_on_failure:
|
|
523
535
|
raise RuntimeError(summary)
|
|
524
|
-
|
|
536
|
+
logging.info(summary)
|
|
525
537
|
|
|
526
538
|
elif method.upper() == "RETURN_PROCESS":
|
|
527
|
-
|
|
539
|
+
logging.info("Executing simulation...")
|
|
528
540
|
# execute simulation
|
|
529
|
-
return Popen(run_command, cwd=
|
|
541
|
+
return Popen(run_command, cwd=Path(self._filepath).parent)
|
|
530
542
|
|
|
531
543
|
return None
|
|
532
544
|
|
|
@@ -553,7 +565,8 @@ class IEF(FMFile):
|
|
|
553
565
|
result_path = self._get_result_filepath(suffix="zzn")
|
|
554
566
|
|
|
555
567
|
if not result_path.exists():
|
|
556
|
-
|
|
568
|
+
msg = "Simulation results file (zzn) not found"
|
|
569
|
+
raise FileNotFoundError(msg)
|
|
557
570
|
|
|
558
571
|
return ZZN(result_path)
|
|
559
572
|
|
|
@@ -565,7 +578,8 @@ class IEF(FMFile):
|
|
|
565
578
|
"""
|
|
566
579
|
|
|
567
580
|
if not self._log_path.exists():
|
|
568
|
-
|
|
581
|
+
msg = "Log file (LF1) not found"
|
|
582
|
+
raise FileNotFoundError(msg)
|
|
569
583
|
|
|
570
584
|
steady = self.RunType == "Steady"
|
|
571
585
|
return LF1(self._log_path, steady)
|
|
@@ -616,7 +630,8 @@ class IEF(FMFile):
|
|
|
616
630
|
exy_path = self._filepath.with_suffix(".exy")
|
|
617
631
|
|
|
618
632
|
if not exy_path.exists():
|
|
619
|
-
|
|
633
|
+
msg = "Simulation results error log (.exy) not found"
|
|
634
|
+
raise FileNotFoundError(msg)
|
|
620
635
|
|
|
621
636
|
exy_data = pd.read_csv(exy_path, names=["node", "timestep", "severity", "code", "summary"])
|
|
622
637
|
exy_data["type"] = exy_data["code"].apply(
|
|
@@ -678,9 +693,8 @@ class FlowTimeProfile(Jsonable):
|
|
|
678
693
|
self.profile = kwargs.get("profile", "")
|
|
679
694
|
self.comment = kwargs.get("comment", "")
|
|
680
695
|
else:
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
)
|
|
696
|
+
msg = "You must provide either a single raw string argument or keyword arguments."
|
|
697
|
+
raise ValueError(msg)
|
|
684
698
|
|
|
685
699
|
base_path = Path(kwargs.get("ief_filepath", ""))
|
|
686
700
|
self._csvfile = (base_path / self.csv_filepath.strip('"')).resolve()
|
floodmodeller_api/ief_flags.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Flood Modeller Python API
|
|
3
|
-
Copyright (C)
|
|
3
|
+
Copyright (C) 2025 Jacobs U.K. Limited
|
|
4
4
|
|
|
5
5
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
|
|
6
6
|
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
floodmodeller_api/inp.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Flood Modeller Python API
|
|
3
|
-
Copyright (C)
|
|
3
|
+
Copyright (C) 2025 Jacobs U.K. Limited
|
|
4
4
|
|
|
5
5
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
|
|
6
6
|
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
@@ -20,7 +20,7 @@ from pathlib import Path
|
|
|
20
20
|
|
|
21
21
|
from . import units
|
|
22
22
|
from ._base import FMFile
|
|
23
|
-
from .units.
|
|
23
|
+
from .units._helpers import join_n_char_ljust, to_str
|
|
24
24
|
from .urban1d import subsections
|
|
25
25
|
from .urban1d.general_parameters import DEFAULT_OPTIONS
|
|
26
26
|
from .util import handle_exception
|
|
@@ -59,8 +59,8 @@ class INP(FMFile):
|
|
|
59
59
|
|
|
60
60
|
def _read(self):
|
|
61
61
|
# Read INP file
|
|
62
|
-
with open(self._filepath) as inp_file:
|
|
63
|
-
self._raw_data = [line.rstrip("\n") for line in inp_file
|
|
62
|
+
with open(self._filepath, encoding=self.ENCODING) as inp_file:
|
|
63
|
+
self._raw_data = [line.rstrip("\n") for line in inp_file]
|
|
64
64
|
|
|
65
65
|
# Generate INP file structure
|
|
66
66
|
self._update_inp_struct()
|
|
@@ -129,9 +129,8 @@ class INP(FMFile):
|
|
|
129
129
|
return "\n".join(self._raw_data) + "\n"
|
|
130
130
|
|
|
131
131
|
def _create_from_blank(self):
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
132
|
+
msg = "Creating new 1D urban models (INP files) is not yet supported by floodmodeller_api, only existing models can be read"
|
|
133
|
+
raise NotImplementedError(msg)
|
|
135
134
|
|
|
136
135
|
def _get_section_definitions(self):
|
|
137
136
|
"""Internal method used to get section definitions for each supported unit type and general parameters."""
|
|
@@ -159,7 +158,7 @@ class INP(FMFile):
|
|
|
159
158
|
data = units.helpers.split_n_char(line, 21)
|
|
160
159
|
|
|
161
160
|
# Set type to Float or Stirng, as appropirate.
|
|
162
|
-
self.options[data[0].lower()] =
|
|
161
|
+
self.options[data[0].lower()] = to_str(
|
|
163
162
|
data[1],
|
|
164
163
|
None,
|
|
165
164
|
check_float=True,
|
|
@@ -198,8 +197,6 @@ class INP(FMFile):
|
|
|
198
197
|
in_block = False
|
|
199
198
|
unit_block = {}
|
|
200
199
|
for idx, line in enumerate(self._raw_data):
|
|
201
|
-
# TODO: Add functionality to compare first four characters only (alphanumeric) - need to consider names shorter than 4 characters, and those with _ within name
|
|
202
|
-
|
|
203
200
|
# Check if subsection is known
|
|
204
201
|
if line.upper() in subsections.ALL_SUBSECTIONS:
|
|
205
202
|
if in_block is True:
|
floodmodeller_api/logs/lf.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Flood Modeller Python API
|
|
3
|
-
Copyright (C)
|
|
3
|
+
Copyright (C) 2025 Jacobs U.K. Limited
|
|
4
4
|
|
|
5
5
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
|
|
6
6
|
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
@@ -17,6 +17,7 @@ address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import datetime as dt
|
|
20
|
+
import logging
|
|
20
21
|
import time
|
|
21
22
|
from typing import TYPE_CHECKING
|
|
22
23
|
|
|
@@ -55,7 +56,7 @@ class LF(FMFile):
|
|
|
55
56
|
def __init__(
|
|
56
57
|
self,
|
|
57
58
|
lf_filepath: str | Path | None,
|
|
58
|
-
data_to_extract: dict,
|
|
59
|
+
data_to_extract: dict[str, dict],
|
|
59
60
|
steady: bool = False,
|
|
60
61
|
):
|
|
61
62
|
FMFile.__init__(self, lf_filepath)
|
|
@@ -70,7 +71,7 @@ class LF(FMFile):
|
|
|
70
71
|
def _read(self, force_reread: bool = False, suppress_final_step: bool = False):
|
|
71
72
|
# Read LF file
|
|
72
73
|
with open(self._filepath) as lf_file:
|
|
73
|
-
self._raw_data = [line.rstrip("\n") for line in lf_file
|
|
74
|
+
self._raw_data = [line.rstrip("\n") for line in lf_file]
|
|
74
75
|
|
|
75
76
|
# Force rereading from start of file
|
|
76
77
|
if force_reread is True:
|
|
@@ -140,17 +141,12 @@ class LF(FMFile):
|
|
|
140
141
|
def _get_index(self):
|
|
141
142
|
"""Finds key and dataframe for variable that is the index"""
|
|
142
143
|
|
|
143
|
-
for
|
|
144
|
-
|
|
145
|
-
self.
|
|
146
|
-
index_key = key
|
|
147
|
-
index_df = self._extracted_data[key].data.get_value()
|
|
148
|
-
return index_key, index_df
|
|
149
|
-
|
|
150
|
-
except KeyError:
|
|
151
|
-
pass
|
|
144
|
+
for k, v in self._data_to_extract.items():
|
|
145
|
+
if "is_index" in v:
|
|
146
|
+
return k, self._extracted_data[k].data.get_value()
|
|
152
147
|
|
|
153
|
-
|
|
148
|
+
msg = "No index variable found"
|
|
149
|
+
raise Exception(msg)
|
|
154
150
|
|
|
155
151
|
def _set_attributes(self):
|
|
156
152
|
"""Makes each Parser value an attribute; "last" values in dictionary"""
|
|
@@ -180,7 +176,7 @@ class LF(FMFile):
|
|
|
180
176
|
|
|
181
177
|
delattr(self, "info")
|
|
182
178
|
|
|
183
|
-
def to_dataframe(self, *, include_tuflow: bool = False) -> pd.DataFrame:
|
|
179
|
+
def to_dataframe(self, variable: str = "all", *, include_tuflow: bool = False) -> pd.DataFrame:
|
|
184
180
|
"""Collects parameter values that change throughout simulation into a dataframe
|
|
185
181
|
|
|
186
182
|
Args:
|
|
@@ -190,20 +186,23 @@ class LF(FMFile):
|
|
|
190
186
|
pd.DataFrame: DataFrame of log file parameters indexed by simulation time (unsteady) or network iterations (steady)
|
|
191
187
|
"""
|
|
192
188
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
data_type_all = {
|
|
189
|
+
lf_df_data = {
|
|
196
190
|
k: getattr(self, k)
|
|
197
191
|
for k, v in self._data_to_extract.items()
|
|
198
|
-
if v["data_type"] == "all"
|
|
192
|
+
if v["data_type"] == "all" # with entries every iteration
|
|
193
|
+
and (include_tuflow or "tuflow" not in k) # tuflow-related only if requested
|
|
194
|
+
and (variable in ("all", k, *v.get("subheaders", []))) # if it or all are requested
|
|
199
195
|
}
|
|
200
196
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
df.sort_index(inplace=True)
|
|
197
|
+
if lf_df_data == {}:
|
|
198
|
+
msg = f"No data extracted for variable '{variable}'"
|
|
199
|
+
raise ValueError(msg)
|
|
205
200
|
|
|
206
|
-
|
|
201
|
+
lf_df = pd.concat(lf_df_data, axis=1)
|
|
202
|
+
lf_df.columns = lf_df.columns.droplevel()
|
|
203
|
+
if variable != "all":
|
|
204
|
+
lf_df = lf_df[variable] # otherwise subheaders result in extra columns
|
|
205
|
+
return lf_df.sort_index()
|
|
207
206
|
|
|
208
207
|
def _sync_cols(self):
|
|
209
208
|
"""Ensures Parser values (of type "all") have an entry each iteration"""
|
|
@@ -224,7 +223,7 @@ class LF(FMFile):
|
|
|
224
223
|
def _print_no_lines(self):
|
|
225
224
|
"""Prints number of lines that have been read so far"""
|
|
226
225
|
|
|
227
|
-
|
|
226
|
+
logging.info("Last line read: %s", self._no_lines)
|
|
228
227
|
|
|
229
228
|
def report_progress(self) -> float:
|
|
230
229
|
"""Returns progress for unsteady simulations
|
|
@@ -323,7 +322,7 @@ def create_lf(filepath: Path, suffix: str) -> LF1 | LF2 | None:
|
|
|
323
322
|
"""Checks for a new log file, waiting for its creation if necessary"""
|
|
324
323
|
|
|
325
324
|
def _no_log_file(reason: str) -> None:
|
|
326
|
-
|
|
325
|
+
logging.warning("No progress bar as %s. Simulation will continue as usual.", reason)
|
|
327
326
|
|
|
328
327
|
# ensure progress bar is supported
|
|
329
328
|
if suffix not in {"lf1", "lf2"}:
|
|
@@ -356,7 +355,7 @@ def create_lf(filepath: Path, suffix: str) -> LF1 | LF2 | None:
|
|
|
356
355
|
last_modified = dt.datetime.fromtimestamp(last_modified_timestamp)
|
|
357
356
|
time_diff_sec = (dt.datetime.now() - last_modified).total_seconds()
|
|
358
357
|
|
|
359
|
-
# it's old if it's over OLD_FILE seconds old
|
|
358
|
+
# it's old if it's over OLD_FILE seconds old
|
|
360
359
|
old_log_file = time_diff_sec > OLD_FILE
|
|
361
360
|
|
|
362
361
|
# timeout
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Flood Modeller Python API
|
|
3
|
-
Copyright (C)
|
|
3
|
+
Copyright (C) 2025 Jacobs U.K. Limited
|
|
4
4
|
|
|
5
5
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
|
|
6
6
|
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
@@ -64,42 +64,40 @@ class AllData(Data):
|
|
|
64
64
|
index_key: str | None = None,
|
|
65
65
|
index_df: pd.Series | None = None,
|
|
66
66
|
) -> pd.DataFrame:
|
|
67
|
-
|
|
67
|
+
value_df = pd.DataFrame(self._value)
|
|
68
68
|
|
|
69
69
|
# do nothing to empty dataframes
|
|
70
|
-
if
|
|
71
|
-
return
|
|
70
|
+
if value_df.empty:
|
|
71
|
+
return value_df
|
|
72
72
|
|
|
73
73
|
# overall header
|
|
74
74
|
if self._subheaders is None:
|
|
75
|
-
|
|
75
|
+
value_df = value_df.rename(columns={value_df.columns[0]: self.header})
|
|
76
76
|
|
|
77
77
|
elif index_key is not None:
|
|
78
78
|
# subheaders
|
|
79
|
-
|
|
79
|
+
value_df = value_df.set_axis(self._subheaders, axis=1)
|
|
80
80
|
|
|
81
81
|
# remove duplicate of index
|
|
82
82
|
# sometimes it includes extra values
|
|
83
83
|
# it also has different precision
|
|
84
84
|
index_duplicate = index_key + "_duplicate"
|
|
85
|
-
if index_duplicate in
|
|
85
|
+
if index_duplicate in value_df.columns:
|
|
86
86
|
try:
|
|
87
|
-
index_df =
|
|
88
|
-
|
|
87
|
+
index_df = value_df[index_duplicate].dt.round("1s")
|
|
88
|
+
value_df = value_df.drop(index_duplicate, axis=1)
|
|
89
89
|
except AttributeError:
|
|
90
|
-
|
|
90
|
+
value_df = value_df.drop(columns=index_duplicate)
|
|
91
91
|
|
|
92
92
|
# there is no index because *this* is the index
|
|
93
93
|
if index_key is None:
|
|
94
|
-
return
|
|
94
|
+
return value_df
|
|
95
95
|
|
|
96
96
|
# made lf index the dataframe index
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return df
|
|
97
|
+
value_df[index_key] = index_df
|
|
98
|
+
value_df = value_df.dropna()
|
|
99
|
+
value_df = value_df.drop_duplicates(subset=index_key, keep="last")
|
|
100
|
+
return value_df.set_index(index_key)
|
|
103
101
|
|
|
104
102
|
|
|
105
103
|
def data_factory(data_type: str, header: str, subheaders: list | None = None):
|
|
@@ -107,7 +105,8 @@ def data_factory(data_type: str, header: str, subheaders: list | None = None):
|
|
|
107
105
|
return LastData(header, subheaders)
|
|
108
106
|
if data_type == "all":
|
|
109
107
|
return AllData(header, subheaders)
|
|
110
|
-
|
|
108
|
+
msg = f'Unexpected data "{data_type}"'
|
|
109
|
+
raise ValueError(msg)
|
|
111
110
|
|
|
112
111
|
|
|
113
112
|
class State(ABC):
|
|
@@ -140,7 +139,8 @@ class SteadyState(State):
|
|
|
140
139
|
pass
|
|
141
140
|
|
|
142
141
|
def report_progress(self):
|
|
143
|
-
|
|
142
|
+
msg = "No progress reporting for steady simulations"
|
|
143
|
+
raise NotImplementedError(msg)
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
def state_factory(steady: bool, extracted_data: Data) -> State:
|
|
@@ -264,7 +264,7 @@ class TimeDeltaSParser(Parser):
|
|
|
264
264
|
def _process_line(self, raw: str) -> dt.timedelta:
|
|
265
265
|
"""Converts string S (with decimal place and "s") to timedelta"""
|
|
266
266
|
|
|
267
|
-
s = raw.split("s")[0] #
|
|
267
|
+
s = raw.split("s")[0] # not necessary for simulation time
|
|
268
268
|
return dt.timedelta(seconds=float(s))
|
|
269
269
|
|
|
270
270
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Flood Modeller Python API
|
|
3
|
-
Copyright (C)
|
|
3
|
+
Copyright (C) 2025 Jacobs U.K. Limited
|
|
4
4
|
|
|
5
5
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
|
|
6
6
|
as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
@@ -390,10 +390,6 @@ lf1_steady_data_to_extract = {
|
|
|
390
390
|
},
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
# TODO for LF2:
|
|
394
|
-
# - "start" only works for v6
|
|
395
|
-
# - what if there are multiple domains?
|
|
396
|
-
|
|
397
393
|
lf2_data_to_extract = {
|
|
398
394
|
# start
|
|
399
395
|
"version": {
|
floodmodeller_api/mapping.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from typing import Any
|
|
2
4
|
|
|
3
5
|
from . import DAT, IED, IEF, INP, LF1, LF2, XML2D, ZZN
|
|
@@ -14,12 +16,15 @@ from .units import (
|
|
|
14
16
|
HTBDY,
|
|
15
17
|
IIC,
|
|
16
18
|
INTERPOLATE,
|
|
19
|
+
JUNCTION,
|
|
20
|
+
LATERAL,
|
|
17
21
|
ORIFICE,
|
|
18
22
|
OUTFALL,
|
|
19
23
|
QHBDY,
|
|
20
24
|
QTBDY,
|
|
21
25
|
REFHBDY,
|
|
22
26
|
REPLICATE,
|
|
27
|
+
RESERVOIR,
|
|
23
28
|
RIVER,
|
|
24
29
|
RNWEIR,
|
|
25
30
|
SLUICE,
|
|
@@ -30,7 +35,8 @@ from .units import (
|
|
|
30
35
|
)
|
|
31
36
|
from .urban1d.conduits import CONDUIT as CONDUIT_URBAN
|
|
32
37
|
from .urban1d.conduits import CONDUITS as CONDUITS_URBAN
|
|
33
|
-
from .urban1d.junctions import JUNCTION
|
|
38
|
+
from .urban1d.junctions import JUNCTION as JUNCTION_URBAN
|
|
39
|
+
from .urban1d.junctions import JUNCTIONS
|
|
34
40
|
from .urban1d.losses import LOSS, LOSSES
|
|
35
41
|
from .urban1d.outfalls import OUTFALL as OUTFALL_URBAN
|
|
36
42
|
from .urban1d.outfalls import OUTFALLS as OUTFALLS_URBAN
|
|
@@ -49,7 +55,7 @@ api_class_mapping: dict[str, Any] = {
|
|
|
49
55
|
"floodmodeller_api.zzn.ZZN": ZZN,
|
|
50
56
|
"floodmodeller_api.backup.File": File,
|
|
51
57
|
"floodmodeller_api.urban1d.junctions.JUNCTIONS": JUNCTIONS,
|
|
52
|
-
"floodmodeller_api.urban1d.junctions.JUNCTION":
|
|
58
|
+
"floodmodeller_api.urban1d.junctions.JUNCTION": JUNCTION_URBAN,
|
|
53
59
|
"floodmodeller_api.urban1d.outfalls.OUTFALLS": OUTFALLS_URBAN,
|
|
54
60
|
"floodmodeller_api.urban1d.outfalls.OUTFALL": OUTFALL_URBAN,
|
|
55
61
|
"floodmodeller_api.urban1d.raingauges.RAINGAUGES": RAINGAUGES,
|
|
@@ -66,6 +72,9 @@ api_class_mapping: dict[str, Any] = {
|
|
|
66
72
|
"floodmodeller_api.units.boundaries.REFHBDY": REFHBDY,
|
|
67
73
|
"floodmodeller_api.units.comment.COMMENT": COMMENT,
|
|
68
74
|
"floodmodeller_api.units.conduits.CONDUIT": CONDUIT,
|
|
75
|
+
"floodmodeller_api.units.connectors.JUNCTION": JUNCTION,
|
|
76
|
+
"floodmodeller_api.units.connectors.LATERAL": LATERAL,
|
|
77
|
+
"floodmodeller_api.units.controls.RESERVOIR": RESERVOIR,
|
|
69
78
|
"floodmodeller_api.units.iic.IIC": IIC,
|
|
70
79
|
"floodmodeller_api.units.losses.BLOCKAGE": BLOCKAGE,
|
|
71
80
|
"floodmodeller_api.units.losses.CULVERT": CULVERT,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
from unittest.mock import patch
|
|
4
3
|
|
|
@@ -6,8 +5,8 @@ import pytest
|
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
@pytest.fixture(scope="session")
|
|
9
|
-
def test_workspace():
|
|
10
|
-
return Path(
|
|
8
|
+
def test_workspace() -> Path:
|
|
9
|
+
return Path(__file__).parent / "test_data"
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
@pytest.fixture()
|
|
@@ -6,13 +6,13 @@ import pytest
|
|
|
6
6
|
from floodmodeller_api.backup import BackupControl, File
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
@pytest.fixture
|
|
9
|
+
@pytest.fixture()
|
|
10
10
|
def backup_control():
|
|
11
11
|
# Use a different directory for testing
|
|
12
12
|
return BackupControl()
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
@pytest.fixture
|
|
15
|
+
@pytest.fixture()
|
|
16
16
|
def file(test_workspace):
|
|
17
17
|
test_file = Path(test_workspace, "EX1.DAT")
|
|
18
18
|
file = File(test_file)
|