Skip to content

Commit 1afdd5c

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

File tree

2 files changed

+883
-0
lines changed

2 files changed

+883
-0
lines changed

autotest/test_channel_utils.py

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
import math
2+
3+
import numpy as np
4+
import pytest
5+
6+
from flopy.utils.channel_utils import (
7+
get_depth,
8+
get_discharge,
9+
get_discharge_rect,
10+
get_segment_wetted_area,
11+
get_segment_wetted_perimeter,
12+
get_segment_wetted_station,
13+
get_wetted_area,
14+
get_wetted_perimeter,
15+
)
16+
17+
18+
def test_get_segment_wetted_station_all_dry():
19+
depth = 10
20+
p0 = (0, 12)
21+
p1 = (10, 11)
22+
x0, x1 = get_segment_wetted_station(
23+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
24+
)
25+
# zero-length segment at x0
26+
assert x0 == x1 == p0[0]
27+
28+
29+
def test_get_segment_wetted_station_partial():
30+
depth = 10
31+
32+
# left bank (sloping downwards to the right)
33+
p0 = (0, 12)
34+
p1 = (10, 8)
35+
x0, x1 = get_segment_wetted_station(
36+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
37+
)
38+
# left endpoint should be moved to the right
39+
assert x0 != x1
40+
assert x0 != p0[0]
41+
assert x1 == p1[0]
42+
assert x0 == (p1[0] - p0[0]) / 2
43+
44+
# right bank (sloping upwards to the right)
45+
p0 = (0, 8)
46+
p1 = (10, 12)
47+
x0, x1 = get_segment_wetted_station(
48+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
49+
)
50+
# right endpoint should be moved to the left
51+
assert x0 != x1
52+
assert x0 == p0[0]
53+
assert x1 != p1[0]
54+
assert x1 == (p1[0] - p0[0]) / 2
55+
56+
57+
def test_get_segment_wetted_station_submerged():
58+
depth = 13
59+
p0 = (0, 12)
60+
p1 = (10, 11)
61+
x0, x1 = get_segment_wetted_station(
62+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
63+
)
64+
# entire segment should be returned
65+
assert x0 == p0[0]
66+
assert x1 == p1[0]
67+
68+
69+
def test_get_segment_wetted_perimeter_dry():
70+
depth = 10
71+
p0 = (0, 12)
72+
p1 = (10, 11)
73+
perim = get_segment_wetted_perimeter(
74+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
75+
)
76+
assert perim == 0
77+
78+
79+
point_lists = [
80+
# single segments
81+
[(0, -1), (1, -1)],
82+
[(0, 0), (1, 1)],
83+
[(0, 1), (2, -1)],
84+
[(0, -1), (2, 1)],
85+
[(0, 1), (1, 0)],
86+
# channels with multiple segments
87+
[(0, -1), (1, -1), (2, -1)], # flat
88+
[(0, 0), (1, -1), (2, 0)], # triangular
89+
[(0, -1), (1, -2), (2, -2), (3, -1)], # trapezoidal
90+
[(0, -1), (1, -2), (2, -4), (3, -4), (4, -1)], # complex
91+
]
92+
93+
94+
@pytest.mark.parametrize(
95+
"depth, p0, p1",
96+
[
97+
(0, point_lists[0][0], point_lists[0][1]),
98+
(0, point_lists[1][0], point_lists[1][1]),
99+
(0, point_lists[2][0], point_lists[2][1]),
100+
(3, point_lists[3][0], point_lists[3][1]),
101+
],
102+
)
103+
def test_get_segment_wetted_perimeter(depth, p0, p1):
104+
wp = get_segment_wetted_perimeter(
105+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
106+
)
107+
108+
xlen = abs(p1[0] - p0[0])
109+
hlen = abs(p1[1] - p0[1])
110+
hmax = max([p0[1], p1[1]])
111+
hmin = min([p0[1], p1[1]])
112+
seg_len = math.sqrt(hlen**2 + xlen**2)
113+
114+
# if segment is fully wetted, wetted perimeter is just its length
115+
if depth >= hmax:
116+
# expect perimeter 0 if the water surface is level with a flat segment
117+
if depth == hmin == hmax:
118+
assert wp == 0
119+
else:
120+
assert wp == seg_len
121+
122+
# if segment is partially submerged, wetted perimeter should be
123+
# less than the length of the segment but greater than zero
124+
elif depth > hmin:
125+
assert wp > 0
126+
assert wp < seg_len
127+
128+
# if segment is completely dry, wetted perimeter should be zero
129+
else:
130+
assert wp == 0
131+
132+
133+
@pytest.mark.parametrize(
134+
"depth, p0, p1",
135+
[
136+
(0, point_lists[0][0], point_lists[0][1]),
137+
(0, point_lists[1][0], point_lists[1][1]),
138+
(0, point_lists[2][0], point_lists[2][1]),
139+
(3, point_lists[3][0], point_lists[3][1]),
140+
],
141+
)
142+
def test_get_segment_wetted_area(depth, p0, p1):
143+
wa = get_segment_wetted_area(
144+
x0=p0[0], x1=p1[0], h0=p0[1], h1=p1[1], depth=depth
145+
)
146+
147+
xlen = abs(p1[0] - p0[0])
148+
hlen = abs(p1[1] - p0[1])
149+
hmax = max([p0[1], p1[1]])
150+
hmin = min([p0[1], p1[1]])
151+
seg_len = math.sqrt(hlen**2 + xlen**2)
152+
tri_area = 0 if hlen == 0 else (0.5 * hlen * xlen)
153+
154+
# if segment is submerged wetted area is that of quadrilateral
155+
# formed by the segment, the water surface, and vertical sides
156+
if depth > hmax:
157+
rect_area = xlen * (depth - hmax)
158+
expected = rect_area + tri_area * (0.5 if p0[1] != p1[1] else 1)
159+
assert wa == expected
160+
161+
# if segment is partially submerged, wetted area should be
162+
# less than the area of the triangle formed by the segment
163+
# and the water surface, with one vertical side
164+
elif depth > hmin:
165+
assert wa > 0
166+
assert wa < tri_area
167+
168+
# if segment is completely dry, wetted area should be zero
169+
else:
170+
assert wa == 0
171+
172+
173+
def test_get_wetted_perimeter_rectangular():
174+
depth = 0
175+
points = np.array([[0, -0.2], [0, -1.4], [1.5, -1.4], [1.5, -0.2]])
176+
perim = get_wetted_perimeter(points[:, 0], points[:, 1], depth)
177+
assert perim == 1.5
178+
179+
180+
@pytest.mark.parametrize("points", [np.array(pts) for pts in point_lists])
181+
@pytest.mark.parametrize("depth", [0, 1, -1, -2])
182+
def test_get_wetted_perimeter(depth, points):
183+
def total_perim(pts):
184+
return sum(
185+
[
186+
math.sqrt(
187+
(pts[i][0] - pts[i - 1][0]) ** 2
188+
+ (pts[i][1] - pts[i - 1][1]) ** 2
189+
)
190+
for i in range(1, len(pts))
191+
]
192+
)
193+
194+
wp = get_wetted_perimeter(
195+
x=points[:, 0], h=points[:, 1], depth=depth, verbose=True
196+
)
197+
198+
hmax = max(points[:, 1])
199+
hmin = min(points[:, 1])
200+
total_perim = total_perim(points)
201+
202+
# if all segments are submerged, wetted perimeter is total perimeter
203+
if depth >= hmax:
204+
# expect perimeter 0 if the water surface is level with a flat channel
205+
if all(p == depth for p in points[:, 1]):
206+
assert wp == 0
207+
else:
208+
assert wp == total_perim
209+
210+
# if at least some segments are at least partially submerged...
211+
elif depth > hmin:
212+
assert wp > 0
213+
assert wp < total_perim
214+
215+
def patch(x0, x1, h0, h1, depth):
216+
# TODO: refactor get_segment_wetted_perimeter() to handle partial wetting
217+
# internally so separate get_segment_wetted_station() is no longer needed?
218+
219+
x0, x1 = get_segment_wetted_station(
220+
x0=x0, x1=x1, h0=h0, h1=h1, depth=depth
221+
)
222+
return get_segment_wetted_perimeter(
223+
x0=x0, x1=x1, h0=h0, h1=h1, depth=depth
224+
)
225+
226+
assert np.isclose(
227+
wp,
228+
sum(
229+
[
230+
patch(
231+
x0=points[i][0],
232+
x1=points[i + 1][0],
233+
h0=points[i][1],
234+
h1=points[i + 1][1],
235+
depth=depth,
236+
)
237+
for i in range(0, len(points) - 1)
238+
]
239+
),
240+
)
241+
242+
# if all segments are dry, wetted perimeter is zero
243+
else:
244+
assert wp == 0
245+
246+
247+
def test_get_wetted_area_rectangular():
248+
depth = 0
249+
points = np.array([[0, -0.2], [0, -1.4], [1.5, -1.4], [1.5, -0.2]])
250+
expected = 1.5 * 1.4
251+
area = get_wetted_area(points[:, 0], points[:, 1], depth)
252+
assert area == expected
253+
254+
255+
def test_get_discharge_rect():
256+
# adapted from https://www.youtube.com/watch?v=R-Vhs_AH8mA
257+
258+
width = 0.5
259+
depth = 0.25
260+
n = 0.022
261+
s = 0.005
262+
k = 1.0
263+
expected = 0.1
264+
q = get_discharge_rect(width, depth, n, s, k)
265+
assert np.isclose(expected, q, rtol=1e-2)
266+
267+
268+
def test_get_discharge_rectangular():
269+
# adapted from https://www.youtube.com/watch?v=R-Vhs_AH8mA
270+
271+
# x and h as ints (width and height)
272+
width = 0.5
273+
depth = 0.25
274+
n = 0.022
275+
s = 0.005
276+
k = 1.0
277+
expected = 0.1
278+
q = get_discharge(width, depth, depth, n, s, k)
279+
assert np.isclose(expected, q, rtol=1e-2)
280+
281+
# x and h as arrays
282+
x = np.array([0, 0, width, width])
283+
h = np.array([depth, 0, 0, depth])
284+
q = get_discharge(x, h, depth, n, s, k)
285+
assert np.isclose(expected, q, rtol=1e-2)
286+
287+
288+
def test_get_discharge_trapezoidal():
289+
# adapted from https://www.youtube.com/watch?v=ucLa9_DDWPA
290+
291+
x = np.array([0, 2, 4, 8, 10, 12])
292+
h = np.array([2, 1, 0, 0, 1, 2])
293+
n = 0.015
294+
s = 0.002
295+
k = 1.0
296+
depth = 1.3
297+
expected = 23.4
298+
q = get_discharge(x, h, depth, n, s, k)
299+
assert np.isclose(expected, q, rtol=0.1)
300+
301+
302+
@pytest.mark.xfail(reason="debug complex array input case")
303+
def test_get_discharge_compound_rectangular():
304+
# adapted from https://www.youtube.com/watch?v=BJZ73WWEc3M
305+
306+
# manually sum over discharge for each rectangle
307+
n = 0.016
308+
s = 0.001
309+
k = 1.0
310+
expected = 3.3
311+
q0 = get_discharge(3, 0.2, roughness=n, slope=s, conv=k)
312+
q1 = get_discharge(1.5, 1.4, roughness=n, slope=s, conv=k)
313+
q2 = get_discharge(3, 0.2, roughness=n, slope=s, conv=k)
314+
sm = q0 + q1 + q2
315+
assert np.isclose(expected, sm, rtol=1e-2)
316+
317+
# check single rectangular segment with array input for x and h
318+
x = np.array([0, 0, 3, 3])
319+
h = np.array([0, -0.2, -0.2, 0])
320+
q = get_discharge(x, h, 0, n, s, k)
321+
assert np.isclose(q, q0, rtol=1e-3)
322+
323+
# check multiple rectangles with array input
324+
x = np.array([0, 0, 3, 3, 4.5, 4.5, 7.5, 7.5])
325+
h = np.array([0, -0.2, -0.2, -1.4, -1.4, -0.2, -0.2, 0])
326+
q = get_discharge(x, h, 0, n, s, k)
327+
assert np.isclose(expected, q, rtol=1e-2)
328+
329+
330+
def test_get_depth():
331+
# adapted from https://www.youtube.com/watch?v=t9ywTXEcScE
332+
333+
width = 5
334+
x = np.array([0, 0, width, width])
335+
h = np.array([2, 0, 0, 2])
336+
q = 6.5
337+
n = 0.02
338+
s = 0.0005
339+
k = 1.0
340+
expected = 1.3
341+
depth = get_depth(x, h, q, n, s, k)
342+
assert np.isclose(expected, depth, rtol=1e-1)
343+
344+
345+
@pytest.mark.skip(reason="todo")
346+
def test_get_depths():
347+
pass

0 commit comments

Comments
 (0)