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/model.py CHANGED
@@ -1,19 +1,27 @@
1
+ # Used to postpone evaluation of type annotations
2
+ from __future__ import annotations
3
+
1
4
  import os
2
5
  import pathlib
3
6
  import warnings
4
7
  from concurrent.futures import ProcessPoolExecutor
5
8
  from functools import partial
9
+ from typing import TYPE_CHECKING
10
+
11
+ # Only import if running type checking
12
+ if TYPE_CHECKING:
13
+ import xarray as xr
6
14
 
7
15
  import geopandas as gpd
8
16
  import numpy as np
9
- import odc.geo.xr
10
17
  import pandas as pd
11
18
  import pyproj
12
19
  import pyTMD
20
+ from colorama import Style, init
13
21
  from pyTMD.io.model import load_database, model
14
22
  from tqdm import tqdm
15
23
 
16
- from eo_tides.utils import idw
24
+ from .utils import idw
17
25
 
18
26
 
19
27
  def _set_directory(directory):
@@ -40,7 +48,12 @@ def _set_directory(directory):
40
48
  return directory
41
49
 
42
50
 
43
- def list_models(directory=None, show_available=True, show_supported=True, raise_error=False):
51
+ def list_models(
52
+ directory: str | os.PathLike | None = None,
53
+ show_available: bool = True,
54
+ show_supported: bool = True,
55
+ raise_error: bool = False,
56
+ ) -> tuple[list[str], list[str]]:
44
57
  """
45
58
  List all tide models available for tide modelling, and
46
59
  all models supported by `eo-tides` and `pyTMD`.
@@ -54,7 +67,7 @@ def list_models(directory=None, show_available=True, show_supported=True, raise_
54
67
 
55
68
  Parameters
56
69
  ----------
57
- directory : string, optional
70
+ directory : str, optional
58
71
  The directory containing tide model data files. If no path is
59
72
  provided, this will default to the environment variable
60
73
  `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
@@ -66,45 +79,79 @@ def list_models(directory=None, show_available=True, show_supported=True, raise_
66
79
  show_supported : bool, optional
67
80
  Whether to print a list of all supported models, in
68
81
  addition to models available locally.
82
+ raise_error : bool, optional
83
+ If True, raise an error if no available models are found.
84
+ If False, raise a warning.
69
85
 
70
86
  Returns
71
87
  -------
72
- available_models : list
73
- A list of alltide models available within `directory`.
74
- supported_models : list
88
+ available_models : list of str
89
+ A list of all tide models available within `directory`.
90
+ supported_models : list of str
75
91
  A list of all tide models supported by `eo-tides`.
76
92
  """
93
+ init() # Initialize colorama
94
+
77
95
  # Set tide modelling files directory. If no custom path is
78
96
  # provided, try global environment variable.
79
97
  directory = _set_directory(directory)
80
98
 
81
99
  # Get full list of supported models from pyTMD database
82
- supported_models = list(load_database()["elevation"].keys())
100
+ model_database = load_database()["elevation"]
101
+ supported_models = list(model_database.keys())
102
+
103
+ # Extract expected model paths
104
+ expected_paths = {}
105
+ for m in supported_models:
106
+ model_file = model_database[m]["model_file"]
107
+ model_file = model_file[0] if isinstance(model_file, list) else model_file
108
+ expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)
109
+
110
+ # Define column widths
111
+ status_width = 4 # Width for emoji
112
+ name_width = max(len(name) for name in supported_models)
113
+ path_width = max(len(path) for path in expected_paths.values())
83
114
 
84
115
  # Print list of supported models, marking available and
85
116
  # unavailable models and appending available to list
86
117
  if show_available or show_supported:
87
- print(f"Tide models available in `{directory}`:")
118
+ total_width = min(status_width + name_width + path_width + 6, 80)
119
+ print("─" * total_width)
120
+ print(f"{'󠀠🌊':^{status_width}} | {'Model':<{name_width}} | {'Expected path':<{path_width}}")
121
+ print("─" * total_width)
122
+
88
123
  available_models = []
89
124
  for m in supported_models:
90
125
  try:
91
- model(directory=directory).elevation(m=m)
126
+ model_file = model(directory=directory).elevation(m=m)
127
+ available_models.append(m)
128
+
92
129
  if show_available:
93
130
  # Mark available models with a green tick
