floodmodeller-api 0.5.2.post1__py3-none-any.whl → 0.5.3.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/_base.py +10 -4
- floodmodeller_api/dat.py +2 -4
- floodmodeller_api/ied.py +1 -1
- floodmodeller_api/ief.py +20 -3
- floodmodeller_api/logs/lf.py +14 -7
- floodmodeller_api/logs/lf_helpers.py +2 -0
- floodmodeller_api/logs/lf_params.py +54 -26
- floodmodeller_api/mapping.py +2 -0
- floodmodeller_api/test/test_dat.py +13 -1
- floodmodeller_api/test/test_data/floodplain.dat +52 -0
- floodmodeller_api/test/test_data/lf_complex_ex.lf1 +51499 -0
- floodmodeller_api/test/test_data/multievent.ief +43 -0
- floodmodeller_api/test/test_fmfile.py +20 -0
- floodmodeller_api/test/test_ief.py +67 -0
- floodmodeller_api/test/test_logs_lf.py +57 -12
- floodmodeller_api/test/test_sluice.py +31 -0
- floodmodeller_api/test/test_unit.py +57 -0
- floodmodeller_api/units/__init__.py +2 -1
- floodmodeller_api/units/_base.py +18 -6
- floodmodeller_api/units/sections.py +4 -4
- floodmodeller_api/units/structures.py +143 -1
- floodmodeller_api/units/units.py +1 -1
- floodmodeller_api/validation/parameters.py +16 -0
- floodmodeller_api/version.py +1 -1
- floodmodeller_api/xml2d.py +1 -1
- {floodmodeller_api-0.5.2.post1.dist-info → floodmodeller_api-0.5.3.post1.dist-info}/METADATA +5 -3
- {floodmodeller_api-0.5.2.post1.dist-info → floodmodeller_api-0.5.3.post1.dist-info}/RECORD +31 -25
- {floodmodeller_api-0.5.2.post1.dist-info → floodmodeller_api-0.5.3.post1.dist-info}/WHEEL +1 -1
- {floodmodeller_api-0.5.2.post1.dist-info → floodmodeller_api-0.5.3.post1.dist-info}/entry_points.txt +0 -0
- {floodmodeller_api-0.5.2.post1.dist-info → floodmodeller_api-0.5.3.post1.dist-info/licenses}/LICENSE.txt +0 -0
- {floodmodeller_api-0.5.2.post1.dist-info → floodmodeller_api-0.5.3.post1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[ISIS Event Header]
|
|
2
|
+
Title=Multievent Test IEF
|
|
3
|
+
Datafile=network.dat
|
|
4
|
+
Results=..\Somewhere
|
|
5
|
+
[ISIS Event Details]
|
|
6
|
+
RunType=Unsteady
|
|
7
|
+
InitialConditions=..\initial_conditions.iic
|
|
8
|
+
Start=0
|
|
9
|
+
Finish=360
|
|
10
|
+
InitialTimestep=10
|
|
11
|
+
MinimumTimestep=10
|
|
12
|
+
MaximumTimestep=10
|
|
13
|
+
SaveInterval=900
|
|
14
|
+
;Fluvial Inflow
|
|
15
|
+
EventData=..\network.ied
|
|
16
|
+
;Event Override
|
|
17
|
+
EventData=..\event_override.ied
|
|
18
|
+
;Spill Data
|
|
19
|
+
EventData=..\spill1.ied
|
|
20
|
+
;Spill Data
|
|
21
|
+
EventData=..\spill2.ied
|
|
22
|
+
;
|
|
23
|
+
EventData=..\ied_01.IED
|
|
24
|
+
;
|
|
25
|
+
EventData=..\ied_02.IED
|
|
26
|
+
;
|
|
27
|
+
EventData=..\ied_03.IED
|
|
28
|
+
Dflood=5
|
|
29
|
+
Qtol=0.1
|
|
30
|
+
Minitr=5
|
|
31
|
+
Maxitr=19
|
|
32
|
+
Theta=0.55
|
|
33
|
+
Weight=0.1
|
|
34
|
+
AdaptiveTimestep=1
|
|
35
|
+
RefineBridgeSecProps=0
|
|
36
|
+
SolveDHEqualsZeroAtStart=1
|
|
37
|
+
RulesAtTimeZero=1
|
|
38
|
+
RulesOnFirstIteration=0
|
|
39
|
+
ResetTimesAfterPos=1
|
|
40
|
+
UseFPSModularLimit=1
|
|
41
|
+
LaunchDoublePrecisionVersion=1
|
|
42
|
+
UseRemoteQ=1
|
|
43
|
+
2DFLOW=1
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from floodmodeller_api import read_file
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.parametrize(
|
|
9
|
+
"file",
|
|
10
|
+
["BRIDGE.DAT", "network.dat", "Domain1_W.xml", "T5.ief", "network.ied"],
|
|
11
|
+
)
|
|
12
|
+
def test_crlf_line_endings(test_workspace: Path, tmp_path: Path, file: str):
|
|
13
|
+
obj = read_file(test_workspace / file)
|
|
14
|
+
new_path = tmp_path / file
|
|
15
|
+
obj.save(new_path)
|
|
16
|
+
with open(new_path, "rb") as f:
|
|
17
|
+
contents = f.readlines()
|
|
18
|
+
|
|
19
|
+
# Check all line endings except last
|
|
20
|
+
assert all(line.endswith(b"\r\n") for line in contents[:-1])
|
|
@@ -18,6 +18,11 @@ def ief(ief_fp: Path) -> IEF:
|
|
|
18
18
|
return IEF(ief_fp)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
@pytest.fixture()
|
|
22
|
+
def multievent_ief(test_workspace: Path) -> IEF:
|
|
23
|
+
return IEF(test_workspace / "multievent.ief")
|
|
24
|
+
|
|
25
|
+
|
|
21
26
|
@pytest.fixture()
|
|
22
27
|
def exe_bin(tmpdir) -> Path:
|
|
23
28
|
for exe in ["ISISf32.exe", "ISISf32_DoubleP.exe"]:
|
|
@@ -40,6 +45,7 @@ def sleep():
|
|
|
40
45
|
|
|
41
46
|
def test_ief_read_doesnt_change_data(test_workspace, tmpdir):
|
|
42
47
|
"""IEF: Check all '.ief' files in folder by reading the _write() output into a new IEF instance and checking it stays the same."""
|
|
48
|
+
# we use this instead of parameterise so it will just automatically test any ief in the test data.
|
|
43
49
|
for ief_file in Path(test_workspace).glob("*.ief"):
|
|
44
50
|
ief = IEF(ief_file)
|
|
45
51
|
first_output = ief._write()
|
|
@@ -182,3 +188,64 @@ def test_datafile_path(test_workspace: Path):
|
|
|
182
188
|
path = Path(ief.Datafile)
|
|
183
189
|
assert path.stem == "UptonP8_Panels"
|
|
184
190
|
assert path.parent == Path("../../networks")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_unique_events_retained(multievent_ief: IEF):
|
|
194
|
+
"""Tests that the .eventdata attribute retains the same number of items as the original ief"""
|
|
195
|
+
event_dict = multievent_ief.eventdata
|
|
196
|
+
assert len(event_dict) == 7
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.parametrize(
|
|
200
|
+
("sample_eventdata"),
|
|
201
|
+
[
|
|
202
|
+
({}),
|
|
203
|
+
({"Fluvial Inflow": "..\\network.ied", "Event Override": "..\\event_override.ied"}),
|
|
204
|
+
(
|
|
205
|
+
{
|
|
206
|
+
"Fluvial Inflow": "..\\network.ied",
|
|
207
|
+
"Event Override": "..\\event_override.ied",
|
|
208
|
+
"Spill Data": "..\\spill1.ied",
|
|
209
|
+
"Spill Data<0>": "..\\spill2.ied",
|
|
210
|
+
"<0>": "..\\ied_01.IED",
|
|
211
|
+
"<1>": "..\\ied_02.IED",
|
|
212
|
+
"<2>": "..\\ied_03.IED",
|
|
213
|
+
"Added Event": "../added.ied",
|
|
214
|
+
}
|
|
215
|
+
),
|
|
216
|
+
],
|
|
217
|
+
)
|
|
218
|
+
def test_adding_eventdata(multievent_ief, sample_eventdata, tmpdir):
|
|
219
|
+
"""Tests modifying, saving and reading eventdata dictionary.
|
|
220
|
+
|
|
221
|
+
Compares that the input is equal to the output."""
|
|
222
|
+
multievent_ief.eventdata = sample_eventdata
|
|
223
|
+
new_path = Path(tmpdir) / "tmp.ief"
|
|
224
|
+
multievent_ief.save(new_path)
|
|
225
|
+
|
|
226
|
+
new_ief = IEF(new_path)
|
|
227
|
+
assert new_ief.eventdata == sample_eventdata
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_renaming_eventdata(multievent_ief, tmpdir):
|
|
231
|
+
"""Tests renaming an event, after it has been substituted for a temporary one."""
|
|
232
|
+
|
|
233
|
+
mapping = {"<1>": "New Title"}
|
|
234
|
+
|
|
235
|
+
multievent_ief.eventdata = {
|
|
236
|
+
mapping.get(key, key): value for key, value in multievent_ief.eventdata.items()
|
|
237
|
+
}
|
|
238
|
+
new_path = Path(tmpdir) / "tmp.ief"
|
|
239
|
+
multievent_ief.save(new_path)
|
|
240
|
+
|
|
241
|
+
new_ief = IEF(new_path)
|
|
242
|
+
|
|
243
|
+
assert new_ief.eventdata == {
|
|
244
|
+
"Fluvial Inflow": "..\\network.ied",
|
|
245
|
+
"Event Override": "..\\event_override.ied",
|
|
246
|
+
"Spill Data": "..\\spill1.ied",
|
|
247
|
+
"Spill Data<0>": "..\\spill2.ied",
|
|
248
|
+
"<0>": "..\\ied_01.IED",
|
|
249
|
+
"New Title": "..\\ied_02.IED",
|
|
250
|
+
"<1>": "..\\ied_03.IED",
|
|
251
|
+
}
|
|
@@ -11,28 +11,34 @@ from floodmodeller_api.logs import create_lf
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@pytest.fixture()
|
|
14
|
-
def
|
|
14
|
+
def lf1_fp_simple(test_workspace: Path) -> Path:
|
|
15
15
|
return Path(test_workspace, "ex3.lf1")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
@pytest.fixture()
|
|
19
|
+
def lf1_fp_complex(test_workspace: Path) -> Path:
|
|
20
|
+
return Path(test_workspace, "lf_complex_ex.lf1")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_lf1_info_dict(lf1_fp_simple: Path):
|
|
19
24
|
"""LF1: Check info dictionary"""
|
|
20
|
-
lf1 = LF1(
|
|
25
|
+
lf1 = LF1(lf1_fp_simple)
|
|
21
26
|
assert lf1.info["version"] == "5.0.0.7752"
|
|
22
27
|
assert lf1.info["max_system_volume"] == 270549
|
|
23
28
|
assert lf1.info["mass_balance_error"] == -0.03
|
|
24
29
|
assert lf1.info["progress"] == 100
|
|
30
|
+
assert lf1.info["total_boundary_inflow"] == 5506290
|
|
25
31
|
|
|
26
32
|
|
|
27
|
-
def test_lf1_report_progress(
|
|
33
|
+
def test_lf1_report_progress(lf1_fp_simple: Path):
|
|
28
34
|
"""LF1: Check report_progress()"""
|
|
29
|
-
lf1 = LF1(
|
|
35
|
+
lf1 = LF1(lf1_fp_simple)
|
|
30
36
|
assert lf1.report_progress() == 100
|
|
31
37
|
|
|
32
38
|
|
|
33
|
-
def test_lf1_to_dataframe(
|
|
39
|
+
def test_lf1_to_dataframe(lf1_fp_simple: Path):
|
|
34
40
|
"""LF1: Check to_dataframe()"""
|
|
35
|
-
lf1 = LF1(
|
|
41
|
+
lf1 = LF1(lf1_fp_simple)
|
|
36
42
|
lf1_df = lf1.to_dataframe(variable="all")
|
|
37
43
|
|
|
38
44
|
assert lf1_df.loc[lf1_df.index[0], "iter"] == 6
|
|
@@ -56,9 +62,9 @@ def test_lf1_to_dataframe(lf1_fp: Path):
|
|
|
56
62
|
assert lf1_tuflow_df[col].isna().all() # there is no tuflow in this lf1
|
|
57
63
|
|
|
58
64
|
|
|
59
|
-
def test_lf1_from_ief(
|
|
65
|
+
def test_lf1_from_ief(lf1_fp_simple: Path, test_workspace: Path):
|
|
60
66
|
"""LF1: Check IEF.get_log()"""
|
|
61
|
-
lf1 = LF1(
|
|
67
|
+
lf1 = LF1(lf1_fp_simple)
|
|
62
68
|
|
|
63
69
|
ief_fp = Path(test_workspace, "ex3.ief")
|
|
64
70
|
ief = IEF(ief_fp)
|
|
@@ -76,7 +82,7 @@ def test_log_file_unsupported(caplog):
|
|
|
76
82
|
assert lf is None
|
|
77
83
|
assert (
|
|
78
84
|
caplog.text
|
|
79
|
-
== "WARNING root:lf.py:
|
|
85
|
+
== "WARNING root:lf.py:332 No progress bar as log file must have suffix lf1 or lf2. Simulation will continue as usual.\n"
|
|
80
86
|
)
|
|
81
87
|
|
|
82
88
|
|
|
@@ -90,7 +96,7 @@ def test_log_file_timeout(caplog):
|
|
|
90
96
|
assert lf is None
|
|
91
97
|
assert (
|
|
92
98
|
caplog.text
|
|
93
|
-
== "WARNING root:lf.py:
|
|
99
|
+
== "WARNING root:lf.py:332 No progress bar as log file is expected but not detected. Simulation will continue as usual.\n"
|
|
94
100
|
)
|
|
95
101
|
|
|
96
102
|
|
|
@@ -106,7 +112,7 @@ def test_log_file_from_old_run(caplog):
|
|
|
106
112
|
assert lf is None
|
|
107
113
|
assert (
|
|
108
114
|
caplog.text
|
|
109
|
-
== "WARNING root:lf.py:
|
|
115
|
+
== "WARNING root:lf.py:332 No progress bar as log file is from previous run. Simulation will continue as usual.\n"
|
|
110
116
|
)
|
|
111
117
|
|
|
112
118
|
|
|
@@ -121,3 +127,42 @@ def test_log_file_found():
|
|
|
121
127
|
|
|
122
128
|
assert lf is not None
|
|
123
129
|
lf1.assert_called_once_with(lf_filepath)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_lf1_info_dict_all_params_present(lf1_fp_complex: Path):
|
|
133
|
+
"""LF1: Check info dictionary contains all params required"""
|
|
134
|
+
lf1 = LF1(lf1_fp_complex)
|
|
135
|
+
expected_keys = {
|
|
136
|
+
"version",
|
|
137
|
+
"number_of_1D_river_nodes",
|
|
138
|
+
"qtol",
|
|
139
|
+
"htol",
|
|
140
|
+
"start_time",
|
|
141
|
+
"end_time",
|
|
142
|
+
"ran_at",
|
|
143
|
+
"max_itr",
|
|
144
|
+
"min_itr",
|
|
145
|
+
"progress",
|
|
146
|
+
"EFT",
|
|
147
|
+
"ETR",
|
|
148
|
+
"simulation_time_elapsed",
|
|
149
|
+
"number_of_unconverged_timesteps",
|
|
150
|
+
"proportion_of_simulation_unconverged",
|
|
151
|
+
"mass_balance_calculated_every",
|
|
152
|
+
"initial_volume",
|
|
153
|
+
"final_volume",
|
|
154
|
+
"total_boundary_inflow",
|
|
155
|
+
"total_boundary_outflow",
|
|
156
|
+
"total_lat_link_inflow",
|
|
157
|
+
"total_lat_link_outflow",
|
|
158
|
+
"max_system_volume",
|
|
159
|
+
"max_volume_increase",
|
|
160
|
+
"max_boundary_inflow",
|
|
161
|
+
"max_boundary_outflow",
|
|
162
|
+
"net_volume_increase",
|
|
163
|
+
"net_inflow_volume",
|
|
164
|
+
"volume_discrepancy",
|
|
165
|
+
"mass_balance_error",
|
|
166
|
+
"mass_balance_error_2",
|
|
167
|
+
}
|
|
168
|
+
assert expected_keys == lf1.info.keys()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from floodmodeller_api.units import SLUICE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_non_supported_sluice_type_writes():
|
|
5
|
+
unit_block = [
|
|
6
|
+
"SLUICE Sluice with type Water1",
|
|
7
|
+
"VERTICAL",
|
|
8
|
+
"USNODE DSNODE ",
|
|
9
|
+
" 0.700 0.700 1.950 1.050 1.390 5.400",
|
|
10
|
+
" 0.270 1.420 1.000 1.000 0.000 1.000 1.000",
|
|
11
|
+
" 3SECONDS EXTEND ",
|
|
12
|
+
"WATER1 0.000",
|
|
13
|
+
"GATE 1 ",
|
|
14
|
+
" 3",
|
|
15
|
+
" 0.000 2.200",
|
|
16
|
+
" 3.000 2.200",
|
|
17
|
+
"1.000e+010 2.200",
|
|
18
|
+
"GATE 2 ",
|
|
19
|
+
" 3",
|
|
20
|
+
" 0.000 2.200",
|
|
21
|
+
" 3.000 2.200",
|
|
22
|
+
"1.000e+010 2.200",
|
|
23
|
+
"GATE 3 ",
|
|
24
|
+
" 3",
|
|
25
|
+
" 0.000 2.200",
|
|
26
|
+
" 3.000 2.200",
|
|
27
|
+
"1.000e+010 2.200",
|
|
28
|
+
]
|
|
29
|
+
sluice = SLUICE(unit_block)
|
|
30
|
+
result = sluice._write()
|
|
31
|
+
assert list(map(str.rstrip, result)) == list(map(str.rstrip, unit_block))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from floodmodeller_api.units._base import Unit # update this import path to match your repo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DummyUnit(Unit):
|
|
9
|
+
def __init__(self, unit_value: str):
|
|
10
|
+
self._unit = unit_value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.parametrize(
|
|
14
|
+
("unit", "header", "expected_revision", "expected_comment"),
|
|
15
|
+
[
|
|
16
|
+
("RESERVOIR", "RESERVOIR 45678 This is a comment", None, "45678 This is a comment"),
|
|
17
|
+
("RESERVOIR", "RESERVOIR #revision#1 Mr Comment123", 1, "Mr Comment123"),
|
|
18
|
+
("LATERAL", "LATERAL #revision#1", 1, ""),
|
|
19
|
+
("RIVER", "RIVER look at this lovely RIVER", None, "look at this lovely RIVER"),
|
|
20
|
+
],
|
|
21
|
+
)
|
|
22
|
+
def test_get_revision_and_comment(
|
|
23
|
+
unit: str,
|
|
24
|
+
header: str,
|
|
25
|
+
expected_revision: int | None,
|
|
26
|
+
expected_comment: str,
|
|
27
|
+
):
|
|
28
|
+
dummy_unit = DummyUnit(unit)
|
|
29
|
+
revision, comment = dummy_unit._get_revision_and_comment(header)
|
|
30
|
+
assert revision == expected_revision
|
|
31
|
+
assert comment == expected_comment
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize(
|
|
35
|
+
("unit", "header", "remove_revision", "expected_result"),
|
|
36
|
+
[
|
|
37
|
+
("RESERVOIR", "RESERVOIR 45678 This is a comment", True, "45678 This is a comment"),
|
|
38
|
+
("RESERVOIR", "RESERVOIR #revision#1 Mr Comment123", True, "Mr Comment123"),
|
|
39
|
+
(
|
|
40
|
+
"LATERAL",
|
|
41
|
+
"LATERAL #revision#1 another #revision#1 tag",
|
|
42
|
+
True,
|
|
43
|
+
"another #revision#1 tag",
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
"LATERAL",
|
|
47
|
+
"LATERAL #revision#1 another #revision#1 tag",
|
|
48
|
+
False,
|
|
49
|
+
"#revision#1 another #revision#1 tag",
|
|
50
|
+
),
|
|
51
|
+
("RIVER", "RIVER look at this lovely RIVER", False, "look at this lovely RIVER"),
|
|
52
|
+
],
|
|
53
|
+
)
|
|
54
|
+
def test_remove_unit_name(unit: str, header: str, remove_revision: bool, expected_result: str):
|
|
55
|
+
dummy_unit = DummyUnit(unit)
|
|
56
|
+
result = dummy_unit._remove_unit_name(header, remove_revision=remove_revision)
|
|
57
|
+
assert result == expected_result
|
|
@@ -12,6 +12,7 @@ from .structures import (
|
|
|
12
12
|
BRIDGE,
|
|
13
13
|
CRUMP,
|
|
14
14
|
FLAT_V_WEIR,
|
|
15
|
+
FLOODPLAIN,
|
|
15
16
|
ORIFICE,
|
|
16
17
|
OUTFALL,
|
|
17
18
|
RNWEIR,
|
|
@@ -30,6 +31,6 @@ TConnectors: TypeAlias = JUNCTION | LATERAL
|
|
|
30
31
|
TControls: TypeAlias = RESERVOIR
|
|
31
32
|
TLosses: TypeAlias = BLOCKAGE | CULVERT
|
|
32
33
|
TStructures: TypeAlias = (
|
|
33
|
-
BRIDGE | CRUMP | FLAT_V_WEIR | ORIFICE | OUTFALL | RNWEIR | SLUICE | SPILL | WEIR
|
|
34
|
+
BRIDGE | CRUMP | FLAT_V_WEIR | ORIFICE | OUTFALL | RNWEIR | SLUICE | SPILL | WEIR | FLOODPLAIN
|
|
34
35
|
)
|
|
35
36
|
TUnsupported: TypeAlias = UNSUPPORTED
|
floodmodeller_api/units/_base.py
CHANGED
|
@@ -18,7 +18,9 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
""" Holds the base unit class for all FM Units """
|
|
20
20
|
|
|
21
|
+
import contextlib
|
|
21
22
|
import logging
|
|
23
|
+
import re
|
|
22
24
|
from itertools import chain
|
|
23
25
|
from typing import Any
|
|
24
26
|
|
|
@@ -26,7 +28,7 @@ import pandas as pd
|
|
|
26
28
|
|
|
27
29
|
from ..diff import check_item_with_dataframe_equal
|
|
28
30
|
from ..to_from_json import Jsonable
|
|
29
|
-
from ._helpers import join_10_char, join_n_char_ljust, split_10_char, to_float,
|
|
31
|
+
from ._helpers import join_10_char, join_n_char_ljust, split_10_char, to_float, to_str
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class Unit(Jsonable):
|
|
@@ -138,6 +140,10 @@ class Unit(Jsonable):
|
|
|
138
140
|
|
|
139
141
|
result = True
|
|
140
142
|
diff = []
|
|
143
|
+
# Reset data attributes before checking equivalent
|
|
144
|
+
with contextlib.suppress(AttributeError):
|
|
145
|
+
_ = self.data, other.data
|
|
146
|
+
|
|
141
147
|
result, diff = check_item_with_dataframe_equal(
|
|
142
148
|
self.__dict__,
|
|
143
149
|
other.__dict__,
|
|
@@ -257,7 +263,7 @@ class Unit(Jsonable):
|
|
|
257
263
|
def _remove_unit_name(self, line: str, *, remove_revision: bool = False) -> str:
|
|
258
264
|
line = line.replace(self._unit, "", 1)
|
|
259
265
|
if remove_revision:
|
|
260
|
-
line =
|
|
266
|
+
line = re.sub(r"^\s*#revision#\d+\s*", "", line)
|
|
261
267
|
return line.strip()
|
|
262
268
|
|
|
263
269
|
def _create_header(self, *, include_revision: bool = False) -> str:
|
|
@@ -272,10 +278,16 @@ class Unit(Jsonable):
|
|
|
272
278
|
return line.split(" ")[0].strip()
|
|
273
279
|
|
|
274
280
|
def _get_revision_and_comment(self, line: str) -> tuple[int | None, str]:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
281
|
+
unit = re.escape(self._unit)
|
|
282
|
+
pattern = rf"^{unit}(?:\s+#revision#(\d+))?(?:\s+(.*))?$"
|
|
283
|
+
|
|
284
|
+
match = re.match(pattern, line.strip())
|
|
285
|
+
if not match:
|
|
286
|
+
return None, ""
|
|
287
|
+
|
|
288
|
+
revision_str, comment = match.groups()
|
|
289
|
+
revision = int(revision_str) if revision_str else None
|
|
290
|
+
return revision, comment or ""
|
|
279
291
|
|
|
280
292
|
def _enforce_dataframe(self, data: Any, columns: tuple[str, ...]) -> pd.DataFrame:
|
|
281
293
|
return data if isinstance(data, pd.DataFrame) else pd.DataFrame([], columns=columns)
|
|
@@ -112,7 +112,7 @@ class RIVER(Unit):
|
|
|
112
112
|
|
|
113
113
|
self._subtype = riv_block[1].split(" ")[0].strip()
|
|
114
114
|
# Extends label line to be correct length before splitting to pick up blank labels
|
|
115
|
-
labels = split_n_char(f"{riv_block[2]:<{7*self._label_len}}", self._label_len)
|
|
115
|
+
labels = split_n_char(f"{riv_block[2]:<{7 * self._label_len}}", self._label_len)
|
|
116
116
|
|
|
117
117
|
# Only supporting 'SECTION' subtype for now
|
|
118
118
|
if self.subtype == "SECTION":
|
|
@@ -203,7 +203,7 @@ class RIVER(Unit):
|
|
|
203
203
|
self.lat4,
|
|
204
204
|
)
|
|
205
205
|
# Manual so slope can have more sf
|
|
206
|
-
params = f
|
|
206
|
+
params = f"{self.dist_to_next:>10.3f}{'':>10}{self.slope:>10.6f}{self.density:>10.3f}"
|
|
207
207
|
self.nrows = len(self._data)
|
|
208
208
|
riv_block = [header, self.subtype, labels, params, f"{self.nrows!s:>10}"]
|
|
209
209
|
|
|
@@ -373,7 +373,7 @@ class INTERPOLATE(Unit):
|
|
|
373
373
|
"""Function to read a given INTERPOLATE WEIR block and store data as class attributes"""
|
|
374
374
|
|
|
375
375
|
# Extends label line to be correct length before splitting to pick up blank labels
|
|
376
|
-
labels = split_n_char(f"{block[1]:<{7*self._label_len}}", self._label_len)
|
|
376
|
+
labels = split_n_char(f"{block[1]:<{7 * self._label_len}}", self._label_len)
|
|
377
377
|
self.name = labels[0]
|
|
378
378
|
self.first_spill = labels[1]
|
|
379
379
|
self.second_spill = labels[2]
|
|
@@ -468,7 +468,7 @@ class REPLICATE(Unit):
|
|
|
468
468
|
"""Function to read a given REPLICATE block and store data as class attributes"""
|
|
469
469
|
|
|
470
470
|
# Extends label line to be correct length before splitting to pick up blank labels
|
|
471
|
-
labels = split_n_char(f"{block[1]:<{7*self._label_len}}", self._label_len)
|
|
471
|
+
labels = split_n_char(f"{block[1]:<{7 * self._label_len}}", self._label_len)
|
|
472
472
|
self.name = labels[0]
|
|
473
473
|
self.first_spill = labels[1]
|
|
474
474
|
self.second_spill = labels[2]
|
|
@@ -696,7 +696,7 @@ class SLUICE(Unit):
|
|
|
696
696
|
)
|
|
697
697
|
|
|
698
698
|
# Third parameter line
|
|
699
|
-
self.ngates = len(self.gates)
|
|
699
|
+
self.ngates = len(self.gates) if hasattr(self, "gates") else self.ngates
|
|
700
700
|
params3 = join_10_char(self.ngates)
|
|
701
701
|
|
|
702
702
|
if self.subtype == "RADIAL":
|
|
@@ -1518,3 +1518,145 @@ class OUTFALL(Unit):
|
|
|
1518
1518
|
"modular_limit": modular_limit,
|
|
1519
1519
|
}.items():
|
|
1520
1520
|
setattr(self, param, val)
|
|
1521
|
+
|
|
1522
|
+
|
|
1523
|
+
class FLOODPLAIN(Unit):
|
|
1524
|
+
"""Class to hold and process FLOODPLAIN unit type.
|
|
1525
|
+
|
|
1526
|
+
Args:
|
|
1527
|
+
name (str, optional): FLOODPLAIN section name
|
|
1528
|
+
comment (str, optional): Comment included in unit
|
|
1529
|
+
ds_label (str, optional): Downstream node label
|
|
1530
|
+
data (pandas.Dataframe, optional): Dataframe object containing all the floodplain section data as well as all other relevant data.
|
|
1531
|
+
Columns are ``'X', 'Y', 'Mannings n', 'Easting', 'Northing'``
|
|
1532
|
+
calibration_coefficient (float, optional): Weir coefficient (includes discharge, velocity and calibration coefficients, optional)
|
|
1533
|
+
modular_limit (float, optional): Ratio of upstream and downstream heads when switching between free and drowned mode
|
|
1534
|
+
upstream_separation (float, optional): Distance from centre of upstream cell to section (m)
|
|
1535
|
+
downstream_separation (float, optional): Distance from section to centre of downstream cell (m)
|
|
1536
|
+
force_friction_flow (bool, optional): Force friction flow for all segments
|
|
1537
|
+
ds_area_constraint (float, optional): Minimum value of downstream area (relative to upstream area) when Manning's equation applies. Typical value 0.1.
|
|
1538
|
+
|
|
1539
|
+
Returns:
|
|
1540
|
+
FLOODPLAIN: Flood Modeller FLOODPLAIN Unit class object
|
|
1541
|
+
"""
|
|
1542
|
+
|
|
1543
|
+
_unit = "FLOODPLAIN"
|
|
1544
|
+
_required_columns = (
|
|
1545
|
+
"X",
|
|
1546
|
+
"Y",
|
|
1547
|
+
"Mannings n",
|
|
1548
|
+
"Easting",
|
|
1549
|
+
"Northing",
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
def _read(self, fp_block):
|
|
1553
|
+
"""Function to read a given FLOODPLAIN block and store data as class attributes."""
|
|
1554
|
+
|
|
1555
|
+
self._subtype = self._get_first_word(fp_block[1])
|
|
1556
|
+
# Extends label line to be correct length before splitting to pick up blank labels
|
|
1557
|
+
labels = split_n_char(f"{fp_block[2]:<{7 * self._label_len}}", self._label_len)
|
|
1558
|
+
self.name = labels[0]
|
|
1559
|
+
self.ds_label = labels[1]
|
|
1560
|
+
self.comment = self._remove_unit_name(fp_block[0])
|
|
1561
|
+
|
|
1562
|
+
params = split_10_char(f"{fp_block[3]:<60}")
|
|
1563
|
+
self.calibration_coefficient = to_float(params[0])
|
|
1564
|
+
self.modular_limit = to_float(params[1])
|
|
1565
|
+
self.upstream_separation = to_float(params[2])
|
|
1566
|
+
self.downstream_separation = to_float(params[3])
|
|
1567
|
+
self.force_friction_flow = params[4].upper() == "FRICTION"
|
|
1568
|
+
self.ds_area_constraint = to_float(params[5])
|
|
1569
|
+
|
|
1570
|
+
self.nrows = int(split_10_char(fp_block[4])[0])
|
|
1571
|
+
data_list = []
|
|
1572
|
+
for row in fp_block[5:]:
|
|
1573
|
+
row_split = split_10_char(f"{row:<50}")
|
|
1574
|
+
x = to_float(row_split[0]) # chainage
|
|
1575
|
+
y = to_float(row_split[1]) # elevation
|
|
1576
|
+
n = to_float(row_split[2]) # Mannings
|
|
1577
|
+
easting = to_float(row_split[3]) # easting
|
|
1578
|
+
northing = to_float(row_split[4]) # northing
|
|
1579
|
+
|
|
1580
|
+
data_list.append(
|
|
1581
|
+
[
|
|
1582
|
+
x,
|
|
1583
|
+
y,
|
|
1584
|
+
n,
|
|
1585
|
+
easting,
|
|
1586
|
+
northing,
|
|
1587
|
+
],
|
|
1588
|
+
)
|
|
1589
|
+
self._data = pd.DataFrame(
|
|
1590
|
+
data_list,
|
|
1591
|
+
columns=self._required_columns,
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
def _write(self):
|
|
1595
|
+
"""Function to write a valid FLOODPLAIN block"""
|
|
1596
|
+
|
|
1597
|
+
# Function to check the params are valid for FLOODPLAIN SECTION unit
|
|
1598
|
+
_validate_unit(self)
|
|
1599
|
+
header = self._create_header()
|
|
1600
|
+
labels = join_n_char_ljust(self._label_len, self.name, self.ds_label)
|
|
1601
|
+
# Manual so slope can have more sf
|
|
1602
|
+
params = join_10_char(
|
|
1603
|
+
self.calibration_coefficient,
|
|
1604
|
+
self.modular_limit,
|
|
1605
|
+
self.upstream_separation,
|
|
1606
|
+
self.downstream_separation,
|
|
1607
|
+
"FRICTION" if self.force_friction_flow else "",
|
|
1608
|
+
self.ds_area_constraint,
|
|
1609
|
+
)
|
|
1610
|
+
self.nrows = len(self._data)
|
|
1611
|
+
return [header, self.subtype, labels, params, *write_dataframe(self.nrows, self._data)]
|
|
1612
|
+
|
|
1613
|
+
def _create_from_blank( # noqa: PLR0913
|
|
1614
|
+
self,
|
|
1615
|
+
name="new_floodplain",
|
|
1616
|
+
comment="",
|
|
1617
|
+
ds_label="",
|
|
1618
|
+
data=None,
|
|
1619
|
+
calibration_coefficient=1.0,
|
|
1620
|
+
modular_limit=0.8,
|
|
1621
|
+
upstream_separation=0.0,
|
|
1622
|
+
downstream_separation=0.0,
|
|
1623
|
+
force_friction_flow=False,
|
|
1624
|
+
ds_area_constraint=0.1,
|
|
1625
|
+
):
|
|
1626
|
+
# Initiate new FLOODPLAIN (currently hardcoding this as default)
|
|
1627
|
+
self._subtype = "SECTION"
|
|
1628
|
+
|
|
1629
|
+
for param, val in {
|
|
1630
|
+
"name": name,
|
|
1631
|
+
"comment": comment,
|
|
1632
|
+
"ds_label": ds_label,
|
|
1633
|
+
"calibration_coefficient": calibration_coefficient,
|
|
1634
|
+
"modular_limit": modular_limit,
|
|
1635
|
+
"upstream_separation": upstream_separation,
|
|
1636
|
+
"downstream_separation": downstream_separation,
|
|
1637
|
+
"force_friction_flow": force_friction_flow,
|
|
1638
|
+
"ds_area_constraint": ds_area_constraint,
|
|
1639
|
+
}.items():
|
|
1640
|
+
setattr(self, param, val)
|
|
1641
|
+
|
|
1642
|
+
self._data = self._enforce_dataframe(data, self._required_columns)
|
|
1643
|
+
|
|
1644
|
+
@property
|
|
1645
|
+
def data(self) -> pd.DataFrame:
|
|
1646
|
+
"""Data table for the FLOODPLAIN cross section.
|
|
1647
|
+
|
|
1648
|
+
Returns:
|
|
1649
|
+
pd.DataFrame: Pandas dataframe for the cross section data with columns: 'X', 'Y',
|
|
1650
|
+
'Mannings n','Easting', 'Northing'
|
|
1651
|
+
"""
|
|
1652
|
+
return self._data
|
|
1653
|
+
|
|
1654
|
+
@data.setter
|
|
1655
|
+
def data(self, new_df: pd.DataFrame) -> None:
|
|
1656
|
+
if not isinstance(new_df, pd.DataFrame):
|
|
1657
|
+
msg = "The updated data table for a floodplain section must be a pandas DataFrame."
|
|
1658
|
+
raise TypeError(msg)
|
|
1659
|
+
if list(map(str.lower, new_df.columns)) != list(map(str.lower, self._required_columns)):
|
|
1660
|
+
msg = f"The DataFrame must only contain columns: {self._required_columns}"
|
|
1661
|
+
raise ValueError(msg)
|
|
1662
|
+
self._data = new_df
|
floodmodeller_api/units/units.py
CHANGED
|
@@ -56,6 +56,7 @@ SUPPORTED_UNIT_TYPES: dict[str, SupportedUnitTypes] = {
|
|
|
56
56
|
"JUNCTION": {"group": "connectors", "has_subtype": True},
|
|
57
57
|
"LATERAL": {"group": "connectors", "has_subtype": False},
|
|
58
58
|
"RESERVOIR": {"group": "controls", "has_subtype": False},
|
|
59
|
+
"FLOODPLAIN": {"group": "structures", "has_subtype": True},
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
UNSUPPORTED_UNIT_TYPES: dict[str, UnsupportedUnitTypes] = {
|
|
@@ -65,7 +66,6 @@ UNSUPPORTED_UNIT_TYPES: dict[str, UnsupportedUnitTypes] = {
|
|
|
65
66
|
"FEHBDY": {"group": "boundaries", "has_subtype": False}, # RAINFALL RUNOFF METHOD boundary
|
|
66
67
|
"FLOOD RELIEF": {"group": None, "has_subtype": True}, # found in dat file
|
|
67
68
|
"FLOOD RELIEF ARCH": {"group": "structures", "has_subtype": True}, # found in FM help
|
|
68
|
-
"FLOODPLAIN": {"group": None, "has_subtype": True}, # floodplain section culvert
|
|
69
69
|
"FRQSIM": {"group": "boundaries", "has_subtype": False}, # flood FReQuency SIMulation
|
|
70
70
|
"FSRBDY": {"group": "boundaries", "has_subtype": False}, # FEH Rainfall Runoff Method
|
|
71
71
|
"FSSR16BDY": {"group": "boundaries", "has_subtype": False}, # FSSR16 Method
|
|
@@ -661,4 +661,20 @@ parameter_options = {
|
|
|
661
661
|
"type": "list-string-length",
|
|
662
662
|
"max_length": 12,
|
|
663
663
|
},
|
|
664
|
+
"upstream_separation": {
|
|
665
|
+
"type": "type-match",
|
|
666
|
+
"options": (float, int),
|
|
667
|
+
},
|
|
668
|
+
"downstream_separation": {
|
|
669
|
+
"type": "type-match",
|
|
670
|
+
"options": (float, int),
|
|
671
|
+
},
|
|
672
|
+
"force_friction_flow": {
|
|
673
|
+
"type": "type-match",
|
|
674
|
+
"options": (bool),
|
|
675
|
+
},
|
|
676
|
+
"ds_area_constraint": {
|
|
677
|
+
"type": "type-match",
|
|
678
|
+
"options": (float, int),
|
|
679
|
+
},
|
|
664
680
|
}
|
floodmodeller_api/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.5.
|
|
1
|
+
__version__ = "0.5.3.post1"
|