roms-tools 0.0.6__py3-none-any.whl → 0.20__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,376 @@
1
+ import numpy as np
2
+ import xarray as xr
3
+ from numba import jit
4
+
5
+
6
+ def fill_and_interpolate(
7
+ field,
8
+ mask,
9
+ fill_dims,
10
+ coords,
11
+ method="linear",
12
+ fillvalue_fill=0.0,
13
+ fillvalue_interp=np.nan,
14
+ ):
15
+ """
16
+ Propagates ocean values into land areas and interpolates the data to specified coordinates using a given method.
17
+
18
+ Parameters
19
+ ----------
20
+ field : xr.DataArray
21
+ The data array to be interpolated, typically containing oceanographic or atmospheric data
22
+ with dimensions such as latitude and longitude.
23
+
24
+ mask : xr.DataArray
25
+ A data array with the same spatial dimensions as `field`, where `1` indicates ocean points
26
+ and `0` indicates land points. This mask is used to identify land and ocean areas in the dataset.
27
+
28
+ fill_dims : list of str
29
+ List specifying the dimensions along which to perform the lateral fill, typically the horizontal
30
+ dimensions such as latitude and longitude, e.g., ["latitude", "longitude"].
31
+
32
+ coords : dict
33
+ Dictionary specifying the target coordinates for interpolation. The keys should match the dimensions
34
+ of `field` (e.g., {"longitude": lon_values, "latitude": lat_values, "depth": depth_values}).
35
+ This dictionary provides the new coordinates onto which the data array will be interpolated.
36
+
37
+ method : str, optional, default='linear'
38
+ The interpolation method to use. Valid options are those supported by `xarray.DataArray.interp`,
39
+ such as 'linear' or 'nearest'.
40
+
41
+ fillvalue_fill : float, optional, default=0.0
42
+ Value to use in the fill step if an entire data slice along the fill dimensions contains only NaNs.
43
+
44
+ fillvalue_interp : float, optional, default=np.nan
45
+ Value to use in the interpolation step. `np.nan` means that no extrapolation is applied.
46
+ `None` means that extrapolation is applied, which often makes sense when interpolating in the
47
+ vertical direction to avoid NaNs at the surface if the lowest depth is greater than zero.
48
+
49
+ Returns
50
+ -------
51
+ xr.DataArray
52
+ The interpolated data array. This array has the same dimensions as the input `field` but with values
53
+ interpolated to the new coordinates specified in `coords`.
54
+
55
+ Notes
56
+ -----
57
+ This method performs the following steps:
58
+ 1. Sets land values to NaN based on the provided mask to ensure that interpolation does not cross
59
+ the land-ocean boundary.
60
+ 2. Uses the `lateral_fill` function to propagate ocean values into the land interior, helping to fill
61
+ gaps in the dataset.
62
+ 3. Interpolates the filled data array over the specified coordinates using the selected interpolation method.
63
+
64
+ Example
65
+ -------
66
+ >>> import xarray as xr
67
+ >>> field = xr.DataArray(...)
68
+ >>> mask = xr.DataArray(...)
69
+ >>> fill_dims = ["latitude", "longitude"]
70
+ >>> coords = {"latitude": new_lat_values, "longitude": new_lon_values}
71
+ >>> interpolated_field = fill_and_interpolate(
72
+ ... field, mask, fill_dims, coords, method="linear"
73
+ ... )
74
+ >>> print(interpolated_field)
75
+ """
76
+ if not isinstance(field, xr.DataArray):
77
+ raise TypeError("field must be an xarray.DataArray")
78
+ if not isinstance(mask, xr.DataArray):
79
+ raise TypeError("mask must be an xarray.DataArray")
80
+ if not isinstance(coords, dict):
81
+ raise TypeError("coords must be a dictionary")
82
+ if not all(dim in field.dims for dim in coords.keys()):
83
+ raise ValueError("All keys in coords must match dimensions of field")
84
+ if method not in ["linear", "nearest"]:
85
+ raise ValueError(
86
+ "Unsupported interpolation method. Choose from 'linear', 'nearest'"
87
+ )
88
+
89
+ # Set land values to NaN
90
+ field = field.where(mask)
91
+
92
+ # Propagate ocean values into land interior before interpolation
93
+ field = lateral_fill(field, 1 - mask, fill_dims, fillvalue_fill)
94
+
95
+ field_interpolated = field.interp(
96
+ coords, method=method, kwargs={"fill_value": fillvalue_interp}
97
+ ).drop_vars(list(coords.keys()))
98
+
99
+ return field_interpolated
100
+
101
+
102
+ def lateral_fill(var, land_mask, dims=["latitude", "longitude"], fillvalue=0.0):
103
+ """
104
+ Perform lateral fill on an xarray DataArray using a land mask.
105
+
106
+ Parameters
107
+ ----------
108
+ var : xarray.DataArray
109
+ DataArray on which to fill NaNs. The fill is performed on the dimensions specified
110
+ in `dims`.
111
+
112
+ land_mask : xarray.DataArray
113
+ Boolean DataArray indicating valid values: `True` where data should be filled. Must have the
114
+ same shape as `var` for the specified dimensions.
115
+
116
+ dims : list of str, optional, default=['latitude', 'longitude']
117
+ Dimensions along which to perform the fill. The default is ['latitude', 'longitude'].
118
+
119
+ fillvalue : float, optional, default=0.0
120
+ Value to use if an entire data slice along the dims contains only NaNs.
121
+
122
+ Returns
123
+ -------
124
+ var_filled : xarray.DataArray
125
+ DataArray with NaNs filled by iterative smoothing, except for the regions
126
+ specified by `land_mask` where NaNs are preserved.
127
+
128
+ """
129
+
130
+ var_filled = xr.apply_ufunc(
131
+ _lateral_fill_np_array,
132
+ var,
133
+ land_mask,
134
+ input_core_dims=[dims, dims],
135
+ output_core_dims=[dims],
136
+ output_dtypes=[var.dtype],
137
+ dask="parallelized",
138
+ vectorize=True,
139
+ kwargs={"fillvalue": fillvalue},
140
+ )
141
+
142
+ return var_filled
143
+
144
+
145
+ def _lateral_fill_np_array(
146
+ var, isvalid_mask, fillvalue=0.0, tol=1.0e-4, rc=1.8, max_iter=10000
147
+ ):
148
+ """
149
+ Perform lateral fill on a numpy array.
150
+
151
+ Parameters
152
+ ----------
153
+ var : numpy.array
154
+ Two-dimensional array on which to fill NaNs.Only NaNs where `isvalid_mask` is
155
+ True will be filled.
156
+
157
+ isvalid_mask : numpy.array, boolean
158
+ Valid values mask: `True` where data should be filled. Must have same shape
159
+ as `var`.
160
+
161
+ fillvalue: float
162
+ Value to use if the full field `var` contains only NaNs. Default is 0.0.
163
+
164
+ tol : float, optional, default=1.0e-4
165
+ Convergence criteria: stop filling when the value change is less than
166
+ or equal to `tol * var`, i.e., `delta <= tol * np.abs(var[j, i])`.
167
+
168
+ rc : float, optional, default=1.8
169
+ Over-relaxation coefficient to use in the Successive Over-Relaxation (SOR)
170
+ fill algorithm. Larger arrays (or extent of region to be filled if not global)
171
+ typically converge faster with larger coefficients. For completely
172
+ land-filling a 1-degree grid (360x180), a coefficient in the range 1.85-1.9
173
+ is near optimal. Valid bounds are (1.0, 2.0).
174
+
175
+ max_iter : int, optional, default=10000
176
+ Maximum number of iterations to perform before giving up if the tolerance
177
+ is not reached.
178
+
179
+ Returns
180
+ -------
181
+ var : numpy.array
182
+ Array with NaNs filled by iterative smoothing, except for the regions
183
+ specified by `isvalid_mask` where NaNs are preserved.
184
+
185
+
186
+ Example
187
+ -------
188
+ >>> import numpy as np
189
+ >>> var = np.array([[1, 2, np.nan], [4, np.nan, 6]])
190
+ >>> isvalid_mask = np.array([[True, True, True], [True, True, True]])
191
+ >>> filled_var = lateral_fill_np_array(var, isvalid_mask)
192
+ >>> print(filled_var)
193
+ """
194
+ nlat, nlon = var.shape[-2:]
195
+ var = var.copy()
196
+
197
+ fillmask = np.isnan(var) # Fill all NaNs
198
+ keepNaNs = ~isvalid_mask & np.isnan(var)
199
+ var = _iterative_fill_sor(nlat, nlon, var, fillmask, tol, rc, max_iter, fillvalue)
200
+ var[keepNaNs] = np.nan # Replace NaNs in areas not designated for filling
201
+
202
+ return var
203
+
204
+
205
+ @jit(nopython=True, parallel=True)
206
+ def _iterative_fill_sor(nlat, nlon, var, fillmask, tol, rc, max_iter, fillvalue=0.0):
207
+ """
208
+ Perform an iterative land fill algorithm using the Successive Over-Relaxation (SOR)
209
+ solution of the Laplace Equation.
210
+
211
+ Parameters
212
+ ----------
213
+ nlat : int
214
+ Number of latitude points in the array.
215
+
216
+ nlon : int
217
+ Number of longitude points in the array.
218
+
219
+ var : numpy.array
220
+ Two-dimensional array on which to fill NaNs.
221
+
222
+ fillmask : numpy.array, boolean
223
+ Mask indicating positions to be filled: `True` where data should be filled.
224
+
225
+ tol : float
226
+ Convergence criterion: the iterative process stops when the maximum residual change
227
+ is less than or equal to `tol`.
228
+
229
+ rc : float
230
+ Over-relaxation coefficient used in the SOR algorithm. Must be between 1.0 and 2.0.
231
+
232
+ max_iter : int
233
+ Maximum number of iterations allowed before the process is terminated.
234
+
235
+ fillvalue: float
236
+ Value to use if the full field is NaNs. Default is 0.0.
237
+
238
+ Returns
239
+ -------
240
+ None
241
+ The input array `var` is modified in-place with the NaN values filled.
242
+
243
+ Notes
244
+ -----
245
+ This function performs the following steps:
246
+ 1. Computes a zonal mean to use as an initial guess for the fill.
247
+ 2. Replaces missing values in the input array with the computed zonal average.
248
+ 3. Iteratively fills the missing values using the SOR algorithm until the specified
249
+ tolerance `tol` is reached or the maximum number of iterations `max_iter` is exceeded.
250
+
251
+ Example
252
+ -------
253
+ >>> nlat, nlon = 180, 360
254
+ >>> var = np.random.rand(nlat, nlon)
255
+ >>> fillmask = np.isnan(var)
256
+ >>> tol = 1.0e-4
257
+ >>> rc = 1.8
258
+ >>> max_iter = 10000
259
+ >>> _iterative_fill_sor(nlat, nlon, var, fillmask, tol, rc, max_iter)
260
+ """
261
+
262
+ # If field consists only of zeros, fill NaNs in with zeros and all done
263
+ # Note: this will happen for shortwave downward radiation at night time
264
+ if np.max(np.fabs(var)) == 0.0:
265
+ var = np.zeros_like(var)
266
+ return var
267
+ # If field consists only of NaNs, fill NaNs with fill value
268
+ if np.isnan(var).all():
269
+ var = fillvalue * np.ones_like(var)
270
+ return var
271
+
272
+ # Compute a zonal mean to use as a first guess
273
+ zoncnt = np.zeros(nlat)
274
+ zonavg = np.zeros(nlat)
275
+ for j in range(0, nlat):
276
+ zoncnt[j] = np.sum(np.where(fillmask[j, :], 0, 1))
277
+ zonavg[j] = np.sum(np.where(fillmask[j, :], 0, var[j, :]))
278
+ if zoncnt[j] != 0:
279
+ zonavg[j] = zonavg[j] / zoncnt[j]
280
+
281
+ # Fill missing zonal averages for rows that are entirely land
282
+ for j in range(0, nlat - 1): # northward pass
283
+ if zoncnt[j] > 0 and zoncnt[j + 1] == 0:
284
+ zoncnt[j + 1] = 1
285
+ zonavg[j + 1] = zonavg[j]
286
+ for j in range(nlat - 1, 0, -1): # southward pass
287
+ if zoncnt[j] > 0 and zoncnt[j - 1] == 0:
288
+ zoncnt[j - 1] = 1
289
+ zonavg[j - 1] = zonavg[j]
290
+
291
+ # Replace the input array missing values with zonal average as first guess
292
+ for j in range(0, nlat):
293
+ for i in range(0, nlon):
294
+ if fillmask[j, i]:
295
+ var[j, i] = zonavg[j]
296
+
297
+ # Now do the iterative 2D fill
298
+ res = np.zeros((nlat, nlon)) # work array hold residuals
299
+ res_max = tol
300
+ iter_cnt = 0
301
+ while iter_cnt < max_iter and res_max >= tol:
302
+ res[:] = 0.0 # reset the residual to zero for this iteration
303
+
304
+ for j in range(1, nlat - 1):
305
+ jm1 = j - 1
306
+ jp1 = j + 1
307
+
308
+ for i in range(1, nlon - 1):
309
+ if fillmask[j, i]:
310
+ im1 = i - 1
311
+ ip1 = i + 1
312
+
313
+ # this is SOR
314
+ res[j, i] = (
315
+ var[j, ip1]
316
+ + var[j, im1]
317
+ + var[jm1, i]
318
+ + var[jp1, i]
319
+ - 4.0 * var[j, i]
320
+ )
321
+ var[j, i] = var[j, i] + rc * 0.25 * res[j, i]
322
+
323
+ # do 1D smooth on top and bottom row if there is some valid data there in the input
324
+ # otherwise leave it set to zonal average
325
+ for j in [0, nlat - 1]:
326
+ if zoncnt[j] > 1:
327
+
328
+ for i in range(1, nlon - 1):
329
+ if fillmask[j, i]:
330
+ im1 = i - 1
331
+ ip1 = i + 1
332
+
333
+ res[j, i] = var[j, ip1] + var[j, im1] - 2.0 * var[j, i]
334
+ var[j, i] = var[j, i] + rc * 0.5 * res[j, i]
335
+
336
+ # do 1D smooth in the vertical on left and right column
337
+ for i in [0, nlon - 1]:
338
+
339
+ for j in range(1, nlat - 1):
340
+ if fillmask[j, i]:
341
+ jm1 = j - 1
342
+ jp1 = j + 1
343
+
344
+ res[j, i] = var[jp1, i] + var[jm1, i] - 2.0 * var[j, i]
345
+ var[j, i] = var[j, i] + rc * 0.5 * res[j, i]
346
+
347
+ # four corners
348
+ for j in [0, nlat - 1]:
349
+ if j == 0:
350
+ jp1 = j + 1
351
+ jm1 = j
352
+ elif j == nlat - 1:
353
+ jp1 = j
354
+ jm1 = j - 1
355
+
356
+ for i in [0, nlon - 1]:
357
+ if i == 0:
358
+ ip1 = i + 1
359
+ im1 = i
360
+ elif i == nlon - 1:
361
+ ip1 = i
362
+ im1 = i - 1
363
+
364
+ res[j, i] = (
365
+ var[j, ip1]
366
+ + var[j, im1]
367
+ + var[jm1, i]
368
+ + var[jp1, i]
369
+ - 4.0 * var[j, i]
370
+ )
371
+ var[j, i] = var[j, i] + rc * 0.25 * res[j, i]
372
+
373
+ res_max = np.max(np.fabs(res)) / np.max(np.fabs(var))
374
+ iter_cnt += 1
375
+
376
+ return var