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.
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
- from dataclasses import dataclass, field
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 os
8
- import hashlib
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
- @dataclass(frozen=True, kw_only=True)
228
- class TPXO:
616
+ def compute_equilibrium_tide(lon, lat):
229
617
  """
230
- Represents TPXO tidal atlas.
618
+ Compute equilibrium tide for given longitudes and latitudes.
231
619
 
232
620
  Parameters
233
621
  ----------
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.
622
+ lon : xr.DataArray
623
+ Longitudes in degrees.
624
+ lat : xr.DataArray
625
+ Latitudes in degrees.
297
626
 
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.
627
+ Returns
628
+ -------
629
+ tpc : xr.DataArray
630
+ Equilibrium tide complex amplitude.
495
631
 
496
632
  Notes
497
633
  -----
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.
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
- 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
-
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
- Examples
650
- --------
651
- >>> tidal_forcing = TidalForcing(grid)
652
- >>> tidal_forcing.plot("ssh_Re", nc=0)
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
- 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}
694
+ d2r = np.pi / 180
695
+ coslat2 = np.cos(d2r * lat) ** 2
696
+ sin2lat = np.sin(2 * d2r * lat)
659
697
 
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
- )
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
- def save(self, filepath: str) -> None:
669
- """
670
- Save the tidal forcing information to a netCDF4 file.
709
+ tpc = p_amp * np.exp(-1j * p_pha)
671
710
 
672
- Parameters
673
- ----------
674
- filepath
675
- """
676
- self.ds.to_netcdf(filepath)
711
+ return tpc