eo-tides 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eo_tides/__init__.py +4 -2
- eo_tides/eo.py +186 -161
- eo_tides/model.py +214 -123
- eo_tides/stats.py +76 -36
- {eo_tides-0.1.0.dist-info → eo_tides-0.2.0.dist-info}/METADATA +10 -4
- eo_tides-0.2.0.dist-info/RECORD +11 -0
- {eo_tides-0.1.0.dist-info → eo_tides-0.2.0.dist-info}/WHEEL +1 -1
- eo_tides-0.1.0.dist-info/RECORD +0 -11
- {eo_tides-0.1.0.dist-info → eo_tides-0.2.0.dist-info}/LICENSE +0 -0
- {eo_tides-0.1.0.dist-info → eo_tides-0.2.0.dist-info}/top_level.txt +0 -0
eo_tides/model.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Used to postpone evaluation of type annotations
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
|
+
import datetime
|
4
5
|
import os
|
5
6
|
import pathlib
|
6
7
|
import textwrap
|
@@ -8,7 +9,7 @@ import warnings
|
|
8
9
|
from concurrent.futures import ProcessPoolExecutor
|
9
10
|
from concurrent.futures.process import BrokenProcessPool
|
10
11
|
from functools import partial
|
11
|
-
from typing import TYPE_CHECKING
|
12
|
+
from typing import TYPE_CHECKING, List, Union
|
12
13
|
|
13
14
|
# Only import if running type checking
|
14
15
|
if TYPE_CHECKING:
|
@@ -25,8 +26,13 @@ from tqdm import tqdm
|
|
25
26
|
|
26
27
|
from .utils import idw
|
27
28
|
|
29
|
+
# Type alias for all possible inputs to "time" params
|
30
|
+
DatetimeLike = Union[np.ndarray, pd.DatetimeIndex, pd.Timestamp, datetime.datetime, str, List[str]]
|
28
31
|
|
29
|
-
|
32
|
+
|
33
|
+
def _set_directory(
|
34
|
+
directory: str | os.PathLike | None = None,
|
35
|
+
) -> os.PathLike:
|
30
36
|
"""
|
31
37
|
Set tide modelling files directory. If no custom
|
32
38
|
path is provided, try global environmental variable
|
@@ -50,6 +56,25 @@ def _set_directory(directory):
|
|
50
56
|
return directory
|
51
57
|
|
52
58
|
|
59
|
+
def _standardise_time(
|
60
|
+
time: DatetimeLike | None,
|
61
|
+
) -> np.ndarray | None:
|
62
|
+
"""
|
63
|
+
Accept any time format accepted by `pd.to_datetime`,
|
64
|
+
and return a datetime64 ndarray. Return None if None
|
65
|
+
passed.
|
66
|
+
"""
|
67
|
+
# Return time as-is if None
|
68
|
+
if time is None:
|
69
|
+
return None
|
70
|
+
|
71
|
+
# Use pd.to_datetime for conversion, then convert to numpy array
|
72
|
+
time = pd.to_datetime(time).to_numpy().astype("datetime64[ns]")
|
73
|
+
|
74
|
+
# Ensure that data has at least one dimension
|
75
|
+
return np.atleast_1d(time)
|
76
|
+
|
77
|
+
|
53
78
|
def list_models(
|
54
79
|
directory: str | os.PathLike | None = None,
|
55
80
|
show_available: bool = True,
|
@@ -57,12 +82,12 @@ def list_models(
|
|
57
82
|
raise_error: bool = False,
|
58
83
|
) -> tuple[list[str], list[str]]:
|
59
84
|
"""
|
60
|
-
List all tide models available for tide modelling
|
61
|
-
all models supported by `eo-tides` and `pyTMD`.
|
85
|
+
List all tide models available for tide modelling.
|
62
86
|
|
63
87
|
This function scans the specified tide model directory
|
64
88
|
and returns a list of models that are available in the
|
65
|
-
directory as well as the full list of all supported
|
89
|
+
directory as well as the full list of all models supported
|
90
|
+
by `eo-tides` and `pyTMD`.
|
66
91
|
|
67
92
|
For instructions on setting up tide models, see:
|
68
93
|
<https://geoscienceaustralia.github.io/eo-tides/setup/>
|
@@ -188,117 +213,41 @@ def _model_tides(
|
|
188
213
|
# Obtain model details
|
189
214
|
pytmd_model = pyTMD.io.model(directory).elevation(model)
|
190
215
|
|
191
|
-
#
|
216
|
+
# Reproject x, y to latitude/longitude
|
192
217
|
transformer = pyproj.Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
|
193
218
|
lon, lat = transformer.transform(x.flatten(), y.flatten())
|
194
219
|
|
195
220
|
# Convert datetime
|
196
221
|
timescale = pyTMD.time.timescale().from_datetime(time.flatten())
|
197
222
|
|
198
|
-
# Calculate bounds for cropping
|
199
|
-
buffer = 1 # one degree on either side
|
200
|
-
bounds = [
|
201
|
-
lon.min() - buffer,
|
202
|
-
lon.max() + buffer,
|
203
|
-
lat.min() - buffer,
|
204
|
-
lat.max() + buffer,
|
205
|
-
]
|
206
|
-
|
207
223
|
try:
|
208
224
|
# Read tidal constants and interpolate to grid points
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
extrapolate=extrapolate,
|
222
|
-
cutoff=cutoff,
|
223
|
-
)
|
224
|
-
|
225
|
-
# Use delta time at 2000.0 to match TMD outputs
|
226
|
-
deltat = np.zeros((len(timescale)), dtype=np.float64)
|
227
|
-
|
228
|
-
elif pytmd_model.format in ("ATLAS-netcdf",):
|
229
|
-
amp, ph, D, c = pyTMD.io.ATLAS.extract_constants(
|
230
|
-
lon,
|
231
|
-
lat,
|
232
|
-
pytmd_model.grid_file,
|
233
|
-
pytmd_model.model_file,
|
234
|
-
type=pytmd_model.type,
|
235
|
-
crop=crop,
|
236
|
-
bounds=bounds,
|
237
|
-
method=method,
|
238
|
-
extrapolate=extrapolate,
|
239
|
-
cutoff=cutoff,
|
240
|
-
scale=pytmd_model.scale,
|
241
|
-
compressed=pytmd_model.compressed,
|
242
|
-
)
|
243
|
-
|
244
|
-
# Use delta time at 2000.0 to match TMD outputs
|
245
|
-
deltat = np.zeros((len(timescale)), dtype=np.float64)
|
246
|
-
|
247
|
-
elif pytmd_model.format in ("GOT-ascii", "GOT-netcdf"):
|
248
|
-
amp, ph, c = pyTMD.io.GOT.extract_constants(
|
249
|
-
lon,
|
250
|
-
lat,
|
251
|
-
pytmd_model.model_file,
|
252
|
-
grid=pytmd_model.file_format,
|
253
|
-
crop=crop,
|
254
|
-
bounds=bounds,
|
255
|
-
method=method,
|
256
|
-
extrapolate=extrapolate,
|
257
|
-
cutoff=cutoff,
|
258
|
-
scale=pytmd_model.scale,
|
259
|
-
compressed=pytmd_model.compressed,
|
260
|
-
)
|
261
|
-
|
262
|
-
# Delta time (TT - UT1)
|
263
|
-
deltat = timescale.tt_ut1
|
264
|
-
|
265
|
-
elif pytmd_model.format in ("FES-ascii", "FES-netcdf"):
|
266
|
-
amp, ph = pyTMD.io.FES.extract_constants(
|
267
|
-
lon,
|
268
|
-
lat,
|
269
|
-
pytmd_model.model_file,
|
270
|
-
type=pytmd_model.type,
|
271
|
-
version=pytmd_model.version,
|
272
|
-
crop=crop,
|
273
|
-
bounds=bounds,
|
274
|
-
method=method,
|
275
|
-
extrapolate=extrapolate,
|
276
|
-
cutoff=cutoff,
|
277
|
-
scale=pytmd_model.scale,
|
278
|
-
compressed=pytmd_model.compressed,
|
279
|
-
)
|
280
|
-
|
281
|
-
# Available model constituents
|
282
|
-
c = pytmd_model.constituents
|
225
|
+
amp, ph, c = pytmd_model.extract_constants(
|
226
|
+
lon,
|
227
|
+
lat,
|
228
|
+
type=pytmd_model.type,
|
229
|
+
crop=crop,
|
230
|
+
bounds=None,
|
231
|
+
method=method,
|
232
|
+
extrapolate=extrapolate,
|
233
|
+
cutoff=cutoff,
|
234
|
+
append_node=False,
|
235
|
+
# append_node=True,
|
236
|
+
)
|
283
237
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
raise Exception(
|
288
|
-
f"Unsupported model format ({pytmd_model.format}). This may be due to an incompatible version of `pyTMD`."
|
289
|
-
)
|
238
|
+
# TODO: Return constituents
|
239
|
+
# print(amp.shape, ph.shape, c)
|
240
|
+
# print(pd.DataFrame({"amplitude": amp}))
|
290
241
|
|
291
242
|
# Raise error if constituent files no not cover analysis extent
|
292
|
-
except IndexError:
|
293
|
-
error_msg =
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
).strip()
|
301
|
-
raise Exception(error_msg)
|
243
|
+
except IndexError as e:
|
244
|
+
error_msg = f"""
|
245
|
+
The {model} tide model constituent files do not cover the requested analysis extent.
|
246
|
+
This can occur if you are using clipped model files to improve run times.
|
247
|
+
Consider using model files that cover your entire analysis area, or set `crop=False`
|
248
|
+
to reduce the extent of tide model constituent files that is loaded.
|
249
|
+
"""
|
250
|
+
raise Exception(textwrap.dedent(error_msg).strip()) from None
|
302
251
|
|
303
252
|
# Calculate complex phase in radians for Euler's
|
304
253
|
cph = -1j * ph * np.pi / 180.0
|
@@ -306,30 +255,42 @@ def _model_tides(
|
|
306
255
|
# Calculate constituent oscillation
|
307
256
|
hc = amp * np.exp(cph)
|
308
257
|
|
258
|
+
# Compute deltat based on model
|
259
|
+
if pytmd_model.corrections in ("OTIS", "ATLAS", "TMD3", "netcdf"):
|
260
|
+
# Use delta time at 2000.0 to match TMD outputs
|
261
|
+
deltat = np.zeros_like(timescale.tt_ut1)
|
262
|
+
else:
|
263
|
+
# Use interpolated delta times
|
264
|
+
deltat = timescale.tt_ut1
|
265
|
+
|
309
266
|
# Determine the number of points and times to process. If in
|
310
267
|
# "one-to-many" mode, these counts are used to repeat our extracted
|
311
268
|
# constituents and timesteps so we can extract tides for all
|
312
269
|
# combinations of our input times and tide modelling points.
|
270
|
+
# If in "one-to-many" mode, repeat constituents to length of time
|
271
|
+
# and number of input coords before passing to `predict_tide_drift`
|
313
272
|
# If in "one-to-one" mode, we avoid this step by setting counts to 1
|
314
273
|
# (e.g. "repeat 1 times")
|
315
274
|
points_repeat = len(x) if mode == "one-to-many" else 1
|
316
275
|
time_repeat = len(time) if mode == "one-to-many" else 1
|
317
|
-
|
318
|
-
# If in "one-to-many" mode, repeat constituents to length of time
|
319
|
-
# and number of input coords before passing to `predict_tide_drift`
|
320
276
|
t, hc, deltat = (
|
321
277
|
np.tile(timescale.tide, points_repeat),
|
322
278
|
hc.repeat(time_repeat, axis=0),
|
323
279
|
np.tile(deltat, points_repeat),
|
324
280
|
)
|
325
281
|
|
326
|
-
#
|
327
|
-
|
328
|
-
tide = np.ma.zeros((npts), fill_value=np.nan)
|
282
|
+
# Create arrays to hold outputs
|
283
|
+
tide = np.ma.zeros((len(t)), fill_value=np.nan)
|
329
284
|
tide.mask = np.any(hc.mask, axis=1)
|
330
285
|
|
331
|
-
# Predict
|
332
|
-
tide.data[:] = pyTMD.predict.drift(
|
286
|
+
# Predict tidal elevations at time and infer minor corrections
|
287
|
+
tide.data[:] = pyTMD.predict.drift(
|
288
|
+
t,
|
289
|
+
hc,
|
290
|
+
c,
|
291
|
+
deltat=deltat,
|
292
|
+
corrections=pytmd_model.corrections,
|
293
|
+
)
|
333
294
|
minor = pyTMD.predict.infer_minor(
|
334
295
|
t,
|
335
296
|
hc,
|
@@ -532,7 +493,7 @@ def _ensemble_model(
|
|
532
493
|
def model_tides(
|
533
494
|
x: float | list[float] | xr.DataArray,
|
534
495
|
y: float | list[float] | xr.DataArray,
|
535
|
-
time:
|
496
|
+
time: DatetimeLike,
|
536
497
|
model: str | list[str] = "EOT20",
|
537
498
|
directory: str | os.PathLike | None = None,
|
538
499
|
crs: str = "EPSG:4326",
|
@@ -578,10 +539,11 @@ def model_tides(
|
|
578
539
|
the location at which to model tides. By default these
|
579
540
|
coordinates should be lat/lon; use "crs" if they
|
580
541
|
are in a custom coordinate reference system.
|
581
|
-
time :
|
582
|
-
|
583
|
-
|
584
|
-
|
542
|
+
time : DatetimeLike
|
543
|
+
Times at which to model tide heights (in UTC). Accepts
|
544
|
+
any format that can be converted by `pandas.to_datetime()`;
|
545
|
+
e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp,
|
546
|
+
datetime.datetime and strings (e.g. "2020-01-01 23:00").
|
585
547
|
model : str or list of str, optional
|
586
548
|
The tide model (or models) to use to model tides.
|
587
549
|
Defaults to "EOT20"; for a full list of available/supported
|
@@ -674,9 +636,10 @@ def model_tides(
|
|
674
636
|
models_requested = list(np.atleast_1d(model))
|
675
637
|
x = np.atleast_1d(x)
|
676
638
|
y = np.atleast_1d(y)
|
677
|
-
time =
|
639
|
+
time = _standardise_time(time)
|
678
640
|
|
679
641
|
# Validate input arguments
|
642
|
+
assert time is not None, "Times for modelling tides muyst be provided via `time`."
|
680
643
|
assert method in ("bilinear", "spline", "linear", "nearest")
|
681
644
|
assert output_units in (
|
682
645
|
"m",
|
@@ -695,10 +658,6 @@ def model_tides(
|
|
695
658
|
"you intended to model multiple timesteps at each point."
|
696
659
|
)
|
697
660
|
|
698
|
-
# If time passed as a single Timestamp, convert to datetime64
|
699
|
-
if isinstance(time, pd.Timestamp):
|
700
|
-
time = time.to_datetime64()
|
701
|
-
|
702
661
|
# Set tide modelling files directory. If no custom path is
|
703
662
|
# provided, try global environment variable.
|
704
663
|
directory = _set_directory(directory)
|
@@ -854,3 +813,135 @@ def model_tides(
|
|
854
813
|
tide_df = tide_df.reindex(output_indices)
|
855
814
|
|
856
815
|
return tide_df
|
816
|
+
|
817
|
+
|
818
|
+
def model_phases(
|
819
|
+
x: float | list[float] | xr.DataArray,
|
820
|
+
y: float | list[float] | xr.DataArray,
|
821
|
+
time: DatetimeLike,
|
822
|
+
model: str | list[str] = "EOT20",
|
823
|
+
directory: str | os.PathLike | None = None,
|
824
|
+
time_offset: str = "15 min",
|
825
|
+
return_tides: bool = False,
|
826
|
+
**model_tides_kwargs,
|
827
|
+
) -> pd.DataFrame:
|
828
|
+
"""
|
829
|
+
Model tide phases (low-flow, high-flow, high-ebb, low-ebb)
|
830
|
+
at multiple coordinates and/or timesteps using using one
|
831
|
+
or more ocean tide models.
|
832
|
+
|
833
|
+
Ebb and low phases are calculated by running the
|
834
|
+
`eo_tides.model.model_tides` function twice, once for
|
835
|
+
the requested timesteps, and again after subtracting a
|
836
|
+
small time offset (by default, 15 minutes). If tides
|
837
|
+
increased over this period, they are assigned as "flow";
|
838
|
+
if they decreased, they are assigned as "ebb".
|
839
|
+
Tides are considered "high" if equal or greater than 0
|
840
|
+
metres tide height, otherwise "low".
|
841
|
+
|
842
|
+
This function supports all parameters that are supported
|
843
|
+
by `model_tides`.
|
844
|
+
|
845
|
+
Parameters
|
846
|
+
----------
|
847
|
+
x, y : float or list of float
|
848
|
+
One or more x and y coordinates used to define
|
849
|
+
the location at which to model tide phases. By default
|
850
|
+
these coordinates should be lat/lon; use "crs" if they
|
851
|
+
are in a custom coordinate reference system.
|
852
|
+
time : DatetimeLike
|
853
|
+
Times at which to model tide phases (in UTC). Accepts
|
854
|
+
any format that can be converted by `pandas.to_datetime()`;
|
855
|
+
e.g. np.ndarray[datetime64], pd.DatetimeIndex, pd.Timestamp,
|
856
|
+
datetime.datetime and strings (e.g. "2020-01-01 23:00").
|
857
|
+
model : str or list of str, optional
|
858
|
+
The tide model (or models) to use to compute tide phases.
|
859
|
+
Defaults to "EOT20"; for a full list of available/supported
|
860
|
+
models, run `eo_tides.model.list_models`.
|
861
|
+
directory : str, optional
|
862
|
+
The directory containing tide model data files. If no path is
|
863
|
+
provided, this will default to the environment variable
|
864
|
+
`EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
|
865
|
+
Tide modelling files should be stored in sub-folders for each
|
866
|
+
model that match the structure required by `pyTMD`
|
867
|
+
(<https://geoscienceaustralia.github.io/eo-tides/setup/>).
|
868
|
+
time_offset: str, optional
|
869
|
+
The time offset/delta used to generate a time series of
|
870
|
+
offset tide heights required for phase calculation. Defeaults
|
871
|
+
to "15 min"; can be any string passed to `pandas.Timedelta`.
|
872
|
+
return_tides: bool, optional
|
873
|
+
Whether to return intermediate modelled tide heights as a
|
874
|
+
"tide_height" column in the output dataframe. Defaults to False.
|
875
|
+
**model_tides_kwargs :
|
876
|
+
Optional parameters passed to the `eo_tides.model.model_tides`
|
877
|
+
function. Important parameters include `output_format` (e.g.
|
878
|
+
whether to return results in wide or long format), `crop`
|
879
|
+
(whether to crop tide model constituent files on-the-fly to
|
880
|
+
improve performance) etc.
|
881
|
+
|
882
|
+
Returns
|
883
|
+
-------
|
884
|
+
pandas.DataFrame
|
885
|
+
A dataframe containing modelled tide phases.
|
886
|
+
|
887
|
+
"""
|
888
|
+
|
889
|
+
# Pop output format and mode for special handling
|
890
|
+
output_format = model_tides_kwargs.pop("output_format", "long")
|
891
|
+
mode = model_tides_kwargs.pop("mode", "one-to-many")
|
892
|
+
|
893
|
+
# Model tides
|
894
|
+
tide_df = model_tides(
|
895
|
+
x=x,
|
896
|
+
y=y,
|
897
|
+
time=time,
|
898
|
+
model=model,
|
899
|
+
directory=directory,
|
900
|
+
**model_tides_kwargs,
|
901
|
+
)
|
902
|
+
|
903
|
+
# Model tides for a time 15 minutes prior to each previously
|
904
|
+
# modelled satellite acquisition time. This allows us to compare
|
905
|
+
# tide heights to see if they are rising or falling.
|
906
|
+
pre_df = model_tides(
|
907
|
+
x=x,
|
908
|
+
y=y,
|
909
|
+
time=time - pd.Timedelta(time_offset),
|
910
|
+
model=model,
|
911
|
+
directory=directory,
|
912
|
+
**model_tides_kwargs,
|
913
|
+
)
|
914
|
+
|
915
|
+
# Compare tides computed for each timestep. If the previous tide
|
916
|
+
# was higher than the current tide, the tide is 'ebbing'. If the
|
917
|
+
# previous tide was lower, the tide is 'flowing'
|
918
|
+
ebb_flow = (tide_df.tide_height < pre_df.tide_height.values).replace({True: "ebb", False: "flow"})
|
919
|
+
|
920
|
+
# If tides are greater than 0, then "high", otherwise "low"
|
921
|
+
high_low = (tide_df.tide_height >= 0).replace({True: "high", False: "low"})
|
922
|
+
|
923
|
+
# Combine into one string and add to data
|
924
|
+
tide_df["tide_phase"] = high_low.astype(str) + "-" + ebb_flow.astype(str)
|
925
|
+
|
926
|
+
# Optionally convert to a wide format dataframe with a tide model in
|
927
|
+
# each dataframe column
|
928
|
+
if output_format == "wide":
|
929
|
+
# Pivot into wide format with each time model as a column
|
930
|
+
print("Converting to a wide format dataframe")
|
931
|
+
tide_df = tide_df.pivot(columns="tide_model")
|
932
|
+
|
933
|
+
# If in 'one-to-one' mode, reindex using our input time/x/y
|
934
|
+
# values to ensure the output is sorted the same as our inputs
|
935
|
+
if mode == "one-to-one":
|
936
|
+
output_indices = pd.MultiIndex.from_arrays([time, x, y], names=["time", "x", "y"])
|
937
|
+
tide_df = tide_df.reindex(output_indices)
|
938
|
+
|
939
|
+
# Optionally drop tides
|
940
|
+
if not return_tides:
|
941
|
+
return tide_df.drop("tide_height", axis=1)["tide_phase"]
|
942
|
+
|
943
|
+
# Optionally drop tide heights
|
944
|
+
if not return_tides:
|
945
|
+
return tide_df.drop("tide_height", axis=1)
|
946
|
+
|
947
|
+
return tide_df
|