94
- print(f" {m}")
95
- available_models.append(m)
131
+ status = "✅"
132
+ print(f"{status:^{status_width}}│ {m:<{name_width}} │ {expected_paths[m]:<{path_width}}")
96
133
  except:
97
134
  if show_supported:
98
135
  # Mark unavailable models with a red cross
99
- print(f" {m}")
136
+ status = "❌"
137
+ print(
138
+ f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}"
139
+ )
140
+
141
+ if show_available or show_supported:
142
+ print("─" * total_width)
143
+
144
+ # Print summary
145
+ print(f"\n{Style.BRIGHT}Summary:{Style.RESET_ALL}")
146
+ print(f"Available models: {len(available_models)}/{len(supported_models)}")
100
147
 
101
148
  # Raise error or warning if no models are available
102
149
  if not available_models:
103
150
  warning_text = (
104
151
  f"No valid tide models are available in `{directory}`. "
105
- "Verify that you have provided the correct `directory` path, "
152
+ "Are you sure you have provided the correct `directory` path, "
106
153
  "or set the `EO_TIDES_TIDE_MODELS` environment variable "
107
- "to point to the location of your tide model directory."
154
+ "to point to the location of your tide model directory?"
108
155
  )
109
156
  if raise_error:
110
157
  raise Exception(warning_text)
