eo-tides 0.7.6.dev1__py3-none-any.whl → 0.7.6.dev3__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.
- eo_tides/__init__.py +14 -18
- eo_tides/eo.py +57 -41
- eo_tides/model.py +227 -153
- eo_tides/stats.py +65 -27
- eo_tides/utils.py +107 -71
- eo_tides/validation.py +26 -21
- {eo_tides-0.7.6.dev1.dist-info → eo_tides-0.7.6.dev3.dist-info}/METADATA +2 -2
- eo_tides-0.7.6.dev3.dist-info/RECORD +10 -0
- eo_tides-0.7.6.dev1.dist-info/RECORD +0 -10
- {eo_tides-0.7.6.dev1.dist-info → eo_tides-0.7.6.dev3.dist-info}/WHEEL +0 -0
- {eo_tides-0.7.6.dev1.dist-info → eo_tides-0.7.6.dev3.dist-info}/licenses/LICENSE +0 -0
eo_tides/stats.py
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
+
"""Tools for analysing local tide dynamics and satellite biases.
|
2
|
+
|
3
|
+
This module provides functions to assess how well satellite EO data
|
4
|
+
captures real-world tides, and reveals potential tide biases in
|
5
|
+
satellite EO data coverage.
|
6
|
+
"""
|
7
|
+
|
1
8
|
# Used to postpone evaluation of type annotations
|
2
9
|
from __future__ import annotations
|
3
10
|
|
4
|
-
import
|
5
|
-
from typing import TYPE_CHECKING
|
11
|
+
from typing import TYPE_CHECKING, cast
|
6
12
|
|
7
13
|
import matplotlib.pyplot as plt
|
8
14
|
import numpy as np
|
@@ -11,13 +17,21 @@ import xarray as xr
|
|
11
17
|
|
12
18
|
# Only import if running type checking
|
13
19
|
if TYPE_CHECKING:
|
20
|
+
import os
|
21
|
+
|
14
22
|
from odc.geo.geobox import GeoBox
|
15
23
|
|
24
|
+
from .utils import DatetimeLike
|
25
|
+
|
16
26
|
from .eo import _pixel_tides_resample, _resample_chunks, _standardise_inputs, pixel_tides, tag_tides
|
17
|
-
from .utils import DatetimeLike
|
18
27
|
|
19
28
|
|
20
|
-
def _tide_statistics(
|
29
|
+
def _tide_statistics(
|
30
|
+
obs_tides: xr.DataArray,
|
31
|
+
all_tides: xr.DataArray,
|
32
|
+
min_max_q: tuple = (0.0, 1.0),
|
33
|
+
dim: str = "time",
|
34
|
+
) -> xr.Dataset:
|
21
35
|
# Calculate means of observed and modelled tides
|
22
36
|
mot = obs_tides.mean(dim=dim)
|
23
37
|
mat = all_tides.mean(dim=dim)
|
@@ -62,7 +76,19 @@ def _tide_statistics(obs_tides, all_tides, min_max_q=(0.0, 1.0), dim="time"):
|
|
62
76
|
)
|
63
77
|
|
64
78
|
|
65
|
-
def _stats_plain_english(
|
79
|
+
def _stats_plain_english(
|
80
|
+
mot,
|
81
|
+
mat,
|
82
|
+
hot,
|
83
|
+
hat,
|
84
|
+
lot,
|
85
|
+
lat,
|
86
|
+
otr,
|
87
|
+
tr,
|
88
|
+
spread,
|
89
|
+
offset_low,
|
90
|
+
offset_high,
|
91
|
+
) -> None:
|
66
92
|
# Plain text descriptors
|
67
93
|
mean_diff = "higher" if mot > mat else "lower"
|
68
94
|
mean_diff_icon = "⬆️" if mot > mat else "⬇️"
|
@@ -75,26 +101,32 @@ def _stats_plain_english(mot, mat, hot, hat, lot, lat, otr, tr, spread, offset_l
|
|
75
101
|
print(f"🛰️ Observed tide range: {otr:.2f} m ({lot:.2f} to {hot:.2f} m).\n")
|
76
102
|
print(f"{spread_icon} {spread:.0%} of the modelled astronomical tide range was observed at this location.")
|
77
103
|
print(
|
78
|
-
f"{high_tide_icon} The highest {offset_high:.0%} ({offset_high * tr:.2f} m) of the tide range was never observed."
|
104
|
+
f"{high_tide_icon} The highest {offset_high:.0%} ({offset_high * tr:.2f} m) of the tide range was never observed.",
|
79
105
|
)
|
80
106
|
print(
|
81
|
-
f"{low_tide_icon} The lowest {offset_low:.0%} ({offset_low * tr:.2f} m) of the tide range was never observed.\n"
|
107
|
+
f"{low_tide_icon} The lowest {offset_low:.0%} ({offset_low * tr:.2f} m) of the tide range was never observed.\n",
|
82
108
|
)
|
83
109
|
print(f"🌊 Mean modelled astronomical tide height: {mat:.2f} m.")
|
84
110
|
print(f"🛰️ Mean observed tide height: {mot:.2f} m.")
|
85
111
|
print(
|
86
|
-
f"{mean_diff_icon} The mean observed tide height was {mot - mat:.2f} m {mean_diff} than the mean modelled astronomical tide height."
|
112
|
+
f"{mean_diff_icon} The mean observed tide height was {mot - mat:.2f} m {mean_diff} than the mean modelled astronomical tide height.",
|
87
113
|
)
|
88
114
|
|
89
115
|
|
90
116
|
def _stats_figure(
|
91
|
-
all_tides_da,
|
117
|
+
all_tides_da,
|
118
|
+
obs_tides_da,
|
119
|
+
hot,
|
120
|
+
hat,
|
121
|
+
lot,
|
122
|
+
lat,
|
123
|
+
spread,
|
124
|
+
offset_low,
|
125
|
+
offset_high,
|
126
|
+
plot_var,
|
127
|
+
point_col=None,
|
92
128
|
):
|
93
|
-
"""
|
94
|
-
Plot tide bias statistics as a figure, including both
|
95
|
-
satellite observations and all modelled tides.
|
96
|
-
"""
|
97
|
-
|
129
|
+
"""Plot tide bias statistics as a figure comparing satellite observations and all modelled tides."""
|
98
130
|
# Create plot and add all modelled tides
|
99
131
|
fig, ax = plt.subplots(figsize=(10, 6))
|
100
132
|
all_tides_da.plot(ax=ax, alpha=0.4, label="Modelled tides")
|
@@ -207,7 +239,8 @@ def tide_stats(
|
|
207
239
|
round_stats: int = 3,
|
208
240
|
**tag_tides_kwargs,
|
209
241
|
) -> pd.Series:
|
210
|
-
"""
|
242
|
+
"""Generate tide statistics and satellite tide bias metrics for every dataset timestep.
|
243
|
+
|
211
244
|
Takes a multi-dimensional dataset and generate tide statistics
|
212
245
|
and satellite-observed tide bias metrics, calculated based on
|
213
246
|
every timestep in the satellite data and the geographic centroid
|
@@ -222,7 +255,7 @@ def tide_stats(
|
|
222
255
|
|
223
256
|
For more information about the tidal statistics computed by this
|
224
257
|
function, refer to Figure 8 in Bishop-Taylor et al. 2018:
|
225
|
-
|
258
|
+
https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8
|
226
259
|
|
227
260
|
Parameters
|
228
261
|
----------
|
@@ -309,13 +342,13 @@ def tide_stats(
|
|
309
342
|
- `spread`: proportion of the full modelled tidal range observed by the satellite
|
310
343
|
- `offset_low`: proportion of the lowest tides never observed by the satellite
|
311
344
|
- `offset_high`: proportion of the highest tides never observed by the satellite
|
312
|
-
"""
|
313
345
|
|
346
|
+
"""
|
314
347
|
# Standardise data inputs, time and models
|
315
348
|
gbox, obs_times = _standardise_inputs(data, time)
|
316
349
|
|
317
350
|
# Generate range of times covering entire period of satellite record
|
318
|
-
assert obs_times is not None
|
351
|
+
assert obs_times is not None # noqa: S101
|
319
352
|
all_times = pd.date_range(
|
320
353
|
start=obs_times.min().item(),
|
321
354
|
end=obs_times.max().item(),
|
@@ -333,8 +366,8 @@ def tide_stats(
|
|
333
366
|
time=obs_times,
|
334
367
|
model=model,
|
335
368
|
directory=directory,
|
336
|
-
tidepost_lat=tidepost_lat,
|
337
|
-
tidepost_lon=tidepost_lon,
|
369
|
+
tidepost_lat=tidepost_lat,
|
370
|
+
tidepost_lon=tidepost_lon,
|
338
371
|
**tag_tides_kwargs,
|
339
372
|
)
|
340
373
|
|
@@ -344,13 +377,18 @@ def tide_stats(
|
|
344
377
|
time=all_times,
|
345
378
|
model=model,
|
346
379
|
directory=directory,
|
347
|
-
tidepost_lat=tidepost_lat,
|
348
|
-
tidepost_lon=tidepost_lon,
|
380
|
+
tidepost_lat=tidepost_lat,
|
381
|
+
tidepost_lon=tidepost_lon,
|
349
382
|
**tag_tides_kwargs,
|
350
383
|
)
|
351
384
|
|
352
385
|
# Calculate statistics
|
353
|
-
|
386
|
+
# # (cast ensures typing knows these are always DataArrays)
|
387
|
+
stats_ds = _tide_statistics(
|
388
|
+
cast(xr.DataArray, obs_tides_da),
|
389
|
+
cast(xr.DataArray, all_tides_da),
|
390
|
+
min_max_q=min_max_q,
|
391
|
+
)
|
354
392
|
|
355
393
|
# Convert to pandas and add tide post coordinates
|
356
394
|
stats_df = stats_ds.to_pandas().astype("float32")
|
@@ -412,8 +450,9 @@ def pixel_stats(
|
|
412
450
|
cutoff: float = 10,
|
413
451
|
**pixel_tides_kwargs,
|
414
452
|
) -> xr.Dataset:
|
415
|
-
"""
|
416
|
-
|
453
|
+
"""Generate tide statistics and satellite tide bias metrics for every dataset pixel.
|
454
|
+
|
455
|
+
Takes a multi-dimensional dataset and generate pixel-level
|
417
456
|
tide statistics and satellite-observed tide bias metrics,
|
418
457
|
calculated based on every timestep in the satellite data and
|
419
458
|
modelled into the spatial extent of the imagery.
|
@@ -519,14 +558,13 @@ def pixel_stats(
|
|
519
558
|
- `offset_high`: proportion of the highest tides never observed by the satellite
|
520
559
|
|
521
560
|
"""
|
522
|
-
|
523
561
|
# Standardise data inputs, time and models
|
524
562
|
gbox, obs_times = _standardise_inputs(data, time)
|
525
563
|
dask_chunks = _resample_chunks(data, dask_chunks)
|
526
564
|
model = [model] if isinstance(model, str) else model
|
527
565
|
|
528
566
|
# Generate range of times covering entire period of satellite record
|
529
|
-
assert obs_times is not None
|
567
|
+
assert obs_times is not None # noqa: S101
|
530
568
|
all_times = pd.date_range(
|
531
569
|
start=obs_times.min().item(),
|
532
570
|
end=obs_times.max().item(),
|
eo_tides/utils.py
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
"""General-purpose utilities for tide model setup and data processing.
|
2
|
+
|
3
|
+
This module includes tools for listing and clipping model files,
|
4
|
+
performing spatial interpolation, and other helper tools used across
|
5
|
+
the eo_tides package.
|
6
|
+
"""
|
7
|
+
|
1
8
|
# Used to postpone evaluation of type annotations
|
2
9
|
from __future__ import annotations
|
3
10
|
|
@@ -7,16 +14,22 @@ import pathlib
|
|
7
14
|
import textwrap
|
8
15
|
import warnings
|
9
16
|
from collections import Counter
|
10
|
-
from typing import
|
17
|
+
from typing import TYPE_CHECKING
|
18
|
+
|
19
|
+
# Only import if running type checking
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from collections.abc import Sequence
|
22
|
+
from typing import Any, TypeAlias
|
23
|
+
|
24
|
+
from odc.geo.geom import BoundingBox
|
11
25
|
|
12
26
|
import numpy as np
|
13
27
|
import odc.geo
|
14
28
|
import pandas as pd
|
29
|
+
import pyTMD
|
15
30
|
import xarray as xr
|
16
31
|
from colorama import Style, init
|
17
|
-
from odc.geo.geom import BoundingBox
|
18
32
|
from pyTMD.io.model import load_database
|
19
|
-
from pyTMD.io.model import model as pytmd_model
|
20
33
|
from scipy.spatial import cKDTree as KDTree
|
21
34
|
from tqdm import tqdm
|
22
35
|
|
@@ -24,10 +37,8 @@ from tqdm import tqdm
|
|
24
37
|
DatetimeLike: TypeAlias = np.ndarray | pd.DatetimeIndex | pd.Timestamp | datetime.datetime | str | list[str]
|
25
38
|
|
26
39
|
|
27
|
-
def _get_duplicates(array):
|
28
|
-
"""
|
29
|
-
Return any duplicates in a list or array.
|
30
|
-
"""
|
40
|
+
def _get_duplicates(array: Sequence[Any]) -> list[Any]:
|
41
|
+
"""Return any duplicates in a list or array."""
|
31
42
|
c = Counter(array)
|
32
43
|
return [k for k in c if c[k] > 1]
|
33
44
|
|
@@ -35,35 +46,37 @@ def _get_duplicates(array):
|
|
35
46
|
def _set_directory(
|
36
47
|
directory: str | os.PathLike | None = None,
|
37
48
|
) -> os.PathLike:
|
38
|
-
"""
|
39
|
-
|
40
|
-
path is provided, try global `EO_TIDES_TIDE_MODELS`
|
49
|
+
"""Set tide modelling files directory.
|
50
|
+
|
51
|
+
If no custom path is provided, try global `EO_TIDES_TIDE_MODELS`
|
41
52
|
environmental variable instead.
|
42
53
|
"""
|
43
54
|
if directory is None:
|
44
55
|
if "EO_TIDES_TIDE_MODELS" in os.environ:
|
45
56
|
directory = os.environ["EO_TIDES_TIDE_MODELS"]
|
46
57
|
else:
|
47
|
-
|
58
|
+
err_msg = (
|
48
59
|
"No tide model directory provided via `directory`, and/or no "
|
49
60
|
"`EO_TIDES_TIDE_MODELS` environment variable found. "
|
50
61
|
"Please provide a valid path to your tide model directory."
|
51
62
|
)
|
63
|
+
raise Exception(err_msg)
|
52
64
|
|
53
65
|
# Verify path exists
|
54
66
|
directory = pathlib.Path(directory).expanduser()
|
55
67
|
if not directory.exists():
|
56
|
-
|
68
|
+
err_msg = f"No valid tide model directory found at path `{directory}`"
|
69
|
+
raise FileNotFoundError(err_msg)
|
57
70
|
return directory
|
58
71
|
|
59
72
|
|
60
73
|
def _standardise_time(
|
61
74
|
time: DatetimeLike | None,
|
62
75
|
) -> np.ndarray | None:
|
63
|
-
"""
|
64
|
-
|
65
|
-
|
66
|
-
passed.
|
76
|
+
"""Standardise input times for analysis.
|
77
|
+
|
78
|
+
Accept any time format accepted by `pd.to_datetime`, and
|
79
|
+
return a datetime64 ndarray. Return None if None passed.
|
67
80
|
"""
|
68
81
|
# Return time as-is if None
|
69
82
|
if time is None:
|
@@ -80,8 +93,10 @@ def _standardise_models(
|
|
80
93
|
model: str | list[str],
|
81
94
|
directory: str | os.PathLike,
|
82
95
|
ensemble_models: list[str] | None = None,
|
96
|
+
extra_databases: str | os.PathLike | list | None = None,
|
83
97
|
) -> tuple[list[str], list[str], list[str] | None]:
|
84
|
-
"""
|
98
|
+
"""Standardise lists of models for analysis.
|
99
|
+
|
85
100
|
Take an input model name or list of names, and return a list
|
86
101
|
of models to process, requested models, and ensemble models,
|
87
102
|
as required by the `model_tides` function.
|
@@ -89,42 +104,46 @@ def _standardise_models(
|
|
89
104
|
Handles two special values passed to `model`: "all", which
|
90
105
|
will model tides for all models available in `directory`, and
|
91
106
|
"ensemble", which will model tides for all models in a list
|
92
|
-
of
|
107
|
+
of ensemble models.
|
93
108
|
"""
|
94
|
-
|
95
109
|
# Turn inputs into arrays for consistent handling
|
96
|
-
models_requested =
|
110
|
+
models_requested = [str(m) for m in np.atleast_1d(model)]
|
97
111
|
|
98
112
|
# Raise error if list contains duplications
|
99
113
|
duplicates = _get_duplicates(models_requested)
|
100
114
|
if len(duplicates) > 0:
|
101
|
-
|
115
|
+
err_msg = f"The model parameter contains duplicate values: {duplicates}"
|
116
|
+
raise ValueError(err_msg)
|
102
117
|
|
103
|
-
#
|
118
|
+
# Load supported models from pyTMD database
|
104
119
|
available_models, valid_models = list_models(
|
105
|
-
directory,
|
120
|
+
directory,
|
121
|
+
show_available=False,
|
122
|
+
show_supported=False,
|
123
|
+
raise_error=True,
|
124
|
+
extra_databases=extra_databases,
|
106
125
|
)
|
107
126
|
custom_options = ["ensemble", "all"]
|
108
127
|
|
109
128
|
# Error if any models are not supported
|
110
129
|
if not all(m in valid_models + custom_options for m in models_requested):
|
111
130
|
error_text = (
|
112
|
-
f"One or more of the requested models are not valid
|
113
|
-
f"{models_requested}\n
|
114
|
-
"
|
115
|
-
|
131
|
+
f"One or more of the requested models are not valid.\n"
|
132
|
+
f"Requested models: {models_requested}\n"
|
133
|
+
f"Valid models: {valid_models}\n"
|
134
|
+
"For tide model setup instructions, refer to the guide: https://geoscienceaustralia.github.io/eo-tides/setup/"
|
116
135
|
)
|
117
|
-
raise ValueError(error_text)
|
136
|
+
raise ValueError(error_text) from None
|
118
137
|
|
119
138
|
# Error if any models are not available in `directory`
|
120
139
|
if not all(m in available_models + custom_options for m in models_requested):
|
121
140
|
error_text = (
|
122
|
-
f"One or more of the requested models are
|
123
|
-
f"{models_requested}\n
|
124
|
-
f"
|
125
|
-
|
141
|
+
f"One or more of the requested tide models are not available in `{directory}`.\n"
|
142
|
+
f"Requested models: {models_requested}\n"
|
143
|
+
f"Available models: {available_models}\n"
|
144
|
+
"For tide model setup instructions, refer to the guide: https://geoscienceaustralia.github.io/eo-tides/setup/"
|
126
145
|
)
|
127
|
-
raise ValueError(error_text)
|
146
|
+
raise ValueError(error_text) from None
|
128
147
|
|
129
148
|
# If "all" models are requested, update requested list to include available models
|
130
149
|
if "all" in models_requested:
|
@@ -177,8 +196,7 @@ def _clip_model_file(
|
|
177
196
|
ycoord: str,
|
178
197
|
xcoord: str,
|
179
198
|
) -> xr.Dataset:
|
180
|
-
"""
|
181
|
-
Clips tide model netCDF datasets to a bounding box.
|
199
|
+
"""Clips tide model netCDF datasets to a bounding box.
|
182
200
|
|
183
201
|
If the bounding box crosses 0 degrees longitude (e.g. Greenwich prime
|
184
202
|
meridian), the dataset will be clipped into two parts and concatenated
|
@@ -219,8 +237,8 @@ def _clip_model_file(
|
|
219
237
|
>>> nc = xr.open_dataset("GOT5.5/ocean_tides/2n2.nc")
|
220
238
|
>>> bbox = BoundingBox(left=108, bottom=-48, right=158, top=-6, crs='EPSG:4326')
|
221
239
|
>>> clipped_nc = _clip_model_file(nc, bbox, xdim="lon", ydim="lat", ycoord="latitude", xcoord="longitude")
|
222
|
-
"""
|
223
240
|
|
241
|
+
"""
|
224
242
|
# Extract x and y coords from xarray and load into memory
|
225
243
|
xcoords = nc[xcoord].compute()
|
226
244
|
ycoords = nc[ycoord].compute()
|
@@ -265,12 +283,12 @@ def _clip_model_file(
|
|
265
283
|
# Combine left and right data along x dimension
|
266
284
|
nc_clipped = xr.concat([nc_left, nc_right], dim=xdim)
|
267
285
|
|
268
|
-
#
|
286
|
+
# Temporary fix to remove expanded x dim on lat variables issue
|
269
287
|
# for TPXO data; remove x dim by selecting the first obs
|
270
288
|
for i in ["lat_z", "lat_v", "lat_u", "con"]:
|
271
289
|
try:
|
272
290
|
nc_clipped[i] = nc_clipped[i].isel(nx=0)
|
273
|
-
except KeyError:
|
291
|
+
except KeyError: # noqa: PERF203
|
274
292
|
pass
|
275
293
|
|
276
294
|
return nc_clipped
|
@@ -283,9 +301,8 @@ def clip_models(
|
|
283
301
|
model: list | None = None,
|
284
302
|
buffer: float = 5,
|
285
303
|
overwrite: bool = False,
|
286
|
-
):
|
287
|
-
"""
|
288
|
-
Clip NetCDF-format ocean tide models to a bounding box.
|
304
|
+
) -> None:
|
305
|
+
"""Clip NetCDF-format ocean tide models to a bounding box.
|
289
306
|
|
290
307
|
This function identifies all NetCDF-format tide models in a
|
291
308
|
given input directory, including "ATLAS-netcdf" (e.g. TPXO9-atlas-nc),
|
@@ -296,8 +313,8 @@ def clip_models(
|
|
296
313
|
directory and verified with `pyTMD` to ensure the clipped data is
|
297
314
|
suitable for tide modelling.
|
298
315
|
|
299
|
-
For
|
300
|
-
|
316
|
+
For tide model setup instructions, refer to the guide:
|
317
|
+
https://geoscienceaustralia.github.io/eo-tides/setup/
|
301
318
|
|
302
319
|
Parameters
|
303
320
|
----------
|
@@ -326,8 +343,8 @@ def clip_models(
|
|
326
343
|
... output_directory="tide_models_clipped/",
|
327
344
|
... bbox=(-8.968392, 50.070574, 2.447160, 59.367122),
|
328
345
|
... )
|
329
|
-
"""
|
330
346
|
|
347
|
+
"""
|
331
348
|
# Get input and output paths
|
332
349
|
input_directory = _set_directory(input_directory)
|
333
350
|
output_directory = pathlib.Path(output_directory)
|
@@ -347,7 +364,8 @@ def clip_models(
|
|
347
364
|
|
348
365
|
# Raise error if no valid models found
|
349
366
|
if len(available_netcdf_models) == 0:
|
350
|
-
|
367
|
+
err_msg = f"No valid NetCDF models found in {input_directory}."
|
368
|
+
raise ValueError(err_msg)
|
351
369
|
|
352
370
|
# If model list is provided,
|
353
371
|
print(f"Preparing to clip suitable NetCDF models: {available_netcdf_models}\n")
|
@@ -426,15 +444,18 @@ def clip_models(
|
|
426
444
|
)
|
427
445
|
|
428
446
|
else:
|
429
|
-
|
447
|
+
err_msg = f"Model {m} not supported"
|
448
|
+
raise Exception(err_msg)
|
430
449
|
|
431
450
|
# Create directory and export
|
432
451
|
(output_directory / file).parent.mkdir(parents=True, exist_ok=True)
|
433
452
|
nc_clipped.to_netcdf(output_directory / file, mode="w")
|
434
453
|
|
435
454
|
# Verify that models are ready
|
436
|
-
|
437
|
-
|
455
|
+
if pyTMD.io.model(directory=output_directory).elevation(m=m).verify:
|
456
|
+
print(" ✅ Clipped model exported and verified")
|
457
|
+
else:
|
458
|
+
print(" ❌ Clipped model exported but unable to be verified")
|
438
459
|
|
439
460
|
print(f"\nOutputs exported to {output_directory}")
|
440
461
|
list_models(directory=output_directory, show_available=True, show_supported=False)
|
@@ -445,17 +466,17 @@ def list_models(
|
|
445
466
|
show_available: bool = True,
|
446
467
|
show_supported: bool = True,
|
447
468
|
raise_error: bool = False,
|
469
|
+
extra_databases: str | os.PathLike | list | None = None,
|
448
470
|
) -> tuple[list[str], list[str]]:
|
449
|
-
"""
|
450
|
-
List all tide models available for tide modelling.
|
471
|
+
"""List all tide models available for tide modelling.
|
451
472
|
|
452
473
|
This function scans the specified tide model directory
|
453
474
|
and returns a list of models that are available in the
|
454
475
|
directory as well as the full list of all models supported
|
455
476
|
by `eo-tides` and `pyTMD`.
|
456
477
|
|
457
|
-
For
|
458
|
-
|
478
|
+
For tide model setup instructions, refer to the guide:
|
479
|
+
https://geoscienceaustralia.github.io/eo-tides/setup/
|
459
480
|
|
460
481
|
Parameters
|
461
482
|
----------
|
@@ -474,6 +495,11 @@ def list_models(
|
|
474
495
|
raise_error : bool, optional
|
475
496
|
If True, raise an error if no available models are found.
|
476
497
|
If False, raise a warning.
|
498
|
+
extra_databases : str or path or list, optional
|
499
|
+
Additional custom tide model definitions to load, provided as
|
500
|
+
dictionaries or paths to JSON database files. Use this to
|
501
|
+
enable custom tide models not included with `pyTMD`.
|
502
|
+
See: https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#model-database
|
477
503
|
|
478
504
|
Returns
|
479
505
|
-------
|
@@ -481,6 +507,7 @@ def list_models(
|
|
481
507
|
A list of all tide models available within `directory`.
|
482
508
|
supported_models : list of str
|
483
509
|
A list of all tide models supported by `eo-tides`.
|
510
|
+
|
484
511
|
"""
|
485
512
|
init() # Initialize colorama
|
486
513
|
|
@@ -488,8 +515,11 @@ def list_models(
|
|
488
515
|
# provided, try global environment variable.
|
489
516
|
directory = _set_directory(directory)
|
490
517
|
|
491
|
-
#
|
492
|
-
|
518
|
+
# Load supported models from pyTMD database, adding extras if required
|
519
|
+
extra_databases = [] if extra_databases is None else extra_databases
|
520
|
+
model_database = load_database(extra_databases=extra_databases)["elevation"]
|
521
|
+
|
522
|
+
# Get full list of supported models
|
493
523
|
supported_models = list(model_database.keys())
|
494
524
|
|
495
525
|
# Extract expected model paths
|
@@ -499,11 +529,11 @@ def list_models(
|
|
499
529
|
|
500
530
|
# Handle GOT5.6 differently to ensure we test for presence of GOT5.6 constituents
|
501
531
|
if m in ("GOT5.6", "GOT5.6_extrapolated"):
|
502
|
-
model_file =
|
532
|
+
model_file = next(file for file in model_file if "GOT5.6" in file)
|
503
533
|
else:
|
504
534
|
model_file = model_file[0] if isinstance(model_file, list) else model_file
|
505
535
|
|
506
|
-
# Add path to dict
|
536
|
+
# Add expected path to dict, adding directory prefix
|
507
537
|
expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)
|
508
538
|
|
509
539
|
# Define column widths
|
@@ -522,19 +552,22 @@ def list_models(
|
|
522
552
|
available_models = []
|
523
553
|
for m in supported_models:
|
524
554
|
try:
|
525
|
-
|
555
|
+
# Load model
|
556
|
+
model_file = pyTMD.io.model(directory=directory, extra_databases=extra_databases).elevation(m=m)
|
557
|
+
|
558
|
+
# Append model to list of available model
|
526
559
|
available_models.append(m)
|
527
560
|
|
528
561
|
if show_available:
|
529
562
|
# Mark available models with a green tick
|
530
563
|
status = "✅"
|
531
564
|
print(f"{status:^{status_width}}│ {m:<{name_width}} │ {expected_paths[m]:<{path_width}}")
|
532
|
-
except FileNotFoundError:
|
565
|
+
except FileNotFoundError: # noqa: PERF203
|
533
566
|
if show_supported:
|
534
567
|
# Mark unavailable models with a red cross
|
535
568
|
status = "❌"
|
536
569
|
print(
|
537
|
-
f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}"
|
570
|
+
f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}",
|
538
571
|
)
|
539
572
|
|
540
573
|
if show_available or show_supported:
|
@@ -548,16 +581,15 @@ def list_models(
|
|
548
581
|
if not available_models:
|
549
582
|
warning_msg = textwrap.dedent(
|
550
583
|
f"""
|
551
|
-
No valid tide models
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
"""
|
584
|
+
No valid tide models were found in `{directory}`.
|
585
|
+
Please ensure that the path you provided is correct, or set the `EO_TIDES_TIDE_MODELS` environment variable to point to a valid tide model directory.
|
586
|
+
For tide model setup instructions, refer to the guide: https://geoscienceaustralia.github.io/eo-tides/setup/
|
587
|
+
""",
|
556
588
|
).strip()
|
557
589
|
|
558
590
|
if raise_error:
|
559
|
-
raise Exception(warning_msg)
|
560
|
-
warnings.warn(warning_msg, UserWarning)
|
591
|
+
raise Exception(warning_msg) from None
|
592
|
+
warnings.warn(warning_msg, UserWarning, stacklevel=2)
|
561
593
|
|
562
594
|
# Return list of available and supported models
|
563
595
|
return available_models, supported_models
|
@@ -649,18 +681,22 @@ def idw(
|
|
649
681
|
|
650
682
|
# Verify input and outputs have matching lengths
|
651
683
|
if not (input_z.shape[0] == len(input_x) == len(input_y)):
|
652
|
-
|
653
|
-
|
654
|
-
|
684
|
+
err_msg = "All of `input_z`, `input_x` and `input_y` must be the same length."
|
685
|
+
raise ValueError(err_msg)
|
686
|
+
if len(output_x) != len(output_y):
|
687
|
+
err_msg = "Both `output_x` and `output_y` must be the same length."
|
688
|
+
raise ValueError(err_msg)
|
655
689
|
|
656
690
|
# Verify k is smaller than total number of points, and non-zero
|
657
691
|
if k > input_z.shape[0]:
|
658
|
-
|
692
|
+
err_msg = (
|
659
693
|
f"The requested number of nearest neighbours (`k={k}`) "
|
660
694
|
f"is smaller than the total number of points ({input_z.shape[0]}).",
|
661
695
|
)
|
696
|
+
raise ValueError(err_msg)
|
662
697
|
if k == 0:
|
663
|
-
|
698
|
+
err_msg = "Interpolation based on `k=0` nearest neighbours is not valid."
|
699
|
+
raise ValueError(err_msg)
|
664
700
|
|
665
701
|
# Create KDTree to efficiently find nearest neighbours
|
666
702
|
points_xy = np.column_stack((input_y, input_x))
|