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/__init__.py +46 -0
- eo_tides/eo.py +519 -0
- eo_tides/model.py +131 -449
- eo_tides/stats.py +311 -6
- eo_tides/validation.py +3 -3
- eo_tides-0.0.22.dist-info/LICENSE +201 -0
- {eo_tides-0.0.20.dist-info → eo_tides-0.0.22.dist-info}/METADATA +48 -29
- eo_tides-0.0.22.dist-info/RECORD +11 -0
- eo_tides-0.0.20.dist-info/LICENSE +0 -21
- eo_tides-0.0.20.dist-info/RECORD +0 -10
- {eo_tides-0.0.20.dist-info → eo_tides-0.0.22.dist-info}/WHEEL +0 -0
- {eo_tides-0.0.20.dist-info → eo_tides-0.0.22.dist-info}/top_level.txt +0 -0
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
|
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(
|
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 :
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
95
|
-
|
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
|
-
|
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
|
-
"
|
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.
|
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
|
319
|
-
|
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
|
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
|
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. "
|
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="
|
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
|
-
|
484
|
-
|
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 (
|
490
|
-
- Finite Element Solution tide models (
|
491
|
-
- TOPEX/POSEIDON global tide models (
|
492
|
-
- Global Ocean Tide models (
|
493
|
-
- Hamburg direct data Assimilation Methods for Tides models (
|
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
|
-
`
|
503
|
-
<https://pytmd.readthedocs.io/en/
|
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
|
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 :
|
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 :
|
517
|
-
The tide model
|
518
|
-
|
519
|
-
|
520
|
-
|
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
|
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 :
|
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 :
|
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 :
|
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 :
|
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(
|
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
|