eo-tides 0.2.0__py3-none-any.whl → 0.3.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 CHANGED
@@ -28,9 +28,9 @@ validation : Load observed tide gauge data to validate modelled tides
28
28
 
29
29
  # Import commonly used functions for convenience
30
30
  from .eo import pixel_tides, tag_tides
31
- from .model import list_models, model_phases, model_tides
31
+ from .model import model_phases, model_tides
32
32
  from .stats import pixel_stats, tide_stats
33
- from .utils import idw
33
+ from .utils import clip_models, idw, list_models
34
34
  from .validation import eval_metrics, load_gauge_gesla
35
35
 
36
36
  # Define what should be imported with "from eo_tides import *"
@@ -42,7 +42,8 @@ __all__ = [
42
42
  "pixel_tides",
43
43
  "tide_stats",
44
44
  "pixel_stats",
45
- "idw",
46
45
  "eval_metrics",
47
46
  "load_gauge_gesla",
47
+ "clip_models",
48
+ "idw",
48
49
  ]
eo_tides/eo.py CHANGED
@@ -8,17 +8,15 @@ from typing import TYPE_CHECKING
8
8
 
9
9
  import numpy as np
10
10
  import odc.geo.xr
11
- import pandas as pd
12
11
  import xarray as xr
13
12
  from odc.geo.geobox import GeoBox
14
13
 
15
14
  # Only import if running type checking
16
15
  if TYPE_CHECKING:
17
- import datetime
18
-
19
16
  from odc.geo import Shape2d
20
17
 
21
- from .model import DatetimeLike, _standardise_time, model_tides
18
+ from .model import model_tides
19
+ from .utils import DatetimeLike, _standardise_time
22
20
 
23
21
 
