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