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.
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
- from dataclasses import dataclass, field
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 os
8
- import hashlib
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
- @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:
714
+ def compute_equilibrium_tide(lon, lat):
473
715
  """
474
- Represents tidal forcing data used in ocean modeling.
716
+ Compute equilibrium tide for given longitudes and latitudes.
475
717
 
476
718
  Parameters
477
719
  ----------
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.
720
+ lon : xr.DataArray
721
+ Longitudes in degrees.
722
+ lat : xr.DataArray
723
+ Latitudes in degrees.
490
724
 
491
- Attributes
492
- ----------
493
- ds : xr.Dataset
494
- The xarray Dataset containing the tidal forcing data.
725
+ Returns
726
+ -------
727
+ tpc : xr.DataArray
728
+ Equilibrium tide complex amplitude.
495
729
 
496
730
  Notes
497
731
  -----
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.
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
- 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
-
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
- Examples
650
- --------
651
- >>> tidal_forcing = TidalForcing(grid)
652
- >>> tidal_forcing.plot("ssh_Re", nc=0)
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
- 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}
792
+ d2r = np.pi / 180
793
+ coslat2 = np.cos(d2r * lat) ** 2
794
+ sin2lat = np.sin(2 * d2r * lat)
659
795
 
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
- )
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
- def save(self, filepath: str) -> None:
669
- """
670
- Save the tidal forcing information to a netCDF4 file.
807
+ tpc = p_amp * np.exp(-1j * p_pha)
671
808
 
672
- Parameters
673
- ----------
674
- filepath
675
- """
676
- self.ds.to_netcdf(filepath)
809
+ return tpc