roms-tools 0.1.0__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.
- ci/environment.yml +2 -0
- roms_tools/__init__.py +4 -2
- roms_tools/_version.py +1 -1
- roms_tools/setup/boundary_forcing.py +757 -0
- roms_tools/setup/datasets.py +1141 -35
- roms_tools/setup/download.py +118 -0
- roms_tools/setup/fill.py +118 -5
- roms_tools/setup/grid.py +145 -19
- roms_tools/setup/initial_conditions.py +557 -0
- roms_tools/setup/mixins.py +395 -0
- roms_tools/setup/plot.py +149 -4
- roms_tools/setup/surface_forcing.py +596 -0
- roms_tools/setup/tides.py +472 -437
- roms_tools/setup/topography.py +18 -3
- roms_tools/setup/utils.py +352 -0
- roms_tools/setup/vertical_coordinate.py +494 -0
- roms_tools/tests/test_boundary_forcing.py +706 -0
- roms_tools/tests/test_datasets.py +370 -0
- roms_tools/tests/test_grid.py +226 -0
- roms_tools/tests/test_initial_conditions.py +520 -0
- roms_tools/tests/test_surface_forcing.py +2622 -0
- roms_tools/tests/test_tides.py +365 -0
- roms_tools/tests/test_topography.py +78 -0
- roms_tools/tests/test_utils.py +16 -0
- roms_tools/tests/test_vertical_coordinate.py +337 -0
- {roms_tools-0.1.0.dist-info → roms_tools-1.0.0.dist-info}/METADATA +9 -4
- roms_tools-1.0.0.dist-info/RECORD +31 -0
- {roms_tools-0.1.0.dist-info → roms_tools-1.0.0.dist-info}/WHEEL +1 -1
- roms_tools/setup/atmospheric_forcing.py +0 -993
- 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-1.0.0.dist-info}/LICENSE +0 -0
- {roms_tools-0.1.0.dist-info → roms_tools-1.0.0.dist-info}/top_level.txt +0 -0
roms_tools/setup/tides.py
CHANGED
|
@@ -1,11 +1,400 @@
|
|
|
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
|
+
from typing import Dict, Union
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field, asdict
|
|
5
9
|
from roms_tools.setup.grid import Grid
|
|
6
10
|
from roms_tools.setup.plot import _plot
|
|
7
|
-
import
|
|
8
|
-
import
|
|
11
|
+
from roms_tools.setup.fill import fill_and_interpolate
|
|
12
|
+
from roms_tools.setup.datasets import TPXODataset
|
|
13
|
+
from roms_tools.setup.utils import (
|
|
14
|
+
nan_check,
|
|
15
|
+
interpolate_from_rho_to_u,
|
|
16
|
+
interpolate_from_rho_to_v,
|
|
17
|
+
)
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, kw_only=True)
|
|
22
|
+
class TidalForcing:
|
|
23
|
+
"""
|
|
24
|
+
Represents tidal forcing data used in ocean modeling.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
grid : Grid
|
|
29
|
+
The grid object representing the ROMS grid associated with the tidal forcing data.
|
|
30
|
+
source : Dict[str, Union[str, None]]
|
|
31
|
+
Dictionary specifying the source of the tidal data:
|
|
32
|
+
- "name" (str): Name of the data source (e.g., "TPXO").
|
|
33
|
+
- "path" (str): Path to the tidal data file. Can contain wildcards.
|
|
34
|
+
ntides : int, optional
|
|
35
|
+
Number of constituents to consider. Maximum number is 14. Default is 10.
|
|
36
|
+
allan_factor : float, optional
|
|
37
|
+
The Allan factor used in tidal model computation. Default is 2.0.
|
|
38
|
+
model_reference_date : datetime, optional
|
|
39
|
+
The reference date for the ROMS simulation. Default is datetime(2000, 1, 1).
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
ds : xr.Dataset
|
|
44
|
+
The xarray Dataset containing the tidal forcing data.
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
>>> tidal_forcing = TidalForcing(
|
|
49
|
+
... grid=grid, source={"name": "TPXO", "path": "tpxo_data.nc"}
|
|
50
|
+
... )
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
grid: Grid
|
|
54
|
+
source: Dict[str, Union[str, None]]
|
|
55
|
+
ntides: int = 10
|
|
56
|
+
allan_factor: float = 2.0
|
|
57
|
+
model_reference_date: datetime = datetime(2000, 1, 1)
|
|
58
|
+
|
|
59
|
+
ds: xr.Dataset = field(init=False, repr=False)
|
|
60
|
+
|
|
61
|
+
def __post_init__(self):
|
|
62
|
+
if "name" not in self.source.keys():
|
|
63
|
+
raise ValueError("`source` must include a 'name'.")
|
|
64
|
+
if "path" not in self.source.keys():
|
|
65
|
+
raise ValueError("`source` must include a 'path'.")
|
|
66
|
+
if self.source["name"] == "TPXO":
|
|
67
|
+
data = TPXODataset(filename=self.source["path"])
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError('Only "TPXO" is a valid option for source["name"].')
|
|
70
|
+
|
|
71
|
+
data.check_number_constituents(self.ntides)
|
|
72
|
+
# operate on longitudes between -180 and 180 unless ROMS domain lies at least 5 degrees in longitude away from Greenwich meridian
|
|
73
|
+
lon = self.grid.ds.lon_rho
|
|
74
|
+
lat = self.grid.ds.lat_rho
|
|
75
|
+
angle = self.grid.ds.angle
|
|
76
|
+
|
|
77
|
+
lon = xr.where(lon > 180, lon - 360, lon)
|
|
78
|
+
straddle = True
|
|
79
|
+
if not self.grid.straddle and abs(lon).min() > 5:
|
|
80
|
+
lon = xr.where(lon < 0, lon + 360, lon)
|
|
81
|
+
straddle = False
|
|
82
|
+
|
|
83
|
+
# Restrict data to relevant subdomain to achieve better performance and to avoid discontinuous longitudes introduced by converting
|
|
84
|
+
# to a different longitude range (+- 360 degrees). Discontinues longitudes can lead to artifacts in the interpolation process that
|
|
85
|
+
# would not be detected by the nan_check function.
|
|
86
|
+
data.choose_subdomain(
|
|
87
|
+
latitude_range=[lat.min().values, lat.max().values],
|
|
88
|
+
longitude_range=[lon.min().values, lon.max().values],
|
|
89
|
+
margin=2,
|
|
90
|
+
straddle=straddle,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
tides = self._get_corrected_tides(data)
|
|
94
|
+
|
|
95
|
+
# select desired number of constituents
|
|
96
|
+
for k in tides.keys():
|
|
97
|
+
tides[k] = tides[k].isel(ntides=slice(None, self.ntides))
|
|
98
|
+
|
|
99
|
+
# interpolate onto desired grid
|
|
100
|
+
coords = {"latitude": lat, "longitude": lon}
|
|
101
|
+
mask = xr.where(data.ds.depth > 0, 1, 0)
|
|
102
|
+
|
|
103
|
+
varnames = [
|
|
104
|
+
"ssh_Re",
|
|
105
|
+
"ssh_Im",
|
|
106
|
+
"pot_Re",
|
|
107
|
+
"pot_Im",
|
|
108
|
+
"u_Re",
|
|
109
|
+
"u_Im",
|
|
110
|
+
"v_Re",
|
|
111
|
+
"v_Im",
|
|
112
|
+
]
|
|
113
|
+
data_vars = {}
|
|
114
|
+
|
|
115
|
+
for var in varnames:
|
|
116
|
+
data_vars[var] = fill_and_interpolate(
|
|
117
|
+
tides[var],
|
|
118
|
+
mask,
|
|
119
|
+
list(coords.keys()),
|
|
120
|
+
coords,
|
|
121
|
+
method="linear",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Rotate to grid orientation
|
|
125
|
+
u_Re = data_vars["u_Re"] * np.cos(angle) + data_vars["v_Re"] * np.sin(angle)
|
|
126
|
+
v_Re = data_vars["v_Re"] * np.cos(angle) - data_vars["u_Re"] * np.sin(angle)
|
|
127
|
+
u_Im = data_vars["u_Im"] * np.cos(angle) + data_vars["v_Im"] * np.sin(angle)
|
|
128
|
+
v_Im = data_vars["v_Im"] * np.cos(angle) - data_vars["u_Im"] * np.sin(angle)
|
|
129
|
+
|
|
130
|
+
# Convert to barotropic velocity
|
|
131
|
+
u_Re = u_Re / self.grid.ds.h
|
|
132
|
+
v_Re = v_Re / self.grid.ds.h
|
|
133
|
+
u_Im = u_Im / self.grid.ds.h
|
|
134
|
+
v_Im = v_Im / self.grid.ds.h
|
|
135
|
+
|
|
136
|
+
# Interpolate from rho- to velocity points
|
|
137
|
+
u_Re = interpolate_from_rho_to_u(u_Re)
|
|
138
|
+
v_Re = interpolate_from_rho_to_v(v_Re)
|
|
139
|
+
u_Im = interpolate_from_rho_to_u(u_Im)
|
|
140
|
+
v_Im = interpolate_from_rho_to_v(v_Im)
|
|
141
|
+
|
|
142
|
+
# save in new dataset
|
|
143
|
+
ds = xr.Dataset()
|
|
144
|
+
|
|
145
|
+
# ds["omega"] = tides["omega"]
|
|
146
|
+
|
|
147
|
+
ds["ssh_Re"] = data_vars["ssh_Re"].astype(np.float32)
|
|
148
|
+
ds["ssh_Im"] = data_vars["ssh_Im"].astype(np.float32)
|
|
149
|
+
ds["ssh_Re"].attrs["long_name"] = "Tidal elevation, real part"
|
|
150
|
+
ds["ssh_Im"].attrs["long_name"] = "Tidal elevation, complex part"
|
|
151
|
+
ds["ssh_Re"].attrs["units"] = "m"
|
|
152
|
+
ds["ssh_Im"].attrs["units"] = "m"
|
|
153
|
+
|
|
154
|
+
ds["pot_Re"] = data_vars["pot_Re"].astype(np.float32)
|
|
155
|
+
ds["pot_Im"] = data_vars["pot_Im"].astype(np.float32)
|
|
156
|
+
ds["pot_Re"].attrs["long_name"] = "Tidal potential, real part"
|
|
157
|
+
ds["pot_Im"].attrs["long_name"] = "Tidal potential, complex part"
|
|
158
|
+
ds["pot_Re"].attrs["units"] = "m"
|
|
159
|
+
ds["pot_Im"].attrs["units"] = "m"
|
|
160
|
+
|
|
161
|
+
ds["u_Re"] = u_Re.astype(np.float32)
|
|
162
|
+
ds["u_Im"] = u_Im.astype(np.float32)
|
|
163
|
+
ds["u_Re"].attrs["long_name"] = "Tidal velocity in x-direction, real part"
|
|
164
|
+
ds["u_Im"].attrs["long_name"] = "Tidal velocity in x-direction, complex part"
|
|
165
|
+
ds["u_Re"].attrs["units"] = "m/s"
|
|
166
|
+
ds["u_Im"].attrs["units"] = "m/s"
|
|
167
|
+
|
|
168
|
+
ds["v_Re"] = v_Re.astype(np.float32)
|
|
169
|
+
ds["v_Im"] = v_Im.astype(np.float32)
|
|
170
|
+
ds["v_Re"].attrs["long_name"] = "Tidal velocity in y-direction, real part"
|
|
171
|
+
ds["v_Im"].attrs["long_name"] = "Tidal velocity in y-direction, complex part"
|
|
172
|
+
ds["v_Re"].attrs["units"] = "m/s"
|
|
173
|
+
ds["v_Im"].attrs["units"] = "m/s"
|
|
174
|
+
|
|
175
|
+
ds.attrs["title"] = "ROMS tidal forcing created by ROMS-Tools"
|
|
176
|
+
# Include the version of roms-tools
|
|
177
|
+
try:
|
|
178
|
+
roms_tools_version = importlib.metadata.version("roms-tools")
|
|
179
|
+
except importlib.metadata.PackageNotFoundError:
|
|
180
|
+
roms_tools_version = "unknown"
|
|
181
|
+
|
|
182
|
+
ds.attrs["roms_tools_version"] = roms_tools_version
|
|
183
|
+
|
|
184
|
+
ds.attrs["source"] = self.source["name"]
|
|
185
|
+
ds.attrs["model_reference_date"] = str(self.model_reference_date)
|
|
186
|
+
ds.attrs["allan_factor"] = self.allan_factor
|
|
187
|
+
|
|
188
|
+
object.__setattr__(self, "ds", ds)
|
|
189
|
+
|
|
190
|
+
for var in ["ssh_Re", "u_Re", "v_Im"]:
|
|
191
|
+
nan_check(self.ds[var].isel(ntides=0), self.grid.ds.mask_rho)
|
|
192
|
+
|
|
193
|
+
def plot(self, varname, ntides=0) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Plot the specified tidal forcing variable for a given tidal constituent.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
varname : str
|
|
200
|
+
The tidal forcing variable to plot. Options include:
|
|
201
|
+
- "ssh_Re": Real part of tidal elevation.
|
|
202
|
+
- "ssh_Im": Imaginary part of tidal elevation.
|
|
203
|
+
- "pot_Re": Real part of tidal potential.
|
|
204
|
+
- "pot_Im": Imaginary part of tidal potential.
|
|
205
|
+
- "u_Re": Real part of tidal velocity in the x-direction.
|
|
206
|
+
- "u_Im": Imaginary part of tidal velocity in the x-direction.
|
|
207
|
+
- "v_Re": Real part of tidal velocity in the y-direction.
|
|
208
|
+
- "v_Im": Imaginary part of tidal velocity in the y-direction.
|
|
209
|
+
ntides : int, optional
|
|
210
|
+
The index of the tidal constituent to plot. Default is 0, which corresponds
|
|
211
|
+
to the first constituent.
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
None
|
|
216
|
+
This method does not return any value. It generates and displays a plot.
|
|
217
|
+
|
|
218
|
+
Raises
|
|
219
|
+
------
|
|
220
|
+
ValueError
|
|
221
|
+
If the specified field is not one of the valid options.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
Examples
|
|
225
|
+
--------
|
|
226
|
+
>>> tidal_forcing = TidalForcing(grid)
|
|
227
|
+
>>> tidal_forcing.plot("ssh_Re", nc=0)
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
field = self.ds[varname].isel(ntides=ntides).compute()
|
|
231
|
+
|
|
232
|
+
title = "%s, ntides = %i" % (field.long_name, self.ds[varname].ntides[ntides])
|
|
233
|
+
|
|
234
|
+
vmax = max(field.max(), -field.min())
|
|
235
|
+
vmin = -vmax
|
|
236
|
+
cmap = plt.colormaps.get_cmap("RdBu_r")
|
|
237
|
+
cmap.set_bad(color="gray")
|
|
238
|
+
|
|
239
|
+
kwargs = {"vmax": vmax, "vmin": vmin, "cmap": cmap}
|
|
240
|
+
|
|
241
|
+
_plot(
|
|
242
|
+
self.grid.ds,
|
|
243
|
+
field=field,
|
|
244
|
+
straddle=self.grid.straddle,
|
|
245
|
+
c="g",
|
|
246
|
+
kwargs=kwargs,
|
|
247
|
+
title=title,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def save(self, filepath: str) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Save the tidal forcing information to a netCDF4 file.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
filepath
|
|
257
|
+
"""
|
|
258
|
+
self.ds.to_netcdf(filepath)
|
|
259
|
+
|
|
260
|
+
def to_yaml(self, filepath: str) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Export the parameters of the class to a YAML file, including the version of roms-tools.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
filepath : str
|
|
267
|
+
The path to the YAML file where the parameters will be saved.
|
|
268
|
+
"""
|
|
269
|
+
grid_data = asdict(self.grid)
|
|
270
|
+
grid_data.pop("ds", None) # Exclude non-serializable fields
|
|
271
|
+
grid_data.pop("straddle", None)
|
|
272
|
+
|
|
273
|
+
# Include the version of roms-tools
|
|
274
|
+
try:
|
|
275
|
+
roms_tools_version = importlib.metadata.version("roms-tools")
|
|
276
|
+
except importlib.metadata.PackageNotFoundError:
|
|
277
|
+
roms_tools_version = "unknown"
|
|
278
|
+
|
|
279
|
+
# Create header
|
|
280
|
+
header = f"---\nroms_tools_version: {roms_tools_version}\n---\n"
|
|
281
|
+
|
|
282
|
+
# Extract grid data
|
|
283
|
+
grid_yaml_data = {"Grid": grid_data}
|
|
284
|
+
|
|
285
|
+
# Extract tidal forcing data
|
|
286
|
+
tidal_forcing_data = {
|
|
287
|
+
"TidalForcing": {
|
|
288
|
+
"source": self.source,
|
|
289
|
+
"ntides": self.ntides,
|
|
290
|
+
"model_reference_date": self.model_reference_date.isoformat(),
|
|
291
|
+
"allan_factor": self.allan_factor,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Combine both sections
|
|
296
|
+
yaml_data = {**grid_yaml_data, **tidal_forcing_data}
|
|
297
|
+
|
|
298
|
+
with open(filepath, "w") as file:
|
|
299
|
+
# Write header
|
|
300
|
+
file.write(header)
|
|
301
|
+
# Write YAML data
|
|
302
|
+
yaml.dump(yaml_data, file, default_flow_style=False)
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def from_yaml(cls, filepath: str) -> "TidalForcing":
|
|
306
|
+
"""
|
|
307
|
+
Create an instance of the TidalForcing class from a YAML file.
|
|
308
|
+
|
|
309
|
+
Parameters
|
|
310
|
+
----------
|
|
311
|
+
filepath : str
|
|
312
|
+
The path to the YAML file from which the parameters will be read.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
TidalForcing
|
|
317
|
+
An instance of the TidalForcing class.
|
|
318
|
+
"""
|
|
319
|
+
# Read the entire file content
|
|
320
|
+
with open(filepath, "r") as file:
|
|
321
|
+
file_content = file.read()
|
|
322
|
+
|
|
323
|
+
# Split the content into YAML documents
|
|
324
|
+
documents = list(yaml.safe_load_all(file_content))
|
|
325
|
+
|
|
326
|
+
tidal_forcing_data = None
|
|
327
|
+
|
|
328
|
+
# Process the YAML documents
|
|
329
|
+
for doc in documents:
|
|
330
|
+
if doc is None:
|
|
331
|
+
continue
|
|
332
|
+
if "TidalForcing" in doc:
|
|
333
|
+
tidal_forcing_data = doc["TidalForcing"]
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
if tidal_forcing_data is None:
|
|
337
|
+
raise ValueError("No TidalForcing configuration found in the YAML file.")
|
|
338
|
+
|
|
339
|
+
# Convert the model_reference_date from string to datetime
|
|
340
|
+
tidal_forcing_params = tidal_forcing_data
|
|
341
|
+
tidal_forcing_params["model_reference_date"] = datetime.fromisoformat(
|
|
342
|
+
tidal_forcing_params["model_reference_date"]
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Create Grid instance from the YAML file
|
|
346
|
+
grid = Grid.from_yaml(filepath)
|
|
347
|
+
|
|
348
|
+
# Create and return an instance of TidalForcing
|
|
349
|
+
return cls(grid=grid, **tidal_forcing_params)
|
|
350
|
+
|
|
351
|
+
def _get_corrected_tides(self, data):
|
|
352
|
+
|
|
353
|
+
# Get equilibrium tides
|
|
354
|
+
tpc = compute_equilibrium_tide(
|
|
355
|
+
data.ds[data.dim_names["longitude"]], data.ds[data.dim_names["latitude"]]
|
|
356
|
+
)
|
|
357
|
+
tpc = tpc.isel(**{data.dim_names["ntides"]: data.ds[data.dim_names["ntides"]]})
|
|
358
|
+
# Correct for SAL
|
|
359
|
+
tsc = self.allan_factor * (
|
|
360
|
+
data.ds[data.var_names["sal_Re"]] + 1j * data.ds[data.var_names["sal_Im"]]
|
|
361
|
+
)
|
|
362
|
+
tpc = tpc - tsc
|
|
363
|
+
|
|
364
|
+
# Elevations and transports
|
|
365
|
+
thc = data.ds[data.var_names["ssh_Re"]] + 1j * data.ds[data.var_names["ssh_Im"]]
|
|
366
|
+
tuc = data.ds[data.var_names["u_Re"]] + 1j * data.ds[data.var_names["u_Im"]]
|
|
367
|
+
tvc = data.ds[data.var_names["v_Re"]] + 1j * data.ds[data.var_names["v_Im"]]
|
|
368
|
+
|
|
369
|
+
# Apply correction for phases and amplitudes
|
|
370
|
+
pf, pu, aa = egbert_correction(self.model_reference_date)
|
|
371
|
+
pf = pf.isel(**{data.dim_names["ntides"]: data.ds[data.dim_names["ntides"]]})
|
|
372
|
+
pu = pu.isel(**{data.dim_names["ntides"]: data.ds[data.dim_names["ntides"]]})
|
|
373
|
+
aa = aa.isel(**{data.dim_names["ntides"]: data.ds[data.dim_names["ntides"]]})
|
|
374
|
+
|
|
375
|
+
dt = (self.model_reference_date - data.reference_date).days * 3600 * 24
|
|
376
|
+
|
|
377
|
+
thc = pf * thc * np.exp(1j * (data.ds["omega"] * dt + pu + aa))
|
|
378
|
+
tuc = pf * tuc * np.exp(1j * (data.ds["omega"] * dt + pu + aa))
|
|
379
|
+
tvc = pf * tvc * np.exp(1j * (data.ds["omega"] * dt + pu + aa))
|
|
380
|
+
tpc = pf * tpc * np.exp(1j * (data.ds["omega"] * dt + pu + aa))
|
|
381
|
+
|
|
382
|
+
tides = {
|
|
383
|
+
"ssh_Re": thc.real,
|
|
384
|
+
"ssh_Im": thc.imag,
|
|
385
|
+
"u_Re": tuc.real,
|
|
386
|
+
"u_Im": tuc.imag,
|
|
387
|
+
"v_Re": tvc.real,
|
|
388
|
+
"v_Im": tvc.imag,
|
|
389
|
+
"pot_Re": tpc.real,
|
|
390
|
+
"pot_Im": tpc.imag,
|
|
391
|
+
"omega": data.ds["omega"],
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
for k in tides.keys():
|
|
395
|
+
tides[k] = tides[k].rename({data.dim_names["ntides"]: "ntides"})
|
|
396
|
+
|
|
397
|
+
return tides
|
|
9
398
|
|
|
10
399
|
|
|
11
400
|
def modified_julian_days(year, month, day, hour=0):
|
|
@@ -224,453 +613,99 @@ def egbert_correction(date):
|
|
|
224
613
|
return pf, pu, aa
|
|
225
614
|
|
|
226
615
|
|
|
227
|
-
|
|
228
|
-
class TPXO:
|
|
616
|
+
def compute_equilibrium_tide(lon, lat):
|
|
229
617
|
"""
|
|
230
|
-
|
|
618
|
+
Compute equilibrium tide for given longitudes and latitudes.
|
|
231
619
|
|
|
232
620
|
Parameters
|
|
233
621
|
----------
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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.
|
|
622
|
+
lon : xr.DataArray
|
|
623
|
+
Longitudes in degrees.
|
|
624
|
+
lat : xr.DataArray
|
|
625
|
+
Latitudes in degrees.
|
|
297
626
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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:
|
|
473
|
-
"""
|
|
474
|
-
Represents tidal forcing data used in ocean modeling.
|
|
475
|
-
|
|
476
|
-
Parameters
|
|
477
|
-
----------
|
|
478
|
-
grid : Grid
|
|
479
|
-
The grid object representing the ROMS grid associated with the tidal forcing data.
|
|
480
|
-
filename: str
|
|
481
|
-
The path to the native tidal dataset.
|
|
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.
|
|
490
|
-
|
|
491
|
-
Attributes
|
|
492
|
-
----------
|
|
493
|
-
ds : xr.Dataset
|
|
494
|
-
The xarray Dataset containing the tidal forcing data.
|
|
627
|
+
Returns
|
|
628
|
+
-------
|
|
629
|
+
tpc : xr.DataArray
|
|
630
|
+
Equilibrium tide complex amplitude.
|
|
495
631
|
|
|
496
632
|
Notes
|
|
497
633
|
-----
|
|
498
|
-
This
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
634
|
+
This method computes the equilibrium tide complex amplitude for given longitudes
|
|
635
|
+
and latitudes. It considers 15 tidal constituents and their corresponding
|
|
636
|
+
amplitudes and elasticity factors. The types of tides are classified as follows:
|
|
637
|
+
- 2: semidiurnal
|
|
638
|
+
- 1: diurnal
|
|
639
|
+
- 0: long-term
|
|
502
640
|
|
|
503
|
-
Examples
|
|
504
|
-
--------
|
|
505
|
-
>>> grid = Grid(...)
|
|
506
|
-
>>> tidal_forcing = TidalForcing(grid)
|
|
507
|
-
>>> print(tidal_forcing.ds)
|
|
508
641
|
"""
|
|
509
642
|
|
|
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
|
-
|
|
643
|
+
# Amplitudes and elasticity factors for 15 tidal constituents
|
|
644
|
+
A = xr.DataArray(
|
|
645
|
+
data=np.array(
|
|
646
|
+
[
|
|
647
|
+
0.242334, # M2
|
|
648
|
+
0.112743, # S2
|
|
649
|
+
0.046397, # N2
|
|
650
|
+
0.030684, # K2
|
|
651
|
+
0.141565, # K1
|
|
652
|
+
0.100661, # O1
|
|
653
|
+
0.046848, # P1
|
|
654
|
+
0.019273, # Q1
|
|
655
|
+
0.042041, # Mf
|
|
656
|
+
0.022191, # Mm
|
|
657
|
+
0.0, # M4
|
|
658
|
+
0.0, # Mn4
|
|
659
|
+
0.0, # Ms4
|
|
660
|
+
0.006141, # 2n2
|
|
661
|
+
0.000764, # S1
|
|
662
|
+
]
|
|
663
|
+
),
|
|
664
|
+
dims="nc",
|
|
665
|
+
)
|
|
666
|
+
B = xr.DataArray(
|
|
667
|
+
data=np.array(
|
|
668
|
+
[
|
|
669
|
+
0.693, # M2
|
|
670
|
+
0.693, # S2
|
|
671
|
+
0.693, # N2
|
|
672
|
+
0.693, # K2
|
|
673
|
+
0.736, # K1
|
|
674
|
+
0.695, # O1
|
|
675
|
+
0.706, # P1
|
|
676
|
+
0.695, # Q1
|
|
677
|
+
0.693, # Mf
|
|
678
|
+
0.693, # Mm
|
|
679
|
+
0.693, # M4
|
|
680
|
+
0.693, # Mn4
|
|
681
|
+
0.693, # Ms4
|
|
682
|
+
0.693, # 2n2
|
|
683
|
+
0.693, # S1
|
|
684
|
+
]
|
|
685
|
+
),
|
|
686
|
+
dims="nc",
|
|
687
|
+
)
|
|
648
688
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
"""
|
|
689
|
+
# types: 2 = semidiurnal, 1 = diurnal, 0 = long-term
|
|
690
|
+
ityp = xr.DataArray(
|
|
691
|
+
data=np.array([2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 2, 1]), dims="nc"
|
|
692
|
+
)
|
|
654
693
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
kwargs = {"cmap": "RdBu_r", "vmax": vmax, "vmin": -vmax}
|
|
694
|
+
d2r = np.pi / 180
|
|
695
|
+
coslat2 = np.cos(d2r * lat) ** 2
|
|
696
|
+
sin2lat = np.sin(2 * d2r * lat)
|
|
659
697
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
)
|
|
698
|
+
p_amp = (
|
|
699
|
+
xr.where(ityp == 2, 1, 0) * A * B * coslat2 # semidiurnal
|
|
700
|
+
+ xr.where(ityp == 1, 1, 0) * A * B * sin2lat # diurnal
|
|
701
|
+
+ xr.where(ityp == 0, 1, 0) * A * B * (0.5 - 1.5 * coslat2) # long-term
|
|
702
|
+
)
|
|
703
|
+
p_pha = (
|
|
704
|
+
xr.where(ityp == 2, 1, 0) * (-2 * lon * d2r) # semidiurnal
|
|
705
|
+
+ xr.where(ityp == 1, 1, 0) * (-lon * d2r) # diurnal
|
|
706
|
+
+ xr.where(ityp == 0, 1, 0) * xr.zeros_like(lon) # long-term
|
|
707
|
+
)
|
|
667
708
|
|
|
668
|
-
|
|
669
|
-
"""
|
|
670
|
-
Save the tidal forcing information to a netCDF4 file.
|
|
709
|
+
tpc = p_amp * np.exp(-1j * p_pha)
|
|
671
710
|
|
|
672
|
-
|
|
673
|
-
----------
|
|
674
|
-
filepath
|
|
675
|
-
"""
|
|
676
|
-
self.ds.to_netcdf(filepath)
|
|
711
|
+
return tpc
|