roms-tools 2.6.2__py3-none-any.whl → 2.7.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.
Files changed (51) hide show
  1. roms_tools/__init__.py +1 -0
  2. roms_tools/analysis/roms_output.py +11 -77
  3. roms_tools/analysis/utils.py +0 -66
  4. roms_tools/constants.py +2 -0
  5. roms_tools/download.py +46 -3
  6. roms_tools/plot.py +22 -5
  7. roms_tools/setup/cdr_forcing.py +1126 -0
  8. roms_tools/setup/datasets.py +742 -87
  9. roms_tools/setup/grid.py +42 -4
  10. roms_tools/setup/river_forcing.py +11 -84
  11. roms_tools/setup/tides.py +81 -411
  12. roms_tools/setup/utils.py +241 -37
  13. roms_tools/tests/test_setup/test_cdr_forcing.py +772 -0
  14. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +53 -1
  15. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_tracer/.zattrs +1 -1
  16. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/.zarray +20 -0
  17. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/.zattrs +6 -0
  18. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/0 +0 -0
  19. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/.zarray +20 -0
  20. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/.zattrs +6 -0
  21. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/0 +0 -0
  22. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +53 -1
  23. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_tracer/.zattrs +1 -1
  24. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/.zarray +20 -0
  25. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/.zattrs +6 -0
  26. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/0 +0 -0
  27. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/.zarray +20 -0
  28. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/.zattrs +6 -0
  29. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/0 +0 -0
  30. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/.zattrs +1 -2
  31. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/.zmetadata +27 -5
  32. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/.zarray +20 -0
  33. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/.zattrs +5 -0
  34. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/0 +0 -0
  35. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/omega/.zattrs +1 -3
  36. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/pot_Im/0.0.0 +0 -0
  37. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/pot_Re/0.0.0 +0 -0
  38. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ssh_Im/0.0.0 +0 -0
  39. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ssh_Re/0.0.0 +0 -0
  40. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/u_Im/0.0.0 +0 -0
  41. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/u_Re/0.0.0 +0 -0
  42. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Im/0.0.0 +0 -0
  43. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Re/0.0.0 +0 -0
  44. roms_tools/tests/test_setup/test_datasets.py +103 -1
  45. roms_tools/tests/test_setup/test_tides.py +112 -47
  46. roms_tools/utils.py +115 -1
  47. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/METADATA +1 -1
  48. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/RECORD +51 -33
  49. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/WHEEL +1 -1
  50. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/licenses/LICENSE +0 -0
  51. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/top_level.txt +0 -0
roms_tools/setup/tides.py CHANGED
@@ -10,7 +10,7 @@ from roms_tools import Grid
10
10
  from roms_tools.plot import _plot
11
11
  from roms_tools.regrid import LateralRegridToROMS
12
12
  from roms_tools.utils import save_datasets
