cht_utils 2.0.0__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.
- cht_utils/__init__.py +28 -0
- cht_utils/cog/__init__.py +6 -0
- cht_utils/cog/geotiff_to_cog.py +79 -0
- cht_utils/cog/netcdf_to_cog.py +85 -0
- cht_utils/cog/xyz_to_cog.py +86 -0
- cht_utils/colors/__init__.py +6 -0
- cht_utils/colors/colors.py +117 -0
- cht_utils/fileio/__init__.py +21 -0
- cht_utils/fileio/deltares_ini.py +326 -0
- cht_utils/fileio/json_js.py +72 -0
- cht_utils/fileio/pli_file.py +233 -0
- cht_utils/fileio/tekal.py +234 -0
- cht_utils/fileio/xml.py +184 -0
- cht_utils/fileio/yaml.py +39 -0
- cht_utils/fileops/__init__.py +25 -0
- cht_utils/fileops/fileops.py +344 -0
- cht_utils/interpolation/__init__.py +5 -0
- cht_utils/interpolation/interpolation.py +152 -0
- cht_utils/maps/__init__.py +2 -0
- cht_utils/maps/fileops.py +191 -0
- cht_utils/maps/flood_map.py +1231 -0
- cht_utils/maps/topobathy_map.py +463 -0
- cht_utils/maps/utils.py +700 -0
- cht_utils/physics/__init__.py +8 -0
- cht_utils/physics/deshoal.py +63 -0
- cht_utils/physics/disper.py +91 -0
- cht_utils/physics/runup_vo21.py +229 -0
- cht_utils/physics/waves.py +59 -0
- cht_utils/probabilistic/__init__.py +5 -0
- cht_utils/probabilistic/prob_maps.py +263 -0
- cht_utils/remote/__init__.py +4 -0
- cht_utils/remote/s3.py +380 -0
- cht_utils/remote/sftp.py +192 -0
- cht_utils-2.0.0.dist-info/METADATA +30 -0
- cht_utils-2.0.0.dist-info/RECORD +39 -0
- cht_utils-2.0.0.dist-info/WHEEL +5 -0
- cht_utils-2.0.0.dist-info/licenses/LICENSE +21 -0
- cht_utils-2.0.0.dist-info/top_level.txt +1 -0
- cht_utils-2.0.0.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Wave shoaling adjustment for significant wave height."""
|
|
2
|
+
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from cht_utils.physics.disper import disper
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def deshoal(
|
|
11
|
+
hm0: Union[float, np.ndarray],
|
|
12
|
+
Tp: float,
|
|
13
|
+
d_profile: Union[float, np.ndarray],
|
|
14
|
+
d_BC: float,
|
|
15
|
+
) -> Union[float, np.ndarray]:
|
|
16
|
+
"""Adjust significant wave height for shoaling.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
hm0 : float or np.ndarray
|
|
21
|
+
Significant wave height at the boundary.
|
|
22
|
+
Tp : float
|
|
23
|
+
Peak wave period.
|
|
24
|
+
d_profile : float or np.ndarray
|
|
25
|
+
Water depth(s) along the profile.
|
|
26
|
+
d_BC : float
|
|
27
|
+
Water depth at the boundary condition location.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
float or np.ndarray
|
|
32
|
+
De-shoaled significant wave height.
|
|
33
|
+
"""
|
|
34
|
+
cg_profile = wavecelerity(Tp, d_profile)
|
|
35
|
+
cg_BC = wavecelerity(Tp, d_BC)
|
|
36
|
+
return hm0 * np.sqrt(cg_profile / cg_BC)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def wavecelerity(
|
|
40
|
+
Tp: float,
|
|
41
|
+
d: Union[float, np.ndarray],
|
|
42
|
+
g: float = 9.81,
|
|
43
|
+
) -> Union[float, np.ndarray]:
|
|
44
|
+
"""Compute wave group velocity.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
Tp : float
|
|
49
|
+
Peak wave period.
|
|
50
|
+
d : float or np.ndarray
|
|
51
|
+
Water depth.
|
|
52
|
+
g : float
|
|
53
|
+
Gravitational acceleration.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
float or np.ndarray
|
|
58
|
+
Group velocity.
|
|
59
|
+
"""
|
|
60
|
+
k = disper(2 * np.pi / Tp, d, g)
|
|
61
|
+
n = 0.5 * (1 + 2 * k * d / np.sinh(2 * k * d))
|
|
62
|
+
c = g * Tp / (2 * np.pi) * np.tanh(k * d)
|
|
63
|
+
return n * c
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Linear wave dispersion relation solver.
|
|
2
|
+
|
|
3
|
+
Absolute error in k*h < 5.0e-16 for all k*h.
|
|
4
|
+
|
|
5
|
+
Example::
|
|
6
|
+
|
|
7
|
+
k = disper(2 * np.pi / 5, 5, 9.81)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import List, Union
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def disper(
|
|
16
|
+
w: Union[float, List[float]],
|
|
17
|
+
h: Union[float, List[float]],
|
|
18
|
+
g: float = 9.81,
|
|
19
|
+
) -> np.ndarray:
|
|
20
|
+
"""Solve the linear dispersion relation for wave number.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
w : float or list of float
|
|
25
|
+
Angular frequency (2*pi/T).
|
|
26
|
+
h : float or list of float
|
|
27
|
+
Water depth.
|
|
28
|
+
g : float
|
|
29
|
+
Gravitational acceleration.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
np.ndarray
|
|
34
|
+
Wave number(s).
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(w, list):
|
|
37
|
+
w = [w]
|
|
38
|
+
if not isinstance(h, list):
|
|
39
|
+
h = [h]
|
|
40
|
+
|
|
41
|
+
w2 = [iw**2 * ih / g for iw, ih in zip(w, h)]
|
|
42
|
+
q = [iw2 / (1 - np.exp(-(iw2 ** (5 / 4)))) ** (2 / 5) for iw2 in w2]
|
|
43
|
+
|
|
44
|
+
the = np.tanh(q)
|
|
45
|
+
the2 = 1 - the**2
|
|
46
|
+
|
|
47
|
+
a = (1 - q * the) * the2
|
|
48
|
+
b = the + q * the2
|
|
49
|
+
c = q * the - w2
|
|
50
|
+
|
|
51
|
+
D = b**2 - 4 * a * c
|
|
52
|
+
arg = (-b + np.sqrt(D)) / (2 * a)
|
|
53
|
+
iq = np.where(D < 0)[0]
|
|
54
|
+
if len(iq) > 0:
|
|
55
|
+
arg[iq] = -c[iq] / b[iq]
|
|
56
|
+
q = q + arg
|
|
57
|
+
|
|
58
|
+
k = np.sign(w) * q / h
|
|
59
|
+
if np.isnan(k).any():
|
|
60
|
+
k = np.array(k)
|
|
61
|
+
k[np.isnan(k)] = 0
|
|
62
|
+
|
|
63
|
+
return k
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def disper_fentonmckee(
|
|
67
|
+
sigma: Union[float, np.ndarray],
|
|
68
|
+
d: Union[float, np.ndarray],
|
|
69
|
+
g: float = 9.81,
|
|
70
|
+
) -> Union[float, np.ndarray]:
|
|
71
|
+
"""Fenton-McKee approximation of the linear dispersion relation.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
sigma : float or np.ndarray
|
|
76
|
+
Angular frequency.
|
|
77
|
+
d : float or np.ndarray
|
|
78
|
+
Water depth.
|
|
79
|
+
g : float
|
|
80
|
+
Gravitational acceleration.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
float or np.ndarray
|
|
85
|
+
Approximate wave number(s).
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def coth(x):
|
|
89
|
+
return 1.0 / np.tanh(x)
|
|
90
|
+
|
|
91
|
+
return (sigma**2 / g) * (coth(sigma * np.sqrt(d / g)) ** (3 / 2)) ** (2 / 3)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Wave runup prediction using Van Ormondt (2021) empirical formulation.
|
|
2
|
+
|
|
3
|
+
Computes the 2% exceedance runup level from offshore wave conditions
|
|
4
|
+
and nearshore profile information, decomposed into setup, low-frequency,
|
|
5
|
+
and high-frequency components with directional spreading corrections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy import interpolate as intp
|
|
12
|
+
|
|
13
|
+
from cht_utils.physics.disper import disper
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class runup_vo21:
|
|
17
|
+
"""Wave runup calculator following Van Ormondt (2021).
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
hm0 : np.ndarray
|
|
22
|
+
Significant wave height.
|
|
23
|
+
tp : float
|
|
24
|
+
Peak wave period.
|
|
25
|
+
ztide : float or np.ndarray
|
|
26
|
+
Tidal water level.
|
|
27
|
+
sl1 : float, np.ndarray, or dict
|
|
28
|
+
Surf slope parameter. Interpretation depends on *sl1opt*.
|
|
29
|
+
sl2 : float or np.ndarray
|
|
30
|
+
Beach slope (foreshore).
|
|
31
|
+
sl1opt : str
|
|
32
|
+
Slope option: ``"ad"`` (adaptive), ``"slope"`` (direct), or
|
|
33
|
+
``"xz"`` (cross-shore profile dict with keys ``"x"`` and ``"z"``).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
hm0: np.ndarray,
|
|
39
|
+
tp: float,
|
|
40
|
+
ztide: Union[float, np.ndarray],
|
|
41
|
+
sl1: Any,
|
|
42
|
+
sl2: Union[float, np.ndarray],
|
|
43
|
+
sl1opt: str,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.hm0 = hm0
|
|
46
|
+
self.tp = tp
|
|
47
|
+
self.h = self.tp * 10
|
|
48
|
+
|
|
49
|
+
k = disper(2 * np.pi / self.tp, self.h, 9.81)
|
|
50
|
+
self.l1 = 2 * np.pi / k
|
|
51
|
+
self.steepness = self.hm0 / self.l1
|
|
52
|
+
|
|
53
|
+
if np.size(ztide) == 1:
|
|
54
|
+
ztide = np.zeros(np.size(self.hm0)) + ztide
|
|
55
|
+
if np.size(sl2) == 1:
|
|
56
|
+
sl2 = np.zeros(np.size(self.hm0)) + sl2
|
|
57
|
+
|
|
58
|
+
if sl1opt == "ad":
|
|
59
|
+
gambr = 1
|
|
60
|
+
fh = 1 / gambr
|
|
61
|
+
surfslope2 = np.zeros(np.size(ztide))
|
|
62
|
+
surfslope2[0] = (fh * self.hm0) / (fh * self.hm0 / self.surfslope) ** 1.5
|
|
63
|
+
for itide in range(np.size(ztide)):
|
|
64
|
+
if ztide[itide] == 0:
|
|
65
|
+
pass
|
|
66
|
+
else:
|
|
67
|
+
xxx = np.arange(-1000, 5005, 5)
|
|
68
|
+
yyy0 = -sl1[itide] * xxx ** (2 / 3)
|
|
69
|
+
yyy0[np.argwhere(xxx < 0)] = -xxx[np.argwhere(xxx < 0)] * sl2[itide]
|
|
70
|
+
yyy = yyy0 - ztide[itide]
|
|
71
|
+
ii1 = np.argwhere(yyy < 0)[0]
|
|
72
|
+
ii2 = np.argwhere(yyy < -self.hm0[itide] / gambr)[0]
|
|
73
|
+
surfslope2[itide] = (yyy[ii1] - yyy[ii2]) / (xxx[ii2] - xxx[ii1])
|
|
74
|
+
elif sl1opt == "slope":
|
|
75
|
+
surfslope2 = sl1
|
|
76
|
+
elif sl1opt == "xz":
|
|
77
|
+
gambr = 1
|
|
78
|
+
xxxx = np.arange(sl1["x"][0], sl1["x"][-1], 1)
|
|
79
|
+
zzzz = intp.interp1d(sl1["x"], sl1["z"])(xxxx)
|
|
80
|
+
sl1["x"] = xxxx
|
|
81
|
+
sl1["z"] = zzzz
|
|
82
|
+
surfslope2 = np.zeros(np.size(ztide))
|
|
83
|
+
xxx = sl1["x"]
|
|
84
|
+
yyy0 = sl1["z"]
|
|
85
|
+
for itide in range(np.size(ztide)):
|
|
86
|
+
yyy = yyy0 - ztide[itide]
|
|
87
|
+
ii1 = np.argwhere(yyy < 0)[0]
|
|
88
|
+
ii2 = np.argwhere(yyy < -self.hm0[itide] / gambr)[0]
|
|
89
|
+
surfslope2[itide] = (yyy[ii1] - yyy[ii2]) / (xxx[ii2] - xxx[ii1])
|
|
90
|
+
|
|
91
|
+
self.sl2 = sl2
|
|
92
|
+
self.ztide = ztide
|
|
93
|
+
self.surfslope = surfslope2
|
|
94
|
+
self.ksis = self.surfslope / np.sqrt(self.hm0 / self.l1)
|
|
95
|
+
|
|
96
|
+
def compute_r2p(self, drspr: np.ndarray, phi: np.ndarray) -> None:
|
|
97
|
+
"""Compute the 2% exceedance runup level.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
drspr : np.ndarray
|
|
102
|
+
Directional spreading (degrees).
|
|
103
|
+
phi : np.ndarray
|
|
104
|
+
Wave direction relative to shore normal (degrees).
|
|
105
|
+
"""
|
|
106
|
+
ksi1 = self.surfslope / np.sqrt(self.hm0 / self.l1)
|
|
107
|
+
ksi2 = self.sl2 / np.sqrt(self.hm0 / self.l1)
|
|
108
|
+
|
|
109
|
+
self.compute_setup(drspr, phi)
|
|
110
|
+
self.compute_hm0_lf(drspr, phi)
|
|
111
|
+
self.compute_hm0_hf(drspr, phi)
|
|
112
|
+
|
|
113
|
+
self.r2p = self.setup + 0.82396 * np.sqrt(
|
|
114
|
+
(0.82694 * self.hm0_lf) ** 2 + (0.73965 * self.hm0_hf) ** 2
|
|
115
|
+
) * ksi2**0.15201 * ksi1 ** (-0.086635)
|
|
116
|
+
|
|
117
|
+
def compute_setup(self, drspr: np.ndarray, phi: np.ndarray) -> None:
|
|
118
|
+
"""Compute wave setup component."""
|
|
119
|
+
b = [
|
|
120
|
+
4.0455506e00,
|
|
121
|
+
2.3740615e-02,
|
|
122
|
+
2.0340287e00,
|
|
123
|
+
4.6497588e-01,
|
|
124
|
+
7.0244541e-01,
|
|
125
|
+
5.0000000e-01,
|
|
126
|
+
-3.1727583e-01,
|
|
127
|
+
]
|
|
128
|
+
ksib = self.sl2 / np.sqrt(self.hm0 / self.l1)
|
|
129
|
+
v = (
|
|
130
|
+
b[0]
|
|
131
|
+
* self.hm0
|
|
132
|
+
* (b[1] + np.exp(-b[2] * self.ksis ** b[6] * ksib ** b[3]) * ksib ** b[4])
|
|
133
|
+
)
|
|
134
|
+
fac1 = self._dirspreadfac_setup(drspr)
|
|
135
|
+
fac2 = self._directionfac_setup(phi)
|
|
136
|
+
self.setup = v * fac1 * fac2
|
|
137
|
+
|
|
138
|
+
def compute_hm0_lf(self, drspr: np.ndarray, phi: np.ndarray) -> None:
|
|
139
|
+
"""Compute low-frequency wave height component."""
|
|
140
|
+
b = [
|
|
141
|
+
3.4547125e00,
|
|
142
|
+
5.8790748e-01,
|
|
143
|
+
3.6906975e00,
|
|
144
|
+
2.3378556e-01,
|
|
145
|
+
2.3038164e00,
|
|
146
|
+
0,
|
|
147
|
+
5.0000000e-01,
|
|
148
|
+
0,
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
self.tm0_ig = self._compute_tm01_ig()
|
|
152
|
+
self.IG = self._compute_ig_in()
|
|
153
|
+
|
|
154
|
+
l0 = np.squeeze(np.sqrt(9.81 * 0.33333 * self.hm0) * self.tm0_ig)
|
|
155
|
+
ksib = np.squeeze(self.sl2 / np.sqrt(self.IG / l0))
|
|
156
|
+
ksib = np.maximum(ksib - b[5], 0)
|
|
157
|
+
ksibm = b[4] * self.surfslope ** b[3]
|
|
158
|
+
psibd = b[0] * ksib ** b[1]
|
|
159
|
+
psibr = (b[0] * ksibm ** b[1] - 2.0) * np.exp(-(ksib - ksibm) / b[2]) + 2
|
|
160
|
+
psib = psibd.copy()
|
|
161
|
+
ind = np.argwhere(ksib > ksibm)
|
|
162
|
+
psib[ind] = psibr[ind]
|
|
163
|
+
|
|
164
|
+
v = self.IG * psib
|
|
165
|
+
fac1 = self._dirspreadfac_lf(drspr)
|
|
166
|
+
fac2 = self._directionfac_lf(phi)
|
|
167
|
+
self.hm0_lf = v * fac1 * fac2
|
|
168
|
+
|
|
169
|
+
def compute_hm0_hf(self, drspr: np.ndarray, phi: np.ndarray) -> None:
|
|
170
|
+
"""Compute high-frequency wave height component."""
|
|
171
|
+
b = [
|
|
172
|
+
9.5635099e-01,
|
|
173
|
+
2.0143005e00,
|
|
174
|
+
5.3602429e-01,
|
|
175
|
+
2.0000000e00,
|
|
176
|
+
0.0000000e00,
|
|
177
|
+
6.1856544e-01,
|
|
178
|
+
1.0000000e00,
|
|
179
|
+
0.0000000e00,
|
|
180
|
+
]
|
|
181
|
+
ksib = self.sl2 / np.sqrt(self.hm0 / self.l1)
|
|
182
|
+
self.hm0_hf = (
|
|
183
|
+
b[0]
|
|
184
|
+
* self.hm0
|
|
185
|
+
* ksib ** b[1]
|
|
186
|
+
* np.tanh((self.ksis + b[4]) ** b[5] / (b[2] * ksib ** b[3]))
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _compute_tm01_ig(self) -> np.ndarray:
|
|
190
|
+
"""Compute infragravity wave period."""
|
|
191
|
+
b = [4.4021341e-07, 1.8635421e00, -4.2705433e-01, 7.2541023e-02, 2.0058478e01]
|
|
192
|
+
tm0_ig = (
|
|
193
|
+
b[0]
|
|
194
|
+
+ b[1] * self.surfslope ** b[2] * self.steepness ** b[3]
|
|
195
|
+
+ b[4] * self.surfslope
|
|
196
|
+
)
|
|
197
|
+
return tm0_ig * self.tp
|
|
198
|
+
|
|
199
|
+
def _compute_ig_in(self) -> np.ndarray:
|
|
200
|
+
"""Compute incoming infragravity wave amplitude."""
|
|
201
|
+
b = [2.2740842e00, 1.0, 0.5, 2.7211454e03, 2.0, 1.7794945e01, 1.8728433e-01]
|
|
202
|
+
return self.hm0 * (
|
|
203
|
+
b[0] * self.surfslope ** b[2] * np.exp(-b[5] * self.surfslope ** b[1])
|
|
204
|
+
+ b[6] * np.exp(-b[3] * self.steepness ** b[4])
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _directionfac_setup(self, phi: np.ndarray) -> np.ndarray:
|
|
208
|
+
b = [1.4291, 0.0035124, 0.31891]
|
|
209
|
+
return 1 - b[1] * self.steepness ** b[2] * np.absolute(phi) ** b[0]
|
|
210
|
+
|
|
211
|
+
def _dirspreadfac_setup(self, drspr: np.ndarray) -> np.ndarray:
|
|
212
|
+
b = [0.031448, 0.69432, 0.66677]
|
|
213
|
+
return np.exp(-b[0] * drspr ** b[1] * (1.0 - np.tanh(b[2] * self.ksis)))
|
|
214
|
+
|
|
215
|
+
def _dirspreadfac_lf(self, drspr: np.ndarray) -> np.ndarray:
|
|
216
|
+
b = [0.047593, 0.67228, 0.50777]
|
|
217
|
+
return np.exp(-b[0] * drspr ** b[1] * (1.0 - np.tanh(b[2] * self.ksis)))
|
|
218
|
+
|
|
219
|
+
def _directionfac_lf(self, phi: np.ndarray) -> np.ndarray:
|
|
220
|
+
b = [0.40488, 2.7073]
|
|
221
|
+
return np.cos(phi * np.pi / 180) ** (b[0] + b[1] * self.surfslope)
|
|
222
|
+
|
|
223
|
+
def _directionfac_hf(self, phi: np.ndarray) -> np.ndarray:
|
|
224
|
+
b = [4.1355e-10]
|
|
225
|
+
return np.cos(phi * np.pi / 180) ** b[0]
|
|
226
|
+
|
|
227
|
+
def _dirspreadfac_hf(self, drspr: np.ndarray) -> np.ndarray:
|
|
228
|
+
b = [0.044544, 1, 21.1281]
|
|
229
|
+
return np.exp(-b[0] * drspr ** b[1] * (1.0 - np.tanh(b[2] * self.ksis)))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Wave decomposition utilities."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from scipy.signal import detrend
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def split_waves_guza(df: pd.DataFrame, zb: float) -> pd.DataFrame:
|
|
9
|
+
"""Decompose water surface into incident and reflected waves (Guza method).
|
|
10
|
+
|
|
11
|
+
Performs a time-domain decomposition of the water surface elevation and
|
|
12
|
+
velocity into incoming and outgoing components using shallow-water theory.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
df : pd.DataFrame
|
|
17
|
+
DataFrame with columns ``"z"`` (water level) and ``"u"`` (velocity).
|
|
18
|
+
zb : float
|
|
19
|
+
Bed level elevation.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
pd.DataFrame
|
|
24
|
+
Input DataFrame with added columns ``"zin"``, ``"zout"``,
|
|
25
|
+
``"uin"``, ``"uout"``.
|
|
26
|
+
"""
|
|
27
|
+
g = 9.81
|
|
28
|
+
|
|
29
|
+
zsi = df["z"].values
|
|
30
|
+
umi = df["u"].values
|
|
31
|
+
|
|
32
|
+
zsm = np.mean(zsi)
|
|
33
|
+
umm = np.mean(umi)
|
|
34
|
+
h = zsm - zb
|
|
35
|
+
|
|
36
|
+
zs = zsi - zsm
|
|
37
|
+
um = umi - umm
|
|
38
|
+
|
|
39
|
+
zsd = detrend(zs, type="linear")
|
|
40
|
+
umd = detrend(um, type="linear")
|
|
41
|
+
|
|
42
|
+
zsm = zsm + (zs - zsd)
|
|
43
|
+
zs = zsd
|
|
44
|
+
umm = umm + (um - umd)
|
|
45
|
+
um = umd
|
|
46
|
+
|
|
47
|
+
hh = zs + h
|
|
48
|
+
c = np.sqrt(g * hh)
|
|
49
|
+
q = umd * hh
|
|
50
|
+
|
|
51
|
+
ein = (zs * c + q) / (2 * c)
|
|
52
|
+
eout = (zs * c - q) / (2 * c)
|
|
53
|
+
|
|
54
|
+
df["zin"] = ein + zsm
|
|
55
|
+
df["zout"] = eout + zsm
|
|
56
|
+
df["uin"] = np.sqrt(1.0 / hh**2) * c * ein + umm
|
|
57
|
+
df["uout"] = -np.sqrt(1.0 / hh**2) * c * eout + umm
|
|
58
|
+
|
|
59
|
+
return df
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""Probabilistic post-processing for ensemble model output."""
|
|
2
|
+
|
|
3
|
+
from cht_utils.probabilistic.prob_maps import merge_nc_his as merge_nc_his
|
|
4
|
+
from cht_utils.probabilistic.prob_maps import merge_nc_map as merge_nc_map
|
|
5
|
+
from cht_utils.probabilistic.prob_maps import prob_floodmaps as prob_floodmaps
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Generate probability maps and merge ensemble NetCDF output.
|
|
2
|
+
|
|
3
|
+
Supports both regular grids and quadtree (unstructured mesh) datasets.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import xarray as xr
|
|
10
|
+
|
|
11
|
+
from cht_utils.fileops import delete_file
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def prob_floodmaps(
|
|
15
|
+
file_list: List[str],
|
|
16
|
+
variables: List[str],
|
|
17
|
+
prcs: List[float],
|
|
18
|
+
delete: bool = False,
|
|
19
|
+
output_file_name: Optional[str] = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Generate probability flood maps from ensemble NetCDF files.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
file_list : List[str]
|
|
26
|
+
Paths to ensemble member NetCDF files.
|
|
27
|
+
variables : List[str]
|
|
28
|
+
Variable names to process.
|
|
29
|
+
prcs : List[float]
|
|
30
|
+
Percentiles to compute (0-100 scale).
|
|
31
|
+
delete : bool
|
|
32
|
+
Delete input files after reading.
|
|
33
|
+
output_file_name : str or None
|
|
34
|
+
Output NetCDF file path.
|
|
35
|
+
"""
|
|
36
|
+
ds_concat = []
|
|
37
|
+
for file in file_list:
|
|
38
|
+
dsin = xr.open_dataset(file)
|
|
39
|
+
ds_concat.append(dsin[variables])
|
|
40
|
+
if delete:
|
|
41
|
+
delete_file(file)
|
|
42
|
+
|
|
43
|
+
combined_ds = xr.concat(ds_concat, dim="ensemble")
|
|
44
|
+
|
|
45
|
+
out_qq = {}
|
|
46
|
+
for v in variables:
|
|
47
|
+
combined_ds[v] = xr.where(np.isnan(combined_ds[v]), 0, combined_ds[v])
|
|
48
|
+
out_qq[v] = np.percentile(combined_ds[v], prcs, axis=0)
|
|
49
|
+
out_qq[v] = np.where(out_qq[v] == 0, np.nan, out_qq[v])
|
|
50
|
+
|
|
51
|
+
dsin = xr.open_dataset(file_list[0])
|
|
52
|
+
for i, p in enumerate(prcs):
|
|
53
|
+
for v in variables:
|
|
54
|
+
dsin[f"{v}_{p}"] = xr.DataArray(out_qq[v][i], dims=dsin[v].dims)
|
|
55
|
+
dsin[f"{v}_{p}"].attrs["long_name"] = f"{v}_{p}"
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
delete_file(output_file_name)
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
dsin.to_netcdf(path=output_file_name)
|
|
62
|
+
dsin.close()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def merge_nc_his(
|
|
66
|
+
file_list: List[str],
|
|
67
|
+
variables: List[str],
|
|
68
|
+
prcs: Optional[List[float]] = None,
|
|
69
|
+
delete: bool = False,
|
|
70
|
+
output_file_name: Optional[str] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Merge ensemble history files and compute quantiles.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
file_list : List[str]
|
|
77
|
+
Paths to ensemble member NetCDF files.
|
|
78
|
+
variables : List[str]
|
|
79
|
+
Variable names to merge.
|
|
80
|
+
prcs : List[float] or None
|
|
81
|
+
Quantile probabilities (0-1 scale). Defaults to ``[0.05, 0.5, 0.95]``.
|
|
82
|
+
delete : bool
|
|
83
|
+
Delete input files after reading.
|
|
84
|
+
output_file_name : str or None
|
|
85
|
+
Output NetCDF file path.
|
|
86
|
+
"""
|
|
87
|
+
if prcs is None:
|
|
88
|
+
prcs = [0.05, 0.5, 0.95]
|
|
89
|
+
if len(file_list) == 0:
|
|
90
|
+
print("his-file list is empty")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
nens = len(file_list)
|
|
94
|
+
ds = xr.open_dataset(file_list[0])
|
|
95
|
+
if "runtime" in ds.dims:
|
|
96
|
+
ds = ds.drop_dims("runtime")
|
|
97
|
+
if "structures" in ds.dims:
|
|
98
|
+
ds = ds.drop_dims("structures")
|
|
99
|
+
|
|
100
|
+
dimensions = list(ds.dims.keys())
|
|
101
|
+
dimensions = ["time"] + [d for d in dimensions if d != "time"]
|
|
102
|
+
new_dimensions = dimensions + ["ensemble"]
|
|
103
|
+
ens = range(nens)
|
|
104
|
+
|
|
105
|
+
for v in variables:
|
|
106
|
+
ds[v] = xr.DataArray(
|
|
107
|
+
data=np.zeros(
|
|
108
|
+
tuple(ds.dims[d] if d in ds.dims else nens for d in new_dimensions)
|
|
109
|
+
),
|
|
110
|
+
dims=new_dimensions,
|
|
111
|
+
coords={d: ds[d] if d in ds.dims else ens for d in new_dimensions},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
for iens, file in enumerate(file_list):
|
|
115
|
+
dsin = xr.open_dataset(file)
|
|
116
|
+
if "runtime" in dsin.dims:
|
|
117
|
+
dsin = dsin.drop_dims("runtime")
|
|
118
|
+
if "structures" in dsin.dims:
|
|
119
|
+
dsin = dsin.drop_dims("structures")
|
|
120
|
+
for v in variables:
|
|
121
|
+
ds[v][:, :, iens] = dsin[v].transpose("time", ...)
|
|
122
|
+
|
|
123
|
+
for v in variables:
|
|
124
|
+
arr = ds[v].fillna(-999.0).quantile(prcs, dim="ensemble")
|
|
125
|
+
for ip, p in enumerate(prcs):
|
|
126
|
+
ds[f"{v}_{round(p * 100)}"] = arr[ip, :, :]
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
delete_file(output_file_name)
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
ds.to_netcdf(path=output_file_name)
|
|
133
|
+
ds.close()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def merge_nc_map(
|
|
137
|
+
file_list: List[str],
|
|
138
|
+
variables: List[str],
|
|
139
|
+
prcs: Optional[List[float]] = None,
|
|
140
|
+
delete: bool = False,
|
|
141
|
+
output_file_name: Optional[str] = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Merge ensemble map files and compute quantiles.
|
|
144
|
+
|
|
145
|
+
Supports both regular grids and quadtree (unstructured) meshes.
|
|
146
|
+
Uses sorting instead of ``xr.quantile`` for performance.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
file_list : List[str]
|
|
151
|
+
Paths to ensemble member NetCDF files.
|
|
152
|
+
variables : List[str]
|
|
153
|
+
Variable names to merge.
|
|
154
|
+
prcs : List[float] or None
|
|
155
|
+
Quantile probabilities (0-1 scale). Defaults to ``[0.9]``.
|
|
156
|
+
delete : bool
|
|
157
|
+
Delete input files after reading.
|
|
158
|
+
output_file_name : str or None
|
|
159
|
+
Output NetCDF file path.
|
|
160
|
+
"""
|
|
161
|
+
if prcs is None:
|
|
162
|
+
prcs = [0.9]
|
|
163
|
+
|
|
164
|
+
nens = len(file_list)
|
|
165
|
+
ds = xr.open_dataset(file_list[0])
|
|
166
|
+
|
|
167
|
+
if "mesh2d_face_nodes" in ds:
|
|
168
|
+
_merge_quadtree(ds, file_list, variables, prcs, nens)
|
|
169
|
+
else:
|
|
170
|
+
_merge_regular(ds, file_list, variables, prcs, nens)
|
|
171
|
+
|
|
172
|
+
# Remove original variables
|
|
173
|
+
to_drop = ["zs", "zsmax", "cumprcp", "cuminf", "qinf", "hm0max"]
|
|
174
|
+
for v in to_drop:
|
|
175
|
+
if v in ds:
|
|
176
|
+
ds = ds.drop(v)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
delete_file(output_file_name)
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
ds.to_netcdf(path=output_file_name)
|
|
183
|
+
ds.close()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _merge_quadtree(
|
|
187
|
+
ds: xr.Dataset,
|
|
188
|
+
file_list: List[str],
|
|
189
|
+
variables: List[str],
|
|
190
|
+
prcs: List[float],
|
|
191
|
+
nens: int,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Merge quadtree (unstructured) map data."""
|
|
194
|
+
npoints = ds["nmesh2d_face"].size
|
|
195
|
+
for v in variables:
|
|
196
|
+
attrs = ds[v].attrs
|
|
197
|
+
timedim = ds[v].dims[0]
|
|
198
|
+
ntimes = ds.sizes[timedim]
|
|
199
|
+
|
|
200
|
+
arr = xr.DataArray(
|
|
201
|
+
data=np.zeros((ntimes, npoints, nens)),
|
|
202
|
+
dims=(timedim, "nmesh2d_face", "ensemble"),
|
|
203
|
+
coords={timedim: ds[timedim], "ensemble": range(nens)},
|
|
204
|
+
)
|
|
205
|
+
ds[v] = arr
|
|
206
|
+
|
|
207
|
+
for iens, file in enumerate(file_list):
|
|
208
|
+
dsin = xr.open_dataset(file)
|
|
209
|
+
ds[v][:, :, iens] = dsin[v]
|
|
210
|
+
dsin.close()
|
|
211
|
+
|
|
212
|
+
sorted_arr = np.sort(ds[v].values, axis=2)
|
|
213
|
+
for ip, p in enumerate(prcs):
|
|
214
|
+
indx = min(max(int(np.ceil(p * nens)) - 1, 0), nens - 1)
|
|
215
|
+
da = xr.DataArray(
|
|
216
|
+
data=sorted_arr[:, :, indx],
|
|
217
|
+
dims=(timedim, "nmesh2d_face"),
|
|
218
|
+
coords={timedim: ds[timedim]},
|
|
219
|
+
)
|
|
220
|
+
da.attrs = attrs
|
|
221
|
+
ds[f"{v}_{round(p * 100)}"] = da
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _merge_regular(
|
|
225
|
+
ds: xr.Dataset,
|
|
226
|
+
file_list: List[str],
|
|
227
|
+
variables: List[str],
|
|
228
|
+
prcs: List[float],
|
|
229
|
+
nens: int,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Merge regular grid map data."""
|
|
232
|
+
for v in variables:
|
|
233
|
+
attrs = ds[v].attrs
|
|
234
|
+
timedim = ds[v].dims[0]
|
|
235
|
+
ntimes = ds.sizes[timedim]
|
|
236
|
+
|
|
237
|
+
arr = xr.DataArray(
|
|
238
|
+
data=np.zeros((ntimes, ds.x.shape[0], ds.x.shape[1], nens)),
|
|
239
|
+
dims=(timedim, "n", "m", "ensemble"),
|
|
240
|
+
coords={
|
|
241
|
+
timedim: ds[timedim],
|
|
242
|
+
"x": ds.x,
|
|
243
|
+
"y": ds.y,
|
|
244
|
+
"ensemble": range(nens),
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
ds[v] = arr
|
|
248
|
+
|
|
249
|
+
for iens, file in enumerate(file_list):
|
|
250
|
+
dsin = xr.open_dataset(file)
|
|
251
|
+
ds[v][:, :, :, iens] = dsin[v]
|
|
252
|
+
dsin.close()
|
|
253
|
+
|
|
254
|
+
sorted_arr = np.sort(ds[v].values, axis=3)
|
|
255
|
+
for ip, p in enumerate(prcs):
|
|
256
|
+
indx = min(max(int(np.ceil(p * nens)) - 1, 0), nens - 1)
|
|
257
|
+
da = xr.DataArray(
|
|
258
|
+
data=sorted_arr[:, :, :, indx],
|
|
259
|
+
dims=(timedim, "n", "m"),
|
|
260
|
+
coords={timedim: ds[timedim], "x": ds.x, "y": ds.y},
|
|
261
|
+
)
|
|
262
|
+
da.attrs = attrs
|
|
263
|
+
ds[f"{v}_{round(p * 100)}"] = da
|