eo-tides 0.0.20__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/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)
@@ -296,10 +343,8 @@ def _model_tides(
296
343
 
297
344
 
298
345
  def _ensemble_model(
299
- x,
300
- y,
301
- crs,
302
346
  tide_df,
347
+ crs,
303
348
  ensemble_models,
304
349
  ensemble_func=None,
305
350
  ensemble_top_n=3,
@@ -313,29 +358,27 @@ def _ensemble_model(
313
358
  to inform the selection of the best local models.
314
359
 
315
360
  This function performs the following steps:
361
+ 1. Takes a dataframe of tide heights from multiple tide models, as
362
+ produced by `eo_tides.model.model_tides`
316
363
  1. Loads model ranking points from a GeoJSON file, filters them
317
364
  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)
365
+ 2. Interpolates the model rankings into the "x" and "y" coordinates
366
+ of the original dataframe using Inverse Weighted Interpolation (IDW)
320
367
  3. Uses rankings to combine multiple tide models into a single
321
368
  optimised ensemble model (by default, by taking the mean of the
322
369
  top 3 ranked models)
323
- 4. Returns a DataFrame with the combined ensemble model predictions
370
+ 4. Returns a new dataFrame with the combined ensemble model predictions
324
371
 
325
372
  Parameters
326
373
  ----------
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
374
  tide_df : pandas.DataFrame
337
- DataFrame containing tide model predictions with columns
375
+ DataFrame produced by `eo_tides.model.model_tides`, containing
376
+ tide model predictions with columns:
338
377
  `["time", "x", "y", "tide_height", "tide_model"]`.
378
+ crs : string
379
+ Coordinate reference system for the "x" and "y" coordinates in
380
+ `tide_df`. Used to ensure that interpolations are performed
381
+ in the correct CRS.
339
382
  ensemble_models : list
340
383
  A list of models to include in the ensemble modelling process.
341
384
  All values must exist as columns with the prefix "rank_" in
@@ -354,7 +397,7 @@ def _ensemble_model(
354
397
  ranking_points : str, optional
355
398
  Path to the GeoJSON file containing model ranking points. This
356
399
  dataset should include columns containing rankings for each tide
357
- model, named with the prefix "rank_". e.g. "rank_FES2014".
400
+ model, named with the prefix "rank_". e.g. "rank_EOT20".
358
401
  Low values should represent high rankings (e.g. 1 = top ranked).
359
402
  ranking_valid_perc : float, optional
360
403
  Minimum percentage of valid data required to include a model
@@ -379,6 +422,10 @@ def _ensemble_model(
379
422
  the provided dictionary keys).
380
423
 
381
424
  """
425
+ # Extract x and y coords from dataframe
426
+ x = tide_df.index.get_level_values(level="x")
427
+ y = tide_df.index.get_level_values(level="y")
428
+
382
429
  # Load model ranks points and reproject to same CRS as x and y
383
430
  model_ranking_cols = [f"rank_{m}" for m in ensemble_models]
384
431
  model_ranks_gdf = (
@@ -461,36 +508,36 @@ def _ensemble_model(
461
508
 
462
509
 
463
510
  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,
511
+ x: float | list[float] | xr.DataArray,
512
+ y: float | list[float] | xr.DataArray,
513
+ time: np.ndarray | pd.DatetimeIndex,
514
+ model: str | list[str] = "EOT20",
515
+ directory: str | os.PathLike | None = None,
516
+ crs: str = "EPSG:4326",
517
+ crop: bool = True,
518
+ method: str = "spline",
519
+ extrapolate: bool = True,
520
+ cutoff: float | None = None,
521
+ mode: str = "one-to-many",
522
+ parallel: bool = True,
523
+ parallel_splits: int = 5,
524
+ output_units: str = "m",
525
+ output_format: str = "long",
526
+ ensemble_models: list[str] | None = None,
480
527
  **ensemble_kwargs,
481
- ):
528
+ ) -> pd.DataFrame:
482
529
  """
483
- Compute tide heights from multiple tide models and for
484
- multiple coordinates and/or timesteps.
530
+ Model tide heights at multiple coordinates and/or timesteps
531
+ using using one or more ocean tide models.
485
532
 
486
533
  This function is parallelised to improve performance, and
487
534
  supports all tidal models supported by `pyTMD`, including:
488
535
 
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`)
536
+ - Empirical Ocean Tide model (EOT20)
537
+ - Finite Element Solution tide models (FES2022, FES2014, FES2012)
538
+ - TOPEX/POSEIDON global tide models (TPXO10, TPXO9, TPXO8)
539
+ - Global Ocean Tide models (GOT5.6, GOT5.5, GOT4.10, GOT4.8, GOT4.7)
540
+ - Hamburg direct data Assimilation Methods for Tides models (HAMTIDE11)
494
541
 
495
542
  This function requires access to tide model data files.
496
543
  These should be placed in a folder with subfolders matching
@@ -499,52 +546,39 @@ def model_tides(
499
546
  <https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories>
500
547
 
501
548
  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>
549
+ `compute_tidal_elevations` function. For more info:
550
+ <https://pytmd.readthedocs.io/en/latest/api_reference/compute_tidal_elevations.html>
504
551
 
505
552
  Parameters
506
553
  ----------
507
- x, y : float or list of floats
554
+ x, y : float or list of float
508
555
  One or more x and y coordinates used to define
509
556
  the location at which to model tides. By default these
510
557
  coordinates should be lat/lon; use "crs" if they
511
558
  are in a custom coordinate reference system.
512
- time : A datetime array or pandas.DatetimeIndex
559
+ time : Numpy datetime array or pandas.DatetimeIndex
513
560
  An array containing `datetime64[ns]` values or a
514
561
  `pandas.DatetimeIndex` providing the times at which to
515
562
  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
563
+ model : str or list of str, optional
564
+ The tide model (or models) to use to model tides.
565
+ Defaults to "EOT20"; for a full list of available/supported
566
+ models, run `eo_tides.model.list_models`.
567
+ directory : str, optional
529
568
  The directory containing tide model data files. If no path is
530
569
  provided, this will default to the environment variable
531
570
  `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
532
571
  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/`
572
+ model that match the structure required by `pyTMD`
573
+ (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
540
574
  crs : str, optional
541
575
  Input coordinate reference system for x and y coordinates.
542
576
  Defaults to "EPSG:4326" (WGS84; degrees latitude, longitude).
543
- crop : bool optional
577
+ crop : bool, optional
544
578
  Whether to crop tide model constituent files on-the-fly to
545
579
  improve performance. Cropping will be performed based on a
546
580
  1 degree buffer around all input points. Defaults to True.
547
- method : string, optional
581
+ method : str, optional
548
582
  Method used to interpolate tidal constituents
549
583
  from model files. Options include:
550
584
 
@@ -554,11 +588,11 @@ def model_tides(
554
588
  extrapolate : bool, optional
555
589
  Whether to extrapolate tides for x and y coordinates outside of
556
590
  the valid tide modelling domain using nearest-neighbor.
557
- cutoff : int or float, optional
591
+ cutoff : float, optional
558
592
  Extrapolation cutoff in kilometers. The default is None, which
559
593
  will extrapolate for all points regardless of distance from the
560
594
  valid tide modelling domain.
561
- mode : string, optional
595
+ mode : str, optional
562
596
  The analysis mode to use for tide modelling. Supports two options:
563
597
 
564
598
  - "one-to-many": Models tides for every timestep in "time" at
@@ -570,7 +604,7 @@ def model_tides(
570
604
  set of x and y coordinates. In this mode, the number of x and
571
605
  y points must equal the number of timesteps provided in "time".
572
606
 
573
- parallel : boolean, optional
607
+ parallel : bool, optional
574
608
  Whether to parallelise tide modelling using `concurrent.futures`.
575
609
  If multiple tide models are requested, these will be run in
576
610
  parallel. Optionally, tide modelling can also be run in parallel
@@ -594,7 +628,7 @@ def model_tides(
594
628
  results stacked vertically along "tide_model" and "tide_height"
595
629
  columns), or wide format (with a column for each tide model).
596
630
  Defaults to "long".
597
- ensemble_models : list, optional
631
+ ensemble_models : list of str, optional
598
632
  An optional list of models used to generate the ensemble tide
599
633
  model if "ensemble" tide modelling is requested. Defaults to
600
634
  ["FES2014", "TPXO9-atlas-v5", "EOT20", "HAMTIDE11", "GOT4.10",
@@ -615,7 +649,7 @@ def model_tides(
615
649
 
616
650
  """
617
651
  # Turn inputs into arrays for consistent handling
618
- models_requested = np.atleast_1d(model)
652
+ models_requested = list(np.atleast_1d(model))
619
653
  x = np.atleast_1d(x)
620
654
  y = np.atleast_1d(y)
621
655
  time = np.atleast_1d(time)
@@ -652,6 +686,9 @@ def model_tides(
652
686
  available_models, valid_models = list_models(
653
687
  directory, show_available=False, show_supported=False, raise_error=True
654
688
  )
689
+ # TODO: This is hacky, find a better way. Perhaps a kwarg that
690
+ # turns ensemble functionality on, and checks that supplied
691
+ # models match models expected for ensemble?
655
692
  available_models = available_models + ["ensemble"]
656
693
  valid_models = valid_models + ["ensemble"]
657
694
 
@@ -767,11 +804,11 @@ def model_tides(
767
804
 
768
805
  # Optionally compute ensemble model and add to dataframe
769
806
  if "ensemble" in models_requested:
770
- ensemble_df = _ensemble_model(x, y, crs, tide_df, models_to_process, **ensemble_kwargs)
807
+ ensemble_df = _ensemble_model(tide_df, crs, models_to_process, **ensemble_kwargs)
771
808
 
772
809
  # Update requested models with any custom ensemble models, then
773
810
  # filter the dataframe to keep only models originally requested
774
- models_requested = np.union1d(models_requested, ensemble_df.tide_model.unique())
811
+ models_requested = list(np.union1d(models_requested, ensemble_df.tide_model.unique()))
775
812
  tide_df = pd.concat([tide_df, ensemble_df]).query("tide_model in @models_requested")
776
813
 
777
814
  # Optionally convert to a wide format dataframe with a tide model in
@@ -788,362 +825,3 @@ def model_tides(
788
825
  tide_df = tide_df.reindex(output_indices)
789
826
 
790
827
  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