roms-tools 0.1.0__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 +1 -0
- roms_tools/__init__.py +3 -0
- roms_tools/_version.py +1 -1
- roms_tools/setup/atmospheric_forcing.py +335 -393
- roms_tools/setup/boundary_forcing.py +711 -0
- roms_tools/setup/datasets.py +434 -25
- roms_tools/setup/fill.py +118 -5
- roms_tools/setup/grid.py +145 -19
- roms_tools/setup/initial_conditions.py +528 -0
- roms_tools/setup/plot.py +149 -4
- roms_tools/setup/tides.py +570 -437
- roms_tools/setup/topography.py +17 -2
- 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.1.0.dist-info → roms_tools-0.20.dist-info}/METADATA +3 -2
- roms_tools-0.20.dist-info/RECORD +28 -0
- {roms_tools-0.1.0.dist-info → roms_tools-0.20.dist-info}/WHEEL +1 -1
- roms_tools/tests/test_setup.py +0 -181
- roms_tools-0.1.0.dist-info/RECORD +0 -17
- {roms_tools-0.1.0.dist-info → roms_tools-0.20.dist-info}/LICENSE +0 -0
- {roms_tools-0.1.0.dist-info → roms_tools-0.20.dist-info}/top_level.txt +0 -0
roms_tools/setup/tides.py
CHANGED
|
@@ -1,11 +1,498 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
import xarray as xr
|
|
3
3
|
import numpy as np
|
|
4
|
-
|
|
4
|
+
import yaml
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field, asdict
|
|
5
8
|
from roms_tools.setup.grid import Grid
|
|
6
9
|
from roms_tools.setup.plot import _plot
|
|
7
|
-
import
|
|
8
|
-
import
|
|
10
|
+
from roms_tools.setup.fill import fill_and_interpolate
|
|
11
|
+
from roms_tools.setup.datasets import Dataset
|
|
12
|
+
from roms_tools.setup.utils import (
|
|
13
|
+
nan_check,
|
|
14
|
+
interpolate_from_rho_to_u,
|
|
15
|
+
interpolate_from_rho_to_v,
|
|
16
|
+
)
|
|
17
|
+
from typing import Dict, List
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, kw_only=True)
|
|
22
|
+
class TPXO(Dataset):
|
|
23
|
+
"""
|
|
24
|
+
Represents tidal data on original grid.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
filename : str
|
|
29
|
+
The path to the TPXO dataset.
|
|
30
|
+
var_names : List[str], optional
|
|
31
|
+
List of variable names that are required in the dataset. Defaults to
|
|
32
|
+
["h_Re", "h_Im", "sal_Re", "sal_Im", "u_Re", "u_Im", "v_Re", "v_Im"].
|
|
33
|
+
dim_names: Dict[str, str], optional
|
|
34
|
+
Dictionary specifying the names of dimensions in the dataset. Defaults to
|
|
35
|
+
{"longitude": "ny", "latitude": "nx"}.
|
|
36
|
+
|
|
37
|
+
Attributes
|
|
38
|
+
----------
|
|
39
|
+
ds : xr.Dataset
|
|
40
|
+
The xarray Dataset containing TPXO tidal model data.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
filename: str
|
|
44
|
+
var_names: List[str] = field(
|
|
45
|
+
default_factory=lambda: [
|
|
46
|
+
"h_Re",
|
|
47
|
+
"h_Im",
|
|
48
|
+
"sal_Re",
|
|
49
|
+
"sal_Im",
|
|
50
|
+
"u_Re",
|
|
51
|
+
"u_Im",
|
|
52
|
+
"v_Re",
|
|
53
|
+
"v_Im",
|
|
54
|
+
"depth",
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
dim_names: Dict[str, str] = field(
|
|
58
|
+
default_factory=lambda: {"longitude": "ny", "latitude": "nx", "ntides": "nc"}
|
|
59
|
+
)
|
|
60
|
+
ds: xr.Dataset = field(init=False, repr=False)
|
|
61
|
+
|
|
62
|
+
def __post_init__(self):
|
|
63
|
+
# Perform any necessary dataset initialization or modifications here
|
|
64
|
+
ds = super().load_data()
|
|
65
|
+
|
|
66
|
+
# Clean up dataset
|
|
67
|
+
ds = ds.assign_coords(
|
|
68
|
+
{
|
|
69
|
+
"omega": ds["omega"],
|
|
70
|
+
"nx": ds["lon_r"].isel(
|
|
71
|
+
ny=0
|
|
72
|
+
), # lon_r is constant along ny, i.e., is only a function of nx
|
|
73
|
+
"ny": ds["lat_r"].isel(
|
|
74
|
+
nx=0
|
|
75
|
+
), # lat_r is constant along nx, i.e., is only a function of ny
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
ds = ds.rename({"nx": "longitude", "ny": "latitude"})
|
|
79
|
+
|
|
80
|
+
object.__setattr__(
|
|
81
|
+
self,
|
|
82
|
+
"dim_names",
|
|
83
|
+
{
|
|
84
|
+
"latitude": "latitude",
|
|
85
|
+
"longitude": "longitude",
|
|
86
|
+
"ntides": self.dim_names["ntides"],
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
# Select relevant fields
|
|
90
|
+
ds = super().select_relevant_fields(ds)
|
|
91
|
+
|
|
92
|
+
# Check whether the data covers the entire globe
|
|
93
|
+
is_global = self.check_if_global(ds)
|
|
94
|
+
|
|
95
|
+
if is_global:
|
|
96
|
+
ds = self.concatenate_longitudes(ds)
|
|
97
|
+
|
|
98
|
+
object.__setattr__(self, "ds", ds)
|
|
99
|
+
|
|
100
|
+
def check_number_constituents(self, ntides: int):
|
|
101
|
+
"""
|
|
102
|
+
Checks if the number of constituents in the dataset is at least `ntides`.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
ntides : int
|
|
107
|
+
The required number of tidal constituents.
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
ValueError
|
|
112
|
+
If the number of constituents in the dataset is less than `ntides`.
|
|
113
|
+
"""
|
|
114
|
+
if len(self.ds[self.dim_names["ntides"]]) < ntides:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"The dataset contains fewer than {ntides} tidal constituents."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_corrected_tides(self, model_reference_date, allan_factor):
|
|
120
|
+
# Get equilibrium tides
|
|
121
|
+
tpc = compute_equilibrium_tide(self.ds["longitude"], self.ds["latitude"]).isel(
|
|
122
|
+
nc=self.ds.nc
|
|
123
|
+
)
|
|
124
|
+
# Correct for SAL
|
|
125
|
+
tsc = allan_factor * (self.ds["sal_Re"] + 1j * self.ds["sal_Im"])
|
|
126
|
+
tpc = tpc - tsc
|
|
127
|
+
|
|
128
|
+
# Elevations and transports
|
|
129
|
+
thc = self.ds["h_Re"] + 1j * self.ds["h_Im"]
|
|
130
|
+
tuc = self.ds["u_Re"] + 1j * self.ds["u_Im"]
|
|
131
|
+
tvc = self.ds["v_Re"] + 1j * self.ds["v_Im"]
|
|
132
|
+
|
|
133
|
+
# Apply correction for phases and amplitudes
|
|
134
|
+
pf, pu, aa = egbert_correction(model_reference_date)
|
|
135
|
+
pf = pf.isel(nc=self.ds.nc)
|
|
136
|
+
pu = pu.isel(nc=self.ds.nc)
|
|
137
|
+
aa = aa.isel(nc=self.ds.nc)
|
|
138
|
+
|
|
139
|
+
tpxo_reference_date = datetime(1992, 1, 1)
|
|
140
|
+
dt = (model_reference_date - tpxo_reference_date).days * 3600 * 24
|
|
141
|
+
|
|
142
|
+
thc = pf * thc * np.exp(1j * (self.ds["omega"] * dt + pu + aa))
|
|
143
|
+
tuc = pf * tuc * np.exp(1j * (self.ds["omega"] * dt + pu + aa))
|
|
144
|
+
tvc = pf * tvc * np.exp(1j * (self.ds["omega"] * dt + pu + aa))
|
|
145
|
+
tpc = pf * tpc * np.exp(1j * (self.ds["omega"] * dt + pu + aa))
|
|
146
|
+
|
|
147
|
+
tides = {
|
|
148
|
+
"ssh_Re": thc.real,
|
|
149
|
+
"ssh_Im": thc.imag,
|
|
150
|
+
"u_Re": tuc.real,
|
|
151
|
+
"u_Im": tuc.imag,
|
|
152
|
+
"v_Re": tvc.real,
|
|
153
|
+
"v_Im": tvc.imag,
|
|
154
|
+
"pot_Re": tpc.real,
|
|
155
|
+
"pot_Im": tpc.imag,
|
|
156
|
+
"omega": self.ds["omega"],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for k in tides.keys():
|
|
160
|
+
tides[k] = tides[k].rename({"nc": "ntides"})
|
|
161
|
+
|
|
162
|
+
return tides
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True, kw_only=True)
|
|
166
|
+
class TidalForcing:
|
|
167
|
+
"""
|
|
168
|
+
Represents tidal forcing data used in ocean modeling.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
grid : Grid
|
|
173
|
+
The grid object representing the ROMS grid associated with the tidal forcing data.
|
|
174
|
+
filename: str
|
|
175
|
+
The path to the native tidal dataset.
|
|
176
|
+
ntides : int, optional
|
|
177
|
+
Number of constituents to consider. Maximum number is 14. Default is 10.
|
|
178
|
+
model_reference_date : datetime, optional
|
|
179
|
+
The reference date for the ROMS simulation. Default is datetime(2000, 1, 1).
|
|
180
|
+
source : str, optional
|
|
181
|
+
The source of the tidal data. Default is "TPXO".
|
|
182
|
+
allan_factor : float, optional
|
|
183
|
+
The Allan factor used in tidal model computation. Default is 2.0.
|
|
184
|
+
|
|
185
|
+
Attributes
|
|
186
|
+
----------
|
|
187
|
+
ds : xr.Dataset
|
|
188
|
+
The xarray Dataset containing the tidal forcing data.
|
|
189
|
+
|
|
190
|
+
Examples
|
|
191
|
+
--------
|
|
192
|
+
>>> grid = Grid(...)
|
|
193
|
+
>>> tidal_forcing = TidalForcing(grid)
|
|
194
|
+
>>> print(tidal_forcing.ds)
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
grid: Grid
|
|
198
|
+
filename: str
|
|
199
|
+
ntides: int = 10
|
|
200
|
+
model_reference_date: datetime = datetime(2000, 1, 1)
|
|
201
|
+
source: str = "TPXO"
|
|
202
|
+
allan_factor: float = 2.0
|
|
203
|
+
ds: xr.Dataset = field(init=False, repr=False)
|
|
204
|
+
|
|
205
|
+
def __post_init__(self):
|
|
206
|
+
if self.source == "TPXO":
|
|
207
|
+
data = TPXO(filename=self.filename)
|
|
208
|
+
else:
|
|
209
|
+
raise ValueError('Only "TPXO" is a valid option for source.')
|
|
210
|
+
|
|
211
|
+
data.check_number_constituents(self.ntides)
|
|
212
|
+
# operate on longitudes between -180 and 180 unless ROMS domain lies at least 5 degrees in lontitude away from Greenwich meridian
|
|
213
|
+
lon = self.grid.ds.lon_rho
|
|
214
|
+
lat = self.grid.ds.lat_rho
|
|
215
|
+
angle = self.grid.ds.angle
|
|
216
|
+
|
|
217
|
+
lon = xr.where(lon > 180, lon - 360, lon)
|
|
218
|
+
straddle = True
|
|
219
|
+
if not self.grid.straddle and abs(lon).min() > 5:
|
|
220
|
+
lon = xr.where(lon < 0, lon + 360, lon)
|
|
221
|
+
straddle = False
|
|
222
|
+
|
|
223
|
+
# The following consists of two steps:
|
|
224
|
+
# Step 1: Choose subdomain of forcing data including safety margin for interpolation, and Step 2: Convert to the proper longitude range.
|
|
225
|
+
# We perform these two steps for two reasons:
|
|
226
|
+
# A) Since the horizontal dimensions consist of a single chunk, selecting a subdomain before interpolation is a lot more performant.
|
|
227
|
+
# B) Step 1 is necessary to avoid discontinuous longitudes that could be introduced by Step 2. Specifically, discontinuous longitudes
|
|
228
|
+
# can lead to artifacts in the interpolation process. Specifically, if there is a data gap if data is not global,
|
|
229
|
+
# discontinuous longitudes could result in values that appear to come from a distant location instead of producing NaNs.
|
|
230
|
+
# These NaNs are important as they can be identified and handled appropriately by the nan_check function.
|
|
231
|
+
data.choose_subdomain(
|
|
232
|
+
latitude_range=[lat.min().values, lat.max().values],
|
|
233
|
+
longitude_range=[lon.min().values, lon.max().values],
|
|
234
|
+
margin=2,
|
|
235
|
+
straddle=straddle,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
tides = data.get_corrected_tides(self.model_reference_date, self.allan_factor)
|
|
239
|
+
|
|
240
|
+
# select desired number of constituents
|
|
241
|
+
for k in tides.keys():
|
|
242
|
+
tides[k] = tides[k].isel(ntides=slice(None, self.ntides))
|
|
243
|
+
|
|
244
|
+
# interpolate onto desired grid
|
|
245
|
+
coords = {"latitude": lat, "longitude": lon}
|
|
246
|
+
mask = xr.where(data.ds.depth > 0, 1, 0)
|
|
247
|
+
|
|
248
|
+
varnames = [
|
|
249
|
+
"ssh_Re",
|
|
250
|
+
"ssh_Im",
|
|
251
|
+
"pot_Re",
|
|
252
|
+
"pot_Im",
|
|
253
|
+
"u_Re",
|
|
254
|
+
"u_Im",
|
|
255
|
+
"v_Re",
|
|
256
|
+
"v_Im",
|
|
257
|
+
]
|
|
258
|
+
data_vars = {}
|
|
259
|
+
|
|
260
|
+
for var in varnames:
|
|
261
|
+
data_vars[var] = fill_and_interpolate(
|
|
262
|
+
tides[var],
|
|
263
|
+
mask,
|
|
264
|
+
list(coords.keys()),
|
|
265
|
+
coords,
|
|
266
|
+
method="linear",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Rotate to grid orientation
|
|
270
|
+
u_Re = data_vars["u_Re"] * np.cos(angle) + data_vars["v_Re"] * np.sin(angle)
|
|
271
|
+
v_Re = data_vars["v_Re"] * np.cos(angle) - data_vars["u_Re"] * np.sin(angle)
|
|
272
|
+
u_Im = data_vars["u_Im"] * np.cos(angle) + data_vars["v_Im"] * np.sin(angle)
|
|
273
|
+
v_Im = data_vars["v_Im"] * np.cos(angle) - data_vars["u_Im"] * np.sin(angle)
|
|
274
|
+
|
|
275
|
+
# Convert to barotropic velocity
|
|
276
|
+
u_Re = u_Re / self.grid.ds.h
|
|
277
|
+
v_Re = v_Re / self.grid.ds.h
|
|
278
|
+
u_Im = u_Im / self.grid.ds.h
|
|
279
|
+
v_Im = v_Im / self.grid.ds.h
|
|
280
|
+
|
|
281
|
+
# Interpolate from rho- to velocity points
|
|
282
|
+
u_Re = interpolate_from_rho_to_u(u_Re)
|
|
283
|
+
v_Re = interpolate_from_rho_to_v(v_Re)
|
|
284
|
+
u_Im = interpolate_from_rho_to_u(u_Im)
|
|
285
|
+
v_Im = interpolate_from_rho_to_v(v_Im)
|
|
286
|
+
|
|
287
|
+
# save in new dataset
|
|
288
|
+
ds = xr.Dataset()
|
|
289
|
+
|
|
290
|
+
# ds["omega"] = tides["omega"]
|
|
291
|
+
|
|
292
|
+
ds["ssh_Re"] = data_vars["ssh_Re"].astype(np.float32)
|
|
293
|
+
ds["ssh_Im"] = data_vars["ssh_Im"].astype(np.float32)
|
|
294
|
+
ds["ssh_Re"].attrs["long_name"] = "Tidal elevation, real part"
|
|
295
|
+
ds["ssh_Im"].attrs["long_name"] = "Tidal elevation, complex part"
|
|
296
|
+
ds["ssh_Re"].attrs["units"] = "m"
|
|
297
|
+
ds["ssh_Im"].attrs["units"] = "m"
|
|
298
|
+
|
|
299
|
+
ds["pot_Re"] = data_vars["pot_Re"].astype(np.float32)
|
|
300
|
+
ds["pot_Im"] = data_vars["pot_Im"].astype(np.float32)
|
|
301
|
+
ds["pot_Re"].attrs["long_name"] = "Tidal potential, real part"
|
|
302
|
+
ds["pot_Im"].attrs["long_name"] = "Tidal potential, complex part"
|
|
303
|
+
ds["pot_Re"].attrs["units"] = "m"
|
|
304
|
+
ds["pot_Im"].attrs["units"] = "m"
|
|
305
|
+
|
|
306
|
+
ds["u_Re"] = u_Re.astype(np.float32)
|
|
307
|
+
ds["u_Im"] = u_Im.astype(np.float32)
|
|
308
|
+
ds["u_Re"].attrs["long_name"] = "Tidal velocity in x-direction, real part"
|
|
309
|
+
ds["u_Im"].attrs["long_name"] = "Tidal velocity in x-direction, complex part"
|
|
310
|
+
ds["u_Re"].attrs["units"] = "m/s"
|
|
311
|
+
ds["u_Im"].attrs["units"] = "m/s"
|
|
312
|
+
|
|
313
|
+
ds["v_Re"] = v_Re.astype(np.float32)
|
|
314
|
+
ds["v_Im"] = v_Im.astype(np.float32)
|
|
315
|
+
ds["v_Re"].attrs["long_name"] = "Tidal velocity in y-direction, real part"
|
|
316
|
+
ds["v_Im"].attrs["long_name"] = "Tidal velocity in y-direction, complex part"
|
|
317
|
+
ds["v_Re"].attrs["units"] = "m/s"
|
|
318
|
+
ds["v_Im"].attrs["units"] = "m/s"
|
|
319
|
+
|
|
320
|
+
ds.attrs["title"] = "ROMS tidal forcing created by ROMS-Tools"
|
|
321
|
+
# Include the version of roms-tools
|
|
322
|
+
try:
|
|
323
|
+
roms_tools_version = importlib.metadata.version("roms-tools")
|
|
324
|
+
except importlib.metadata.PackageNotFoundError:
|
|
325
|
+
roms_tools_version = "unknown"
|
|
326
|
+
|
|
327
|
+
ds.attrs["roms_tools_version"] = roms_tools_version
|
|
328
|
+
|
|
329
|
+
ds.attrs["source"] = self.source
|
|
330
|
+
ds.attrs["model_reference_date"] = str(self.model_reference_date)
|
|
331
|
+
ds.attrs["allan_factor"] = self.allan_factor
|
|
332
|
+
|
|
333
|
+
object.__setattr__(self, "ds", ds)
|
|
334
|
+
|
|
335
|
+
for var in ["ssh_Re", "u_Re", "v_Im"]:
|
|
336
|
+
nan_check(self.ds[var].isel(ntides=0), self.grid.ds.mask_rho)
|
|
337
|
+
|
|
338
|
+
def plot(self, varname, ntides=0) -> None:
|
|
339
|
+
"""
|
|
340
|
+
Plot the specified tidal forcing variable for a given tidal constituent.
|
|
341
|
+
|
|
342
|
+
Parameters
|
|
343
|
+
----------
|
|
344
|
+
varname : str
|
|
345
|
+
The tidal forcing variable to plot. Options include:
|
|
346
|
+
- "ssh_Re": Real part of tidal elevation.
|
|
347
|
+
- "ssh_Im": Imaginary part of tidal elevation.
|
|
348
|
+
- "pot_Re": Real part of tidal potential.
|
|
349
|
+
- "pot_Im": Imaginary part of tidal potential.
|
|
350
|
+
- "u_Re": Real part of tidal velocity in the x-direction.
|
|
351
|
+
- "u_Im": Imaginary part of tidal velocity in the x-direction.
|
|
352
|
+
- "v_Re": Real part of tidal velocity in the y-direction.
|
|
353
|
+
- "v_Im": Imaginary part of tidal velocity in the y-direction.
|
|
354
|
+
ntides : int, optional
|
|
355
|
+
The index of the tidal constituent to plot. Default is 0, which corresponds
|
|
356
|
+
to the first constituent.
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
None
|
|
361
|
+
This method does not return any value. It generates and displays a plot.
|
|
362
|
+
|
|
363
|
+
Raises
|
|
364
|
+
------
|
|
365
|
+
ValueError
|
|
366
|
+
If the specified field is not one of the valid options.
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
Examples
|
|
370
|
+
--------
|
|
371
|
+
>>> tidal_forcing = TidalForcing(grid)
|
|
372
|
+
>>> tidal_forcing.plot("ssh_Re", nc=0)
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
field = self.ds[varname].isel(ntides=ntides).compute()
|
|
376
|
+
|
|
377
|
+
title = "%s, ntides = %i" % (field.long_name, self.ds[varname].ntides[ntides])
|
|
378
|
+
|
|
379
|
+
vmax = max(field.max(), -field.min())
|
|
380
|
+
vmin = -vmax
|
|
381
|
+
cmap = plt.colormaps.get_cmap("RdBu_r")
|
|
382
|
+
cmap.set_bad(color="gray")
|
|
383
|
+
|
|
384
|
+
kwargs = {"vmax": vmax, "vmin": vmin, "cmap": cmap}
|
|
385
|
+
|
|
386
|
+
_plot(
|
|
387
|
+
self.grid.ds,
|
|
388
|
+
field=field,
|
|
389
|
+
straddle=self.grid.straddle,
|
|
390
|
+
c="g",
|
|
391
|
+
kwargs=kwargs,
|
|
392
|
+
title=title,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def save(self, filepath: str) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Save the tidal forcing information to a netCDF4 file.
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
filepath
|
|
402
|
+
"""
|
|
403
|
+
self.ds.to_netcdf(filepath)
|
|
404
|
+
|
|
405
|
+
def to_yaml(self, filepath: str) -> None:
|
|
406
|
+
"""
|
|
407
|
+
Export the parameters of the class to a YAML file, including the version of roms-tools.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
filepath : str
|
|
412
|
+
The path to the YAML file where the parameters will be saved.
|
|
413
|
+
"""
|
|
414
|
+
grid_data = asdict(self.grid)
|
|
415
|
+
grid_data.pop("ds", None) # Exclude non-serializable fields
|
|
416
|
+
grid_data.pop("straddle", None)
|
|
417
|
+
|
|
418
|
+
# Include the version of roms-tools
|
|
419
|
+
try:
|
|
420
|
+
roms_tools_version = importlib.metadata.version("roms-tools")
|
|
421
|
+
except importlib.metadata.PackageNotFoundError:
|
|
422
|
+
roms_tools_version = "unknown"
|
|
423
|
+
|
|
424
|
+
# Create header
|
|
425
|
+
header = f"---\nroms_tools_version: {roms_tools_version}\n---\n"
|
|
426
|
+
|
|
427
|
+
# Extract grid data
|
|
428
|
+
grid_yaml_data = {"Grid": grid_data}
|
|
429
|
+
|
|
430
|
+
# Extract tidal forcing data
|
|
431
|
+
tidal_forcing_data = {
|
|
432
|
+
"TidalForcing": {
|
|
433
|
+
"filename": self.filename,
|
|
434
|
+
"ntides": self.ntides,
|
|
435
|
+
"model_reference_date": self.model_reference_date.isoformat(),
|
|
436
|
+
"source": self.source,
|
|
437
|
+
"allan_factor": self.allan_factor,
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# Combine both sections
|
|
442
|
+
yaml_data = {**grid_yaml_data, **tidal_forcing_data}
|
|
443
|
+
|
|
444
|
+
with open(filepath, "w") as file:
|
|
445
|
+
# Write header
|
|
446
|
+
file.write(header)
|
|
447
|
+
# Write YAML data
|
|
448
|
+
yaml.dump(yaml_data, file, default_flow_style=False)
|
|
449
|
+
|
|
450
|
+
@classmethod
|
|
451
|
+
def from_yaml(cls, filepath: str) -> "TidalForcing":
|
|
452
|
+
"""
|
|
453
|
+
Create an instance of the TidalForcing class from a YAML file.
|
|
454
|
+
|
|
455
|
+
Parameters
|
|
456
|
+
----------
|
|
457
|
+
filepath : str
|
|
458
|
+
The path to the YAML file from which the parameters will be read.
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
TidalForcing
|
|
463
|
+
An instance of the TidalForcing class.
|
|
464
|
+
"""
|
|
465
|
+
# Read the entire file content
|
|
466
|
+
with open(filepath, "r") as file:
|
|
467
|
+
file_content = file.read()
|
|
468
|
+
|
|
469
|
+
# Split the content into YAML documents
|
|
470
|
+
documents = list(yaml.safe_load_all(file_content))
|
|
471
|
+
|
|
472
|
+
tidal_forcing_data = None
|
|
473
|
+
|
|
474
|
+
# Process the YAML documents
|
|
475
|
+
for doc in documents:
|
|
476
|
+
if doc is None:
|
|
477
|
+
continue
|
|
478
|
+
if "TidalForcing" in doc:
|
|
479
|
+
tidal_forcing_data = doc["TidalForcing"]
|
|
480
|
+
break
|
|
481
|
+
|
|
482
|
+
if tidal_forcing_data is None:
|
|
483
|
+
raise ValueError("No TidalForcing configuration found in the YAML file.")
|
|
484
|
+
|
|
485
|
+
# Convert the model_reference_date from string to datetime
|
|
486
|
+
tidal_forcing_params = tidal_forcing_data
|
|
487
|
+
tidal_forcing_params["model_reference_date"] = datetime.fromisoformat(
|
|
488
|
+
tidal_forcing_params["model_reference_date"]
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Create Grid instance from the YAML file
|
|
492
|
+
grid = Grid.from_yaml(filepath)
|
|
493
|
+
|
|
494
|
+
# Create and return an instance of TidalForcing
|
|
495
|
+
return cls(grid=grid, **tidal_forcing_params)
|
|
9
496
|
|
|
10
497
|
|
|
11
498
|
def modified_julian_days(year, month, day, hour=0):
|
|
@@ -224,453 +711,99 @@ def egbert_correction(date):
|
|
|
224
711
|
return pf, pu, aa
|
|
225
712
|
|
|
226
713
|
|
|
227
|
-
|
|
228
|
-
class TPXO:
|
|
229
|
-
"""
|
|
230
|
-
Represents TPXO tidal atlas.
|
|
231
|
-
|
|
232
|
-
Parameters
|
|
233
|
-
----------
|
|
234
|
-
filename : str
|
|
235
|
-
The path to the TPXO dataset.
|
|
236
|
-
|
|
237
|
-
Attributes
|
|
238
|
-
----------
|
|
239
|
-
ds : xr.Dataset
|
|
240
|
-
The xarray Dataset containing TPXO tidal model data.
|
|
241
|
-
|
|
242
|
-
Notes
|
|
243
|
-
-----
|
|
244
|
-
This class provides a convenient interface to work with TPXO tidal atlas.
|
|
245
|
-
|
|
246
|
-
Examples
|
|
247
|
-
--------
|
|
248
|
-
>>> tpxo = TPXO()
|
|
249
|
-
>>> tpxo.load_data("tpxo_data.nc")
|
|
250
|
-
>>> print(tpxo.ds)
|
|
251
|
-
<xarray.Dataset>
|
|
252
|
-
Dimensions: ...
|
|
253
|
-
"""
|
|
254
|
-
|
|
255
|
-
filename: str
|
|
256
|
-
ds: xr.Dataset = field(init=False, repr=False)
|
|
257
|
-
|
|
258
|
-
def __post_init__(self):
|
|
259
|
-
ds = self.load_data(self.filename)
|
|
260
|
-
# Lon_r is constant along ny, i.e., is only a function of nx
|
|
261
|
-
ds["nx"] = ds["lon_r"].isel(ny=0)
|
|
262
|
-
# Lat_r is constant along nx, i.e., is only a function of ny
|
|
263
|
-
ds["ny"] = ds["lat_r"].isel(nx=0)
|
|
264
|
-
|
|
265
|
-
object.__setattr__(self, "ds", ds)
|
|
266
|
-
|
|
267
|
-
def get_corrected_tides(self, model_reference_date, allan_factor):
|
|
268
|
-
# Get equilibrium tides
|
|
269
|
-
tpc = self.compute_equilibrium_tide(self.ds["lon_r"], self.ds["lat_r"])
|
|
270
|
-
# Correct for SAL
|
|
271
|
-
tsc = allan_factor * (self.ds["sal_Re"] + 1j * self.ds["sal_Im"])
|
|
272
|
-
tpc = tpc - tsc
|
|
273
|
-
|
|
274
|
-
# Elevations and transports
|
|
275
|
-
thc = self.ds["h_Re"] + 1j * self.ds["h_Im"]
|
|
276
|
-
tuc = self.ds["u_Re"] + 1j * self.ds["u_Im"]
|
|
277
|
-
tvc = self.ds["v_Re"] + 1j * self.ds["v_Im"]
|
|
278
|
-
|
|
279
|
-
# Apply correction for phases and amplitudes
|
|
280
|
-
pf, pu, aa = egbert_correction(model_reference_date)
|
|
281
|
-
tpxo_reference_date = datetime(1992, 1, 1)
|
|
282
|
-
dt = (model_reference_date - tpxo_reference_date).days * 3600 * 24
|
|
283
|
-
|
|
284
|
-
thc = pf * thc * np.exp(1j * (self.ds["omega"] * dt + pu + aa))
|
|
285
|
-
tuc = pf * tuc * np.exp(1j * (self.ds["omega"] * dt + pu + aa))
|
|
286
|
-
tvc = pf * tvc * np.exp(1j * (self.ds["omega"] * dt + pu + aa))
|
|
287
|
-
tpc = pf * tpc * np.exp(1j * (self.ds["omega"] * dt + pu + aa))
|
|
288
|
-
|
|
289
|
-
tides = {"ssh": thc, "u": tuc, "v": tvc, "pot": tpc, "omega": self.ds["omega"]}
|
|
290
|
-
|
|
291
|
-
return tides
|
|
292
|
-
|
|
293
|
-
@staticmethod
|
|
294
|
-
def load_data(filename):
|
|
295
|
-
"""
|
|
296
|
-
Load tidal forcing data from the specified file.
|
|
297
|
-
|
|
298
|
-
Parameters
|
|
299
|
-
----------
|
|
300
|
-
filename : str
|
|
301
|
-
The path to the tidal dataset file.
|
|
302
|
-
|
|
303
|
-
Returns
|
|
304
|
-
-------
|
|
305
|
-
ds : xr.Dataset
|
|
306
|
-
The loaded xarray Dataset containing the tidal forcing data.
|
|
307
|
-
|
|
308
|
-
Raises
|
|
309
|
-
------
|
|
310
|
-
FileNotFoundError
|
|
311
|
-
If the specified file does not exist.
|
|
312
|
-
ValueError
|
|
313
|
-
If the file checksum does not match the expected value.
|
|
314
|
-
|
|
315
|
-
Notes
|
|
316
|
-
-----
|
|
317
|
-
This method performs basic file existence and checksum checks to ensure the integrity of the loaded dataset.
|
|
318
|
-
|
|
319
|
-
"""
|
|
320
|
-
# Check if the file exists
|
|
321
|
-
if not os.path.exists(filename):
|
|
322
|
-
raise FileNotFoundError(f"File '{filename}' not found.")
|
|
323
|
-
|
|
324
|
-
# Calculate the checksum of the file
|
|
325
|
-
expected_checksum = (
|
|
326
|
-
"306956d8769737ba39040118d8d08f467187fe453e02a5651305621d095bce6e"
|
|
327
|
-
)
|
|
328
|
-
with open(filename, "rb") as file:
|
|
329
|
-
actual_checksum = hashlib.sha256(file.read()).hexdigest()
|
|
330
|
-
|
|
331
|
-
# Compare the checksums
|
|
332
|
-
if actual_checksum != expected_checksum:
|
|
333
|
-
raise ValueError(
|
|
334
|
-
"Checksum mismatch. The file may be corrupted or tampered with."
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
# Load the dataset
|
|
338
|
-
ds = xr.open_dataset(filename)
|
|
339
|
-
|
|
340
|
-
return ds
|
|
341
|
-
|
|
342
|
-
@staticmethod
|
|
343
|
-
def compute_equilibrium_tide(lon, lat):
|
|
344
|
-
"""
|
|
345
|
-
Compute equilibrium tide for given longitudes and latitudes.
|
|
346
|
-
|
|
347
|
-
Parameters
|
|
348
|
-
----------
|
|
349
|
-
lon : xr.DataArray
|
|
350
|
-
Longitudes in degrees.
|
|
351
|
-
lat : xr.DataArray
|
|
352
|
-
Latitudes in degrees.
|
|
353
|
-
|
|
354
|
-
Returns
|
|
355
|
-
-------
|
|
356
|
-
tpc : xr.DataArray
|
|
357
|
-
Equilibrium tide complex amplitude.
|
|
358
|
-
|
|
359
|
-
Notes
|
|
360
|
-
-----
|
|
361
|
-
This method computes the equilibrium tide complex amplitude for given longitudes
|
|
362
|
-
and latitudes. It considers 15 tidal constituents and their corresponding
|
|
363
|
-
amplitudes and elasticity factors. The types of tides are classified as follows:
|
|
364
|
-
- 2: semidiurnal
|
|
365
|
-
- 1: diurnal
|
|
366
|
-
- 0: long-term
|
|
367
|
-
|
|
368
|
-
"""
|
|
369
|
-
|
|
370
|
-
# Amplitudes and elasticity factors for 15 tidal constituents
|
|
371
|
-
A = xr.DataArray(
|
|
372
|
-
data=np.array(
|
|
373
|
-
[
|
|
374
|
-
0.242334, # M2
|
|
375
|
-
0.112743, # S2
|
|
376
|
-
0.046397, # N2
|
|
377
|
-
0.030684, # K2
|
|
378
|
-
0.141565, # K1
|
|
379
|
-
0.100661, # O1
|
|
380
|
-
0.046848, # P1
|
|
381
|
-
0.019273, # Q1
|
|
382
|
-
0.042041, # Mf
|
|
383
|
-
0.022191, # Mm
|
|
384
|
-
0.0, # M4
|
|
385
|
-
0.0, # Mn4
|
|
386
|
-
0.0, # Ms4
|
|
387
|
-
0.006141, # 2n2
|
|
388
|
-
0.000764, # S1
|
|
389
|
-
]
|
|
390
|
-
),
|
|
391
|
-
dims="nc",
|
|
392
|
-
)
|
|
393
|
-
B = xr.DataArray(
|
|
394
|
-
data=np.array(
|
|
395
|
-
[
|
|
396
|
-
0.693, # M2
|
|
397
|
-
0.693, # S2
|
|
398
|
-
0.693, # N2
|
|
399
|
-
0.693, # K2
|
|
400
|
-
0.736, # K1
|
|
401
|
-
0.695, # O1
|
|
402
|
-
0.706, # P1
|
|
403
|
-
0.695, # Q1
|
|
404
|
-
0.693, # Mf
|
|
405
|
-
0.693, # Mm
|
|
406
|
-
0.693, # M4
|
|
407
|
-
0.693, # Mn4
|
|
408
|
-
0.693, # Ms4
|
|
409
|
-
0.693, # 2n2
|
|
410
|
-
0.693, # S1
|
|
411
|
-
]
|
|
412
|
-
),
|
|
413
|
-
dims="nc",
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
# types: 2 = semidiurnal, 1 = diurnal, 0 = long-term
|
|
417
|
-
ityp = xr.DataArray(
|
|
418
|
-
data=np.array([2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 2, 1]), dims="nc"
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
d2r = np.pi / 180
|
|
422
|
-
coslat2 = np.cos(d2r * lat) ** 2
|
|
423
|
-
sin2lat = np.sin(2 * d2r * lat)
|
|
424
|
-
|
|
425
|
-
p_amp = (
|
|
426
|
-
xr.where(ityp == 2, 1, 0) * A * B * coslat2 # semidiurnal
|
|
427
|
-
+ xr.where(ityp == 1, 1, 0) * A * B * sin2lat # diurnal
|
|
428
|
-
+ xr.where(ityp == 0, 1, 0) * A * B * (0.5 - 1.5 * coslat2) # long-term
|
|
429
|
-
)
|
|
430
|
-
p_pha = (
|
|
431
|
-
xr.where(ityp == 2, 1, 0) * (-2 * lon * d2r) # semidiurnal
|
|
432
|
-
+ xr.where(ityp == 1, 1, 0) * (-lon * d2r) # diurnal
|
|
433
|
-
+ xr.where(ityp == 0, 1, 0) * xr.zeros_like(lon) # long-term
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
tpc = p_amp * np.exp(-1j * p_pha)
|
|
437
|
-
|
|
438
|
-
return tpc
|
|
439
|
-
|
|
440
|
-
@staticmethod
|
|
441
|
-
def concatenate_across_dateline(field):
|
|
442
|
-
"""
|
|
443
|
-
Concatenate a field across the dateline for TPXO atlas.
|
|
444
|
-
|
|
445
|
-
Parameters
|
|
446
|
-
----------
|
|
447
|
-
field : xr.DataArray
|
|
448
|
-
The field to be concatenated across the dateline.
|
|
449
|
-
|
|
450
|
-
Returns
|
|
451
|
-
-------
|
|
452
|
-
field_concatenated : xr.DataArray
|
|
453
|
-
The field concatenated across the dateline.
|
|
454
|
-
|
|
455
|
-
Notes
|
|
456
|
-
-----
|
|
457
|
-
The TPXO atlas has a minimum longitude of 0.167 and a maximum longitude of 360.0.
|
|
458
|
-
This method concatenates the field along the dateline on the lower end, considering
|
|
459
|
-
the discontinuity in longitudes.
|
|
460
|
-
|
|
461
|
-
"""
|
|
462
|
-
lon = field["nx"]
|
|
463
|
-
lon_minus360 = lon - 360
|
|
464
|
-
lon_concatenated = xr.concat([lon_minus360, lon], dim="nx")
|
|
465
|
-
field_concatenated = xr.concat([field, field], dim="nx")
|
|
466
|
-
field_concatenated["nx"] = lon_concatenated
|
|
467
|
-
|
|
468
|
-
return field_concatenated
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
@dataclass(frozen=True, kw_only=True)
|
|
472
|
-
class TidalForcing:
|
|
714
|
+
def compute_equilibrium_tide(lon, lat):
|
|
473
715
|
"""
|
|
474
|
-
|
|
716
|
+
Compute equilibrium tide for given longitudes and latitudes.
|
|
475
717
|
|
|
476
718
|
Parameters
|
|
477
719
|
----------
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
nc : int, optional
|
|
483
|
-
Number of constituents to consider. Maximum number is 14. Default is 10.
|
|
484
|
-
model_reference_date : datetime, optional
|
|
485
|
-
The reference date for the ROMS simulation. Default is datetime(2000, 1, 1).
|
|
486
|
-
source : str, optional
|
|
487
|
-
The source of the tidal data. Default is "tpxo".
|
|
488
|
-
allan_factor : float, optional
|
|
489
|
-
The Allan factor used in tidal model computation. Default is 2.0.
|
|
720
|
+
lon : xr.DataArray
|
|
721
|
+
Longitudes in degrees.
|
|
722
|
+
lat : xr.DataArray
|
|
723
|
+
Latitudes in degrees.
|
|
490
724
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
725
|
+
Returns
|
|
726
|
+
-------
|
|
727
|
+
tpc : xr.DataArray
|
|
728
|
+
Equilibrium tide complex amplitude.
|
|
495
729
|
|
|
496
730
|
Notes
|
|
497
731
|
-----
|
|
498
|
-
This
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
732
|
+
This method computes the equilibrium tide complex amplitude for given longitudes
|
|
733
|
+
and latitudes. It considers 15 tidal constituents and their corresponding
|
|
734
|
+
amplitudes and elasticity factors. The types of tides are classified as follows:
|
|
735
|
+
- 2: semidiurnal
|
|
736
|
+
- 1: diurnal
|
|
737
|
+
- 0: long-term
|
|
502
738
|
|
|
503
|
-
Examples
|
|
504
|
-
--------
|
|
505
|
-
>>> grid = Grid(...)
|
|
506
|
-
>>> tidal_forcing = TidalForcing(grid)
|
|
507
|
-
>>> print(tidal_forcing.ds)
|
|
508
739
|
"""
|
|
509
740
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
.
|
|
539
|
-
.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
.
|
|
544
|
-
.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
.
|
|
549
|
-
.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
# Rotate to grid orientation
|
|
558
|
-
u_tide = u * np.cos(self.grid.ds.angle) + v * np.sin(self.grid.ds.angle)
|
|
559
|
-
v_tide = v * np.cos(self.grid.ds.angle) - u * np.sin(self.grid.ds.angle)
|
|
560
|
-
|
|
561
|
-
# Convert to barotropic velocity
|
|
562
|
-
u_tide = u_tide / self.grid.ds.h
|
|
563
|
-
v_tide = v_tide / self.grid.ds.h
|
|
564
|
-
|
|
565
|
-
# Interpolate from rho- to velocity points
|
|
566
|
-
u_tide = (
|
|
567
|
-
(u_tide + u_tide.shift(xi_rho=1))
|
|
568
|
-
.isel(xi_rho=slice(1, None))
|
|
569
|
-
.drop_vars(["lat_rho", "lon_rho"])
|
|
570
|
-
)
|
|
571
|
-
u_tide = u_tide.swap_dims({"xi_rho": "xi_u"})
|
|
572
|
-
v_tide = (
|
|
573
|
-
(v_tide + v_tide.shift(eta_rho=1))
|
|
574
|
-
.isel(eta_rho=slice(1, None))
|
|
575
|
-
.drop_vars(["lat_rho", "lon_rho"])
|
|
576
|
-
)
|
|
577
|
-
v_tide = v_tide.swap_dims({"eta_rho": "eta_v"})
|
|
578
|
-
|
|
579
|
-
# save in new dataset
|
|
580
|
-
ds = xr.Dataset()
|
|
581
|
-
|
|
582
|
-
ds["omega"] = tides["omega"]
|
|
583
|
-
|
|
584
|
-
ds["ssh_Re"] = ssh_tide.real
|
|
585
|
-
ds["ssh_Im"] = ssh_tide.imag
|
|
586
|
-
ds["ssh_Re"].attrs["long_name"] = "Tidal elevation, real part"
|
|
587
|
-
ds["ssh_Im"].attrs["long_name"] = "Tidal elevation, complex part"
|
|
588
|
-
ds["ssh_Re"].attrs["units"] = "m"
|
|
589
|
-
ds["ssh_Im"].attrs["units"] = "m"
|
|
590
|
-
|
|
591
|
-
ds["pot_Re"] = pot_tide.real
|
|
592
|
-
ds["pot_Im"] = pot_tide.imag
|
|
593
|
-
ds["pot_Re"].attrs["long_name"] = "Tidal potential, real part"
|
|
594
|
-
ds["pot_Im"].attrs["long_name"] = "Tidal potential, complex part"
|
|
595
|
-
ds["pot_Re"].attrs["units"] = "m"
|
|
596
|
-
ds["pot_Im"].attrs["units"] = "m"
|
|
597
|
-
|
|
598
|
-
ds["u_Re"] = u_tide.real
|
|
599
|
-
ds["u_Im"] = u_tide.imag
|
|
600
|
-
ds["u_Re"].attrs["long_name"] = "Tidal velocity in x-direction, real part"
|
|
601
|
-
ds["u_Im"].attrs["long_name"] = "Tidal velocity in x-direction, complex part"
|
|
602
|
-
ds["u_Re"].attrs["units"] = "m/s"
|
|
603
|
-
ds["u_Im"].attrs["units"] = "m/s"
|
|
604
|
-
|
|
605
|
-
ds["v_Re"] = v_tide.real
|
|
606
|
-
ds["v_Im"] = v_tide.imag
|
|
607
|
-
ds["v_Re"].attrs["long_name"] = "Tidal velocity in y-direction, real part"
|
|
608
|
-
ds["v_Im"].attrs["long_name"] = "Tidal velocity in y-direction, complex part"
|
|
609
|
-
ds["v_Re"].attrs["units"] = "m/s"
|
|
610
|
-
ds["v_Im"].attrs["units"] = "m/s"
|
|
611
|
-
|
|
612
|
-
ds.attrs["source"] = self.source
|
|
613
|
-
ds.attrs["model_reference_date"] = self.model_reference_date
|
|
614
|
-
ds.attrs["allan_factor"] = self.allan_factor
|
|
615
|
-
|
|
616
|
-
object.__setattr__(self, "ds", ds)
|
|
617
|
-
|
|
618
|
-
def plot(self, var, nc=0) -> None:
|
|
619
|
-
"""
|
|
620
|
-
Plot the specified tidal forcing variable for a given tidal constituent.
|
|
621
|
-
|
|
622
|
-
Parameters
|
|
623
|
-
----------
|
|
624
|
-
var : str
|
|
625
|
-
The tidal forcing variable to plot. Options include:
|
|
626
|
-
- "ssh_Re": Real part of tidal elevation.
|
|
627
|
-
- "ssh_Im": Imaginary part of tidal elevation.
|
|
628
|
-
- "pot_Re": Real part of tidal potential.
|
|
629
|
-
- "pot_Im": Imaginary part of tidal potential.
|
|
630
|
-
- "u_Re": Real part of tidal velocity in the x-direction.
|
|
631
|
-
- "u_Im": Imaginary part of tidal velocity in the x-direction.
|
|
632
|
-
- "v_Re": Real part of tidal velocity in the y-direction.
|
|
633
|
-
- "v_Im": Imaginary part of tidal velocity in the y-direction.
|
|
634
|
-
nc : int, optional
|
|
635
|
-
The index of the tidal constituent to plot. Default is 0, which corresponds
|
|
636
|
-
to the first constituent.
|
|
637
|
-
|
|
638
|
-
Returns
|
|
639
|
-
-------
|
|
640
|
-
None
|
|
641
|
-
This method does not return any value. It generates and displays a plot.
|
|
642
|
-
|
|
643
|
-
Raises
|
|
644
|
-
------
|
|
645
|
-
ValueError
|
|
646
|
-
If the specified field is not one of the valid options.
|
|
647
|
-
|
|
741
|
+
# Amplitudes and elasticity factors for 15 tidal constituents
|
|
742
|
+
A = xr.DataArray(
|
|
743
|
+
data=np.array(
|
|
744
|
+
[
|
|
745
|
+
0.242334, # M2
|
|
746
|
+
0.112743, # S2
|
|
747
|
+
0.046397, # N2
|
|
748
|
+
0.030684, # K2
|
|
749
|
+
0.141565, # K1
|
|
750
|
+
0.100661, # O1
|
|
751
|
+
0.046848, # P1
|
|
752
|
+
0.019273, # Q1
|
|
753
|
+
0.042041, # Mf
|
|
754
|
+
0.022191, # Mm
|
|
755
|
+
0.0, # M4
|
|
756
|
+
0.0, # Mn4
|
|
757
|
+
0.0, # Ms4
|
|
758
|
+
0.006141, # 2n2
|
|
759
|
+
0.000764, # S1
|
|
760
|
+
]
|
|
761
|
+
),
|
|
762
|
+
dims="nc",
|
|
763
|
+
)
|
|
764
|
+
B = xr.DataArray(
|
|
765
|
+
data=np.array(
|
|
766
|
+
[
|
|
767
|
+
0.693, # M2
|
|
768
|
+
0.693, # S2
|
|
769
|
+
0.693, # N2
|
|
770
|
+
0.693, # K2
|
|
771
|
+
0.736, # K1
|
|
772
|
+
0.695, # O1
|
|
773
|
+
0.706, # P1
|
|
774
|
+
0.695, # Q1
|
|
775
|
+
0.693, # Mf
|
|
776
|
+
0.693, # Mm
|
|
777
|
+
0.693, # M4
|
|
778
|
+
0.693, # Mn4
|
|
779
|
+
0.693, # Ms4
|
|
780
|
+
0.693, # 2n2
|
|
781
|
+
0.693, # S1
|
|
782
|
+
]
|
|
783
|
+
),
|
|
784
|
+
dims="nc",
|
|
785
|
+
)
|
|
648
786
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
"""
|
|
787
|
+
# types: 2 = semidiurnal, 1 = diurnal, 0 = long-term
|
|
788
|
+
ityp = xr.DataArray(
|
|
789
|
+
data=np.array([2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 2, 1]), dims="nc"
|
|
790
|
+
)
|
|
654
791
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
kwargs = {"cmap": "RdBu_r", "vmax": vmax, "vmin": -vmax}
|
|
792
|
+
d2r = np.pi / 180
|
|
793
|
+
coslat2 = np.cos(d2r * lat) ** 2
|
|
794
|
+
sin2lat = np.sin(2 * d2r * lat)
|
|
659
795
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
)
|
|
796
|
+
p_amp = (
|
|
797
|
+
xr.where(ityp == 2, 1, 0) * A * B * coslat2 # semidiurnal
|
|
798
|
+
+ xr.where(ityp == 1, 1, 0) * A * B * sin2lat # diurnal
|
|
799
|
+
+ xr.where(ityp == 0, 1, 0) * A * B * (0.5 - 1.5 * coslat2) # long-term
|
|
800
|
+
)
|
|
801
|
+
p_pha = (
|
|
802
|
+
xr.where(ityp == 2, 1, 0) * (-2 * lon * d2r) # semidiurnal
|
|
803
|
+
+ xr.where(ityp == 1, 1, 0) * (-lon * d2r) # diurnal
|
|
804
|
+
+ xr.where(ityp == 0, 1, 0) * xr.zeros_like(lon) # long-term
|
|
805
|
+
)
|
|
667
806
|
|
|
668
|
-
|
|
669
|
-
"""
|
|
670
|
-
Save the tidal forcing information to a netCDF4 file.
|
|
807
|
+
tpc = p_amp * np.exp(-1j * p_pha)
|
|
671
808
|
|
|
672
|
-
|
|
673
|
-
----------
|
|
674
|
-
filepath
|
|
675
|
-
"""
|
|
676
|
-
self.ds.to_netcdf(filepath)
|
|
809
|
+
return tpc
|