@@ -196,7 +243,7 @@ def _model_tides(
196
243
  lon,
197
244
  lat,
198
245
  pytmd_model.model_file,
199
- grid=pytmd_model.type,
246
+ grid=pytmd_model.file_format,
200
247
  crop=crop,
201
248
  bounds=bounds,
202
249
  method=method,
@@ -230,6 +277,10 @@ def _model_tides(
230
277
 
231
278
  # Delta time (TT - UT1)
232
279
  deltat = timescale.tt_ut1
280
+ else:
281
+ raise Exception(
282
+ f"Unsupported model format ({pytmd_model.format}). This may be due to an incompatible version of `pyTMD`."
283
+ )
233
284
 
234
285
  # Calculate complex phase in radians for Euler's
235
286
  cph = -1j * ph * np.pi / 180.0
@@ -296,10 +347,8 @@ def _model_tides(
296
347
 
297
348
 
298
349
  def _ensemble_model(
299
- x,
300
- y,
301
- crs,
302
350
  tide_df,
351
+ crs,
303
352
  ensemble_models,
304
353
  ensemble_func=None,
305
354
  ensemble_top_n=3,
@@ -313,29 +362,27 @@ def _ensemble_model(
313
362
  to inform the selection of the best local models.
314
363
 
315
364
  This function performs the following steps:
365
+ 1. Takes a dataframe of tide heights from multiple tide models, as
366
+ produced by `eo_tides.model.model_tides`
316
367
  1. Loads model ranking points from a GeoJSON file, filters them
317
368
  based on the valid data percentage, and retains relevant columns
318
- 2. Interpolates the model rankings into the requested x and y
319
- coordinates using Inverse Weighted Interpolation (IDW)
369
+ 2. Interpolates the model rankings into the "x" and "y" coordinates
370
+ of the original dataframe using Inverse Weighted Interpolation (IDW)
320
371
  3. Uses rankings to combine multiple tide models into a single
321
372
  optimised ensemble model (by default, by taking the mean of the
322
373
  top 3 ranked models)
323
- 4. Returns a DataFrame with the combined ensemble model predictions
374
+ 4. Returns a new dataFrame with the combined ensemble model predictions
324
375
 
325
376
  Parameters
326
377
  ----------
327
- x : array-like
328
- Array of x-coordinates where the ensemble model predictions are
329
- required.
330
- y : array-like
331
- Array of y-coordinates where the ensemble model predictions are
332
- required.
333
- crs : string
334
- Input coordinate reference system for x and y coordinates. Used
335
- to ensure that interpolations are performed in the correct CRS.
336
378
  tide_df : pandas.DataFrame
337
- DataFrame containing tide model predictions with columns
379
+ DataFrame produced by `eo_tides.model.model_tides`, containing
380
+ tide model predictions with columns:
338
381
  `["time", "x", "y", "tide_height", "tide_model"]`.
382
+ crs : string
383
+ Coordinate reference system for the "x" and "y" coordinates in
384
+ `tide_df`. Used to ensure that interpolations are performed
385
+ in the correct CRS.
339
386
  ensemble_models : list
340
387
  A list of models to include in the ensemble modelling process.
341
388
  All values must exist as columns with the prefix "rank_" in
@@ -354,7 +401,7 @@ def _ensemble_model(
354
401
  ranking_points : str, optional
355
402
  Path to the GeoJSON file containing model ranking points. This
356
403
  dataset should include columns containing rankings for each tide
357
- model, named with the prefix "rank_". e.g. "rank_FES2014".
404
+ model, named with the prefix "rank_". e.g. "rank_EOT20".
358
405
  Low values should represent high rankings (e.g. 1 = top ranked).
359
406
  ranking_valid_perc : float, optional
360
407
  Minimum percentage of valid data required to include a model
@@ -379,6 +426,10 @@ def _ensemble_model(
379
426
  the provided dictionary keys).
380
427
 
381
428
  """
429
+ # Extract x and y coords from dataframe
430
+ x = tide_df.index.get_level_values(level="x")
431
+ y = tide_df.index.get_level_values(level="y")
432
+
382
433
  # Load model ranks points and reproject to same CRS as x and y
383
434
  model_ranking_cols = [f"rank_{m}" for m in ensemble_models]
384
435
  model_ranks_gdf = (
@@ -461,36 +512,36 @@ def _ensemble_model(
461
512
 
462
513
 
463
514
  def model_tides(
464
- x,
465
- y,
466
- time,
467
- model="FES2014",
468
- directory=None,
469
- crs="EPSG:4326",
470
- crop=True,
471
- method="spline",
472
- extrapolate=True,
473
- cutoff=None,
474
- mode="one-to-many",
475
- parallel=True,
476
- parallel_splits=5,
477
- output_units="m",
478
- output_format="long",
479
- ensemble_models=None,
515
+ x: float | list[float] | xr.DataArray,
516
+ y: float | list[float] | xr.DataArray,
517
+ time: np.ndarray | pd.DatetimeIndex,
518
+ model: str | list[str] = "EOT20",
519
+ directory: str | os.PathLike | None = None,
520
+ crs: str = "EPSG:4326",
521
+ crop: bool = True,
522
+ method: str = "spline",
523
+ extrapolate: bool = True,
524
+ cutoff: float | None = None,
525
+ mode: str = "one-to-many",
526
+ parallel: bool = True,
527
+ parallel_splits: int = 5,
528
+ output_units: str = "m",
529
+ output_format: str = "long",
530
+ ensemble_models: list[str] | None = None,
480
531
  **ensemble_kwargs,
481
- ):
532
+ ) -> pd.DataFrame:
482
533
  """
483
- Compute tide heights from multiple tide models and for
484
- multiple coordinates and/or timesteps.
534
+ Model tide heights at multiple coordinates and/or timesteps
535
+ using using one or more ocean tide models.
485
536
 
486
537
  This function is parallelised to improve performance, and
487
538
  supports all tidal models supported by `pyTMD`, including:
488
539
 
489
- - Empirical Ocean Tide model (`EOT20`)
490
- - Finite Element Solution tide models (`FES2022`, `FES2014`, `FES2012`)
491
- - TOPEX/POSEIDON global tide models (`TPXO10`, `TPXO9`, `TPXO8`)
492
- - Global Ocean Tide models (`GOT5.6`, `GOT5.5`, `GOT4.10`, `GOT4.8`, `GOT4.7`)
493
- - Hamburg direct data Assimilation Methods for Tides models (`HAMTIDE11`)
540
+ - Empirical Ocean Tide model (EOT20)
541
+ - Finite Element Solution tide models (FES2022, FES2014, FES2012)
542
+ - TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
543
+ - Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
544
+ - Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
494
545
 
495
546
  This function requires access to tide model data files.
496
547
  These should be placed in a folder with subfolders matching
@@ -499,52 +550,39 @@ def model_tides(
499
550
  <https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories>
500
551
 
501
552
  This function is a modification of the `pyTMD` package's
502
- `compute_tide_corrections` function. For more info:
503
- <https://pytmd.readthedocs.io/en/stable/user_guide/compute_tide_corrections.html>
553
+ `compute_tidal_elevations` function. For more info:
554
+ <https://pytmd.readthedocs.io/en/latest/api_reference/compute_tidal_elevations.html>
504
555
 
505
556
  Parameters
506
557
  ----------
507
- x, y : float or list of floats
558
+ x, y : float or list of float
508
559
  One or more x and y coordinates used to define
509
560
  the location at which to model tides. By default these
510
561
  coordinates should be lat/lon; use "crs" if they
511
562
  are in a custom coordinate reference system.
512
- time : A datetime array or pandas.DatetimeIndex
563
+ time : Numpy datetime array or pandas.DatetimeIndex
513
564
  An array containing `datetime64[ns]` values or a
514
565
  `pandas.DatetimeIndex` providing the times at which to
515
566
  model tides in UTC time.
516
- model : string, optional
517
- The tide model used to model tides. Options include:
518
-
519
- - "EOT20"
520
- - "FES2014"
521
- - "FES2022"
522
- - "TPXO9-atlas-v5"
523
- - "TPXO8-atlas"
524
- - "HAMTIDE11"
525
- - "GOT4.10"
526
- - "ensemble" (advanced ensemble tide model functionality;
527
- combining multiple models based on external model rankings)
528
- directory : string, optional
567
+ model : str or list of str, optional
568
+ The tide model (or models) to use to model tides.
569
+ Defaults to "EOT20"; for a full list of available/supported
570
+ models, run `eo_tides.model.list_models`.
571
+ directory : str, optional
529
572
  The directory containing tide model data files. If no path is
530
573
  provided, this will default to the environment variable
531
574
  `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
532
575
  Tide modelling files should be stored in sub-folders for each
533
- model that match the structure provided by `pyTMD`.
534
-
535
- For example:
536
-
537
- - `{directory}/fes2014/ocean_tide/`
538
- - `{directory}/tpxo8_atlas/`
539
- - `{directory}/TPXO9_atlas_v5/`
576
+ model that match the structure required by `pyTMD`
577
+ (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
540
578
  crs : str, optional
541
579
  Input coordinate reference system for x and y coordinates.
542
580
  Defaults to "EPSG:4326" (WGS84; degrees latitude, longitude).
543
- crop : bool optional
581
+ crop : bool, optional
544
582
  Whether to crop tide model constituent files on-the-fly to
545
583
  improve performance. Cropping will be performed based on a
546
584
  1 degree buffer around all input points. Defaults to True.
547
- method : string, optional
585
+ method : str, optional
548
586
  Method used to interpolate tidal constituents
549
587
  from model files. Options include:
550
588
 
@@ -554,11 +592,11 @@ def model_tides(
554
592
  extrapolate : bool, optional
555
593
  Whether to extrapolate tides for x and y coordinates outside of
556
594
  the valid tide modelling domain using nearest-neighbor.
557
- cutoff : int or float, optional
595
+ cutoff : float, optional
558
596
  Extrapolation cutoff in kilometers. The default is None, which
559
597
  will extrapolate for all points regardless of distance from the
560
598
  valid tide modelling domain.
561
- mode : string, optional
599
+ mode : str, optional
562
600
  The analysis mode to use for tide modelling. Supports two options:
563
601
 
564
602
  - "one-to-many": Models tides for every timestep in "time" at
@@ -570,7 +608,7 @@ def model_tides(
570
608
  set of x and y coordinates. In this mode, the number of x and
571
609
  y points must equal the number of timesteps provided in "time".
572
610
 
573
- parallel : boolean, optional
611
+ parallel : bool, optional
574
612
  Whether to parallelise tide modelling using `concurrent.futures`.
575
613
  If multiple tide models are requested, these will be run in
576
614
  parallel. Optionally, tide modelling can also be run in parallel
@@ -594,7 +632,7 @@ def model_tides(
594
632
  results stacked vertically along "tide_model" and "tide_height"
595
633
  columns), or wide format (with a column for each tide model).
596
634
  Defaults to "long".
597
- ensemble_models : list, optional
635
+ ensemble_models : list of str, optional
598
636
  An optional list of models used to generate the ensemble tide
599
637
  model if "ensemble" tide modelling is requested. Defaults to
600
638
  ["FES2014", "TPXO9-atlas-v5", "EOT20", "HAMTIDE11", "GOT4.10",
@@ -615,7 +653,7 @@ def model_tides(
615
653
 
616
654
  """
617
655
  # Turn inputs into arrays for consistent handling
618
- models_requested = np.atleast_1d(model)
656
+ models_requested = list(np.atleast_1d(model))
619
657
  x = np.atleast_1d(x)
620
658
  y = np.atleast_1d(y)
621
659
  time = np.atleast_1d(time)
@@ -652,6 +690,9 @@ def model_tides(
652
690
  available_models, valid_models = list_models(
653
691
  directory, show_available=False, show_supported=False, raise_error=True
654
692
  )
693
+ # TODO: This is hacky, find a better way. Perhaps a kwarg that
694
+ # turns ensemble functionality on, and checks that supplied
695
+ # models match models expected for ensemble?
655
696
  available_models = available_models + ["ensemble"]
656
697
  valid_models = valid_models + ["ensemble"]
657
698
 
@@ -767,11 +808,11 @@ def model_tides(
767
808
 
768
809
  # Optionally compute ensemble model and add to dataframe
769
810
  if "ensemble" in models_requested:
770
- ensemble_df = _ensemble_model(x, y, crs, tide_df, models_to_process, **ensemble_kwargs)
811
+ ensemble_df = _ensemble_model(tide_df, crs, models_to_process, **ensemble_kwargs)
771
812
 
772
813
  # Update requested models with any custom ensemble models, then
773
814
  # filter the dataframe to keep only models originally requested
774
- models_requested = np.union1d(models_requested, ensemble_df.tide_model.unique())
815
+ models_requested = list(np.union1d(models_requested, ensemble_df.tide_model.unique()))
775
816
  tide_df = pd.concat([tide_df, ensemble_df]).query("tide_model in @models_requested")
776
817
 
777
818
  # Optionally convert to a wide format dataframe with a tide model in
@@ -788,362 +829,3 @@ def model_tides(
788
829
  tide_df = tide_df.reindex(output_indices)
789
830
 
790
831
  return tide_df
791
-
792
-
793
- def _pixel_tides_resample(
794
- tides_lowres,
795
- ds,
796
- resample_method="bilinear",
797
- dask_chunks="auto",
798
- dask_compute=True,
799
- ):
800
- """Resamples low resolution tides modelled by `pixel_tides` into the
801
- geobox (e.g. spatial resolution and extent) of the original higher
802
- resolution satellite dataset.
803
-
804
- Parameters
805
- ----------
806
- tides_lowres : xarray.DataArray
807
- The low resolution tide modelling data array to be resampled.
808
- ds : xarray.Dataset
809
- The dataset whose geobox will be used as the template for the
810
- resampling operation. This is typically the same satellite
811
- dataset originally passed to `pixel_tides`.
812
- resample_method : string, optional
813
- The resampling method to use. Defaults to "bilinear"; valid
814
- options include "nearest", "cubic", "min", "max", "average" etc.
815
- dask_chunks : str or tuple, optional
816
- Can be used to configure custom Dask chunking for the final
817
- resampling step. The default of "auto" will automatically set
818
- x/y chunks to match those in `ds` if they exist, otherwise will
819
- set x/y chunks that cover the entire extent of the dataset.
820
- For custom chunks, provide a tuple in the form `(y, x)`, e.g.
821
- `(2048, 2048)`.
822
- dask_compute : bool, optional
823
- Whether to compute results of the resampling step using Dask.
824
- If False, this will return `tides_highres` as a Dask array.
825
-
826
- Returns
827
- -------
828
- tides_highres, tides_lowres : tuple of xr.DataArrays
829
- In addition to `tides_lowres` (see above), a high resolution
830
- array of tide heights will be generated matching the
831
- exact spatial resolution and extent of `ds`.
832
-
833
- """
834
- # Determine spatial dimensions
835
- y_dim, x_dim = ds.odc.spatial_dims
836
-
837
- # Convert array to Dask, using no chunking along y and x dims,
838
- # and a single chunk for each timestep/quantile and tide model
839
- tides_lowres_dask = tides_lowres.chunk({d: None if d in [y_dim, x_dim] else 1 for d in tides_lowres.dims})
840
-
841
- # Automatically set Dask chunks for reprojection if set to "auto".
842
- # This will either use x/y chunks if they exist in `ds`, else
843
- # will cover the entire x and y dims) so we don't end up with
844
- # hundreds of tiny x and y chunks due to the small size of
845
- # `tides_lowres` (possible odc.geo bug?)
846
- if dask_chunks == "auto":
847
- if ds.chunks is not None:
848
- if (y_dim in ds.chunks) & (x_dim in ds.chunks):
849
- dask_chunks = (ds.chunks[y_dim], ds.chunks[x_dim])
850
- else:
851
- dask_chunks = ds.odc.geobox.shape
852
- else:
853
- dask_chunks = ds.odc.geobox.shape
854
-
855
- # Reproject into the GeoBox of `ds` using odc.geo and Dask
856
- tides_highres = tides_lowres_dask.odc.reproject(
857
- how=ds.odc.geobox,
858
- chunks=dask_chunks,
859
- resampling=resample_method,
860
- ).rename("tide_height")
861
-
862
- # Optionally process and load into memory with Dask
863
- if dask_compute:
864
- tides_highres.load()
865
-
866
- return tides_highres, tides_lowres
867
-
868
-
869
- def pixel_tides(
870
- ds,
871
- times=None,
872
- resample=True,
873
- calculate_quantiles=None,
874
- resolution=None,
875
- buffer=None,
876
- resample_method="bilinear",
877
- model="FES2014",
878
- dask_chunks="auto",
879
- dask_compute=True,
880
- **model_tides_kwargs,
881
- ):
882
- """Obtain tide heights for each pixel in a dataset by modelling
883
- tides into a low-resolution grid surrounding the dataset,
884
- then (optionally) spatially resample this low-res data back
885
- into the original higher resolution dataset extent and resolution.
886
-
887
- Parameters
888
- ----------
889
- ds : xarray.Dataset
890
- A dataset whose geobox (`ds.odc.geobox`) will be used to define
891
- the spatial extent of the low resolution tide modelling grid.
892
- times : pandas.DatetimeIndex or list of pandas.Timestamps, optional
893
- By default, the function will model tides using the times
894
- contained in the `time` dimension of `ds`. Alternatively, this
895
- param can be used to model tides for a custom set of times
896
- instead. For example:
897
- `times=pd.date_range(start="2000", end="2001", freq="5h")`
898
- resample : bool, optional
899
- Whether to resample low resolution tides back into `ds`'s original
900
- higher resolution grid. Set this to `False` if you do not want
901
- low resolution tides to be re-projected back to higher resolution.
902
- calculate_quantiles : list or np.array, optional
903
- Rather than returning all individual tides, low-resolution tides
904
- can be first aggregated using a quantile calculation by passing in
905
- a list or array of quantiles to compute. For example, this could
906
- be used to calculate the min/max tide across all times:
907
- `calculate_quantiles=[0.0, 1.0]`.
908
- resolution : int, optional
909
- The desired resolution of the low-resolution grid used for tide
910
- modelling. The default None will create a 5000 m resolution grid
911
- if `ds` has a projected CRS (i.e. metre units), or a 0.05 degree
912
- resolution grid if `ds` has a geographic CRS (e.g. degree units).
913
- Note: higher resolutions do not necessarily provide better
914
- tide modelling performance, as results will be limited by the
915
- resolution of the underlying global tide model (e.g. 1/16th
916
- degree / ~5 km resolution grid for FES2014).
917
- buffer : int, optional
918
- The amount by which to buffer the higher resolution grid extent
919
- when creating the new low resolution grid. This buffering is
920
- important as it ensures that ensure pixel-based tides are seamless
921
- across dataset boundaries. This buffer will eventually be clipped
922
- away when the low-resolution data is re-projected back to the
923
- resolution and extent of the higher resolution dataset. To
924
- ensure that at least two pixels occur outside of the dataset
925
- bounds, the default None applies a 12000 m buffer if `ds` has a
926
- projected CRS (i.e. metre units), or a 0.12 degree buffer if
927
- `ds` has a geographic CRS (e.g. degree units).
928
- resample_method : string, optional
929
- If resampling is requested (see `resample` above), use this
930
- resampling method when converting from low resolution to high
931
- resolution pixels. Defaults to "bilinear"; valid options include
932
- "nearest", "cubic", "min", "max", "average" etc.
933
- model : string or list of strings
934
- The tide model or a list of models used to model tides, as
935
- supported by the `pyTMD` Python package. Options include:
936
- - "FES2014" (default; pre-configured on DEA Sandbox)
937
- - "FES2022"
938
- - "TPXO8-atlas"
939
- - "TPXO9-atlas-v5"
940
- - "EOT20"
941
- - "HAMTIDE11"
942
- - "GOT4.10"
943
- dask_chunks : str or tuple, optional
944
- Can be used to configure custom Dask chunking for the final
945
- resampling step. The default of "auto" will automatically set
946
- x/y chunks to match those in `ds` if they exist, otherwise will
947
- set x/y chunks that cover the entire extent of the dataset.
948
- For custom chunks, provide a tuple in the form `(y, x)`, e.g.
949
- `(2048, 2048)`.
950
- dask_compute : bool, optional
951
- Whether to compute results of the resampling step using Dask.
952
- If False, this will return `tides_highres` as a Dask array.
953
- **model_tides_kwargs :
954
- Optional parameters passed to the `dea_tools.coastal.model_tides`
955
- function. Important parameters include "directory" (used to
956
- specify the location of input tide modelling files) and "cutoff"
957
- (used to extrapolate modelled tides away from the coast; if not
958
- specified here, cutoff defaults to `np.inf`).
959
-
960
- Returns
961
- -------
962
- If `resample` is False:
963
-
964
- tides_lowres : xr.DataArray
965
- A low resolution data array giving either tide heights every
966
- timestep in `ds` (if `times` is None), tide heights at every
967
- time in `times` (if `times` is not None), or tide height quantiles
968
- for every quantile provided by `calculate_quantiles`.
969
-
970
- If `resample` is True:
971
-
972
- tides_highres, tides_lowres : tuple of xr.DataArrays
973
- In addition to `tides_lowres` (see above), a high resolution
974
- array of tide heights will be generated that matches the
975
- exact spatial resolution and extent of `ds`. This will contain
976
- either tide heights every timestep in `ds` (if `times` is None),
977
- tide heights at every time in `times` (if `times` is not None),
978
- or tide height quantiles for every quantile provided by
979
- `calculate_quantiles`.
980
-
981
- """
982
- from odc.geo.geobox import GeoBox
983
-
984
- # First test if no time dimension and nothing passed to `times`
985
- if ("time" not in ds.dims) & (times is None):
986
- raise ValueError(
987
- "`ds` does not contain a 'time' dimension. Times are required "
988
- "for modelling tides: please pass in a set of custom tides "
989
- "using the `times` parameter. For example: "
990
- "`times=pd.date_range(start='2000', end='2001', freq='5h')`",
991
- )
992
-
993
- # If custom times are provided, convert them to a consistent
994
- # pandas.DatatimeIndex format
995
- if times is not None:
996
- if isinstance(times, list):
997
- time_coords = pd.DatetimeIndex(times)
998
- elif isinstance(times, pd.Timestamp):
999
- time_coords = pd.DatetimeIndex([times])
1000
- else:
1001
- time_coords = times
1002
-
1003
- # Otherwise, use times from `ds` directly
1004
- else:
1005
- time_coords = ds.coords["time"]
1006
-
1007
- # Set defaults passed to `model_tides`
1008
- model_tides_kwargs.setdefault("cutoff", np.inf)
1009
-
1010
- # Standardise model into a list for easy handling
1011
- model = [model] if isinstance(model, str) else model
1012
-
1013
- # Test if no time dimension and nothing passed to `times`
1014
- if ("time" not in ds.dims) & (times is None):
1015
- raise ValueError(
1016
- "`ds` does not contain a 'time' dimension. Times are required "
1017
- "for modelling tides: please pass in a set of custom tides "
1018
- "using the `times` parameter. For example: "
1019
- "`times=pd.date_range(start='2000', end='2001', freq='5h')`",
1020
- )
1021
-
1022
- # If custom times are provided, convert them to a consistent
1023
- # pandas.DatatimeIndex format
1024
- if times is not None:
1025
- if isinstance(times, list):
1026
- time_coords = pd.DatetimeIndex(times)
1027
- elif isinstance(times, pd.Timestamp):
1028
- time_coords = pd.DatetimeIndex([times])
1029
- else:
1030
- time_coords = times
1031
-
1032
- # Otherwise, use times from `ds` directly
1033
- else:
1034
- time_coords = ds.coords["time"]
1035
-
1036
- # Determine spatial dimensions
1037
- y_dim, x_dim = ds.odc.spatial_dims
1038
-
1039
- # Determine resolution and buffer, using different defaults for
1040
- # geographic (i.e. degrees) and projected (i.e. metres) CRSs:
1041
- crs_units = ds.odc.geobox.crs.units[0][0:6]
1042
- if ds.odc.geobox.crs.geographic:
1043
- if resolution is None:
1044
- resolution = 0.05
1045
- elif resolution > 360:
1046
- raise ValueError(
1047
- f"A resolution of greater than 360 was "
1048
- f"provided, but `ds` has a geographic CRS "
1049
- f"in {crs_units} units. Did you accidently "
1050
- f"provide a resolution in projected "
1051
- f"(i.e. metre) units?",
1052
- )
1053
- if buffer is None:
1054
- buffer = 0.12
1055
- else:
1056
- if resolution is None:
1057
- resolution = 5000
1058
- elif resolution < 1:
1059
- raise ValueError(
1060
- f"A resolution of less than 1 was provided, "
1061
- f"but `ds` has a projected CRS in "
1062
- f"{crs_units} units. Did you accidently "
1063
- f"provide a resolution in geographic "
1064
- f"(degree) units?",
1065
- )
1066
- if buffer is None:
1067
- buffer = 12000
1068
-
1069
- # Raise error if resolution is less than dataset resolution
1070
- dataset_res = ds.odc.geobox.resolution.x
1071
- if resolution < dataset_res:
1072
- raise ValueError(
1073
- f"The resolution of the low-resolution tide "
1074
- f"modelling grid ({resolution:.2f}) is less "
1075
- f"than `ds`'s pixel resolution ({dataset_res:.2f}). "
1076
- f"This can cause extremely slow tide modelling "
1077
- f"performance. Please select provide a resolution "
1078
- f"greater than {dataset_res:.2f} using "
1079
- f"`pixel_tides`'s 'resolution' parameter.",
1080
- )
1081
-
1082
- # Create a new reduced resolution tide modelling grid after
1083
- # first buffering the grid
1084
- print(f"Creating reduced resolution {resolution} x {resolution} {crs_units} tide modelling array")
1085
- buffered_geobox = ds.odc.geobox.buffered(buffer)
1086
- rescaled_geobox = GeoBox.from_bbox(bbox=buffered_geobox.boundingbox, resolution=resolution)
1087
- rescaled_ds = odc.geo.xr.xr_zeros(rescaled_geobox)
1088
-
1089
- # Flatten grid to 1D, then add time dimension
1090
- flattened_ds = rescaled_ds.stack(z=(x_dim, y_dim))
1091
- flattened_ds = flattened_ds.expand_dims(dim={"time": time_coords.values})
1092
-
1093
- # Model tides in parallel, returning a pandas.DataFrame
1094
- tide_df = model_tides(
1095
- x=flattened_ds[x_dim],
1096
- y=flattened_ds[y_dim],
1097
- time=flattened_ds.time,
1098
- crs=f"EPSG:{ds.odc.geobox.crs.epsg}",
1099
- model=model,
1100
- **model_tides_kwargs,
1101
- )
1102
-
1103
- # Convert our pandas.DataFrame tide modelling outputs to xarray
1104
- tides_lowres = (
1105
- # Rename x and y dataframe indexes to match x and y xarray dims
1106
- tide_df.rename_axis(["time", x_dim, y_dim])
1107
- # Add tide model column to dataframe indexes so we can convert
1108
- # our dataframe to a multidimensional xarray
1109
- .set_index("tide_model", append=True)
1110
- # Convert to xarray and select our tide modelling xr.DataArray
1111
- .to_xarray()
1112
- .tide_height
1113
- # Re-index and transpose into our input coordinates and dim order
1114
- .reindex_like(rescaled_ds)
1115
- .transpose("tide_model", "time", y_dim, x_dim)
1116
- )
1117
-
1118
- # Optionally calculate and return quantiles rather than raw data.
1119
- # Set dtype to dtype of the input data as quantile always returns
1120
- # float64 (memory intensive)
1121
- if calculate_quantiles is not None:
1122
- print("Computing tide quantiles")
1123
- tides_lowres = tides_lowres.quantile(q=calculate_quantiles, dim="time").astype(tides_lowres.dtype)
1124
-
1125
- # If only one tidal model exists, squeeze out "tide_model" dim
1126
- if len(tides_lowres.tide_model) == 1:
1127
- tides_lowres = tides_lowres.squeeze("tide_model")
1128
-
1129
- # Ensure CRS is present before we apply any resampling
1130
- tides_lowres = tides_lowres.odc.assign_crs(ds.odc.geobox.crs)
1131
-
1132
- # Reproject into original high resolution grid
1133
- if resample:
1134
- print("Reprojecting tides into original array")
1135
- tides_highres, tides_lowres = _pixel_tides_resample(
1136
- tides_lowres,
1137
- ds,
1138
- resample_method,
1139
- dask_chunks,
1140
- dask_compute,
1141
- )
1142
- return tides_highres, tides_lowres
1143
-
1144
- print("Returning low resolution tide array")
1145
- return tides_lowres
1146
-
1147
-
1148
- if __name__ == "__main__": # pragma: no cover
1149
- pass