pyTMD 3.0.2__tar.gz → 3.0.4__tar.gz
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.
- {pytmd-3.0.2/pyTMD.egg-info → pytmd-3.0.4}/PKG-INFO +3 -3
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/compute.py +34 -15
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/reduce_otis.py +7 -7
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/ATLAS.py +15 -9
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/FES.py +9 -7
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/GOT.py +16 -5
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/OTIS.py +20 -15
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/dataset.py +70 -4
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/model.py +42 -10
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/math.py +84 -1
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/predict.py +512 -318
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/spatial.py +32 -1
- {pytmd-3.0.2 → pytmd-3.0.4/pyTMD.egg-info}/PKG-INFO +3 -3
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/requires.txt +2 -2
- {pytmd-3.0.2 → pytmd-3.0.4}/pyproject.toml +5 -6
- {pytmd-3.0.2 → pytmd-3.0.4}/LICENSE +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/MANIFEST.in +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/README.md +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/__init__.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/astro.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/constituents.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/ct1971_tab6.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/cte1973_tab.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/d1921_tab.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/database.json +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/doodson.json +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/hw1995_tab.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/re14_tab3.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/t1987_tab.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/tab5.2e.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/tab5.3a.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/tab5.3b.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/w1990_tab.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/__init__.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_arcticdata.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_aviso_fes.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_box_tpxo.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_gsfc_got.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_iers_opole.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_jpl_ssd.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_test_data.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/ellipse.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/interpolate.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/IERS.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/NOAA.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/__init__.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/solve/__init__.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/solve/constants.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/tools.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/utilities.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/version.py +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/SOURCES.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/dependency_links.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/entry_points.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/top_level.txt +0 -0
- {pytmd-3.0.2 → pytmd-3.0.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyTMD
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.4
|
|
4
4
|
Summary: Python-based tidal prediction software for estimating ocean, load, solid Earth and pole tides
|
|
5
5
|
Author: Tyler Sutterley
|
|
6
6
|
Author-email: tsutterl@uw.edu
|
|
@@ -55,10 +55,10 @@ Requires-Dist: pint
|
|
|
55
55
|
Requires-Dist: platformdirs
|
|
56
56
|
Requires-Dist: pyproj>=2.5.0
|
|
57
57
|
Requires-Dist: scipy>=1.10.1
|
|
58
|
-
Requires-Dist: timescale>=0.
|
|
58
|
+
Requires-Dist: timescale>=0.1.1
|
|
59
59
|
Requires-Dist: xarray
|
|
60
60
|
Provides-Extra: doc
|
|
61
|
-
Requires-Dist: docutils; extra == "doc"
|
|
61
|
+
Requires-Dist: docutils>=0.17; extra == "doc"
|
|
62
62
|
Requires-Dist: graphviz; extra == "doc"
|
|
63
63
|
Requires-Dist: ipympl; extra == "doc"
|
|
64
64
|
Requires-Dist: myst-nb; extra == "doc"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
compute.py
|
|
4
|
-
Written by Tyler Sutterley (
|
|
4
|
+
Written by Tyler Sutterley (02/2026)
|
|
5
5
|
Calculates tidal elevations for correcting elevation or imagery data
|
|
6
6
|
Calculates tidal currents at locations and times
|
|
7
7
|
|
|
@@ -64,6 +64,7 @@ PROGRAM DEPENDENCIES:
|
|
|
64
64
|
interpolate.py: interpolation routines for spatial data
|
|
65
65
|
|
|
66
66
|
UPDATE HISTORY:
|
|
67
|
+
Updated 02/2026: added attributes for constituents to output DataArrays
|
|
67
68
|
Updated 12/2025: use coords functions to convert x and y to DataArrays
|
|
68
69
|
no longer subclassing pathlib.Path for working directories
|
|
69
70
|
Updated 11/2025: use xarray DataArrays for input coordinates
|
|
@@ -321,8 +322,8 @@ def tide_elevations(
|
|
|
321
322
|
|
|
322
323
|
Returns
|
|
323
324
|
-------
|
|
324
|
-
|
|
325
|
-
|
|
325
|
+
tpred: xarray.DataArray
|
|
326
|
+
predicted tide elevation (meters)
|
|
326
327
|
"""
|
|
327
328
|
# default keyword arguments
|
|
328
329
|
kwargs.setdefault("chunks", None)
|
|
@@ -404,20 +405,28 @@ def tide_elevations(
|
|
|
404
405
|
X, Y, method=method, extrapolate=extrapolate, cutoff=cutoff
|
|
405
406
|
)
|
|
406
407
|
# calculate tide values for input data type
|
|
407
|
-
|
|
408
|
+
tpred = local.tmd.predict(
|
|
408
409
|
ts.tide, deltat=deltat, corrections=nodal_corrections
|
|
409
410
|
)
|
|
410
411
|
# calculate values for minor constituents by inference
|
|
411
412
|
if kwargs["infer_minor"]:
|
|
412
|
-
#
|
|
413
|
-
|
|
413
|
+
# infer minor constituents
|
|
414
|
+
tinfer = local.tmd.infer(
|
|
414
415
|
ts.tide,
|
|
415
416
|
deltat=deltat,
|
|
416
417
|
corrections=nodal_corrections,
|
|
417
418
|
minor=minor_constituents,
|
|
418
419
|
)
|
|
420
|
+
# add major and minor components
|
|
421
|
+
tpred += tinfer
|
|
422
|
+
# add attributes for inferred constituents
|
|
423
|
+
tpred.attrs["inferred"] = []
|
|
424
|
+
if hasattr(tinfer, "constituents"):
|
|
425
|
+
tpred.attrs["inferred"].extend(tinfer.constituents)
|
|
426
|
+
# add attributes
|
|
427
|
+
tpred.attrs["nodal_corrections"] = nodal_corrections
|
|
419
428
|
# return the ocean or load tide correction
|
|
420
|
-
return
|
|
429
|
+
return tpred
|
|
421
430
|
|
|
422
431
|
|
|
423
432
|
# PURPOSE: compute tides at points and times using tide model algorithms
|
|
@@ -508,8 +517,8 @@ def tide_currents(
|
|
|
508
517
|
|
|
509
518
|
Returns
|
|
510
519
|
-------
|
|
511
|
-
|
|
512
|
-
tidal currents in cm/s
|
|
520
|
+
tpred: xr.DataTree
|
|
521
|
+
predicted tidal currents in cm/s
|
|
513
522
|
|
|
514
523
|
u: xr.Dataset
|
|
515
524
|
zonal velocities
|
|
@@ -584,7 +593,7 @@ def tide_currents(
|
|
|
584
593
|
deltat = ts.tt_ut1
|
|
585
594
|
|
|
586
595
|
# python dictionary with tide model data
|
|
587
|
-
|
|
596
|
+
tpred = xr.DataTree()
|
|
588
597
|
# iterate over u and v currents
|
|
589
598
|
for key, ds in dtree.items():
|
|
590
599
|
# convert component to dataset
|
|
@@ -594,20 +603,28 @@ def tide_currents(
|
|
|
594
603
|
X, Y, method=method, extrapolate=extrapolate, cutoff=cutoff
|
|
595
604
|
)
|
|
596
605
|
# calculate tide values for input data type
|
|
597
|
-
|
|
606
|
+
tpred[key] = local.tmd.predict(
|
|
598
607
|
ts.tide, deltat=deltat, corrections=nodal_corrections
|
|
599
608
|
)
|
|
600
609
|
# calculate values for minor constituents by inference
|
|
601
610
|
if kwargs["infer_minor"]:
|
|
602
|
-
#
|
|
603
|
-
|
|
611
|
+
# infer minor constituents
|
|
612
|
+
tinfer = local.tmd.infer(
|
|
604
613
|
ts.tide,
|
|
605
614
|
deltat=deltat,
|
|
606
615
|
corrections=nodal_corrections,
|
|
607
616
|
minor=minor_constituents,
|
|
608
617
|
)
|
|
618
|
+
# add major and minor components
|
|
619
|
+
tpred[key] += tinfer
|
|
620
|
+
# add attributes for inferred constituents
|
|
621
|
+
tpred[key].attrs["inferred"] = []
|
|
622
|
+
if hasattr(tinfer, "constituents"):
|
|
623
|
+
tpred[key].attrs["inferred"].extend(tinfer.constituents)
|
|
624
|
+
# add attributes
|
|
625
|
+
tpred[key].attrs["nodal_corrections"] = nodal_corrections
|
|
609
626
|
# return the tidal currents
|
|
610
|
-
return
|
|
627
|
+
return tpred
|
|
611
628
|
|
|
612
629
|
|
|
613
630
|
# PURPOSE: check if points are within a tide model domain
|
|
@@ -1389,8 +1406,10 @@ def _catalog_SET(
|
|
|
1389
1406
|
longitude, latitude = pyTMD.io.dataset._coords(
|
|
1390
1407
|
x, y, type=type, source_crs=crs, target_crs=4326
|
|
1391
1408
|
)
|
|
1409
|
+
# geocentric latitude (degrees)
|
|
1410
|
+
latitude_geocentric = pyTMD.spatial.geocentric_latitude(latitude)
|
|
1392
1411
|
# create dataset
|
|
1393
|
-
ds = xr.Dataset(coords={"x": longitude, "y":
|
|
1412
|
+
ds = xr.Dataset(coords={"x": longitude, "y": latitude_geocentric})
|
|
1394
1413
|
|
|
1395
1414
|
# verify that delta time is an array
|
|
1396
1415
|
delta_time = np.atleast_1d(delta_time)
|
|
@@ -114,10 +114,10 @@ def reduce_otis(
|
|
|
114
114
|
m["u"].model_file
|
|
115
115
|
)
|
|
116
116
|
# combine local solutions with global solution
|
|
117
|
-
dsg = dsg.compact.combine_local(dtg)
|
|
118
|
-
dsz = dsz.compact.combine_local(dtz)
|
|
119
|
-
dsu = dsu.compact.combine_local(dtu)
|
|
120
|
-
dsv = dsv.compact.combine_local(dtv)
|
|
117
|
+
dsg = dsg.tmd.compact.combine_local(dtg)
|
|
118
|
+
dsz = dsz.tmd.compact.combine_local(dtz)
|
|
119
|
+
dsu = dsu.tmd.compact.combine_local(dtu)
|
|
120
|
+
dsv = dsv.tmd.compact.combine_local(dtv)
|
|
121
121
|
else:
|
|
122
122
|
# if reading a pure global solution
|
|
123
123
|
dsg = pyTMD.io.OTIS.open_otis_grid(m["z"].grid_file)
|
|
@@ -140,9 +140,9 @@ def reduce_otis(
|
|
|
140
140
|
new_elevation_file = _unique_filename(m["z"].model_file)
|
|
141
141
|
new_transport_file = _unique_filename(m["u"].model_file)
|
|
142
142
|
# output reduced datasets to file
|
|
143
|
-
dtree.otis.to_grid(new_grid_file)
|
|
144
|
-
dtree.otis.to_elevation(new_elevation_file)
|
|
145
|
-
dtree.otis.to_transport(new_transport_file)
|
|
143
|
+
dtree.tmd.otis.to_grid(new_grid_file)
|
|
144
|
+
dtree.tmd.otis.to_elevation(new_elevation_file)
|
|
145
|
+
dtree.tmd.otis.to_transport(new_transport_file)
|
|
146
146
|
# change the permissions level to mode
|
|
147
147
|
new_grid_file.chmod(mode=mode)
|
|
148
148
|
new_elevation_file.chmod(mode=mode)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
ATLAS.py
|
|
4
|
-
Written by Tyler Sutterley (
|
|
4
|
+
Written by Tyler Sutterley (02/2026)
|
|
5
5
|
|
|
6
6
|
Reads netCDF4 ATLAS tidal solutions provided by Oregon State University
|
|
7
7
|
|
|
@@ -10,6 +10,8 @@ PYTHON DEPENDENCIES:
|
|
|
10
10
|
https://docs.xarray.dev/en/stable/
|
|
11
11
|
|
|
12
12
|
UPDATE HISTORY:
|
|
13
|
+
Updated 02/2026: make dataset and datatree accessors for ATLAS
|
|
14
|
+
be subaccessors from dataset module
|
|
13
15
|
Updated 12/2025: no longer subclassing pathlib.Path for working directories
|
|
14
16
|
added option to change the output datatype when writing netCDF files
|
|
15
17
|
Updated 11/2025: near-complete rewrite of program to use xarray
|
|
@@ -63,6 +65,10 @@ import datetime
|
|
|
63
65
|
import xarray as xr
|
|
64
66
|
import pyTMD.version
|
|
65
67
|
import pyTMD.utilities
|
|
68
|
+
from .dataset import (
|
|
69
|
+
register_dataset_subaccessor,
|
|
70
|
+
register_datatree_subaccessor,
|
|
71
|
+
)
|
|
66
72
|
|
|
67
73
|
# attempt imports
|
|
68
74
|
dask = pyTMD.utilities.import_dependency("dask")
|
|
@@ -295,11 +301,10 @@ def open_atlas_dataset(
|
|
|
295
301
|
|
|
296
302
|
|
|
297
303
|
# PURPOSE: ATLAS-netcdf utilities for xarray Datasets
|
|
298
|
-
@
|
|
304
|
+
@register_dataset_subaccessor("atlas")
|
|
299
305
|
class ATLASDataset:
|
|
300
306
|
"""
|
|
301
|
-
|
|
302
|
-
tidal models
|
|
307
|
+
``xarray.Dataset`` utilities for ATLAS-netcdf tidal models
|
|
303
308
|
"""
|
|
304
309
|
|
|
305
310
|
def __init__(self, ds):
|
|
@@ -462,11 +467,10 @@ class ATLASDataset:
|
|
|
462
467
|
|
|
463
468
|
|
|
464
469
|
# PURPOSE: ATLAS-netcdf utilities for xarray DataTrees
|
|
465
|
-
@
|
|
470
|
+
@register_datatree_subaccessor("atlas")
|
|
466
471
|
class ATLASDataTree:
|
|
467
472
|
"""
|
|
468
|
-
|
|
469
|
-
tidal models
|
|
473
|
+
``xarray.DataTree`` utilities for ATLAS-netcdf tidal models
|
|
470
474
|
"""
|
|
471
475
|
|
|
472
476
|
def __init__(self, dtree):
|
|
@@ -500,6 +504,8 @@ class ATLASDataTree:
|
|
|
500
504
|
ds = self._dtree[group].to_dataset()
|
|
501
505
|
# write in append mode to add group to same grid and directory
|
|
502
506
|
# output grid file
|
|
503
|
-
ds.
|
|
507
|
+
ATLASDataset(ds).to_grid(grid_file, group=group, mode="a", **kwargs)
|
|
504
508
|
# output constituent files
|
|
505
|
-
ds.
|
|
509
|
+
ATLASDataset(ds).to_netcdf(
|
|
510
|
+
directory, group=group, mode="a", **kwargs
|
|
511
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
FES.py
|
|
4
|
-
Written by Tyler Sutterley (
|
|
4
|
+
Written by Tyler Sutterley (02/2026)
|
|
5
5
|
|
|
6
6
|
Reads ascii and netCDF4 files for FES tidal solutions provided by AVISO
|
|
7
7
|
https://www.aviso.altimetry.fr/data/products/auxiliary-products/
|
|
@@ -15,6 +15,7 @@ PYTHON DEPENDENCIES:
|
|
|
15
15
|
https://docs.xarray.dev/en/stable/
|
|
16
16
|
|
|
17
17
|
UPDATE HISTORY:
|
|
18
|
+
Updated 02/2026: make dataset accessor for FES be a subaccessor from dataset
|
|
18
19
|
Updated 12/2025: no longer subclassing pathlib.Path for working directories
|
|
19
20
|
Updated 11/2025: near-complete rewrite of program to use xarray
|
|
20
21
|
Updated 10/2025: simplify ascii read function to use masked_equal
|
|
@@ -71,6 +72,7 @@ import numpy as np
|
|
|
71
72
|
import xarray as xr
|
|
72
73
|
import pyTMD.constituents
|
|
73
74
|
import pyTMD.utilities
|
|
75
|
+
from .dataset import register_dataset_subaccessor
|
|
74
76
|
|
|
75
77
|
# attempt imports
|
|
76
78
|
dask = pyTMD.utilities.import_dependency("dask")
|
|
@@ -311,14 +313,14 @@ def open_fes_netcdf(
|
|
|
311
313
|
if "Ha" in tmp.variables:
|
|
312
314
|
# FES2012 variable names
|
|
313
315
|
mapping_coords = dict(lon="x", lat="y")
|
|
314
|
-
mapping_amp = dict(z="Ha"
|
|
315
|
-
mapping_ph = dict(z="Hg"
|
|
316
|
-
elif
|
|
316
|
+
mapping_amp = dict(z="Ha")
|
|
317
|
+
mapping_ph = dict(z="Hg")
|
|
318
|
+
elif any([v in tmp.variables for v in ["amplitude", "Ua", "Va"]]):
|
|
317
319
|
# FES2014/2022 variable names
|
|
318
320
|
mapping_coords = dict(lon="x", lat="y")
|
|
319
321
|
mapping_amp = dict(z="amplitude", u="Ua", v="Va")
|
|
320
322
|
mapping_ph = dict(z="phase", u="Ug", v="Vg")
|
|
321
|
-
elif
|
|
323
|
+
elif any([v in tmp.variables for v in ["AMPL", "UAMP", "VAMP"]]):
|
|
322
324
|
# HAMTIDE11 variable names
|
|
323
325
|
mapping_coords = dict(LON="x", LAT="y")
|
|
324
326
|
mapping_amp = dict(z="AMPL", u="UAMP", v="VAMP")
|
|
@@ -344,9 +346,9 @@ def open_fes_netcdf(
|
|
|
344
346
|
|
|
345
347
|
|
|
346
348
|
# PURPOSE: FES utilities for xarray Datasets
|
|
347
|
-
@
|
|
349
|
+
@register_dataset_subaccessor("fes")
|
|
348
350
|
class FESDataset:
|
|
349
|
-
"""
|
|
351
|
+
"""``xarray.Dataset`` utilities for FES tidal models"""
|
|
350
352
|
|
|
351
353
|
def __init__(self, ds):
|
|
352
354
|
self._ds = ds
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
GOT.py
|
|
4
|
-
Written by Tyler Sutterley (
|
|
4
|
+
Written by Tyler Sutterley (02/2026)
|
|
5
5
|
|
|
6
6
|
Reads ascii and netCDF4 files from Richard Ray's Goddard Ocean Tide (GOT) model
|
|
7
7
|
https://earth.gsfc.nasa.gov/geo/data/ocean-tide-models
|
|
@@ -14,6 +14,8 @@ PYTHON DEPENDENCIES:
|
|
|
14
14
|
https://docs.xarray.dev/en/stable/
|
|
15
15
|
|
|
16
16
|
UPDATE HISTORY:
|
|
17
|
+
Updated 02/2026: make dataset accessor for GOT be a subaccessor from dataset
|
|
18
|
+
some models have units in the second line of the header text
|
|
17
19
|
Updated 12/2025: no longer subclassing pathlib.Path for working directories
|
|
18
20
|
added function to write to output GOT-formatted ascii files
|
|
19
21
|
fixed writing of output constituents to match GOT attribute format
|
|
@@ -74,6 +76,7 @@ import xarray as xr
|
|
|
74
76
|
import pyTMD.version
|
|
75
77
|
import pyTMD.constituents
|
|
76
78
|
import pyTMD.utilities
|
|
79
|
+
from .dataset import register_dataset_subaccessor
|
|
77
80
|
|
|
78
81
|
# attempt imports
|
|
79
82
|
dask = pyTMD.utilities.import_dependency("dask")
|
|
@@ -208,8 +211,16 @@ def open_got_ascii(
|
|
|
208
211
|
# parse header text
|
|
209
212
|
# constituent identifier
|
|
210
213
|
cons = pyTMD.constituents._parse_name(file_contents[0])
|
|
211
|
-
# get units
|
|
212
|
-
|
|
214
|
+
# get units from header if available
|
|
215
|
+
rx = re.compile(r"\((\w+m)\)", re.IGNORECASE)
|
|
216
|
+
# GOT headers from Richard Ray have units on the first line
|
|
217
|
+
# some other models have units on the second line
|
|
218
|
+
if rx.search(file_contents[0]):
|
|
219
|
+
units = rx.findall(file_contents[0], re.IGNORECASE)
|
|
220
|
+
elif rx.search(file_contents[1]):
|
|
221
|
+
units = rx.findall(file_contents[1], re.IGNORECASE)
|
|
222
|
+
else:
|
|
223
|
+
units = None
|
|
213
224
|
# grid dimensions
|
|
214
225
|
nlat, nlon = np.array(file_contents[2].split(), dtype=int)
|
|
215
226
|
# longitude range
|
|
@@ -331,9 +342,9 @@ def open_got_netcdf(
|
|
|
331
342
|
|
|
332
343
|
|
|
333
344
|
# PURPOSE: GOT utilities for xarray Datasets
|
|
334
|
-
@
|
|
345
|
+
@register_dataset_subaccessor("got")
|
|
335
346
|
class GOTDataset:
|
|
336
|
-
"""
|
|
347
|
+
"""``xarray.Dataset`` utilities for GOT tidal models"""
|
|
337
348
|
|
|
338
349
|
def __init__(self, ds):
|
|
339
350
|
self._ds = ds
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
OTIS.py
|
|
4
|
-
Written by Tyler Sutterley (
|
|
4
|
+
Written by Tyler Sutterley (02/2026)
|
|
5
5
|
|
|
6
6
|
Reads OTIS format tidal solutions provided by Oregon State University and ESR
|
|
7
7
|
http://volkov.oce.orst.edu/tides/region.html
|
|
@@ -19,6 +19,8 @@ PYTHON DEPENDENCIES:
|
|
|
19
19
|
https://docs.xarray.dev/en/stable/
|
|
20
20
|
|
|
21
21
|
UPDATE HISTORY:
|
|
22
|
+
Updated 02/2026: make dataset and datatree accessors for OTIS
|
|
23
|
+
be subaccessors from dataset module
|
|
22
24
|
Updated 01/2026: check if flexure variable exists in TMD3 files
|
|
23
25
|
Updated 12/2025: no longer subclassing pathlib.Path for working directories
|
|
24
26
|
Updated 11/2025: near-complete rewrite of program to use xarray
|
|
@@ -102,6 +104,10 @@ import warnings
|
|
|
102
104
|
import numpy as np
|
|
103
105
|
import xarray as xr
|
|
104
106
|
import pyTMD.utilities
|
|
107
|
+
from .dataset import (
|
|
108
|
+
register_dataset_subaccessor,
|
|
109
|
+
register_datatree_subaccessor,
|
|
110
|
+
)
|
|
105
111
|
|
|
106
112
|
# attempt imports
|
|
107
113
|
dask = pyTMD.utilities.import_dependency("dask")
|
|
@@ -126,7 +132,7 @@ __all__ = [
|
|
|
126
132
|
"write_raw_binary",
|
|
127
133
|
"OTISDataset",
|
|
128
134
|
"OTISDataTree",
|
|
129
|
-
"
|
|
135
|
+
"CompactDataset",
|
|
130
136
|
]
|
|
131
137
|
|
|
132
138
|
# variable attributes
|
|
@@ -373,7 +379,7 @@ def open_otis_dataset(
|
|
|
373
379
|
# transports are returned as (u,v)
|
|
374
380
|
ds2 = open_otis_transport(model_file, **kwargs)[1]
|
|
375
381
|
# merge datasets
|
|
376
|
-
ds = ds1.
|
|
382
|
+
ds = OTISDataset(ds1).merge(ds2, group=group)
|
|
377
383
|
# add attributes
|
|
378
384
|
ds.attrs["group"] = group.upper() if group in ("u", "v") else group
|
|
379
385
|
# return xarray dataset
|
|
@@ -421,22 +427,22 @@ def open_atlas_dataset(
|
|
|
421
427
|
crs = kwargs.get("crs", 4326)
|
|
422
428
|
# open grid file
|
|
423
429
|
dsg, dtg = open_atlas_grid(grid_file, use_mmap=use_mmap)
|
|
424
|
-
ds1 = dsg.
|
|
430
|
+
ds1 = CompactDataset(dsg).combine_local(dtg, chunks=chunks)
|
|
425
431
|
# add attributes
|
|
426
432
|
ds1.attrs["crs"] = pyproj.CRS.from_user_input(crs).to_dict()
|
|
427
433
|
# open model file(s)
|
|
428
434
|
if group == "z":
|
|
429
435
|
# elevations are returned as (z, localz)
|
|
430
436
|
dsh, dth = open_atlas_elevation(model_file, use_mmap=use_mmap)
|
|
431
|
-
ds2 = dsh.
|
|
437
|
+
ds2 = CompactDataset(dsh).combine_local(dth, chunks=chunks)
|
|
432
438
|
elif group in ("u", "U"):
|
|
433
439
|
# transports are returned as (u, v, localu, localv)
|
|
434
440
|
dsu, dtu, dsv, dtv = open_atlas_transport(model_file, use_mmap=use_mmap)
|
|
435
|
-
ds2 = dsu.
|
|
441
|
+
ds2 = CompactDataset(dsu).combine_local(dtu, chunks=chunks)
|
|
436
442
|
elif group in ("v", "V"):
|
|
437
443
|
# transports are returned as (u, v, localu, localv)
|
|
438
444
|
dsu, dtu, dsv, dtv = open_atlas_transport(model_file, use_mmap=use_mmap)
|
|
439
|
-
ds2 = dsv.
|
|
445
|
+
ds2 = CompactDataset(dsv).combine_local(dtv, chunks=chunks)
|
|
440
446
|
# merge datasets
|
|
441
447
|
ds = xr.merge([ds1, ds2], compat="override")
|
|
442
448
|
# add attributes
|
|
@@ -1836,9 +1842,9 @@ def write_raw_binary(
|
|
|
1836
1842
|
|
|
1837
1843
|
|
|
1838
1844
|
# PURPOSE: OTIS utilities for xarray Datasets
|
|
1839
|
-
@
|
|
1845
|
+
@register_dataset_subaccessor("otis")
|
|
1840
1846
|
class OTISDataset:
|
|
1841
|
-
"""
|
|
1847
|
+
"""``xarray.Dataset`` utilities for OTIS tidal models"""
|
|
1842
1848
|
|
|
1843
1849
|
def __init__(self, ds):
|
|
1844
1850
|
# initialize dataset
|
|
@@ -1913,9 +1919,9 @@ class OTISDataset:
|
|
|
1913
1919
|
|
|
1914
1920
|
|
|
1915
1921
|
# PURPOSE: OTIS utilities for xarray datatrees
|
|
1916
|
-
@
|
|
1922
|
+
@register_datatree_subaccessor("otis")
|
|
1917
1923
|
class OTISDataTree:
|
|
1918
|
-
"""
|
|
1924
|
+
"""``xarray.DataTree`` utilities for OTIS tidal models"""
|
|
1919
1925
|
|
|
1920
1926
|
def __init__(self, dtree):
|
|
1921
1927
|
# initialize datatree
|
|
@@ -2144,11 +2150,10 @@ class OTISDataTree:
|
|
|
2144
2150
|
|
|
2145
2151
|
|
|
2146
2152
|
# PURPOSE: ATLAS-compact utilities for xarray Datasets
|
|
2147
|
-
@
|
|
2148
|
-
class
|
|
2153
|
+
@register_datatree_subaccessor("compact")
|
|
2154
|
+
class CompactDataset:
|
|
2149
2155
|
"""
|
|
2150
|
-
|
|
2151
|
-
tidal models
|
|
2156
|
+
``xarray.Dataset`` utilities for ATLAS-compact tidal models
|
|
2152
2157
|
"""
|
|
2153
2158
|
|
|
2154
2159
|
def __init__(self, ds, spacing: float | list[float] = 1.0 / 30.0):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
dataset.py
|
|
4
|
-
Written by Tyler Sutterley (
|
|
4
|
+
Written by Tyler Sutterley (02/2026)
|
|
5
5
|
An xarray.Dataset extension for tidal model data
|
|
6
6
|
|
|
7
7
|
PYTHON DEPENDENCIES:
|
|
@@ -17,6 +17,8 @@ PYTHON DEPENDENCIES:
|
|
|
17
17
|
https://docs.xarray.dev/en/stable/
|
|
18
18
|
|
|
19
19
|
UPDATE HISTORY:
|
|
20
|
+
Updated 02/2026: create subaccessor registration functions
|
|
21
|
+
add functions to test if units are compatible with known groups
|
|
20
22
|
Updated 01/2026: handle scalar inputs for coordinate transformations
|
|
21
23
|
Updated 12/2025: add coords functions to transform coordinates
|
|
22
24
|
set units attribute for amplitude and phase data arrays
|
|
@@ -40,7 +42,16 @@ import xarray as xr
|
|
|
40
42
|
# suppress warnings
|
|
41
43
|
warnings.filterwarnings("ignore", category=UserWarning)
|
|
42
44
|
|
|
43
|
-
__all__ = [
|
|
45
|
+
__all__ = [
|
|
46
|
+
"DataTree",
|
|
47
|
+
"Dataset",
|
|
48
|
+
"DataArray",
|
|
49
|
+
"register_datatree_subaccessor",
|
|
50
|
+
"register_dataset_subaccessor",
|
|
51
|
+
"register_dataarray_subaccessor",
|
|
52
|
+
"_transform",
|
|
53
|
+
"_coords",
|
|
54
|
+
]
|
|
44
55
|
|
|
45
56
|
# pint unit registry
|
|
46
57
|
__ureg__ = pint.UnitRegistry()
|
|
@@ -896,7 +907,12 @@ class DataArray:
|
|
|
896
907
|
@property
|
|
897
908
|
def units(self):
|
|
898
909
|
"""Units of the ``DataArray``"""
|
|
899
|
-
|
|
910
|
+
try:
|
|
911
|
+
return __ureg__.parse_units(self._units)
|
|
912
|
+
except TypeError as exc:
|
|
913
|
+
raise ValueError(f"Unknown units: {self._units}") from exc
|
|
914
|
+
except AttributeError as exc:
|
|
915
|
+
raise AttributeError("DataArray has no attribute 'units'") from exc
|
|
900
916
|
|
|
901
917
|
@property
|
|
902
918
|
def quantity(self):
|
|
@@ -912,8 +928,58 @@ class DataArray:
|
|
|
912
928
|
return "current"
|
|
913
929
|
elif self.units.is_compatible_with("m^2/s"):
|
|
914
930
|
return "transport"
|
|
931
|
+
elif self.units.is_compatible_with("degrees"):
|
|
932
|
+
return "angle"
|
|
915
933
|
else:
|
|
916
|
-
raise ValueError(f"Unknown unit group: {self.
|
|
934
|
+
raise ValueError(f"Unknown unit group: {self._units}")
|
|
935
|
+
|
|
936
|
+
@property
|
|
937
|
+
def _units(self):
|
|
938
|
+
"""Units attribute of the ``DataArray`` as a string"""
|
|
939
|
+
return self._da.attrs.get("units")
|
|
940
|
+
|
|
941
|
+
@property
|
|
942
|
+
def _has_compatible_units(self):
|
|
943
|
+
"""Tests that units are compatible with known groups"""
|
|
944
|
+
try:
|
|
945
|
+
unit_group = self.group
|
|
946
|
+
except (TypeError, ValueError, AttributeError) as exc:
|
|
947
|
+
return False
|
|
948
|
+
else:
|
|
949
|
+
return True
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def register_datatree_subaccessor(name):
|
|
953
|
+
"""Register a subaccessor on ``DataTree`` objects
|
|
954
|
+
|
|
955
|
+
Parameters
|
|
956
|
+
----------
|
|
957
|
+
name: str
|
|
958
|
+
subaccessor name
|
|
959
|
+
"""
|
|
960
|
+
return xr.core.extensions._register_accessor(name, DataTree)
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def register_dataset_subaccessor(name):
|
|
964
|
+
"""Register a subaccessor on ``Dataset`` objects
|
|
965
|
+
|
|
966
|
+
Parameters
|
|
967
|
+
----------
|
|
968
|
+
name: str
|
|
969
|
+
subaccessor name
|
|
970
|
+
"""
|
|
971
|
+
return xr.core.extensions._register_accessor(name, Dataset)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def register_dataarray_subaccessor(name):
|
|
975
|
+
"""Register a subaccessor on ``DataArray`` objects
|
|
976
|
+
|
|
977
|
+
Parameters
|
|
978
|
+
----------
|
|
979
|
+
name: str
|
|
980
|
+
subaccessor name
|
|
981
|
+
"""
|
|
982
|
+
return xr.core.extensions._register_accessor(name, DataArray)
|
|
917
983
|
|
|
918
984
|
|
|
919
985
|
def _transform(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""
|
|
3
3
|
model.py
|
|
4
|
-
Written by Tyler Sutterley (
|
|
4
|
+
Written by Tyler Sutterley (02/2026)
|
|
5
5
|
Retrieves tide model parameters for named tide models and
|
|
6
6
|
from model definition files
|
|
7
7
|
|
|
@@ -13,6 +13,9 @@ PYTHON DEPENDENCIES:
|
|
|
13
13
|
https://docs.xarray.dev/en/stable/
|
|
14
14
|
|
|
15
15
|
UPDATE HISTORY:
|
|
16
|
+
Updated 02/2026: add HTML representation for model objects using xarray
|
|
17
|
+
set tidal constituent units (if unset) in a loop
|
|
18
|
+
check if units are compatible with known types before setting units
|
|
16
19
|
Updated 11/2025: use default cache directory if directory is None
|
|
17
20
|
added crs property for model coordinate reference system
|
|
18
21
|
refactor to use new simpler (flattened) database format
|
|
@@ -121,14 +124,14 @@ class DataBase:
|
|
|
121
124
|
"""Returns the items of the model database"""
|
|
122
125
|
return self.__dict__.items()
|
|
123
126
|
|
|
124
|
-
def __repr__(self):
|
|
125
|
-
"""Representation of the ``DataBase`` object"""
|
|
126
|
-
return str(self.__dict__)
|
|
127
|
-
|
|
128
127
|
def __str__(self):
|
|
129
128
|
"""String representation of the ``DataBase`` object"""
|
|
130
129
|
return str(self.__dict__)
|
|
131
130
|
|
|
131
|
+
def __repr__(self):
|
|
132
|
+
"""Representation of the ``DataBase`` object"""
|
|
133
|
+
return self.__str__()
|
|
134
|
+
|
|
132
135
|
def get(self, key, default=None):
|
|
133
136
|
if not hasattr(self, key) or getattr(self, key) is None:
|
|
134
137
|
return default
|
|
@@ -218,6 +221,7 @@ class model:
|
|
|
218
221
|
self.format = None
|
|
219
222
|
self.name = None
|
|
220
223
|
self.verify = copy.copy(kwargs["verify"])
|
|
224
|
+
self.__parameters__ = {}
|
|
221
225
|
|
|
222
226
|
def from_database(self, m: str, group: tuple = ("z", "u", "v")):
|
|
223
227
|
"""
|
|
@@ -833,11 +837,11 @@ class model:
|
|
|
833
837
|
ds.attrs["source"] = self.name
|
|
834
838
|
# add coordinate reference system to Dataset
|
|
835
839
|
ds.attrs["crs"] = self.crs.to_dict()
|
|
836
|
-
#
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
840
|
+
# check if units attribute can be parsed and is a known type
|
|
841
|
+
# if units cannot be parsed: use value defined in the model database
|
|
842
|
+
for c in ds.tmd.constituents:
|
|
843
|
+
if not ds[c].tmd._has_compatible_units:
|
|
844
|
+
ds[c].attrs["units"] = self[group].units
|
|
841
845
|
# convert to default units
|
|
842
846
|
if kwargs["use_default_units"]:
|
|
843
847
|
ds = ds.tmd.to_default_units()
|
|
@@ -873,6 +877,34 @@ class model:
|
|
|
873
877
|
properties.append(f" name: {self.name}")
|
|
874
878
|
return "\n".join(properties)
|
|
875
879
|
|
|
880
|
+
def __repr__(self):
|
|
881
|
+
"""Representation of the ``io.model`` object"""
|
|
882
|
+
return self.__str__()
|
|
883
|
+
|
|
884
|
+
def _repr_html_(self):
|
|
885
|
+
"""HTML representation of the ``io.model`` object"""
|
|
886
|
+
header = "pyTMD.io.model"
|
|
887
|
+
header_components = [f"<div class='xr-obj-type'>{header}</div>"]
|
|
888
|
+
sections = []
|
|
889
|
+
data_vars = [k for k in ("z", "u", "v") if k in self.__parameters__]
|
|
890
|
+
parameters = {
|
|
891
|
+
k: v for k, v in self.__parameters__.items() if k not in data_vars
|
|
892
|
+
}
|
|
893
|
+
sections.append(xr.core.formatting_html.attr_section(parameters))
|
|
894
|
+
for v in data_vars:
|
|
895
|
+
sections.append(
|
|
896
|
+
xr.core.formatting_html._mapping_section(
|
|
897
|
+
mapping=self.__parameters__[v],
|
|
898
|
+
name=f"{v}-Attributes",
|
|
899
|
+
details_func=xr.core.formatting_html.summarize_attrs,
|
|
900
|
+
max_items_collapse=0,
|
|
901
|
+
expand_option_name="display_expand_attrs",
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
return xr.core.formatting_html._obj_repr(
|
|
905
|
+
self, header_components, sections
|
|
906
|
+
)
|
|
907
|
+
|
|
876
908
|
def get(self, key, default=None):
|
|
877
909
|
return getattr(self, key, default) or default
|
|
878
910
|
|