eo-tides 0.5.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 +50 -0
- eo_tides/eo.py +532 -0
- eo_tides/model.py +825 -0
- eo_tides/stats.py +581 -0
- eo_tides/utils.py +705 -0
- eo_tides/validation.py +334 -0
- eo_tides-0.5.0.dist-info/LICENSE +201 -0
- eo_tides-0.5.0.dist-info/METADATA +118 -0
- eo_tides-0.5.0.dist-info/RECORD +11 -0
- eo_tides-0.5.0.dist-info/WHEEL +5 -0
- eo_tides-0.5.0.dist-info/top_level.txt +1 -0
eo_tides/__init__.py
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
"""
|
2
|
+
eo_tides
|
3
|
+
========
|
4
|
+
|
5
|
+
Tide modelling tools for large-scale satellite earth observation analysis.
|
6
|
+
|
7
|
+
`eo-tides` provides powerful parallelized tools for integrating satellite
|
8
|
+
Earth observation data with tide modelling. `eo-tides` combines advanced
|
9
|
+
tide modelling functionality from the `pyTMD` package with `pandas`,
|
10
|
+
`xarray` and `odc-geo`, providing a suite of flexible tools for efficient
|
11
|
+
analysis of coastal and ocean Earth observation data – from regional,
|
12
|
+
continental, to global scale.
|
13
|
+
|
14
|
+
These tools can be applied to petabytes of freely available satellite
|
15
|
+
data (e.g. from Digital Earth Australia or Microsoft Planetary Computer)
|
16
|
+
loaded via Open Data Cube's `odc-stac` or `datacube` packages, supporting
|
17
|
+
coastal and ocean earth observation analysis for any time period or
|
18
|
+
location globally.
|
19
|
+
|
20
|
+
Modules
|
21
|
+
-------
|
22
|
+
model : Core tide modelling functionality
|
23
|
+
eo : Combine satellite EO data with tide modelling
|
24
|
+
stats : Calculate local tide dynamics and satellite bias statistics
|
25
|
+
utils : Utility functions and helper tools
|
26
|
+
validation : Load observed tide gauge data to validate modelled tides
|
27
|
+
"""
|
28
|
+
|
29
|
+
# Import commonly used functions for convenience
|
30
|
+
from .eo import pixel_tides, tag_tides
|
31
|
+
from .model import ensemble_tides, model_phases, model_tides
|
32
|
+
from .stats import pixel_stats, tide_stats
|
33
|
+
from .utils import clip_models, idw, list_models
|
34
|
+
from .validation import eval_metrics, load_gauge_gesla
|
35
|
+
|
36
|
+
# Define what should be imported with "from eo_tides import *"
|
37
|
+
__all__ = [
|
38
|
+
"list_models",
|
39
|
+
"model_tides",
|
40
|
+
"model_phases",
|
41
|
+
"ensemble_tides",
|
42
|
+
"tag_tides",
|
43
|
+
"pixel_tides",
|
44
|
+
"tide_stats",
|
45
|
+
"pixel_stats",
|
46
|
+
"eval_metrics",
|
47
|
+
"load_gauge_gesla",
|
48
|
+
"clip_models",
|
49
|
+
"idw",
|
50
|
+
]
|
eo_tides/eo.py
ADDED
@@ -0,0 +1,532 @@
|
|
1
|
+
# Used to postpone evaluation of type annotations
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import os
|
5
|
+
import textwrap
|
6
|
+
import warnings
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
import odc.geo.xr
|
11
|
+
import xarray as xr
|
12
|
+
from odc.geo.geobox import GeoBox
|
13
|
+
|
14
|
+
# Only import if running type checking
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from odc.geo import Shape2d
|
17
|
+
|
18
|
+
from .model import model_tides
|
19
|
+
from .utils import DatetimeLike, _standardise_time
|
20
|
+
|
21
|
+
|
22
|
+
def _resample_chunks(
|
23
|
+
data: xr.DataArray | xr.Dataset | GeoBox,
|
24
|
+
dask_chunks: tuple | None = None,
|
25
|
+
) -> tuple | Shape2d:
|
26
|
+
"""
|
27
|
+
Automatically return optimised dask chunks
|
28
|
+
for reprojection with `_pixel_tides_resample`.
|
29
|
+
Use entire image if GeoBox or if no default
|
30
|
+
chunks; use existing chunks if they exist.
|
31
|
+
"""
|
32
|
+
|
33
|
+
# If dask_chunks is provided, return directly
|
34
|
+
if dask_chunks is not None:
|
35
|
+
return dask_chunks
|
36
|
+
|
37
|
+
# If data is a GeoBox, return its shape
|
38
|
+
if isinstance(data, GeoBox):
|
39
|
+
return data.shape
|
40
|
+
|
41
|
+
# if data has chunks, then return just spatial chunks
|
42
|
+
if data.chunks:
|
43
|
+
y_dim, x_dim = data.odc.spatial_dims
|
44
|
+
return data.chunks[y_dim], data.chunks[x_dim]
|
45
|
+
|
46
|
+
# if data has no chunks, then return entire image shape
|
47
|
+
return data.odc.geobox.shape
|
48
|
+
|
49
|
+
|
50
|
+
def _standardise_inputs(
|
51
|
+
data: xr.DataArray | xr.Dataset | GeoBox,
|
52
|
+
time: DatetimeLike | None,
|
53
|
+
) -> tuple[GeoBox, np.ndarray | None]:
|
54
|
+
"""
|
55
|
+
Takes an xarray or GeoBox input and an optional custom times,
|
56
|
+
and returns a standardised GeoBox and times (usually an
|
57
|
+
array, but possibly None).
|
58
|
+
"""
|
59
|
+
|
60
|
+
# If `data` is an xarray object, extract its GeoBox and time
|
61
|
+
if isinstance(data, (xr.DataArray, xr.Dataset)):
|
62
|
+
# Try to extract GeoBox
|
63
|
+
try:
|
64
|
+
gbox: GeoBox = data.odc.geobox
|
65
|
+
except AttributeError:
|
66
|
+
error_msg = """
|
67
|
+
Cannot extract a valid GeoBox for `data`. This is required for
|
68
|
+
extracting details about `data`'s CRS and spatial location.
|
69
|
+
|
70
|
+
Import `odc.geo.xr` then run `data = data.odc.assign_crs(crs=...)`
|
71
|
+
to prepare your data before passing it to this function.
|
72
|
+
"""
|
73
|
+
raise Exception(textwrap.dedent(error_msg).strip())
|
74
|
+
|
75
|
+
# Use custom time by default if provided; otherwise try and extract from `data`
|
76
|
+
if time is not None:
|
77
|
+
time = _standardise_time(time)
|
78
|
+
elif "time" in data.dims:
|
79
|
+
time = np.asarray(data.coords["time"].values)
|
80
|
+
else:
|
81
|
+
raise ValueError("`data` does not have a 'time' dimension, and no custom times were provided via `time`.")
|
82
|
+
|
83
|
+
# If `data` is a GeoBox, use it directly; raise an error if no time was provided
|
84
|
+
elif isinstance(data, GeoBox):
|
85
|
+
gbox = data
|
86
|
+
if time is not None:
|
87
|
+
time = _standardise_time(time)
|
88
|
+
else:
|
89
|
+
raise ValueError("If `data` is a GeoBox, custom times must be provided via `time`.")
|
90
|
+
|
91
|
+
# Raise error if no valid inputs were provided
|
92
|
+
else:
|
93
|
+
raise TypeError("`data` must be an xarray.DataArray, xarray.Dataset, or odc.geo.geobox.GeoBox.")
|
94
|
+
|
95
|
+
return gbox, time
|
96
|
+
|
97
|
+
|
98
|
+
def _pixel_tides_resample(
|
99
|
+
tides_lowres,
|
100
|
+
gbox,
|
101
|
+
resample_method="bilinear",
|
102
|
+
dask_chunks=None,
|
103
|
+
dask_compute=True,
|
104
|
+
name="tide_height",
|
105
|
+
):
|
106
|
+
"""Resamples low resolution tides modelled by `pixel_tides` into the
|
107
|
+
geobox (e.g. spatial resolution and extent) of the original higher
|
108
|
+
resolution satellite dataset.
|
109
|
+
|
110
|
+
Parameters
|
111
|
+
----------
|
112
|
+
tides_lowres : xarray.DataArray
|
113
|
+
The low resolution tide modelling data array to be resampled.
|
114
|
+
gbox : GeoBox
|
115
|
+
The GeoBox to use as the template for the resampling operation.
|
116
|
+
This is typically comes from the same satellite dataset originally
|
117
|
+
passed to `pixel_tides` (e.g. `data.odc.geobox`).
|
118
|
+
resample_method : string, optional
|
119
|
+
The resampling method to use. Defaults to "bilinear"; valid
|
120
|
+
options include "nearest", "cubic", "min", "max", "average" etc.
|
121
|
+
dask_chunks : tuple of float, optional
|
122
|
+
Can be used to configure custom Dask chunking for the final
|
123
|
+
resampling step. For custom chunks, provide a tuple in the form
|
124
|
+
(y, x), e.g. (2048, 2048).
|
125
|
+
dask_compute : bool, optional
|
126
|
+
Whether to compute results of the resampling step using Dask.
|
127
|
+
If False, this will return `tides_highres` as a lazy loaded
|
128
|
+
Dask-enabled array.
|
129
|
+
name : str, optional
|
130
|
+
The name used for the output array. Defaults to "tide_height".
|
131
|
+
|
132
|
+
Returns
|
133
|
+
-------
|
134
|
+
tides_highres : xr.DataArray
|
135
|
+
A high resolution array of tide heights matching the exact
|
136
|
+
spatial resolution and extent of `gbox`.
|
137
|
+
|
138
|
+
"""
|
139
|
+
# Determine spatial dimensions
|
140
|
+
y_dim, x_dim = gbox.dimensions
|
141
|
+
|
142
|
+
# Convert array to Dask, using no chunking along y and x dims,
|
143
|
+
# and a single chunk for each timestep/quantile and tide model
|
144
|
+
tides_lowres_dask = tides_lowres.chunk({d: None if d in [y_dim, x_dim] else 1 for d in tides_lowres.dims})
|
145
|
+
|
146
|
+
# Reproject into the pixel grid of `gbox` using odc.geo and Dask
|
147
|
+
tides_highres = tides_lowres_dask.odc.reproject(
|
148
|
+
how=gbox,
|
149
|
+
chunks=dask_chunks,
|
150
|
+
resampling=resample_method,
|
151
|
+
)
|
152
|
+
|
153
|
+
# Set output name
|
154
|
+
if name is not None:
|
155
|
+
tides_highres = tides_highres.rename(name)
|
156
|
+
|
157
|
+
# Optionally process and load into memory with Dask
|
158
|
+
if dask_compute:
|
159
|
+
tides_highres.load()
|
160
|
+
|
161
|
+
return tides_highres
|
162
|
+
|
163
|
+
|
164
|
+
def tag_tides(
|
165
|
+
data: xr.Dataset | xr.DataArray | GeoBox,
|
166
|
+
time: DatetimeLike | None = None,
|
167
|
+
model: str | list[str] = "EOT20",
|
168
|
+
directory: str | os.PathLike | None = None,
|
169
|
+
tidepost_lat: float | None = None,
|
170
|
+
tidepost_lon: float | None = None,
|
171
|
+
**model_tides_kwargs,
|
172
|
+
) -> xr.DataArray:
|
173
|
+
"""
|
174
|
+
Model tide heights for every timestep in a multi-dimensional
|
175
|
+
dataset, and return a new `tide_height` array that can
|
176
|
+
be used to "tag" each observation with tide heights.
|
177
|
+
|
178
|
+
The function models tides at the centroid of the dataset
|
179
|
+
by default, but a custom tidal modelling location can
|
180
|
+
be specified using `tidepost_lat` and `tidepost_lon`.
|
181
|
+
|
182
|
+
This function uses the parallelised `model_tides` function
|
183
|
+
under the hood. It supports all tidal models supported by
|
184
|
+
`pyTMD`, including:
|
185
|
+
|
186
|
+
- Empirical Ocean Tide model (EOT20)
|
187
|
+
- Finite Element Solution tide models (FES2022, FES2014, FES2012)
|
188
|
+
- TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
|
189
|
+
- Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
|
190
|
+
- Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
|
191
|
+
|
192
|
+
Parameters
|
193
|
+
----------
|
194
|
+
data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox
|
195
|
+
A multi-dimensional dataset or GeoBox pixel grid that will
|
196
|
+
be used to define the tide modelling location. If `data`
|
197
|
+
is an xarray object, it should include a "time" dimension.
|
198
|
+
If no "time" dimension exists or if `data` is a GeoBox,
|
199
|
+
then times must be passed using the `time` parameter.
|
200
|
+
time : DatetimeLike, optional
|
201
|
+
By default, tides will be modelled using times from the
|
202
|
+
"time" dimension of `data`. Alternatively, this param can
|
203
|
+
be used to provide a custom set of times. Accepts any format
|
204
|
+
that can be converted by `pandas.to_datetime()`. For example:
|
205
|
+
`time=pd.date_range(start="2000", end="2001", freq="5h")`
|
206
|
+
model : str or list of str, optional
|
207
|
+
The tide model (or list of models) to use to model tides.
|
208
|
+
If a list is provided, a new "tide_model" dimension will be
|
209
|
+
added to the `xarray.DataArray` outputs. Defaults to "EOT20";
|
210
|
+
specify "all" to use all models available in `directory`.
|
211
|
+
For a full list of available and supported models, run
|
212
|
+
`eo_tides.utils.list_models`.
|
213
|
+
directory : str, optional
|
214
|
+
The directory containing tide model data files. If no path is
|
215
|
+
provided, this will default to the environment variable
|
216
|
+
`EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
|
217
|
+
Tide modelling files should be stored in sub-folders for each
|
218
|
+
model that match the structure required by `pyTMD`
|
219
|
+
(<https://geoscienceaustralia.github.io/eo-tides/setup/>).
|
220
|
+
tidepost_lat, tidepost_lon : float, optional
|
221
|
+
Optional coordinates used to model tides. The default is None,
|
222
|
+
which uses the centroid of the dataset as the tide modelling
|
223
|
+
location.
|
224
|
+
**model_tides_kwargs :
|
225
|
+
Optional parameters passed to the `eo_tides.model.model_tides`
|
226
|
+
function. Important parameters include `cutoff` (used to
|
227
|
+
extrapolate modelled tides away from the coast; defaults to
|
228
|
+
`np.inf`), `crop` (whether to crop tide model constituent files
|
229
|
+
on-the-fly to improve performance) etc.
|
230
|
+
|
231
|
+
Returns
|
232
|
+
-------
|
233
|
+
tides_da : xr.DataArray
|
234
|
+
A one-dimensional tide height array. This will contain either
|
235
|
+
tide heights for every timestep in `data`, or for every time in
|
236
|
+
`times` if provided.
|
237
|
+
"""
|
238
|
+
# Standardise data inputs, time and models
|
239
|
+
gbox, time_coords = _standardise_inputs(data, time)
|
240
|
+
model = [model] if isinstance(model, str) else model
|
241
|
+
|
242
|
+
# If custom tide posts are not provided, use dataset centroid
|
243
|
+
if tidepost_lat is None or tidepost_lon is None:
|
244
|
+
lon, lat = gbox.geographic_extent.centroid.coords[0]
|
245
|
+
print(f"Setting tide modelling location from dataset centroid: {lon:.2f}, {lat:.2f}")
|
246
|
+
else:
|
247
|
+
lon, lat = tidepost_lon, tidepost_lat
|
248
|
+
print(f"Using tide modelling location: {lon:.2f}, {lat:.2f}")
|
249
|
+
|
250
|
+
# Model tide heights for each observation:
|
251
|
+
tide_df = model_tides(
|
252
|
+
x=lon, # type: ignore
|
253
|
+
y=lat, # type: ignore
|
254
|
+
time=time_coords,
|
255
|
+
model=model,
|
256
|
+
directory=directory,
|
257
|
+
crs="EPSG:4326",
|
258
|
+
**model_tides_kwargs,
|
259
|
+
)
|
260
|
+
|
261
|
+
# If tides cannot be successfully modeled (e.g. if the centre of the
|
262
|
+
# xarray dataset is located is over land), raise an exception
|
263
|
+
if tide_df.tide_height.isnull().all():
|
264
|
+
raise ValueError(
|
265
|
+
f"Tides could not be modelled for dataset centroid located "
|
266
|
+
f"at {tidepost_lon:.2f}, {tidepost_lat:.2f}. This can occur if "
|
267
|
+
f"this coordinate occurs over land. Please manually specify "
|
268
|
+
f"a tide modelling location located over water using the "
|
269
|
+
f"`tidepost_lat` and `tidepost_lon` parameters."
|
270
|
+
)
|
271
|
+
|
272
|
+
# Convert to xarray format
|
273
|
+
tides_da = tide_df.reset_index().set_index(["time", "tide_model"]).drop(["x", "y"], axis=1).tide_height.to_xarray()
|
274
|
+
|
275
|
+
# If only one tidal model exists, squeeze out "tide_model" dim
|
276
|
+
if len(tides_da.tide_model) == 1:
|
277
|
+
tides_da = tides_da.squeeze("tide_model")
|
278
|
+
|
279
|
+
return tides_da
|
280
|
+
|
281
|
+
|
282
|
+
def pixel_tides(
|
283
|
+
data: xr.Dataset | xr.DataArray | GeoBox,
|
284
|
+
time: DatetimeLike | None = None,
|
285
|
+
model: str | list[str] = "EOT20",
|
286
|
+
directory: str | os.PathLike | None = None,
|
287
|
+
resample: bool = True,
|
288
|
+
calculate_quantiles: np.ndarray | tuple[float, float] | None = None,
|
289
|
+
resolution: float | None = None,
|
290
|
+
buffer: float | None = None,
|
291
|
+
resample_method: str = "bilinear",
|
292
|
+
dask_chunks: tuple[float, float] | None = None,
|
293
|
+
dask_compute: bool = True,
|
294
|
+
**model_tides_kwargs,
|
295
|
+
) -> xr.DataArray:
|
296
|
+
"""
|
297
|
+
Model tide heights for every pixel in a multi-dimensional
|
298
|
+
dataset, using one or more ocean tide models.
|
299
|
+
|
300
|
+
This function models tides into a low-resolution tide
|
301
|
+
modelling grid covering the spatial extent of the input
|
302
|
+
data (buffered to reduce potential edge effects). These
|
303
|
+
modelled tides can then be resampled back into the original
|
304
|
+
higher resolution dataset's extent and resolution to
|
305
|
+
produce a modelled tide height for every pixel through time.
|
306
|
+
|
307
|
+
This function uses the parallelised `model_tides` function
|
308
|
+
under the hood. It supports all tidal models supported by
|
309
|
+
`pyTMD`, including:
|
310
|
+
|
311
|
+
- Empirical Ocean Tide model (EOT20)
|
312
|
+
- Finite Element Solution tide models (FES2022, FES2014, FES2012)
|
313
|
+
- TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
|
314
|
+
- Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
|
315
|
+
- Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
|
316
|
+
|
317
|
+
This function requires access to tide model data files.
|
318
|
+
These should be placed in a folder with subfolders matching
|
319
|
+
the structure required by `pyTMD`. For more details:
|
320
|
+
<https://geoscienceaustralia.github.io/eo-tides/setup/>
|
321
|
+
<https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories>
|
322
|
+
|
323
|
+
Parameters
|
324
|
+
----------
|
325
|
+
data : xarray.Dataset or xarray.DataArray or odc.geo.geobox.GeoBox
|
326
|
+
A multi-dimensional dataset or GeoBox pixel grid that will
|
327
|
+
be used to define the spatial tide modelling grid. If `data`
|
328
|
+
is an xarray object, it should include a "time" dimension.
|
329
|
+
If no "time" dimension exists or if `data` is a GeoBox,
|
330
|
+
then times must be passed using the `time` parameter.
|
331
|
+
time : DatetimeLike, optional
|
332
|
+
By default, tides will be modelled using times from the
|
333
|
+
"time" dimension of `data`. Alternatively, this param can
|
334
|
+
be used to provide a custom set of times. Accepts any format
|
335
|
+
that can be converted by `pandas.to_datetime()`. For example:
|
336
|
+
`time=pd.date_range(start="2000", end="2001", freq="5h")`
|
337
|
+
model : str or list of str, optional
|
338
|
+
The tide model (or list of models) to use to model tides.
|
339
|
+
If a list is provided, a new "tide_model" dimension will be
|
340
|
+
added to the `xarray.DataArray` outputs. Defaults to "EOT20";
|
341
|
+
specify "all" to use all models available in `directory`.
|
342
|
+
For a full list of available and supported models, run
|
343
|
+
`eo_tides.utils.list_models`.
|
344
|
+
directory : str, optional
|
345
|
+
The directory containing tide model data files. If no path is
|
346
|
+
provided, this will default to the environment variable
|
347
|
+
`EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
|
348
|
+
Tide modelling files should be stored in sub-folders for each
|
349
|
+
model that match the structure required by `pyTMD`
|
350
|
+
(<https://geoscienceaustralia.github.io/eo-tides/setup/>).
|
351
|
+
resample : bool, optional
|
352
|
+
Whether to resample low resolution tides back into `data`'s original
|
353
|
+
higher resolution grid. Set this to `False` if you do not want
|
354
|
+
low resolution tides to be re-projected back to higher resolution.
|
355
|
+
calculate_quantiles : tuple of float or numpy.ndarray, optional
|
356
|
+
Rather than returning all individual tides, low-resolution tides
|
357
|
+
can be first aggregated using a quantile calculation by passing in
|
358
|
+
a tuple or array of quantiles to compute. For example, this could
|
359
|
+
be used to calculate the min/max tide across all times:
|
360
|
+
`calculate_quantiles=(0.0, 1.0)`.
|
361
|
+
resolution : float, optional
|
362
|
+
The desired resolution of the low-resolution grid used for tide
|
363
|
+
modelling. The default None will create a 5000 m resolution grid
|
364
|
+
if `data` has a projected CRS (i.e. metre units), or a 0.05 degree
|
365
|
+
resolution grid if `data` has a geographic CRS (e.g. degree units).
|
366
|
+
Note: higher resolutions do not necessarily provide better
|
367
|
+
tide modelling performance, as results will be limited by the
|
368
|
+
resolution of the underlying global tide model (e.g. 1/16th
|
369
|
+
degree / ~5 km resolution grid for FES2014).
|
370
|
+
buffer : float, optional
|
371
|
+
The amount by which to buffer the higher resolution grid extent
|
372
|
+
when creating the new low resolution grid. This buffering
|
373
|
+
ensures that modelled tides are seamless across analysis
|
374
|
+
boundaries. This buffer is eventually be clipped away when
|
375
|
+
the low-resolution modelled tides are re-projected back to the
|
376
|
+
original resolution and extent of `data`. To ensure that at least
|
377
|
+
two low-resolution grid pixels occur outside of the dataset
|
378
|
+
bounds, the default None applies a 12000 m buffer if `data` has a
|
379
|
+
projected CRS (i.e. metre units), or a 0.12 degree buffer if
|
380
|
+
`data` has a geographic CRS (e.g. degree units).
|
381
|
+
resample_method : str, optional
|
382
|
+
If resampling is requested (see `resample` above), use this
|
383
|
+
resampling method when resampling from low resolution to high
|
384
|
+
resolution pixels. Defaults to "bilinear"; valid options include
|
385
|
+
"nearest", "cubic", "min", "max", "average" etc.
|
386
|
+
dask_chunks : tuple of float, optional
|
387
|
+
Can be used to configure custom Dask chunking for the final
|
388
|
+
resampling step. By default, chunks will be automatically set
|
389
|
+
to match y/x chunks from `data` if they exist; otherwise chunks
|
390
|
+
will be chosen to cover the entire y/x extent of the dataset.
|
391
|
+
For custom chunks, provide a tuple in the form `(y, x)`, e.g.
|
392
|
+
`(2048, 2048)`.
|
393
|
+
dask_compute : bool, optional
|
394
|
+
Whether to compute results of the resampling step using Dask.
|
395
|
+
If False, `tides_highres` will be returned as a Dask-enabled array.
|
396
|
+
**model_tides_kwargs :
|
397
|
+
Optional parameters passed to the `eo_tides.model.model_tides`
|
398
|
+
function. Important parameters include `cutoff` (used to
|
399
|
+
extrapolate modelled tides away from the coast; defaults to
|
400
|
+
`np.inf`), `crop` (whether to crop tide model constituent files
|
401
|
+
on-the-fly to improve performance) etc.
|
402
|
+
Returns
|
403
|
+
-------
|
404
|
+
tides_da : xr.DataArray
|
405
|
+
A three-dimensional tide height array.
|
406
|
+
If `resample=True` (default), a high-resolution array of tide
|
407
|
+
heights will be returned that matches the exact spatial resolution
|
408
|
+
and extents of `data`. This will contain either tide heights for
|
409
|
+
every timestep in `data` (or in `times` if provided), or tide height
|
410
|
+
quantiles for every quantile provided by `calculate_quantiles`.
|
411
|
+
If `resample=False`, results for the intermediate low-resolution
|
412
|
+
tide modelling grid will be returned instead.
|
413
|
+
"""
|
414
|
+
# Standardise data inputs, time and models
|
415
|
+
gbox, time_coords = _standardise_inputs(data, time)
|
416
|
+
dask_chunks = _resample_chunks(data, dask_chunks)
|
417
|
+
model = [model] if isinstance(model, str) else model
|
418
|
+
|
419
|
+
# Determine spatial dimensions
|
420
|
+
y_dim, x_dim = gbox.dimensions
|
421
|
+
|
422
|
+
# Determine resolution and buffer, using different defaults for
|
423
|
+
# geographic (i.e. degrees) and projected (i.e. metres) CRSs:
|
424
|
+
assert gbox.crs is not None
|
425
|
+
crs_units = gbox.crs.units[0][0:6]
|
426
|
+
if gbox.crs.geographic:
|
427
|
+
if resolution is None:
|
428
|
+
resolution = 0.05
|
429
|
+
elif resolution > 360:
|
430
|
+
raise ValueError(
|
431
|
+
f"A resolution of greater than 360 was "
|
432
|
+
f"provided, but `data` has a geographic CRS "
|
433
|
+
f"in {crs_units} units. Did you accidently "
|
434
|
+
f"provide a resolution in projected "
|
435
|
+
f"(i.e. metre) units?",
|
436
|
+
)
|
437
|
+
if buffer is None:
|
438
|
+
buffer = 0.12
|
439
|
+
else:
|
440
|
+
if resolution is None:
|
441
|
+
resolution = 5000
|
442
|
+
elif resolution < 1:
|
443
|
+
raise ValueError(
|
444
|
+
f"A resolution of less than 1 was provided, "
|
445
|
+
f"but `data` has a projected CRS in "
|
446
|
+
f"{crs_units} units. Did you accidently "
|
447
|
+
f"provide a resolution in geographic "
|
448
|
+
f"(degree) units?",
|
449
|
+
)
|
450
|
+
if buffer is None:
|
451
|
+
buffer = 12000
|
452
|
+
|
453
|
+
# Raise error if resolution is less than dataset resolution
|
454
|
+
dataset_res = gbox.resolution.x
|
455
|
+
if resolution < dataset_res:
|
456
|
+
raise ValueError(
|
457
|
+
f"The resolution of the low-resolution tide "
|
458
|
+
f"modelling grid ({resolution:.2f}) is less "
|
459
|
+
f"than `data`'s pixel resolution ({dataset_res:.2f}). "
|
460
|
+
f"This can cause extremely slow tide modelling "
|
461
|
+
f"performance. Please select provide a resolution "
|
462
|
+
f"greater than {dataset_res:.2f} using "
|
463
|
+
f"`pixel_tides`'s 'resolution' parameter.",
|
464
|
+
)
|
465
|
+
|
466
|
+
# Create a new reduced resolution tide modelling grid after
|
467
|
+
# first buffering the grid
|
468
|
+
print(f"Creating reduced resolution {resolution} x {resolution} {crs_units} tide modelling array")
|
469
|
+
buffered_geobox = gbox.buffered(buffer)
|
470
|
+
rescaled_geobox = GeoBox.from_bbox(bbox=buffered_geobox.boundingbox, resolution=resolution)
|
471
|
+
rescaled_ds = odc.geo.xr.xr_zeros(rescaled_geobox)
|
472
|
+
|
473
|
+
# Flatten grid to 1D, then add time dimension
|
474
|
+
flattened_ds = rescaled_ds.stack(z=(x_dim, y_dim))
|
475
|
+
flattened_ds = flattened_ds.expand_dims(dim={"time": time_coords})
|
476
|
+
|
477
|
+
# Model tides in parallel, returning a pandas.DataFrame
|
478
|
+
tide_df = model_tides(
|
479
|
+
x=flattened_ds[x_dim],
|
480
|
+
y=flattened_ds[y_dim],
|
481
|
+
time=flattened_ds.time,
|
482
|
+
crs=f"EPSG:{gbox.crs.epsg}",
|
483
|
+
model=model,
|
484
|
+
directory=directory,
|
485
|
+
**model_tides_kwargs,
|
486
|
+
)
|
487
|
+
|
488
|
+
# Convert our pandas.DataFrame tide modelling outputs to xarray
|
489
|
+
tides_lowres = (
|
490
|
+
# Rename x and y dataframe indexes to match x and y xarray dims
|
491
|
+
tide_df.rename_axis(["time", x_dim, y_dim])
|
492
|
+
# Add tide model column to dataframe indexes so we can convert
|
493
|
+
# our dataframe to a multidimensional xarray
|
494
|
+
.set_index("tide_model", append=True)
|
495
|
+
# Convert to xarray and select our tide modelling xr.DataArray
|
496
|
+
.to_xarray()
|
497
|
+
.tide_height
|
498
|
+
# Re-index and transpose into our input coordinates and dim order
|
499
|
+
.reindex_like(rescaled_ds)
|
500
|
+
.transpose("tide_model", "time", y_dim, x_dim)
|
501
|
+
)
|
502
|
+
|
503
|
+
# Optionally calculate and return quantiles rather than raw data.
|
504
|
+
# Set dtype to dtype of the input data as quantile always returns
|
505
|
+
# float64 (memory intensive)
|
506
|
+
if calculate_quantiles is not None:
|
507
|
+
with warnings.catch_warnings():
|
508
|
+
warnings.simplefilter("ignore")
|
509
|
+
print("Computing tide quantiles")
|
510
|
+
tides_lowres = tides_lowres.quantile(q=calculate_quantiles, dim="time").astype(tides_lowres.dtype)
|
511
|
+
|
512
|
+
# If only one tidal model exists, squeeze out "tide_model" dim
|
513
|
+
if len(tides_lowres.tide_model) == 1:
|
514
|
+
tides_lowres = tides_lowres.squeeze("tide_model")
|
515
|
+
|
516
|
+
# Ensure CRS is present before we apply any resampling
|
517
|
+
tides_lowres = tides_lowres.odc.assign_crs(gbox.crs)
|
518
|
+
|
519
|
+
# Reproject into original high resolution grid
|
520
|
+
if resample:
|
521
|
+
print("Reprojecting tides into original resolution")
|
522
|
+
tides_highres = _pixel_tides_resample(
|
523
|
+
tides_lowres,
|
524
|
+
gbox,
|
525
|
+
resample_method,
|
526
|
+
dask_chunks,
|
527
|
+
dask_compute,
|
528
|
+
)
|
529
|
+
return tides_highres
|
530
|
+
|
531
|
+
print("Returning low resolution tide array")
|
532
|
+
return tides_lowres
|