pyTMD 2.1.2__tar.gz → 2.1.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 (61) hide show
  1. {pytmd-2.1.2 → pytmd-2.1.4}/PKG-INFO +1 -1
  2. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/compute.py +53 -26
  3. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/crs.py +54 -13
  4. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/interpolate.py +9 -5
  5. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/io/ATLAS.py +177 -12
  6. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/io/FES.py +164 -15
  7. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/io/GOT.py +170 -22
  8. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/io/OTIS.py +200 -23
  9. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/io/model.py +193 -5
  10. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD.egg-info/PKG-INFO +1 -1
  11. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/compute_tidal_currents.py +20 -5
  12. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/compute_tidal_elevations.py +20 -5
  13. pytmd-2.1.4/version.txt +1 -0
  14. pytmd-2.1.2/version.txt +0 -1
  15. {pytmd-2.1.2 → pytmd-2.1.4}/.dockerignore +0 -0
  16. {pytmd-2.1.2 → pytmd-2.1.4}/.gitignore +0 -0
  17. {pytmd-2.1.2 → pytmd-2.1.4}/CODE_OF_CONDUCT.rst +0 -0
  18. {pytmd-2.1.2 → pytmd-2.1.4}/CONTRIBUTORS.rst +0 -0
  19. {pytmd-2.1.2 → pytmd-2.1.4}/LICENSE +0 -0
  20. {pytmd-2.1.2 → pytmd-2.1.4}/MANIFEST.in +0 -0
  21. {pytmd-2.1.2 → pytmd-2.1.4}/README.rst +0 -0
  22. {pytmd-2.1.2 → pytmd-2.1.4}/postBuild +0 -0
  23. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/__init__.py +0 -0
  24. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/arguments.py +0 -0
  25. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/astro.py +0 -0
  26. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/check_points.py +0 -0
  27. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/compute_tide_corrections.py +0 -0
  28. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/data/opoleloadcoefcmcor.txt.gz +0 -0
  29. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/data/tab5.2e.txt +0 -0
  30. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/data/tab5.3a.txt +0 -0
  31. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/data/tab5.3b.txt +0 -0
  32. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/ellipse.py +0 -0
  33. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/eop.py +0 -0
  34. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/io/__init__.py +0 -0
  35. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/io/constituents.py +0 -0
  36. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/io/ocean_pole_tide.py +0 -0
  37. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/predict.py +0 -0
  38. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/solve/__init__.py +0 -0
  39. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/solve/constants.py +0 -0
  40. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/spatial.py +0 -0
  41. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/time.py +0 -0
  42. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/tools.py +0 -0
  43. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/utilities.py +0 -0
  44. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD/version.py +0 -0
  45. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD.egg-info/SOURCES.txt +0 -0
  46. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD.egg-info/dependency_links.txt +0 -0
  47. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD.egg-info/requires.txt +0 -0
  48. {pytmd-2.1.2 → pytmd-2.1.4}/pyTMD.egg-info/top_level.txt +0 -0
  49. {pytmd-2.1.2 → pytmd-2.1.4}/requirements-dev.txt +0 -0
  50. {pytmd-2.1.2 → pytmd-2.1.4}/requirements.txt +0 -0
  51. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/arcticdata_tides.py +0 -0
  52. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/aviso_fes_tides.py +0 -0
  53. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/compute_LPET_elevations.py +0 -0
  54. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/compute_LPT_displacements.py +0 -0
  55. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/compute_OPT_displacements.py +0 -0
  56. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/compute_SET_displacements.py +0 -0
  57. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/reduce_OTIS_files.py +0 -0
  58. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/usap_cats_tides.py +0 -0
  59. {pytmd-2.1.2 → pytmd-2.1.4}/scripts/verify_box_tpxo.py +0 -0
  60. {pytmd-2.1.2 → pytmd-2.1.4}/setup.cfg +0 -0
  61. {pytmd-2.1.2 → pytmd-2.1.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyTMD
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: Tide Model Driver to read OTIS, GOT and FES formatted tidal solutions and make tidal predictions
5
5
  Home-page: https://github.com/tsutterley/pyTMD
6
6
  Author: Tyler Sutterley
@@ -62,6 +62,8 @@ PROGRAM DEPENDENCIES:
62
62
  UPDATE HISTORY:
63
63
  Updated 07/2024: assert that data type is a known value
64
64
  make number of days to convert JD to MJD a variable
65
+ added option to crop tide models to the domain of the input data
66
+ added option to use JSON format definition files
65
67
  Updated 06/2024: use np.clongdouble instead of np.longcomplex
66
68
  Updated 04/2024: use wrapper to importlib for optional dependencies
67
69
  Updated 02/2024: changed class name for ellipsoid parameters to datum
@@ -114,6 +116,7 @@ from __future__ import print_function, annotations
114
116
  import logging
115
117
  import pathlib
116
118
  import numpy as np
119
+ from io import IOBase
117
120
  import scipy.interpolate
118
121
  import pyTMD.crs
119
122
  import pyTMD.io
@@ -183,7 +186,10 @@ def tide_elevations(
183
186
  MODEL: str | None = None,
184
187
  ATLAS_FORMAT: str = 'netcdf',
185
188
  GZIP: bool = False,
186
- DEFINITION_FILE: str | pathlib.Path | None = None,
189
+ DEFINITION_FILE: str | pathlib.Path | IOBase | None = None,
190
+ DEFINITION_FORMAT: str = 'ascii',
191
+ CROP: bool = False,
192
+ BOUNDS: list | np.ndarray | None = None,
187
193
  EPSG: str | int = 3031,
188
194
  EPOCH: list | tuple = (2000, 1, 1, 0, 0, 0),
189
195
  TYPE: str | None = 'drift',
@@ -219,8 +225,17 @@ def tide_elevations(
219
225
  - ``'netcdf'``
220
226
  GZIP: bool, default False
221
227
  Tide model files are gzip compressed
222
- DEFINITION_FILE: str or NoneType, default None
228
+ DEFINITION_FILE: str, pathlib.Path, io.IOBase or NoneType, default None
223
229
  Tide model definition file for use
230
+ DEFINITION_FORMAT: str, default 'ascii'
231
+ Format for model definition file
232
+
233
+ - ``'ascii'``: tab-delimited definition file
234
+ - ``'json'``: JSON formatted definition file
235
+ CROP: bool, default False
236
+ Crop tide model data to (buffered) bounds
237
+ BOUNDS: list, np.ndarray or NoneType, default None
238
+ Boundaries for cropping tide model data
224
239
  EPSG: int, default: 3031 (Polar Stereographic South, WGS84)
225
240
  Input coordinate system
226
241
  EPOCH: tuple, default (2000,1,1,0,0,0)
@@ -280,8 +295,8 @@ def tide_elevations(
280
295
 
281
296
  # get parameters for tide model
282
297
  if DEFINITION_FILE is not None:
283
- model = pyTMD.io.model(DIRECTORY).from_file(
284
- pathlib.Path(DEFINITION_FILE).expanduser())
298
+ model = pyTMD.io.model(DIRECTORY).from_file(DEFINITION_FILE,
299
+ format=DEFINITION_FORMAT)
285
300
  else:
286
301
  model = pyTMD.io.model(DIRECTORY, format=ATLAS_FORMAT,
287
302
  compressed=GZIP).elevation(MODEL)
@@ -323,28 +338,28 @@ def tide_elevations(
323
338
  if model.format in ('OTIS', 'ATLAS', 'TMD3'):
324
339
  amp,ph,D,c = pyTMD.io.OTIS.extract_constants(lon, lat, model.grid_file,
325
340
  model.model_file, model.projection, type=model.type,
326
- method=METHOD, extrapolate=EXTRAPOLATE, cutoff=CUTOFF,
327
- grid=model.format, apply_flexure=APPLY_FLEXURE)
341
+ crop=CROP, bounds=BOUNDS, method=METHOD, extrapolate=EXTRAPOLATE,
342
+ cutoff=CUTOFF, grid=model.format, apply_flexure=APPLY_FLEXURE)
328
343
  # use delta time at 2000.0 to match TMD outputs
329
344
  deltat = np.zeros((nt), dtype=np.float64)
330
345
  elif (model.format == 'netcdf'):
331
346
  amp,ph,D,c = pyTMD.io.ATLAS.extract_constants(lon, lat, model.grid_file,
332
- model.model_file, type=model.type, method=METHOD,
333
- extrapolate=EXTRAPOLATE, cutoff=CUTOFF, scale=model.scale,
334
- compressed=model.compressed)
347
+ model.model_file, type=model.type, crop=CROP, bounds=BOUNDS,
348
+ method=METHOD, extrapolate=EXTRAPOLATE, cutoff=CUTOFF,
349
+ scale=model.scale, compressed=model.compressed)
335
350
  # use delta time at 2000.0 to match TMD outputs
336
351
  deltat = np.zeros((nt), dtype=np.float64)
337
352
  elif (model.format == 'GOT'):
338
353
  amp,ph,c = pyTMD.io.GOT.extract_constants(lon, lat, model.model_file,
339
- method=METHOD, extrapolate=EXTRAPOLATE, cutoff=CUTOFF,
340
- scale=model.scale, compressed=model.compressed)
354
+ crop=CROP, bounds=BOUNDS, method=METHOD, extrapolate=EXTRAPOLATE,
355
+ cutoff=CUTOFF, scale=model.scale, compressed=model.compressed)
341
356
  # delta time (TT - UT1)
342
357
  deltat = ts.tt_ut1
343
358
  elif (model.format == 'FES'):
344
359
  amp,ph = pyTMD.io.FES.extract_constants(lon, lat, model.model_file,
345
- type=model.type, version=model.version, method=METHOD,
346
- extrapolate=EXTRAPOLATE, cutoff=CUTOFF, scale=model.scale,
347
- compressed=model.compressed)
360
+ type=model.type, version=model.version, crop=CROP, bounds=BOUNDS,
361
+ method=METHOD, extrapolate=EXTRAPOLATE, cutoff=CUTOFF,
362
+ scale=model.scale, compressed=model.compressed)
348
363
  # available model constituents
349
364
  c = model.constituents
350
365
  # delta time (TT - UT1)
@@ -412,7 +427,10 @@ def tide_currents(
412
427
  MODEL: str | None = None,
413
428
  ATLAS_FORMAT: str = 'netcdf',
414
429
  GZIP: bool = False,
415
- DEFINITION_FILE: str | pathlib.Path | None = None,
430
+ DEFINITION_FILE: str | pathlib.Path | IOBase | None = None,
431
+ DEFINITION_FORMAT: str = 'ascii',
432
+ CROP: bool = False,
433
+ BOUNDS: list | np.ndarray | None = None,
416
434
  EPSG: str | int = 3031,
417
435
  EPOCH: list | tuple = (2000, 1, 1, 0, 0, 0),
418
436
  TYPE: str | None = 'drift',
@@ -447,8 +465,17 @@ def tide_currents(
447
465
  - ``'netcdf'``
448
466
  GZIP: bool, default False
449
467
  Tide model files are gzip compressed
450
- DEFINITION_FILE: str or NoneType, default None
468
+ DEFINITION_FILE: str, pathlib.Path, io.IOBase or NoneType, default None
451
469
  Tide model definition file for use
470
+ DEFINITION_FORMAT: str, default 'ascii'
471
+ Format for model definition file
472
+
473
+ - ``'ascii'``: tab-delimited definition file
474
+ - ``'json'``: JSON formatted definition file
475
+ CROP: bool, default False
476
+ Crop tide model data to (buffered) bounds
477
+ BOUNDS: list, np.ndarray or NoneType, default None
478
+ Boundaries for cropping tide model data
452
479
  EPSG: int, default: 3031 (Polar Stereographic South, WGS84)
453
480
  Input coordinate system
454
481
  EPOCH: tuple, default (2000,1,1,0,0,0)
@@ -504,8 +531,8 @@ def tide_currents(
504
531
 
505
532
  # get parameters for tide model
506
533
  if DEFINITION_FILE is not None:
507
- model = pyTMD.io.model(DIRECTORY).from_file(
508
- pathlib.Path(DEFINITION_FILE).expanduser())
534
+ model = pyTMD.io.model(DIRECTORY).from_file(DEFINITION_FILE,
535
+ format=DEFINITION_FORMAT)
509
536
  else:
510
537
  model = pyTMD.io.model(DIRECTORY, format=ATLAS_FORMAT,
511
538
  compressed=GZIP).current(MODEL)
@@ -551,22 +578,22 @@ def tide_currents(
551
578
  if model.format in ('OTIS', 'ATLAS', 'TMD3'):
552
579
  amp,ph,D,c = pyTMD.io.OTIS.extract_constants(lon, lat, model.grid_file,
553
580
  model.model_file['u'], model.projection, type=t,
554
- method=METHOD, extrapolate=EXTRAPOLATE, cutoff=CUTOFF,
555
- grid=model.format)
581
+ crop=CROP, bounds=BOUNDS, method=METHOD, extrapolate=EXTRAPOLATE,
582
+ cutoff=CUTOFF, grid=model.format)
556
583
  # use delta time at 2000.0 to match TMD outputs
557
584
  deltat = np.zeros((nt), dtype=np.float64)
558
585
  elif (model.format == 'netcdf'):
559
586
  amp,ph,D,c = pyTMD.io.ATLAS.extract_constants(lon, lat, model.grid_file,
560
- model.model_file[t], type=t, method=METHOD,
561
- extrapolate=EXTRAPOLATE, cutoff=CUTOFF, scale=model.scale,
562
- compressed=model.compressed)
587
+ model.model_file[t], type=t, crop=CROP, bounds=BOUNDS,
588
+ method=METHOD, extrapolate=EXTRAPOLATE, cutoff=CUTOFF,
589
+ scale=model.scale, compressed=model.compressed)
563
590
  # use delta time at 2000.0 to match TMD outputs
564
591
  deltat = np.zeros((nt), dtype=np.float64)
565
592
  elif (model.format == 'FES'):
566
593
  amp,ph = pyTMD.io.FES.extract_constants(lon, lat, model.model_file[t],
567
- type=t, version=model.version, method=METHOD,
568
- extrapolate=EXTRAPOLATE, cutoff=CUTOFF, scale=model.scale,
569
- compressed=model.compressed)
594
+ type=t, version=model.version, crop=CROP, bounds=BOUNDS,
595
+ method=METHOD, extrapolate=EXTRAPOLATE, cutoff=CUTOFF,
596
+ scale=model.scale, compressed=model.compressed)
570
597
  # available model constituents
571
598
  c = model.constituents
572
599
  # delta time (TT - UT1)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  u"""
3
3
  crs.py
4
- Written by Tyler Sutterley (05/2024)
4
+ Written by Tyler Sutterley (07/2024)
5
5
  Coordinates Reference System (CRS) routines
6
6
 
7
7
  CALLING SEQUENCE:
@@ -30,6 +30,7 @@ PYTHON DEPENDENCIES:
30
30
  https://pyproj4.github.io/pyproj/
31
31
 
32
32
  UPDATE HISTORY:
33
+ Updated 07/2024: added function to get the CRS transform
33
34
  Updated 05/2024: make subscriptable and allow item assignment
34
35
  Updated 04/2024: use wrapper to importlib for optional dependencies
35
36
  Updated 02/2024: changed class name for ellipsoid parameters to datum
@@ -106,6 +107,34 @@ class crs:
106
107
  o2: np.ndarray
107
108
  Output transformed y-coordinates
108
109
  """
110
+ # name of the projection
111
+ self.name = PROJ
112
+ # set the transform and transform direction
113
+ self.get(PROJ, EPSG=EPSG)
114
+ self._direction = BF[0].upper()
115
+ # run conversion program and return values
116
+ return self.transform(i1, i2)
117
+
118
+ # PURPOSE: try to get the projection information
119
+ def get(self, PROJ: str, EPSG: int | str = 4326):
120
+ """
121
+ Tries to get the CRS transformer for given values
122
+
123
+ Parameters
124
+ ----------
125
+ PROJ: str
126
+ Spatial reference system code for coordinate transformations
127
+ EPSG: int or str, default 4326 (WGS84 Latitude/Longitude)
128
+ input (``'F'``) or output (``'B'``) coordinate system
129
+
130
+ Returns
131
+ -------
132
+ o1: np.ndarray
133
+ Output transformed x-coordinates
134
+ o2: np.ndarray
135
+ Output transformed y-coordinates
136
+ """
137
+ # name of the projection
109
138
  self.name = PROJ
110
139
  # python dictionary with named conversion functions
111
140
  transforms = {}
@@ -115,8 +144,6 @@ class crs:
115
144
  transforms['3976'] = self._EPSG3976
116
145
  transforms['PSNorth'] = self._PSNorth
117
146
  transforms['4326'] = self._EPSG4326
118
- # set the direction of the transform
119
- self._direction = BF.upper()
120
147
  # check that PROJ for conversion was entered correctly
121
148
  # run named conversion program and return values
122
149
  try:
@@ -125,7 +152,7 @@ class crs:
125
152
  pass
126
153
  else:
127
154
  # return the output variables
128
- return self.transform(i1, i2)
155
+ return self
129
156
  # try changing the projection using a custom projection
130
157
  # run custom conversion program and return values
131
158
  try:
@@ -133,11 +160,14 @@ class crs:
133
160
  except Exception as exc:
134
161
  pass
135
162
  else:
136
- return self.transform(i1, i2)
163
+ return self
137
164
  # projection not found or available
138
165
  raise Exception(f'PROJ: {PROJ} conversion function not found')
139
166
 
140
- def transform(self, i1: np.ndarray, i2: np.ndarray):
167
+ def transform(self,
168
+ i1: np.ndarray,
169
+ i2: np.ndarray,
170
+ **kwargs):
141
171
  """
142
172
  Performs Coordinates Reference System (CRS) transformations
143
173
 
@@ -147,6 +177,8 @@ class crs:
147
177
  Input x-coordinates
148
178
  i2: np.ndarray
149
179
  Input y-coordinates
180
+ kwargs: dict
181
+ Keyword arguments for the transformation
150
182
 
151
183
  Returns
152
184
  -------
@@ -155,10 +187,11 @@ class crs:
155
187
  o2: np.ndarray
156
188
  Output transformed y-coordinates
157
189
  """
190
+ # set the direction of the transformation
191
+ kwargs.setdefault('direction', self.direction)
158
192
  if (self.name == 'PSNorth') and (self.direction.name == 'FORWARD'):
159
193
  # convert input coordinate reference system to lat/lon
160
- lon, lat = self.transformer.transform(i1, i2,
161
- direction=self.direction)
194
+ lon, lat = self.transformer.transform(i1, i2, **kwargs)
162
195
  # convert lat/lon to (idealized) Polar-Stereographic x/y
163
196
  o1 = (90.0 - lat)*111.7*np.cos(lon/180.0*np.pi)
164
197
  o2 = (90.0 - lat)*111.7*np.sin(lon/180.0*np.pi)
@@ -170,12 +203,10 @@ class crs:
170
203
  ii, = np.nonzero(lon < 0)
171
204
  lon[ii] += 360.0
172
205
  # convert to output coordinate reference system
173
- o1, o2 = self.transformer.transform(lon, lat,
174
- direction=self.direction)
206
+ o1, o2 = self.transformer.transform(lon, lat, **kwargs)
175
207
  else:
176
208
  # convert coordinate reference system
177
- o1, o2 = self.transformer.transform(i1, i2,
178
- direction=self.direction)
209
+ o1, o2 = self.transformer.transform(i1, i2, **kwargs)
179
210
  # return the transformed coordinates
180
211
  return (o1, o2)
181
212
 
@@ -340,12 +371,22 @@ class crs:
340
371
  ``pyproj`` direction of the coordinate transform
341
372
  """
342
373
  # convert from input coordinates to model coordinates
343
- if (self._direction.upper() == 'F'):
374
+ if (self._direction is None) or (self._direction.upper() == 'F'):
344
375
  return pyproj.enums.TransformDirection.FORWARD
345
376
  # convert from model coordinates to coordinates
346
377
  elif (self._direction.upper() == 'B'):
347
378
  return pyproj.enums.TransformDirection.INVERSE
348
379
 
380
+ @property
381
+ def is_geographic(self):
382
+ """
383
+ Check if the coordinate reference system is geographic
384
+ """
385
+ if (self.name == 'PSNorth'):
386
+ return False
387
+ else:
388
+ return self.transformer.target_crs.is_geographic
389
+
349
390
  def __str__(self):
350
391
  """String representation of the ``crs`` object
351
392
  """
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  u"""
3
3
  interpolate.py
4
- Written by Tyler Sutterley (12/2022)
4
+ Written by Tyler Sutterley (07/2024)
5
5
  Interpolators for spatial data
6
6
 
7
7
  PYTHON DEPENDENCIES:
@@ -12,6 +12,7 @@ PYTHON DEPENDENCIES:
12
12
  https://docs.scipy.org/doc/
13
13
 
14
14
  UPDATE HISTORY:
15
+ Updated 07/2024: changed projection flag in extrapolation to is_geographic
15
16
  Written 12/2022
16
17
  """
17
18
  from __future__ import annotations
@@ -275,7 +276,7 @@ def extrapolate(
275
276
  fill_value: float = None,
276
277
  dtype: str | np.dtype = np.float64,
277
278
  cutoff: int | float = np.inf,
278
- EPSG: str | int = '4326',
279
+ is_geographic: bool = True,
279
280
  **kwargs
280
281
  ):
281
282
  """
@@ -303,14 +304,17 @@ def extrapolate(
303
304
  return only neighbors within distance [km]
304
305
 
305
306
  Set to ``np.inf`` to extrapolate for all points
306
- EPSG: str, default '4326'
307
- projection of tide model data
307
+ is_geographic: bool, default True
308
+ input grid is in geographic coordinates
308
309
 
309
310
  Returns
310
311
  -------
311
312
  DATA: np.ndarray
312
313
  interpolated data
313
314
  """
315
+ # set geographic flag if using old EPSG projection keyword
316
+ if hasattr(kwargs, 'EPSG') and (kwargs['EPSG'] == '4326'):
317
+ is_geographic = True
314
318
  # verify output dimensions
315
319
  lon = np.atleast_1d(lon)
316
320
  lat = np.atleast_1d(lat)
@@ -332,7 +336,7 @@ def extrapolate(
332
336
  valid_bounds = np.ones_like(idata.mask, dtype=bool)
333
337
 
334
338
  # calculate coordinates for nearest-neighbors
335
- if (EPSG == '4326'):
339
+ if is_geographic:
336
340
  # global or regional equirectangular model
337
341
  # calculate meshgrid of model coordinates
338
342
  gridlon, gridlat = np.meshgrid(ilon, ilat)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  u"""
3
3
  ATLAS.py
4
- Written by Tyler Sutterley (02/2024)
4
+ Written by Tyler Sutterley (07/2024)
5
5
 
6
6
  Reads files for a tidal model and makes initial calculations to run tide program
7
7
  Includes functions to extract tidal harmonic constants from OTIS tide models for
@@ -55,6 +55,7 @@ PROGRAM DEPENDENCIES:
55
55
  interpolate.py: interpolation routines for spatial data
56
56
 
57
57
  UPDATE HISTORY:
58
+ Updated 07/2024: added crop and bounds keywords for trimming model data
58
59
  Updated 02/2024: changed variable for setting global grid flag to is_global
59
60
  Updated 10/2023: add generic wrapper function for reading constituents
60
61
  Updated 04/2023: using pathlib to define and expand tide model paths
@@ -141,6 +142,10 @@ def extract_constants(
141
142
  - ``'U'``: horizontal depth-averaged transport
142
143
  - ``'v'``: vertical transport velocities
143
144
  - ``'V'``: vertical depth-averaged transport
145
+ crop: bool, default False
146
+ Crop tide model data to (buffered) bounds
147
+ bounds: list or NoneType, default None
148
+ Boundaries for cropping tide model data
144
149
  method: str, default 'spline'
145
150
  Interpolation method
146
151
 
@@ -171,6 +176,7 @@ def extract_constants(
171
176
  """
172
177
  # set default keyword arguments
173
178
  kwargs.setdefault('type', 'z')
179
+ kwargs.setdefault('crop', False)
174
180
  kwargs.setdefault('method', 'spline')
175
181
  kwargs.setdefault('extrapolate', False)
176
182
  kwargs.setdefault('cutoff', 10.0)
@@ -204,9 +210,24 @@ def extract_constants(
204
210
  # adjust dimensions of input coordinates to be iterable
205
211
  ilon = np.atleast_1d(np.copy(ilon))
206
212
  ilat = np.atleast_1d(np.copy(ilat))
207
- # adjust longitudinal convention of input latitude and longitude
208
- # to fit tide model convention
209
- if (np.min(ilon) < 0.0) & (np.max(lon) > 180.0):
213
+ # set default bounds if cropping
214
+ xmin, xmax = np.min(ilon), np.max(ilon)
215
+ ymin, ymax = np.min(ilat), np.max(ilat)
216
+ kwargs.setdefault('bounds', [xmin, xmax, ymin, ymax])
217
+ # grid step size of tide model
218
+ dlon = lon[1] - lon[0]
219
+ # if global: extend limits
220
+ is_global = False
221
+
222
+ # crop bathymetry data to (buffered) bounds
223
+ # or adjust longitudinal convention to fit tide model
224
+ if kwargs['crop'] and np.any(kwargs['bounds']):
225
+ mlon, mlat = np.copy(lon), np.copy(lat)
226
+ bathymetry, lon, lat = _crop(bathymetry, mlon, mlat,
227
+ bounds=kwargs['bounds'],
228
+ buffer=4*dlon
229
+ )
230
+ elif (np.min(ilon) < 0.0) & (np.max(lon) > 180.0):
210
231
  # input points convention (-180:180)
211
232
  # tide model convention (0:360)
212
233
  ilon[ilon < 0.0] += 360.0
@@ -215,10 +236,6 @@ def extract_constants(
215
236
  # tide model convention (-180:180)
216
237
  ilon[ilon > 180.0] -= 360.0
217
238
 
218
- # grid step size of tide model
219
- dlon = lon[1] - lon[0]
220
- # if global: extend limits
221
- is_global = False
222
239
  # replace original values with extend arrays/matrices
223
240
  if np.isclose(lon[-1] - lon[0], 360.0 - dlon):
224
241
  lon = _extend_array(lon, dlon)
@@ -280,6 +297,12 @@ def extract_constants(
280
297
  compressed=kwargs['compressed'])
281
298
  # append constituent to list
282
299
  constituents.append(cons)
300
+ # crop tide model data to (buffered) bounds
301
+ if kwargs['crop'] and np.any(kwargs['bounds']):
302
+ hc, _, _ = _crop(hc, mlon, mlat,
303
+ bounds=kwargs['bounds'],
304
+ buffer=4*dlon
305
+ )
283
306
  # replace original values with extend matrices
284
307
  if is_global:
285
308
  hc = _extend_matrix(hc)
@@ -366,6 +389,10 @@ def read_constants(
366
389
  - ``'V'``: vertical depth-averaged transport
367
390
  compressed: bool, default False
368
391
  Input files are gzip compressed
392
+ crop: bool, default False
393
+ Crop tide model data to (buffered) bounds
394
+ bounds: list or NoneType, default None
395
+ Boundaries for cropping tide model data
369
396
 
370
397
  Returns
371
398
  -------
@@ -375,6 +402,8 @@ def read_constants(
375
402
  # set default keyword arguments
376
403
  kwargs.setdefault('type', 'z')
377
404
  kwargs.setdefault('compressed', True)
405
+ kwargs.setdefault('crop', False)
406
+ kwargs.setdefault('bounds', None)
378
407
 
379
408
  # raise warning if model files are entered as a string or path
380
409
  if isinstance(model_files, (str, pathlib.Path)):
@@ -389,12 +418,21 @@ def read_constants(
389
418
  # read the tide grid file for bathymetry and spatial coordinates
390
419
  lon, lat, bathymetry = read_netcdf_grid(grid_file, kwargs['type'],
391
420
  compressed=kwargs['compressed'])
421
+ is_global = False
392
422
 
423
+ # crop bathymetry data to (buffered) bounds
424
+ if kwargs['crop'] and np.any(kwargs['bounds']):
425
+ mlon, mlat = np.copy(lon), np.copy(lat)
426
+ bathymetry, lon, lat = _crop(bathymetry, mlon, mlat,
427
+ bounds=kwargs['bounds'],
428
+ )
393
429
  # grid step size of tide model
394
430
  dlon = lon[1] - lon[0]
395
431
  # replace original values with extend arrays/matrices
396
- lon = _extend_array(lon, dlon)
397
- bathymetry = _extend_matrix(bathymetry)
432
+ if np.isclose(lon[-1] - lon[0], 360.0 - dlon):
433
+ lon = _extend_array(lon, dlon)
434
+ bathymetry = _extend_matrix(bathymetry)
435
+ is_global = True
398
436
  # save output constituents
399
437
  constituents = pyTMD.io.constituents(
400
438
  longitude=lon,
@@ -412,8 +450,15 @@ def read_constants(
412
450
  # read constituent from netCDF4 file
413
451
  hc, cons = read_netcdf_file(model_file, kwargs['type'],
414
452
  compressed=kwargs['compressed'])
453
+ # crop tide model data to (buffered) bounds
454
+ if kwargs['crop'] and np.any(kwargs['bounds']):
455
+ hc, lon, lat = _crop(hc, mlon, mlat,
456
+ bounds=kwargs['bounds'],
457
+ )
415
458
  # replace original values with extend matrices
416
- hc = _extend_matrix(hc)
459
+ if is_global:
460
+ hc = _extend_matrix(hc)
461
+ # set constituent masks
417
462
  hc.mask[:] |= bathymetry.mask[:]
418
463
  # append extended constituent
419
464
  constituents.append(cons, hc)
@@ -1171,8 +1216,128 @@ def _extend_matrix(input_matrix: np.ndarray):
1171
1216
  extended matrix
1172
1217
  """
1173
1218
  ny, nx = np.shape(input_matrix)
1174
- temp = np.ma.zeros((ny,nx+2), dtype=input_matrix.dtype)
1219
+ # allocate for extended matrix
1220
+ if np.ma.isMA(input_matrix):
1221
+ temp = np.ma.zeros((ny,nx+2), dtype=input_matrix.dtype)
1222
+ else:
1223
+ temp = np.zeros((ny,nx+2), dtype=input_matrix.dtype)
1224
+ # extend matrix
1175
1225
  temp[:,0] = input_matrix[:,-1]
1176
1226
  temp[:,1:-1] = input_matrix[:,:]
1177
1227
  temp[:,-1] = input_matrix[:,0]
1178
1228
  return temp
1229
+
1230
+ # PURPOSE: crop tide model data to bounds
1231
+ def _crop(
1232
+ input_matrix: np.ndarray,
1233
+ ilon: np.ndarray,
1234
+ ilat: np.ndarray,
1235
+ bounds: list | tuple,
1236
+ buffer: int | float = 0
1237
+ ):
1238
+ """
1239
+ Crop tide model data to bounds
1240
+
1241
+ Parameters
1242
+ ----------
1243
+ input_matrix: np.ndarray
1244
+ matrix to crop
1245
+ ilon: np.ndarray
1246
+ longitude of tidal model
1247
+ ilat: np.ndarray
1248
+ latitude of tidal model
1249
+ bounds: list, tuple
1250
+ bounding box: ``[xmin, xmax, ymin, ymax]``
1251
+ buffer: int or float, default 0
1252
+ buffer to add to bounds for cropping
1253
+
1254
+ Returns
1255
+ -------
1256
+ temp: np.ndarray
1257
+ cropped matrix
1258
+ lon: np.ndarray
1259
+ cropped longitude
1260
+ lat: np.ndarray
1261
+ cropped latitude
1262
+ """
1263
+ # adjust longitudinal convention of tide model
1264
+ if (np.min(bounds[:2]) < 0.0) & (np.max(ilon) > 180.0):
1265
+ input_matrix, ilon = _shift(input_matrix, ilon,
1266
+ lon0=180.0, cyclic=360.0, direction='west')
1267
+ elif (np.max(bounds[:2]) > 180.0) & (np.min(ilon) < 0.0):
1268
+ input_matrix, ilon = _shift(input_matrix, ilon,
1269
+ lon0=0.0, cyclic=360.0, direction='east')
1270
+ # unpack bounds and buffer
1271
+ xmin = bounds[0] - buffer
1272
+ xmax = bounds[1] + buffer
1273
+ ymin = bounds[2] - buffer
1274
+ ymax = bounds[3] + buffer
1275
+ # find indices for cropping
1276
+ yind = np.flatnonzero((ilat >= ymin) & (ilat <= ymax))
1277
+ xind = np.flatnonzero((ilon >= xmin) & (ilon <= xmax))
1278
+ # slices for cropping axes
1279
+ rows = slice(yind[0], yind[-1]+1)
1280
+ cols = slice(xind[0], xind[-1]+1)
1281
+ # crop matrix
1282
+ temp = input_matrix[rows, cols]
1283
+ lon = ilon[cols]
1284
+ lat = ilat[rows]
1285
+ # return cropped data
1286
+ return (temp, lon, lat)
1287
+
1288
+ # PURPOSE: shift a grid east or west
1289
+ def _shift(
1290
+ input_matrix: np.ndarray,
1291
+ ilon: np.ndarray,
1292
+ lon0: int | float = 180,
1293
+ cyclic: int | float = 360,
1294
+ direction: str = 'west'
1295
+ ):
1296
+ """
1297
+ Shift global grid east or west to a new base longitude
1298
+
1299
+ Parameters
1300
+ ----------
1301
+ input_matrix: np.ndarray
1302
+ input matrix to shift
1303
+ ilon: np.ndarray
1304
+ longitude of tidal model
1305
+ lon0: int or float, default 180
1306
+ Starting longitude for shifted grid
1307
+ cyclic: int or float, default 360
1308
+ width of periodic domain
1309
+ direction: str, default 'west'
1310
+ Direction to shift grid
1311
+
1312
+ - ``'west'``
1313
+ - ``'east'``
1314
+
1315
+ Returns
1316
+ -------
1317
+ temp: np.ndarray
1318
+ shifted matrix
1319
+ lon: np.ndarray
1320
+ shifted longitude
1321
+ """
1322
+ # find the starting index if cyclic
1323
+ offset = 0 if (np.fabs(ilon[-1]-ilon[0]-cyclic) > 1e-4) else 1
1324
+ i0 = np.argmin(np.fabs(ilon - lon0))
1325
+ # shift longitudinal values
1326
+ lon = np.zeros(ilon.shape, ilon.dtype)
1327
+ lon[0:-i0] = ilon[i0:]
1328
+ lon[-i0:] = ilon[offset: i0+offset]
1329
+ # add or remove the cyclic
1330
+ if (direction == 'east'):
1331
+ lon[-i0:] += cyclic
1332
+ elif (direction == 'west'):
1333
+ lon[0:-i0] -= cyclic
1334
+ # allocate for shifted data
1335
+ if np.ma.isMA(input_matrix):
1336
+ temp = np.ma.zeros(input_matrix.shape,input_matrix.dtype)
1337
+ else:
1338
+ temp = np.zeros(input_matrix.shape, input_matrix.dtype)
1339
+ # shift data values
1340
+ temp[:,:-i0] = input_matrix[:,i0:]
1341
+ temp[:,-i0:] = input_matrix[:,offset: i0+offset]
1342
+ # return the shifted values
1343
+ return (temp, lon)