eo-tides 0.0.21__py3-none-any.whl → 0.0.23__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/eo.py CHANGED
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import os
5
+ import warnings
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  import odc.geo.xr
@@ -93,19 +94,17 @@ def _pixel_tides_resample(
93
94
 
94
95
 
95
96
  def tag_tides(
96
- ds: xr.Dataset,
97
+ ds: xr.Dataset | xr.DataArray,
97
98
  model: str | list[str] = "EOT20",
98
99
  directory: str | os.PathLike | None = None,
99
100
  tidepost_lat: float | None = None,
100
101
  tidepost_lon: float | None = None,
101
- ebb_flow: bool = False,
102
- swap_dims: bool = False,
103
102
  **model_tides_kwargs,
104
- ) -> xr.Dataset:
103
+ ) -> xr.DataArray:
105
104
  """
106
105
  Model tide heights for every timestep in a multi-dimensional
107
- dataset, and add them as a new `tide_height` (and optionally,
108
- `ebb_flow`) variable that "tags" each observation with tide data.
106
+ dataset, and return a new `tide_height` array that can
107
+ be used to "tag" each observation with tide data.
109
108
 
110
109
  The function models tides at the centroid of the dataset
111
110
  by default, but a custom tidal modelling location can
@@ -123,7 +122,7 @@ def tag_tides(
123
122
 
124
123
  Parameters
125
124
  ----------
126
- ds : xarray.Dataset
125
+ ds : xarray.Dataset or xarray.DataArray
127
126
  A multi-dimensional dataset (e.g. "x", "y", "time") to
128
127
  tag with tide heights. This dataset must contain a "time"
129
128
  dimension.
@@ -143,16 +142,6 @@ def tag_tides(
143
142
  Optional coordinates used to model tides. The default is None,
144
143
  which uses the centroid of the dataset as the tide modelling
145
144
  location.
146
- ebb_flow : bool, optional
147
- An optional boolean indicating whether to compute if the
148
- tide phase was ebbing (falling) or flowing (rising) for each
149
- observation. The default is False; if set to True, a new
150
- "ebb_flow" variable will be added to the dataset with each
151
- observation labelled with "Ebb" or "Flow".
152
- swap_dims : bool, optional
153
- An optional boolean indicating whether to swap the `time`
154
- dimension in the original `ds` to the new "tide_height"
155
- variable. Defaults to False.
156
145
  **model_tides_kwargs :
157
146
  Optional parameters passed to the `eo_tides.model.model_tides`
158
147
  function. Important parameters include `cutoff` (used to
@@ -174,8 +163,6 @@ def tag_tides(
174
163
 
175
164
  # Standardise model into a list for easy handling. and verify only one
176
165
  model = [model] if isinstance(model, str) else model
177
- if (len(model) > 1) & swap_dims:
178
- raise ValueError("Can only swap dimensions when a single tide model is passed to `model`.")
179
166
 
180
167
  # If custom tide modelling locations are not provided, use the
181
168
  # dataset centroid
@@ -208,48 +195,38 @@ def tag_tides(
208
195
  f"`tidepost_lat` and `tidepost_lon` parameters."
209
196
  )
210
197
 
211
- # Optionally calculate the tide phase for each observation
212
- if ebb_flow:
213
- # Model tides for a time 15 minutes prior to each previously
214
- # modelled satellite acquisition time. This allows us to compare
215
- # tide heights to see if they are rising or falling.
216
- print("Modelling tidal phase (e.g. ebb or flow)")
217
- tide_pre_df = model_tides(
218
- x=lon, # type: ignore
219
- y=lat, # type: ignore
220
- time=(ds.time - pd.Timedelta("15 min")),
221
- model=model,
222
- directory=directory,
223
- crs="EPSG:4326",
224
- **model_tides_kwargs,
225
- )
226
-
227
- # Compare tides computed for each timestep. If the previous tide
228
- # was higher than the current tide, the tide is 'ebbing'. If the
229
- # previous tide was lower, the tide is 'flowing'
230
- tide_df["ebb_flow"] = (tide_df.tide_height < tide_pre_df.tide_height.values).replace({
231
- True: "Ebb",
232
- False: "Flow",
233
- })
198
+ # # Optionally calculate the tide phase for each observation
199
+ # if ebb_flow:
200
+ # # Model tides for a time 15 minutes prior to each previously
201
+ # # modelled satellite acquisition time. This allows us to compare
202
+ # # tide heights to see if they are rising or falling.
203
+ # print("Modelling tidal phase (e.g. ebb or flow)")
204
+ # tide_pre_df = model_tides(
205
+ # x=lon, # type: ignore
206
+ # y=lat, # type: ignore
207
+ # time=(ds.time - pd.Timedelta("15 min")),
208
+ # model=model,
209
+ # directory=directory,
210
+ # crs="EPSG:4326",
211
+ # **model_tides_kwargs,
212
+ # )
213
+
214
+ # # Compare tides computed for each timestep. If the previous tide
215
+ # # was higher than the current tide, the tide is 'ebbing'. If the
216
+ # # previous tide was lower, the tide is 'flowing'
217
+ # tide_df["ebb_flow"] = (tide_df.tide_height < tide_pre_df.tide_height.values).replace({
218
+ # True: "Ebb",
219
+ # False: "Flow",
220
+ # })
234
221
 
235
222
  # Convert to xarray format
236
- tide_xr = tide_df.reset_index().set_index(["time", "tide_model"]).drop(["x", "y"], axis=1).to_xarray()
223
+ tide_xr = tide_df.reset_index().set_index(["time", "tide_model"]).drop(["x", "y"], axis=1).tide_height.to_xarray()
237
224
 
238
225
  # If only one tidal model exists, squeeze out "tide_model" dim
239
226
  if len(tide_xr.tide_model) == 1:
240
- tide_xr = tide_xr.squeeze("tide_model", drop=True)
241
-
242
- # Add each array into original dataset
243
- for var in tide_xr.data_vars:
244
- ds[var] = tide_xr[var]
245
-
246
- # Swap dimensions and sort by tide height
247
- if swap_dims:
248
- ds = ds.swap_dims({"time": "tide_height"})
249
- ds = ds.sortby("tide_height")
250
- ds = ds.drop_vars("time")
227
+ tide_xr = tide_xr.squeeze("tide_model")
251
228
 
252
- return ds
229
+ return tide_xr
253
230
 
254
231
 
255
232
  def pixel_tides(
@@ -493,8 +470,10 @@ def pixel_tides(
493
470
  # Set dtype to dtype of the input data as quantile always returns
494
471
  # float64 (memory intensive)
495
472
  if calculate_quantiles is not None:
496
- print("Computing tide quantiles")
497
- tides_lowres = tides_lowres.quantile(q=calculate_quantiles, dim="time").astype(tides_lowres.dtype)
473
+ with warnings.catch_warnings():
474
+ warnings.simplefilter("ignore")
475
+ print("Computing tide quantiles")
476
+ tides_lowres = tides_lowres.quantile(q=calculate_quantiles, dim="time").astype(tides_lowres.dtype)
498
477
 
499
478
  # If only one tidal model exists, squeeze out "tide_model" dim
500
479
  if len(tides_lowres.tide_model) == 1:
eo_tides/model.py CHANGED
@@ -5,6 +5,7 @@ import os
5
5
  import pathlib
6
6
  import warnings
7
7
  from concurrent.futures import ProcessPoolExecutor
8
+ from concurrent.futures.process import BrokenProcessPool
8
9
  from functools import partial
9
10
  from typing import TYPE_CHECKING
10
11
 
@@ -130,7 +131,7 @@ def list_models(
130
131
  # Mark available models with a green tick
131
132
  status = "✅"
132
133
  print(f"{status:^{status_width}}│ {m:<{name_width}} │ {expected_paths[m]:<{path_width}}")
133
- except:
134
+ except FileNotFoundError:
134
135
  if show_supported:
135
136
  # Mark unavailable models with a red cross
136
137
  status = "❌"
@@ -199,84 +200,99 @@ def _model_tides(
199
200
  lat.max() + buffer,
200
201
  ]
201
202
 
202
- # Read tidal constants and interpolate to grid points
203
- if pytmd_model.format in ("OTIS", "ATLAS-compact", "TMD3"):
204
- amp, ph, D, c = pyTMD.io.OTIS.extract_constants(
205
- lon,
206
- lat,
207
- pytmd_model.grid_file,
208
- pytmd_model.model_file,
209
- pytmd_model.projection,
210
- type=pytmd_model.type,
211
- grid=pytmd_model.file_format,
212
- crop=crop,
213
- bounds=bounds,
214
- method=method,
215
- extrapolate=extrapolate,
216
- cutoff=cutoff,
217
- )
203
+ try:
204
+ # Read tidal constants and interpolate to grid points
205
+ if pytmd_model.format in ("OTIS", "ATLAS-compact", "TMD3"):
206
+ amp, ph, D, c = pyTMD.io.OTIS.extract_constants(
207
+ lon,
208
+ lat,
209
+ pytmd_model.grid_file,
210
+ pytmd_model.model_file,
211
+ pytmd_model.projection,
212
+ type=pytmd_model.type,
213
+ grid=pytmd_model.file_format,
214
+ crop=crop,
215
+ bounds=bounds,
216
+ method=method,
217
+ extrapolate=extrapolate,
218
+ cutoff=cutoff,
219
+ )
218
220
 
219
- # Use delta time at 2000.0 to match TMD outputs
220
- deltat = np.zeros((len(timescale)), dtype=np.float64)
221
-
222
- elif pytmd_model.format in ("ATLAS-netcdf",):
223
- amp, ph, D, c = pyTMD.io.ATLAS.extract_constants(
224
- lon,
225
- lat,
226
- pytmd_model.grid_file,
227
- pytmd_model.model_file,
228
- type=pytmd_model.type,
229
- crop=crop,
230
- bounds=bounds,
231
- method=method,
232
- extrapolate=extrapolate,
233
- cutoff=cutoff,
234
- scale=pytmd_model.scale,
235
- compressed=pytmd_model.compressed,
236
- )
221
+ # Use delta time at 2000.0 to match TMD outputs
222
+ deltat = np.zeros((len(timescale)), dtype=np.float64)
223
+
224
+ elif pytmd_model.format in ("ATLAS-netcdf",):
225
+ amp, ph, D, c = pyTMD.io.ATLAS.extract_constants(
226
+ lon,
227
+ lat,
228
+ pytmd_model.grid_file,
229
+ pytmd_model.model_file,
230
+ type=pytmd_model.type,
231
+ crop=crop,
232
+ bounds=bounds,
233
+ method=method,
234
+ extrapolate=extrapolate,
235
+ cutoff=cutoff,
236
+ scale=pytmd_model.scale,
237
+ compressed=pytmd_model.compressed,
238
+ )
237
239
 
238
- # Use delta time at 2000.0 to match TMD outputs
239
- deltat = np.zeros((len(timescale)), dtype=np.float64)
240
-
241
- elif pytmd_model.format in ("GOT-ascii", "GOT-netcdf"):
242
- amp, ph, c = pyTMD.io.GOT.extract_constants(
243
- lon,
244
- lat,
245
- pytmd_model.model_file,
246
- grid=pytmd_model.type,
247
- crop=crop,
248
- bounds=bounds,
249
- method=method,
250
- extrapolate=extrapolate,
251
- cutoff=cutoff,
252
- scale=pytmd_model.scale,
253
- compressed=pytmd_model.compressed,
254
- )
240
+ # Use delta time at 2000.0 to match TMD outputs
241
+ deltat = np.zeros((len(timescale)), dtype=np.float64)
242
+
243
+ elif pytmd_model.format in ("GOT-ascii", "GOT-netcdf"):
244
+ amp, ph, c = pyTMD.io.GOT.extract_constants(
245
+ lon,
246
+ lat,
247
+ pytmd_model.model_file,
248
+ grid=pytmd_model.file_format,
249
+ crop=crop,
250
+ bounds=bounds,
251
+ method=method,
252
+ extrapolate=extrapolate,
253
+ cutoff=cutoff,
254
+ scale=pytmd_model.scale,
255
+ compressed=pytmd_model.compressed,
256
+ )
255
257
 
256
- # Delta time (TT - UT1)
257
- deltat = timescale.tt_ut1
258
-
259
- elif pytmd_model.format in ("FES-ascii", "FES-netcdf"):
260
- amp, ph = pyTMD.io.FES.extract_constants(
261
- lon,
262
- lat,
263
- pytmd_model.model_file,
264
- type=pytmd_model.type,
265
- version=pytmd_model.version,
266
- crop=crop,
267
- bounds=bounds,
268
- method=method,
269
- extrapolate=extrapolate,
270
- cutoff=cutoff,
271
- scale=pytmd_model.scale,
272
- compressed=pytmd_model.compressed,
273
- )
258
+ # Delta time (TT - UT1)
259
+ deltat = timescale.tt_ut1
260
+
261
+ elif pytmd_model.format in ("FES-ascii", "FES-netcdf"):
262
+ amp, ph = pyTMD.io.FES.extract_constants(
263
+ lon,
264
+ lat,
265
+ pytmd_model.model_file,
266
+ type=pytmd_model.type,
267
+ version=pytmd_model.version,
268
+ crop=crop,
269
+ bounds=bounds,
270
+ method=method,
271
+ extrapolate=extrapolate,
272
+ cutoff=cutoff,
273
+ scale=pytmd_model.scale,
274
+ compressed=pytmd_model.compressed,
275
+ )
274
276
 
275
- # Available model constituents
276
- c = pytmd_model.constituents
277
+ # Available model constituents
278
+ c = pytmd_model.constituents
277
279
 
278
- # Delta time (TT - UT1)
279
- deltat = timescale.tt_ut1
280
+ # Delta time (TT - UT1)
281
+ deltat = timescale.tt_ut1
282
+ else:
283
+ raise Exception(
284
+ f"Unsupported model format ({pytmd_model.format}). This may be due to an incompatible version of `pyTMD`."
285
+ )
286
+
287
+ # Raise error if constituent files no not cover analysis extent
288
+ except IndexError:
289
+ error_msg = (
290
+ f"The {model} tide model constituent files do not cover the requested analysis extent. "
291
+ "This can occur if you are using clipped model files to improve run times. "
292
+ "Consider using model files that cover your analysis area, or set `crop=False` "
293
+ "to reduce the extent of tide model constituent files that is loaded."
294
+ )
295
+ raise Exception(error_msg)
280
296
 
281
297
  # Calculate complex phase in radians for Euler's
282
298
  cph = -1j * ph * np.pi / 180.0
@@ -783,12 +799,19 @@ def model_tides(
783
799
  )
784
800
 
785
801
  # Apply func in parallel, iterating through each input param
786
- model_outputs = list(
787
- tqdm(
788
- executor.map(iter_func, model_iters, x_iters, y_iters, time_iters),
789
- total=len(model_iters),
790
- ),
791
- )
802
+ try:
803
+ model_outputs = list(
804
+ tqdm(
805
+ executor.map(iter_func, model_iters, x_iters, y_iters, time_iters),
806
+ total=len(model_iters),
807
+ ),
808
+ )
809
+ except BrokenProcessPool:
810
+ error_msg = (
811
+ "Parallelised tide modelling failed, likely to to an out-of-memory error. "
812
+ "Try reducing the size of your analysis, or set `parallel=False`."
813
+ )
814
+ raise RuntimeError(error_msg)
792
815
 
793
816
  # Model tides in series if parallelisation is off
794
817
  else:
eo_tides/stats.py CHANGED
@@ -8,16 +8,133 @@ import matplotlib.pyplot as plt
8
8
  import numpy as np
9
9
  import odc.geo.xr
10
10
  import pandas as pd
11
+ import xarray as xr
11
12
  from scipy import stats
12
13
 
13
14
  # Only import if running type checking
14
15
  if TYPE_CHECKING:
15
16
  import xarray as xr
16
17
 
17
- from .eo import tag_tides
18
+ from .eo import pixel_tides, tag_tides
18
19
  from .model import model_tides
19
20
 
20
21
 
22
+ def _plot_biases(
23
+ all_tides_df,
24
+ obs_tides_da,
25
+ lat,
26
+ lot,
27
+ hat,
28
+ hot,
29
+ offset_low,
30
+ offset_high,
31
+ spread,
32
+ plot_col,
33
+ obs_linreg,
34
+ obs_x,
35
+ all_timerange,
36
+ ):
37
+ """
38
+ Plot tide bias statistics as a figure, including both
39
+ satellite observations and all modelled tides.
40
+ """
41
+
42
+ # Create plot and add all time and observed tide data
43
+ fig, ax = plt.subplots(figsize=(10, 6))
44
+ all_tides_df.reset_index(["x", "y"]).tide_height.plot(ax=ax, alpha=0.4, label="Modelled tides")
45
+
46
+ # Look through custom column values if provided
47
+ if plot_col is not None:
48
+ # Create a list of marker styles
49
+ markers = [
50
+ "o",
51
+ "^",
52
+ "s",
53
+ "D",
54
+ "v",
55
+ "<",
56
+ ">",
57
+ "p",
58
+ "*",
59
+ "h",
60
+ "H",
61
+ "+",
62
+ "x",
63
+ "d",
64
+ "|",
65
+ "_",
66
+ ]
67
+ for i, value in enumerate(np.unique(plot_col)):
68
+ obs_tides_da.sel(time=plot_col == value).plot.line(
69
+ ax=ax,
70
+ linewidth=0.0,
71
+ color="black",
72
+ marker=markers[i % len(markers)],
73
+ markersize=4,
74
+ label=value,
75
+ )
76
+ # Otherwise, plot all data at once
77
+ else:
78
+ obs_tides_da.plot.line(
79
+ ax=ax,
80
+ marker="o",
81
+ linewidth=0.0,
82
+ color="black",
83
+ markersize=3.5,
84
+ label="Satellite observations",
85
+ )
86
+
87
+ # Add legend and remove title
88
+ ax.legend(
89
+ loc="upper center",
90
+ bbox_to_anchor=(0.5, 1.04),
91
+ ncol=20,
92
+ borderaxespad=0,
93
+ frameon=False,
94
+ )
95
+ ax.set_title("")
96
+
97
+ # Add linear regression line
98
+ if obs_linreg is not None:
99
+ ax.plot(
100
+ obs_tides_da.time.isel(time=[0, -1]),
101
+ obs_linreg.intercept + obs_linreg.slope * obs_x[[0, -1]],
102
+ "r",
103
+ label="fitted line",
104
+ )
105
+
106
+ # Add horizontal lines for spread/offsets
107
+ ax.axhline(lot, color="black", linestyle=":", linewidth=1)
108
+ ax.axhline(hot, color="black", linestyle=":", linewidth=1)
109
+ ax.axhline(lat, color="black", linestyle=":", linewidth=1)
110
+ ax.axhline(hat, color="black", linestyle=":", linewidth=1)
111
+
112
+ # Add text annotations for spread/offsets
113
+ ax.annotate(
114
+ f" High tide\n offset ({offset_high:.0%})",
115
+ xy=(all_timerange.max(), np.mean([hat, hot])),
116
+ va="center",
117
+ )
118
+ ax.annotate(
119
+ f" Spread\n ({spread:.0%})",
120
+ xy=(all_timerange.max(), np.mean([lot, hot])),
121
+ va="center",
122
+ )
123
+ ax.annotate(
124
+ f" Low tide\n offset ({offset_low:.0%})",
125
+ xy=(all_timerange.max(), np.mean([lat, lot])),
126
+ )
127
+
128
+ # Remove top right axes and add labels
129
+ ax.spines["right"].set_visible(False)
130
+ ax.spines["top"].set_visible(False)
131
+ ax.set_ylabel("Tide height (m)")
132
+ ax.set_xlabel("")
133
+ ax.margins(x=0.015)
134
+
135
+ return fig
136
+
137
+
21
138
  def tide_stats(
22
139
  ds: xr.Dataset,
23
140
  model: str = "EOT20",
@@ -26,15 +143,18 @@ def tide_stats(
26
143
  tidepost_lon: float | None = None,
27
144
  plain_english: bool = True,
28
145
  plot: bool = True,
29
- modelled_freq: str = "2h",
146
+ plot_col: str | None = None,
147
+ modelled_freq: str = "3h",
30
148
  linear_reg: bool = False,
149
+ min_max_q: tuple = (0.0, 1.0),
31
150
  round_stats: int = 3,
32
151
  **model_tides_kwargs,
33
152
  ) -> pd.Series:
34
153
  """
