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