eo-tides 0.7.6.dev1__py3-none-any.whl → 0.7.6.dev3__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/stats.py CHANGED
@@ -1,8 +1,14 @@
1
+ """Tools for analysing local tide dynamics and satellite biases.
2
+
3
+ This module provides functions to assess how well satellite EO data
4
+ captures real-world tides, and reveals potential tide biases in
5
+ satellite EO data coverage.
6
+ """
7
+
1
8
  # Used to postpone evaluation of type annotations
2
9
  from __future__ import annotations
3
10
 
4
- import os
5
- from typing import TYPE_CHECKING
11
+ from typing import TYPE_CHECKING, cast
6
12
 
7
13
  import matplotlib.pyplot as plt
8
14
  import numpy as np
@@ -11,13 +17,21 @@ import xarray as xr
11
17
 
12
18
  # Only import if running type checking
13
19
  if TYPE_CHECKING:
20
+ import os
21
+
14
22
  from odc.geo.geobox import GeoBox
15
23
 
24
+ from .utils import DatetimeLike
25
+
16
26
  from .eo import _pixel_tides_resample, _resample_chunks, _standardise_inputs, pixel_tides, tag_tides
17
- from .utils import DatetimeLike
18
27
 
19
28
 
20
- def _tide_statistics(obs_tides, all_tides, min_max_q=(0.0, 1.0), dim="time"):
29
+ def _tide_statistics(
30
+ obs_tides: xr.DataArray,
31
+ all_tides: xr.DataArray,
32
+ min_max_q: tuple = (0.0, 1.0),
33
+ dim: str = "time",
34
+ ) -> xr.Dataset:
21
35
  # Calculate means of observed and modelled tides
22
36
  mot = obs_tides.mean(dim=dim)
23
37
  mat = all_tides.mean(dim=dim)
@@ -62,7 +76,19 @@ def _tide_statistics(obs_tides, all_tides, min_max_q=(0.0, 1.0), dim="time"):
62
76
  )
63
77
 
64
78
 
65
- def _stats_plain_english(mot, mat, hot, hat, lot, lat, otr, tr, spread, offset_low, offset_high):
79
+ def _stats_plain_english(
80
+ mot,
81
+ mat,
82
+ hot,
83
+ hat,
84
+ lot,
85
+ lat,
86
+ otr,
87
+ tr,
88
+ spread,
89
+ offset_low,
90
+ offset_high,
91
+ ) -> None:
66
92
  # Plain text descriptors
67
93
  mean_diff = "higher" if mot > mat else "lower"
68
94
  mean_diff_icon = "⬆️" if mot > mat else "⬇️"
