roms-tools 0.20__py3-none-any.whl → 1.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,395 @@
1
+ from dataclasses import dataclass
2
+ from roms_tools.setup.grid import Grid
3
+ from roms_tools.setup.vertical_coordinate import VerticalCoordinate
4
+ from roms_tools.setup.fill import fill_and_interpolate
5
+ from roms_tools.setup.utils import (
6
+ extrapolate_deepest_to_bottom,
7
+ interpolate_from_rho_to_u,
8
+ interpolate_from_rho_to_v,
9
+ )
10
+ import xarray as xr
11
+ import numpy as np
12
+
13
+
14
+ @dataclass(frozen=True, kw_only=True)
15
+ class ROMSToolsMixins:
16
+ """
17
+ Represents a mixin tool for ROMS-Tools with capabilities shared by the various
18
+ ROMS-Tools dataclasses.
19
+
20
+ Parameters
21
+ ----------
22
+ grid : Grid
23
+ Object representing the grid information used for the model.
24
+ vertical_coordinate : VerticalCoordinate
25
+ Object representing the vertical coordinate system. Defaults to None.
26
+
27
+ """
28
+
29
+ grid: Grid
30
+ vertical_coordinate: VerticalCoordinate = None
31
+
32
+ def get_target_lon_lat(self, use_coarse_grid=False):
33
+ """
34
+ Retrieves the longitude and latitude arrays from the grid and adjusts them based on the grid's orientation.
35
+
36
+ This method provides longitude and latitude coordinates, with options for using a coarse grid
37
+ if specified. It also handles longitudes to ensure they are between -180 and 180 degrees and adjusts
38
+ based on whether the grid straddles the Greenwich meridian.
39
+
40
+ Parameters
41
+ ----------
42
+ use_coarse_grid : bool, optional
43
+ If True, uses the coarse grid data for longitude and latitude. Defaults to False.
44
+
45
+ Returns
46
+ -------
47
+ tuple of (xarray.DataArray, xarray.DataArray, xarray.DataArray, bool)
48
+ The longitude latitude, and angle arrays, and a boolean indicating whether the grid straddles the meridian.
49
+
50
+ Raises
51
+ ------
52
+ ValueError
53
+ If the coarse grid data has not been generated yet.
54
+ """
55
+
56
+ if use_coarse_grid:
57
+ if "lon_coarse" not in self.grid.ds:
58
+ raise ValueError(
59
+ "Grid has not been coarsened yet. Execute grid.coarsen() first."
60
+ )
61
+
62
+ lon = self.grid.ds.lon_coarse
63
+ lat = self.grid.ds.lat_coarse
64
+ angle = self.grid.ds.angle_coarse
65
+ else:
66
+ lon = self.grid.ds.lon_rho
67
+ lat = self.grid.ds.lat_rho
68
+ angle = self.grid.ds.angle
69
+
70
+ # operate on longitudes between -180 and 180 unless ROMS domain lies at least 5 degrees in lontitude away from Greenwich meridian
71
+ lon = xr.where(lon > 180, lon - 360, lon)
72
+ straddle = True
73
+ if not self.grid.straddle and abs(lon).min() > 5:
74
+ lon = xr.where(lon < 0, lon + 360, lon)
75
+ straddle = False
76
+
77
+ return lon, lat, angle, straddle
78
+
79
+ def regrid_data(self, data, vars_2d, vars_3d, lon, lat):
80
+
81
+ """
82
+ Interpolates data onto the desired grid and processes it for 2D and 3D variables.
83
+
84
+ This method interpolates the specified 2D and 3D variables onto a new grid defined by the provided
85
+ longitude and latitude coordinates. It handles both 2D and 3D data, performing extrapolation for 3D
86
+ variables to fill values up to the bottom of the depth range.
87
+
88
+ Parameters
89
+ ----------
90
+ data : DataContainer
91
+ The container holding the variables to be interpolated. It must include attributes such as
92
+ `dim_names` and `var_names`.
93
+ vars_2d : list of str
94
+ List of 2D variable names that should be interpolated.
95
+ vars_3d : list of str
96
+ List of 3D variable names that should be interpolated.
97
+ lon : xarray.DataArray
98
+ Longitude coordinates for interpolation.
99
+ lat : xarray.DataArray
100
+ Latitude coordinates for interpolation.
101
+
102
+ Returns
103
+ -------
104
+ dict of str: xarray.DataArray
105
+ A dictionary where keys are variable names and values are the interpolated DataArrays.
106
+
107
+ Notes
108
+ -----
109
+ - 2D interpolation is performed using linear interpolation on the provided latitude and longitude coordinates.
110
+ - For 3D variables, the method extrapolates the deepest values to the bottom of the depth range and interpolates
111
+ using the specified depth coordinates.
112
+ - The method assumes the presence of `dim_names` and `var_names` attributes in the `data` object.
113
+ """
114
+
115
+ # interpolate onto desired grid
116
+ data_vars = {}
117
+
118
+ # 2d interpolation
119
+ fill_dims = [data.dim_names["latitude"], data.dim_names["longitude"]]
120
+ coords = {data.dim_names["latitude"]: lat, data.dim_names["longitude"]: lon}
121
+ for var in vars_2d:
122
+ mask = xr.where(data.ds[data.var_names[var]].isel(time=0).isnull(), 0, 1)
123
+
124
+ data_vars[var] = fill_and_interpolate(
125
+ data.ds[data.var_names[var]].astype(np.float64),
126
+ mask,
127
+ fill_dims=fill_dims,
128
+ coords=coords,
129
+ method="linear",
130
+ )
131
+
132
+ if vars_3d:
133
+ # 3d interpolation
134
+ coords = {
135
+ data.dim_names["latitude"]: lat,
136
+ data.dim_names["longitude"]: lon,
137
+ data.dim_names["depth"]: self.vertical_coordinate.ds["layer_depth_rho"],
138
+ }
139
+ # extrapolate deepest value all the way to bottom ("flooding")
140
+ for var in vars_3d:
141
+ data.ds[data.var_names[var]] = extrapolate_deepest_to_bottom(
142
+ data.ds[data.var_names[var]], data.dim_names["depth"]
143
+ )
144
+ mask = xr.where(data.ds[data.var_names[var]].isel(time=0).isnull(), 0, 1)
145
+
146
+ # setting fillvalue_interp to None means that we allow extrapolation in the
147
+ # interpolation step to avoid NaNs at the surface if the lowest depth in original
148
+ # data is greater than zero
149
+
150
+ data_vars[var] = fill_and_interpolate(
151
+ data.ds[data.var_names[var]].astype(np.float64),
152
+ mask,
153
+ fill_dims=fill_dims,
154
+ coords=coords,
155
+ method="linear",
156
+ fillvalue_interp=None,
157
+ )
158
+ if data.dim_names["time"] != "time":
159
+ data_vars[var] = data_vars[var].rename({data.dim_names["time"]: "time"})
160
+
161
+ return data_vars
162
+
163
+ def process_velocities(self, data_vars, angle, interpolate=True):
164
+ """
165
+ Processes and rotates velocity components, and interpolates them to the appropriate grid points.
166
+
167
+ This method performs the following steps:
168
+ 1. Rotates the velocity components to align with the grid orientation using the provided angle.
169
+ 2. Optionally interpolates the rotated velocities to the u- and v-points of the grid.
170
+ 3. If a vertical coordinate is provided, computes the barotropic velocities by integrating
171
+ over the vertical dimension.
172
+
173
+ Parameters
174
+ ----------
175
+ data_vars : dict of str: xarray.DataArray
176
+ Dictionary containing the velocity components to be processed. Must include keys "u" and "v"
177
+ or "uwnd" and "vwnd".
178
+ angle : xarray.DataArray
179
+ DataArray containing the angle used for rotating the velocity components to the grid orientation.
180
+ interpolate : bool, optional
181
+ If True, interpolates the velocities to the u- and v-points. Defaults to True.
182
+
183
+ Returns
184
+ -------
185
+ dict of str: xarray.DataArray
186
+ Dictionary of processed velocity components. Includes "ubar" and "vbar" if a vertical coordinate
187
+ is provided.
188
+ """
189
+ # Determine the correct variable names based on the keys in data_vars
190
+ uname = "u" if "u" in data_vars else "uwnd"
191
+ vname = "v" if "v" in data_vars else "vwnd"
192
+
193
+ # Rotate velocities to grid orientation
194
+ u_rot = data_vars[uname] * np.cos(angle) + data_vars[vname] * np.sin(angle)
195
+ v_rot = data_vars[vname] * np.cos(angle) - data_vars[uname] * np.sin(angle)
196
+
197
+ # Interpolate to u- and v-points
198
+ if interpolate:
199
+ data_vars[uname] = interpolate_from_rho_to_u(u_rot)
200
+ data_vars[vname] = interpolate_from_rho_to_v(v_rot)
201
+ else:
202
+ data_vars[uname] = u_rot
203
+ data_vars[vname] = v_rot
204
+
205
+ if self.vertical_coordinate is not None:
206
+ # 3D masks for ROMS domain
207
+ umask = self.grid.ds.mask_u.expand_dims({"s_rho": data_vars[uname].s_rho})
208
+ vmask = self.grid.ds.mask_v.expand_dims({"s_rho": data_vars[vname].s_rho})
209
+
210
+ data_vars[uname] = data_vars[uname] * umask
211
+ data_vars[vname] = data_vars[vname] * vmask
212
+
213
+ # Compute barotropic velocity
214
+ dz = -self.vertical_coordinate.ds["interface_depth_rho"].diff(dim="s_w")
215
+ dz = dz.rename({"s_w": "s_rho"})
216
+ dzu = interpolate_from_rho_to_u(dz)
217
+ dzv = interpolate_from_rho_to_v(dz)
218
+
219
+ data_vars["ubar"] = (
220
+ (dzu * data_vars[uname]).sum(dim="s_rho") / dzu.sum(dim="s_rho")
221
+ ).transpose("time", "eta_rho", "xi_u")
222
+ data_vars["vbar"] = (
223
+ (dzv * data_vars[vname]).sum(dim="s_rho") / dzv.sum(dim="s_rho")
224
+ ).transpose("time", "eta_v", "xi_rho")
225
+
226
+ return data_vars
227
+
228
+ def get_variable_metadata(self):
229
+ """
230
+ Retrieves metadata for commonly used variables in the dataset.
231
+
232
+ This method returns a dictionary containing the metadata for various variables, including long names
233
+ and units for each variable.
234
+
235
+ Returns
236
+ -------
237
+ dict of str: dict
238
+ Dictionary where keys are variable names and values are dictionaries with "long_name" and "units" keys.
239
+
240
+ """
241
+
242
+ d = {
243
+ "uwnd": {"long_name": "10 meter wind in x-direction", "units": "m/s"},
244
+ "vwnd": {"long_name": "10 meter wind in y-direction", "units": "m/s"},
245
+ "swrad": {
246
+ "long_name": "downward short-wave (solar) radiation",
247
+ "units": "W/m^2",
248
+ },
249
+ "lwrad": {
250
+ "long_name": "downward long-wave (thermal) radiation",
251
+ "units": "W/m^2",
252
+ },
253
+ "Tair": {"long_name": "air temperature at 2m", "units": "degrees Celsius"},
254
+ "qair": {"long_name": "absolute humidity at 2m", "units": "kg/kg"},
255
+ "rain": {"long_name": "total precipitation", "units": "cm/day"},
256
+ "temp": {"long_name": "potential temperature", "units": "degrees Celsius"},
257
+ "salt": {"long_name": "salinity", "units": "PSU"},
258
+ "zeta": {"long_name": "sea surface height", "units": "m"},
259
+ "u": {"long_name": "u-flux component", "units": "m/s"},
260
+ "v": {"long_name": "v-flux component", "units": "m/s"},
261
+ "w": {"long_name": "w-flux component", "units": "m/s"},
262
+ "ubar": {
263
+ "long_name": "vertically integrated u-flux component",
264
+ "units": "m/s",
265
+ },
266
+ "vbar": {
267
+ "long_name": "vertically integrated v-flux component",
268
+ "units": "m/s",
269
+ },
270
+ "PO4": {"long_name": "dissolved inorganic phosphate", "units": "mmol/m^3"},
271
+ "NO3": {"long_name": "dissolved inorganic nitrate", "units": "mmol/m^3"},
272
+ "SiO3": {"long_name": "dissolved inorganic silicate", "units": "mmol/m^3"},
273
+ "NH4": {"long_name": "dissolved ammonia", "units": "mmol/m^3"},
274
+ "Fe": {"long_name": "dissolved inorganic iron", "units": "mmol/m^3"},
275
+ "Lig": {"long_name": "iron binding ligand", "units": "mmol/m^3"},
276
+ "O2": {"long_name": "dissolved oxygen", "units": "mmol/m^3"},
277
+ "DIC": {"long_name": "dissolved inorganic carbon", "units": "mmol/m^3"},
278
+ "DIC_ALT_CO2": {
279
+ "long_name": "dissolved inorganic carbon, alternative CO2",
280
+ "units": "mmol/m^3",
281
+ },
282
+ "ALK": {"long_name": "alkalinity", "units": "meq/m^3"},
283
+ "ALK_ALT_CO2": {
284
+ "long_name": "alkalinity, alternative CO2",
285
+ "units": "meq/m^3",
286
+ },
287
+ "DOC": {"long_name": "dissolved organic carbon", "units": "mmol/m^3"},
288
+ "DON": {"long_name": "dissolved organic nitrogen", "units": "mmol/m^3"},
289
+ "DOP": {"long_name": "dissolved organic phosphorus", "units": "mmol/m^3"},
290
+ "DOCr": {
291
+ "long_name": "refractory dissolved organic carbon",
292
+ "units": "mmol/m^3",
293
+ },
294
+ "DONr": {
295
+ "long_name": "refractory dissolved organic nitrogen",
296
+ "units": "mmol/m^3",
297
+ },
298
+ "DOPr": {
299
+ "long_name": "refractory dissolved organic phosphorus",
300
+ "units": "mmol/m^3",
301
+ },
302
+ "zooC": {"long_name": "zooplankton carbon", "units": "mmol/m^3"},
303
+ "spChl": {
304
+ "long_name": "small phytoplankton chlorophyll",
305
+ "units": "mg/m^3",
306
+ },
307
+ "spC": {"long_name": "small phytoplankton carbon", "units": "mmol/m^3"},
308
+ "spP": {
309
+ "long_name": "small phytoplankton phosphorous",
310
+ "units": "mmol/m^3",
311
+ },
312
+ "spFe": {"long_name": "small phytoplankton iron", "units": "mmol/m^3"},
313
+ "spCaCO3": {"long_name": "small phytoplankton CaCO3", "units": "mmol/m^3"},
314
+ "diatChl": {"long_name": "diatom chloropyll", "units": "mg/m^3"},
315
+ "diatC": {"long_name": "diatom carbon", "units": "mmol/m^3"},
316
+ "diatP": {"long_name": "diatom phosphorus", "units": "mmol/m^3"},
317
+ "diatFe": {"long_name": "diatom iron", "units": "mmol/m^3"},
318
+ "diatSi": {"long_name": "diatom silicate", "units": "mmol/m^3"},
319
+ "diazChl": {"long_name": "diazotroph chloropyll", "units": "mg/m^3"},
320
+ "diazC": {"long_name": "diazotroph carbon", "units": "mmol/m^3"},
321
+ "diazP": {"long_name": "diazotroph phosphorus", "units": "mmol/m^3"},
322
+ "diazFe": {"long_name": "diazotroph iron", "units": "mmol/m^3"},
323
+ "pco2_air": {"long_name": "atmospheric pCO2", "units": "ppmv"},
324
+ "pco2_air_alt": {
325
+ "long_name": "atmospheric pCO2, alternative CO2",
326
+ "units": "ppmv",
327
+ },
328
+ "iron": {"long_name": "iron decomposition", "units": "nmol/cm^2/s"},
329
+ "dust": {"long_name": "dust decomposition", "units": "kg/m^2/s"},
330
+ "nox": {"long_name": "NOx decomposition", "units": "kg/m^2/s"},
331
+ "nhy": {"long_name": "NHy decomposition", "units": "kg/m^2/s"},
332
+ }
333
+ return d
334
+
335
+ def get_boundary_info(self):
336
+ """
337
+ Provides boundary coordinate information and renaming conventions for grid boundaries.
338
+
339
+ This method returns two dictionaries: one specifying the boundary coordinates for different types of
340
+ grid variables (e.g., "rho", "u", "v"), and another specifying how to rename dimensions for these boundaries.
341
+
342
+ Returns
343
+ -------
344
+ tuple of (dict, dict)
345
+ - A dictionary mapping variable types and directions to boundary coordinates.
346
+ - A dictionary mapping variable types and directions to new dimension names.
347
+
348
+ """
349
+
350
+ # Boundary coordinates
351
+ bdry_coords = {
352
+ "rho": {
353
+ "south": {"eta_rho": 0},
354
+ "east": {"xi_rho": -1},
355
+ "north": {"eta_rho": -1},
356
+ "west": {"xi_rho": 0},
357
+ },
358
+ "u": {
359
+ "south": {"eta_rho": 0},
360
+ "east": {"xi_u": -1},
361
+ "north": {"eta_rho": -1},
362
+ "west": {"xi_u": 0},
363
+ },
364
+ "v": {
365
+ "south": {"eta_v": 0},
366
+ "east": {"xi_rho": -1},
367
+ "north": {"eta_v": -1},
368
+ "west": {"xi_rho": 0},
369
+ },
370
+ }
371
+
372
+ # How to rename the dimensions
373
+
374
+ rename = {
375
+ "rho": {
376
+ "south": {"xi_rho": "xi_rho_south"},
377
+ "east": {"eta_rho": "eta_rho_east"},
378
+ "north": {"xi_rho": "xi_rho_north"},
379
+ "west": {"eta_rho": "eta_rho_west"},
380
+ },
381
+ "u": {
382
+ "south": {"xi_u": "xi_u_south"},
383
+ "east": {"eta_rho": "eta_u_east"},
384
+ "north": {"xi_u": "xi_u_north"},
385
+ "west": {"eta_rho": "eta_u_west"},
386
+ },
387
+ "v": {
388
+ "south": {"xi_rho": "xi_v_south"},
389
+ "east": {"eta_v": "eta_v_east"},
390
+ "north": {"xi_rho": "xi_v_north"},
391
+ "west": {"eta_v": "eta_v_west"},
392
+ },
393
+ }
394
+
395
+ return bdry_coords, rename