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.
@@ -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