eo-tides 0.0.19__py3-none-any.whl → 0.0.21__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 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