@@ -75,26 +101,32 @@ def _stats_plain_english(mot, mat, hot, hat, lot, lat, otr, tr, spread, offset_l
75
101
  print(f"🛰️ Observed tide range: {otr:.2f} m ({lot:.2f} to {hot:.2f} m).\n")
76
102
  print(f"{spread_icon} {spread:.0%} of the modelled astronomical tide range was observed at this location.")
77
103
  print(
78
- f"{high_tide_icon} The highest {offset_high:.0%} ({offset_high * tr:.2f} m) of the tide range was never observed."
104
+ f"{high_tide_icon} The highest {offset_high:.0%} ({offset_high * tr:.2f} m) of the tide range was never observed.",
79
105
  )
80
106
  print(
81
- f"{low_tide_icon} The lowest {offset_low:.0%} ({offset_low * tr:.2f} m) of the tide range was never observed.\n"
107
+ f"{low_tide_icon} The lowest {offset_low:.0%} ({offset_low * tr:.2f} m) of the tide range was never observed.\n",
82
108
  )
83
109
  print(f"🌊 Mean modelled astronomical tide height: {mat:.2f} m.")
84
110
  print(f"🛰️ Mean observed tide height: {mot:.2f} m.")
85
111
  print(
86
- f"{mean_diff_icon} The mean observed tide height was {mot - mat:.2f} m {mean_diff} than the mean modelled astronomical tide height."
112
+ f"{mean_diff_icon} The mean observed tide height was {mot - mat:.2f} m {mean_diff} than the mean modelled astronomical tide height.",
87
113
  )
88
114
 
89
115
 
90
116
  def _stats_figure(
91
- all_tides_da, obs_tides_da, hot, hat, lot, lat, spread, offset_low, offset_high, plot_var, point_col=None
117
+ all_tides_da,
118
+ obs_tides_da,
119
+ hot,
120
+ hat,
121
+ lot,
122
+ lat,
123
+ spread,
124
+ offset_low,
125
+ offset_high,
126
+ plot_var,
127
+ point_col=None,
92
128
  ):
93
- """
94
- Plot tide bias statistics as a figure, including both
95
- satellite observations and all modelled tides.
96
- """
97
-
129
+ """Plot tide bias statistics as a figure comparing satellite observations and all modelled tides."""
98
130
  # Create plot and add all modelled tides
99
131
  fig, ax = plt.subplots(figsize=(10, 6))
100
132
  all_tides_da.plot(ax=ax, alpha=0.4, label="Modelled tides")
@@ -207,7 +239,8 @@ def tide_stats(
207
239
  round_stats: int = 3,
208
240
  **tag_tides_kwargs,
209
241
  ) -> pd.Series:
210
- """
242
+ """Generate tide statistics and satellite tide bias metrics for every dataset timestep.
243
+
211
244
  Takes a multi-dimensional dataset and generate tide statistics
212
245
  and satellite-observed tide bias metrics, calculated based on
213
246
  every timestep in the satellite data and the geographic centroid
@@ -222,7 +255,7 @@ def tide_stats(
222
255
 
223
256
  For more information about the tidal statistics computed by this
224
257
  function, refer to Figure 8 in Bishop-Taylor et al. 2018:
225
- <https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8>
258
+ https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8
226
259
 
227
260
  Parameters
228
261
  ----------
@@ -309,13 +342,13 @@ def tide_stats(
309
342
  - `spread`: proportion of the full modelled tidal range observed by the satellite
310
343
  - `offset_low`: proportion of the lowest tides never observed by the satellite
311
344
  - `offset_high`: proportion of the highest tides never observed by the satellite
312
- """
313
345
 
346
+ """
314
347
  # Standardise data inputs, time and models
315
348
  gbox, obs_times = _standardise_inputs(data, time)
316
349
 
317
350
  # Generate range of times covering entire period of satellite record
318
- assert obs_times is not None
351
+ assert obs_times is not None # noqa: S101
319
352
  all_times = pd.date_range(
320
353
  start=obs_times.min().item(),
321
354
  end=obs_times.max().item(),
@@ -333,8 +366,8 @@ def tide_stats(
333
366
  time=obs_times,
334
367
  model=model,
335
368
  directory=directory,
336
- tidepost_lat=tidepost_lat, # type: ignore
337
- tidepost_lon=tidepost_lon, # type: ignore
369
+ tidepost_lat=tidepost_lat,
370
+ tidepost_lon=tidepost_lon,
338
371
  **tag_tides_kwargs,
339
372
  )
340
373
 
@@ -344,13 +377,18 @@ def tide_stats(
344
377
  time=all_times,
345
378
  model=model,
346
379
  directory=directory,
347
- tidepost_lat=tidepost_lat, # type: ignore
348
- tidepost_lon=tidepost_lon, # type: ignore
380
+ tidepost_lat=tidepost_lat,
381
+ tidepost_lon=tidepost_lon,
349
382
  **tag_tides_kwargs,
350
383
  )
351
384
 
352
385
  # Calculate statistics
353
- stats_ds = _tide_statistics(obs_tides_da, all_tides_da, min_max_q=min_max_q)
386
+ # # (cast ensures typing knows these are always DataArrays)
387
+ stats_ds = _tide_statistics(
388
+ cast(xr.DataArray, obs_tides_da),
389
+ cast(xr.DataArray, all_tides_da),
390
+ min_max_q=min_max_q,
391
+ )
354
392
 
355
393
  # Convert to pandas and add tide post coordinates
356
394
  stats_df = stats_ds.to_pandas().astype("float32")
@@ -412,8 +450,9 @@ def pixel_stats(
412
450
  cutoff: float = 10,
413
451
  **pixel_tides_kwargs,
414
452
  ) -> xr.Dataset:
415
- """
416
- Takes a multi-dimensional dataset and generate spatial
453
+ """Generate tide statistics and satellite tide bias metrics for every dataset pixel.
454
+
455
+ Takes a multi-dimensional dataset and generate pixel-level
417
456
  tide statistics and satellite-observed tide bias metrics,
418
457
  calculated based on every timestep in the satellite data and
419
458
  modelled into the spatial extent of the imagery.
@@ -519,14 +558,13 @@ def pixel_stats(
519
558
  - `offset_high`: proportion of the highest tides never observed by the satellite
520
559
 
521
560
  """
522
-
523
561
  # Standardise data inputs, time and models
524
562
  gbox, obs_times = _standardise_inputs(data, time)
525
563
  dask_chunks = _resample_chunks(data, dask_chunks)
526
564
  model = [model] if isinstance(model, str) else model
527
565
 
528
566
  # Generate range of times covering entire period of satellite record
529
- assert obs_times is not None
567
+ assert obs_times is not None # noqa: S101
530
568
  all_times = pd.date_range(
531
569
  start=obs_times.min().item(),
532
570
  end=obs_times.max().item(),
eo_tides/utils.py CHANGED
@@ -1,3 +1,10 @@
1
+ """General-purpose utilities for tide model setup and data processing.
2
+
3
+ This module includes tools for listing and clipping model files,
4
+ performing spatial interpolation, and other helper tools used across
5
+ the eo_tides package.
6
+ """
7
+
1
8
  # Used to postpone evaluation of type annotations
2
9
  from __future__ import annotations
3
10
 
@@ -7,16 +14,22 @@ import pathlib
7
14
  import textwrap
8
15
  import warnings
9
16
  from collections import Counter
10
- from typing import TypeAlias
17
+ from typing import TYPE_CHECKING
18
+
19
+ # Only import if running type checking
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Sequence
22
+ from typing import Any, TypeAlias
23
+
24
+ from odc.geo.geom import BoundingBox
11
25
 
12
26
  import numpy as np
13
27
  import odc.geo
14
28
  import pandas as pd
29
+ import pyTMD
15
30
  import xarray as xr
16
31
  from colorama import Style, init
17
- from odc.geo.geom import BoundingBox
18
32
  from pyTMD.io.model import load_database
19
- from pyTMD.io.model import model as pytmd_model
20
33
  from scipy.spatial import cKDTree as KDTree
21
34
  from tqdm import tqdm
22
35
 
@@ -24,10 +37,8 @@ from tqdm import tqdm
24
37
  DatetimeLike: TypeAlias = np.ndarray | pd.DatetimeIndex | pd.Timestamp | datetime.datetime | str | list[str]
25
38
 
26
39
 
27
- def _get_duplicates(array):
28
- """
29
- Return any duplicates in a list or array.
30
- """
40
+ def _get_duplicates(array: Sequence[Any]) -> list[Any]:
41
+ """Return any duplicates in a list or array."""
31
42
  c = Counter(array)
32
43
  return [k for k in c if c[k] > 1]
33
44
 
@@ -35,35 +46,37 @@ def _get_duplicates(array):
35
46
  def _set_directory(
36
47
  directory: str | os.PathLike | None = None,
37
48
  ) -> os.PathLike:
38
- """
39
- Set tide modelling files directory. If no custom
40
- path is provided, try global `EO_TIDES_TIDE_MODELS`
49
+ """Set tide modelling files directory.
50
+
51
+ If no custom path is provided, try global `EO_TIDES_TIDE_MODELS`
41
52
  environmental variable instead.
42
53
  """
43
54
  if directory is None:
44
55
  if "EO_TIDES_TIDE_MODELS" in os.environ:
45
56
  directory = os.environ["EO_TIDES_TIDE_MODELS"]
46
57
  else:
47
- raise Exception(
58
+ err_msg = (
48
59
  "No tide model directory provided via `directory`, and/or no "
49
60
  "`EO_TIDES_TIDE_MODELS` environment variable found. "
50
61
  "Please provide a valid path to your tide model directory."
51
62
  )
63
+ raise Exception(err_msg)
52
64
 
53
65
  # Verify path exists
54
66
  directory = pathlib.Path(directory).expanduser()
55
67
  if not directory.exists():
56
- raise FileNotFoundError(f"No valid tide model directory found at path `{directory}`")
68
+ err_msg = f"No valid tide model directory found at path `{directory}`"
69
+ raise FileNotFoundError(err_msg)
57
70
  return directory
58
71
 
59
72
 
60
73
  def _standardise_time(
61
74
  time: DatetimeLike | None,
62
75
  ) -> np.ndarray | None:
63
- """
64
- Accept any time format accepted by `pd.to_datetime`,
65
- and return a datetime64 ndarray. Return None if None
66
- passed.
76
+ """Standardise input times for analysis.
77
+
78
+ Accept any time format accepted by `pd.to_datetime`, and
79
+ return a datetime64 ndarray. Return None if None passed.
67
80
  """
68
81
  # Return time as-is if None
69
82
  if time is None:
@@ -80,8 +93,10 @@ def _standardise_models(
80
93
  model: str | list[str],
81
94
  directory: str | os.PathLike,
82
95
  ensemble_models: list[str] | None = None,
96
+ extra_databases: str | os.PathLike | list | None = None,
83
97
  ) -> tuple[list[str], list[str], list[str] | None]:
84
- """
98
+ """Standardise lists of models for analysis.
99
+
85
100
  Take an input model name or list of names, and return a list
86
101
  of models to process, requested models, and ensemble models,
87
102
  as required by the `model_tides` function.
@@ -89,42 +104,46 @@ def _standardise_models(
89
104
  Handles two special values passed to `model`: "all", which
90
105
  will model tides for all models available in `directory`, and
91
106
  "ensemble", which will model tides for all models in a list
92
- of custom ensemble models.
107
+ of ensemble models.
93
108
  """
94
-
95
109
  # Turn inputs into arrays for consistent handling
96
- models_requested = list(np.atleast_1d(model))
110
+ models_requested = [str(m) for m in np.atleast_1d(model)]
97
111
 
98
112
  # Raise error if list contains duplications
99
113
  duplicates = _get_duplicates(models_requested)
100
114
  if len(duplicates) > 0:
101
- raise ValueError(f"The model parameter contains duplicate values: {duplicates}")
115
+ err_msg = f"The model parameter contains duplicate values: {duplicates}"
116
+ raise ValueError(err_msg)
102
117
 
103
- # Get full list of supported models from pyTMD database
118
+ # Load supported models from pyTMD database
104
119
  available_models, valid_models = list_models(
105
- directory, show_available=False, show_supported=False, raise_error=True
120
+ directory,
121
+ show_available=False,
122
+ show_supported=False,
123
+ raise_error=True,
124
+ extra_databases=extra_databases,
106
125
  )
107
126
  custom_options = ["ensemble", "all"]
108
127
 
109
128
  # Error if any models are not supported
110
129
  if not all(m in valid_models + custom_options for m in models_requested):
111
130
  error_text = (
112
- f"One or more of the requested models are not valid:\n"
113
- f"{models_requested}\n\n"
114
- "The following models are supported:\n"
115
- f"{valid_models}"
131
+ f"One or more of the requested models are not valid.\n"
132
+ f"Requested models: {models_requested}\n"
133
+ f"Valid models: {valid_models}\n"
134
+ "For tide model setup instructions, refer to the guide: https://geoscienceaustralia.github.io/eo-tides/setup/"
116
135
  )
117
- raise ValueError(error_text)
136
+ raise ValueError(error_text) from None
118
137
 
119
138
  # Error if any models are not available in `directory`
120
139
  if not all(m in available_models + custom_options for m in models_requested):
121
140
  error_text = (
122
- f"One or more of the requested models are valid, but not available in `{directory}`:\n"
123
- f"{models_requested}\n\n"
124
- f"The following models are available in `{directory}`:\n"
125
- f"{available_models}"
141
+ f"One or more of the requested tide models are not available in `{directory}`.\n"
142
+ f"Requested models: {models_requested}\n"
143
+ f"Available models: {available_models}\n"
144
+ "For tide model setup instructions, refer to the guide: https://geoscienceaustralia.github.io/eo-tides/setup/"
126
145
  )
127
- raise ValueError(error_text)
146
+ raise ValueError(error_text) from None
128
147
 
129
148
  # If "all" models are requested, update requested list to include available models
130
149
  if "all" in models_requested:
@@ -177,8 +196,7 @@ def _clip_model_file(
177
196
  ycoord: str,
178
197
  xcoord: str,
179
198
  ) -> xr.Dataset:
180
- """
181
- Clips tide model netCDF datasets to a bounding box.
199
+ """Clips tide model netCDF datasets to a bounding box.
182
200
 
183
201
  If the bounding box crosses 0 degrees longitude (e.g. Greenwich prime
184
202
  meridian), the dataset will be clipped into two parts and concatenated
@@ -219,8 +237,8 @@ def _clip_model_file(
219
237
  >>> nc = xr.open_dataset("GOT5.5/ocean_tides/2n2.nc")
220
238
  >>> bbox = BoundingBox(left=108, bottom=-48, right=158, top=-6, crs='EPSG:4326')
221
239
  >>> clipped_nc = _clip_model_file(nc, bbox, xdim="lon", ydim="lat", ycoord="latitude", xcoord="longitude")
222
- """
223
240
 
241
+ """
224
242
  # Extract x and y coords from xarray and load into memory
225
243
  xcoords = nc[xcoord].compute()
226
244
  ycoords = nc[ycoord].compute()
@@ -265,12 +283,12 @@ def _clip_model_file(
265
283
  # Combine left and right data along x dimension
266
284
  nc_clipped = xr.concat([nc_left, nc_right], dim=xdim)
267
285
 
268
- # Hack fix to remove expanded x dim on lat variables issue
286
+ # Temporary fix to remove expanded x dim on lat variables issue
269
287
  # for TPXO data; remove x dim by selecting the first obs
270
288
  for i in ["lat_z", "lat_v", "lat_u", "con"]:
271
289
  try:
272
290
  nc_clipped[i] = nc_clipped[i].isel(nx=0)
273
- except KeyError:
291
+ except KeyError: # noqa: PERF203
274
292
  pass
275
293
 
276
294
  return nc_clipped
@@ -283,9 +301,8 @@ def clip_models(
283
301
  model: list | None = None,
284
302
  buffer: float = 5,
285
303
  overwrite: bool = False,
286
- ):
287
- """
288
- Clip NetCDF-format ocean tide models to a bounding box.
304
+ ) -> None:
305
+ """Clip NetCDF-format ocean tide models to a bounding box.
289
306
 
290
307
  This function identifies all NetCDF-format tide models in a
291
308
  given input directory, including "ATLAS-netcdf" (e.g. TPXO9-atlas-nc),
@@ -296,8 +313,8 @@ def clip_models(
296
313
  directory and verified with `pyTMD` to ensure the clipped data is
297
314
  suitable for tide modelling.
298
315
 
299
- For instructions on accessing and downloading tide models, see:
300
- <https://geoscienceaustralia.github.io/eo-tides/setup/>
316
+ For tide model setup instructions, refer to the guide:
317
+ https://geoscienceaustralia.github.io/eo-tides/setup/
301
318
 
302
319
  Parameters
303
320
  ----------
@@ -326,8 +343,8 @@ def clip_models(
326
343
  ... output_directory="tide_models_clipped/",
327
344
  ... bbox=(-8.968392, 50.070574, 2.447160, 59.367122),
328
345
  ... )
329
- """
330
346
 
347
+ """
331
348
  # Get input and output paths
332
349
  input_directory = _set_directory(input_directory)
333
350
  output_directory = pathlib.Path(output_directory)
@@ -347,7 +364,8 @@ def clip_models(
347
364
 
348
365
  # Raise error if no valid models found
349
366
  if len(available_netcdf_models) == 0:
350
- raise ValueError(f"No valid NetCDF models found in {input_directory}.")
367
+ err_msg = f"No valid NetCDF models found in {input_directory}."
368
+ raise ValueError(err_msg)
351
369
 
352
370
  # If model list is provided,
353
371
  print(f"Preparing to clip suitable NetCDF models: {available_netcdf_models}\n")
@@ -426,15 +444,18 @@ def clip_models(
426
444
  )
427
445
 
428
446
  else:
429
- raise Exception(f"Model {m} not supported")
447
+ err_msg = f"Model {m} not supported"
448
+ raise Exception(err_msg)
430
449
 
431
450
  # Create directory and export
432
451
  (output_directory / file).parent.mkdir(parents=True, exist_ok=True)
433
452
  nc_clipped.to_netcdf(output_directory / file, mode="w")
434
453
 
435
454
  # Verify that models are ready
436
- pytmd_model(directory=output_directory).elevation(m=m).verify
437
- print(" ✅ Clipped model exported and verified")
455
+ if pyTMD.io.model(directory=output_directory).elevation(m=m).verify:
456
+ print(" ✅ Clipped model exported and verified")
457
+ else:
458
+ print(" ❌ Clipped model exported but unable to be verified")
438
459
 
439
460
  print(f"\nOutputs exported to {output_directory}")
440
461
  list_models(directory=output_directory, show_available=True, show_supported=False)
@@ -445,17 +466,17 @@ def list_models(
445
466
  show_available: bool = True,
446
467
  show_supported: bool = True,
447
468
  raise_error: bool = False,
469
+ extra_databases: str | os.PathLike | list | None = None,
448
470
  ) -> tuple[list[str], list[str]]:
449
- """
450
- List all tide models available for tide modelling.
471
+ """List all tide models available for tide modelling.
451
472
 
452
473
  This function scans the specified tide model directory
453
474
  and returns a list of models that are available in the
454
475
  directory as well as the full list of all models supported
455
476
  by `eo-tides` and `pyTMD`.
456
477
 
457
- For instructions on setting up tide models, see:
458
- <https://geoscienceaustralia.github.io/eo-tides/setup/>
478
+ For tide model setup instructions, refer to the guide:
479
+ https://geoscienceaustralia.github.io/eo-tides/setup/
459
480
 
460
481
  Parameters
461
482
  ----------
@@ -474,6 +495,11 @@ def list_models(
474
495
  raise_error : bool, optional
475
496
  If True, raise an error if no available models are found.
476
497
  If False, raise a warning.
498
+ extra_databases : str or path or list, optional
499
+ Additional custom tide model definitions to load, provided as
500
+ dictionaries or paths to JSON database files. Use this to
501
+ enable custom tide models not included with `pyTMD`.
502
+ See: https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#model-database
477
503
 
478
504
  Returns
479
505
  -------
@@ -481,6 +507,7 @@ def list_models(
481
507
  A list of all tide models available within `directory`.
482
508
  supported_models : list of str
483
509
  A list of all tide models supported by `eo-tides`.
510
+
484
511
  """
485
512
  init() # Initialize colorama
486
513
 
@@ -488,8 +515,11 @@ def list_models(
488
515
  # provided, try global environment variable.
489
516
  directory = _set_directory(directory)
490
517
 
491
- # Get full list of supported models from pyTMD database
492
- model_database = load_database()["elevation"]
518
+ # Load supported models from pyTMD database, adding extras if required
519
+ extra_databases = [] if extra_databases is None else extra_databases
520
+ model_database = load_database(extra_databases=extra_databases)["elevation"]
521
+
522
+ # Get full list of supported models
493
523
  supported_models = list(model_database.keys())
494
524
 
495
525
  # Extract expected model paths
@@ -499,11 +529,11 @@ def list_models(
499
529
 
500
530
  # Handle GOT5.6 differently to ensure we test for presence of GOT5.6 constituents
501
531
  if m in ("GOT5.6", "GOT5.6_extrapolated"):
502
- model_file = [file for file in model_file if "GOT5.6" in file][0]
532
+ model_file = next(file for file in model_file if "GOT5.6" in file)
503
533
  else:
504
534
  model_file = model_file[0] if isinstance(model_file, list) else model_file
505
535
 
506
- # Add path to dict
536
+ # Add expected path to dict, adding directory prefix
507
537
  expected_paths[m] = str(directory / pathlib.Path(model_file).expanduser().parent)
508
538
 
509
539
  # Define column widths
@@ -522,19 +552,22 @@ def list_models(
522
552
  available_models = []
523
553
  for m in supported_models:
524
554
  try:
525
- model_file = pytmd_model(directory=directory).elevation(m=m)
555
+ # Load model
556
+ model_file = pyTMD.io.model(directory=directory, extra_databases=extra_databases).elevation(m=m)
557
+
558
+ # Append model to list of available model
526
559
  available_models.append(m)
527
560
 
528
561
  if show_available:
529
562
  # Mark available models with a green tick
530
563
  status = "✅"
531
564
  print(f"{status:^{status_width}}│ {m:<{name_width}} │ {expected_paths[m]:<{path_width}}")
532
- except FileNotFoundError:
565
+ except FileNotFoundError: # noqa: PERF203
533
566
  if show_supported:
534
567
  # Mark unavailable models with a red cross
535
568
  status = "❌"
536
569
  print(
537
- f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}"
570
+ f"{status:^{status_width}}│ {Style.DIM}{m:<{name_width}} │ {expected_paths[m]:<{path_width}}{Style.RESET_ALL}",
538
571
  )
539
572
 
540
573
  if show_available or show_supported:
@@ -548,16 +581,15 @@ def list_models(
548
581
  if not available_models:
549
582
  warning_msg = textwrap.dedent(
550
583
  f"""
551
- No valid tide models are available in `{directory}`.
552
- Are you sure you have provided the correct `directory` path, or set the
553
- `EO_TIDES_TIDE_MODELS` environment variable to point to the location of your
554
- tide model directory?
555
- """
584
+ No valid tide models were found in `{directory}`.
585
+ Please ensure that the path you provided is correct, or set the `EO_TIDES_TIDE_MODELS` environment variable to point to a valid tide model directory.
586
+ For tide model setup instructions, refer to the guide: https://geoscienceaustralia.github.io/eo-tides/setup/
587
+ """,
556
588
  ).strip()
557
589
 
558
590
  if raise_error:
559
- raise Exception(warning_msg)
560
- warnings.warn(warning_msg, UserWarning)
591
+ raise Exception(warning_msg) from None
592
+ warnings.warn(warning_msg, UserWarning, stacklevel=2)
561
593
 
562
594
  # Return list of available and supported models
563
595
  return available_models, supported_models
@@ -649,18 +681,22 @@ def idw(
649
681
 
650
682
  # Verify input and outputs have matching lengths
651
683
  if not (input_z.shape[0] == len(input_x) == len(input_y)):
652
- raise ValueError("All of `input_z`, `input_x` and `input_y` must be the same length.")
653
- if not (len(output_x) == len(output_y)):
654
- raise ValueError("Both `output_x` and `output_y` must be the same length.")
684
+ err_msg = "All of `input_z`, `input_x` and `input_y` must be the same length."
685
+ raise ValueError(err_msg)
686
+ if len(output_x) != len(output_y):
687
+ err_msg = "Both `output_x` and `output_y` must be the same length."
688
+ raise ValueError(err_msg)
655
689
 
656
690
  # Verify k is smaller than total number of points, and non-zero
657
691
  if k > input_z.shape[0]:
658
- raise ValueError(
692
+ err_msg = (
659
693
  f"The requested number of nearest neighbours (`k={k}`) "
660
694
  f"is smaller than the total number of points ({input_z.shape[0]}).",
661
695
  )
696
+ raise ValueError(err_msg)
662
697
  if k == 0:
663
- raise ValueError("Interpolation based on `k=0` nearest neighbours is not valid.")
698
+ err_msg = "Interpolation based on `k=0` nearest neighbours is not valid."
699
+ raise ValueError(err_msg)
664
700
 
665
701
  # Create KDTree to efficiently find nearest neighbours
666
702
  points_xy = np.column_stack((input_y, input_x))