13
- from roms_tools.setup.datasets import TPXODataset
13
+ from roms_tools.setup.datasets import TPXOManager
14
14
  from roms_tools.setup.utils import (
15
15
  nan_check,
16
16
  substitute_nans_by_fillvalue,
@@ -33,20 +33,21 @@ class TidalForcing:
33
33
  ----------
34
34
  grid : Grid
35
35
  The grid object representing the ROMS grid associated with the tidal forcing data.
36
- source : Dict[str, Union[str, Path, List[Union[str, Path]]]]
36
+ source : Dict[str, Union[str, Path, Dict[str, Union[str, Path]]]]
37
37
  Dictionary specifying the source of the tidal data. Keys include:
38
38
 
39
39
  - "name" (str): Name of the data source (e.g., "TPXO").
40
- - "path" (Union[str, Path, List[Union[str, Path]]]): The path to the raw data file(s). This can be:
40
+ - "path" (Union[str, Path, Dict[str, Union[str, Path]]]):
41
41
 
42
- - A single string (with or without wildcards).
43
- - A single Path object.
44
- - A list of strings or Path objects containing multiple files.
42
+ - If a string or Path is provided, it represents a single file.
43
+ - If "name" is "TPXO", "path" can also be a dictionary with the following keys:
44
+
45
+ - "grid" (Union[str, Path]): Path to the TPXO grid file.
46
+ - "h" (Union[str, Path]): Path to the TPXO h-file.
47
+ - "u" (Union[str, Path]): Path to the TPXO u-file.
45
48
 
46
49
  ntides : int, optional
47
- Number of constituents to consider. Maximum number is 14. Default is 10.
48
- allan_factor : float, optional
49
- The Allan factor used in tidal model computation. Default is 2.0.
50
+ Number of constituents to consider. Maximum number is 15. Default is 10.
50
51
  model_reference_date : datetime, optional
51
52
  The reference date for the ROMS simulation. Default is datetime(2000, 1, 1).
52
53
  use_dask: bool, optional
@@ -58,15 +59,27 @@ class TidalForcing:
58
59
 
59
60
  Examples
60
61
  --------
62
+ Using a TPXO dataset with separate grid, h, and u files:
63
+
61
64
  >>> tidal_forcing = TidalForcing(
62
- ... grid=grid, source={"name": "TPXO", "path": "tpxo_data.nc"}
65
+ ... grid=grid,
66
+ ... source={
67
+ ... "name": "TPXO",
68
+ ... "path": {"grid": "tpxo_grid.nc", "h": "tpxo_h.nc", "u": "tpxo_u.nc"},
69
+ ... },
70
+ ... )
71
+
72
+ Using a single file as a source:
73
+
74
+ >>> tidal_forcing = TidalForcing(
75
+ ... grid=grid,
76
+ ... source={"name": "TPXO", "path": "tpxo_merged.nc"},
63
77
  ... )
64
78
  """
65
79
 
66
80
  grid: Grid
67
81
  source: Dict[str, Union[str, Path, List[Union[str, Path]]]]
68
82
  ntides: int = 10
69
- allan_factor: float = 2.0
70
83
  model_reference_date: datetime = datetime(2000, 1, 1)
71
84
  use_dask: bool = False
72
85
  bypass_validation: bool = False
@@ -78,32 +91,35 @@ class TidalForcing:
78
91
  self._input_checks()
79
92
  target_coords = get_target_coords(self.grid)
80
93
 
81
- data = self._get_data()
82
- data.check_number_constituents(self.ntides)
83
- data.choose_subdomain(
84
- target_coords,
85
- buffer_points=20,
86
- )
87
- # Enforce double precision to ensure reproducibility
88
- data.convert_to_float64()
94
+ tidal_data = self._get_data()
89
95
 
90
- # select desired number of constituents
91
- data.ds = data.ds.isel(ntides=slice(None, self.ntides))
92
- self._correct_tides(data)
96
+ for key, data in tidal_data.datasets.items():
97
+ if key != "omega":
98
+ data.choose_subdomain(
99
+ target_coords,
100
+ buffer_points=20,
101
+ )
102
+ # Enforce double precision to ensure reproducibility
103
+ data.convert_to_float64()
93
104
 
94
- data.apply_lateral_fill()
105
+ tidal_data.correct_tides(self.model_reference_date)
95
106
 
96
107
  self._set_variable_info()
97
108
  var_names = self.variable_info.keys()
98
109
 
99
110
  processed_fields = {}
100
- # lateral regridding
101
- lateral_regrid = LateralRegridToROMS(target_coords, data.dim_names)
102
- for var_name in var_names:
103
- if var_name in data.var_names.keys():
104
- processed_fields[var_name] = lateral_regrid.apply(
105
- data.ds[data.var_names[var_name]]
106
- )
111
+
112
+ # lateral fill and regridding
113
+ for key, data in tidal_data.datasets.items():
114
+ if key != "omega":
115
+ data.apply_lateral_fill()
116
+ lateral_regrid = LateralRegridToROMS(target_coords, data.dim_names)
117
+
118
+ for var_name in var_names:
119
+ if var_name in data.var_names.keys():
120
+ processed_fields[var_name] = lateral_regrid.apply(
121
+ data.ds[data.var_names[var_name]]
122
+ )
107
123
 
108
124
  # rotation of velocities and interpolation to u/v points
109
125
  vector_pairs = get_vector_pairs(self.variable_info)
@@ -133,13 +149,15 @@ class TidalForcing:
133
149
 
134
150
  d_meta = get_variable_metadata()
135
151
  ds = self._write_into_dataset(processed_fields, d_meta)
136
- ds["omega"] = data.ds["omega"]
137
152
 
138
153
  ds = self._add_global_metadata(ds)
139
154
 
140
155
  if not self.bypass_validation:
141
156
  self._validate(ds)
142
157
 
158
+ ds = ds.assign_coords({"omega": tidal_data.datasets["omega"]})
159
+ ds["ntides"].attrs["long_name"] = "constituent label"
160
+
143
161
  # substitute NaNs over land by a fill value to avoid blow-up of ROMS
144
162
  for var_name in ds.data_vars:
145
163
  ds[var_name] = substitute_nans_by_fillvalue(ds[var_name])
@@ -152,13 +170,40 @@ class TidalForcing:
152
170
  raise ValueError("`source` must include a 'name'.")
153
171
  if "path" not in self.source.keys():
154
172
  raise ValueError("`source` must include a 'path'.")
173
+ if self.ntides > 15:
174
+ raise ValueError("`ntides` must be at most 15.")
155
175
 
156
176
  def _get_data(self):
177
+ """Loads tidal forcing data based on the specified source."""
157
178
 
158
179
  if self.source["name"] == "TPXO":
159
- data = TPXODataset(filename=self.source["path"], use_dask=self.use_dask)
180
+ if isinstance(self.source["path"], dict):
181
+ fname_dict = {
182
+ "grid": self.source["path"]["grid"],
183
+ "h": self.source["path"]["h"],
184
+ "u": self.source["path"]["u"],
185
+ }
186
+
187
+ elif isinstance(self.source["path"], (str, Path)):
188
+ fname_dict = {
189
+ "grid": self.source["path"],
190
+ "h": self.source["path"],
191
+ "u": self.source["path"],
192
+ }
193
+ else:
194
+ raise ValueError(
195
+ 'For TPXO, source["path"] must be either a string, Path, or a dictionary with "grid", "h", and "u" keys.'
196
+ )
197
+
198
+ data = TPXOManager(
199
+ filenames=fname_dict,
200
+ ntides=self.ntides,
201
+ use_dask=self.use_dask,
202
+ )
203
+
160
204
  else:
161
205
  raise ValueError('Only "TPXO" is a valid option for source["name"].')
206
+
162
207
  return data
163
208
 
164
209
  def _set_variable_info(self):
@@ -168,7 +213,6 @@ class TidalForcing:
168
213
  - `location`: Where the variable resides in the grid (e.g., rho, u, or v points).
169
214
  - `is_vector`: Whether the variable is part of a vector (True for velocity components like 'u' and 'v').
170
215
  - `vector_pair`: For vector variables, this indicates the associated variable that forms the vector (e.g., 'u' and 'v').
171
- - `is_3d`: Indicates whether the variable is 3D (True for variables like 'temp' and 'salt') or 2D (False for 'zeta').
172
216
 
173
217
  Returns
174
218
  -------
@@ -179,7 +223,6 @@ class TidalForcing:
179
223
  "location": "rho",
180
224
  "is_vector": False,
181
225
  "vector_pair": None,
182
- "is_3d": False,
183
226
  }
184
227
 
185
228
  # Define a dictionary for variable names and their associated information
@@ -192,28 +235,24 @@ class TidalForcing:
192
235
  "location": "u",
193
236
  "is_vector": True,
194
237
  "vector_pair": "v_Re",
195
- "is_3d": False,
196
238
  "validate": True,
197
239
  },
198
240
  "v_Re": {
199
241
  "location": "v",
200
242
  "is_vector": True,
201
243
  "vector_pair": "u_Re",
202
- "is_3d": False,
203
244
  "validate": True,
204
245
  },
205
246
  "u_Im": {
206
247
  "location": "u",
207
248
  "is_vector": True,
208
249
  "vector_pair": "v_Im",
209
- "is_3d": False,
210
250
  "validate": False,
211
251
  },
212
252
  "v_Im": {
213
253
  "location": "v",
214
254
  "is_vector": True,
215
255
  "vector_pair": "u_Im",
216
- "is_3d": False,
217
256
  "validate": False,
218
257
  },
219
258
  }
@@ -247,7 +286,6 @@ class TidalForcing:
247
286
 
248
287
  ds.attrs["source"] = self.source["name"]
249
288
  ds.attrs["model_reference_date"] = str(self.model_reference_date)
250
- ds.attrs["allan_factor"] = self.allan_factor
251
289
 
252
290
  return ds
253
291
 
@@ -355,7 +393,10 @@ class TidalForcing:
355
393
  lon_deg = xr.where(lon_deg > 180, lon_deg - 360, lon_deg)
356
394
  field = field.assign_coords({"lon": lon_deg, "lat": lat_deg})
357
395
 
358
- title = "%s, ntides = %i" % (field.long_name, self.ds[var_name].ntides[ntides])
396
+ title = "%s, constituent: %s" % (
397
+ field.long_name,
398
+ self.ds[var_name].ntides[ntides].values.item().decode("utf-8"),
399
+ )
359
400
 
360
401
  vmax = max(field.max(), -field.min())
361
402
  vmin = -vmax
@@ -443,374 +484,3 @@ class TidalForcing:
443
484
  **tidal_forcing_params,
444
485
  use_dask=use_dask,
445
486
  )
446
-
447
- def _correct_tides(self, data):
448
- """Apply tidal corrections to the dataset. This method corrects the dataset for
449
- equilibrium tides, self-attraction and loading (SAL) effects, and adjusts phases
450
- and amplitudes of tidal elevations and transports using Egbert's correction.
451
-
452
- Parameters
453
- ----------
454
- data : Dataset
455
- The dataset containing tidal data, including variables for sea surface height (ssh), zonal and meridional
456
- currents (u, v), and self-attraction and loading corrections (sal).
457
- Returns
458
- -------
459
- None
460
- The dataset is modified in-place with corrected real and imaginary components for ssh, u, v, and the
461
- potential field ('pot_Re', 'pot_Im').
462
- """
463
-
464
- # Get equilibrium tides
465
- tpc = compute_equilibrium_tide(
466
- data.ds[data.dim_names["longitude"]], data.ds[data.dim_names["latitude"]]
467
- )
468
- tpc = tpc.isel(ntides=data.ds["ntides"])
469
- # Correct for SAL
470
- tsc = self.allan_factor * (
471
- data.ds[data.var_names["sal_Re"]] + 1j * data.ds[data.var_names["sal_Im"]]
472
- )
473
- tpc = tpc - tsc
474
-
475
- # Elevations and transports
476
- thc = data.ds[data.var_names["ssh_Re"]] + 1j * data.ds[data.var_names["ssh_Im"]]
477
- tuc = data.ds[data.var_names["u_Re"]] + 1j * data.ds[data.var_names["u_Im"]]
478
- tvc = data.ds[data.var_names["v_Re"]] + 1j * data.ds[data.var_names["v_Im"]]
479
-
480
- # Apply correction for phases and amplitudes
481
- pf, pu, aa = egbert_correction(self.model_reference_date)
482
- pf = pf.isel(ntides=data.ds["ntides"])
483
- pu = pu.isel(ntides=data.ds["ntides"])
484
- aa = aa.isel(ntides=data.ds["ntides"])
485
-
486
- dt = (self.model_reference_date - data.reference_date).days * 3600 * 24
487
-
488
- thc = pf * thc * np.exp(1j * (data.ds["omega"] * dt + pu + aa))
489
- tuc = pf * tuc * np.exp(1j * (data.ds["omega"] * dt + pu + aa))
490
- tvc = pf * tvc * np.exp(1j * (data.ds["omega"] * dt + pu + aa))
491
- tpc = pf * tpc * np.exp(1j * (data.ds["omega"] * dt + pu + aa))
492
-
493
- data.ds[data.var_names["ssh_Re"]] = thc.real
494
- data.ds[data.var_names["ssh_Im"]] = thc.imag
495
- data.ds[data.var_names["u_Re"]] = tuc.real
496
- data.ds[data.var_names["u_Im"]] = tuc.imag
497
- data.ds[data.var_names["v_Re"]] = tvc.real
498
- data.ds[data.var_names["v_Im"]] = tvc.imag
499
- data.ds["pot_Re"] = tpc.real
500
- data.ds["pot_Im"] = tpc.imag
501
-
502
- # Update var_names dictionary
503
- var_names = {**data.var_names, "pot_Re": "pot_Re", "pot_Im": "pot_Im"}
504
- var_names.pop("sal_Re", None) # Remove "sal_Re" if it exists
505
- var_names.pop("sal_Im", None) # Remove "sal_Im" if it exists
506
-
507
- data.var_names = var_names
508
-
509
-
510
- def modified_julian_days(year, month, day, hour=0):
511
- """Calculate the Modified Julian Day (MJD) for a given date and time.
512
-
513
- The Modified Julian Day (MJD) is a modified Julian day count starting from
514
- November 17, 1858 AD. It is commonly used in astronomy and geodesy.
515
-
516
- Parameters
517
- ----------
518
- year : int
519
- The year.
520
- month : int
521
- The month (1-12).
522
- day : int
523
- The day of the month.
524
- hour : float, optional
525
- The hour of the day as a fractional number (0 to 23.999...). Default is 0.
526
-
527
- Returns
528
- -------
529
- mjd : float
530
- The Modified Julian Day (MJD) corresponding to the input date and time.
531
-
532
- Notes
533
- -----
534
- The algorithm assumes that the input date (year, month, day) is within the
535
- Gregorian calendar, i.e., after October 15, 1582. Negative MJD values are
536
- allowed for dates before November 17, 1858.
537
-
538
- References
539
- ----------
540
- - Wikipedia article on Julian Day: https://en.wikipedia.org/wiki/Julian_day
541
- - Wikipedia article on Modified Julian Day: https://en.wikipedia.org/wiki/Modified_Julian_day
542
-
543
- Examples
544
- --------
545
- >>> modified_julian_days(2024, 5, 20, 12)
546
- 58814.0
547
- >>> modified_julian_days(1858, 11, 17)
548
- 0.0
549
- >>> modified_julian_days(1582, 10, 4)
550
- -141428.5
551
- """
552
-
553
- if month < 3:
554
- year -= 1
555
- month += 12
556
-
557
- A = year // 100
558
- B = A // 4
559
- C = 2 - A + B
560
- E = int(365.25 * (year + 4716))
561
- F = int(30.6001 * (month + 1))
562
- jd = C + day + hour / 24 + E + F - 1524.5
563
- mjd = jd - 2400000.5
564
-
565
- return mjd
566
-
567
-
568
- def egbert_correction(date):
569
- """Correct phases and amplitudes for real-time runs using parts of the post-
570
- processing code from Egbert's & Erofeeva's (OSU) TPXO model.
571
-
572
- Parameters
573
- ----------
574
- date : datetime.datetime
575
- The date and time for which corrections are to be applied.
576
-
577
- Returns
578
- -------
579
- pf : xr.DataArray
580
- Amplitude scaling factor for each of the 15 tidal constituents.
581
- pu : xr.DataArray
582
- Phase correction [radians] for each of the 15 tidal constituents.
583
- aa : xr.DataArray
584
- Astronomical arguments [radians] associated with the corrections.
585
-
586
- References
587
- ----------
588
- - Egbert, G.D., and S.Y. Erofeeva. "Efficient inverse modeling of barotropic ocean
589
- tides." Journal of Atmospheric and Oceanic Technology 19, no. 2 (2002): 183-204.
590
- """
591
-
592
- year = date.year
593
- month = date.month
594
- day = date.day
595
- hour = date.hour
596
- minute = date.minute
597
- second = date.second
598
-
599
- rad = np.pi / 180.0
600
- deg = 180.0 / np.pi
601
- mjd = modified_julian_days(year, month, day)
602
- tstart = mjd + hour / 24 + minute / (60 * 24) + second / (60 * 60 * 24)
603
-
604
- # Determine nodal corrections pu & pf : these expressions are valid for period 1990-2010 (Cartwright 1990).
605
- # Reset time origin for astronomical arguments to 4th of May 1860:
606
- timetemp = tstart - 51544.4993
607
-
608
- # mean longitude of lunar perigee
609
- P = 83.3535 + 0.11140353 * timetemp
610
- P = np.mod(P, 360.0)
611
- if P < 0:
612
- P = +360
613
- P *= rad
614
-
615
- # mean longitude of ascending lunar node
616
- N = 125.0445 - 0.05295377 * timetemp
617
- N = np.mod(N, 360.0)
618
- if N < 0:
619
- N = +360
620
- N *= rad
621
-
622
- sinn = np.sin(N)
623
- cosn = np.cos(N)
624
- sin2n = np.sin(2 * N)
625
- cos2n = np.cos(2 * N)
626
- sin3n = np.sin(3 * N)
627
-
628
- pftmp = np.sqrt(
629
- (1 - 0.03731 * cosn + 0.00052 * cos2n) ** 2
630
- + (0.03731 * sinn - 0.00052 * sin2n) ** 2
631
- ) # 2N2
632
-
633
- pf = np.zeros(15)
634
- pf[0] = pftmp # M2
635
- pf[1] = 1.0 # S2
636
- pf[2] = pftmp # N2
637
- pf[3] = np.sqrt(
638
- (1 + 0.2852 * cosn + 0.0324 * cos2n) ** 2
639
- + (0.3108 * sinn + 0.0324 * sin2n) ** 2
640
- ) # K2
641
- pf[4] = np.sqrt(
642
- (1 + 0.1158 * cosn - 0.0029 * cos2n) ** 2
643
- + (0.1554 * sinn - 0.0029 * sin2n) ** 2
644
- ) # K1
645
- pf[5] = np.sqrt(
646
- (1 + 0.189 * cosn - 0.0058 * cos2n) ** 2 + (0.189 * sinn - 0.0058 * sin2n) ** 2
647
- ) # O1
648
- pf[6] = 1.0 # P1
649
- pf[7] = np.sqrt((1 + 0.188 * cosn) ** 2 + (0.188 * sinn) ** 2) # Q1
650
- pf[8] = 1.043 + 0.414 * cosn # Mf
651
- pf[9] = 1.0 - 0.130 * cosn # Mm
652
- pf[10] = pftmp**2 # M4
653
- pf[11] = pftmp**2 # Mn4
654
- pf[12] = pftmp**2 # Ms4
655
- pf[13] = pftmp # 2n2
656
- pf[14] = 1.0 # S1
657
- pf = xr.DataArray(pf, dims="ntides")
658
-
659
- putmp = (
660
- np.arctan(
661
- (-0.03731 * sinn + 0.00052 * sin2n)
662
- / (1.0 - 0.03731 * cosn + 0.00052 * cos2n)
663
- )
664
- * deg
665
- ) # 2N2
666
-
667
- pu = np.zeros(15)
668
- pu[0] = putmp # M2
669
- pu[1] = 0.0 # S2
670
- pu[2] = putmp # N2
671
- pu[3] = (
672
- np.arctan(
673
- -(0.3108 * sinn + 0.0324 * sin2n) / (1.0 + 0.2852 * cosn + 0.0324 * cos2n)
674
- )
675
- * deg
676
- ) # K2
677
- pu[4] = (
678
- np.arctan(
679
- (-0.1554 * sinn + 0.0029 * sin2n) / (1.0 + 0.1158 * cosn - 0.0029 * cos2n)
680
- )
681
- * deg
682
- ) # K1
683
- pu[5] = 10.8 * sinn - 1.3 * sin2n + 0.2 * sin3n # O1
684
- pu[6] = 0.0 # P1
685
- pu[7] = np.arctan(0.189 * sinn / (1.0 + 0.189 * cosn)) * deg # Q1
686
- pu[8] = -23.7 * sinn + 2.7 * sin2n - 0.4 * sin3n # Mf
687
- pu[9] = 0.0 # Mm
688
- pu[10] = putmp * 2.0 # M4
689
- pu[11] = putmp * 2.0 # Mn4
690
- pu[12] = putmp # Ms4
691
- pu[13] = putmp # 2n2
692
- pu[14] = 0.0 # S1
693
- pu = xr.DataArray(pu, dims="ntides")
694
- # convert from degrees to radians
695
- pu = pu * rad
696
-
697
- aa = xr.DataArray(
698
- data=np.array(
699
- [
700
- 1.731557546, # M2
701
- 0.0, # S2
702
- 6.050721243, # N2
703
- 3.487600001, # K2
704
- 0.173003674, # K1
705
- 1.558553872, # O1
706
- 6.110181633, # P1
707
- 5.877717569, # Q1
708
- 1.964021610, # Mm
709
- 1.756042456, # Mf
710
- 3.463115091, # M4
711
- 1.499093481, # Mn4
712
- 1.731557546, # Ms4
713
- 4.086699633, # 2n2
714
- 0.0, # S1
715
- ]
716
- ),
717
- dims="ntides",
718
- )
719
-
720
- return pf, pu, aa
721
-
722
-
723
- def compute_equilibrium_tide(lon, lat):
724
- """Compute equilibrium tide for given longitudes and latitudes.
725
-
726
- Parameters
727
- ----------
728
- lon : xr.DataArray
729
- Longitudes in degrees.
730
- lat : xr.DataArray
731
- Latitudes in degrees.
732
-
733
- Returns
734
- -------
735
- tpc : xr.DataArray
736
- Equilibrium tide complex amplitude.
737
-
738
- Notes
739
- -----
740
- This method computes the equilibrium tide complex amplitude for given longitudes
741
- and latitudes. It considers 15 tidal constituents and their corresponding
742
- amplitudes and elasticity factors. The types of tides are classified as follows:
743
- - 2: semidiurnal
744
- - 1: diurnal
745
- - 0: long-term
746
- """
747
-
748
- # Amplitudes and elasticity factors for 15 tidal constituents
749
- A = xr.DataArray(
750
- data=np.array(
751
- [
752
- 0.242334, # M2
753
- 0.112743, # S2
754
- 0.046397, # N2
755
- 0.030684, # K2
756
- 0.141565, # K1
757
- 0.100661, # O1
758
- 0.046848, # P1
759
- 0.019273, # Q1
760
- 0.042041, # Mf
761
- 0.022191, # Mm
762
- 0.0, # M4
763
- 0.0, # Mn4
764
- 0.0, # Ms4
765
- 0.006141, # 2n2
766
- 0.000764, # S1
767
- ]
768
- ),
769
- dims="ntides",
770
- )
771
- B = xr.DataArray(
772
- data=np.array(
773
- [
774
- 0.693, # M2
775
- 0.693, # S2
776
- 0.693, # N2
777
- 0.693, # K2
778
- 0.736, # K1
779
- 0.695, # O1
780
- 0.706, # P1
781
- 0.695, # Q1
782
- 0.693, # Mf
783
- 0.693, # Mm
784
- 0.693, # M4
785
- 0.693, # Mn4
786
- 0.693, # Ms4
787
- 0.693, # 2n2
788
- 0.693, # S1
789
- ]
790
- ),
791
- dims="ntides",
792
- )
793
-
794
- # types: 2 = semidiurnal, 1 = diurnal, 0 = long-term
795
- ityp = xr.DataArray(
796
- data=np.array([2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 2, 1]), dims="ntides"
797
- )
798
-
799
- d2r = np.pi / 180
800
- coslat2 = np.cos(d2r * lat) ** 2
801
- sin2lat = np.sin(2 * d2r * lat)
802
-
803
- p_amp = (
804
- xr.where(ityp == 2, 1, 0) * A * B * coslat2 # semidiurnal
805
- + xr.where(ityp == 1, 1, 0) * A * B * sin2lat # diurnal
806
- + xr.where(ityp == 0, 1, 0) * A * B * (0.5 - 1.5 * coslat2) # long-term
807
- )
808
- p_pha = (
809
- xr.where(ityp == 2, 1, 0) * (-2 * lon * d2r) # semidiurnal
810
- + xr.where(ityp == 1, 1, 0) * (-lon * d2r) # diurnal
811
- + xr.where(ityp == 0, 1, 0) * xr.zeros_like(lon) # long-term
812
- )
813
-
814
- tpc = p_amp * np.exp(-1j * p_pha)
815
-
816
- return tpc