24
22
  def _resample_chunks(
eo_tides/model.py CHANGED
@@ -1,15 +1,14 @@
1
1
  # Used to postpone evaluation of type annotations
2
2
  from __future__ import annotations
3
3
 
4
- import datetime
5
4
  import os
6
- import pathlib
7
5
  import textwrap
8
- import warnings
9
6
  from concurrent.futures import ProcessPoolExecutor
10
7
  from concurrent.futures.process import BrokenProcessPool
11
8
  from functools import partial
12
- from typing import TYPE_CHECKING, List, Union
9
+ from typing import TYPE_CHECKING
10
+
11
+ import psutil
13
12
 
14
13
  # Only import if running type checking
15
14
  if TYPE_CHECKING:
@@ -20,309 +19,9 @@ import numpy as np
20
19
  import pandas as pd
21
20
  import pyproj
22
21
  import pyTMD
23
- from colorama import Style, init
24
- from pyTMD.io.model import load_database, model
25
22
  from tqdm import tqdm
26
23
 
27
- from .utils import idw
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]]
31
-
32
-
33
- def _set_directory(
34
- directory: str | os.PathLike | None = None,
35
- ) -> os.PathLike:
36
- """
37
- Set tide modelling files directory. If no custom
38
- path is provided, try global environmental variable
39
- instead.
40
- """
41
- if directory is None:
42
- if "EO_TIDES_TIDE_MODELS" in os.environ:
43
- directory = os.environ["EO_TIDES_TIDE_MODELS"]
44
- else:
45
- raise Exception(
46
- "No tide model directory provided via `directory`, and/or no "
47
- "`EO_TIDES_TIDE_MODELS` environment variable found. "
48
- "Please provide a valid path to your tide model directory."
49
- )
50
-
51
- # Verify path exists
52
- directory = pathlib.Path(directory).expanduser()
53
- if not directory.exists():
54
- raise FileNotFoundError(f"No valid tide model directory found at path `{directory}`")
55
- else:
56
- return directory
57
-
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
-
78
- def list_models(
79
- directory: str | os.PathLike | None = None,
80
- show_available: bool = True,
81
- show_supported: bool = True,
82
- raise_error: bool = False,
83
- ) -> tuple[list[str], list[str]]:
84
- """
85
- List all tide models available for tide modelling.
86
-
87
- This function scans the specified tide model directory
88
- and returns a list of models that are available in the
89
- directory as well as the full list of all models supported
90
- by `eo-tides` and `pyTMD`.
91
-
92
- For instructions on setting up tide models, see:
93
- <https://geoscienceaustralia.github.io/eo-tides/setup/>
94
-
95
- Parameters
96
- ----------
97
- directory : str, optional
98
- The directory containing tide model data files. If no path is
99
- provided, this will default to the environment variable
100
- `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
101
- Tide modelling files should be stored in sub-folders for each
102
- model that match the structure required by `pyTMD`
103
- (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
104
- show_available : bool, optional
105
- Whether to print a list of locally available models.
106
- show_supported : bool, optional
107
- Whether to print a list of all supported models, in
108
- addition to models available locally.
109
- raise_error : bool, optional
110
- If True, raise an error if no available models are found.
111
- If False, raise a warning.
112
-
113
- Returns
114
- -------
115
- available_models : list of str
116
- A list of all tide models available within `directory`.
117
- supported_models : list of str
118
- A list of all tide models supported by `eo-tides`.
119
- """
120
- init() # Initialize colorama
121
-
122
- # Set tide modelling files directory. If no custom path is
123
- # provided, try global environment variable.
124
- directory = _set_directory(directory)
125
-
126
- # Get full list of supported models from pyTMD database
127
- model_database = load_database()["elevation"]
128
- supported_models = list(model_database.keys())
129
-
130
- # Extract expected model paths
131
- expected_paths = {}
132
- for m in supported_models:
133
- model_file = model_database[m]["model_file"]
134
- model_file = model_file[0] if isinstance(model_file, list) else model_file
135
- expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)
136
-
137
- # Define column widths
138
- status_width = 4 # Width for emoji
139
- name_width = max(len(name) for name in supported_models)
140
- path_width = max(len(path) for path in expected_paths.values())
141
-
142
- # Print list of supported models, marking available and
143
- # unavailable models and appending available to list
144
- if show_available or show_supported:
145
- total_width = min(status_width + name_width + path_width + 6, 80)
146
- print("─" * total_width)
147
- print(f"{'󠀠🌊':^{status_width}} | {'Model':<{name_width}} | {'Expected path':<{path_width}}")
148
- print("─" * total_width)
149
-
150
- available_models = []
151
- for m in supported_models:
152
- try:
153
- model_file = model(directory=directory).elevation(m=m)
154
- available_models.append(m)
155
-
156
- if show_available:
157
- # Mark available models with a green tick
158
- status = "✅"
159
- print(f"{status:^{status_width}}│ {m:<{name_width}} │ {expected_paths[m]:<{path_width}}")
160
- except FileNotFoundError:
161
- if show_supported:
162
- # Mark unavailable models with a red cross
163
- status = "❌"
164
- print(
165
- f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}"
166
- )
167
-
168
- if show_available or show_supported:
169
- print("─" * total_width)
170
-
171
- # Print summary
172
- print(f"\n{Style.BRIGHT}Summary:{Style.RESET_ALL}")
173
- print(f"Available models: {len(available_models)}/{len(supported_models)}")
174
-
175
- # Raise error or warning if no models are available
176
- if not available_models:
177
- warning_msg = textwrap.dedent(
178
- f"""
179
- No valid tide models are available in `{directory}`.
180
- Are you sure you have provided the correct `directory` path, or set the
181
- `EO_TIDES_TIDE_MODELS` environment variable to point to the location of your
182
- tide model directory?
183
- """
184
- ).strip()
185
-
186
- if raise_error:
187
- raise Exception(warning_msg)
188
- else:
189
- warnings.warn(warning_msg, UserWarning)
190
-
191
- # Return list of available and supported models
192
- return available_models, supported_models
193
-
194
-
195
- def _model_tides(
196
- model,
197
- x,
198
- y,
199
- time,
200
- directory,
201
- crs,
202
- crop,
203
- method,
204
- extrapolate,
205
- cutoff,
206
- output_units,
207
- mode,
208
- ):
209
- """Worker function applied in parallel by `model_tides`. Handles the
210
- extraction of tide modelling constituents and tide modelling using
211
- `pyTMD`.
212
- """
213
- # Obtain model details
214
- pytmd_model = pyTMD.io.model(directory).elevation(model)
215
-
216
- # Reproject x, y to latitude/longitude
217
- transformer = pyproj.Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
218
- lon, lat = transformer.transform(x.flatten(), y.flatten())
219
-
220
- # Convert datetime
221
- timescale = pyTMD.time.timescale().from_datetime(time.flatten())
222
-
223
- try:
224
- # Read tidal constants and interpolate to grid points
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
- )
237
-
238
- # TODO: Return constituents
239
- # print(amp.shape, ph.shape, c)
240
- # print(pd.DataFrame({"amplitude": amp}))
241
-
242
- # Raise error if constituent files no not cover analysis extent
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
251
-
252
- # Calculate complex phase in radians for Euler's
253
- cph = -1j * ph * np.pi / 180.0
254
-
255
- # Calculate constituent oscillation
256
- hc = amp * np.exp(cph)
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
-
266
- # Determine the number of points and times to process. If in
267
- # "one-to-many" mode, these counts are used to repeat our extracted
268
- # constituents and timesteps so we can extract tides for all
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`
272
- # If in "one-to-one" mode, we avoid this step by setting counts to 1
273
- # (e.g. "repeat 1 times")
274
- points_repeat = len(x) if mode == "one-to-many" else 1
275
- time_repeat = len(time) if mode == "one-to-many" else 1
276
- t, hc, deltat = (
277
- np.tile(timescale.tide, points_repeat),
278
- hc.repeat(time_repeat, axis=0),
279
- np.tile(deltat, points_repeat),
280
- )
281
-
282
- # Create arrays to hold outputs
283
- tide = np.ma.zeros((len(t)), fill_value=np.nan)
284
- tide.mask = np.any(hc.mask, axis=1)
285
-
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
- )
294
- minor = pyTMD.predict.infer_minor(
295
- t,
296
- hc,
297
- c,
298
- deltat=deltat,
299
- corrections=pytmd_model.corrections,
300
- minor=pytmd_model.minor,
301
- )
302
- tide.data[:] += minor.data[:]
303
-
304
- # Replace invalid values with fill value
305
- tide.data[tide.mask] = tide.fill_value
306
-
307
- # Convert data to pandas.DataFrame, and set index to our input
308
- # time/x/y values
309
- tide_df = pd.DataFrame({
310
- "time": np.tile(time, points_repeat),
311
- "x": np.repeat(x, time_repeat),
312
- "y": np.repeat(y, time_repeat),
313
- "tide_model": model,
314
- "tide_height": tide,
315
- }).set_index(["time", "x", "y"])
316
-
317
- # Optionally convert outputs to integer units (can save memory)
318
- if output_units == "m":
319
- tide_df["tide_height"] = tide_df.tide_height.astype(np.float32)
320
- elif output_units == "cm":
321
- tide_df["tide_height"] = (tide_df.tide_height * 100).astype(np.int16)
322
- elif output_units == "mm":
323
- tide_df["tide_height"] = (tide_df.tide_height * 1000).astype(np.int16)
324
-
325
- return tide_df
24
+ from .utils import DatetimeLike, _set_directory, _standardise_time, idw, list_models
326
25
 
327
26
 
328
27
  def _ensemble_model(
@@ -490,6 +189,180 @@ def _ensemble_model(
490
189
  return pd.concat(ensemble_list)
491
190
 
492
191
 
192
+ def _parallel_splits(
193
+ total_points: int,
194
+ model_count: int,
195
+ parallel_max: int | None = None,
196
+ min_points_per_split: int = 1000,
197
+ ) -> int:
198
+ """
199
+ Calculates the optimal number of parallel splits for data
200
+ processing based on system resources and processing constraints.
201
+
202
+ Parameters:
203
+ -----------
204
+ total_points : int
205
+ Total number of data points to process
206
+ model_count : int
207
+ Number of models that will be run in parallel
208
+ parallel_max : int, optional
209
+ Maximum number of parallel processes to use. If None, uses CPU core count
210
+ min_points_per_split : int, default=1000
211
+ Minimum number of points that should be processed in each split
212
+ """
213
+ # Get available CPUs. First see if `CPU_GUARANTEE` exists in
214
+ # environment (if running in JupyterHub); if not use psutil
215
+ # followed by standard CPU count
216
+ if parallel_max is None:
217
+ # Take the first valid output
218
+ raw_value = os.environ.get("CPU_GUARANTEE") or psutil.cpu_count(logical=False) or os.cpu_count() or 1
219
+
220
+ # Convert to integer
221
+ if isinstance(raw_value, str):
222
+ parallel_max = int(float(raw_value))
223
+ else:
224
+ parallel_max = int(raw_value)
225
+
226
+ # Calculate optimal number of splits based on constraints
227
+ splits_by_size = total_points / min_points_per_split
228
+ splits_by_cpu = parallel_max / model_count
229
+ optimal_splits = min(splits_by_size, splits_by_cpu)
230
+
231
+ # Convert to integer and ensure at least 1 split
232
+ final_split_count = int(max(1, optimal_splits))
233
+ return final_split_count
234
+
235
+
236
+ def _model_tides(
237
+ model,
238
+ x,
239
+ y,
240
+ time,
241
+ directory,
242
+ crs,
243
+ crop,
244
+ method,
245
+ extrapolate,
246
+ cutoff,
247
+ output_units,
248
+ mode,
249
+ ):
250
+ """Worker function applied in parallel by `model_tides`. Handles the
251
+ extraction of tide modelling constituents and tide modelling using
252
+ `pyTMD`.
253
+ """
254
+ # Obtain model details
255
+ pytmd_model = pyTMD.io.model(directory).elevation(model)
256
+
257
+ # Reproject x, y to latitude/longitude
258
+ transformer = pyproj.Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
259
+ lon, lat = transformer.transform(x.flatten(), y.flatten())
260
+
261
+ # Convert datetime
262
+ timescale = pyTMD.time.timescale().from_datetime(time.flatten())
263
+
264
+ try:
265
+ # Read tidal constants and interpolate to grid points
266
+ amp, ph, c = pytmd_model.extract_constants(
267
+ lon,
268
+ lat,
269
+ type=pytmd_model.type,
270
+ crop=crop,
271
+ method=method,
272
+ extrapolate=extrapolate,
273
+ cutoff=cutoff,
274
+ append_node=False,
275
+ # append_node=True,
276
+ )
277
+
278
+ # TODO: Return constituents
279
+ # print(amp.shape, ph.shape, c)
280
+ # print(pd.DataFrame({"amplitude": amp}))
281
+
282
+ # Raise error if constituent files no not cover analysis extent
283
+ except IndexError:
284
+ error_msg = f"""
285
+ The {model} tide model constituent files do not cover the analysis extent
286
+ ({min(lon):.2f}, {max(lon):.2f}, {min(lat):.2f}, {max(lat):.2f}).
287
+ This can occur if you are using clipped model files to improve run times.
288
+ Consider using model files that cover your entire analysis area, or set `crop=False`
289
+ to reduce the extent of tide model constituent files that is loaded.
290
+ """
291
+ raise Exception(textwrap.dedent(error_msg).strip()) from None
292
+
293
+ # Calculate complex phase in radians for Euler's
294
+ cph = -1j * ph * np.pi / 180.0
295
+
296
+ # Calculate constituent oscillation
297
+ hc = amp * np.exp(cph)
298
+
299
+ # Compute delta times based on model
300
+ if pytmd_model.corrections in ("OTIS", "ATLAS", "TMD3", "netcdf"):
301
+ # Use delta time at 2000.0 to match TMD outputs
302
+ deltat = np.zeros_like(timescale.tt_ut1)
303
+ else:
304
+ # Use interpolated delta times
305
+ deltat = timescale.tt_ut1
306
+
307
+ # In "one-to-many" mode, extracted tidal constituents and timesteps
308
+ # are repeated/multiplied out to match the number of input points and
309
+ # timesteps, enabling the modeling of tides across all combinations
310
+ # of input times and points. In "one-to-one" mode, no repetition is
311
+ # needed, so each repeat count is set to 1.
312
+ points_repeat = len(x) if mode == "one-to-many" else 1
313
+ time_repeat = len(time) if mode == "one-to-many" else 1
314
+ t, hc, deltat = (
315
+ np.tile(timescale.tide, points_repeat),
316
+ hc.repeat(time_repeat, axis=0),
317
+ np.tile(deltat, points_repeat),
318
+ )
319
+
320
+ # Create arrays to hold outputs
321
+ tide = np.ma.zeros((len(t)), fill_value=np.nan)
322
+ tide.mask = np.any(hc.mask, axis=1)
323
+
324
+ # Predict tidal elevations at time and infer minor corrections
325
+ tide.data[:] = pyTMD.predict.drift(
326
+ t,
327
+ hc,
328
+ c,
329
+ deltat=deltat,
330
+ corrections=pytmd_model.corrections,
331
+ )
332
+ minor = pyTMD.predict.infer_minor(
333
+ t,
334
+ hc,
335
+ c,
336
+ deltat=deltat,
337
+ corrections=pytmd_model.corrections,
338
+ minor=pytmd_model.minor,
339
+ )
340
+ tide.data[:] += minor.data[:]
341
+
342
+ # Replace invalid values with fill value
343
+ tide.data[tide.mask] = tide.fill_value
344
+
345
+ # Convert data to pandas.DataFrame, and set index to our input
346
+ # time/x/y values
347
+ tide_df = pd.DataFrame({
348
+ "time": np.tile(time, points_repeat),
349
+ "x": np.repeat(x, time_repeat),
350
+ "y": np.repeat(y, time_repeat),
351
+ "tide_model": model,
352
+ "tide_height": tide,
353
+ }).set_index(["time", "x", "y"])
354
+
355
+ # Optionally convert outputs to integer units (can save memory)
356
+ if output_units == "m":
357
+ tide_df["tide_height"] = tide_df.tide_height.astype(np.float32)
358
+ elif output_units == "cm":
359
+ tide_df["tide_height"] = (tide_df.tide_height * 100).astype(np.int16)
360
+ elif output_units == "mm":
361
+ tide_df["tide_height"] = (tide_df.tide_height * 1000).astype(np.int16)
362
+
363
+ return tide_df
364
+
365
+
493
366
  def model_tides(
494
367
  x: float | list[float] | xr.DataArray,
495
368
  y: float | list[float] | xr.DataArray,
@@ -498,12 +371,13 @@ def model_tides(
498
371
  directory: str | os.PathLike | None = None,
499
372
  crs: str = "EPSG:4326",
500
373
  crop: bool = True,
501
- method: str = "spline",
374
+ method: str = "linear",
502
375
  extrapolate: bool = True,
503
376
  cutoff: float | None = None,
504
377
  mode: str = "one-to-many",
505
378
  parallel: bool = True,
506
- parallel_splits: int = 5,
379
+ parallel_splits: int | str = "auto",
380
+ parallel_max: int | None = None,
507
381
  output_units: str = "m",
508
382
  output_format: str = "long",
509
383
  ensemble_models: list[str] | None = None,
@@ -564,11 +438,11 @@ def model_tides(
564
438
  1 degree buffer around all input points. Defaults to True.
565
439
  method : str, optional
566
440
  Method used to interpolate tidal constituents
567
- from model files. Options include:
441
+ from model files. Defaults to "linear"; options include:
568
442
 
569
- - "spline": scipy bivariate spline interpolation (default)
570
- - "bilinear": quick bilinear interpolation
571
443
  - "linear", "nearest": scipy regular grid interpolations
444
+ - "spline": scipy bivariate spline interpolation
445
+ - "bilinear": quick bilinear interpolation
572
446
  extrapolate : bool, optional
573
447
  Whether to extrapolate tides for x and y coordinates outside of
574
448
  the valid tide modelling domain using nearest-neighbor.
@@ -594,12 +468,16 @@ def model_tides(
594
468
  parallel. Optionally, tide modelling can also be run in parallel
595
469
  across input x and y coordinates (see "parallel_splits" below).
596
470
  Default is True.
597
- parallel_splits : int, optional
471
+ parallel_splits : str or int, optional
598
472
  Whether to split the input x and y coordinates into smaller,
599
473
  evenly-sized chunks that are processed in parallel. This can
600
474
  provide a large performance boost when processing large numbers
601
- of coordinates. The default is 5 chunks, which will split
602
- coordinates into 5 parallelised chunks.
475
+ of coordinates. The default is "auto", which will automatically
476
+ attempt to determine optimal splits based on available CPUs,
477
+ the number of input points, and the number of models.
478
+ parallel_max : int, optional
479
+ Maximum number of processes to run in parallel. The default of
480
+ None will automatically determine this from your available CPUs.
603
481
  output_units : str, optional
604
482
  Whether to return modelled tides in floating point metre units,
605
483
  or integer centimetre units (i.e. scaled by 100) or integer
@@ -729,13 +607,28 @@ def model_tides(
729
607
  mode=mode,
730
608
  )
731
609
 
732
- # Ensure requested parallel splits is not smaller than number of points
733
- parallel_splits = min(parallel_splits, len(x))
610
+ # If automatic parallel splits, calculate optimal value
611
+ # based on available parallelisation, number of points
612
+ # and number of models
613
+ if parallel_splits == "auto":
614
+ parallel_splits = _parallel_splits(
615
+ total_points=len(x),
616
+ model_count=len(models_to_process),
617
+ parallel_max=parallel_max,
618
+ )
619
+
620
+ # Verify that parallel splits are not larger than number of points
621
+ assert isinstance(parallel_splits, int)
622
+ if parallel_splits > len(x):
623
+ raise ValueError(f"Parallel splits ({parallel_splits}) cannot be larger than the number of points ({len(x)}).")
734
624
 
735
625
  # Parallelise if either multiple models or multiple splits requested
626
+
736
627
  if parallel & ((len(models_to_process) > 1) | (parallel_splits > 1)):
737
- with ProcessPoolExecutor() as executor:
738
- print(f"Modelling tides using {', '.join(models_to_process)} in parallel")
628
+ with ProcessPoolExecutor(max_workers=parallel_max) as executor:
629
+ print(
630
+ f"Modelling tides with {', '.join(models_to_process)} in parallel (models: {len(models_to_process)}, splits: {parallel_splits})"
631
+ )
739
632
 
740
633
  # Optionally split lon/lat points into `splits_n` chunks
741
634
  # that will be applied in parallel
@@ -783,7 +676,7 @@ def model_tides(
783
676
  model_outputs = []
784
677
 
785
678
  for model_i in models_to_process:
786
- print(f"Modelling tides using {model_i}")
679
+ print(f"Modelling tides with {model_i}")
787
680
  tide_df = iter_func(model_i, x, y, time)
788
681
  model_outputs.append(tide_df)
789
682
 
eo_tides/stats.py CHANGED
@@ -6,20 +6,18 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  import matplotlib.pyplot as plt
8
8
  import numpy as np
9
- import odc.geo.xr
10
9
  import pandas as pd
11
10
  import xarray as xr
12
11
  from scipy import stats
13
12
 
14
13
  # Only import if running type checking
15
14
  if TYPE_CHECKING:
16
- import datetime
17
-
18
15
  import xarray as xr
19
16
  from odc.geo.geobox import GeoBox
20
17
 
21
18
  from .eo import _standardise_inputs, pixel_tides, tag_tides
22
- from .model import DatetimeLike, model_tides
19
+ from .model import model_tides
20
+ from .utils import DatetimeLike
23
21
 
24
22
 
25
23
  def _plot_biases(
eo_tides/utils.py CHANGED
@@ -1,5 +1,457 @@
1
+ # Used to postpone evaluation of type annotations
2
+ from __future__ import annotations
3
+
4
+ import datetime
5
+ import os
6
+ import pathlib
7
+ import textwrap
8
+ import warnings
9
+ from typing import List, Union
10
+
1
11
  import numpy as np
12
+ import odc.geo
13
+ import pandas as pd
14
+ import xarray as xr
15
+ from colorama import Style, init
16
+ from odc.geo.geom import BoundingBox
17
+ from pyTMD.io.model import load_database
18
+ from pyTMD.io.model import model as pytmd_model
2
19
  from scipy.spatial import cKDTree as KDTree
20
+ from tqdm import tqdm
21
+
22
+ # Type alias for all possible inputs to "time" params
23
+ DatetimeLike = Union[np.ndarray, pd.DatetimeIndex, pd.Timestamp, datetime.datetime, str, List[str]]
24
+
25
+
26
+ def _set_directory(
27
+ directory: str | os.PathLike | None = None,
28
+ ) -> os.PathLike:
29
+ """
30
+ Set tide modelling files directory. If no custom
31
+ path is provided, try global `EO_TIDES_TIDE_MODELS`
32
+ environmental variable instead.
33
+ """
34
+ if directory is None:
35
+ if "EO_TIDES_TIDE_MODELS" in os.environ:
36
+ directory = os.environ["EO_TIDES_TIDE_MODELS"]
37
+ else:
38
+ raise Exception(
39
+ "No tide model directory provided via `directory`, and/or no "
40
+ "`EO_TIDES_TIDE_MODELS` environment variable found. "
41
+ "Please provide a valid path to your tide model directory."
42
+ )
43
+
44
+ # Verify path exists
45
+ directory = pathlib.Path(directory).expanduser()
46
+ if not directory.exists():
47
+ raise FileNotFoundError(f"No valid tide model directory found at path `{directory}`")
48
+ else:
49
+ return directory
50
+
51
+
52
+ def _standardise_time(
53
+ time: DatetimeLike | None,
54
+ ) -> np.ndarray | None:
55
+ """
56
+ Accept any time format accepted by `pd.to_datetime`,
57
+ and return a datetime64 ndarray. Return None if None
58
+ passed.
59
+ """
60
+ # Return time as-is if None
61
+ if time is None:
62
+ return None
63
+
64
+ # Use pd.to_datetime for conversion, then convert to numpy array
65
+ time = pd.to_datetime(time).to_numpy().astype("datetime64[ns]")
66
+
67
+ # Ensure that data has at least one dimension
68
+ return np.atleast_1d(time)
69
+
70
+
71
+ def _clip_model_file(
72
+ nc: xr.Dataset,
73
+ bbox: BoundingBox,
74
+ ydim: str,
75
+ xdim: str,
76
+ ycoord: str,
77
+ xcoord: str,
78
+ ) -> xr.Dataset:
79
+ """
80
+ Clips tide model netCDF datasets to a bounding box.
81
+
82
+ If the bounding box crosses 0 degrees longitude (e.g. Greenwich),
83
+ the function will clip the dataset into two parts and concatenate
84
+ them along the x-dimension to create a continuous result.
85
+
86
+ Parameters
87
+ ----------
88
+ nc : xr.Dataset
89
+ Input tide model xarray dataset.
90
+ bbox : odc.geo.geom.BoundingBox
91
+ A BoundingBox object for clipping the dataset in EPSG:4326
92
+ degrees coordinates. For example:
93
+ `BoundingBox(left=108, bottom=-48, right=158, top=-6, crs='EPSG:4326')`
94
+ ydim : str
95
+ The name of the xarray dimension representing the y-axis.
96
+ Depending on the tide model, this may or may not contain
97
+ actual latitude values.
98
+ xdim : str
99
+ The name of the xarray dimension representing the x-axis.
100
+ Depending on the tide model, this may or may not contain
101
+ actual longitude values.
102
+ ycoord : str
103
+ The name of the coordinate, variable or dimension containing
104
+ actual latitude values used for clipping the data.
105
+ xcoord : str
106
+ The name of the coordinate, variable or dimension containing
107
+ actual longitude values used for clipping the data.
108
+
109
+ Returns
110
+ -------
111
+ xr.Dataset
112
+ A dataset clipped to the specified bounding box, with
113
+ appropriate adjustments if the bounding box crosses 0
114
+ degrees longitude.
115
+
116
+ Examples
117
+ --------
118
+ >>> nc = xr.open_dataset("GOT5.5/ocean_tides/2n2.nc")
119
+ >>> bbox = BoundingBox(left=108, bottom=-48, right=158, top=-6, crs='EPSG:4326')
120
+ >>> clipped_nc = _clip_model_file(nc, bbox, xdim="lon", ydim="lat", ycoord="latitude", xcoord="longitude")
121
+ """
122
+
123
+ # Extract x and y coords from xarray and load into memory
124
+ xcoords = nc[xcoord].compute()
125
+ ycoords = nc[ycoord].compute()
126
+
127
+ # If data falls within 0-360 degree bounds, then clip directly
128
+ if (bbox.left >= 0) & (bbox.right <= 360):
129
+ nc_clipped = nc.sel({
130
+ ydim: (ycoords >= bbox.bottom) & (ycoords <= bbox.top),
131
+ xdim: (xcoords >= bbox.left) & (xcoords <= bbox.right),
132
+ })
133
+
134
+ # If bbox crosses zero longitude, extract left and right
135
+ # separately and then combine into one concatenated dataset
136
+ elif (bbox.left < 0) & (bbox.right > 0):
137
+ # Convert longitudes to 0-360 range
138
+ left = bbox.left % 360
139
+ right = bbox.right % 360
140
+
141
+ # Extract data from left of 0 longitude, and convert lon
142
+ # coords to -180 to 0 range to enable continuous interpolation
143
+ # across 0 boundary
144
+ nc_left = nc.sel({
145
+ ydim: (ycoords >= bbox.bottom) & (ycoords <= bbox.top),
146
+ xdim: (xcoords >= left) & (xcoords <= 360),
147
+ }).assign({xcoord: lambda x: x[xcoord] - 360})
148
+
149
+ # Convert additional lon variables for TXPO
150
+ if "lon_v" in nc_left:
151
+ nc_left = nc_left.assign({
152
+ "lon_v": lambda x: x["lon_v"] - 360,
153
+ "lon_u": lambda x: x["lon_u"] - 360,
154
+ })
155
+
156
+ # Extract data to right of 0 longitude
157
+ nc_right = nc.sel({
158
+ ydim: (ycoords >= bbox.bottom) & (ycoords <= bbox.top),
159
+ xdim: (xcoords > 0) & (xcoords <= right),
160
+ })
161
+
162
+ # Combine left and right data along x dimension
163
+ nc_clipped = xr.concat([nc_left, nc_right], dim=xdim)
164
+
165
+ # Hack fix to remove expanded x dim on lat variables issue
166
+ # for TPXO data; remove x dim by selecting the first obs
167
+ for i in ["lat_z", "lat_v", "lat_u", "con"]:
168
+ try:
169
+ nc_clipped[i] = nc_clipped[i].isel(nx=0)
170
+ except:
171
+ pass
172
+
173
+ return nc_clipped
174
+
175
+
176
+ def clip_models(
177
+ input_directory: str | os.PathLike,
178
+ output_directory: str | os.PathLike,
179
+ bbox: tuple[float, float, float, float],
180
+ model: list | None = None,
181
+ buffer: float = 1,
182
+ overwrite: bool = False,
183
+ ):
184
+ """
185
+ Clip NetCDF-format ocean tide models to a bounding box.
186
+
187
+ This function identifies all NetCDF-format tide models in a
188
+ given input directory, including "ATLAS-netcdf" (e.g. TPXO9-atlas-nc),
189
+ "FES-netcdf" (e.g. FES2022, EOT20), and "GOT-netcdf" (e.g. GOT5.5)
190
+ format files. Files for each model are then clipped to the extent of
191
+ the provided bounding box, handling model-specific file structures.
192
+ After each model is clipped, the result is exported to the output
193
+ directory and verified with `pyTMD` to ensure the clipped data is
194
+ suitable for tide modelling.
195
+
196
+ For instructions on accessing and downloading tide models, see:
197
+ <https://geoscienceaustralia.github.io/eo-tides/setup/>
198
+
199
+ Parameters
200
+ ----------
201
+ input_directory : str or os.PathLike
202
+ Path to directory containing input NetCDF-format tide model files.
203
+ output_directory : str or os.PathLike
204
+ Path to directory where clipped NetCDF files will be exported.
205
+ bbox : tuple of float
206
+ Bounding box for clipping the tide models in EPSG:4326 degrees
207
+ coordinates, specified as `(left, bottom, right, top)`.
208
+ model : str or list of str, optional
209
+ The tide model (or models) to clip. Defaults to None, which
210
+ will automatically identify and clip all NetCDF-format models
211
+ in the input directly.
212
+ buffer : float, optional
213
+ Buffer distance (in degrees) added to the bounding box to provide
214
+ sufficient data on edges of study area. Defaults to 1 degree.
215
+ overwrite : bool, optional
216
+ If True, overwrite existing files in the output directory.
217
+ Defaults to False.
218
+
219
+ Examples
220
+ --------
221
+ >>> clip_models(
222
+ ... input_directory="tide_models/",
223
+ ... output_directory="tide_models_clipped/",
224
+ ... bbox=(-8.968392, 50.070574, 2.447160, 59.367122),
225
+ ... )
226
+ """
227
+
228
+ # Get input and output paths
229
+ input_directory = _set_directory(input_directory)
230
+ output_directory = pathlib.Path(output_directory)
231
+
232
+ # Prepare bounding box
233
+ bbox = odc.geo.geom.BoundingBox(*bbox, crs="EPSG:4326").buffered(buffer)
234
+
235
+ # Identify NetCDF models
236
+ model_database = load_database()["elevation"]
237
+ netcdf_formats = ["ATLAS-netcdf", "FES-netcdf", "GOT-netcdf"]
238
+ netcdf_models = {k for k, v in model_database.items() if v["format"] in netcdf_formats}
239
+
240
+ # Identify subset of available and requested NetCDF models
241
+ available_models, _ = list_models(directory=input_directory, show_available=False, show_supported=False)
242
+ requested_models = list(np.atleast_1d(model)) if model is not None else available_models
243
+ available_netcdf_models = list(set(available_models) & set(requested_models) & set(netcdf_models))
244
+
245
+ # Raise error if no valid models found
246
+ if len(available_netcdf_models) == 0:
247
+ raise ValueError(f"No valid NetCDF models found in {input_directory}.")
248
+
249
+ # If model list is provided,
250
+ print(f"Preparing to clip suitable NetCDF models: {available_netcdf_models}\n")
251
+
252
+ # Loop through suitable models and export
253
+ for m in available_netcdf_models:
254
+ # Get model file and grid file list if they exist
255
+ model_files = model_database[m].get("model_file", [])
256
+ grid_file = model_database[m].get("grid_file", [])
257
+
258
+ # Convert to list if strings and combine
259
+ model_files = model_files if isinstance(model_files, list) else [model_files]
260
+ grid_file = grid_file if isinstance(grid_file, list) else [grid_file]
261
+ all_files = model_files + grid_file
262
+
263
+ # Loop through each model file and clip
264
+ for file in tqdm(all_files, desc=f"Clipping {m}"):
265
+ # Skip if it exists in output directory
266
+ if (output_directory / file).exists() and not overwrite:
267
+ continue
268
+
269
+ # Load model file
270
+ nc = xr.open_mfdataset(input_directory / file)
271
+
272
+ # Open file and clip according to model
273
+ if m in (
274
+ "GOT5.5",
275
+ "GOT5.5_load",
276
+ "GOT5.5_extrapolated",
277
+ "GOT5.5D",
278
+ "GOT5.5D_extrapolated",
279
+ "GOT5.6",
280
+ "GOT5.6_extrapolated",
281
+ ):
282
+ nc_clipped = _clip_model_file(
283
+ nc,
284
+ bbox,
285
+ xdim="lon",
286
+ ydim="lat",
287
+ ycoord="latitude",
288
+ xcoord="longitude",
289
+ )
290
+
291
+ elif m in ("HAMTIDE11",):
292
+ nc_clipped = _clip_model_file(nc, bbox, xdim="LON", ydim="LAT", ycoord="LAT", xcoord="LON")
293
+
294
+ elif m in (
295
+ "EOT20",
296
+ "EOT20_load",
297
+ "FES2012",
298
+ "FES2014",
299
+ "FES2014_extrapolated",
300
+ "FES2014_load",
301
+ "FES2022",
302
+ "FES2022_extrapolated",
303
+ "FES2022_load",
304
+ ):
305
+ nc_clipped = _clip_model_file(nc, bbox, xdim="lon", ydim="lat", ycoord="lat", xcoord="lon")
306
+
307
+ elif m in (
308
+ "TPXO8-atlas-nc",
309
+ "TPXO9-atlas-nc",
310
+ "TPXO9-atlas-v2-nc",
311
+ "TPXO9-atlas-v3-nc",
312
+ "TPXO9-atlas-v4-nc",
313
+ "TPXO9-atlas-v5-nc",
314
+ "TPXO10-atlas-v2-nc",
315
+ ):
316
+ nc_clipped = _clip_model_file(
317
+ nc,
318
+ bbox,
319
+ xdim="nx",
320
+ ydim="ny",
321
+ ycoord="lat_z",
322
+ xcoord="lon_z",
323
+ )
324
+
325
+ else:
326
+ raise Exception(f"Model {m} not supported")
327
+
328
+ # Create directory and export
329
+ (output_directory / file).parent.mkdir(parents=True, exist_ok=True)
330
+ nc_clipped.to_netcdf(output_directory / file, mode="w")
331
+
332
+ # Verify that models are ready
333
+ pytmd_model(directory=output_directory).elevation(m=m).verify
334
+ print(" ✅ Clipped model exported and verified")
335
+
336
+ print(f"\nOutputs exported to {output_directory}")
337
+ list_models(directory=output_directory, show_available=True, show_supported=False)
338
+
339
+
340
+ def list_models(
341
+ directory: str | os.PathLike | None = None,
342
+ show_available: bool = True,
343
+ show_supported: bool = True,
344
+ raise_error: bool = False,
345
+ ) -> tuple[list[str], list[str]]:
346
+ """
347
+ List all tide models available for tide modelling.
348
+
349
+ This function scans the specified tide model directory
350
+ and returns a list of models that are available in the
351
+ directory as well as the full list of all models supported
352
+ by `eo-tides` and `pyTMD`.
353
+
354
+ For instructions on setting up tide models, see:
355
+ <https://geoscienceaustralia.github.io/eo-tides/setup/>
356
+
357
+ Parameters
358
+ ----------
359
+ directory : str, optional
360
+ The directory containing tide model data files. If no path is
361
+ provided, this will default to the environment variable
362
+ `EO_TIDES_TIDE_MODELS` if set, or raise an error if not.
363
+ Tide modelling files should be stored in sub-folders for each
364
+ model that match the structure required by `pyTMD`
365
+ (<https://geoscienceaustralia.github.io/eo-tides/setup/>).
366
+ show_available : bool, optional
367
+ Whether to print a list of locally available models.
368
+ show_supported : bool, optional
369
+ Whether to print a list of all supported models, in
370
+ addition to models available locally.
371
+ raise_error : bool, optional
372
+ If True, raise an error if no available models are found.
373
+ If False, raise a warning.
374
+
375
+ Returns
376
+ -------
377
+ available_models : list of str
378
+ A list of all tide models available within `directory`.
379
+ supported_models : list of str
380
+ A list of all tide models supported by `eo-tides`.
381
+ """
382
+ init() # Initialize colorama
383
+
384
+ # Set tide modelling files directory. If no custom path is
385
+ # provided, try global environment variable.
386
+ directory = _set_directory(directory)
387
+
388
+ # Get full list of supported models from pyTMD database
389
+ model_database = load_database()["elevation"]
390
+ supported_models = list(model_database.keys())
391
+
392
+ # Extract expected model paths
393
+ expected_paths = {}
394
+ for m in supported_models:
395
+ model_file = model_database[m]["model_file"]
396
+ model_file = model_file[0] if isinstance(model_file, list) else model_file
397
+ expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)
398
+
399
+ # Define column widths
400
+ status_width = 4 # Width for emoji
401
+ name_width = max(len(name) for name in supported_models)
402
+ path_width = max(len(path) for path in expected_paths.values())
403
+
404
+ # Print list of supported models, marking available and
405
+ # unavailable models and appending available to list
406
+ if show_available or show_supported:
407
+ total_width = min(status_width + name_width + path_width + 6, 80)
408
+ print("─" * total_width)
409
+ print(f"{'󠀠🌊':^{status_width}} | {'Model':<{name_width}} | {'Expected path':<{path_width}}")
410
+ print("─" * total_width)
411
+
412
+ available_models = []
413
+ for m in supported_models:
414
+ try:
415
+ model_file = pytmd_model(directory=directory).elevation(m=m)
416
+ available_models.append(m)
417
+
418
+ if show_available:
419
+ # Mark available models with a green tick
420
+ status = "✅"
421
+ print(f"{status:^{status_width}}│ {m:<{name_width}} │ {expected_paths[m]:<{path_width}}")
422
+ except FileNotFoundError:
423
+ if show_supported:
424
+ # Mark unavailable models with a red cross
425
+ status = "❌"
426
+ print(
427
+ f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}"
428
+ )
429
+
430
+ if show_available or show_supported:
431
+ print("─" * total_width)
432
+
433
+ # Print summary
434
+ print(f"\n{Style.BRIGHT}Summary:{Style.RESET_ALL}")
435
+ print(f"Available models: {len(available_models)}/{len(supported_models)}")
436
+
437
+ # Raise error or warning if no models are available
438
+ if not available_models:
439
+ warning_msg = textwrap.dedent(
440
+ f"""
441
+ No valid tide models are available in `{directory}`.
442
+ Are you sure you have provided the correct `directory` path, or set the
443
+ `EO_TIDES_TIDE_MODELS` environment variable to point to the location of your
444
+ tide model directory?
445
+ """
446
+ ).strip()
447
+
448
+ if raise_error:
449
+ raise Exception(warning_msg)
450
+ else:
451
+ warnings.warn(warning_msg, UserWarning)
452
+
453
+ # Return list of available and supported models
454
+ return available_models, supported_models
3
455
 
4
456
 
5
457
  def idw(
@@ -22,7 +474,7 @@ def idw(
22
474
  inverse distance to each neighbor, with weights descreasing with
23
475
  increasing distance.
24
476
 
25
- Code inspired by: https://github.com/DahnJ/REM-xarray
477
+ Code inspired by: <https://github.com/DahnJ/REM-xarray>
26
478
 
27
479
  Parameters
28
480
  ----------
eo_tides/validation.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import datetime
2
- import glob
3
2
  import warnings
4
3
  from math import sqrt
5
4
  from numbers import Number
@@ -193,11 +192,12 @@ def load_gauge_gesla(
193
192
  metadata_path="/gdata1/data/sea_level/GESLA3_ALL 2.csv",
194
193
  ):
195
194
  """
196
- Load and process all available Global Extreme Sea Level Analysis
197
- (GESLA) tide gauge data with an `x, y, time` spatiotemporal query,
198
- or from a list of specific tide gauges.
195
+ Load Global Extreme Sea Level Analysis (GESLA) tide gauge data.
199
196
 
200
- Can optionally filter by gauge quality and append detailed gauge metadata.
197
+ Load and process all available GESLA measured sea-level data
198
+ with an `x, y, time` spatio-temporal query, or from a list of
199
+ specific tide gauges. Can optionally filter by gauge quality
200
+ and append detailed gauge metadata.
201
201
 
202
202
  Modified from original code in <https://github.com/philiprt/GeslaDataset>.
203
203
 
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eo-tides
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Tide modelling tools for large-scale satellite earth observation analysis
5
+ Author: Stephen Sagar, Claire Phillips, Vanessa Newey
5
6
  Author-email: Robbi Bishop-Taylor <Robbi.BishopTaylor@ga.gov.au>
6
7
  Project-URL: Homepage, https://GeoscienceAustralia.github.io/eo-tides/
7
8
  Project-URL: Repository, https://github.com/GeoscienceAustralia/eo-tides
@@ -19,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.9
19
20
  Classifier: Programming Language :: Python :: 3.10
20
21
  Classifier: Programming Language :: Python :: 3.11
21
22
  Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
22
24
  Requires-Python: <4.0,>=3.9
23
25
  Description-Content-Type: text/markdown
24
26
  License-File: LICENSE
@@ -28,8 +30,9 @@ Requires-Dist: matplotlib >=3.8.0
28
30
  Requires-Dist: numpy >=1.26.0
29
31
  Requires-Dist: odc-geo >=0.4.7
30
32
  Requires-Dist: pandas >=2.2.0
33
+ Requires-Dist: psutil >=5.8.0
31
34
  Requires-Dist: pyproj >=3.6.1
32
- Requires-Dist: pyTMD ==2.1.7
35
+ Requires-Dist: pyTMD ==2.1.8
33
36
  Requires-Dist: scikit-learn >=1.4.0
34
37
  Requires-Dist: scipy >=1.11.2
35
38
  Requires-Dist: shapely >=2.0.6
@@ -44,16 +47,17 @@ Requires-Dist: planetary-computer >=1.0.0 ; extra == 'notebooks'
44
47
 
45
48
  # `eo-tides`: Tide modelling tools for large-scale satellite earth observation analysis
46
49
 
47
- <img align="right" width="200" src="docs/assets/eo-tides-logo.gif" alt="eo-tides logo" style="margin-right: 40px;">
50
+ <img align="right" width="200" src="https://github.com/GeoscienceAustralia/eo-tides/blob/main/docs/assets/eo-tides-logo.gif?raw=true" alt="eo-tides logo" style="margin-right: 40px;">
48
51
 
49
52
  [![Release](https://img.shields.io/github/v/release/GeoscienceAustralia/eo-tides)](https://pypi.org/project/eo-tides/)
50
53
  [![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)
51
- ![Python Version from PEP 621 TOML](https://img.shields.io/pypi/pyversions/eo-tides)
54
+ [![Python Version from PEP 621 TOML](https://img.shields.io/pypi/pyversions/eo-tides)](https://github.com/GeoscienceAustralia/eo-tides/blob/main/pyproject.toml)
52
55
  [![codecov](https://codecov.io/gh/GeoscienceAustralia/eo-tides/branch/main/graph/badge.svg)](https://codecov.io/gh/GeoscienceAustralia/eo-tides)
53
56
  [![License](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)
54
57
 
55
- - **Github repository**: <https://github.com/GeoscienceAustralia/eo-tides/>
56
- - **Documentation** <https://GeoscienceAustralia.github.io/eo-tides/>
58
+ - ⚙️ **Github repository**: <https://github.com/GeoscienceAustralia/eo-tides/>
59
+ - 📘 **Documentation**: <https://GeoscienceAustralia.github.io/eo-tides/>
60
+ - 🐍 **PyPI**: <https://pypi.org/project/eo-tides/>
57
61
 
58
62
  > [!CAUTION]
59
63
  > This package is a work in progress, and not currently ready for operational use.
@@ -64,12 +68,12 @@ Requires-Dist: planetary-computer >=1.0.0 ; extra == 'notebooks'
64
68
 
65
69
  These tools can be applied to petabytes of freely available satellite data (e.g. from [Digital Earth Australia](https://knowledge.dea.ga.gov.au/) or [Microsoft Planetary Computer](https://planetarycomputer.microsoft.com/)) loaded via Open Data Cube's [`odc-stac`](https://odc-stac.readthedocs.io/en/latest/) or [`datacube`](https://opendatacube.readthedocs.io/en/latest/) packages, supporting coastal and ocean earth observation analysis for any time period or location globally.
66
70
 
67
- ![eo-tides abstract showing satellite data, tide data array and tide animation](docs/assets/eo-tides-abstract.gif)
71
+ ![eo-tides abstract showing satellite data, tide data array and tide animation](https://github.com/GeoscienceAustralia/eo-tides/blob/main/docs/assets/eo-tides-abstract.gif?raw=true)
68
72
 
69
73
  ## Highlights
70
74
 
71
75
  - 🌊 Model tide heights and phases (e.g. high, low, ebb, flow) from multiple global ocean tide models in parallel, and return a `pandas.DataFrame` for further analysis
72
- - 🛰️ "Tag" satellite data with tide height and stage based on the exact moment of image acquisition
76
+ - 🛰️ "Tag" satellite data with tide heights based on the exact moment of image acquisition
73
77
  - 🌐 Model tides for every individual satellite pixel through time, producing three-dimensional "tide height" `xarray`-format datacubes that can be integrated with satellite data
74
78
  - 📈 Calculate statistics describing local tide dynamics, as well as biases caused by interactions between tidal processes and satellite orbits
75
79
  - 🛠️ Validate modelled tides using measured sea levels from coastal tide gauges (e.g. [GESLA Global Extreme Sea Level Analysis](https://gesla.org/))
@@ -0,0 +1,11 @@
1
+ eo_tides/__init__.py,sha256=rn6slQP0bAhqM9cL2W8omiEM8f0b7libt6WsQ5DvYTE,1655
2
+ eo_tides/eo.py,sha256=Vc_AHqT0_IDqdwbOpNcONjHhiCtbCe8Osk2gvzUeNmU,22377
3
+ eo_tides/model.py,sha256=T0IFvSiXjiTvlY-Vi7-jneHPfaCLt95pmcRd8CZxOjE,34478
4
+ eo_tides/stats.py,sha256=lchWWJ5gBDuZWvaD8TF-12Xlo2qCWiNI2IcgAaKWy2U,22668
5
+ eo_tides/utils.py,sha256=tqXAkLk_Dm8s3yuXPxBBkHW3PZ42ayV8VCQjNw3wDZw,22433
6
+ eo_tides/validation.py,sha256=UREsc0yWRO4x0PJXvyoIx8gYiBZiRSim4z6TmAz_VDM,11857
7
+ eo_tides-0.3.0.dist-info/LICENSE,sha256=owxWsXViCL2J6Ks3XYhot7t4Y93nstmXAT95Zf030Cc,11350
8
+ eo_tides-0.3.0.dist-info/METADATA,sha256=lajVK9XoAA2fJqA9R3HHYJ7iUJFMtmxh3U07uGTs4_4,8040
9
+ eo_tides-0.3.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
10
+ eo_tides-0.3.0.dist-info/top_level.txt,sha256=lXZDUUM1DlLdKWHRn8zdmtW8Rx-eQOIWVvt0b8VGiyQ,9
11
+ eo_tides-0.3.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- eo_tides/__init__.py,sha256=MKL_HjRECVHHQVnMVg-TSz3J-vjxPZgbnyWu0RAXZ0U,1623
2
- eo_tides/eo.py,sha256=98llXpF2lSDfsVQBao-dpr8Z4zH5q0C2Rfu4X-qmPiE,22400
3
- eo_tides/model.py,sha256=kaYXU_jmv91ONam_CpoVN137aJsmqHvKHEPWe-DRJnI,38280
4
- eo_tides/stats.py,sha256=sOwXEh8RPb8muh2o9-z1c0GDnV5FJa_TgsiGb4rMkiU,22689
5
- eo_tides/utils.py,sha256=l9VXJawQzaRBYaFMsP8VBeaN5VA3rFDdzcvF7Rk04Vc,5620
6
- eo_tides/validation.py,sha256=JjTUqDfbR189m_6W1bpaSolQIHNTLicTHN7z9O_nr3s,11828
7
- eo_tides-0.2.0.dist-info/LICENSE,sha256=owxWsXViCL2J6Ks3XYhot7t4Y93nstmXAT95Zf030Cc,11350
8
- eo_tides-0.2.0.dist-info/METADATA,sha256=msiUYdlCm5pTix7LXA7RzM8Hg-9r4JHpWTSUyxdPpg4,7637
9
- eo_tides-0.2.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
10
- eo_tides-0.2.0.dist-info/top_level.txt,sha256=lXZDUUM1DlLdKWHRn8zdmtW8Rx-eQOIWVvt0b8VGiyQ,9
11
- eo_tides-0.2.0.dist-info/RECORD,,