diff --git a/docs/notebooks/Model_tides.ipynb b/docs/notebooks/Model_tides.ipynb index 0a44a1c..650aed8 100644 --- a/docs/notebooks/Model_tides.ipynb +++ b/docs/notebooks/Model_tides.ipynb @@ -32,7 +32,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can use the `eo_tides.model.available_models` function to verify that we have some tide model data available in this directory:" + "We can use the `eo_tides.model.list_models` function to verify that we have some tide model data available in this directory:" ] }, { @@ -53,9 +53,9 @@ } ], "source": [ - "from eo_tides.model import available_models\n", + "from eo_tides.model import list_models\n", "\n", - "available_models(directory, show_supported=False);" + "list_models(directory, show_supported=False);" ] }, { @@ -267,7 +267,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 2/2 [00:01<00:00, 1.46it/s]\n" + "100%|██████████| 2/2 [00:01<00:00, 1.52it/s]\n" ] }, { @@ -453,7 +453,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 2/2 [00:01<00:00, 1.47it/s]\n" + "100%|██████████| 2/2 [00:01<00:00, 1.49it/s]\n" ] }, { @@ -657,7 +657,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 2/2 [00:01<00:00, 1.47it/s]\n" + "100%|██████████| 2/2 [00:01<00:00, 1.43it/s]\n" ] }, { @@ -757,7 +757,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 4/4 [00:01<00:00, 2.95it/s]\n" + "100%|██████████| 4/4 [00:01<00:00, 2.89it/s]\n" ] }, { @@ -911,7 +911,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 4/4 [00:01<00:00, 2.92it/s]\n" + "100%|██████████| 4/4 [00:01<00:00, 2.96it/s]\n" ] }, { diff --git a/docs/setup.md b/docs/setup.md index 55d2f6b..6d0a2e8 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -194,7 +194,7 @@ tide_models/ All tide modelling functions from `eo-tides` provide a `directory` parameter that can be used to specify the location of your tide model directory. For example, using `model_tides` from the `eo_tides.model` module: -```py hl_lines="12" +```py hl_lines="8" import pandas as pd from eo_tides.model import model_tides diff --git a/eo_tides/model.py b/eo_tides/model.py index 22c7ce7..1acfec5 100644 --- a/eo_tides/model.py +++ b/eo_tides/model.py @@ -1,5 +1,6 @@ import os import pathlib +import warnings from concurrent.futures import ProcessPoolExecutor from functools import partial @@ -15,65 +16,103 @@ from eo_tides.utils import idw -def available_models(directory=None, show_supported=True): +def _set_directory(directory): """ - Prints a list of all tide models available for tide - modelling using `eo-tides`. + Set tide modelling files directory. If no custom + path is provided, try global environmental variable + instead. + """ + if directory is None: + if "EO_TIDES_TIDE_MODELS" in os.environ: + directory = os.environ["EO_TIDES_TIDE_MODELS"] + else: + raise Exception( + "No tide model directory provided via `directory`, and/or no " + "`EO_TIDES_TIDE_MODELS` environment variable found. " + "Please provide a valid path to your tide model directory." + ) + + # Verify path exists + directory = pathlib.Path(directory).expanduser() + if not directory.exists(): + raise FileNotFoundError(f"No valid tide model directory found at path `{directory}`") + else: + return directory + + +def list_models(directory=None, show_available=True, show_supported=True, raise_error=False): + """ + List all tide models available for tide modelling, and + all models supported by `eo-tides` and `pyTMD`. This function scans the specified tide model directory - for tide models supported by the `pyTMD` package, and - prints a list of models that are available in the - directory as well as the full list of supported models. + and returns a list of models that are available in the + directory as well as the full list of all supported models. For instructions on setting up tide models, see: Parameters ---------- - directory : str - Path to the directory containing tide model files. + directory : string, optional + The directory containing tide model data files. If no path is + provided, this will default to the environment variable + `EO_TIDES_TIDE_MODELS` if set, or raise an error if not. + Tide modelling files should be stored in sub-folders for each + model that match the structure required by `pyTMD` + (). + show_available : bool, optional + Whether to print a list of locally available models. + show_supported : bool, optional + Whether to print a list of all supported models, in + addition to models available locally. Returns ------- - available_m : list - A list of all available tide models within - `directory`. + available_models : list + A list of alltide models available within `directory`. + supported_models : list + A list of all tide models supported by `eo-tides`. """ - # TODO: Pull directory code into re-usable function - - # Set tide modelling files directory. If no custom path is provided, - # first try global environmental var, then "/var/share/tide_models" - if directory is None: - if "EO_TIDES_TIDE_MODELS" in os.environ: - directory = os.environ["EO_TIDES_TIDE_MODELS"] - else: - directory = "/var/share/tide_models" + # Set tide modelling files directory. If no custom path is + # provided, try global environment variable. + directory = _set_directory(directory) - # Verify path exists - directory = pathlib.Path(directory).expanduser() - if not directory.exists(): - raise FileNotFoundError("Invalid tide directory") - - # Get full list of supported models from the database - supported_models = load_database()["elevation"].keys() + # Get full list of supported models from pyTMD database + supported_models = list(load_database()["elevation"].keys()) # Print list of supported models, marking available and # unavailable models and appending available to list - print(f"Tide models available in `{directory}`:") - available_m = [] + if show_available or show_supported: + print(f"Tide models available in `{directory}`:") + available_models = [] for m in supported_models: try: model(directory=directory).elevation(m=m) - # Mark available models with a green tick - print(f" ✅ {m}") - available_m.append(m) + if show_available: + # Mark available models with a green tick + print(f" ✅ {m}") + available_models.append(m) except: if show_supported: # Mark unavailable models with a red cross print(f" ❌ {m}") - # Return list of available models - return available_m + # Raise error or warning if no models are available + if not available_models: + warning_text = ( + f"No valid tide models are available in `{directory}`. " + "Verify that you have provided the correct `directory` path, " + "or set the `EO_TIDES_TIDE_MODELS` environment variable " + "to point to the location of your tide model directory." + ) + if raise_error: + raise Exception(warning_text) + else: + warnings.warn(warning_text, UserWarning) + + # Return list of available and supported models + return available_models, supported_models def _model_tides( @@ -94,34 +133,7 @@ def _model_tides( extraction of tide modelling constituents and tide modelling using `pyTMD`. """ - # import pyTMD.eop - # import pyTMD.io - # import pyTMD.io.model - # import pyTMD.predict - # import pyTMD.spatial - # import pyTMD.time - # import pyTMD.utilities - - # Get parameters for tide model; use custom definition file for - # FES2012 (leave this as an undocumented feature for now) - # if model == "FES2012": - # pytmd_model = pyTMD.io.model(directory).from_file( - # directory / "model_FES2012.def" - # ) - # elif model == "TPXO8-atlas-v1": - # pytmd_model = pyTMD.io.model(directory).from_file(directory / "model_TPXO8.def") - # else: - # pytmd_model = pyTMD.io.model( - # directory, format="netcdf", compressed=False - # ).elevation(model) - - # if model in NONSTANDARD_MODELS: - # model_params = NONSTANDARD_MODELS[model] - # model_params_bytes = io.BytesIO(json.dumps(model_params).encode("utf-8")) - # pytmd_model = pyTMD.io.model(directory).from_file(definition_file=model_params_bytes) - - # else: - + # Obtain model details pytmd_model = pyTMD.io.model(directory).elevation(model) # Convert x, y to latitude/longitude @@ -474,11 +486,11 @@ def model_tides( This function is parallelised to improve performance, and supports all tidal models supported by `pyTMD`, including: - - Empirical Ocean Tide model (`EOT20`) - - Finite Element Solution tide models (`FES2022`, `FES2014`, `FES2012`) - - TOPEX/POSEIDON global tide models (`TPXO10`, `TPXO9`, `TPXO8`) - - Global Ocean Tide models (`GOT5.6`, `GOT5.5`, `GOT4.10`, `GOT4.8`, `GOT4.7`) - - Hamburg direct data Assimilation Methods for Tides models (`HAMTIDE11`) + - Empirical Ocean Tide model (`EOT20`) + - Finite Element Solution tide models (`FES2022`, `FES2014`, `FES2012`) + - TOPEX/POSEIDON global tide models (`TPXO10`, `TPXO9`, `TPXO8`) + - Global Ocean Tide models (`GOT5.6`, `GOT5.5`, `GOT4.10`, `GOT4.8`, `GOT4.7`) + - Hamburg direct data Assimilation Methods for Tides models (`HAMTIDE11`) This function requires access to tide model data files. These should be placed in a folder with subfolders matching @@ -504,11 +516,11 @@ def model_tides( model : string, optional The tide model used to model tides. Options include: - - "FES2014" (pre-configured on DEA Sandbox) + - "EOT20" + - "FES2014" - "FES2022" - "TPXO9-atlas-v5" - "TPXO8-atlas" - - "EOT20" - "HAMTIDE11" - "GOT4.10" - "ensemble" (advanced ensemble tide model functionality; @@ -516,7 +528,7 @@ def model_tides( directory : string, optional The directory containing tide model data files. If no path is provided, this will default to the environment variable - `EO_TIDES_TIDE_MODELS` if set, otherwise "/var/share/tide_models". + `EO_TIDES_TIDE_MODELS` if set, or raise an error if not. Tide modelling files should be stored in sub-folders for each model that match the structure provided by `pyTMD`. @@ -602,23 +614,6 @@ def model_tides( A dataframe containing modelled tide heights. """ - # Set tide modelling files directory. If no custom path is provided, - # first try global environmental var, then "/var/share/tide_models" - if directory is None: - if "EO_TIDES_TIDE_MODELS" in os.environ: - directory = os.environ["EO_TIDES_TIDE_MODELS"] - else: - directory = "/var/share/tide_models" - - # Verify path exists - directory = pathlib.Path(directory).expanduser() - if not directory.exists(): - raise FileNotFoundError("Invalid tide directory") - - # If time passed as a single Timestamp, convert to datetime64 - if isinstance(time, pd.Timestamp): - time = time.to_datetime64() - # Turn inputs into arrays for consistent handling models_requested = np.atleast_1d(model) x = np.atleast_1d(x) @@ -644,32 +639,40 @@ def model_tides( "you intended to model multiple timesteps at each point." ) - # Verify that all provided models are supported - valid_models = [ - # Standard built-in pyTMD models - "EOT20", - "FES2014", - "FES2022", - "GOT4.10", - "HAMTIDE11", - "TPXO8-atlas", # binary version, not suitable for clipping - "TPXO9-atlas-v5", - # Non-standard models, defined internally - "FES2012", - "FES2014_extrapolated", - "FES2022_extrapolated", - "GOT5.6", - "GOT5.6_extrapolated", - "TPXO8-atlas-v1", # netCDF version - # Advanced ensemble model functionality - "ensemble", - ] + # If time passed as a single Timestamp, convert to datetime64 + if isinstance(time, pd.Timestamp): + time = time.to_datetime64() + + # Set tide modelling files directory. If no custom path is + # provided, try global environment variable. + directory = _set_directory(directory) + + # Get full list of supported models from pyTMD database; + # add ensemble option to list of supported models + available_models, valid_models = list_models( + directory, show_available=False, show_supported=False, raise_error=True + ) + valid_models = valid_models + ["ensemble"] + + # Error if any models are not supported if not all(m in valid_models for m in models_requested): - raise ValueError( - f"One or more of the models requested {models_requested} is " - f"not valid. The following models are currently supported: " - f"{valid_models}", + error_text = ( + f"One or more of the requested models are not valid:\n" + f"{models_requested}\n\n" + "The following models are supported:\n" + f"{valid_models}" + ) + raise ValueError(error_text) + + # Error if any models are not available in `directory` + if not all(m in available_models for m in models_requested): + error_text = ( + f"One or more of the requested models are valid, but not available in `{directory}`:\n" + f"{models_requested}\n\n" + f"The following models are available in `{directory}`:\n" + f"{available_models}" ) + raise ValueError(error_text) # If ensemble modelling is requested, use a custom list of models # for subsequent processing diff --git a/tests/test_model.py b/tests/test_model.py index 8937b47..97b5fa9 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -6,7 +6,7 @@ import pytest import xarray as xr -from eo_tides.model import available_models, model_tides, pixel_tides +from eo_tides.model import list_models, model_tides, pixel_tides from eo_tides.validation import eval_metrics GAUGE_X = 122.2183 @@ -91,9 +91,12 @@ def satellite_ds(request): # Test available tide models -def test_available_models(): - available_m = available_models() - assert available_m == ["FES2014", "HAMTIDE11"] +def test_list_models(): + available_models, supported_models = list_models() + assert available_models == ["FES2014", "HAMTIDE11"] + + available_models, supported_models = list_models(show_available=False, show_supported=False) + assert available_models == ["FES2014", "HAMTIDE11"] # Run test for multiple input coordinates, CRSs and interpolation methods