Skip to content

Commit 6d1f8d6

Browse files
committed
feat(channel_utils): move open channel flow utils from modflow6
1 parent 1114c93 commit 6d1f8d6

File tree

2 files changed

+771
-0
lines changed

2 files changed

+771
-0
lines changed

autotest/test_channel_utils.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import math
2+
3+
import numpy as np
4+
import pytest
5+
6+
from flopy.utils.channel_utils import (
7+
get_discharge,
8+
get_discharge_rect,
9+
get_segment_wetted_area,
10+
get_segment_wetted_perimeter,
11+
get_segment_wetted_station,
12+
get_wetted_area,
13+
get_wetted_perimeter,
14+
)
15+
16+
17+
def test_get_segment_wetted_station_all_dry():
18+
depth = 10
19+
p0 = (0, 12)
20+
p1 = (10, 11)
21+
x0, x1 = get_segment_wetted_station(
22+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
23+
)
24+
# zero-length segment at x0
25+
assert x0 == x1 == p0[0]
26+
27+
28+
def test_get_segment_wetted_station_partial():
29+
depth = 10
30+
31+
# left bank (sloping downwards to the right)
32+
p0 = (0, 12)
33+
p1 = (10, 8)
34+
x0, x1 = get_segment_wetted_station(
35+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
36+
)
37+
# left endpoint should be moved to the right
38+
assert x0 != x1
39+
assert x0 != p0[0]
40+
assert x1 == p1[0]
41+
assert x0 == (p1[0] - p0[0]) / 2
42+
43+
# right bank (sloping upwards to the right)
44+
p0 = (0, 8)
45+
p1 = (10, 12)
46+
x0, x1 = get_segment_wetted_station(
47+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
48+
)
49+
# right endpoint should be moved to the left
50+
assert x0 != x1
51+
assert x0 == p0[0]
52+
assert x1 != p1[0]
53+
assert x1 == (p1[0] - p0[0]) / 2
54+
55+
56+
def test_get_segment_wetted_station_submerged():
57+
depth = 13
58+
p0 = (0, 12)
59+
p1 = (10, 11)
60+
x0, x1 = get_segment_wetted_station(
61+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
62+
)
63+
# entire segment should be returned
64+
assert x0 == p0[0]
65+
assert x1 == p1[0]
66+
67+
68+
def test_get_segment_wetted_perimeter_dry():
69+
depth = 10
70+
p0 = (0, 12)
71+
p1 = (10, 11)
72+
perim = get_segment_wetted_perimeter(
73+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
74+
)
75+
assert perim == 0
76+
77+
78+
point_lists = [
79+
# single segments
80+
[(0, -1), (1, -1)],
81+
[(0, 0), (1, 1)],
82+
[(0, 1), (2, -1)],
83+
[(0, -1), (2, 1)],
84+
[(0, 1), (1, 0)],
85+
# channels with multiple segments
86+
[(0, -1), (1, -1), (2, -1)], # flat
87+
[(0, 0), (1, -1), (2, 0)], # triangular
88+
[(0, -1), (1, -2), (2, -2), (3, -1)], # trapezoidal
89+
[(0, -1), (1, -2), (2, -4), (3, -4), (4, -1)], # complex
90+
]
91+
92+
93+
@pytest.mark.parametrize(
94+
"depth, p0, p1",
95+
[
96+
(0, point_lists[0][0], point_lists[0][1]),
97+
(0, point_lists[1][0], point_lists[1][1]),
98+
(0, point_lists[2][0], point_lists[2][1]),
99+
(3, point_lists[3][0], point_lists[3][1]),
100+
],
101+
)
102+
def test_get_segment_wetted_perimeter(depth, p0, p1):
103+
wp = get_segment_wetted_perimeter(
104+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
105+
)
106+
107+
xlen = abs(p1[0] - p0[0])
108+
hlen = abs(p1[1] - p0[1])
109+
hmax = max([p0[1], p1[1]])
110+
hmin = min([p0[1], p1[1]])
111+
seg_len = math.sqrt(hlen**2 + xlen**2)
112+
113+
# if segment is fully wetted, wetted perimeter is just its length
114+
if depth >= hmax:
115+
# expect perimeter 0 if the water surface is level with a flat segment
116+
if depth == hmin == hmax:
117+
assert wp == 0
118+
else:
119+
assert wp == seg_len
120+
121+
# if segment is partially submerged, wetted perimeter should be
122+
# less than the length of the segment but greater than zero
123+
elif depth > hmin:
124+
assert wp > 0
125+
assert wp < seg_len
126+
127+
# if segment is completely dry, wetted perimeter should be zero
128+
else:
129+
assert wp == 0
130+
131+
132+
@pytest.mark.parametrize(
133+
"depth, p0, p1",
134+
[
135+
(0, point_lists[0][0], point_lists[0][1]),
136+
(0, point_lists[1][0], point_lists[1][1]),
137+
(0, point_lists[2][0], point_lists[2][1]),
138+
(3, point_lists[3][0], point_lists[3][1]),
139+
],
140+
)
141+
def test_get_segment_wetted_area(depth, p0, p1):
142+
wa = get_segment_wetted_area(
143+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
144+
)
145+
146+
xlen = abs(p1[0] - p0[0])
147+
hlen = abs(p1[1] - p0[1])
148+
hmax = max([p0[1], p1[1]])
149+
hmin = min([p0[1], p1[1]])
150+
seg_len = math.sqrt(hlen**2 + xlen**2)
151+
tri_area = 0 if hlen == 0 else (0.5 * hlen * xlen)
152+
153+
# if segment is submerged wetted area is that of quadrilateral
154+
# formed by the segment, the water surface, and vertical sides
155+
if depth > hmax:
156+
rect_area = xlen * (depth - hmax)
157+
assert wa == (rect_area + tri_area)
158+
159+
# if segment is partially submerged, wetted area should be
160+
# less than the area of the triangle formed by the segment
161+
# and the water surface, with one vertical side
162+
elif depth > hmin:
163+
assert wa > 0
164+
assert wa < tri_area
165+
166+
# if segment is completely dry, wetted area should be zero
167+
else:
168+
assert wa == 0
169+
170+
171+
@pytest.mark.parametrize("points", [np.array(pts) for pts in point_lists])
172+
@pytest.mark.parametrize("depth", [0, 1, -1, -2])
173+
def test_get_wetted_perimeter(depth, points):
174+
def total_perim(pts):
175+
return sum(
176+
[
177+
math.sqrt(
178+
(pts[i][0] - pts[i - 1][0]) ** 2
179+
+ (pts[i][1] - pts[i - 1][1]) ** 2
180+
)
181+
for i in range(1, len(pts))
182+
]
183+
)
184+
185+
wp = get_wetted_perimeter(
186+
x=points[:, 0], h=points[:, 1], depth=depth, verbose=True
187+
)
188+
189+
hmax = max(points[:, 1])
190+
hmin = min(points[:, 1])
191+
total_perim = total_perim(points)
192+
193+
# if all segments are submerged, wetted perimeter is total perimeter
194+
if depth >= hmax:
195+
# expect perimeter 0 if the water surface is level with a flat channel
196+
if all(p == depth for p in points[:, 1]):
197+
assert wp == 0
198+
else:
199+
assert wp == total_perim
200+
201+
# if at least some segments are at least partially submerged...
202+
elif depth > hmin:
203+
assert wp > 0
204+
assert wp < total_perim
205+
206+
def patch(x0, x1, h0, h1, depth):
207+
# TODO: refactor get_segment_wetted_perimeter() to handle partial wetting
208+
# internally so separate get_segment_wetted_station() is no longer needed?
209+
210+
x0, x1 = get_segment_wetted_station(
211+
x0=x0, x1=x1, h0=h0, h1=h1, depth=depth
212+
)
213+
return get_segment_wetted_perimeter(
214+
x0=x0, x1=x1, h0=h0, h1=h1, depth=depth
215+
)
216+
217+
assert np.isclose(
218+
wp,
219+
sum(
220+
[
221+
patch(
222+
x0=points[i][0],
223+
x1=points[i + 1][0],
224+
h0=points[i][1],
225+
h1=points[i + 1][1],
226+
depth=depth,
227+
)
228+
for i in range(0, len(points) - 1)
229+
]
230+
),
231+
)
232+
233+
# if all segments are dry, wetted perimeter is zero
234+
else:
235+
assert wp == 0
236+
237+
238+
@pytest.mark.skip(reason="todo")
239+
def test_get_wetted_area():
240+
pass
241+
242+
243+
def test_get_discharge_rect():
244+
width = 0.5
245+
depth = 0.25
246+
roughness = 0.022
247+
slope = 0.005
248+
conv = 1.0
249+
expected = 0.1
250+
actual = get_discharge_rect(width, depth, roughness, slope, conv)
251+
assert np.isclose(expected, actual, rtol=1e-2)
252+
253+
254+
@pytest.mark.xfail(reason="debug")
255+
def test_get_discharge():
256+
width = 0.5
257+
depth = 0.25
258+
roughness = 0.022
259+
slope = 0.005
260+
conv = 1.0
261+
expected = 0.1
262+
x = np.array([width])
263+
h = np.array([depth])
264+
actual = get_discharge(x, h, depth, roughness, slope, conv)
265+
assert np.isclose(expected, actual, rtol=1e-2)
266+
267+
268+
@pytest.mark.skip(reason="todo")
269+
def test_get_depth():
270+
pass
271+
272+
273+
@pytest.mark.skip(reason="todo")
274+
def test_get_depths():
275+
pass

0 commit comments

Comments
 (0)