35
- Takes a multi-dimensional dataset and generate statistics
36
- about the data's astronomical and satellite-observed tide
37
- conditions.
154
+ Takes a multi-dimensional dataset and generate tide statistics
155
+ and satellite-observed tide bias metrics, calculated based on
156
+ every timestep in the satellte data and the geographic centroid
157
+ of the imagery.
38
158
 
39
159
  By comparing the subset of tides observed by satellites
40
160
  against the full astronomical tidal range, we can evaluate
@@ -48,8 +168,8 @@ def tide_stats(
48
168
  Parameters
49
169
  ----------
50
170
  ds : xarray.Dataset
51
- A multi-dimensional dataset (e.g. "x", "y", "time") to
52
- use to calculate tide statistics. This dataset must contain
171
+ A multi-dimensional dataset (e.g. "x", "y", "time") used
172
+ to calculate tide statistics. This dataset must contain
53
173
  a "time" dimension.
54
174
  model : string, optional
55
175
  The tide model to use to model tides. Defaults to "EOT20";
@@ -73,10 +193,14 @@ def tide_stats(
73
193
  An optional boolean indicating whether to plot how satellite-
74
194
  observed tide heights compare against the full tidal range.
75
195
  Defaults to True.
196
+ plot_col : str, optional
197
+ Optional name of a coordinate, dimension or variable in the array
198
+ that will be used to plot observations with unique symbols.
199
+ Defaults to None, which will plot all observations as circles.
76
200
  modelled_freq : str, optional
77
201
  An optional string giving the frequency at which to model tides
78
- when computing the full modelled tidal range. Defaults to '2h',
79
- which computes a tide height for every two hours across the
202
+ when computing the full modelled tidal range. Defaults to '3h',
203
+ which computes a tide height for every three hours across the
80
204
  temporal extent of `ds`.
81
205
  linear_reg: bool, optional
82
206
  Whether to return linear regression statistics that assess
@@ -84,6 +208,11 @@ def tide_stats(
84
208
  increasing trends over time. This may indicate whether your
85
209
  satellite data may produce misleading trends based on uneven
86
210
  sampling of the local tide regime.
211
+ min_max_q : tuple, optional
212
+ Quantiles used to calculate max and min observed and modelled
213
+ astronomical tides. By default `(0.0, 1.0)` which is equivalent
214
+ to minimum and maximum; to use a softer threshold that is more
215
+ robust to outliers, use e.g. `(0.1, 0.9)`.
87
216
  round_stats : int, optional
88
217
  The number of decimal places used to round the output statistics.
89
218
  Defaults to 3.
@@ -96,25 +225,27 @@ def tide_stats(
96
225
 
97
226
  Returns
98
227
  -------
99
- A `pandas.Series` containing the following statistics:
100
-
101
- - `tidepost_lat`: latitude used for modelling tide heights
102
- - `tidepost_lon`: longitude used for modelling tide heights
103
- - `observed_min_m`: minimum tide height observed by the satellite
104
- - `all_min_m`: minimum tide height from all available tides
105
- - `observed_max_m`: maximum tide height observed by the satellite
106
- - `all_max_m`: maximum tide height from all available tides
107
- - `observed_range_m`: tidal range observed by the satellite
108
- - `all_range_m`: full astronomical tidal range based on all available tides
109
- - `spread_m`: proportion of the full astronomical tidal range observed by the satellite (see Bishop-Taylor et al. 2018)
110
- - `low_tide_offset`: proportion of the lowest tides never observed by the satellite (see Bishop-Taylor et al. 2018)
111
- - `high_tide_offset`: proportion of the highest tides never observed by the satellite (see Bishop-Taylor et al. 2018)
112
-
113
- If `linear_reg = True`, the output will also contain:
228
+ stats_df : pandas.Series
229
+ A `pandas.Series` containing the following statistics:
230
+
231
+ - `y`: latitude used for modelling tide heights
232
+ - `x`: longitude used for modelling tide heights
233
+ - `mot`: mean tide height observed by the satellite (metres)
234
+ - `mat`: mean modelled astronomical tide height (metres)
235
+ - `lot`: minimum tide height observed by the satellite (metres)
236
+ - `lat`: minimum tide height from modelled astronomical tidal range (metres)
237
+ - `hot`: maximum tide height observed by the satellite (metres)
238
+ - `hat`: maximum tide height from modelled astronomical tidal range (metres)
239
+ - `otr`: tidal range observed by the satellite (metres)
240
+ - `tr`: modelled astronomical tide range (metres)
241
+ - `spread`: proportion of the full modelled tidal range observed by the satellite
242
+ - `offset_low`: proportion of the lowest tides never observed by the satellite
243
+ - `offset_high`: proportion of the highest tides never observed by the satellite
244
+
245
+ If `linear_reg = True`, the output will also contain:
114
246
 
115
247
  - `observed_slope`: slope of any relationship between observed tide heights and time
116
248
  - `observed_pval`: significance/p-value of any relationship between observed tide heights and time
117
-
118
249
  """
119
250
  # Verify that only one tide model is provided
120
251
  if isinstance(model, list):
@@ -126,7 +257,7 @@ def tide_stats(
126
257
  tidepost_lon, tidepost_lat = ds.odc.geobox.geographic_extent.centroid.coords[0]
127
258
 
128
259
  # Model tides for each observation in the supplied xarray object
129
- ds_tides = tag_tides(
260
+ obs_tides_da = tag_tides(
130
261
  ds,
131
262
  model=model,
132
263
  directory=directory,
@@ -135,14 +266,12 @@ def tide_stats(
135
266
  return_tideposts=True,
136
267
  **model_tides_kwargs,
137
268
  )
138
-
139
- # Drop spatial ref for nicer plotting
140
- ds_tides = ds_tides.drop_vars("spatial_ref")
269
+ obs_tides_da = obs_tides_da.sortby("time")
141
270
 
142
271
  # Generate range of times covering entire period of satellite record
143
272
  all_timerange = pd.date_range(
144
- start=ds_tides.time.min().item(),
145
- end=ds_tides.time.max().item(),
273
+ start=obs_tides_da.time.min().item(),
274
+ end=obs_tides_da.time.max().item(),
146
275
  freq=modelled_freq,
147
276
  )
148
277
 
@@ -158,10 +287,10 @@ def tide_stats(
158
287
  )
159
288
 
160
289
  # Get coarse statistics on all and observed tidal ranges
161
- obs_mean = ds_tides.tide_height.mean().item()
290
+ obs_mean = obs_tides_da.mean().item()
162
291
  all_mean = all_tides_df.tide_height.mean()
163
- obs_min, obs_max = ds_tides.tide_height.quantile([0.0, 1.0]).values
164
- all_min, all_max = all_tides_df.tide_height.quantile([0.0, 1.0]).values
292
+ obs_min, obs_max = obs_tides_da.quantile(min_max_q).values
293
+ all_min, all_max = all_tides_df.tide_height.quantile(min_max_q).values
165
294
 
166
295
  # Calculate tidal range
167
296
  obs_range = obs_max - obs_min
@@ -169,92 +298,84 @@ def tide_stats(
169
298
 
170
299
  # Calculate Bishop-Taylor et al. 2018 tidal metrics
171
300
  spread = obs_range / all_range
172
- low_tide_offset = abs(all_min - obs_min) / all_range
173
- high_tide_offset = abs(all_max - obs_max) / all_range
174
-
175
- # Extract x (time in decimal years) and y (distance) values
176
- all_times = all_tides_df.index.get_level_values("time")
177
- all_x = all_times.year + ((all_times.dayofyear - 1) / 365) + ((all_times.hour - 1) / 24)
178
- time_period = all_x.max() - all_x.min()
301
+ low_tide_offset_m = abs(all_min - obs_min)
302
+ high_tide_offset_m = abs(all_max - obs_max)
303
+ low_tide_offset = low_tide_offset_m / all_range
304
+ high_tide_offset = high_tide_offset_m / all_range
305
+
306
+ # Plain text descriptors
307
+ mean_diff = "higher" if obs_mean > all_mean else "lower"
308
+ mean_diff_icon = "⬆️" if obs_mean > all_mean else "⬇️"
309
+ spread_icon = "🟢" if spread >= 0.9 else "🟡" if 0.7 < spread <= 0.9 else "🔴"
310
+ low_tide_icon = "🟢" if low_tide_offset <= 0.1 else "🟡" if 0.1 <= low_tide_offset < 0.2 else "🔴"
311
+ high_tide_icon = "🟢" if high_tide_offset <= 0.1 else "🟡" if 0.1 <= high_tide_offset < 0.2 else "🔴"
179
312
 
180
313
  # Extract x (time in decimal years) and y (distance) values
181
- obs_x = ds_tides.time.dt.year + ((ds_tides.time.dt.dayofyear - 1) / 365) + ((ds_tides.time.dt.hour - 1) / 24)
182
- obs_y = ds_tides.tide_height.values.astype(np.float32)
314
+ obs_x = (
315
+ obs_tides_da.time.dt.year + ((obs_tides_da.time.dt.dayofyear - 1) / 365) + ((obs_tides_da.time.dt.hour) / 24)
316
+ )
317
+ obs_y = obs_tides_da.values.astype(np.float32)
183
318
 
184
319
  # Compute linear regression
185
320
  obs_linreg = stats.linregress(x=obs_x, y=obs_y)
186
321
 
187
322
  if plain_english:
323
+ print(f"\n\n🌊 Modelled astronomical tide range: {all_range:.2f} metres.")
324
+ print(f"🛰️ Observed tide range: {obs_range:.2f} metres.\n")
325
+ print(f"{spread_icon} {spread:.0%} of the modelled astronomical tide range was observed at this location.")
188
326
  print(
189
- f"\n{spread:.0%} of the {all_range:.2f} m modelled astronomical "
190
- f"tidal range is observed at this location.\nThe lowest "
191
- f"{low_tide_offset:.0%} and highest {high_tide_offset:.0%} "
192
- f"of astronomical tides are never observed.\n"
327
+ f"{high_tide_icon} The highest {high_tide_offset:.0%} ({high_tide_offset_m:.2f} metres) of the tide range was never observed."
328
+ )
329
+ print(
330
+ f"{low_tide_icon} The lowest {low_tide_offset:.0%} ({low_tide_offset_m:.2f} metres) of the tide range was never observed.\n"
331
+ )
332
+ print(f"🌊 Mean modelled astronomical tide height: {all_mean:.2f} metres.")
333
+ print(f"🛰️ Mean observed tide height: {obs_mean:.2f} metres.\n")
334
+ print(
335
+ f"{mean_diff_icon} The mean observed tide height was {obs_mean - all_mean:.2f} metres {mean_diff} than the mean modelled astronomical tide height."
193
336
  )
194
337
 
195
338
  if linear_reg:
196
- if obs_linreg.pvalue > 0.05:
197
- print(f"Observed tides show no significant trends " f"over the ~{time_period:.0f} year period.")
339
+ if obs_linreg.pvalue > 0.01:
340
+ print("Observed tides showed no significant trends over time.")
198
341
  else:
199
- obs_slope_desc = "decrease" if obs_linreg.slope < 0 else "increase"
342
+ obs_slope_desc = "decreasing" if obs_linreg.slope < 0 else "increasing"
200
343
  print(
201
- f"Observed tides {obs_slope_desc} significantly "
202
- f"(p={obs_linreg.pvalue:.3f}) over time by "
203
- f"{obs_linreg.slope:.03f} m per year (i.e. a "
204
- f"~{time_period * obs_linreg.slope:.2f} m "
205
- f"{obs_slope_desc} over the ~{time_period:.0f} year period)."
344
+ f"⚠️ Observed tides showed a significant {obs_slope_desc} trend over time (p={obs_linreg.pvalue:.3f}, {obs_linreg.slope:.2f} metres per year)"
206
345
  )
207
346
 
208
347
  if plot:
209
- # Create plot and add all time and observed tide data
210
- fig, ax = plt.subplots(figsize=(10, 5))
211
- all_tides_df.reset_index(["x", "y"]).tide_height.plot(ax=ax, alpha=0.4)
212
- ds_tides.tide_height.plot.line(ax=ax, marker="o", linewidth=0.0, color="black", markersize=2)
213
-
214
- # Add horizontal lines for spread/offsets
215
- ax.axhline(obs_min, color="black", linestyle=":", linewidth=1)
216
- ax.axhline(obs_max, color="black", linestyle=":", linewidth=1)
217
- ax.axhline(all_min, color="black", linestyle=":", linewidth=1)
218
- ax.axhline(all_max, color="black", linestyle=":", linewidth=1)
219
-
220
- # Add text annotations for spread/offsets
221
- ax.annotate(
222
- f" High tide\n offset ({high_tide_offset:.0%})",
223
- xy=(all_timerange.max(), np.mean([all_max, obs_max])),
224
- va="center",
225
- )
226
- ax.annotate(
227
- f" Spread\n ({spread:.0%})",
228
- xy=(all_timerange.max(), np.mean([obs_min, obs_max])),
229
- va="center",
230
- )
231
- ax.annotate(
232
- f" Low tide\n offset ({low_tide_offset:.0%})",
233
- xy=(all_timerange.max(), np.mean([all_min, obs_min])),
348
+ _plot_biases(
349
+ all_tides_df=all_tides_df,
350
+ obs_tides_da=obs_tides_da,
351
+ lat=all_min,
352
+ lot=obs_min,
353
+ hat=all_max,
354
+ hot=obs_max,
355
+ offset_low=low_tide_offset,
356
+ offset_high=high_tide_offset,
357
+ spread=spread,
358
+ plot_col=ds[plot_col] if plot_col else None,
359
+ obs_linreg=obs_linreg if linear_reg else None,
360
+ obs_x=obs_x,
361
+ all_timerange=all_timerange,
234
362
  )
235
363
 
236
- # Remove top right axes and add labels
237
- ax.spines["right"].set_visible(False)
238
- ax.spines["top"].set_visible(False)
239
- ax.set_ylabel("Tide height (m)")
240
- ax.set_xlabel("")
241
- ax.margins(x=0.015)
242
-
243
364
  # Export pandas.Series containing tidal stats
244
365
  output_stats = {
245
- "tidepost_lat": tidepost_lat,
246
- "tidepost_lon": tidepost_lon,
247
- "observed_mean_m": obs_mean,
248
- "all_mean_m": all_mean,
249
- "observed_min_m": obs_min,
250
- "all_min_m": all_min,
251
- "observed_max_m": obs_max,
252
- "all_max_m": all_max,
253
- "observed_range_m": obs_range,
254
- "all_range_m": all_range,
366
+ "y": tidepost_lat,
367
+ "x": tidepost_lon,
368
+ "mot": obs_mean,
369
+ "mat": all_mean,
370
+ "lot": obs_min,
371
+ "lat": all_min,
372
+ "hot": obs_max,
373
+ "hat": all_max,
374
+ "otr": obs_range,
375
+ "tr": all_range,
255
376
  "spread": spread,
256
- "low_tide_offset": low_tide_offset,
257
- "high_tide_offset": high_tide_offset,
377
+ "offset_low": low_tide_offset,
378
+ "offset_high": high_tide_offset,
258
379
  }
259
380
 
260
381
  if linear_reg:
@@ -263,4 +384,171 @@ def tide_stats(
263
384
  "observed_pval": obs_linreg.pvalue,
264
385
  })
265
386
 
266
- return pd.Series(output_stats).round(round_stats)
387
+ # Return pandas data
388
+ stats_df = pd.Series(output_stats).round(round_stats)
389
+ return stats_df
390
+
391
+
392
+ def pixel_stats(
393
+ ds: xr.Dataset | xr.DataArray,
394
+ model: str | list[str] = "EOT20",
395
+ directory: str | os.PathLike | None = None,
396
+ resample: bool = False,
397
+ modelled_freq="3h",
398
+ min_max_q=(0.0, 1.0),
399
+ extrapolate: bool = True,
400
+ cutoff: float = 10,
401
+ **pixel_tides_kwargs,
402
+ ) -> xr.Dataset:
403
+ """
404
+ Takes a multi-dimensional dataset and generate two-dimensional
405
+ tide statistics and satellite-observed tide bias metrics,
406
+ calculated based on every timestep in the satellte data and
407
+ modelled into the spatial extent of the imagery.
408
+
409
+ By comparing the subset of tides observed by satellites
410
+ against the full astronomical tidal range, we can evaluate
411
+ whether the tides observed by satellites are biased
412
+ (e.g. fail to observe either the highest or lowest tides).
413
+
414
+ Compared to `tide_stats`, this function models tide metrics
415
+ spatially to produce a two-dimensional output.
416
+
417
+ For more information about the tidal statistics computed by this
418
+ function, refer to Figure 8 in Bishop-Taylor et al. 2018:
419
+ <https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8>
420
+
421
+ Parameters
422
+ ----------
423
+ ds : xarray.Dataset or xarray.DataArray
424
+ A multi-dimensional dataset (e.g. "x", "y", "time") used
425
+ to calculate 2D tide statistics. This dataset must contain
426
+ a "time" dimension.
427
+ model : str or list of str, optional
428
+ The tide model (or models) to use to model tides. If a list is
429
+ provided, a new "tide_model" dimension will be added to `ds`.
430
+ Defaults to "EOT20"; for a full list of available/supported
431
+ models, run `eo_tides.model.list_models`.
432
+ directory : str, optional
433
+ The directory containing tide model data files. If no path is
434
+ provided, this will default to the environment variable
435
+ `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
436
+ Tide modelling files should be stored in sub-folders for each
437
+ model that match the structure required by `pyTMD`
438
+ (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
439
+ resample : bool, optional
440
+ Whether to resample tide statistics back into `ds`'s original
441
+ higher resolution grid. Defaults to False, which will return
442
+ lower-resolution statistics that are typically sufficient for
443
+ most purposes.
444
+ modelled_freq : str, optional
445
+ An optional string giving the frequency at which to model tides
446
+ when computing the full modelled tidal range. Defaults to '3h',
447
+ which computes a tide height for every three hours across the
448
+ temporal extent of `ds`.
449
+ min_max_q : tuple, optional
450
+ Quantiles used to calculate max and min observed and modelled
451
+ astronomical tides. By default `(0.0, 1.0)` which is equivalent
452
+ to minimum and maximum; to use a softer threshold that is more
453
+ robust to outliers, use e.g. `(0.1, 0.9)`.
454
+ extrapolate : bool, optional
455
+ Whether to extrapolate tides for x and y coordinates outside of
456
+ the valid tide modelling domain using nearest-neighbor. Defaults
457
+ to True.
458
+ cutoff : float, optional
459
+ Extrapolation cutoff in kilometers. To avoid producing tide
460
+ statistics too far inland, the default is 10 km.
461
+ **pixel_tides_kwargs :
462
+ Optional parameters passed to the `eo_tides.eo.pixel_tides`
463
+ function.
464
+
465
+ Returns
466
+ -------
467
+ stats_ds : xarray.Dataset
468
+ An `xarray.Dataset` containing the following statistics as two-dimensional data variables:
469
+
470
+ - `lot`: minimum tide height observed by the satellite (metres)
471
+ - `lat`: minimum tide height from modelled astronomical tidal range (metres)
472
+ - `hot`: maximum tide height observed by the satellite (metres)
473
+ - `hat`: maximum tide height from modelled astronomical tidal range (metres)
474
+ - `otr`: tidal range observed by the satellite (metres)
475
+ - `tr`: modelled astronomical tide range (metres)
476
+ - `spread`: proportion of the full modelled tidal range observed by the satellite
477
+ - `offset_low`: proportion of the lowest tides never observed by the satellite
478
+ - `offset_high`: proportion of the highest tides never observed by the satellite
479
+
480
+ """
481
+ # Model observed tides
482
+ obs_tides = pixel_tides(
483
+ ds,
484
+ resample=False,
485
+ model=model,
486
+ directory=directory,
487
+ calculate_quantiles=min_max_q,
488
+ extrapolate=extrapolate,
489
+ cutoff=cutoff,
490
+ **pixel_tides_kwargs,
491
+ )
492
+
493
+ # Generate times covering entire period of satellite record
494
+ all_timerange = pd.date_range(
495
+ start=ds.time.min().item(),
496
+ end=ds.time.max().item(),
497
+ freq=modelled_freq,
498
+ )
499
+
500
+ # Model all tides
501
+ all_tides = pixel_tides(
502
+ ds,
503
+ times=all_timerange,
504
+ model=model,
505
+ directory=directory,
506
+ calculate_quantiles=min_max_q,
507
+ resample=False,
508
+ extrapolate=extrapolate,
509
+ cutoff=cutoff,
510
+ **pixel_tides_kwargs,
511
+ )
512
+
513
+ # Calculate min and max tides
514
+ lot = obs_tides.isel(quantile=0)
515
+ hot = obs_tides.isel(quantile=-1)
516
+ lat = all_tides.isel(quantile=0)
517
+ hat = all_tides.isel(quantile=-1)
518
+
519
+ # Calculate tidal range
520
+ otr = hot - lot
521
+ tr = hat - lat
522
+
523
+ # Calculate Bishop-Taylor et al. 2018 tidal metrics
524
+ spread = otr / tr
525
+ offset_low_m = abs(lat - lot)
526
+ offset_high_m = abs(hat - hot)
527
+ offset_low = offset_low_m / tr
528
+ offset_high = offset_high_m / tr
529
+
530
+ # Combine into a single dataset
531
+ stats_ds = (
532
+ xr.merge(
533
+ [
534
+ hat.rename("hat"),
535
+ hot.rename("hot"),
536
+ lat.rename("lat"),
537
+ lot.rename("lot"),
538
+ otr.rename("otr"),
539
+ tr.rename("tr"),
540
+ spread.rename("spread"),
541
+ offset_low.rename("offset_low"),
542
+ offset_high.rename("offset_high"),
543
+ ],
544
+ compat="override",
545
+ )
546
+ .drop_vars("quantile")
547
+ .odc.assign_crs(crs=ds.odc.crs)
548
+ )
549
+
550
+ # Optionally resample into the original pixel grid of `ds`
551
+ if resample:
552
+ stats_ds = stats_ds.odc.reproject(how=ds.odc.geobox, resample_method="bilinear")
553
+
554
+ return stats_ds
eo_tides/validation.py CHANGED
@@ -153,19 +153,21 @@ def _load_gauge_metadata(metadata_path):
153
153
 
154
154
 
155
155
  def _load_gesla_dataset(site, path, na_value):
156
- gesla_df = (
157
- pd.read_csv(
158
- path,
159
- skiprows=41,
160
- names=["date", "time", "sea_level", "qc_flag", "use_flag"],
161
- sep=r"\s+", # sep="\s+",
162
- parse_dates=[[0, 1]],
163
- index_col=0,
164
- na_values=na_value,
156
+ with warnings.catch_warnings():
157
+ warnings.simplefilter("ignore", FutureWarning)
158
+ gesla_df = (
159
+ pd.read_csv(
160
+ path,
161
+ skiprows=41,
162
+ names=["date", "time", "sea_level", "qc_flag", "use_flag"],
163
+ sep=r"\s+", # sep="\s+",
164
+ parse_dates=[[0, 1]],
165
+ index_col=0,
166
+ na_values=na_value,
167
+ )
168
+ .rename_axis("time")
169
+ .assign(site_code=site)
165
170
  )
166
- .rename_axis("time")
167
- .assign(site_code=site)
168
- )
169
171
 
170
172
  return gesla_df
171
173
 
@@ -267,7 +269,12 @@ def load_gauge_gesla(
267
269
 
268
270
  # If x and y are single numbers, select nearest row
269
271
  elif isinstance(x, Number) & isinstance(y, Number):
270
- site_code = _nearest_row(metadata_gdf, x, y, max_distance).site_code
272
+ with warnings.catch_warnings():
273
+ warnings.simplefilter("ignore")
274
+ site_code = (
275
+ _nearest_row(metadata_gdf, x, y, max_distance).rename({"index_right": "site_code"}, axis=1).site_code
276
+ )
277
+ # site_code = _nearest_row(metadata_gdf, x, y, max_distance).site_code
271
278
 
272
279
  # Raise exception if no valid tide gauges are found
273
280
  if site_code.isnull().all():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eo-tides
3
- Version: 0.0.21
3
+ Version: 0.0.23
4
4
  Summary: Tide modelling tools for large-scale satellite earth observation analysis
5
5
  Author-email: Robbi Bishop-Taylor <Robbi.BishopTaylor@ga.gov.au>
6
6
  Project-URL: Homepage, https://GeoscienceAustralia.github.io/eo-tides/
@@ -22,23 +22,25 @@ Classifier: Programming Language :: Python :: 3.12
22
22
  Requires-Python: <4.0,>=3.9
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: colorama
26
- Requires-Dist: geopandas >=1.0.0
27
- Requires-Dist: numpy
28
- Requires-Dist: odc-geo
29
- Requires-Dist: pandas
30
- Requires-Dist: pyproj
25
+ Requires-Dist: colorama >=0.4.3
26
+ Requires-Dist: geopandas >=0.10.0
27
+ Requires-Dist: matplotlib >=3.8.0
28
+ Requires-Dist: numpy >=1.26.0
29
+ Requires-Dist: odc-geo >=0.4.7
30
+ Requires-Dist: pandas >=2.2.0
31
+ Requires-Dist: pyproj >=3.6.1
31
32
  Requires-Dist: pyTMD ==2.1.6
32
- Requires-Dist: scikit-learn
33
- Requires-Dist: scipy
34
- Requires-Dist: shapely
35
- Requires-Dist: tqdm
36
- Requires-Dist: xarray
33
+ Requires-Dist: scikit-learn >=1.4.0
34
+ Requires-Dist: scipy >=1.11.2
35
+ Requires-Dist: shapely >=2.0.6
36
+ Requires-Dist: tqdm >=4.55.0
37
+ Requires-Dist: xarray >=2022.3.0
37
38
  Provides-Extra: notebooks
38
39
  Requires-Dist: odc-stac >=0.3.10 ; extra == 'notebooks'
39
- Requires-Dist: pystac-client ; extra == 'notebooks'
40
- Requires-Dist: folium ; extra == 'notebooks'
41
- Requires-Dist: matplotlib ; extra == 'notebooks'
40
+ Requires-Dist: odc-geo[tiff,warp] >=0.4.7 ; extra == 'notebooks'
41
+ Requires-Dist: pystac-client >=0.8.3 ; extra == 'notebooks'
42
+ Requires-Dist: folium >=0.16.0 ; extra == 'notebooks'
43
+ Requires-Dist: planetary-computer >=1.0.0 ; extra == 'notebooks'
42
44
 
43
45
  # `eo-tides`: Tide modelling tools for large-scale satellite earth observation analysis
44
46
 
@@ -46,7 +48,7 @@ Requires-Dist: matplotlib ; extra == 'notebooks'
46
48
 
47
49
  [![Release](https://img.shields.io/github/v/release/GeoscienceAustralia/eo-tides)](https://pypi.org/project/eo-tides/)
48
50
  [![Build status](https://img.shields.io/github/actions/workflow/status/GeoscienceAustralia/eo-tides/main.yml?branch=main)](https://github.com/GeoscienceAustralia/eo-tides/actions/workflows/main.yml?query=branch%3Amain)
49
- ![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FGeoscienceAustralia%2Feo-tides%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)
51
+ ![Python Version from PEP 621 TOML](https://img.shields.io/pypi/pyversions/eo-tides)
50
52
  [![codecov](https://codecov.io/gh/GeoscienceAustralia/eo-tides/branch/main/graph/badge.svg)](https://codecov.io/gh/GeoscienceAustralia/eo-tides)
51
53
  [![License](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)
52
54
 
@@ -69,9 +71,9 @@ These tools can be applied to petabytes of freely available satellite data (e.g.
69
71
  - 🌊 Model tides from multiple global ocean tide models in parallel, and return tide heights in standardised `pandas.DataFrame` format for further analysis
70
72
  - 🛰️ "Tag" satellite data with tide height and stage based on the exact moment of image acquisition
71
73
  - 🌐 Model tides for every individual satellite pixel, producing three-dimensional "tide height" `xarray`-format datacubes that can be integrated with satellite data
72
- <!-- - 🎯 Combine multiple tide models into a single locally-optimised "ensemble" model informed by satellite altimetry and satellite-observed patterns of tidal inundation -->
73
74
  - 📈 Calculate statistics describing local tide dynamics, as well as biases caused by interactions between tidal processes and satellite orbits
74
75
  - 🛠️ Validate modelled tides using measured sea levels from coastal tide gauges (e.g. [GESLA Global Extreme Sea Level Analysis](https://gesla.org/))
76
+ <!-- - 🎯 Combine multiple tide models into a single locally-optimised "ensemble" model informed by satellite altimetry and satellite-observed patterns of tidal inundation -->
75
77
 
76
78
  ## Supported tide models
77
79
 
@@ -85,6 +87,10 @@ These tools can be applied to petabytes of freely available satellite data (e.g.
85
87
 
86
88
  For instructions on how to set up these models for use in `eo-tides`, refer to [Setting up tide models](setup.md).
87
89
 
90
+ ## Installing and setting up `eo-tides`
91
+
92
+ To get started with `eo-tides`, follow the [Installation](https://geoscienceaustralia.github.io/eo-tides/install/) and [Setting up tide models](https://geoscienceaustralia.github.io/eo-tides/setup/) guides.
93
+
88
94
  ## Citing `eo-tides`
89
95
 
90
96
  To cite `eo-tides` in your work, please use the following citation:
@@ -92,3 +98,8 @@ To cite `eo-tides` in your work, please use the following citation:
92
98
  ```
93
99
  Bishop-Taylor, R., Sagar, S., Phillips, C., & Newey, V. (2024). eo-tides: Tide modelling tools for large-scale satellite earth observation analysis. https://github.com/GeoscienceAustralia/eo-tides
94
100
  ```
101
+
102
+ ## Acknowledgements
103
+
104
+ For a full list of acknowledgements, refer to [Citations and Credits](https://geoscienceaustralia.github.io/eo-tides/credits/).
105
+ This repository was initialised using the [`cookiecutter-uv`](https://github.com/fpgmaas/cookiecutter-uv) package.
@@ -0,0 +1,11 @@
1
+ eo_tides/__init__.py,sha256=TWmQNplePCcNAlua5WI_H7SShkWNk-Gd3X70EDEknSo,1557
2
+ eo_tides/eo.py,sha256=OTFjchQWpSfgi2Q62mFBj6ckmy_1B_ExCAmex4UT4js,21616
3
+ eo_tides/model.py,sha256=XoJoIa6SmcAR8v1IDisqV8nW44XfY4YU4HWCoTgokx0,34390
4
+ eo_tides/stats.py,sha256=9nfa7_obkS4tiHrQ1WTutuy2jlHtFg-cmOqP3l_Q7b8,20644
5
+ eo_tides/utils.py,sha256=l9VXJawQzaRBYaFMsP8VBeaN5VA3rFDdzcvF7Rk04Vc,5620
6
+ eo_tides/validation.py,sha256=JjTUqDfbR189m_6W1bpaSolQIHNTLicTHN7z9O_nr3s,11828
7
+ eo_tides-0.0.23.dist-info/LICENSE,sha256=owxWsXViCL2J6Ks3XYhot7t4Y93nstmXAT95Zf030Cc,11350
8
+ eo_tides-0.0.23.dist-info/METADATA,sha256=P01mYqtSyfTHIVAduGhTPyRbQe-ZNuCJXhYqKa4sq_s,6902
9
+ eo_tides-0.0.23.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
10
+ eo_tides-0.0.23.dist-info/top_level.txt,sha256=lXZDUUM1DlLdKWHRn8zdmtW8Rx-eQOIWVvt0b8VGiyQ,9
11
+ eo_tides-0.0.23.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- eo_tides/__init__.py,sha256=TWmQNplePCcNAlua5WI_H7SShkWNk-Gd3X70EDEknSo,1557
2
- eo_tides/eo.py,sha256=K2Ubxvp5yYxVwAOaUZDHCIV_AppRuqMiQRtArhe7YOM,22480
3
- eo_tides/model.py,sha256=WC-3gBH-ToREl1RS6CxYJLfXlpy03vtwZKd_464QYcU,32958
4
- eo_tides/stats.py,sha256=gpkewfgM7ixOr_XXepkMHpezmvjlt5-Qo2xbUmzyKhs,10898
5
- eo_tides/utils.py,sha256=l9VXJawQzaRBYaFMsP8VBeaN5VA3rFDdzcvF7Rk04Vc,5620
6
- eo_tides/validation.py,sha256=yFuIjAxS9qf097_n4DHWG4AOa6n4nt1HGibUkOkJW6o,11437
7
- eo_tides-0.0.21.dist-info/LICENSE,sha256=owxWsXViCL2J6Ks3XYhot7t4Y93nstmXAT95Zf030Cc,11350
8
- eo_tides-0.0.21.dist-info/METADATA,sha256=t60AoRbSg9K6GEXlk3TCd9nrEQbaFrHp7rgAmwAda_g,6298
9
- eo_tides-0.0.21.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
10
- eo_tides-0.0.21.dist-info/top_level.txt,sha256=lXZDUUM1DlLdKWHRn8zdmtW8Rx-eQOIWVvt0b8VGiyQ,9
11
- eo_tides-0.0.21.dist-info/RECORD,,