roms-tools 0.0.6__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.
@@ -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)