floodmodeller-api 0.5.1__py3-none-any.whl → 0.5.2.post1__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 +10 -0
- floodmodeller_api/_base.py +29 -20
- floodmodeller_api/backup.py +12 -10
- floodmodeller_api/dat.py +161 -91
- floodmodeller_api/diff.py +1 -1
- floodmodeller_api/hydrology_plus/hydrology_plus_export.py +1 -1
- floodmodeller_api/ied.py +2 -4
- floodmodeller_api/ief.py +29 -17
- floodmodeller_api/ief_flags.py +1 -1
- floodmodeller_api/inp.py +4 -6
- floodmodeller_api/logs/lf.py +18 -12
- floodmodeller_api/logs/lf_helpers.py +2 -2
- floodmodeller_api/logs/lf_params.py +1 -5
- floodmodeller_api/mapping.py +9 -2
- floodmodeller_api/test/test_conveyance.py +9 -4
- floodmodeller_api/test/test_dat.py +166 -18
- 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_ied.py +1 -1
- floodmodeller_api/test/test_ief.py +10 -2
- floodmodeller_api/test/test_integrated_bridge.py +159 -0
- floodmodeller_api/test/test_json.py +9 -3
- floodmodeller_api/test/test_logs_lf.py +45 -24
- floodmodeller_api/test/test_river.py +1 -1
- floodmodeller_api/test/test_toolbox_structure_log.py +0 -1
- floodmodeller_api/test/test_xml2d.py +5 -5
- floodmodeller_api/to_from_json.py +1 -1
- floodmodeller_api/tool.py +3 -5
- floodmodeller_api/toolbox/model_build/add_siltation_definition.py +1 -1
- floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +10 -8
- floodmodeller_api/units/__init__.py +15 -0
- floodmodeller_api/units/_base.py +73 -10
- 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/iic.py +2 -9
- floodmodeller_api/units/losses.py +42 -42
- floodmodeller_api/units/sections.py +40 -43
- floodmodeller_api/units/structures.py +360 -530
- floodmodeller_api/units/units.py +25 -26
- floodmodeller_api/units/unsupported.py +5 -7
- floodmodeller_api/units/variables.py +2 -2
- floodmodeller_api/urban1d/_base.py +7 -8
- 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 +16 -21
- floodmodeller_api/urban1d/raingauges.py +3 -9
- floodmodeller_api/urban1d/subsections.py +3 -4
- floodmodeller_api/urban1d/xsections.py +11 -15
- floodmodeller_api/util.py +7 -4
- floodmodeller_api/validation/parameters.py +7 -3
- floodmodeller_api/validation/urban_parameters.py +1 -4
- floodmodeller_api/validation/validation.py +9 -4
- floodmodeller_api/version.py +1 -1
- floodmodeller_api/xml2d.py +9 -11
- floodmodeller_api/xml2d_template.py +1 -1
- floodmodeller_api/zz.py +7 -6
- {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.dist-info}/LICENSE.txt +1 -1
- {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.dist-info}/METADATA +11 -3
- {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.dist-info}/RECORD +85 -70
- {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.dist-info}/WHEEL +1 -1
- floodmodeller_api/units/helpers.py +0 -121
- {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.dist-info}/entry_points.txt +0 -0
- {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.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,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 csv
|
|
20
|
+
import logging
|
|
20
21
|
import subprocess
|
|
21
22
|
import time
|
|
22
23
|
from io import StringIO
|
|
@@ -32,19 +33,28 @@ from .diff import check_item_with_dataframe_equal
|
|
|
32
33
|
from .ief_flags import flags
|
|
33
34
|
from .logs import LF1, create_lf
|
|
34
35
|
from .to_from_json import Jsonable
|
|
35
|
-
from .util import handle_exception
|
|
36
|
+
from .util import handle_exception, is_windows
|
|
36
37
|
from .zz import ZZN
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def
|
|
40
|
+
def try_converting(value: str) -> str | int | float:
|
|
40
41
|
"""Attempt to parse value as float or int if valid, else return the original string"""
|
|
41
42
|
try:
|
|
42
43
|
return int(value)
|
|
43
44
|
except ValueError:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
48
58
|
|
|
49
59
|
|
|
50
60
|
class IEF(FMFile):
|
|
@@ -74,13 +84,13 @@ class IEF(FMFile):
|
|
|
74
84
|
if ief_filepath is not None:
|
|
75
85
|
FMFile.__init__(self, ief_filepath)
|
|
76
86
|
self._read()
|
|
77
|
-
self._log_path = self.
|
|
87
|
+
self._log_path = self._filepath.with_suffix(".lf1")
|
|
78
88
|
else:
|
|
79
89
|
self._create_from_blank()
|
|
80
90
|
|
|
81
91
|
def _read(self):
|
|
82
92
|
# Read IEF data
|
|
83
|
-
with open(self._filepath) as ief_file:
|
|
93
|
+
with open(self._filepath, encoding=self.ENCODING) as ief_file:
|
|
84
94
|
raw_data = [line.rstrip("\n") for line in ief_file]
|
|
85
95
|
# Clean data and add as class properties
|
|
86
96
|
# Create a list to store the properties which are to be saved in IEF, so as to ignore any temp properties.
|
|
@@ -116,7 +126,7 @@ class IEF(FMFile):
|
|
|
116
126
|
self._ief_properties.append(prop)
|
|
117
127
|
else:
|
|
118
128
|
# Sets the property and value as class properties so they can be edited.
|
|
119
|
-
setattr(self, prop,
|
|
129
|
+
setattr(self, prop, try_converting(value))
|
|
120
130
|
self._ief_properties.append(prop)
|
|
121
131
|
prev_comment = None
|
|
122
132
|
else:
|
|
@@ -208,7 +218,7 @@ class IEF(FMFile):
|
|
|
208
218
|
if "=" in line:
|
|
209
219
|
prop, value = line.split("=")
|
|
210
220
|
# Sets the property and value as class properties so they can be edited.
|
|
211
|
-
setattr(self, prop,
|
|
221
|
+
setattr(self, prop, try_converting(value))
|
|
212
222
|
self._ief_properties.append(prop)
|
|
213
223
|
else:
|
|
214
224
|
# This should add the [] bound headers
|
|
@@ -226,8 +236,9 @@ class IEF(FMFile):
|
|
|
226
236
|
):
|
|
227
237
|
# Check if valid flag
|
|
228
238
|
if prop.upper() not in flags:
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
logging.warning(
|
|
240
|
+
"'%s' is not a valid IEF flag, it will be ommited from the IEF\n",
|
|
241
|
+
prop,
|
|
231
242
|
)
|
|
232
243
|
continue
|
|
233
244
|
|
|
@@ -435,6 +446,7 @@ class IEF(FMFile):
|
|
|
435
446
|
filepath (string): Full filepath to new location for ief file (including '.ief' extension)
|
|
436
447
|
"""
|
|
437
448
|
self._save(filepath)
|
|
449
|
+
self._log_path = self._filepath.with_suffix(".lf1")
|
|
438
450
|
|
|
439
451
|
@handle_exception(when="simulate")
|
|
440
452
|
def simulate( # noqa: C901, PLR0912, PLR0913
|
|
@@ -501,10 +513,10 @@ class IEF(FMFile):
|
|
|
501
513
|
msg = f"Flood Modeller engine not found! Expected location: {isis32_fp}"
|
|
502
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
521
|
process = Popen(run_command, cwd=Path(self._filepath).parent)
|
|
510
522
|
|
|
@@ -521,10 +533,10 @@ 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
541
|
return Popen(run_command, cwd=Path(self._filepath).parent)
|
|
530
542
|
|
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,7 +59,7 @@ class INP(FMFile):
|
|
|
59
59
|
|
|
60
60
|
def _read(self):
|
|
61
61
|
# Read INP file
|
|
62
|
-
with open(self._filepath) as inp_file:
|
|
62
|
+
with open(self._filepath, encoding=self.ENCODING) as inp_file:
|
|
63
63
|
self._raw_data = [line.rstrip("\n") for line in inp_file]
|
|
64
64
|
|
|
65
65
|
# Generate INP file structure
|
|
@@ -158,7 +158,7 @@ class INP(FMFile):
|
|
|
158
158
|
data = units.helpers.split_n_char(line, 21)
|
|
159
159
|
|
|
160
160
|
# Set type to Float or Stirng, as appropirate.
|
|
161
|
-
self.options[data[0].lower()] =
|
|
161
|
+
self.options[data[0].lower()] = to_str(
|
|
162
162
|
data[1],
|
|
163
163
|
None,
|
|
164
164
|
check_float=True,
|
|
@@ -197,8 +197,6 @@ class INP(FMFile):
|
|
|
197
197
|
in_block = False
|
|
198
198
|
unit_block = {}
|
|
199
199
|
for idx, line in enumerate(self._raw_data):
|
|
200
|
-
# TODO: Add functionality to compare first four characters only (alphanumeric) - need to consider names shorter than 4 characters, and those with _ within name
|
|
201
|
-
|
|
202
200
|
# Check if subsection is known
|
|
203
201
|
if line.upper() in subsections.ALL_SUBSECTIONS:
|
|
204
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)
|
|
@@ -175,7 +176,7 @@ class LF(FMFile):
|
|
|
175
176
|
|
|
176
177
|
delattr(self, "info")
|
|
177
178
|
|
|
178
|
-
def to_dataframe(self, *, include_tuflow: bool = False) -> pd.DataFrame:
|
|
179
|
+
def to_dataframe(self, variable: str = "all", *, include_tuflow: bool = False) -> pd.DataFrame:
|
|
179
180
|
"""Collects parameter values that change throughout simulation into a dataframe
|
|
180
181
|
|
|
181
182
|
Args:
|
|
@@ -185,17 +186,22 @@ class LF(FMFile):
|
|
|
185
186
|
pd.DataFrame: DataFrame of log file parameters indexed by simulation time (unsteady) or network iterations (steady)
|
|
186
187
|
"""
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
data_type_all = {
|
|
189
|
+
lf_df_data = {
|
|
191
190
|
k: getattr(self, k)
|
|
192
191
|
for k, v in self._data_to_extract.items()
|
|
193
|
-
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
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
if lf_df_data == {}:
|
|
198
|
+
msg = f"No data extracted for variable '{variable}'"
|
|
199
|
+
raise ValueError(msg)
|
|
198
200
|
|
|
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
|
|
199
205
|
return lf_df.sort_index()
|
|
200
206
|
|
|
201
207
|
def _sync_cols(self):
|
|
@@ -217,7 +223,7 @@ class LF(FMFile):
|
|
|
217
223
|
def _print_no_lines(self):
|
|
218
224
|
"""Prints number of lines that have been read so far"""
|
|
219
225
|
|
|
220
|
-
|
|
226
|
+
logging.info("Last line read: %s", self._no_lines)
|
|
221
227
|
|
|
222
228
|
def report_progress(self) -> float:
|
|
223
229
|
"""Returns progress for unsteady simulations
|
|
@@ -316,7 +322,7 @@ def create_lf(filepath: Path, suffix: str) -> LF1 | LF2 | None:
|
|
|
316
322
|
"""Checks for a new log file, waiting for its creation if necessary"""
|
|
317
323
|
|
|
318
324
|
def _no_log_file(reason: str) -> None:
|
|
319
|
-
|
|
325
|
+
logging.warning("No progress bar as %s. Simulation will continue as usual.", reason)
|
|
320
326
|
|
|
321
327
|
# ensure progress bar is supported
|
|
322
328
|
if suffix not in {"lf1", "lf2"}:
|
|
@@ -349,7 +355,7 @@ def create_lf(filepath: Path, suffix: str) -> LF1 | LF2 | None:
|
|
|
349
355
|
last_modified = dt.datetime.fromtimestamp(last_modified_timestamp)
|
|
350
356
|
time_diff_sec = (dt.datetime.now() - last_modified).total_seconds()
|
|
351
357
|
|
|
352
|
-
# it's old if it's over OLD_FILE seconds old
|
|
358
|
+
# it's old if it's over OLD_FILE seconds old
|
|
353
359
|
old_log_file = time_diff_sec > OLD_FILE
|
|
354
360
|
|
|
355
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.
|
|
@@ -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
|
@@ -16,12 +16,15 @@ from .units import (
|
|
|
16
16
|
HTBDY,
|
|
17
17
|
IIC,
|
|
18
18
|
INTERPOLATE,
|
|
19
|
+
JUNCTION,
|
|
20
|
+
LATERAL,
|
|
19
21
|
ORIFICE,
|
|
20
22
|
OUTFALL,
|
|
21
23
|
QHBDY,
|
|
22
24
|
QTBDY,
|
|
23
25
|
REFHBDY,
|
|
24
26
|
REPLICATE,
|
|
27
|
+
RESERVOIR,
|
|
25
28
|
RIVER,
|
|
26
29
|
RNWEIR,
|
|
27
30
|
SLUICE,
|
|
@@ -32,7 +35,8 @@ from .units import (
|
|
|
32
35
|
)
|
|
33
36
|
from .urban1d.conduits import CONDUIT as CONDUIT_URBAN
|
|
34
37
|
from .urban1d.conduits import CONDUITS as CONDUITS_URBAN
|
|
35
|
-
from .urban1d.junctions import JUNCTION
|
|
38
|
+
from .urban1d.junctions import JUNCTION as JUNCTION_URBAN
|
|
39
|
+
from .urban1d.junctions import JUNCTIONS
|
|
36
40
|
from .urban1d.losses import LOSS, LOSSES
|
|
37
41
|
from .urban1d.outfalls import OUTFALL as OUTFALL_URBAN
|
|
38
42
|
from .urban1d.outfalls import OUTFALLS as OUTFALLS_URBAN
|
|
@@ -51,7 +55,7 @@ api_class_mapping: dict[str, Any] = {
|
|
|
51
55
|
"floodmodeller_api.zzn.ZZN": ZZN,
|
|
52
56
|
"floodmodeller_api.backup.File": File,
|
|
53
57
|
"floodmodeller_api.urban1d.junctions.JUNCTIONS": JUNCTIONS,
|
|
54
|
-
"floodmodeller_api.urban1d.junctions.JUNCTION":
|
|
58
|
+
"floodmodeller_api.urban1d.junctions.JUNCTION": JUNCTION_URBAN,
|
|
55
59
|
"floodmodeller_api.urban1d.outfalls.OUTFALLS": OUTFALLS_URBAN,
|
|
56
60
|
"floodmodeller_api.urban1d.outfalls.OUTFALL": OUTFALL_URBAN,
|
|
57
61
|
"floodmodeller_api.urban1d.raingauges.RAINGAUGES": RAINGAUGES,
|
|
@@ -68,6 +72,9 @@ api_class_mapping: dict[str, Any] = {
|
|
|
68
72
|
"floodmodeller_api.units.boundaries.REFHBDY": REFHBDY,
|
|
69
73
|
"floodmodeller_api.units.comment.COMMENT": COMMENT,
|
|
70
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,
|
|
71
78
|
"floodmodeller_api.units.iic.IIC": IIC,
|
|
72
79
|
"floodmodeller_api.units.losses.BLOCKAGE": BLOCKAGE,
|
|
73
80
|
"floodmodeller_api.units.losses.CULVERT": CULVERT,
|
|
@@ -57,7 +57,9 @@ def from_gui(test_workspace: Path):
|
|
|
57
57
|
def test_results_close_to_gui(section: str, dat: DAT, from_gui: pd.DataFrame):
|
|
58
58
|
threshold = 6
|
|
59
59
|
|
|
60
|
-
actual = dat.sections[section].conveyance
|
|
60
|
+
actual = dat.sections[section].conveyance # type: ignore
|
|
61
|
+
# ignored because we know that these all have type RIVER
|
|
62
|
+
|
|
61
63
|
expected = (
|
|
62
64
|
from_gui.set_index(f"{section}_stage")[f"{section}_conveyance"].dropna().drop_duplicates()
|
|
63
65
|
)
|
|
@@ -74,14 +76,17 @@ def test_results_close_to_gui(section: str, dat: DAT, from_gui: pd.DataFrame):
|
|
|
74
76
|
|
|
75
77
|
@pytest.mark.parametrize("section", ["a", "a2", "b", "b2", "c", "d", "d2", "e", "e2", "e3"])
|
|
76
78
|
def test_results_match_gui_at_shared_points(section: str, dat: DAT, from_gui: pd.DataFrame):
|
|
77
|
-
tolerance = 1e-2 # 0.
|
|
78
|
-
|
|
79
|
+
tolerance = 1e-2 # 0.01
|
|
80
|
+
|
|
81
|
+
actual = dat.sections[section].conveyance # type: ignore
|
|
82
|
+
# ignored because we know that these all have type RIVER
|
|
83
|
+
|
|
79
84
|
expected = (
|
|
80
85
|
from_gui.set_index(f"{section}_stage")[f"{section}_conveyance"].dropna().drop_duplicates()
|
|
81
86
|
)
|
|
82
87
|
shared_index = sorted(set(actual.index).intersection(expected.index))
|
|
83
88
|
diff = expected[shared_index] - actual[shared_index]
|
|
84
|
-
assert (abs(diff) < tolerance).all() # asserts all conveyance values within 0.
|
|
89
|
+
assert (abs(diff) < tolerance).all() # asserts all conveyance values within 0.01 difference
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
def test_calculate_geometry():
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from unittest.mock import patch
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
6
7
|
from floodmodeller_api import DAT
|
|
7
|
-
from floodmodeller_api.units import QTBDY
|
|
8
|
+
from floodmodeller_api.units import JUNCTION, LATERAL, QTBDY, RESERVOIR
|
|
8
9
|
from floodmodeller_api.util import FloodModellerAPIError
|
|
9
10
|
|
|
10
11
|
|
|
@@ -41,13 +42,6 @@ def dat_ex6(test_workspace):
|
|
|
41
42
|
yield dat
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
def test_dat_str_not_changed_by_write(dat_fp, data_before):
|
|
45
|
-
# TODO: Update this test - it isn't really testing anything since the behaviour of the fixture is exactly the same
|
|
46
|
-
"""DAT: Test str representation equal to dat file with no changes"""
|
|
47
|
-
dat = DAT(dat_fp)
|
|
48
|
-
assert dat._write() == data_before
|
|
49
|
-
|
|
50
|
-
|
|
51
45
|
def test_changing_section_and_dist_works(dat_fp, data_before):
|
|
52
46
|
"""DAT: Test changing and reverting section name and dist to next makes no changes"""
|
|
53
47
|
dat = DAT(dat_fp)
|
|
@@ -77,17 +71,17 @@ def test_changing_and_reverting_qtbdy_hydrograph_works(dat_fp, data_before):
|
|
|
77
71
|
assert dat._write() == data_before
|
|
78
72
|
|
|
79
73
|
|
|
80
|
-
def test_dat_read_doesnt_change_data(test_workspace,
|
|
74
|
+
def test_dat_read_doesnt_change_data(test_workspace, tmp_path):
|
|
81
75
|
"""DAT: Check all '.dat' files in folder by reading the _write() output into a new DAT instance and checking it stays the same."""
|
|
82
76
|
for datfile in Path(test_workspace).glob("*.dat"):
|
|
83
77
|
dat = DAT(datfile)
|
|
84
78
|
first_output = dat._write()
|
|
85
|
-
new_path =
|
|
79
|
+
new_path = tmp_path / "tmp.dat"
|
|
86
80
|
dat.save(new_path)
|
|
87
81
|
second_dat = DAT(new_path)
|
|
88
|
-
assert dat == second_dat
|
|
82
|
+
assert dat == second_dat, f"dat objects not equal for {datfile=}"
|
|
89
83
|
second_output = second_dat._write()
|
|
90
|
-
assert first_output == second_output
|
|
84
|
+
assert first_output == second_output, f"dat outputs not equal for {datfile=}"
|
|
91
85
|
|
|
92
86
|
|
|
93
87
|
def test_insert_unit_before(units, dat_ex6):
|
|
@@ -201,12 +195,14 @@ def test_remove_unit(dat_ex3):
|
|
|
201
195
|
assert (prev_dat_struct_len - len(dat_ex3._dat_struct)) == 1
|
|
202
196
|
|
|
203
197
|
|
|
204
|
-
def test_diff(test_workspace,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
198
|
+
def test_diff(test_workspace, caplog):
|
|
199
|
+
with caplog.at_level(logging.INFO):
|
|
200
|
+
dat_ex4 = DAT(Path(test_workspace, "ex4.DAT"))
|
|
201
|
+
dat_ex4_changed = DAT(Path(test_workspace, "ex4_changed.DAT"))
|
|
202
|
+
dat_ex4.diff(dat_ex4_changed)
|
|
203
|
+
|
|
204
|
+
assert caplog.text == (
|
|
205
|
+
"INFO root:_base.py:135 Files not equivalent, 12 difference(s) found:\n"
|
|
210
206
|
" DAT->structures->MILLAu->RNWEIR..MILLAu->upstream_crest_height: 1.07 != 1.37\n"
|
|
211
207
|
" DAT->structures->MILLBu->RNWEIR..MILLBu->upstream_crest_height: 0.43 != 0.73\n"
|
|
212
208
|
" DAT->structures->ROAD1->RNWEIR..ROAD1->upstream_crest_height: 2.02 != 2.32\n"
|
|
@@ -220,3 +216,155 @@ def test_diff(test_workspace, capsys):
|
|
|
220
216
|
" DAT->_all_units->itm[61]->RNWEIR..CSRD01u->upstream_crest_height: 0.81 != 1.11\n"
|
|
221
217
|
" DAT->_all_units->itm[73]->RNWEIR..FOOTa->upstream_crest_height: 2.47 != 2.77\n"
|
|
222
218
|
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_valid_network(test_workspace: Path):
|
|
222
|
+
"""Test against network derived manually."""
|
|
223
|
+
dat = DAT(test_workspace / "network.dat")
|
|
224
|
+
actual_nodes, actual_edges = dat.get_network()
|
|
225
|
+
|
|
226
|
+
expected_edges = [
|
|
227
|
+
("FSSR16BDY_resin", "RIVER_resin"),
|
|
228
|
+
("QTBDY_CS26", "RIVER_CS26"),
|
|
229
|
+
("RIVER_CS26", "RIVER_CS25"),
|
|
230
|
+
("RIVER_CS25", "RIVER_CS24"),
|
|
231
|
+
("RIVER_CS25", "SPILL_RD25Su"),
|
|
232
|
+
("RIVER_CS24", "RIVER_CS23"),
|
|
233
|
+
("RIVER_CS24", "SPILL_RD24Su"),
|
|
234
|
+
("RIVER_CS23", "RIVER_CS22"),
|
|
235
|
+
("RIVER_CS23", "SPILL_RD23Su"),
|
|
236
|
+
("RIVER_CS22", "RIVER_CS21"),
|
|
237
|
+
("RIVER_CS22", "SPILL_RD22Su"),
|
|
238
|
+
("RIVER_CS21", "RIVER_CS20"),
|
|
239
|
+
("RIVER_CS21", "SPILL_RD21Su"),
|
|
240
|
+
("RIVER_CS20", "RIVER_CS19"),
|
|
241
|
+
("RIVER_CS20", "SPILL_RD20Su"),
|
|
242
|
+
("RIVER_CS19", "RIVER_CS18"),
|
|
243
|
+
("RIVER_CS19", "SPILL_RD19Su"),
|
|
244
|
+
("RIVER_CS18", "RIVER_RESOUT2a"),
|
|
245
|
+
("RIVER_CS18", "SPILL_RD18Su"),
|
|
246
|
+
("RIVER_RESOUT2a", "RIVER_RESOUT2b"),
|
|
247
|
+
("RIVER_RESOUT2b", "JUNCTION_RESOUT2b"),
|
|
248
|
+
("JUNCTION_RESOUT2b", "RIVER_RESOUT2u"),
|
|
249
|
+
("JUNCTION_RESOUT2b", "RIVER_RESOUT2d"),
|
|
250
|
+
("RIVER_resin", "RIVER_CSRD25"),
|
|
251
|
+
("RIVER_CSRD25", "RIVER_CSRD24"),
|
|
252
|
+
("RIVER_CSRD25", "SPILL_RD25Su"),
|
|
253
|
+
("RIVER_CSRD24", "RIVER_CSRD23"),
|
|
254
|
+
("RIVER_CSRD24", "SPILL_RD24Su"),
|
|
255
|
+
("RIVER_CSRD23", "RIVER_CSRD22"),
|
|
256
|
+
("RIVER_CSRD23", "SPILL_RD23Su"),
|
|
257
|
+
("RIVER_CSRD22", "RIVER_CSRD21"),
|
|
258
|
+
("RIVER_CSRD22", "SPILL_RD22Su"),
|
|
259
|
+
("RIVER_CSRD21", "RIVER_CSRD20"),
|
|
260
|
+
("RIVER_CSRD21", "SPILL_RD21Su"),
|
|
261
|
+
("RIVER_CSRD20", "RIVER_CSRD19"),
|
|
262
|
+
("RIVER_CSRD20", "SPILL_RD20Su"),
|
|
263
|
+
("RIVER_CSRD19", "RIVER_CSRD18"),
|
|
264
|
+
("RIVER_CSRD19", "SPILL_RD19Su"),
|
|
265
|
+
("RIVER_CSRD18", "RIVER_CSRD17"),
|
|
266
|
+
("RIVER_CSRD18", "SPILL_RD18Su"),
|
|
267
|
+
("RIVER_CSRD17", "RIVER_CSRD16"),
|
|
268
|
+
("RIVER_CSRD16", "RIVER_CSRD15"),
|
|
269
|
+
("RIVER_CSRD15", "RIVER_CSRD14u"),
|
|
270
|
+
("RIVER_CSRD14u", "JUNCTION_CSRD14u"),
|
|
271
|
+
("JUNCTION_CSRD14u", "RNWEIR_MILLBu"),
|
|
272
|
+
("JUNCTION_CSRD14u", "RNWEIR_MILLAu"),
|
|
273
|
+
("RNWEIR_MILLAu", "RIVER_MILLAd"),
|
|
274
|
+
("RIVER_MILLAd", "RIVER_RESOUT2u"),
|
|
275
|
+
("RIVER_RESOUT2d", "RIVER_CSRD13"),
|
|
276
|
+
("RIVER_CSRD13", "RIVER_CSRD12u"),
|
|
277
|
+
("RIVER_CSRD12u", "JUNCTION_CSRD12u"),
|
|
278
|
+
("RNWEIR_MILLBu", "JUNCTION_CSRD12u"),
|
|
279
|
+
("JUNCTION_CSRD12u", "RIVER_CSRD12d"),
|
|
280
|
+
("RIVER_CSRD12d", "RIVER_CSRD10"),
|
|
281
|
+
("RIVER_CSRD10", "RIVER_CSRD09"),
|
|
282
|
+
("RIVER_CSRD09u", "RIVER_CSRD09a"),
|
|
283
|
+
("RIVER_CSRD09", "JUNCTION_CSRD09"),
|
|
284
|
+
("RIVER_CSRD09u", "JUNCTION_CSRD09"),
|
|
285
|
+
("JUNCTION_CSRD09", "RNWEIR_ROAD1"),
|
|
286
|
+
("RIVER_CSRD09a", "BERNOULLI_CSRD09a"),
|
|
287
|
+
("BERNOULLI_CSRD09a", "RIVER_CSRD08u"),
|
|
288
|
+
("RIVER_CSRD08u", "RIVER_CSRD08a"),
|
|
289
|
+
("RIVER_CSRD08a", "JUNCTION_CSRD08a"),
|
|
290
|
+
("JUNCTION_CSRD08a", "RIVER_CSRD08"),
|
|
291
|
+
("RNWEIR_ROAD1", "JUNCTION_CSRD08a"),
|
|
292
|
+
("RIVER_CSRD08", "RIVER_CSRD07"),
|
|
293
|
+
("RIVER_CSRD07", "RIVER_CSRD06"),
|
|
294
|
+
("RIVER_CSRD06", "RIVER_CSRD05"),
|
|
295
|
+
("RIVER_CSRD05", "RIVER_CSRD04"),
|
|
296
|
+
("RIVER_CSRD04", "RIVER_CSRD03"),
|
|
297
|
+
("RIVER_CSRD03", "RIVER_CSRD02"),
|
|
298
|
+
("RIVER_CSRD02", "RIVER_CSRD02d"),
|
|
299
|
+
("RIVER_CSRD02d", "JUNCTION_CSRD02d"),
|
|
300
|
+
("JUNCTION_CSRD02d", "RNWEIR_RAILRDu"),
|
|
301
|
+
("JUNCTION_CSRD02d", "BERNOULLI_RAILBRu"),
|
|
302
|
+
("BERNOULLI_RAILBRu", "JUNCTION_RAILBRd"),
|
|
303
|
+
("RNWEIR_RAILRDu", "JUNCTION_RAILBRd"),
|
|
304
|
+
("JUNCTION_RAILBRd", "RIVER_CSRD01a"),
|
|
305
|
+
("RIVER_CSRD01a", "RIVER_CSRD01u"),
|
|
306
|
+
("RIVER_CSRD01u", "RNWEIR_CSRD01u"),
|
|
307
|
+
("RNWEIR_CSRD01u", "RIVER_CSRD01d"),
|
|
308
|
+
("RIVER_CSRD01d", "RIVER_CSRD01"),
|
|
309
|
+
("RIVER_CSRD01", "INTERPOLATE_DS.001"),
|
|
310
|
+
("INTERPOLATE_DS.001", "INTERPOLATE_DS.002"),
|
|
311
|
+
("INTERPOLATE_DS.002", "INTERPOLATE_DS.003"),
|
|
312
|
+
("INTERPOLATE_DS.003", "INTERPOLATE_DS.004"),
|
|
313
|
+
("INTERPOLATE_DS.004", "INTERPOLATE_DS.005"),
|
|
314
|
+
("INTERPOLATE_DS.005", "INTERPOLATE_DS.006"),
|
|
315
|
+
("INTERPOLATE_DS.006", "RIVER_DS2"),
|
|
316
|
+
("RIVER_DS2", "JUNCTION_DS2"),
|
|
317
|
+
("JUNCTION_DS2", "RNWEIR_FOOTa"),
|
|
318
|
+
("JUNCTION_DS2", "BERNOULLI_FOOTBRu"),
|
|
319
|
+
("RNWEIR_FOOTa", "JUNCTION_FOOTb"),
|
|
320
|
+
("BERNOULLI_FOOTBRu", "JUNCTION_FOOTb"),
|
|
321
|
+
("JUNCTION_FOOTb", "RIVER_DS3"),
|
|
322
|
+
("RIVER_DS3", "RIVER_DS4"),
|
|
323
|
+
("RIVER_DS4", "QHBDY_DS4"),
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
actual = {tuple(x.unique_name for x in y) for y in actual_edges}
|
|
327
|
+
expected = set(expected_edges)
|
|
328
|
+
assert expected == actual
|
|
329
|
+
assert len(actual_nodes) == 86
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_invalid_network(test_workspace: Path):
|
|
333
|
+
"""Test dat file that cannot be made into a valid network."""
|
|
334
|
+
dat = DAT(test_workspace / "All Units 4_6.DAT")
|
|
335
|
+
with pytest.raises(RuntimeError):
|
|
336
|
+
dat.get_network()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_create_and_insert_connectors():
|
|
340
|
+
dat = DAT()
|
|
341
|
+
junction = JUNCTION(comment="hi", labels=["A", "B"])
|
|
342
|
+
lateral = LATERAL(name="lat", comment="bye")
|
|
343
|
+
reservoir = RESERVOIR(
|
|
344
|
+
easting=0,
|
|
345
|
+
northing=0,
|
|
346
|
+
runoff=0,
|
|
347
|
+
name="res",
|
|
348
|
+
comment="hello",
|
|
349
|
+
lateral_inflow_labels=["C", "D"],
|
|
350
|
+
)
|
|
351
|
+
dat.insert_units([junction, lateral, reservoir], add_at=-1)
|
|
352
|
+
assert dat.connectors == {"A": junction, "lat": lateral}
|
|
353
|
+
assert dat.controls == {"res": reservoir}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@pytest.mark.parametrize(
|
|
357
|
+
("dat_str", "label"),
|
|
358
|
+
[
|
|
359
|
+
("encoding_test_utf8.dat", "d\xc3\xa5rek"), # because it's initially saved as utf8
|
|
360
|
+
("encoding_test_cp1252.dat", "d\xe5rek"),
|
|
361
|
+
],
|
|
362
|
+
)
|
|
363
|
+
def test_encoding(test_workspace: Path, dat_str: str, label: str, tmp_path: Path):
|
|
364
|
+
dat_read = DAT(test_workspace / dat_str)
|
|
365
|
+
new_path = tmp_path / "tmp_encoding.dat"
|
|
366
|
+
dat_read.save(new_path)
|
|
367
|
+
dat_write = DAT(new_path)
|
|
368
|
+
|
|
369
|
+
assert label in dat_read.sections
|
|
370
|
+
assert label in dat_write.sections # remains as \xc3\xa5 even for utf8
|