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.
- ci/environment.yml +29 -0
- roms_tools/__init__.py +6 -0
- roms_tools/_version.py +1 -1
- roms_tools/setup/atmospheric_forcing.py +935 -0
- roms_tools/setup/boundary_forcing.py +711 -0
- roms_tools/setup/datasets.py +457 -0
- roms_tools/setup/fill.py +376 -0
- roms_tools/setup/grid.py +610 -325
- roms_tools/setup/initial_conditions.py +528 -0
- roms_tools/setup/plot.py +203 -0
- roms_tools/setup/tides.py +809 -0
- roms_tools/setup/topography.py +257 -0
- roms_tools/setup/utils.py +162 -0
- roms_tools/setup/vertical_coordinate.py +494 -0
- roms_tools/tests/test_atmospheric_forcing.py +1645 -0
- roms_tools/tests/test_boundary_forcing.py +332 -0
- roms_tools/tests/test_datasets.py +306 -0
- roms_tools/tests/test_grid.py +226 -0
- roms_tools/tests/test_initial_conditions.py +300 -0
- roms_tools/tests/test_tides.py +366 -0
- roms_tools/tests/test_topography.py +78 -0
- roms_tools/tests/test_vertical_coordinate.py +337 -0
- roms_tools-0.20.dist-info/METADATA +90 -0
- roms_tools-0.20.dist-info/RECORD +28 -0
- {roms_tools-0.0.6.dist-info → roms_tools-0.20.dist-info}/WHEEL +1 -1
- {roms_tools-0.0.6.dist-info → roms_tools-0.20.dist-info}/top_level.txt +1 -0
- roms_tools/tests/test_setup.py +0 -54
- roms_tools-0.0.6.dist-info/METADATA +0 -134
- roms_tools-0.0.6.dist-info/RECORD +0 -10
- {roms_tools-0.0.6.dist-info → roms_tools-0.20.dist-info}/LICENSE +0 -0
roms_tools/setup/fill.py
ADDED
|
@@ -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
|