Skip to content

Commit bfacc27

Browse files
authored
Merge pull request #99 from GeoscienceAustralia/eo_tides
Update DEA Intertidal to use `eo-tides`
2 parents f42dff6 + 3f52e98 commit bfacc27

15 files changed

+782
-1738
lines changed

intertidal/elevation.py

+41-34
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from odc.geo.geom import BoundingBox
1616
from odc.algo import xr_quantile
1717
from datacube.utils.aws import configure_s3_access
18-
from dea_tools.coastal import pixel_tides
18+
from eo_tides.eo import pixel_tides
1919
from dea_tools.dask import create_local_dask_cluster
2020

2121
from intertidal.io import (
@@ -29,6 +29,7 @@
2929
from intertidal.utils import (
3030
configure_logging,
3131
round_date_strings,
32+
spearman_correlation,
3233
)
3334
from intertidal.extents import extents, load_connectivity_mask
3435
from intertidal.exposure import exposure
@@ -43,6 +44,7 @@ def ds_to_flat(
4344
max_freq=0.99,
4445
min_correlation=0.15,
4546
corr_method="pearson",
47+
apply_threshold=True,
4648
correct_seasonality=False,
4749
valid_mask=None,
4850
):
@@ -77,6 +79,11 @@ def ds_to_flat(
7779
corr_method : str, optional
7880
Correlation method to use. Defaults to "pearson", also supports
7981
"spearman".
82+
apply_threshold : bool, optional
83+
Whether to threshold the water index timeseries before calculating
84+
correlations, to ensure small changes in index values beneath the
85+
water surface are not included when calcualting correlations.
86+
Defaults to True.
8087
correct_seasonality : bool, optional
8188
If True, remove any seasonal signal from the tide height data
8289
by subtracting monthly mean tide height from each value. This
@@ -132,12 +139,17 @@ def ds_to_flat(
132139
clear = clear.stack(z=("y", "x"))
133140

134141
# Calculate correlations between NDWI water observations and tide
135-
# height. Because we are only interested in pixels with inundation
136-
# patterns (e.g.transitions from dry to wet) are driven by tide, we
137-
# first convert NDWI into a boolean dry/wet layer before running the
138-
# correlation. This prevents small changes in NDWI beneath the water
139-
# surface from producing correlations with tide height.
140-
wet_dry = flat_ds[index] > ndwi_thresh
142+
# height. By default, because we are only interested in pixels with
143+
# inundation patterns (e.g.transitions from dry to wet) that are driven
144+
# by the tide, we first convert NDWI into a boolean dry/wet layer before
145+
# running the correlation. This prevents small changes in NDWI beneath
146+
# the water surface from producing correlations with tide height.
147+
# This can be turned off by passing `apply_threshold=False`.
148+
if apply_threshold:
149+
wet_dry = flat_ds[index] > ndwi_thresh
150+
else:
151+
print("Using raw water index values for correlation")
152+
wet_dry = flat_ds[index]
141153

142154
# Use either tides directly or correct to remove seasonal signal
143155
if correct_seasonality:
@@ -149,19 +161,13 @@ def ds_to_flat(
149161

150162
# Calculate correlation
151163
if corr_method == "pearson":
152-
corr = xr.corr(wet_dry, tide_array, dim="time").rename("qa_ndwi_corr")
164+
corr = xr.corr(wet_dry, tide_array, dim="time")
153165
elif corr_method == "spearman":
154-
import xskillscore
155-
156-
corr = xskillscore.spearman_r(
157-
flat_ds[index], tide_array, dim="time", skipna=True, keep_attrs=True
158-
).rename("qa_ndwi_corr")
159-
160-
# TODO: investigate alternative function from DEA Tools
161-
# (doesn't currently handle multiple tide models)
162-
# corr = lag_linregress_3D(x=flat_ds.tide_m, y=wet_dry).cor.rename("qa_ndwi_corr")
166+
print("Applying Spearman correlation")
167+
corr = spearman_correlation(x=wet_dry, y=tide_array, dim="time")
163168

164169
# Keep only pixels with correlations that meet min threshold
170+
corr = corr.rename("qa_ndwi_corr")
165171
corr_mask = corr >= min_correlation
166172
flat_ds = flat_ds.where(corr_mask, drop=True)
167173

@@ -785,14 +791,14 @@ def elevation(
785791
window_prop_tide=0.15,
786792
correct_seasonality=False,
787793
max_workers=None,
788-
tide_model="FES2014",
794+
tide_model="EOT20",
789795
tide_model_dir="/var/share/tide_models",
790796
run_id=None,
791797
log=None,
792798
):
793799
"""
794-
Calculates DEA Intertidal Elevation using satellite imagery and
795-
tidal modeling.
800+
Generates DEA Intertidal Elevation outputs using satellite imagery
801+
and tidal modeling.
796802
797803
Parameters
798804
----------
@@ -835,18 +841,19 @@ def elevation(
835841
determine workers.
836842
tide_model : str, optional
837843
The tide model or a list of models used to model tides, as
838-
supported by the `pyTMD` Python package. Options include:
839-
- "FES2014" (default; pre-configured on DEA Sandbox)
840-
- "TPXO9-atlas-v5"
841-
- "TPXO8-atlas"
842-
- "EOT20"
843-
- "HAMTIDE11"
844-
- "GOT4.10"
844+
supported by the `eo-tides` Python package. Options include:
845+
- "EOT20" (default)
846+
- "TPXO10-atlas-v2-nc"
847+
- "FES2022"
848+
- "FES2022_extrapolated"
849+
- "FES2014"
850+
- "FES2014_extrapolated"
851+
- "GOT5.6"
845852
- "ensemble" (experimental: combine all above into single ensemble)
846853
tide_model_dir : str, optional
847854
The directory containing tide model data files. Defaults to
848855
"/var/share/tide_models"; for more information about the
849-
directory structure, refer to `dea_tools.coastal.model_tides`.
856+
directory structure, refer to `eo-tides.utils.list_models`.
850857
run_id : string, optional
851858
An optional string giving the name of the analysis; used to
852859
prefix log entries.
@@ -881,8 +888,8 @@ def elevation(
881888
# dataset (x by y by time). If `model` is "ensemble" this will model
882889
# tides by combining the best local tide models.
883890
log.info(f"{run_id}: Modelling tide heights for each pixel")
884-
tide_m, _ = pixel_tides(
885-
ds=satellite_ds,
891+
tide_m = pixel_tides(
892+
data=satellite_ds,
886893
model=tide_model,
887894
directory=tide_model_dir,
888895
)
@@ -1097,18 +1104,18 @@ def elevation(
10971104
"--tide_model",
10981105
type=str,
10991106
multiple=True,
1100-
default=["FES2014"],
1107+
default=["EOT20"],
11011108
help="The model used for tide modelling, as supported by the "
1102-
"`pyTMD` Python package. Options include 'FES2014' (default), "
1103-
"'TPXO9-atlas-v5', 'TPXO8-atlas-v1', 'EOT20', 'HAMTIDE11', 'GOT4.10'. ",
1109+
"`eo-tides` Python package. Options include 'EOT20' (default), "
1110+
"'TPXO10-atlas-v2-nc', 'FES2022', 'FES2014', 'GOT5.6', 'ensemble'."
11041111
)
11051112
@click.option(
11061113
"--tide_model_dir",
11071114
type=str,
11081115
default="/var/share/tide_models",
11091116
help="The directory containing tide model data files. Defaults to "
11101117
"'/var/share/tide_models'; for more information about the required "
1111-
"directory structure, refer to `dea_tools.coastal.model_tides`.",
1118+
"directory structure, refer to `eo-tides.utils.list_models`.",
11121119
)
11131120
@click.option(
11141121
"--modelled_freq",

intertidal/exposure.py

+25-24
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pandas as pd
1010

1111
from math import ceil
12-
from dea_tools.coastal import _pixel_tides_resample, pixel_tides
12+
from eo_tides.eo import _pixel_tides_resample, pixel_tides
1313
from intertidal.utils import configure_logging, round_date_strings
1414

1515

@@ -235,7 +235,7 @@ def exposure(
235235
start_date,
236236
end_date,
237237
modelled_freq="30min",
238-
tide_model="FES2014",
238+
tide_model="EOT20",
239239
tide_model_dir="/var/share/tide_models",
240240
filters=None,
241241
filters_combined=None,
@@ -281,19 +281,19 @@ def exposure(
281281
cadence. Defaults to '30min'.
282282
tide_model : str, optional
283283
The tide model or a list of models used to model tides, as
284-
supported by the `pyTMD` Python package. Options include:
285-
- "FES2014" (default; pre-configured on DEA Sandbox)
284+
supported by the `eo-tides` Python package. Options include:
285+
- "EOT20" (default)
286+
- "TPXO10-atlas-v2-nc"
286287
- "FES2022"
287-
- "TPXO9-atlas-v5"
288-
- "TPXO8-atlas"
289-
- "EOT20"
290-
- "HAMTIDE11"
291-
- "GOT4.10"
292-
- "ensemble" (combine above into single ensemble)
288+
- "FES2022_extrapolated"
289+
- "FES2014"
290+
- "FES2014_extrapolated"
291+
- "GOT5.6"
292+
- "ensemble" (experimental: combine all above into single ensemble)
293293
tide_model_dir : str, optional
294294
The directory containing tide model data files. Defaults to
295295
"/var/share/tide_models"; for more information about the
296-
directory structure, refer to `dea_tools.coastal.model_tides`.
296+
directory structure, refer to `eo-tides.utils.list_models`.
297297
filters : list of strings, optional
298298
An optional list of customisation options to input into the tidal
299299
modelling to calculate exposure. Filters include:
@@ -441,9 +441,9 @@ def exposure(
441441

442442
# Run tide model at low resolution
443443
modelledtides_lowres = pixel_tides(
444-
ds=dem,
444+
data=dem,
445+
time=time_range,
445446
model=tide_model,
446-
times=time_range,
447447
directory=tide_model_dir,
448448
resample=False,
449449
)
@@ -455,17 +455,18 @@ def exposure(
455455
# pixel resolution if any filter is "unfiltered"
456456
if "unfiltered" in filters:
457457

458-
# Convert to quantiles
459-
modelledtides_lowres_quantiles = modelledtides_lowres.quantile(
460-
q=calculate_quantiles, dim="time"
461-
).astype(modelledtides_lowres.dtype)
462-
463-
# Reproject into pixel resolution, after making sure CRS is present
464-
modelledtides_highres, _ = _pixel_tides_resample(
465-
tides_lowres=modelledtides_lowres_quantiles.odc.assign_crs(
466-
dem.odc.geobox.crs
467-
),
468-
ds=dem,
458+
# Convert to quantiles, and make sure CRS is present
459+
modelledtides_lowres_quantiles = (
460+
modelledtides_lowres.quantile(q=calculate_quantiles, dim="time")
461+
.astype(modelledtides_lowres.dtype)
462+
.odc.assign_crs(dem.odc.geobox.crs)
463+
)
464+
465+
# Reproject into pixel resolution
466+
modelledtides_highres = _pixel_tides_resample(
467+
tides_lowres=modelledtides_lowres_quantiles,
468+
dask_chunks=dem.shape,
469+
gbox=dem.odc.geobox,
469470
)
470471

471472
# Add pixel resolution tides into to output dataset

intertidal/tidal_bias_offset.py

+2-66
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ def bias_offset(tide_m, tide_cq, lat_hat=True, lot_hot=None):
1111
Optionally, also return the highest and lowest astronomical and
1212
sensor-observed tides for each pixel.
1313
14+
TODO: update to use `eo-tides.stats` functionality
15+
1416
Parameters
1517
----------
1618
tide_m : xr.DataArray
@@ -84,69 +86,3 @@ def bias_offset(tide_m, tide_cq, lat_hat=True, lot_hot=None):
8486
else:
8587
return spread, offset_lowtide, offset_hightide
8688

87-
88-
# def tidal_offset_tidelines(extents, offset_hightide, offset_lowtide, distance=500):
89-
# """
90-
# This function extracts high and low tidelines from a rasterised
91-
# 'sometimes wet' layer in the extents input xr.DataArray,
92-
# calculates the tidal offsets at each point on the lines, and returns
93-
# the offset values in separate `geopandas.GeoDataFrame` objects.
94-
95-
# Parameters
96-
# ----------
97-
# extents : xarray.DataArray
98-
# An xarray.DataArray containing binary shoreline information,
99-
# depicting always, sometimes and never wet pixels.
100-
# offset_hightide: xarray.DataArray
101-
# An xarray.DataArray containing the percentage high-tide offset of the
102-
# satellite observed tide heights from the modelled heights.
103-
# offset_lowtide: xarray.DataArray
104-
# An xarray.DataArray containing the percentage low-tide offset of the
105-
# satellite observed tide heights from the modelled heights.
106-
# distance : integer or float, optional
107-
# A number giving the interval at which to generate points along
108-
# the line feature. Defaults to 500, which will generate a point
109-
# at every 500 metres along the line.
110-
111-
# Returns
112-
# -------
113-
# tuple
114-
# A tuple of two `geopandas.GeoDataFrame` objects containing the
115-
# high and low tidelines with their respective tidal offsets and
116-
# a `geopandas.GeoDataFrame` containing the multilinestring tidelines.
117-
# """
118-
# ## Create a three class extents dataset: tidal wet/intertidal/dry
119-
# extents = extents.where((extents == 0) | (extents == 1) | (extents == 2), np.nan)
120-
121-
# # Extract the high/low tide boundaries
122-
# tidelines_gdf = subpixel_contours(da=extents, z_values=[1.5, 0.5])
123-
124-
# # Translate the high/Low tidelines into point data at regular intervals
125-
# lowtideline = points_on_line(tidelines_gdf, 0, distance=distance)
126-
# hightideline = points_on_line(tidelines_gdf, 1, distance=distance)
127-
128-
# # Extract the point coordinates into xarray for the hightideline dataset
129-
# x_indexer_high = xr.DataArray(hightideline.centroid.x, dims=["point"])
130-
# y_indexer_high = xr.DataArray(hightideline.centroid.y, dims=["point"])
131-
132-
# # Extract the point coordinates into xarray for the lowtideline dataset
133-
# x_indexer_low = xr.DataArray(lowtideline.centroid.x, dims=["point"])
134-
# y_indexer_low = xr.DataArray(lowtideline.centroid.y, dims=["point"])
135-
136-
# # Extract the high or low tide offset at each point in the high and low tidelines respectively
137-
# # From https://stackoverflow.com/questions/67425567/extract-values-from-xarray-dataset-using-geopandas-multilinestring
138-
# highlineoffset = offset_hightide.sel(
139-
# x=x_indexer_high, y=y_indexer_high, method="nearest"
140-
# )
141-
# lowlineoffset = offset_lowtide.sel(
142-
# x=x_indexer_low, y=y_indexer_low, method="nearest"
143-
# )
144-
145-
# # Replace the offset values per point into the master dataframes
146-
# hightideline["offset_hightide"] = highlineoffset
147-
# lowtideline["offset_lowtide"] = lowlineoffset
148-
149-
# ## Consider adding the points to the tidelines
150-
# ## https://gis.stackexchange.com/questions/448788/merging-points-to-linestrings-using-geopandas
151-
152-
# return hightideline, lowtideline, tidelines_gdf

0 commit comments

Comments
 (0)