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.
Files changed (56) hide show
  1. {pytmd-3.0.2/pyTMD.egg-info → pytmd-3.0.4}/PKG-INFO +3 -3
  2. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/compute.py +34 -15
  3. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/reduce_otis.py +7 -7
  4. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/ATLAS.py +15 -9
  5. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/FES.py +9 -7
  6. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/GOT.py +16 -5
  7. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/OTIS.py +20 -15
  8. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/dataset.py +70 -4
  9. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/model.py +42 -10
  10. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/math.py +84 -1
  11. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/predict.py +512 -318
  12. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/spatial.py +32 -1
  13. {pytmd-3.0.2 → pytmd-3.0.4/pyTMD.egg-info}/PKG-INFO +3 -3
  14. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/requires.txt +2 -2
  15. {pytmd-3.0.2 → pytmd-3.0.4}/pyproject.toml +5 -6
  16. {pytmd-3.0.2 → pytmd-3.0.4}/LICENSE +0 -0
  17. {pytmd-3.0.2 → pytmd-3.0.4}/MANIFEST.in +0 -0
  18. {pytmd-3.0.2 → pytmd-3.0.4}/README.md +0 -0
  19. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/__init__.py +0 -0
  20. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/astro.py +0 -0
  21. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/constituents.py +0 -0
  22. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/ct1971_tab6.txt +0 -0
  23. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/cte1973_tab.txt +0 -0
  24. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/d1921_tab.txt +0 -0
  25. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/database.json +0 -0
  26. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/doodson.json +0 -0
  27. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/hw1995_tab.txt +0 -0
  28. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/re14_tab3.txt +0 -0
  29. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/t1987_tab.txt +0 -0
  30. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/tab5.2e.txt +0 -0
  31. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/tab5.3a.txt +0 -0
  32. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/tab5.3b.txt +0 -0
  33. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/data/w1990_tab.txt +0 -0
  34. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/__init__.py +0 -0
  35. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_arcticdata.py +0 -0
  36. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_aviso_fes.py +0 -0
  37. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_box_tpxo.py +0 -0
  38. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_gsfc_got.py +0 -0
  39. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_iers_opole.py +0 -0
  40. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_jpl_ssd.py +0 -0
  41. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/datasets/fetch_test_data.py +0 -0
  42. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/ellipse.py +0 -0
  43. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/interpolate.py +0 -0
  44. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/IERS.py +0 -0
  45. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/NOAA.py +0 -0
  46. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/io/__init__.py +0 -0
  47. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/solve/__init__.py +0 -0
  48. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/solve/constants.py +0 -0
  49. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/tools.py +0 -0
  50. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/utilities.py +0 -0
  51. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD/version.py +0 -0
  52. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/SOURCES.txt +0 -0
  53. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/dependency_links.txt +0 -0
  54. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/entry_points.txt +0 -0
  55. {pytmd-3.0.2 → pytmd-3.0.4}/pyTMD.egg-info/top_level.txt +0 -0
  56. {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.2
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.0.8
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 (12/2025)
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
- tide: xarray.DataArray
325
- tidal elevation (meters)
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
- tide = local.tmd.predict(
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
- # add major and minor components
413
- tide += local.tmd.infer(
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 tide
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
- tide: xr.DataTree
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
- tide = xr.DataTree()
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
- tide[key] = local.tmd.predict(
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
- # add major and minor components
603
- tide[key] += local.tmd.infer(
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 tide
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": latitude})
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 (12/2025)
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
- @xr.register_dataset_accessor("atlas")
304
+ @register_dataset_subaccessor("atlas")
299
305
  class ATLASDataset:
300
306
  """
301
- Accessor for extending an ``xarray.Dataset`` for ATLAS-netcdf
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
- @xr.register_datatree_accessor("atlas")
470
+ @register_datatree_subaccessor("atlas")
466
471
  class ATLASDataTree:
467
472
  """
468
- Accessor for extending an ``xarray.DataTree`` for ATLAS-netcdf
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.atlasnc.to_grid(grid_file, group=group, mode="a", **kwargs)
507
+ ATLASDataset(ds).to_grid(grid_file, group=group, mode="a", **kwargs)
504
508
  # output constituent files
505
- ds.atlasnc.to_netcdf(directory, group=group, mode="a", **kwargs)
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 (12/2025)
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", u="Ua", v="Va")
315
- mapping_ph = dict(z="Hg", u="Ug", v="Vg")
316
- elif "amplitude" in tmp.variables:
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 "AMPL" in tmp.variables:
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
- @xr.register_dataset_accessor("fes")
349
+ @register_dataset_subaccessor("fes")
348
350
  class FESDataset:
349
- """Accessor for extending an ``xarray.Dataset`` for FES tidal models"""
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 (12/2025)
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
- units = re.findall(r"\((\w+m)\)", file_contents[0], re.IGNORECASE)
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
- @xr.register_dataset_accessor("got")
345
+ @register_dataset_subaccessor("got")
335
346
  class GOTDataset:
336
- """Accessor for extending an ``xarray.Dataset`` for GOT tidal models"""
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 (01/2026)
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
- "ATLASDataset",
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.otis.merge(ds2, group=group)
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.compact.combine_local(dtg, chunks=chunks)
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.compact.combine_local(dth, chunks=chunks)
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.compact.combine_local(dtu, chunks=chunks)
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.compact.combine_local(dtv, chunks=chunks)
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
- @xr.register_dataset_accessor("otis")
1845
+ @register_dataset_subaccessor("otis")
1840
1846
  class OTISDataset:
1841
- """Accessor for extending an ``xarray.Dataset`` for OTIS tidal models"""
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
- @xr.register_datatree_accessor("otis")
1922
+ @register_datatree_subaccessor("otis")
1917
1923
  class OTISDataTree:
1918
- """Accessor for extending an ``xarray.DataTree`` for OTIS tidal models"""
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
- @xr.register_dataset_accessor("compact")
2148
- class ATLASDataset:
2153
+ @register_datatree_subaccessor("compact")
2154
+ class CompactDataset:
2149
2155
  """
2150
- Accessor for extending an ``xarray.Dataset`` for ATLAS-compact
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 (01/2026)
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__ = ["DataTree", "Dataset", "DataArray", "_transform", "_coords"]
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
- return __ureg__.parse_units(self._da.attrs.get("units", ""))
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.units}")
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 (11/2025)
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
- # list of constituents
837
- c = ds.tmd.constituents
838
- # set units attribute if not already set
839
- # (uses value defined in the model database)
840
- ds[c].attrs["units"] = ds[c].attrs.get("units", self[group].units)
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