roms-tools 0.0.2__py3-none-any.whl → 0.1.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 +28 -0
- roms_tools/__init__.py +3 -0
- roms_tools/_version.py +1 -1
- roms_tools/setup/atmospheric_forcing.py +993 -0
- roms_tools/setup/datasets.py +48 -0
- roms_tools/setup/fill.py +263 -0
- roms_tools/setup/grid.py +483 -324
- roms_tools/setup/plot.py +58 -0
- roms_tools/setup/tides.py +676 -0
- roms_tools/setup/topography.py +242 -0
- roms_tools/tests/test_setup.py +145 -18
- roms_tools-0.1.0.dist-info/METADATA +89 -0
- roms_tools-0.1.0.dist-info/RECORD +17 -0
- {roms_tools-0.0.2.dist-info → roms_tools-0.1.0.dist-info}/WHEEL +1 -1
- {roms_tools-0.0.2.dist-info → roms_tools-0.1.0.dist-info}/top_level.txt +2 -0
- roms_tools/setup/old_grid_script.py +0 -438
- roms_tools-0.0.2.dist-info/METADATA +0 -134
- roms_tools-0.0.2.dist-info/RECORD +0 -11
- {roms_tools-0.0.2.dist-info → roms_tools-0.1.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import xarray as xr
|
|
3
|
+
import numpy as np
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from roms_tools.setup.grid import Grid
|
|
6
|
+
from roms_tools.setup.plot import _plot
|
|
7
|
+
import os
|
|
8
|
+
import hashlib
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def modified_julian_days(year, month, day, hour=0):
|
|
12
|
+
"""
|
|
13
|
+
Calculate the Modified Julian Day (MJD) for a given date and time.
|
|
14
|
+
|
|
15
|
+
The Modified Julian Day (MJD) is a modified Julian day count starting from
|
|
16
|
+
November 17, 1858 AD. It is commonly used in astronomy and geodesy.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
year : int
|
|
21
|
+
The year.
|
|
22
|
+
month : int
|
|
23
|
+
The month (1-12).
|
|
24
|
+
day : int
|
|
25
|
+
The day of the month.
|
|
26
|
+
hour : float, optional
|
|
27
|
+
The hour of the day as a fractional number (0 to 23.999...). Default is 0.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
mjd : float
|
|
32
|
+
The Modified Julian Day (MJD) corresponding to the input date and time.
|
|
33
|
+
|
|
34
|
+
Notes
|
|
35
|
+
-----
|
|
36
|
+
The algorithm assumes that the input date (year, month, day) is within the
|
|
37
|
+
Gregorian calendar, i.e., after October 15, 1582. Negative MJD values are
|
|
38
|
+
allowed for dates before November 17, 1858.
|
|
39
|
+
|
|
40
|
+
References
|
|
41
|
+
----------
|
|
42
|
+
- Wikipedia article on Julian Day: https://en.wikipedia.org/wiki/Julian_day
|
|
43
|
+
- Wikipedia article on Modified Julian Day: https://en.wikipedia.org/wiki/Modified_Julian_day
|
|
44
|
+
|
|
45
|
+
Examples
|
|
46
|
+
--------
|
|
47
|
+
>>> modified_julian_days(2024, 5, 20, 12)
|
|
48
|
+
58814.0
|
|
49
|
+
>>> modified_julian_days(1858, 11, 17)
|
|
50
|
+
0.0
|
|
51
|
+
>>> modified_julian_days(1582, 10, 4)
|
|
52
|
+
-141428.5
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if month < 3:
|
|
56
|
+
year -= 1
|
|
57
|
+
month += 12
|
|
58
|
+
|
|
59
|
+
A = year // 100
|
|
60
|
+
B = A // 4
|
|
61
|
+
C = 2 - A + B
|
|
62
|
+
E = int(365.25 * (year + 4716))
|
|
63
|
+
F = int(30.6001 * (month + 1))
|
|
64
|
+
jd = C + day + hour / 24 + E + F - 1524.5
|
|
65
|
+
mjd = jd - 2400000.5
|
|
66
|
+
|
|
67
|
+
return mjd
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def egbert_correction(date):
|
|
71
|
+
"""
|
|
72
|
+
Correct phases and amplitudes for real-time runs using parts of the
|
|
73
|
+
post-processing code from Egbert's & Erofeeva's (OSU) TPXO model.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
date : datetime.datetime
|
|
78
|
+
The date and time for which corrections are to be applied.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
pf : xr.DataArray
|
|
83
|
+
Amplitude scaling factor for each of the 15 tidal constituents.
|
|
84
|
+
pu : xr.DataArray
|
|
85
|
+
Phase correction [radians] for each of the 15 tidal constituents.
|
|
86
|
+
aa : xr.DataArray
|
|
87
|
+
Astronomical arguments [radians] associated with the corrections.
|
|
88
|
+
|
|
89
|
+
References
|
|
90
|
+
----------
|
|
91
|
+
- Egbert, G.D., and S.Y. Erofeeva. "Efficient inverse modeling of barotropic ocean
|
|
92
|
+
tides." Journal of Atmospheric and Oceanic Technology 19, no. 2 (2002): 183-204.
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
year = date.year
|
|
97
|
+
month = date.month
|
|
98
|
+
day = date.day
|
|
99
|
+
hour = date.hour
|
|
100
|
+
minute = date.minute
|
|
101
|
+
second = date.second
|
|
102
|
+
|
|
103
|
+
rad = np.pi / 180.0
|
|
104
|
+
deg = 180.0 / np.pi
|
|
105
|
+
mjd = modified_julian_days(year, month, day)
|
|
106
|
+
tstart = mjd + hour / 24 + minute / (60 * 24) + second / (60 * 60 * 24)
|
|
107
|
+
|
|
108
|
+
# Determine nodal corrections pu & pf : these expressions are valid for period 1990-2010 (Cartwright 1990).
|
|
109
|
+
# Reset time origin for astronomical arguments to 4th of May 1860:
|
|
110
|
+
timetemp = tstart - 51544.4993
|
|
111
|
+
|
|
112
|
+
# mean longitude of lunar perigee
|
|
113
|
+
P = 83.3535 + 0.11140353 * timetemp
|
|
114
|
+
P = np.mod(P, 360.0)
|
|
115
|
+
if P < 0:
|
|
116
|
+
P = +360
|
|
117
|
+
P *= rad
|
|
118
|
+
|
|
119
|
+
# mean longitude of ascending lunar node
|
|
120
|
+
N = 125.0445 - 0.05295377 * timetemp
|
|
121
|
+
N = np.mod(N, 360.0)
|
|
122
|
+
if N < 0:
|
|
123
|
+
N = +360
|
|
124
|
+
N *= rad
|
|
125
|
+
|
|
126
|
+
sinn = np.sin(N)
|
|
127
|
+
cosn = np.cos(N)
|
|
128
|
+
sin2n = np.sin(2 * N)
|
|
129
|
+
cos2n = np.cos(2 * N)
|
|
130
|
+
sin3n = np.sin(3 * N)
|
|
131
|
+
|
|
132
|
+
pftmp = np.sqrt(
|
|
133
|
+
(1 - 0.03731 * cosn + 0.00052 * cos2n) ** 2
|
|
134
|
+
+ (0.03731 * sinn - 0.00052 * sin2n) ** 2
|
|
135
|
+
) # 2N2
|
|
136
|
+
|
|
137
|
+
pf = np.zeros(15)
|
|
138
|
+
pf[0] = pftmp # M2
|
|
139
|
+
pf[1] = 1.0 # S2
|
|
140
|
+
pf[2] = pftmp # N2
|
|
141
|
+
pf[3] = np.sqrt(
|
|
142
|
+
(1 + 0.2852 * cosn + 0.0324 * cos2n) ** 2
|
|
143
|
+
+ (0.3108 * sinn + 0.0324 * sin2n) ** 2
|
|
144
|
+
) # K2
|
|
145
|
+
pf[4] = np.sqrt(
|
|
146
|
+
(1 + 0.1158 * cosn - 0.0029 * cos2n) ** 2
|
|
147
|
+
+ (0.1554 * sinn - 0.0029 * sin2n) ** 2
|
|
148
|
+
) # K1
|
|
149
|
+
pf[5] = np.sqrt(
|
|
150
|
+
(1 + 0.189 * cosn - 0.0058 * cos2n) ** 2 + (0.189 * sinn - 0.0058 * sin2n) ** 2
|
|
151
|
+
) # O1
|
|
152
|
+
pf[6] = 1.0 # P1
|
|
153
|
+
pf[7] = np.sqrt((1 + 0.188 * cosn) ** 2 + (0.188 * sinn) ** 2) # Q1
|
|
154
|
+
pf[8] = 1.043 + 0.414 * cosn # Mf
|
|
155
|
+
pf[9] = 1.0 - 0.130 * cosn # Mm
|
|
156
|
+
pf[10] = pftmp**2 # M4
|
|
157
|
+
pf[11] = pftmp**2 # Mn4
|
|
158
|
+
pf[12] = pftmp**2 # Ms4
|
|
159
|
+
pf[13] = pftmp # 2n2
|
|
160
|
+
pf[14] = 1.0 # S1
|
|
161
|
+
pf = xr.DataArray(pf, dims="nc")
|
|
162
|
+
|
|
163
|
+
putmp = (
|
|
164
|
+
np.arctan(
|
|
165
|
+
(-0.03731 * sinn + 0.00052 * sin2n)
|
|
166
|
+
/ (1.0 - 0.03731 * cosn + 0.00052 * cos2n)
|
|
167
|
+
)
|
|
168
|
+
* deg
|
|
169
|
+
) # 2N2
|
|
170
|
+
|
|
171
|
+
pu = np.zeros(15)
|
|
172
|
+
pu[0] = putmp # M2
|
|
173
|
+
pu[1] = 0.0 # S2
|
|
174
|
+
pu[2] = putmp # N2
|
|
175
|
+
pu[3] = (
|
|
176
|
+
np.arctan(
|
|
177
|
+
-(0.3108 * sinn + 0.0324 * sin2n) / (1.0 + 0.2852 * cosn + 0.0324 * cos2n)
|
|
178
|
+
)
|
|
179
|
+
* deg
|
|
180
|
+
) # K2
|
|
181
|
+
pu[4] = (
|
|
182
|
+
np.arctan(
|
|
183
|
+
(-0.1554 * sinn + 0.0029 * sin2n) / (1.0 + 0.1158 * cosn - 0.0029 * cos2n)
|
|
184
|
+
)
|
|
185
|
+
* deg
|
|
186
|
+
) # K1
|
|
187
|
+
pu[5] = 10.8 * sinn - 1.3 * sin2n + 0.2 * sin3n # O1
|
|
188
|
+
pu[6] = 0.0 # P1
|
|
189
|
+
pu[7] = np.arctan(0.189 * sinn / (1.0 + 0.189 * cosn)) * deg # Q1
|
|
190
|
+
pu[8] = -23.7 * sinn + 2.7 * sin2n - 0.4 * sin3n # Mf
|
|
191
|
+
pu[9] = 0.0 # Mm
|
|
192
|
+
pu[10] = putmp * 2.0 # M4
|
|
193
|
+
pu[11] = putmp * 2.0 # Mn4
|
|
194
|
+
pu[12] = putmp # Ms4
|
|
195
|
+
pu[13] = putmp # 2n2
|
|
196
|
+
pu[14] = 0.0 # S1
|
|
197
|
+
pu = xr.DataArray(pu, dims="nc")
|
|
198
|
+
# convert from degrees to radians
|
|
199
|
+
pu = pu * rad
|
|
200
|
+
|
|
201
|
+
aa = xr.DataArray(
|
|
202
|
+
data=np.array(
|
|
203
|
+
[
|
|
204
|
+
1.731557546, # M2
|
|
205
|
+
0.0, # S2
|
|
206
|
+
6.050721243, # N2
|
|
207
|
+
3.487600001, # K2
|
|
208
|
+
0.173003674, # K1
|
|
209
|
+
1.558553872, # O1
|
|
210
|
+
6.110181633, # P1
|
|
211
|
+
5.877717569, # Q1
|
|
212
|
+
1.964021610, # Mm
|
|
213
|
+
1.756042456, # Mf
|
|
214
|
+
3.463115091, # M4
|
|
215
|
+
1.499093481, # Mn4
|
|
216
|
+
1.731557546, # Ms4
|
|
217
|
+
4.086699633, # 2n2
|
|
218
|
+
0.0, # S1
|
|
219
|
+
]
|
|
220
|
+
),
|
|
221
|
+
dims="nc",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return pf, pu, aa
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True, kw_only=True)
|
|
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:
|
|
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.
|
|
495
|
+
|
|
496
|
+
Notes
|
|
497
|
+
-----
|
|
498
|
+
This class represents tidal forcing data used in ocean modeling. It provides
|
|
499
|
+
functionality to load and process tidal data for use in numerical simulations.
|
|
500
|
+
The tidal forcing data is loaded from a TPXO dataset and processed to generate
|
|
501
|
+
tidal elevation, tidal potential, and tidal velocity fields.
|
|
502
|
+
|
|
503
|
+
Examples
|
|
504
|
+
--------
|
|
505
|
+
>>> grid = Grid(...)
|
|
506
|
+
>>> tidal_forcing = TidalForcing(grid)
|
|
507
|
+
>>> print(tidal_forcing.ds)
|
|
508
|
+
"""
|
|
509
|
+
|
|
510
|
+
grid: Grid
|
|
511
|
+
filename: str
|
|
512
|
+
nc: int = 10
|
|
513
|
+
model_reference_date: datetime = datetime(2000, 1, 1)
|
|
514
|
+
source: str = "tpxo"
|
|
515
|
+
allan_factor: float = 2.0
|
|
516
|
+
ds: xr.Dataset = field(init=False, repr=False)
|
|
517
|
+
|
|
518
|
+
def __post_init__(self):
|
|
519
|
+
if self.source == "tpxo":
|
|
520
|
+
tpxo = TPXO(filename=self.filename)
|
|
521
|
+
|
|
522
|
+
tides = tpxo.get_corrected_tides(
|
|
523
|
+
self.model_reference_date, self.allan_factor
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# rename dimension and select desired number of constituents
|
|
527
|
+
for k in tides.keys():
|
|
528
|
+
tides[k] = tides[k].rename({"nc": "ntides"})
|
|
529
|
+
tides[k] = tides[k].isel(ntides=slice(None, self.nc))
|
|
530
|
+
|
|
531
|
+
# make sure interpolation works across dateline
|
|
532
|
+
for key in ["ssh", "pot", "u", "v"]:
|
|
533
|
+
tides[key] = tpxo.concatenate_across_dateline(tides[key])
|
|
534
|
+
|
|
535
|
+
# interpolate onto desired grid
|
|
536
|
+
ssh_tide = (
|
|
537
|
+
tides["ssh"]
|
|
538
|
+
.interp(nx=self.grid.ds.lon_rho, ny=self.grid.ds.lat_rho)
|
|
539
|
+
.drop_vars(["nx", "ny"])
|
|
540
|
+
)
|
|
541
|
+
pot_tide = (
|
|
542
|
+
tides["pot"]
|
|
543
|
+
.interp(nx=self.grid.ds.lon_rho, ny=self.grid.ds.lat_rho)
|
|
544
|
+
.drop_vars(["nx", "ny"])
|
|
545
|
+
)
|
|
546
|
+
u = (
|
|
547
|
+
tides["u"]
|
|
548
|
+
.interp(nx=self.grid.ds.lon_rho, ny=self.grid.ds.lat_rho)
|
|
549
|
+
.drop_vars(["nx", "ny"])
|
|
550
|
+
)
|
|
551
|
+
v = (
|
|
552
|
+
tides["v"]
|
|
553
|
+
.interp(nx=self.grid.ds.lon_rho, ny=self.grid.ds.lat_rho)
|
|
554
|
+
.drop_vars(["nx", "ny"])
|
|
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
|
+
|
|
648
|
+
|
|
649
|
+
Examples
|
|
650
|
+
--------
|
|
651
|
+
>>> tidal_forcing = TidalForcing(grid)
|
|
652
|
+
>>> tidal_forcing.plot("ssh_Re", nc=0)
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
vmax = max(
|
|
656
|
+
self.ds[var].isel(ntides=nc).max(), -self.ds[var].isel(ntides=nc).min()
|
|
657
|
+
)
|
|
658
|
+
kwargs = {"cmap": "RdBu_r", "vmax": vmax, "vmin": -vmax}
|
|
659
|
+
|
|
660
|
+
_plot(
|
|
661
|
+
self.ds,
|
|
662
|
+
field=self.ds[var].isel(ntides=nc),
|
|
663
|
+
straddle=self.grid.straddle,
|
|
664
|
+
c="g",
|
|
665
|
+
kwargs=kwargs,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
def save(self, filepath: str) -> None:
|
|
669
|
+
"""
|
|
670
|
+
Save the tidal forcing information to a netCDF4 file.
|
|
671
|
+
|
|
672
|
+
Parameters
|
|
673
|
+
----------
|
|
674
|
+
filepath
|
|
675
|
+
"""
|
|
676
|
+
self.ds.to_netcdf(filepath)
|