eo-tides 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eo_tides/__init__.py +4 -3
- eo_tides/eo.py +2 -4
- eo_tides/model.py +211 -318
- eo_tides/stats.py +2 -4
- eo_tides/utils.py +453 -1
- eo_tides/validation.py +5 -5
- {eo_tides-0.2.0.dist-info → eo_tides-0.3.0.dist-info}/METADATA +12 -8
- eo_tides-0.3.0.dist-info/RECORD +11 -0
- eo_tides-0.2.0.dist-info/RECORD +0 -11
- {eo_tides-0.2.0.dist-info → eo_tides-0.3.0.dist-info}/LICENSE +0 -0
- {eo_tides-0.2.0.dist-info → eo_tides-0.3.0.dist-info}/WHEEL +0 -0
- {eo_tides-0.2.0.dist-info → eo_tides-0.3.0.dist-info}/top_level.txt +0 -0
eo_tides/__init__.py
CHANGED
@@ -28,9 +28,9 @@ validation : Load observed tide gauge data to validate modelled tides
|
|
28
28
|
|
29
29
|
# Import commonly used functions for convenience
|
30
30
|
from .eo import pixel_tides, tag_tides
|
31
|
-
from .model import
|
31
|
+
from .model import model_phases, model_tides
|
32
32
|
from .stats import pixel_stats, tide_stats
|
33
|
-
from .utils import idw
|
33
|
+
from .utils import clip_models, idw, list_models
|
34
34
|
from .validation import eval_metrics, load_gauge_gesla
|
35
35
|
|
36
36
|
# Define what should be imported with "from eo_tides import *"
|
@@ -42,7 +42,8 @@ __all__ = [
|
|
42
42
|
"pixel_tides",
|
43
43
|
"tide_stats",
|
44
44
|
"pixel_stats",
|
45
|
-
"idw",
|
46
45
|
"eval_metrics",
|
47
46
|
"load_gauge_gesla",
|
47
|
+
"clip_models",
|
48
|
+
"idw",
|
48
49
|
]
|
eo_tides/eo.py
CHANGED
@@ -8,17 +8,15 @@ from typing import TYPE_CHECKING
|
|
8
8
|
|
9
9
|
import numpy as np
|
10
10
|
import odc.geo.xr
|
11
|
-
import pandas as pd
|
12
11
|
import xarray as xr
|
13
12
|
from odc.geo.geobox import GeoBox
|
14
13
|
|
15
14
|
# Only import if running type checking
|
16
15
|
if TYPE_CHECKING:
|
17
|
-
import datetime
|
18
|
-
|
19
16
|
from odc.geo import Shape2d
|
20
17
|
|
21
|
-
from .model import
|
18
|
+
from .model import model_tides
|
19
|
+
from .utils import DatetimeLike, _standardise_time
|
22
20
|
|
23
21
|
|
24
22
|
def _resample_chunks(
|
eo_tides/model.py
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
# Used to postpone evaluation of type annotations
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
|
-
import datetime
|
5
4
|
import os
|
6
|
-
import pathlib
|
7
5
|
import textwrap
|
8
|
-
import warnings
|
9
6
|
from concurrent.futures import ProcessPoolExecutor
|
10
7
|
from concurrent.futures.process import BrokenProcessPool
|
11
8
|
from functools import partial
|
12
|
-
from typing import TYPE_CHECKING
|
9
|
+
from typing import TYPE_CHECKING
|
10
|
+
|
11
|
+
import psutil
|
13
12
|
|
14
13
|
# Only import if running type checking
|
15
14
|
if TYPE_CHECKING:
|
@@ -20,309 +19,9 @@ import numpy as np
|
|
20
19
|
import pandas as pd
|
21
20
|
import pyproj
|
22
21
|
import pyTMD
|
23
|
-
from colorama import Style, init
|
24
|
-
from pyTMD.io.model import load_database, model
|
25
22
|
from tqdm import tqdm
|
26
23
|
|
27
|
-
from .utils import idw
|
28
|
-
|
29
|
-
# Type alias for all possible inputs to "time" params
|
30
|
-
DatetimeLike = Union[np.ndarray, pd.DatetimeIndex, pd.Timestamp, datetime.datetime, str, List[str]]
|
31
|
-
|
32
|
-
|
33
|
-
def _set_directory(
|
34
|
-
directory: str | os.PathLike | None = None,
|
35
|
-
) -> os.PathLike:
|
36
|
-
"""
|
37
|
-
Set tide modelling files directory. If no custom
|
38
|
-
path is provided, try global environmental variable
|
39
|
-
instead.
|
40
|
-
"""
|
41
|
-
if directory is None:
|
42
|
-
if "EO_TIDES_TIDE_MODELS" in os.environ:
|
43
|
-
directory = os.environ["EO_TIDES_TIDE_MODELS"]
|
44
|
-
else:
|
45
|
-
raise Exception(
|
46
|
-
"No tide model directory provided via `directory`, and/or no "
|
47
|
-
"`EO_TIDES_TIDE_MODELS` environment variable found. "
|
48
|
-
"Please provide a valid path to your tide model directory."
|
49
|
-
)
|
50
|
-
|
51
|
-
# Verify path exists
|
52
|
-
directory = pathlib.Path(directory).expanduser()
|
53
|
-
if not directory.exists():
|
54
|
-
raise FileNotFoundError(f"No valid tide model directory found at path `{directory}`")
|
55
|
-
else:
|
56
|
-
return directory
|
57
|
-
|
58
|
-
|
59
|
-
def _standardise_time(
|
60
|
-
time: DatetimeLike | None,
|
61
|
-
) -> np.ndarray | None:
|
62
|
-
"""
|
63
|
-
Accept any time format accepted by `pd.to_datetime`,
|
64
|
-
and return a datetime64 ndarray. Return None if None
|
65
|
-
passed.
|
66
|
-
"""
|
67
|
-
# Return time as-is if None
|
68
|
-
if time is None:
|
69
|
-
return None
|
70
|
-
|
71
|
-
# Use pd.to_datetime for conversion, then convert to numpy array
|
72
|
-
time = pd.to_datetime(time).to_numpy().astype("datetime64[ns]")
|
73
|
-
|
74
|
-
# Ensure that data has at least one dimension
|
75
|
-
return np.atleast_1d(time)
|
76
|
-
|
77
|
-
|
78
|
-
def list_models(
|
79
|
-
directory: str | os.PathLike | None = None,
|
80
|
-
show_available: bool = True,
|
81
|
-
show_supported: bool = True,
|
82
|
-
raise_error: bool = False,
|
83
|
-
) -> tuple[list[str], list[str]]:
|
84
|
-
"""
|
85
|
-
List all tide models available for tide modelling.
|
86
|
-
|
87
|
-
This function scans the specified tide model directory
|
88
|
-
and returns a list of models that are available in the
|
89
|
-
directory as well as the full list of all models supported
|
90
|
-
by `eo-tides` and `pyTMD`.
|
91
|
-
|
92
|
-
For instructions on setting up tide models, see:
|
93
|
-
<https://geoscienceaustralia.github.io/eo-tides/setup/>
|
94
|
-
|
95
|
-
Parameters
|
96
|
-
----------
|
97
|
-
directory : str, optional
|
98
|
-
The directory containing tide model data files. If no path is
|
99
|
-
provided, this will default to the environment variable
|
100
|
-
`EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
|
101
|
-
Tide modelling files should be stored in sub-folders for each
|
102
|
-
model that match the structure required by `pyTMD`
|
103
|
-
(<https://geoscienceaustralia.github.io/eo-tides/setup/>).
|
104
|
-
show_available : bool, optional
|
105
|
-
Whether to print a list of locally available models.
|
106
|
-
show_supported : bool, optional
|
107
|
-
Whether to print a list of all supported models, in
|
108
|
-
addition to models available locally.
|
109
|
-
raise_error : bool, optional
|
110
|
-
If True, raise an error if no available models are found.
|
111
|
-
If False, raise a warning.
|
112
|
-
|
113
|
-
Returns
|
114
|
-
-------
|
115
|
-
available_models : list of str
|
116
|
-
A list of all tide models available within `directory`.
|
117
|
-
supported_models : list of str
|
118
|
-
A list of all tide models supported by `eo-tides`.
|
119
|
-
"""
|
120
|
-
init() # Initialize colorama
|
121
|
-
|
122
|
-
# Set tide modelling files directory. If no custom path is
|
123
|
-
# provided, try global environment variable.
|
124
|
-
directory = _set_directory(directory)
|
125
|
-
|
126
|
-
# Get full list of supported models from pyTMD database
|
127
|
-
model_database = load_database()["elevation"]
|
128
|
-
supported_models = list(model_database.keys())
|
129
|
-
|
130
|
-
# Extract expected model paths
|
131
|
-
expected_paths = {}
|
132
|
-
for m in supported_models:
|
133
|
-
model_file = model_database[m]["model_file"]
|
134
|
-
model_file = model_file[0] if isinstance(model_file, list) else model_file
|
135
|
-
expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)
|
136
|
-
|
137
|
-
# Define column widths
|
138
|
-
status_width = 4 # Width for emoji
|
139
|
-
name_width = max(len(name) for name in supported_models)
|
140
|
-
path_width = max(len(path) for path in expected_paths.values())
|
141
|
-
|
142
|
-
# Print list of supported models, marking available and
|
143
|
-
# unavailable models and appending available to list
|
144
|
-
if show_available or show_supported:
|
145
|
-
total_width = min(status_width + name_width + path_width + 6, 80)
|
146
|
-
print("─" * total_width)
|
147
|
-
print(f"{'🌊':^{status_width}} | {'Model':<{name_width}} | {'Expected path':<{path_width}}")
|
148
|
-
print("─" * total_width)
|
149
|
-
|
150
|
-
available_models = []
|
151
|
-
for m in supported_models:
|
152
|
-
try:
|
153
|
-
model_file = model(directory=directory).elevation(m=m)
|
154
|
-
available_models.append(m)
|
155
|
-
|
156
|
-
if show_available:
|
157
|
-
# Mark available models with a green tick
|
158
|
-
status = "✅"
|
159
|
-
print(f"{status:^{status_width}}│ {m:<{name_width}} │ {expected_paths[m]:<{path_width}}")
|
160
|
-
except FileNotFoundError:
|
161
|
-
if show_supported:
|
162
|
-
# Mark unavailable models with a red cross
|
163
|
-
status = "❌"
|
164
|
-
print(
|
165
|
-
f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}"
|
166
|
-
)
|
167
|
-
|
168
|
-
if show_available or show_supported:
|
169
|
-
print("─" * total_width)
|
170
|
-
|
171
|
-
# Print summary
|
172
|
-
print(f"\n{Style.BRIGHT}Summary:{Style.RESET_ALL}")
|
173
|
-
print(f"Available models: {len(available_models)}/{len(supported_models)}")
|
174
|
-
|
175
|
-
# Raise error or warning if no models are available
|
176
|
-
if not available_models:
|
177
|
-
warning_msg = textwrap.dedent(
|
178
|
-
f"""
|
179
|
-
No valid tide models are available in `{directory}`.
|
180
|
-
Are you sure you have provided the correct `directory` path, or set the
|
181
|
-
`EO_TIDES_TIDE_MODELS` environment variable to point to the location of your
|
182
|
-
tide model directory?
|
183
|
-
"""
|
184
|
-
).strip()
|
185
|
-
|
186
|
-
if raise_error:
|
187
|
-
raise Exception(warning_msg)
|
188
|
-
else:
|
189
|
-
warnings.warn(warning_msg, UserWarning)
|
190
|
-
|
191
|
-
# Return list of available and supported models
|
192
|
-
return available_models, supported_models
|
193
|
-
|
194
|
-
|
195
|
-
def _model_tides(
|
196
|
-
model,
|
197
|
-
x,
|
198
|
-
y,
|
199
|
-
time,
|
200
|
-
directory,
|
201
|
-
crs,
|
202
|
-
crop,
|
203
|
-
method,
|
204
|
-
extrapolate,
|
205
|
-
cutoff,
|
206
|
-
output_units,
|
207
|
-
mode,
|
208
|
-
):
|
209
|
-
"""Worker function applied in parallel by `model_tides`. Handles the
|
210
|
-
extraction of tide modelling constituents and tide modelling using
|
211
|
-
`pyTMD`.
|
212
|
-
"""
|
213
|
-
# Obtain model details
|
214
|
-
pytmd_model = pyTMD.io.model(directory).elevation(model)
|
215
|
-
|
216
|
-
# Reproject x, y to latitude/longitude
|
217
|
-
transformer = pyproj.Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
|
218
|
-
lon, lat = transformer.transform(x.flatten(), y.flatten())
|
219
|
-
|
220
|
-
# Convert datetime
|
221
|
-
timescale = pyTMD.time.timescale().from_datetime(time.flatten())
|
222
|
-
|
223
|
-
try:
|
224
|
-
# Read tidal constants and interpolate to grid points
|
225
|
-
amp, ph, c = pytmd_model.extract_constants(
|
226
|
-
lon,
|
227
|
-
lat,
|
228
|
-
type=pytmd_model.type,
|
229
|
-
crop=crop,
|
230
|
-
bounds=None,
|
231
|
-
method=method,
|
232
|
-
extrapolate=extrapolate,
|
233
|
-
cutoff=cutoff,
|
234
|
-
append_node=False,
|
235
|
-
# append_node=True,
|
236
|
-
)
|
237
|
-
|
238
|
-
# TODO: Return constituents
|
239
|
-
# print(amp.shape, ph.shape, c)
|
240
|
-
# print(pd.DataFrame({"amplitude": amp}))
|
241
|
-
|
242
|
-
# Raise error if constituent files no not cover analysis extent
|
243
|
-
except IndexError as e:
|
244
|
-
error_msg = f"""
|
245
|
-
The {model} tide model constituent files do not cover the requested analysis extent.
|
246
|
-
This can occur if you are using clipped model files to improve run times.
|
247
|
-
Consider using model files that cover your entire analysis area, or set `crop=False`
|
248
|
-
to reduce the extent of tide model constituent files that is loaded.
|
249
|
-
"""
|
250
|
-
raise Exception(textwrap.dedent(error_msg).strip()) from None
|
251
|
-
|
252
|
-
# Calculate complex phase in radians for Euler's
|
253
|
-
cph = -1j * ph * np.pi / 180.0
|
254
|
-
|
255
|
-
# Calculate constituent oscillation
|
256
|
-
hc = amp * np.exp(cph)
|
257
|
-
|
258
|
-
# Compute deltat based on model
|
259
|
-
if pytmd_model.corrections in ("OTIS", "ATLAS", "TMD3", "netcdf"):
|
260
|
-
# Use delta time at 2000.0 to match TMD outputs
|
261
|
-
deltat = np.zeros_like(timescale.tt_ut1)
|
262
|
-
else:
|
263
|
-
# Use interpolated delta times
|
264
|
-
deltat = timescale.tt_ut1
|
265
|
-
|
266
|
-
# Determine the number of points and times to process. If in
|
267
|
-
# "one-to-many" mode, these counts are used to repeat our extracted
|
268
|
-
# constituents and timesteps so we can extract tides for all
|
269
|
-
# combinations of our input times and tide modelling points.
|
270
|
-
# If in "one-to-many" mode, repeat constituents to length of time
|
271
|
-
# and number of input coords before passing to `predict_tide_drift`
|
272
|
-
# If in "one-to-one" mode, we avoid this step by setting counts to 1
|
273
|
-
# (e.g. "repeat 1 times")
|
274
|
-
points_repeat = len(x) if mode == "one-to-many" else 1
|
275
|
-
time_repeat = len(time) if mode == "one-to-many" else 1
|
276
|
-
t, hc, deltat = (
|
277
|
-
np.tile(timescale.tide, points_repeat),
|
278
|
-
hc.repeat(time_repeat, axis=0),
|
279
|
-
np.tile(deltat, points_repeat),
|
280
|
-
)
|
281
|
-
|
282
|
-
# Create arrays to hold outputs
|
283
|
-
tide = np.ma.zeros((len(t)), fill_value=np.nan)
|
284
|
-
tide.mask = np.any(hc.mask, axis=1)
|
285
|
-
|
286
|
-
# Predict tidal elevations at time and infer minor corrections
|
287
|
-
tide.data[:] = pyTMD.predict.drift(
|
288
|
-
t,
|
289
|
-
hc,
|
290
|
-
c,
|
291
|
-
deltat=deltat,
|
292
|
-
corrections=pytmd_model.corrections,
|
293
|
-
)
|
294
|
-
minor = pyTMD.predict.infer_minor(
|
295
|
-
t,
|
296
|
-
hc,
|
297
|
-
c,
|
298
|
-
deltat=deltat,
|
299
|
-
corrections=pytmd_model.corrections,
|
300
|
-
minor=pytmd_model.minor,
|
301
|
-
)
|
302
|
-
tide.data[:] += minor.data[:]
|
303
|
-
|
304
|
-
# Replace invalid values with fill value
|
305
|
-
tide.data[tide.mask] = tide.fill_value
|
306
|
-
|
307
|
-
# Convert data to pandas.DataFrame, and set index to our input
|
308
|
-
# time/x/y values
|
309
|
-
tide_df = pd.DataFrame({
|
310
|
-
"time": np.tile(time, points_repeat),
|
311
|
-
"x": np.repeat(x, time_repeat),
|
312
|
-
"y": np.repeat(y, time_repeat),
|
313
|
-
"tide_model": model,
|
314
|
-
"tide_height": tide,
|
315
|
-
}).set_index(["time", "x", "y"])
|
316
|
-
|
317
|
-
# Optionally convert outputs to integer units (can save memory)
|
318
|
-
if output_units == "m":
|
319
|
-
tide_df["tide_height"] = tide_df.tide_height.astype(np.float32)
|
320
|
-
elif output_units == "cm":
|
321
|
-
tide_df["tide_height"] = (tide_df.tide_height * 100).astype(np.int16)
|
322
|
-
elif output_units == "mm":
|
323
|
-
tide_df["tide_height"] = (tide_df.tide_height * 1000).astype(np.int16)
|
324
|
-
|
325
|
-
return tide_df
|
24
|
+
from .utils import DatetimeLike, _set_directory, _standardise_time, idw, list_models
|
326
25
|
|
327
26
|
|
328
27
|
def _ensemble_model(
|
@@ -490,6 +189,180 @@ def _ensemble_model(
|
|
490
189
|
return pd.concat(ensemble_list)
|
491
190
|
|
492
191
|
|
192
|
+
def _parallel_splits(
|
193
|
+
total_points: int,
|
194
|
+
model_count: int,
|
195
|
+
parallel_max: int | None = None,
|
196
|
+
min_points_per_split: int = 1000,
|
197
|
+
) -> int:
|
198
|
+
"""
|
199
|
+
Calculates the optimal number of parallel splits for data
|
200
|
+
processing based on system resources and processing constraints.
|
201
|
+
|
202
|
+
Parameters:
|
203
|
+
-----------
|
204
|
+
total_points : int
|
205
|
+
Total number of data points to process
|
206
|
+
model_count : int
|
207
|
+
Number of models that will be run in parallel
|
208
|
+
parallel_max : int, optional
|
209
|
+
Maximum number of parallel processes to use. If None, uses CPU core count
|
210
|
+
min_points_per_split : int, default=1000
|
211
|
+
Minimum number of points that should be processed in each split
|
212
|
+
"""
|
213
|
+
# Get available CPUs. First see if `CPU_GUARANTEE` exists in
|
214
|
+
# environment (if running in JupyterHub); if not use psutil
|
215
|
+
# followed by standard CPU count
|
216
|
+
if parallel_max is None:
|
217
|
+
# Take the first valid output
|
218
|
+
raw_value = os.environ.get("CPU_GUARANTEE") or psutil.cpu_count(logical=False) or os.cpu_count() or 1
|
219
|
+
|
220
|
+
# Convert to integer
|
221
|
+
if isinstance(raw_value, str):
|
222
|
+
parallel_max = int(float(raw_value))
|
223
|
+
else:
|
224
|
+
parallel_max = int(raw_value)
|
225
|
+
|
226
|
+
# Calculate optimal number of splits based on constraints
|
227
|
+
splits_by_size = total_points / min_points_per_split
|
228
|
+
splits_by_cpu = parallel_max / model_count
|
229
|
+
optimal_splits = min(splits_by_size, splits_by_cpu)
|
230
|
+
|
231
|
+
# Convert to integer and ensure at least 1 split
|
232
|
+
final_split_count = int(max(1, optimal_splits))
|
233
|
+
return final_split_count
|
234
|
+
|
235
|
+
|
236
|
+
def _model_tides(
|
237
|
+
model,
|
238
|
+
x,
|
239
|
+
y,
|
240
|
+
time,
|
241
|
+
directory,
|
242
|
+
crs,
|
243
|
+
crop,
|
244
|
+
method,
|
245
|
+
extrapolate,
|
246
|
+
cutoff,
|
247
|
+
output_units,
|
248
|
+
mode,
|
249
|
+
):
|
250
|
+
"""Worker function applied in parallel by `model_tides`. Handles the
|
251
|
+
extraction of tide modelling constituents and tide modelling using
|
252
|
+
`pyTMD`.
|
253
|
+
"""
|
254
|
+
# Obtain model details
|
255
|
+
pytmd_model = pyTMD.io.model(directory).elevation(model)
|
256
|
+
|
257
|
+
# Reproject x, y to latitude/longitude
|
258
|
+
transformer = pyproj.Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
|
259
|
+
lon, lat = transformer.transform(x.flatten(), y.flatten())
|
260
|
+
|
261
|
+
# Convert datetime
|
262
|
+
timescale = pyTMD.time.timescale().from_datetime(time.flatten())
|
263
|
+
|
264
|
+
try:
|
265
|
+
# Read tidal constants and interpolate to grid points
|
266
|
+
amp, ph, c = pytmd_model.extract_constants(
|
267
|
+
lon,
|
268
|
+
lat,
|
269
|
+
type=pytmd_model.type,
|
270
|
+
crop=crop,
|
271
|
+
method=method,
|
272
|
+
extrapolate=extrapolate,
|
273
|
+
cutoff=cutoff,
|
274
|
+
append_node=False,
|
275
|
+
# append_node=True,
|
276
|
+
)
|
277
|
+
|
278
|
+
# TODO: Return constituents
|
279
|
+
# print(amp.shape, ph.shape, c)
|
280
|
+
# print(pd.DataFrame({"amplitude": amp}))
|
281
|
+
|
282
|
+
# Raise error if constituent files no not cover analysis extent
|
283
|
+
except IndexError:
|
284
|
+
error_msg = f"""
|
285
|
+
The {model} tide model constituent files do not cover the analysis extent
|
286
|
+
({min(lon):.2f}, {max(lon):.2f}, {min(lat):.2f}, {max(lat):.2f}).
|
287
|
+
This can occur if you are using clipped model files to improve run times.
|
288
|
+
Consider using model files that cover your entire analysis area, or set `crop=False`
|
289
|
+
to reduce the extent of tide model constituent files that is loaded.
|
290
|
+
"""
|
291
|
+
raise Exception(textwrap.dedent(error_msg).strip()) from None
|
292
|
+
|
293
|
+
# Calculate complex phase in radians for Euler's
|
294
|
+
cph = -1j * ph * np.pi / 180.0
|
295
|
+
|
296
|
+
# Calculate constituent oscillation
|
297
|
+
hc = amp * np.exp(cph)
|
298
|
+
|
299
|
+
# Compute delta times based on model
|
300
|
+
if pytmd_model.corrections in ("OTIS", "ATLAS", "TMD3", "netcdf"):
|
301
|
+
# Use delta time at 2000.0 to match TMD outputs
|
302
|
+
deltat = np.zeros_like(timescale.tt_ut1)
|
303
|
+
else:
|
304
|
+
# Use interpolated delta times
|
305
|
+
deltat = timescale.tt_ut1
|
306
|
+
|
307
|
+
# In "one-to-many" mode, extracted tidal constituents and timesteps
|
308
|
+
# are repeated/multiplied out to match the number of input points and
|
309
|
+
# timesteps, enabling the modeling of tides across all combinations
|
310
|
+
# of input times and points. In "one-to-one" mode, no repetition is
|
311
|
+
# needed, so each repeat count is set to 1.
|
312
|
+
points_repeat = len(x) if mode == "one-to-many" else 1
|
313
|
+
time_repeat = len(time) if mode == "one-to-many" else 1
|
314
|
+
t, hc, deltat = (
|
315
|
+
np.tile(timescale.tide, points_repeat),
|
316
|
+
hc.repeat(time_repeat, axis=0),
|
317
|
+
np.tile(deltat, points_repeat),
|
318
|
+
)
|
319
|
+
|
320
|
+
# Create arrays to hold outputs
|
321
|
+
tide = np.ma.zeros((len(t)), fill_value=np.nan)
|
322
|
+
tide.mask = np.any(hc.mask, axis=1)
|
323
|
+
|
324
|
+
# Predict tidal elevations at time and infer minor corrections
|
325
|
+
tide.data[:] = pyTMD.predict.drift(
|
326
|
+
t,
|
327
|
+
hc,
|
328
|
+
c,
|
329
|
+
deltat=deltat,
|
330
|
+
corrections=pytmd_model.corrections,
|
331
|
+
)
|
332
|
+
minor = pyTMD.predict.infer_minor(
|
333
|
+
t,
|
334
|
+
hc,
|
335
|
+
c,
|
336
|
+
deltat=deltat,
|
337
|
+
corrections=pytmd_model.corrections,
|
338
|
+
minor=pytmd_model.minor,
|
339
|
+
)
|
340
|
+
tide.data[:] += minor.data[:]
|
341
|
+
|
342
|
+
# Replace invalid values with fill value
|
343
|
+
tide.data[tide.mask] = tide.fill_value
|
344
|
+
|
345
|
+
# Convert data to pandas.DataFrame, and set index to our input
|
346
|
+
# time/x/y values
|
347
|
+
tide_df = pd.DataFrame({
|
348
|
+
"time": np.tile(time, points_repeat),
|
349
|
+
"x": np.repeat(x, time_repeat),
|
350
|
+
"y": np.repeat(y, time_repeat),
|
351
|
+
"tide_model": model,
|
352
|
+
"tide_height": tide,
|
353
|
+
}).set_index(["time", "x", "y"])
|
354
|
+
|
355
|
+
# Optionally convert outputs to integer units (can save memory)
|
356
|
+
if output_units == "m":
|
357
|
+
tide_df["tide_height"] = tide_df.tide_height.astype(np.float32)
|
358
|
+
elif output_units == "cm":
|
359
|
+
tide_df["tide_height"] = (tide_df.tide_height * 100).astype(np.int16)
|
360
|
+
elif output_units == "mm":
|
361
|
+
tide_df["tide_height"] = (tide_df.tide_height * 1000).astype(np.int16)
|
362
|
+
|
363
|
+
return tide_df
|
364
|
+
|
365
|
+
|
493
366
|
def model_tides(
|
494
367
|
x: float | list[float] | xr.DataArray,
|
495
368
|
y: float | list[float] | xr.DataArray,
|
@@ -498,12 +371,13 @@ def model_tides(
|
|
498
371
|
directory: str | os.PathLike | None = None,
|
499
372
|
crs: str = "EPSG:4326",
|
500
373
|
crop: bool = True,
|
501
|
-
method: str = "
|
374
|
+
method: str = "linear",
|
502
375
|
extrapolate: bool = True,
|
503
376
|
cutoff: float | None = None,
|
504
377
|
mode: str = "one-to-many",
|
505
378
|
parallel: bool = True,
|
506
|
-
parallel_splits: int =
|
379
|
+
parallel_splits: int | str = "auto",
|
380
|
+
parallel_max: int | None = None,
|
507
381
|
output_units: str = "m",
|
508
382
|
output_format: str = "long",
|
509
383
|
ensemble_models: list[str] | None = None,
|
@@ -564,11 +438,11 @@ def model_tides(
|
|
564
438
|
1 degree buffer around all input points. Defaults to True.
|
565
439
|
method : str, optional
|
566
440
|
Method used to interpolate tidal constituents
|
567
|
-
from model files.
|
441
|
+
from model files. Defaults to "linear"; options include:
|
568
442
|
|
569
|
-
- "spline": scipy bivariate spline interpolation (default)
|
570
|
-
- "bilinear": quick bilinear interpolation
|
571
443
|
- "linear", "nearest": scipy regular grid interpolations
|
444
|
+
- "spline": scipy bivariate spline interpolation
|
445
|
+
- "bilinear": quick bilinear interpolation
|
572
446
|
extrapolate : bool, optional
|
573
447
|
Whether to extrapolate tides for x and y coordinates outside of
|
574
448
|
the valid tide modelling domain using nearest-neighbor.
|
@@ -594,12 +468,16 @@ def model_tides(
|
|
594
468
|
parallel. Optionally, tide modelling can also be run in parallel
|
595
469
|
across input x and y coordinates (see "parallel_splits" below).
|
596
470
|
Default is True.
|
597
|
-
parallel_splits : int, optional
|
471
|
+
parallel_splits : str or int, optional
|
598
472
|
Whether to split the input x and y coordinates into smaller,
|
599
473
|
evenly-sized chunks that are processed in parallel. This can
|
600
474
|
provide a large performance boost when processing large numbers
|
601
|
-
of coordinates. The default is
|
602
|
-
|
475
|
+
of coordinates. The default is "auto", which will automatically
|
476
|
+
attempt to determine optimal splits based on available CPUs,
|
477
|
+
the number of input points, and the number of models.
|
478
|
+
parallel_max : int, optional
|
479
|
+
Maximum number of processes to run in parallel. The default of
|
480
|
+
None will automatically determine this from your available CPUs.
|
603
481
|
output_units : str, optional
|
604
482
|
Whether to return modelled tides in floating point metre units,
|
605
483
|
or integer centimetre units (i.e. scaled by 100) or integer
|
@@ -729,13 +607,28 @@ def model_tides(
|
|
729
607
|
mode=mode,
|
730
608
|
)
|
731
609
|
|
732
|
-
#
|
733
|
-
|
610
|
+
# If automatic parallel splits, calculate optimal value
|
611
|
+
# based on available parallelisation, number of points
|
612
|
+
# and number of models
|
613
|
+
if parallel_splits == "auto":
|
614
|
+
parallel_splits = _parallel_splits(
|
615
|
+
total_points=len(x),
|
616
|
+
model_count=len(models_to_process),
|
617
|
+
parallel_max=parallel_max,
|
618
|
+
)
|
619
|
+
|
620
|
+
# Verify that parallel splits are not larger than number of points
|
621
|
+
assert isinstance(parallel_splits, int)
|
622
|
+
if parallel_splits > len(x):
|
623
|
+
raise ValueError(f"Parallel splits ({parallel_splits}) cannot be larger than the number of points ({len(x)}).")
|
734
624
|
|
735
625
|
# Parallelise if either multiple models or multiple splits requested
|
626
|
+
|
736
627
|
if parallel & ((len(models_to_process) > 1) | (parallel_splits > 1)):
|
737
|
-
with ProcessPoolExecutor() as executor:
|
738
|
-
print(
|
628
|
+
with ProcessPoolExecutor(max_workers=parallel_max) as executor:
|
629
|
+
print(
|
630
|
+
f"Modelling tides with {', '.join(models_to_process)} in parallel (models: {len(models_to_process)}, splits: {parallel_splits})"
|
631
|
+
)
|
739
632
|
|
740
633
|
# Optionally split lon/lat points into `splits_n` chunks
|
741
634
|
# that will be applied in parallel
|
@@ -783,7 +676,7 @@ def model_tides(
|
|
783
676
|
model_outputs = []
|
784
677
|
|
785
678
|
for model_i in models_to_process:
|
786
|
-
print(f"Modelling tides
|
679
|
+
print(f"Modelling tides with {model_i}")
|
787
680
|
tide_df = iter_func(model_i, x, y, time)
|
788
681
|
model_outputs.append(tide_df)
|
789
682
|
|
eo_tides/stats.py
CHANGED
@@ -6,20 +6,18 @@ from typing import TYPE_CHECKING
|
|
6
6
|
|
7
7
|
import matplotlib.pyplot as plt
|
8
8
|
import numpy as np
|
9
|
-
import odc.geo.xr
|
10
9
|
import pandas as pd
|
11
10
|
import xarray as xr
|
12
11
|
from scipy import stats
|
13
12
|
|
14
13
|
# Only import if running type checking
|
15
14
|
if TYPE_CHECKING:
|
16
|
-
import datetime
|
17
|
-
|
18
15
|
import xarray as xr
|
19
16
|
from odc.geo.geobox import GeoBox
|
20
17
|
|
21
18
|
from .eo import _standardise_inputs, pixel_tides, tag_tides
|
22
|
-
from .model import
|
19
|
+
from .model import model_tides
|
20
|
+
from .utils import DatetimeLike
|
23
21
|
|
24
22
|
|
25
23
|
def _plot_biases(
|
eo_tides/utils.py
CHANGED
@@ -1,5 +1,457 @@
|
|
1
|
+
# Used to postpone evaluation of type annotations
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import datetime
|
5
|
+
import os
|
6
|
+
import pathlib
|
7
|
+
import textwrap
|
8
|
+
import warnings
|
9
|
+
from typing import List, Union
|
10
|
+
|
1
11
|
import numpy as np
|
12
|
+
import odc.geo
|
13
|
+
import pandas as pd
|
14
|
+
import xarray as xr
|
15
|
+
from colorama import Style, init
|
16
|
+
from odc.geo.geom import BoundingBox
|
17
|
+
from pyTMD.io.model import load_database
|
18
|
+
from pyTMD.io.model import model as pytmd_model
|
2
19
|
from scipy.spatial import cKDTree as KDTree
|
20
|
+
from tqdm import tqdm
|
21
|
+
|
22
|
+
# Type alias for all possible inputs to "time" params
|
23
|
+
DatetimeLike = Union[np.ndarray, pd.DatetimeIndex, pd.Timestamp, datetime.datetime, str, List[str]]
|
24
|
+
|
25
|
+
|
26
|
+
def _set_directory(
|
27
|
+
directory: str | os.PathLike | None = None,
|
28
|
+
) -> os.PathLike:
|
29
|
+
"""
|
30
|
+
Set tide modelling files directory. If no custom
|
31
|
+
path is provided, try global `EO_TIDES_TIDE_MODELS`
|
32
|
+
environmental variable instead.
|
33
|
+
"""
|
34
|
+
if directory is None:
|
35
|
+
if "EO_TIDES_TIDE_MODELS" in os.environ:
|
36
|
+
directory = os.environ["EO_TIDES_TIDE_MODELS"]
|
37
|
+
else:
|
38
|
+
raise Exception(
|
39
|
+
"No tide model directory provided via `directory`, and/or no "
|
40
|
+
"`EO_TIDES_TIDE_MODELS` environment variable found. "
|
41
|
+
"Please provide a valid path to your tide model directory."
|
42
|
+
)
|
43
|
+
|
44
|
+
# Verify path exists
|
45
|
+
directory = pathlib.Path(directory).expanduser()
|
46
|
+
if not directory.exists():
|
47
|
+
raise FileNotFoundError(f"No valid tide model directory found at path `{directory}`")
|
48
|
+
else:
|
49
|
+
return directory
|
50
|
+
|
51
|
+
|
52
|
+
def _standardise_time(
|
53
|
+
time: DatetimeLike | None,
|
54
|
+
) -> np.ndarray | None:
|
55
|
+
"""
|
56
|
+
Accept any time format accepted by `pd.to_datetime`,
|
57
|
+
and return a datetime64 ndarray. Return None if None
|
58
|
+
passed.
|
59
|
+
"""
|
60
|
+
# Return time as-is if None
|
61
|
+
if time is None:
|
62
|
+
return None
|
63
|
+
|
64
|
+
# Use pd.to_datetime for conversion, then convert to numpy array
|
65
|
+
time = pd.to_datetime(time).to_numpy().astype("datetime64[ns]")
|
66
|
+
|
67
|
+
# Ensure that data has at least one dimension
|
68
|
+
return np.atleast_1d(time)
|
69
|
+
|
70
|
+
|
71
|
+
def _clip_model_file(
|
72
|
+
nc: xr.Dataset,
|
73
|
+
bbox: BoundingBox,
|
74
|
+
ydim: str,
|
75
|
+
xdim: str,
|
76
|
+
ycoord: str,
|
77
|
+
xcoord: str,
|
78
|
+
) -> xr.Dataset:
|
79
|
+
"""
|
80
|
+
Clips tide model netCDF datasets to a bounding box.
|
81
|
+
|
82
|
+
If the bounding box crosses 0 degrees longitude (e.g. Greenwich),
|
83
|
+
the function will clip the dataset into two parts and concatenate
|
84
|
+
them along the x-dimension to create a continuous result.
|
85
|
+
|
86
|
+
Parameters
|
87
|
+
----------
|
88
|
+
nc : xr.Dataset
|
89
|
+
Input tide model xarray dataset.
|
90
|
+
bbox : odc.geo.geom.BoundingBox
|
91
|
+
A BoundingBox object for clipping the dataset in EPSG:4326
|
92
|
+
degrees coordinates. For example:
|
93
|
+
`BoundingBox(left=108, bottom=-48, right=158, top=-6, crs='EPSG:4326')`
|
94
|
+
ydim : str
|
95
|
+
The name of the xarray dimension representing the y-axis.
|
96
|
+
Depending on the tide model, this may or may not contain
|
97
|
+
actual latitude values.
|
98
|
+
xdim : str
|
99
|
+
The name of the xarray dimension representing the x-axis.
|
100
|
+
Depending on the tide model, this may or may not contain
|
101
|
+
actual longitude values.
|
102
|
+
ycoord : str
|
103
|
+
The name of the coordinate, variable or dimension containing
|
104
|
+
actual latitude values used for clipping the data.
|
105
|
+
xcoord : str
|
106
|
+
The name of the coordinate, variable or dimension containing
|
107
|
+
actual longitude values used for clipping the data.
|
108
|
+
|
109
|
+
Returns
|
110
|
+
-------
|
111
|
+
xr.Dataset
|
112
|
+
A dataset clipped to the specified bounding box, with
|
113
|
+
appropriate adjustments if the bounding box crosses 0
|
114
|
+
degrees longitude.
|
115
|
+
|
116
|
+
Examples
|
117
|
+
--------
|
118
|
+
>>> nc = xr.open_dataset("GOT5.5/ocean_tides/2n2.nc")
|
119
|
+
>>> bbox = BoundingBox(left=108, bottom=-48, right=158, top=-6, crs='EPSG:4326')
|
120
|
+
>>> clipped_nc = _clip_model_file(nc, bbox, xdim="lon", ydim="lat", ycoord="latitude", xcoord="longitude")
|
121
|
+
"""
|
122
|
+
|
123
|
+
# Extract x and y coords from xarray and load into memory
|
124
|
+
xcoords = nc[xcoord].compute()
|
125
|
+
ycoords = nc[ycoord].compute()
|
126
|
+
|
127
|
+
# If data falls within 0-360 degree bounds, then clip directly
|
128
|
+
if (bbox.left >= 0) & (bbox.right <= 360):
|
129
|
+
nc_clipped = nc.sel({
|
130
|
+
ydim: (ycoords >= bbox.bottom) & (ycoords <= bbox.top),
|
131
|
+
xdim: (xcoords >= bbox.left) & (xcoords <= bbox.right),
|
132
|
+
})
|
133
|
+
|
134
|
+
# If bbox crosses zero longitude, extract left and right
|
135
|
+
# separately and then combine into one concatenated dataset
|
136
|
+
elif (bbox.left < 0) & (bbox.right > 0):
|
137
|
+
# Convert longitudes to 0-360 range
|
138
|
+
left = bbox.left % 360
|
139
|
+
right = bbox.right % 360
|
140
|
+
|
141
|
+
# Extract data from left of 0 longitude, and convert lon
|
142
|
+
# coords to -180 to 0 range to enable continuous interpolation
|
143
|
+
# across 0 boundary
|
144
|
+
nc_left = nc.sel({
|
145
|
+
ydim: (ycoords >= bbox.bottom) & (ycoords <= bbox.top),
|
146
|
+
xdim: (xcoords >= left) & (xcoords <= 360),
|
147
|
+
}).assign({xcoord: lambda x: x[xcoord] - 360})
|
148
|
+
|
149
|
+
# Convert additional lon variables for TXPO
|
150
|
+
if "lon_v" in nc_left:
|
151
|
+
nc_left = nc_left.assign({
|
152
|
+
"lon_v": lambda x: x["lon_v"] - 360,
|
153
|
+
"lon_u": lambda x: x["lon_u"] - 360,
|
154
|
+
})
|
155
|
+
|
156
|
+
# Extract data to right of 0 longitude
|
157
|
+
nc_right = nc.sel({
|
158
|
+
ydim: (ycoords >= bbox.bottom) & (ycoords <= bbox.top),
|
159
|
+
xdim: (xcoords > 0) & (xcoords <= right),
|
160
|
+
})
|
161
|
+
|
162
|
+
# Combine left and right data along x dimension
|
163
|
+
nc_clipped = xr.concat([nc_left, nc_right], dim=xdim)
|
164
|
+
|
165
|
+
# Hack fix to remove expanded x dim on lat variables issue
|
166
|
+
# for TPXO data; remove x dim by selecting the first obs
|
167
|
+
for i in ["lat_z", "lat_v", "lat_u", "con"]:
|
168
|
+
try:
|
169
|
+
nc_clipped[i] = nc_clipped[i].isel(nx=0)
|
170
|
+
except:
|
171
|
+
pass
|
172
|
+
|
173
|
+
return nc_clipped
|
174
|
+
|
175
|
+
|
176
|
+
def clip_models(
|
177
|
+
input_directory: str | os.PathLike,
|
178
|
+
output_directory: str | os.PathLike,
|
179
|
+
bbox: tuple[float, float, float, float],
|
180
|
+
model: list | None = None,
|
181
|
+
buffer: float = 1,
|
182
|
+
overwrite: bool = False,
|
183
|
+
):
|
184
|
+
"""
|
185
|
+
Clip NetCDF-format ocean tide models to a bounding box.
|
186
|
+
|
187
|
+
This function identifies all NetCDF-format tide models in a
|
188
|
+
given input directory, including "ATLAS-netcdf" (e.g. TPXO9-atlas-nc),
|
189
|
+
"FES-netcdf" (e.g. FES2022, EOT20), and "GOT-netcdf" (e.g. GOT5.5)
|
190
|
+
format files. Files for each model are then clipped to the extent of
|
191
|
+
the provided bounding box, handling model-specific file structures.
|
192
|
+
After each model is clipped, the result is exported to the output
|
193
|
+
directory and verified with `pyTMD` to ensure the clipped data is
|
194
|
+
suitable for tide modelling.
|
195
|
+
|
196
|
+
For instructions on accessing and downloading tide models, see:
|
197
|
+
<https://geoscienceaustralia.github.io/eo-tides/setup/>
|
198
|
+
|
199
|
+
Parameters
|
200
|
+
----------
|
201
|
+
input_directory : str or os.PathLike
|
202
|
+
Path to directory containing input NetCDF-format tide model files.
|
203
|
+
output_directory : str or os.PathLike
|
204
|
+
Path to directory where clipped NetCDF files will be exported.
|
205
|
+
bbox : tuple of float
|
206
|
+
Bounding box for clipping the tide models in EPSG:4326 degrees
|
207
|
+
coordinates, specified as `(left, bottom, right, top)`.
|
208
|
+
model : str or list of str, optional
|
209
|
+
The tide model (or models) to clip. Defaults to None, which
|
210
|
+
will automatically identify and clip all NetCDF-format models
|
211
|
+
in the input directly.
|
212
|
+
buffer : float, optional
|
213
|
+
Buffer distance (in degrees) added to the bounding box to provide
|
214
|
+
sufficient data on edges of study area. Defaults to 1 degree.
|
215
|
+
overwrite : bool, optional
|
216
|
+
If True, overwrite existing files in the output directory.
|
217
|
+
Defaults to False.
|
218
|
+
|
219
|
+
Examples
|
220
|
+
--------
|
221
|
+
>>> clip_models(
|
222
|
+
... input_directory="tide_models/",
|
223
|
+
... output_directory="tide_models_clipped/",
|
224
|
+
... bbox=(-8.968392, 50.070574, 2.447160, 59.367122),
|
225
|
+
... )
|
226
|
+
"""
|
227
|
+
|
228
|
+
# Get input and output paths
|
229
|
+
input_directory = _set_directory(input_directory)
|
230
|
+
output_directory = pathlib.Path(output_directory)
|
231
|
+
|
232
|
+
# Prepare bounding box
|
233
|
+
bbox = odc.geo.geom.BoundingBox(*bbox, crs="EPSG:4326").buffered(buffer)
|
234
|
+
|
235
|
+
# Identify NetCDF models
|
236
|
+
model_database = load_database()["elevation"]
|
237
|
+
netcdf_formats = ["ATLAS-netcdf", "FES-netcdf", "GOT-netcdf"]
|
238
|
+
netcdf_models = {k for k, v in model_database.items() if v["format"] in netcdf_formats}
|
239
|
+
|
240
|
+
# Identify subset of available and requested NetCDF models
|
241
|
+
available_models, _ = list_models(directory=input_directory, show_available=False, show_supported=False)
|
242
|
+
requested_models = list(np.atleast_1d(model)) if model is not None else available_models
|
243
|
+
available_netcdf_models = list(set(available_models) & set(requested_models) & set(netcdf_models))
|
244
|
+
|
245
|
+
# Raise error if no valid models found
|
246
|
+
if len(available_netcdf_models) == 0:
|
247
|
+
raise ValueError(f"No valid NetCDF models found in {input_directory}.")
|
248
|
+
|
249
|
+
# If model list is provided,
|
250
|
+
print(f"Preparing to clip suitable NetCDF models: {available_netcdf_models}\n")
|
251
|
+
|
252
|
+
# Loop through suitable models and export
|
253
|
+
for m in available_netcdf_models:
|
254
|
+
# Get model file and grid file list if they exist
|
255
|
+
model_files = model_database[m].get("model_file", [])
|
256
|
+
grid_file = model_database[m].get("grid_file", [])
|
257
|
+
|
258
|
+
# Convert to list if strings and combine
|
259
|
+
model_files = model_files if isinstance(model_files, list) else [model_files]
|
260
|
+
grid_file = grid_file if isinstance(grid_file, list) else [grid_file]
|
261
|
+
all_files = model_files + grid_file
|
262
|
+
|
263
|
+
# Loop through each model file and clip
|
264
|
+
for file in tqdm(all_files, desc=f"Clipping {m}"):
|
265
|
+
# Skip if it exists in output directory
|
266
|
+
if (output_directory / file).exists() and not overwrite:
|
267
|
+
continue
|
268
|
+
|
269
|
+
# Load model file
|
270
|
+
nc = xr.open_mfdataset(input_directory / file)
|
271
|
+
|
272
|
+
# Open file and clip according to model
|
273
|
+
if m in (
|
274
|
+
"GOT5.5",
|
275
|
+
"GOT5.5_load",
|
276
|
+
"GOT5.5_extrapolated",
|
277
|
+
"GOT5.5D",
|
278
|
+
"GOT5.5D_extrapolated",
|
279
|
+
"GOT5.6",
|
280
|
+
"GOT5.6_extrapolated",
|
281
|
+
):
|
282
|
+
nc_clipped = _clip_model_file(
|
283
|
+
nc,
|
284
|
+
bbox,
|
285
|
+
xdim="lon",
|
286
|
+
ydim="lat",
|
287
|
+
ycoord="latitude",
|
288
|
+
xcoord="longitude",
|
289
|
+
)
|
290
|
+
|
291
|
+
elif m in ("HAMTIDE11",):
|
292
|
+
nc_clipped = _clip_model_file(nc, bbox, xdim="LON", ydim="LAT", ycoord="LAT", xcoord="LON")
|
293
|
+
|
294
|
+
elif m in (
|
295
|
+
"EOT20",
|
296
|
+
"EOT20_load",
|
297
|
+
"FES2012",
|
298
|
+
"FES2014",
|
299
|
+
"FES2014_extrapolated",
|
300
|
+
"FES2014_load",
|
301
|
+
"FES2022",
|
302
|
+
"FES2022_extrapolated",
|
303
|
+
"FES2022_load",
|
304
|
+
):
|
305
|
+
nc_clipped = _clip_model_file(nc, bbox, xdim="lon", ydim="lat", ycoord="lat", xcoord="lon")
|
306
|
+
|
307
|
+
elif m in (
|
308
|
+
"TPXO8-atlas-nc",
|
309
|
+
"TPXO9-atlas-nc",
|
310
|
+
"TPXO9-atlas-v2-nc",
|
311
|
+
"TPXO9-atlas-v3-nc",
|
312
|
+
"TPXO9-atlas-v4-nc",
|
313
|
+
"TPXO9-atlas-v5-nc",
|
314
|
+
"TPXO10-atlas-v2-nc",
|
315
|
+
):
|
316
|
+
nc_clipped = _clip_model_file(
|
317
|
+
nc,
|
318
|
+
bbox,
|
319
|
+
xdim="nx",
|
320
|
+
ydim="ny",
|
321
|
+
ycoord="lat_z",
|
322
|
+
xcoord="lon_z",
|
323
|
+
)
|
324
|
+
|
325
|
+
else:
|
326
|
+
raise Exception(f"Model {m} not supported")
|
327
|
+
|
328
|
+
# Create directory and export
|
329
|
+
(output_directory / file).parent.mkdir(parents=True, exist_ok=True)
|
330
|
+
nc_clipped.to_netcdf(output_directory / file, mode="w")
|
331
|
+
|
332
|
+
# Verify that models are ready
|
333
|
+
pytmd_model(directory=output_directory).elevation(m=m).verify
|
334
|
+
print(" ✅ Clipped model exported and verified")
|
335
|
+
|
336
|
+
print(f"\nOutputs exported to {output_directory}")
|
337
|
+
list_models(directory=output_directory, show_available=True, show_supported=False)
|
338
|
+
|
339
|
+
|
340
|
+
def list_models(
|
341
|
+
directory: str | os.PathLike | None = None,
|
342
|
+
show_available: bool = True,
|
343
|
+
show_supported: bool = True,
|
344
|
+
raise_error: bool = False,
|
345
|
+
) -> tuple[list[str], list[str]]:
|
346
|
+
"""
|
347
|
+
List all tide models available for tide modelling.
|
348
|
+
|
349
|
+
This function scans the specified tide model directory
|
350
|
+
and returns a list of models that are available in the
|
351
|
+
directory as well as the full list of all models supported
|
352
|
+
by `eo-tides` and `pyTMD`.
|
353
|
+
|
354
|
+
For instructions on setting up tide models, see:
|
355
|
+
<https://geoscienceaustralia.github.io/eo-tides/setup/>
|
356
|
+
|
357
|
+
Parameters
|
358
|
+
----------
|
359
|
+
directory : str, optional
|
360
|
+
The directory containing tide model data files. If no path is
|
361
|
+
provided, this will default to the environment variable
|
362
|
+
`EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
|
363
|
+
Tide modelling files should be stored in sub-folders for each
|
364
|
+
model that match the structure required by `pyTMD`
|
365
|
+
(<https://geoscienceaustralia.github.io/eo-tides/setup/>).
|
366
|
+
show_available : bool, optional
|
367
|
+
Whether to print a list of locally available models.
|
368
|
+
show_supported : bool, optional
|
369
|
+
Whether to print a list of all supported models, in
|
370
|
+
addition to models available locally.
|
371
|
+
raise_error : bool, optional
|
372
|
+
If True, raise an error if no available models are found.
|
373
|
+
If False, raise a warning.
|
374
|
+
|
375
|
+
Returns
|
376
|
+
-------
|
377
|
+
available_models : list of str
|
378
|
+
A list of all tide models available within `directory`.
|
379
|
+
supported_models : list of str
|
380
|
+
A list of all tide models supported by `eo-tides`.
|
381
|
+
"""
|
382
|
+
init() # Initialize colorama
|
383
|
+
|
384
|
+
# Set tide modelling files directory. If no custom path is
|
385
|
+
# provided, try global environment variable.
|
386
|
+
directory = _set_directory(directory)
|
387
|
+
|
388
|
+
# Get full list of supported models from pyTMD database
|
389
|
+
model_database = load_database()["elevation"]
|
390
|
+
supported_models = list(model_database.keys())
|
391
|
+
|
392
|
+
# Extract expected model paths
|
393
|
+
expected_paths = {}
|
394
|
+
for m in supported_models:
|
395
|
+
model_file = model_database[m]["model_file"]
|
396
|
+
model_file = model_file[0] if isinstance(model_file, list) else model_file
|
397
|
+
expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)
|
398
|
+
|
399
|
+
# Define column widths
|
400
|
+
status_width = 4 # Width for emoji
|
401
|
+
name_width = max(len(name) for name in supported_models)
|
402
|
+
path_width = max(len(path) for path in expected_paths.values())
|
403
|
+
|
404
|
+
# Print list of supported models, marking available and
|
405
|
+
# unavailable models and appending available to list
|
406
|
+
if show_available or show_supported:
|
407
|
+
total_width = min(status_width + name_width + path_width + 6, 80)
|
408
|
+
print("─" * total_width)
|
409
|
+
print(f"{'🌊':^{status_width}} | {'Model':<{name_width}} | {'Expected path':<{path_width}}")
|
410
|
+
print("─" * total_width)
|
411
|
+
|
412
|
+
available_models = []
|
413
|
+
for m in supported_models:
|
414
|
+
try:
|
415
|
+
model_file = pytmd_model(directory=directory).elevation(m=m)
|
416
|
+
available_models.append(m)
|
417
|
+
|
418
|
+
if show_available:
|
419
|
+
# Mark available models with a green tick
|
420
|
+
status = "✅"
|
421
|
+
print(f"{status:^{status_width}}│ {m:<{name_width}} │ {expected_paths[m]:<{path_width}}")
|
422
|
+
except FileNotFoundError:
|
423
|
+
if show_supported:
|
424
|
+
# Mark unavailable models with a red cross
|
425
|
+
status = "❌"
|
426
|
+
print(
|
427
|
+
f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}"
|
428
|
+
)
|
429
|
+
|
430
|
+
if show_available or show_supported:
|
431
|
+
print("─" * total_width)
|
432
|
+
|
433
|
+
# Print summary
|
434
|
+
print(f"\n{Style.BRIGHT}Summary:{Style.RESET_ALL}")
|
435
|
+
print(f"Available models: {len(available_models)}/{len(supported_models)}")
|
436
|
+
|
437
|
+
# Raise error or warning if no models are available
|
438
|
+
if not available_models:
|
439
|
+
warning_msg = textwrap.dedent(
|
440
|
+
f"""
|
441
|
+
No valid tide models are available in `{directory}`.
|
442
|
+
Are you sure you have provided the correct `directory` path, or set the
|
443
|
+
`EO_TIDES_TIDE_MODELS` environment variable to point to the location of your
|
444
|
+
tide model directory?
|
445
|
+
"""
|
446
|
+
).strip()
|
447
|
+
|
448
|
+
if raise_error:
|
449
|
+
raise Exception(warning_msg)
|
450
|
+
else:
|
451
|
+
warnings.warn(warning_msg, UserWarning)
|
452
|
+
|
453
|
+
# Return list of available and supported models
|
454
|
+
return available_models, supported_models
|
3
455
|
|
4
456
|
|
5
457
|
def idw(
|
@@ -22,7 +474,7 @@ def idw(
|
|
22
474
|
inverse distance to each neighbor, with weights descreasing with
|
23
475
|
increasing distance.
|
24
476
|
|
25
|
-
Code inspired by: https://github.com/DahnJ/REM-xarray
|
477
|
+
Code inspired by: <https://github.com/DahnJ/REM-xarray>
|
26
478
|
|
27
479
|
Parameters
|
28
480
|
----------
|
eo_tides/validation.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import datetime
|
2
|
-
import glob
|
3
2
|
import warnings
|
4
3
|
from math import sqrt
|
5
4
|
from numbers import Number
|
@@ -193,11 +192,12 @@ def load_gauge_gesla(
|
|
193
192
|
metadata_path="/gdata1/data/sea_level/GESLA3_ALL 2.csv",
|
194
193
|
):
|
195
194
|
"""
|
196
|
-
Load
|
197
|
-
(GESLA) tide gauge data with an `x, y, time` spatiotemporal query,
|
198
|
-
or from a list of specific tide gauges.
|
195
|
+
Load Global Extreme Sea Level Analysis (GESLA) tide gauge data.
|
199
196
|
|
200
|
-
|
197
|
+
Load and process all available GESLA measured sea-level data
|
198
|
+
with an `x, y, time` spatio-temporal query, or from a list of
|
199
|
+
specific tide gauges. Can optionally filter by gauge quality
|
200
|
+
and append detailed gauge metadata.
|
201
201
|
|
202
202
|
Modified from original code in <https://github.com/philiprt/GeslaDataset>.
|
203
203
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: eo-tides
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Tide modelling tools for large-scale satellite earth observation analysis
|
5
|
+
Author: Stephen Sagar, Claire Phillips, Vanessa Newey
|
5
6
|
Author-email: Robbi Bishop-Taylor <Robbi.BishopTaylor@ga.gov.au>
|
6
7
|
Project-URL: Homepage, https://GeoscienceAustralia.github.io/eo-tides/
|
7
8
|
Project-URL: Repository, https://github.com/GeoscienceAustralia/eo-tides
|
@@ -19,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.10
|
20
21
|
Classifier: Programming Language :: Python :: 3.11
|
21
22
|
Classifier: Programming Language :: Python :: 3.12
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
22
24
|
Requires-Python: <4.0,>=3.9
|
23
25
|
Description-Content-Type: text/markdown
|
24
26
|
License-File: LICENSE
|
@@ -28,8 +30,9 @@ Requires-Dist: matplotlib >=3.8.0
|
|
28
30
|
Requires-Dist: numpy >=1.26.0
|
29
31
|
Requires-Dist: odc-geo >=0.4.7
|
30
32
|
Requires-Dist: pandas >=2.2.0
|
33
|
+
Requires-Dist: psutil >=5.8.0
|
31
34
|
Requires-Dist: pyproj >=3.6.1
|
32
|
-
Requires-Dist: pyTMD ==2.1.
|
35
|
+
Requires-Dist: pyTMD ==2.1.8
|
33
36
|
Requires-Dist: scikit-learn >=1.4.0
|
34
37
|
Requires-Dist: scipy >=1.11.2
|
35
38
|
Requires-Dist: shapely >=2.0.6
|
@@ -44,16 +47,17 @@ Requires-Dist: planetary-computer >=1.0.0 ; extra == 'notebooks'
|
|
44
47
|
|
45
48
|
# `eo-tides`: Tide modelling tools for large-scale satellite earth observation analysis
|
46
49
|
|
47
|
-
<img align="right" width="200" src="docs/assets/eo-tides-logo.gif" alt="eo-tides logo" style="margin-right: 40px;">
|
50
|
+
<img align="right" width="200" src="https://github.com/GeoscienceAustralia/eo-tides/blob/main/docs/assets/eo-tides-logo.gif?raw=true" alt="eo-tides logo" style="margin-right: 40px;">
|
48
51
|
|
49
52
|
[](https://pypi.org/project/eo-tides/)
|
50
53
|
[](https://github.com/GeoscienceAustralia/eo-tides/actions/workflows/main.yml?query=branch%3Amain)
|
51
|
-

|
54
|
+
[](https://github.com/GeoscienceAustralia/eo-tides/blob/main/pyproject.toml)
|
52
55
|
[](https://codecov.io/gh/GeoscienceAustralia/eo-tides)
|
53
56
|
[](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)
|
54
57
|
|
55
|
-
- **Github repository**: <https://github.com/GeoscienceAustralia/eo-tides/>
|
56
|
-
- **Documentation
|
58
|
+
- ⚙️ **Github repository**: <https://github.com/GeoscienceAustralia/eo-tides/>
|
59
|
+
- 📘 **Documentation**: <https://GeoscienceAustralia.github.io/eo-tides/>
|
60
|
+
- 🐍 **PyPI**: <https://pypi.org/project/eo-tides/>
|
57
61
|
|
58
62
|
> [!CAUTION]
|
59
63
|
> This package is a work in progress, and not currently ready for operational use.
|
@@ -64,12 +68,12 @@ Requires-Dist: planetary-computer >=1.0.0 ; extra == 'notebooks'
|
|
64
68
|
|
65
69
|
These tools can be applied to petabytes of freely available satellite data (e.g. from [Digital Earth Australia](https://knowledge.dea.ga.gov.au/) or [Microsoft Planetary Computer](https://planetarycomputer.microsoft.com/)) loaded via Open Data Cube's [`odc-stac`](https://odc-stac.readthedocs.io/en/latest/) or [`datacube`](https://opendatacube.readthedocs.io/en/latest/) packages, supporting coastal and ocean earth observation analysis for any time period or location globally.
|
66
70
|
|
67
|
-

|
71
|
+

|
68
72
|
|
69
73
|
## Highlights
|
70
74
|
|
71
75
|
- 🌊 Model tide heights and phases (e.g. high, low, ebb, flow) from multiple global ocean tide models in parallel, and return a `pandas.DataFrame` for further analysis
|
72
|
-
- 🛰️ "Tag" satellite data with tide
|
76
|
+
- 🛰️ "Tag" satellite data with tide heights based on the exact moment of image acquisition
|
73
77
|
- 🌐 Model tides for every individual satellite pixel through time, producing three-dimensional "tide height" `xarray`-format datacubes that can be integrated with satellite data
|
74
78
|
- 📈 Calculate statistics describing local tide dynamics, as well as biases caused by interactions between tidal processes and satellite orbits
|
75
79
|
- 🛠️ Validate modelled tides using measured sea levels from coastal tide gauges (e.g. [GESLA Global Extreme Sea Level Analysis](https://gesla.org/))
|
@@ -0,0 +1,11 @@
|
|
1
|
+
eo_tides/__init__.py,sha256=rn6slQP0bAhqM9cL2W8omiEM8f0b7libt6WsQ5DvYTE,1655
|
2
|
+
eo_tides/eo.py,sha256=Vc_AHqT0_IDqdwbOpNcONjHhiCtbCe8Osk2gvzUeNmU,22377
|
3
|
+
eo_tides/model.py,sha256=T0IFvSiXjiTvlY-Vi7-jneHPfaCLt95pmcRd8CZxOjE,34478
|
4
|
+
eo_tides/stats.py,sha256=lchWWJ5gBDuZWvaD8TF-12Xlo2qCWiNI2IcgAaKWy2U,22668
|
5
|
+
eo_tides/utils.py,sha256=tqXAkLk_Dm8s3yuXPxBBkHW3PZ42ayV8VCQjNw3wDZw,22433
|
6
|
+
eo_tides/validation.py,sha256=UREsc0yWRO4x0PJXvyoIx8gYiBZiRSim4z6TmAz_VDM,11857
|
7
|
+
eo_tides-0.3.0.dist-info/LICENSE,sha256=owxWsXViCL2J6Ks3XYhot7t4Y93nstmXAT95Zf030Cc,11350
|
8
|
+
eo_tides-0.3.0.dist-info/METADATA,sha256=lajVK9XoAA2fJqA9R3HHYJ7iUJFMtmxh3U07uGTs4_4,8040
|
9
|
+
eo_tides-0.3.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
10
|
+
eo_tides-0.3.0.dist-info/top_level.txt,sha256=lXZDUUM1DlLdKWHRn8zdmtW8Rx-eQOIWVvt0b8VGiyQ,9
|
11
|
+
eo_tides-0.3.0.dist-info/RECORD,,
|
eo_tides-0.2.0.dist-info/RECORD
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
eo_tides/__init__.py,sha256=MKL_HjRECVHHQVnMVg-TSz3J-vjxPZgbnyWu0RAXZ0U,1623
|
2
|
-
eo_tides/eo.py,sha256=98llXpF2lSDfsVQBao-dpr8Z4zH5q0C2Rfu4X-qmPiE,22400
|
3
|
-
eo_tides/model.py,sha256=kaYXU_jmv91ONam_CpoVN137aJsmqHvKHEPWe-DRJnI,38280
|
4
|
-
eo_tides/stats.py,sha256=sOwXEh8RPb8muh2o9-z1c0GDnV5FJa_TgsiGb4rMkiU,22689
|
5
|
-
eo_tides/utils.py,sha256=l9VXJawQzaRBYaFMsP8VBeaN5VA3rFDdzcvF7Rk04Vc,5620
|
6
|
-
eo_tides/validation.py,sha256=JjTUqDfbR189m_6W1bpaSolQIHNTLicTHN7z9O_nr3s,11828
|
7
|
-
eo_tides-0.2.0.dist-info/LICENSE,sha256=owxWsXViCL2J6Ks3XYhot7t4Y93nstmXAT95Zf030Cc,11350
|
8
|
-
eo_tides-0.2.0.dist-info/METADATA,sha256=msiUYdlCm5pTix7LXA7RzM8Hg-9r4JHpWTSUyxdPpg4,7637
|
9
|
-
eo_tides-0.2.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
10
|
-
eo_tides-0.2.0.dist-info/top_level.txt,sha256=lXZDUUM1DlLdKWHRn8zdmtW8Rx-eQOIWVvt0b8VGiyQ,9
|
11
|
-
eo_tides-0.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|