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 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