floodmodeller-api 0.4.4.post1__py3-none-any.whl → 0.5.0.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 +1 -0
- floodmodeller_api/dat.py +117 -96
- floodmodeller_api/hydrology_plus/__init__.py +2 -0
- floodmodeller_api/hydrology_plus/helper.py +23 -0
- floodmodeller_api/hydrology_plus/hydrology_plus_export.py +333 -0
- floodmodeller_api/ied.py +93 -90
- floodmodeller_api/ief.py +233 -50
- floodmodeller_api/ief_flags.py +1 -0
- floodmodeller_api/logs/lf.py +5 -1
- floodmodeller_api/mapping.py +2 -0
- floodmodeller_api/test/test_conveyance.py +23 -32
- floodmodeller_api/test/test_data/7082.ief +28 -0
- floodmodeller_api/test/test_data/BaseModel_2D_Q100.ief +28 -0
- floodmodeller_api/test/test_data/Baseline_unchecked.csv +77 -0
- floodmodeller_api/test/test_data/Constant QT.ief +19 -0
- floodmodeller_api/test/test_data/Domain1_Q_xml_expected.json +7 -7
- floodmodeller_api/test/test_data/EX18_DAT_expected.json +54 -38
- floodmodeller_api/test/test_data/EX3_DAT_expected.json +246 -166
- floodmodeller_api/test/test_data/EX3_IEF_expected.json +25 -20
- floodmodeller_api/test/test_data/EX6_DAT_expected.json +522 -350
- floodmodeller_api/test/test_data/FEH boundary.ief +23 -0
- floodmodeller_api/test/test_data/Linked1D2D_xml_expected.json +7 -7
- floodmodeller_api/test/test_data/P3Panels_UNsteady.ief +25 -0
- floodmodeller_api/test/test_data/QT in dat file.ief +20 -0
- floodmodeller_api/test/test_data/T10.ief +25 -0
- floodmodeller_api/test/test_data/T2.ief +25 -0
- floodmodeller_api/test/test_data/T5.ief +25 -0
- floodmodeller_api/test/test_data/df_flows_hplus.csv +56 -0
- floodmodeller_api/test/test_data/event_hplus.csv +56 -0
- floodmodeller_api/test/test_data/ex4.ief +20 -0
- floodmodeller_api/test/test_data/ex6.ief +21 -0
- floodmodeller_api/test/test_data/example_h+_export.csv +77 -0
- floodmodeller_api/test/test_data/hplus_export_example_1.csv +72 -0
- floodmodeller_api/test/test_data/hplus_export_example_10.csv +77 -0
- floodmodeller_api/test/test_data/hplus_export_example_2.csv +79 -0
- floodmodeller_api/test/test_data/hplus_export_example_3.csv +77 -0
- floodmodeller_api/test/test_data/hplus_export_example_4.csv +131 -0
- floodmodeller_api/test/test_data/hplus_export_example_5.csv +77 -0
- floodmodeller_api/test/test_data/hplus_export_example_6.csv +131 -0
- floodmodeller_api/test/test_data/hplus_export_example_7.csv +131 -0
- floodmodeller_api/test/test_data/hplus_export_example_8.csv +131 -0
- floodmodeller_api/test/test_data/hplus_export_example_9.csv +131 -0
- floodmodeller_api/test/test_data/network_dat_expected.json +312 -210
- floodmodeller_api/test/test_data/network_ied_expected.json +6 -6
- floodmodeller_api/test/test_data/network_with_comments.ied +55 -0
- floodmodeller_api/test/test_flowtimeprofile.py +133 -0
- floodmodeller_api/test/test_hydrology_plus_export.py +210 -0
- floodmodeller_api/test/test_ied.py +12 -0
- floodmodeller_api/test/test_ief.py +49 -9
- floodmodeller_api/test/test_json.py +6 -1
- floodmodeller_api/test/test_read_file.py +27 -0
- floodmodeller_api/test/test_river.py +246 -0
- floodmodeller_api/to_from_json.py +7 -1
- floodmodeller_api/tool.py +6 -10
- floodmodeller_api/units/__init__.py +11 -1
- floodmodeller_api/units/conveyance.py +103 -212
- floodmodeller_api/units/sections.py +120 -39
- floodmodeller_api/util.py +2 -0
- floodmodeller_api/version.py +1 -1
- floodmodeller_api/xml2d.py +20 -13
- floodmodeller_api/xsd_backup.xml +738 -0
- {floodmodeller_api-0.4.4.post1.dist-info → floodmodeller_api-0.5.0.post1.dist-info}/METADATA +2 -1
- {floodmodeller_api-0.4.4.post1.dist-info → floodmodeller_api-0.5.0.post1.dist-info}/RECORD +67 -33
- {floodmodeller_api-0.4.4.post1.dist-info → floodmodeller_api-0.5.0.post1.dist-info}/WHEEL +1 -1
- {floodmodeller_api-0.4.4.post1.dist-info → floodmodeller_api-0.5.0.post1.dist-info}/LICENSE.txt +0 -0
- {floodmodeller_api-0.4.4.post1.dist-info → floodmodeller_api-0.5.0.post1.dist-info}/entry_points.txt +0 -0
- {floodmodeller_api-0.4.4.post1.dist-info → floodmodeller_api-0.5.0.post1.dist-info}/top_level.txt +0 -0
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from functools import lru_cache
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from numpy.typing import NDArray
|
|
8
11
|
|
|
9
12
|
MINIMUM_PERIMETER_THRESHOLD = 1e-8
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
def calculate_cross_section_conveyance(
|
|
13
|
-
x: np.
|
|
14
|
-
y: np.
|
|
15
|
-
n: np.
|
|
16
|
-
rpl: np.
|
|
17
|
-
panel_markers: np.
|
|
16
|
+
x: NDArray[np.float64],
|
|
17
|
+
y: NDArray[np.float64],
|
|
18
|
+
n: NDArray[np.float64],
|
|
19
|
+
rpl: NDArray[np.float64],
|
|
20
|
+
panel_markers: NDArray[np.float64],
|
|
18
21
|
) -> pd.Series:
|
|
19
22
|
"""
|
|
20
23
|
Calculate the conveyance of a cross-section by summing the conveyance
|
|
21
24
|
across all panels defined by panel markers.
|
|
22
25
|
|
|
23
26
|
Args:
|
|
24
|
-
x (np.
|
|
25
|
-
y (np.
|
|
26
|
-
n (np.
|
|
27
|
-
rpl (np.
|
|
28
|
-
panel_markers (np.
|
|
27
|
+
x (NDArray[np.float64]): The x-coordinates of the cross-section.
|
|
28
|
+
y (NDArray[np.float64]): The y-coordinates of the cross-section.
|
|
29
|
+
n (NDArray[np.float64]): Manning's n values for each segment.
|
|
30
|
+
rpl (NDArray[np.float64]): Relative Path Length values for each segment.
|
|
31
|
+
panel_markers (NDArray[np.float64]): Boolean array indicating the start of each panel.
|
|
29
32
|
|
|
30
33
|
Returns:
|
|
31
34
|
pd.Series: A pandas Series containing the conveyance values indexed by water levels.
|
|
@@ -41,164 +44,117 @@ def calculate_cross_section_conveyance(
|
|
|
41
44
|
result = calculate_cross_section_conveyance(x, y, n, rpl, panel_markers)
|
|
42
45
|
print(result)
|
|
43
46
|
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Panel markers are forced true to the bounds to make the process work
|
|
49
|
-
panel_markers = np.array([True, *panel_markers[1:-1], True])
|
|
50
|
-
panel_indices = np.where(panel_markers)[0]
|
|
51
|
-
conveyance_by_panel = []
|
|
52
|
-
for panel_start, panel_end in zip(panel_indices[:-1], panel_indices[1:] + 1):
|
|
53
|
-
panel_x = x[panel_start:panel_end]
|
|
54
|
-
panel_y = y[panel_start:panel_end]
|
|
55
|
-
panel_n = n[panel_start:panel_end]
|
|
56
|
-
# RPL value is only valid for the start of a panel, and set to 1 if it's zero
|
|
57
|
-
panel_rpl = (
|
|
58
|
-
1.0
|
|
59
|
-
if (panel_start == 0 and not panel_markers[0]) or rpl[panel_start] == 0
|
|
60
|
-
else float(rpl[panel_start])
|
|
61
|
-
)
|
|
62
|
-
conveyance_by_panel.append(
|
|
63
|
-
calculate_conveyance_by_panel(panel_x, panel_y, panel_n, panel_rpl, wls),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# Sum conveyance across panels
|
|
67
|
-
conveyance_values = [sum(values) for values in zip(*conveyance_by_panel)]
|
|
68
|
-
|
|
69
|
-
return pd.Series(data=conveyance_values, index=wls)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def calculate_conveyance_by_panel(
|
|
73
|
-
x: np.ndarray,
|
|
74
|
-
y: np.ndarray,
|
|
75
|
-
n: np.ndarray,
|
|
76
|
-
rpl: float,
|
|
77
|
-
wls: np.ndarray,
|
|
78
|
-
) -> list[float]:
|
|
79
|
-
"""
|
|
80
|
-
Calculate the conveyance for a single panel of a cross-section at specified water levels.
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
x (np.ndarray): The x-coordinates of the panel.
|
|
84
|
-
y (np.ndarray): The y-coordinates of the panel.
|
|
85
|
-
n (np.ndarray): Manning's n values for each segment in the panel.
|
|
86
|
-
rpl (float): Relative Path Length for each segment in the panel.
|
|
87
|
-
wls (np.ndarray): The water levels at which to calculate conveyance.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
list[float]: A list of conveyance values for each water level.
|
|
91
|
-
"""
|
|
47
|
+
water_levels = insert_intermediate_wls(np.unique(y), threshold=0.05)
|
|
48
|
+
area, length, mannings = calculate_geometry(x, y, n, water_levels)
|
|
49
|
+
panel = panel_markers.cumsum()[:-1]
|
|
92
50
|
|
|
93
|
-
|
|
94
|
-
|
|
51
|
+
intersection = (y[:-2] < water_levels[:, np.newaxis]) & (y[1:-1] >= water_levels[:, np.newaxis])
|
|
52
|
+
section_markers = np.hstack([np.full((intersection.shape[0], 1), False), intersection])
|
|
53
|
+
section = section_markers.cumsum(axis=1)
|
|
95
54
|
|
|
96
|
-
|
|
97
|
-
x = np.array([x[0], *x, x[-1]])
|
|
98
|
-
y = np.array([max_y, *y, max_y])
|
|
99
|
-
n = np.array([0, *n, 0])
|
|
55
|
+
conveyance = np.zeros_like(water_levels)
|
|
100
56
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# Define linestring geometries representing glass walls, so they can be subtracted later
|
|
106
|
-
glass_walls = (
|
|
107
|
-
LineString(zip([x[0], x[1]], [y[0], y[1]])), # left
|
|
108
|
-
LineString(zip([x[-2], x[-1]], [y[-2], y[-1]])), # right
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
# Remove glass wall sections from coords
|
|
112
|
-
x, y, n = x[1:-1], y[1:-1], n[1:-1]
|
|
113
|
-
|
|
114
|
-
conveyance_values = []
|
|
115
|
-
for wl in wls:
|
|
116
|
-
if wl <= np.min(y):
|
|
117
|
-
# no channel capacity (essentially zero depth) so no need to calculate
|
|
118
|
-
conveyance_values.append(0.0)
|
|
57
|
+
for i in range(panel.max() + 1):
|
|
58
|
+
in_panel = panel == i
|
|
59
|
+
if not in_panel.any():
|
|
119
60
|
continue
|
|
120
61
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
water_plane = intersection(channel_polygon, LineString(zip([start, end], [wl, wl])))
|
|
124
|
-
wetted_polygon = intersection(channel_polygon, water_surface)
|
|
62
|
+
rpl_panel = np.sqrt(rpl[:-1][in_panel][0])
|
|
63
|
+
rpl_panel = 1 if rpl_panel == 0 else rpl_panel
|
|
125
64
|
|
|
126
|
-
|
|
127
|
-
|
|
65
|
+
for j in range(section.max() + 1):
|
|
66
|
+
in_section = section == j
|
|
67
|
+
in_panel_and_section = in_panel & in_section
|
|
68
|
+
if not in_panel_and_section.any():
|
|
69
|
+
continue
|
|
128
70
|
|
|
129
|
-
|
|
71
|
+
total_area = np.where(in_panel_and_section, area, 0).sum(axis=1)
|
|
72
|
+
total_length = np.where(in_panel_and_section, length, 0).sum(axis=1)
|
|
73
|
+
total_mannings = np.where(in_panel_and_section, mannings, 0).sum(axis=1)
|
|
130
74
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
75
|
+
with np.errstate(invalid="ignore"):
|
|
76
|
+
conveyance += np.where(
|
|
77
|
+
total_length >= MINIMUM_PERIMETER_THRESHOLD,
|
|
78
|
+
total_area ** (5 / 3) * total_length ** (1 / 3) / (total_mannings * rpl_panel),
|
|
79
|
+
0,
|
|
80
|
+
)
|
|
136
81
|
|
|
137
|
-
return
|
|
82
|
+
return pd.Series(conveyance, index=water_levels)
|
|
138
83
|
|
|
139
84
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
rpl: float,
|
|
147
|
-
) -> float:
|
|
85
|
+
def calculate_geometry(
|
|
86
|
+
x: NDArray[np.float64],
|
|
87
|
+
y: NDArray[np.float64],
|
|
88
|
+
n: NDArray[np.float64],
|
|
89
|
+
water_levels: NDArray[np.float64],
|
|
90
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
148
91
|
"""
|
|
149
|
-
Calculate
|
|
92
|
+
Calculate area, length, weighted mannings for piecewise linear curve (x, y) below water_level.
|
|
150
93
|
|
|
151
94
|
Args:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
x (np.ndarray): 1D array of channel chainage
|
|
157
|
-
n (np.ndarray): 1D array of channel mannings
|
|
158
|
-
rpl (float): Relative path length of panel
|
|
95
|
+
x (NDArray[np.float64]): 1D array of x-coordinates.
|
|
96
|
+
y (NDArray[np.float64]): 1D array of y-coordinates.
|
|
97
|
+
n (NDArray[np.float64]): 1D array to integrate over the length.
|
|
98
|
+
water_levels (NDArray[np.float64]): The horizontal reference line.
|
|
159
99
|
|
|
160
100
|
Returns:
|
|
161
|
-
|
|
101
|
+
NDArray[np.float64]: The area above the curve and under the reference line.
|
|
102
|
+
NDArray[np.float64]: The length of the curve under the reference line.
|
|
103
|
+
NDArray[np.float64]: Manning's n integrated along the curve under the reference line.
|
|
162
104
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
105
|
+
h = water_levels[:, np.newaxis] - y
|
|
106
|
+
|
|
107
|
+
x1 = x[:-1]
|
|
108
|
+
x2 = x[1:]
|
|
109
|
+
h1 = h[:, :-1]
|
|
110
|
+
h2 = h[:, 1:]
|
|
111
|
+
n1 = n[:-1]
|
|
112
|
+
|
|
113
|
+
dx = x2 - x1
|
|
114
|
+
|
|
115
|
+
is_submerged = (h1 > 0) & (h2 > 0)
|
|
116
|
+
is_submerged_on_left = (h1 > 0) & (h2 <= 0)
|
|
117
|
+
is_submerged_on_right = (h1 <= 0) & (h2 > 0)
|
|
118
|
+
conditions = [is_submerged, is_submerged_on_left, is_submerged_on_right]
|
|
119
|
+
|
|
120
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
121
|
+
# needed for partially submerged sections
|
|
122
|
+
dx_left = dx * h1 / (h1 - h2)
|
|
123
|
+
dx_right = dx * h2 / (h2 - h1)
|
|
124
|
+
|
|
125
|
+
area = np.select(
|
|
126
|
+
conditions,
|
|
127
|
+
[
|
|
128
|
+
0.5 * dx * (h1 + h2),
|
|
129
|
+
0.5 * dx_left * h1,
|
|
130
|
+
0.5 * dx_right * h2,
|
|
131
|
+
],
|
|
132
|
+
default=0,
|
|
171
133
|
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
wetted_polyline: LineString = (
|
|
182
|
-
wetted_polygon.exterior.difference(water_plane_clip)
|
|
183
|
-
.difference(glass_wall_left_clip)
|
|
184
|
-
.difference(glass_wall_right_clip)
|
|
134
|
+
length = np.select(
|
|
135
|
+
conditions,
|
|
136
|
+
[
|
|
137
|
+
np.sqrt((h2 - h1) ** 2 + dx**2),
|
|
138
|
+
np.sqrt(h1**2 + dx_left**2),
|
|
139
|
+
np.sqrt(h2**2 + dx_right**2),
|
|
140
|
+
],
|
|
141
|
+
default=0,
|
|
185
142
|
)
|
|
186
|
-
weighted_mannings =
|
|
143
|
+
weighted_mannings = n1 * length
|
|
187
144
|
|
|
188
|
-
|
|
189
|
-
return (area ** (5 / 3) / wetted_perimeter ** (2 / 3)) * (wetted_perimeter / weighted_mannings)
|
|
145
|
+
return area, length, weighted_mannings
|
|
190
146
|
|
|
191
147
|
|
|
192
|
-
def insert_intermediate_wls(arr: np.
|
|
148
|
+
def insert_intermediate_wls(arr: NDArray[np.float64], threshold: float) -> NDArray[np.float64]:
|
|
193
149
|
"""
|
|
194
150
|
Insert intermediate water levels into an array based on a threshold.
|
|
195
151
|
|
|
196
152
|
Args:
|
|
197
|
-
arr (np.
|
|
153
|
+
arr (NDArray[np.float64]): The array of original water levels.
|
|
198
154
|
threshold (float): The maximum allowed gap between water levels.
|
|
199
155
|
|
|
200
156
|
Returns:
|
|
201
|
-
np.
|
|
157
|
+
NDArray[np.float64]: The array with intermediate water levels inserted.
|
|
202
158
|
"""
|
|
203
159
|
# Calculate gaps between consecutive elements
|
|
204
160
|
gaps = np.diff(arr)
|
|
@@ -207,81 +163,16 @@ def insert_intermediate_wls(arr: np.ndarray, threshold: float):
|
|
|
207
163
|
num_points = (gaps // threshold).astype(int)
|
|
208
164
|
|
|
209
165
|
# Prepare lists to hold the new points and results
|
|
210
|
-
new_points = [
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
new_points.extend(points)
|
|
217
|
-
new_points.append(end)
|
|
218
|
-
|
|
219
|
-
# Combine the original starting point with the new points
|
|
220
|
-
return np.array([arr[0]] + new_points)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def calculate_weighted_mannings(
|
|
224
|
-
x: np.ndarray,
|
|
225
|
-
n: np.ndarray,
|
|
226
|
-
rpl: float,
|
|
227
|
-
wetted_polyline: LineString,
|
|
228
|
-
) -> float:
|
|
229
|
-
"""Calculate the weighted Manning's n value for a wetted polyline."""
|
|
230
|
-
|
|
231
|
-
# We want the polyline to be split into each individual segment
|
|
232
|
-
segments = line_to_segments(wetted_polyline)
|
|
233
|
-
weighted_mannings = 0
|
|
234
|
-
for segment in segments:
|
|
235
|
-
mannings_value = get_mannings_by_segment_x_coords(
|
|
236
|
-
x,
|
|
237
|
-
n,
|
|
238
|
-
segment.coords[0][0],
|
|
239
|
-
segment.coords[1][0],
|
|
240
|
-
)
|
|
241
|
-
weighted_mannings += mannings_value * segment.length * np.sqrt(rpl)
|
|
242
|
-
|
|
243
|
-
return weighted_mannings
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def line_to_segments(line: LineString | MultiLineString) -> list[LineString]:
|
|
247
|
-
"""Convert a LineString or MultiLineString into a list of LineString segments."""
|
|
248
|
-
if isinstance(line, LineString):
|
|
249
|
-
segments = []
|
|
250
|
-
for start, end in zip(line.coords[:-1], line.coords[1:]):
|
|
251
|
-
points = sorted([start, end], key=lambda x: x[0])
|
|
252
|
-
segments.append(LineString(points))
|
|
253
|
-
return segments
|
|
254
|
-
if isinstance(line, MultiLineString):
|
|
255
|
-
segments = []
|
|
256
|
-
for linestring in line.geoms:
|
|
257
|
-
segments.extend(line_to_segments(linestring))
|
|
258
|
-
return segments
|
|
259
|
-
raise TypeError("Input must be a LineString or MultiLineString")
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def get_mannings_by_segment_x_coords(
|
|
263
|
-
x: np.ndarray,
|
|
264
|
-
n: np.ndarray,
|
|
265
|
-
start_x: float,
|
|
266
|
-
end_x: float,
|
|
267
|
-
) -> float:
|
|
268
|
-
"""Get the Manning's n or RPL value for a segment based on its start x-coordinate."""
|
|
269
|
-
|
|
270
|
-
# This method doesn't handle cases where we have multiple manning's values at a vertical section
|
|
271
|
-
# and will always just take the first at any verticle, but it is probably quite rare for this
|
|
272
|
-
# not to be the case
|
|
273
|
-
if start_x == end_x:
|
|
274
|
-
# Vertical segment take first x match
|
|
275
|
-
index = np.searchsorted(x, start_x) - (start_x not in x)
|
|
276
|
-
else:
|
|
277
|
-
# Otherwise non-vertical segment, take last match
|
|
278
|
-
index = np.searchsorted(x, start_x, side="right") - 1
|
|
279
|
-
|
|
280
|
-
return n[index]
|
|
166
|
+
new_points = [
|
|
167
|
+
np.linspace(start, end, num + 2, endpoint=False)
|
|
168
|
+
for start, end, num in zip(arr[:-1], arr[1:], num_points)
|
|
169
|
+
]
|
|
170
|
+
end = np.array([arr[-1]])
|
|
171
|
+
return np.concatenate([*new_points, end])
|
|
281
172
|
|
|
282
173
|
|
|
283
174
|
@lru_cache
|
|
284
|
-
def
|
|
175
|
+
def calculate_cross_section_conveyance_cached(
|
|
285
176
|
x: tuple[float],
|
|
286
177
|
y: tuple[float],
|
|
287
178
|
n: tuple[float],
|
|
@@ -19,7 +19,7 @@ import pandas as pd
|
|
|
19
19
|
from floodmodeller_api.validation import _validate_unit
|
|
20
20
|
|
|
21
21
|
from ._base import Unit
|
|
22
|
-
from .conveyance import
|
|
22
|
+
from .conveyance import calculate_cross_section_conveyance_cached
|
|
23
23
|
from .helpers import (
|
|
24
24
|
_to_float,
|
|
25
25
|
_to_int,
|
|
@@ -51,12 +51,21 @@ class RIVER(Unit):
|
|
|
51
51
|
|
|
52
52
|
Returns:
|
|
53
53
|
RIVER: Flood Modeller RIVER Unit class object
|
|
54
|
-
|
|
55
|
-
Methods:
|
|
56
|
-
convert_to_muskingham: Not currently supported but planned for future release
|
|
57
54
|
"""
|
|
58
55
|
|
|
59
56
|
_unit = "RIVER"
|
|
57
|
+
_required_columns = [
|
|
58
|
+
"X",
|
|
59
|
+
"Y",
|
|
60
|
+
"Mannings n",
|
|
61
|
+
"Panel",
|
|
62
|
+
"RPL",
|
|
63
|
+
"Marker",
|
|
64
|
+
"Easting",
|
|
65
|
+
"Northing",
|
|
66
|
+
"Deactivation",
|
|
67
|
+
"SP. Marker",
|
|
68
|
+
]
|
|
60
69
|
|
|
61
70
|
def _create_from_blank( # noqa: PLR0913
|
|
62
71
|
self,
|
|
@@ -88,29 +97,18 @@ class RIVER(Unit):
|
|
|
88
97
|
"dist_to_next": dist_to_next,
|
|
89
98
|
"slope": slope,
|
|
90
99
|
"density": density,
|
|
91
|
-
"data": data,
|
|
92
100
|
}.items():
|
|
93
101
|
setattr(self, param, val)
|
|
94
102
|
|
|
95
|
-
self.
|
|
103
|
+
self._data = (
|
|
96
104
|
data
|
|
97
105
|
if isinstance(data, pd.DataFrame)
|
|
98
106
|
else pd.DataFrame(
|
|
99
107
|
[],
|
|
100
|
-
columns=
|
|
101
|
-
"X",
|
|
102
|
-
"Y",
|
|
103
|
-
"Mannings n",
|
|
104
|
-
"Panel",
|
|
105
|
-
"RPL",
|
|
106
|
-
"Marker",
|
|
107
|
-
"Easting",
|
|
108
|
-
"Northing",
|
|
109
|
-
"Deactivation",
|
|
110
|
-
"SP. Marker",
|
|
111
|
-
],
|
|
108
|
+
columns=self._required_columns,
|
|
112
109
|
)
|
|
113
110
|
)
|
|
111
|
+
self._active_data = None
|
|
114
112
|
|
|
115
113
|
def _read(self, riv_block):
|
|
116
114
|
"""Function to read a given RIVER block and store data as class attributes."""
|
|
@@ -171,20 +169,9 @@ class RIVER(Unit):
|
|
|
171
169
|
sp_marker,
|
|
172
170
|
],
|
|
173
171
|
)
|
|
174
|
-
self.
|
|
172
|
+
self._data = pd.DataFrame(
|
|
175
173
|
data_list,
|
|
176
|
-
columns=
|
|
177
|
-
"X",
|
|
178
|
-
"Y",
|
|
179
|
-
"Mannings n",
|
|
180
|
-
"Panel",
|
|
181
|
-
"RPL",
|
|
182
|
-
"Marker",
|
|
183
|
-
"Easting",
|
|
184
|
-
"Northing",
|
|
185
|
-
"Deactivation",
|
|
186
|
-
"SP. Marker",
|
|
187
|
-
],
|
|
174
|
+
columns=self._required_columns,
|
|
188
175
|
)
|
|
189
176
|
|
|
190
177
|
else:
|
|
@@ -195,6 +182,8 @@ class RIVER(Unit):
|
|
|
195
182
|
self._raw_block = riv_block
|
|
196
183
|
self.name = riv_block[2][: self._label_len].strip()
|
|
197
184
|
|
|
185
|
+
self._active_data = None
|
|
186
|
+
|
|
198
187
|
def _write(self):
|
|
199
188
|
"""Function to write a valid RIVER block"""
|
|
200
189
|
|
|
@@ -214,7 +203,7 @@ class RIVER(Unit):
|
|
|
214
203
|
)
|
|
215
204
|
# Manual so slope can have more sf
|
|
216
205
|
params = f'{self.dist_to_next:>10.3f}{"":>10}{self.slope:>10.6f}{self.density:>10.3f}'
|
|
217
|
-
self.nrows = len(self.
|
|
206
|
+
self.nrows = len(self._data)
|
|
218
207
|
riv_block = [header, self.subtype, labels, params, f"{str(self.nrows):>10}"]
|
|
219
208
|
|
|
220
209
|
riv_data = []
|
|
@@ -230,7 +219,7 @@ class RIVER(Unit):
|
|
|
230
219
|
northing,
|
|
231
220
|
deactivation,
|
|
232
221
|
sp_marker,
|
|
233
|
-
) in self.
|
|
222
|
+
) in self._data.itertuples():
|
|
234
223
|
row = join_10_char(x, y, n)
|
|
235
224
|
if panel:
|
|
236
225
|
row += "*"
|
|
@@ -245,6 +234,36 @@ class RIVER(Unit):
|
|
|
245
234
|
|
|
246
235
|
return self._raw_block
|
|
247
236
|
|
|
237
|
+
@property
|
|
238
|
+
def data(self) -> pd.DataFrame:
|
|
239
|
+
"""Data table for the river cross section.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
pd.DataFrame: Pandas dataframe for the cross section data with columns: 'X', 'Y',
|
|
243
|
+
'Mannings n', 'Panel', 'RPL', 'Marker', 'Easting', 'Northing', 'Deactivation',
|
|
244
|
+
'SP. Marker'
|
|
245
|
+
"""
|
|
246
|
+
if self._active_data is None:
|
|
247
|
+
return self._data
|
|
248
|
+
|
|
249
|
+
# Replace the active section with the self._active_data df
|
|
250
|
+
left_bank_idx, right_bank_idx = self._get_left_right_active_index()
|
|
251
|
+
self._data = pd.concat(
|
|
252
|
+
[self._data[:left_bank_idx], self._active_data, self._data[right_bank_idx + 1 :]],
|
|
253
|
+
).reset_index(drop=True)
|
|
254
|
+
self._active_data = None
|
|
255
|
+
return self._data
|
|
256
|
+
|
|
257
|
+
@data.setter
|
|
258
|
+
def data(self, new_df: pd.DataFrame) -> None:
|
|
259
|
+
if not isinstance(new_df, pd.DataFrame):
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"The updated data table for a cross section must be a pandas DataFrame.",
|
|
262
|
+
)
|
|
263
|
+
if list(map(str.lower, new_df.columns)) != list(map(str.lower, self._required_columns)):
|
|
264
|
+
raise ValueError(f"The DataFrame must only contain columns: {self._required_columns}")
|
|
265
|
+
self._data = new_df
|
|
266
|
+
|
|
248
267
|
@property
|
|
249
268
|
def conveyance(self) -> pd.Series:
|
|
250
269
|
"""Calculate and return the conveyance curve of the cross-section.
|
|
@@ -257,14 +276,76 @@ class RIVER(Unit):
|
|
|
257
276
|
Returns:
|
|
258
277
|
pd.Series: A pandas Series containing the conveyance values indexed by water levels.
|
|
259
278
|
"""
|
|
260
|
-
return
|
|
261
|
-
x=tuple(self.
|
|
262
|
-
y=tuple(self.
|
|
263
|
-
n=tuple(self.
|
|
264
|
-
rpl=tuple(self.
|
|
265
|
-
panel_markers=tuple(self.
|
|
279
|
+
return calculate_cross_section_conveyance_cached(
|
|
280
|
+
x=tuple(self._data.X.values),
|
|
281
|
+
y=tuple(self._data.Y.values),
|
|
282
|
+
n=tuple(self._data["Mannings n"].values),
|
|
283
|
+
rpl=tuple(self._data.RPL.values),
|
|
284
|
+
panel_markers=tuple(self._data.Panel.values),
|
|
266
285
|
)
|
|
267
286
|
|
|
287
|
+
@property
|
|
288
|
+
def active_data(self) -> pd.DataFrame:
|
|
289
|
+
"""Data table for active subset of the river cross section, defined by deactivation markers.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
pd.DataFrame: Pandas dataframe for the active cross section data with columns: 'X', 'Y',
|
|
293
|
+
'Mannings n', 'Panel', 'RPL', 'Marker', 'Easting', 'Northing', 'Deactivation',
|
|
294
|
+
'SP. Marker'
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
In this example we read in a river section that has deactivation markers
|
|
298
|
+
|
|
299
|
+
.. ipython:: python
|
|
300
|
+
|
|
301
|
+
from floodmodeller_api.units import RIVER
|
|
302
|
+
river_unit = RIVER(
|
|
303
|
+
[
|
|
304
|
+
"RIVER normal case",
|
|
305
|
+
"SECTION",
|
|
306
|
+
"SomeUnit",
|
|
307
|
+
" 0.000 0.000100 1000.000",
|
|
308
|
+
" 5",
|
|
309
|
+
" 0.000 10 0.030 0.000 0.0 0.0 ",
|
|
310
|
+
" 1.000 9 0.030 0.000 0.0 0.0 LEFT",
|
|
311
|
+
" 2.000 5 0.030 0.000 0.0 0.0 ",
|
|
312
|
+
" 3.000 6 0.030 0.000 0.0 0.0 RIGHT",
|
|
313
|
+
" 4.000 10 0.030 0.000 0.0 0.0 ",
|
|
314
|
+
]
|
|
315
|
+
)
|
|
316
|
+
river_unit.data
|
|
317
|
+
river_unit.active_data
|
|
318
|
+
"""
|
|
319
|
+
if self._active_data is not None:
|
|
320
|
+
return self._active_data
|
|
321
|
+
left_bank_idx, right_bank_idx = self._get_left_right_active_index()
|
|
322
|
+
self._active_data = self._data.iloc[left_bank_idx : right_bank_idx + 1].copy()
|
|
323
|
+
return self._active_data
|
|
324
|
+
|
|
325
|
+
@active_data.setter
|
|
326
|
+
def active_data(self, new_df: pd.DataFrame) -> None:
|
|
327
|
+
if not isinstance(new_df, pd.DataFrame):
|
|
328
|
+
raise ValueError(
|
|
329
|
+
"The updated data table for a cross section must be a pandas DataFrame.",
|
|
330
|
+
)
|
|
331
|
+
if new_df.columns.to_list() != self._required_columns:
|
|
332
|
+
raise ValueError(f"The DataFrame must only contain columns: {self._required_columns}")
|
|
333
|
+
|
|
334
|
+
# Ensure activation markers are present
|
|
335
|
+
new_df = new_df.copy()
|
|
336
|
+
new_df.iloc[0, 8] = "LEFT"
|
|
337
|
+
new_df.iloc[-1, 8] = "RIGHT"
|
|
338
|
+
self._active_data = new_df
|
|
339
|
+
|
|
340
|
+
def _get_left_right_active_index(self) -> tuple[int, int]:
|
|
341
|
+
bank_data = self._data.Deactivation.to_list()
|
|
342
|
+
lb_flag = "LEFT" in bank_data
|
|
343
|
+
rb_flag = "RIGHT" in bank_data
|
|
344
|
+
|
|
345
|
+
left_bank_idx = (len(bank_data) - 1) - bank_data[::-1].index("LEFT") if lb_flag else 0
|
|
346
|
+
right_bank_idx = bank_data.index("RIGHT") if rb_flag else len(bank_data) - 1
|
|
347
|
+
return left_bank_idx, right_bank_idx
|
|
348
|
+
|
|
268
349
|
|
|
269
350
|
class INTERPOLATE(Unit):
|
|
270
351
|
"""Class to hold and process INTERPOLATE unit type
|
floodmodeller_api/util.py
CHANGED
|
@@ -59,6 +59,7 @@ def read_file(filepath: str | Path) -> FMFile:
|
|
|
59
59
|
|
|
60
60
|
"""
|
|
61
61
|
from . import DAT, IED, IEF, INP, LF1, LF2, XML2D, ZZN
|
|
62
|
+
from .hydrology_plus import HydrologyPlusExport
|
|
62
63
|
|
|
63
64
|
suffix_to_class = {
|
|
64
65
|
".ief": IEF,
|
|
@@ -69,6 +70,7 @@ def read_file(filepath: str | Path) -> FMFile:
|
|
|
69
70
|
".inp": INP,
|
|
70
71
|
".lf1": LF1,
|
|
71
72
|
".lf2": LF2,
|
|
73
|
+
".csv": HydrologyPlusExport,
|
|
72
74
|
}
|
|
73
75
|
filepath = Path(filepath)
|
|
74
76
|
api_class = suffix_to_class.get(filepath.suffix.lower())
|
floodmodeller_api/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.5.0.post1"
|