tfv-get-tools 0.2.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.
Files changed (62) hide show
  1. tfv_get_tools/__init__.py +4 -0
  2. tfv_get_tools/_standard_attrs.py +107 -0
  3. tfv_get_tools/atmos.py +167 -0
  4. tfv_get_tools/cli/_cli_base.py +173 -0
  5. tfv_get_tools/cli/atmos_cli.py +192 -0
  6. tfv_get_tools/cli/ocean_cli.py +204 -0
  7. tfv_get_tools/cli/tide_cli.py +118 -0
  8. tfv_get_tools/cli/wave_cli.py +183 -0
  9. tfv_get_tools/fvc/__init__.py +3 -0
  10. tfv_get_tools/fvc/_atmos.py +230 -0
  11. tfv_get_tools/fvc/_fvc.py +218 -0
  12. tfv_get_tools/fvc/_ocean.py +171 -0
  13. tfv_get_tools/fvc/_tide.py +195 -0
  14. tfv_get_tools/ocean.py +170 -0
  15. tfv_get_tools/providers/__init__.py +0 -0
  16. tfv_get_tools/providers/_custom_conversions.py +34 -0
  17. tfv_get_tools/providers/_downloader.py +566 -0
  18. tfv_get_tools/providers/_merger.py +520 -0
  19. tfv_get_tools/providers/_utilities.py +255 -0
  20. tfv_get_tools/providers/atmos/barra2.py +209 -0
  21. tfv_get_tools/providers/atmos/cfgs/barra2_c2.yaml +52 -0
  22. tfv_get_tools/providers/atmos/cfgs/barra2_r2.yaml +85 -0
  23. tfv_get_tools/providers/atmos/cfgs/barra2_re2.yaml +70 -0
  24. tfv_get_tools/providers/atmos/cfgs/cfsr.yaml +68 -0
  25. tfv_get_tools/providers/atmos/cfgs/era5.yaml +77 -0
  26. tfv_get_tools/providers/atmos/cfgs/era5_gcp.yaml +77 -0
  27. tfv_get_tools/providers/atmos/cfsr.py +207 -0
  28. tfv_get_tools/providers/atmos/era5.py +20 -0
  29. tfv_get_tools/providers/atmos/era5_gcp.py +20 -0
  30. tfv_get_tools/providers/ocean/cfgs/copernicus_blk.yaml +64 -0
  31. tfv_get_tools/providers/ocean/cfgs/copernicus_glo.yaml +67 -0
  32. tfv_get_tools/providers/ocean/cfgs/copernicus_nws.yaml +62 -0
  33. tfv_get_tools/providers/ocean/cfgs/hycom.yaml +73 -0
  34. tfv_get_tools/providers/ocean/copernicus_ocean.py +457 -0
  35. tfv_get_tools/providers/ocean/hycom.py +611 -0
  36. tfv_get_tools/providers/wave/cawcr.py +166 -0
  37. tfv_get_tools/providers/wave/cfgs/cawcr_aus_10m.yaml +39 -0
  38. tfv_get_tools/providers/wave/cfgs/cawcr_aus_4m.yaml +39 -0
  39. tfv_get_tools/providers/wave/cfgs/cawcr_glob_24m.yaml +39 -0
  40. tfv_get_tools/providers/wave/cfgs/cawcr_pac_10m.yaml +39 -0
  41. tfv_get_tools/providers/wave/cfgs/cawcr_pac_4m.yaml +39 -0
  42. tfv_get_tools/providers/wave/cfgs/copernicus_glo.yaml +56 -0
  43. tfv_get_tools/providers/wave/cfgs/copernicus_nws.yaml +51 -0
  44. tfv_get_tools/providers/wave/cfgs/era5.yaml +48 -0
  45. tfv_get_tools/providers/wave/cfgs/era5_gcp.yaml +48 -0
  46. tfv_get_tools/providers/wave/copernicus_wave.py +38 -0
  47. tfv_get_tools/providers/wave/era5.py +232 -0
  48. tfv_get_tools/providers/wave/era5_gcp.py +169 -0
  49. tfv_get_tools/tide/__init__.py +2 -0
  50. tfv_get_tools/tide/_nodestring.py +214 -0
  51. tfv_get_tools/tide/_tidal_base.py +568 -0
  52. tfv_get_tools/utilities/_tfv_bc.py +78 -0
  53. tfv_get_tools/utilities/horizontal_padding.py +89 -0
  54. tfv_get_tools/utilities/land_masking.py +93 -0
  55. tfv_get_tools/utilities/parsers.py +44 -0
  56. tfv_get_tools/utilities/warnings.py +38 -0
  57. tfv_get_tools/wave.py +179 -0
  58. tfv_get_tools-0.2.0.dist-info/METADATA +286 -0
  59. tfv_get_tools-0.2.0.dist-info/RECORD +62 -0
  60. tfv_get_tools-0.2.0.dist-info/WHEEL +5 -0
  61. tfv_get_tools-0.2.0.dist-info/entry_points.txt +5 -0
  62. tfv_get_tools-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,89 @@
1
+ """
2
+ Set of functions to managing horizontally padding an OCEAN dataset
3
+
4
+ This will cast the nearest value horizontally (lat/lon) for each depth level, through all times
5
+
6
+ """
7
+ import xarray as xr
8
+ import numpy as np
9
+ from scipy import spatial
10
+
11
+ def horizontal_pad(ds):
12
+ """Pad an ocean dataset horizontally for each time.
13
+
14
+ Args:
15
+ ds (xr.Dataset): input merged ocean dataset
16
+
17
+ Returns:
18
+ xr.Dataset: resulting padded ocean dataset
19
+ """
20
+ for var in ds.data_vars:
21
+ if set(ds[var].dims) >= {'latitude', 'longitude', 'time'}:
22
+ # Compute nearest indices for each depth level
23
+ nearest_indices = xr.apply_ufunc(
24
+ _compute_nearest_indices,
25
+ ds[var].isel(time=0),
26
+ input_core_dims=[['latitude', 'longitude']],
27
+ output_core_dims=[['latitude', 'longitude']],
28
+ vectorize=True
29
+ )
30
+
31
+ # Apply filling to all time steps for each depth
32
+ filled_var = xr.apply_ufunc(
33
+ _fill_nans_with_precomputed_indices,
34
+ ds[var],
35
+ nearest_indices,
36
+ input_core_dims=[['latitude', 'longitude'], ['latitude', 'longitude']],
37
+ output_core_dims=[['latitude', 'longitude']],
38
+ vectorize=True
39
+ )
40
+
41
+ ds[var] = filled_var
42
+
43
+ return ds
44
+
45
+ def _compute_nearest_indices(values: np.ndarray) -> np.ndarray:
46
+ """Find the indices of the nearest non nan values across longitude/latitude
47
+
48
+ Args:
49
+ values (np.ndarray): variable values
50
+
51
+ Returns:
52
+ np.ndarray: indices array
53
+ """
54
+ ny, nx = values.shape
55
+ lat = np.arange(ny)
56
+ lon = np.arange(nx)
57
+ lat, lon = np.meshgrid(lat, lon, indexing='ij')
58
+
59
+ valid_mask = ~np.isnan(values)
60
+ valid_points = np.column_stack((lat[valid_mask], lon[valid_mask]))
61
+
62
+ # Create KDTree for efficient nearest neighbor search
63
+ tree = spatial.cKDTree(valid_points)
64
+
65
+ # Find nearest non-NaN point for each point
66
+ all_points = np.column_stack((lat.ravel(), lon.ravel()))
67
+ _, indices = tree.query(all_points)
68
+
69
+ # Reshape indices to match the original shape
70
+ return indices.reshape(values.shape)
71
+
72
+ def _fill_nans_with_precomputed_indices(values: np.ndarray, nearest_indices: np.ndarray) -> np.ndarray:
73
+ """Fill nan values in dataset wih the pre-calced indicies.
74
+
75
+ Args:
76
+ values (np.ndarray): variable values
77
+ nearest_indices (np.ndarray): indicies if nearest non-nan value across longitude/latitude
78
+
79
+ Returns:
80
+ np.ndarray: filled variable values
81
+ """
82
+ valid_values = values[~np.isnan(values)]
83
+
84
+ # Apply the precomputed indices to fill NaN values
85
+ filled_values = values.copy()
86
+ nan_mask = np.isnan(filled_values)
87
+ filled_values[nan_mask] = valid_values[nearest_indices[nan_mask]]
88
+
89
+ return filled_values
@@ -0,0 +1,93 @@
1
+ """
2
+ Module to handle land masking of an atmospheric dataset.
3
+ This has been straight air-lifted from GetAtmos with no modifications.
4
+ """
5
+
6
+ import lzma
7
+ import pickle
8
+ import importlib.resources
9
+
10
+ import xarray as xr
11
+ import numpy as np
12
+ import dask.array as da
13
+ import pandas as pd
14
+ import logging
15
+ from scipy.ndimage import distance_transform_edt
16
+
17
+ from tqdm.auto import trange
18
+
19
+
20
+ def load_atmos_masks():
21
+ """Returns a dictionary containing landmasks for ERA5, CFSR, CFSv2 and BARRA_R.
22
+
23
+ Returns:
24
+ lm (dict): Dict containing keys 'cfsr', 'cfsv2', 'barra_r', 'era5'.
25
+ Each of these contains a sub-dict that has 'x', 'y' and 'lm' keys.
26
+ """
27
+ with importlib.resources.files(__name__).parent.joinpath("providers/atmos/data/atmos_masks.xz").open("rb") as stream:
28
+ with lzma.open(stream, "rb") as f:
29
+ lmdict = pickle.load(f)
30
+
31
+ return lmdict
32
+
33
+
34
+ def mask_land_data(ds, source):
35
+ lms = load_atmos_masks()
36
+
37
+ # Load all land masks (TODO: BETTER HANDLING OF CFSR and CFSv2 SWITCH)
38
+ if "cfs" in source:
39
+ if ds["time"][-1] > pd.Timestamp(2011, 1, 1):
40
+ k = "cfsv2"
41
+ else:
42
+ k = "cfsr"
43
+ else:
44
+ k = source.lower()
45
+ lm = lms[k]
46
+
47
+ dsm = xr.DataArray(
48
+ np.squeeze(lm["lm"]), coords=dict(latitude=lm["y"], longitude=lm["x"])
49
+ )
50
+
51
+ ds["lsm"] = dsm.reindex(
52
+ longitude=ds["longitude"], latitude=ds["latitude"], method="nearest"
53
+ )
54
+ ds["lsm"].attrs = {"units": "-", "long_name": f"{k} land mask index"}
55
+
56
+ # If the Atmos dataset is in negative longitudes, we need to wrap the land mask around 180
57
+ # (i.e., -180 to 180), not (0 to 360)
58
+ if ds["longitude"].min() < 0.0:
59
+ lon = dsm['longitude'].values
60
+ lon[lon > 180] = (lon - 360)[lon > 180]
61
+ dsm['longitude'] = lon
62
+ dsm = dsm.sortby('longitude')
63
+
64
+ mask = (ds["lsm"] > 0.2).values
65
+ mask_3d = repeat_2d_array(mask, ds.sizes["time"])
66
+
67
+ for var in ds.data_vars.keys():
68
+ dims = ds[var].dims
69
+ if ('longitude' in dims) & ('latitude' in dims) & ('time' in dims):
70
+ logging.debug(f"{var} in ds, running land mask")
71
+ ds[var].data = fill(ds[var].values, mask_3d)
72
+
73
+ return ds
74
+
75
+
76
+ def fill(data, mask):
77
+ """Replace "masked" data with the nearest valid data cell
78
+
79
+ Args:
80
+ data (da.ndarray): 2D input data array
81
+ invalid (da.ndarray): 2D boolean array, where True cells are replaced by the nearest data.
82
+
83
+ Returns:
84
+ filled_data: 2D output array with filled data
85
+ """
86
+
87
+ ind = distance_transform_edt(mask, return_distances=False, return_indices=True)
88
+ return data[tuple(ind)]
89
+
90
+
91
+ def repeat_2d_array(array, n):
92
+ repeated_array = da.tile(array, (n, 1, 1))
93
+ return repeated_array
@@ -0,0 +1,44 @@
1
+ from datetime import datetime
2
+ import pandas as pd
3
+ from pathlib import Path
4
+ from typing import Union
5
+
6
+
7
+ def _parse_date(date: Union[str, datetime, pd.Timestamp]) -> datetime:
8
+ if isinstance(date, str):
9
+ if len(date) == 10:
10
+ fmt = "%Y-%m-%d"
11
+ elif len(date) == 13:
12
+ fmt = "%Y-%m-%d %H"
13
+ elif len(date) == 16:
14
+ fmt = "%Y-%m-%d %H:%M"
15
+ elif len(date) == 19:
16
+ fmt = "%Y-%m-%d %H:%M:%S"
17
+ elif len(date) == 8:
18
+ fmt = "%Y%m%d"
19
+ elif len(date) == 15:
20
+ fmt = "%Y%m%d.%H%M%S"
21
+
22
+ try:
23
+ date = datetime.strptime(date, fmt)
24
+ except:
25
+ raise ValueError(f'Failed to convert date `{date}` to a Timestamp, please check format (e.g., YYYY-mm-dd)')
26
+ return date
27
+
28
+ elif isinstance(date, pd.Timestamp):
29
+ return date.to_pydatetime()
30
+ elif isinstance(date, datetime):
31
+ return date
32
+ else:
33
+ raise ValueError(
34
+ "Date must be a string, datetime, or pandas Timestamp object"
35
+ )
36
+
37
+ def _parse_path(path: Union[str, Path]) -> Path:
38
+ path = Path(path)
39
+ if path.is_dir():
40
+ return path
41
+ else:
42
+ raise NotADirectoryError(
43
+ f"`{path.as_posix()}` does not exist, please check."
44
+ )
@@ -0,0 +1,38 @@
1
+ import warnings
2
+ import functools
3
+
4
+
5
+ def deprecated(new_func):
6
+ """
7
+ Decorator to mark functions as deprecated.
8
+
9
+ This decorator will result in a warning being emitted
10
+ when the decorated function is used.
11
+
12
+ Args:
13
+ new_func (callable): The new function to be used instead of the deprecated function.
14
+
15
+ Returns:
16
+ callable: A decorator function.
17
+
18
+ Example:
19
+ >>> def new_function(x, y):
20
+ ... return x + y
21
+ ...
22
+ >>> @deprecated(new_function)
23
+ ... def old_function(x, y):
24
+ ... return new_function(x, y)
25
+ ...
26
+ >>> old_function(1, 2)
27
+ 3
28
+ # Warning: Call to deprecated function old_function. Use new_function instead.
29
+ """
30
+ def decorator(old_func):
31
+ @functools.wraps(old_func)
32
+ def wrapper(*args, **kwargs):
33
+ warnings.warn(f"Call to deprecated function {old_func.__name__}. "
34
+ f"Use {new_func.__name__} instead.",
35
+ category=DeprecationWarning, stacklevel=2)
36
+ return new_func(*args, **kwargs)
37
+ return wrapper
38
+ return decorator
tfv_get_tools/wave.py ADDED
@@ -0,0 +1,179 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ from typing import Union, Tuple, Optional, List
4
+ import pandas as pd
5
+
6
+ from tfv_get_tools.utilities._tfv_bc import write_tuflowfv_fvc
7
+ from tfv_get_tools.providers._downloader import BatchDownloadResult
8
+
9
+ def DownloadWave(
10
+ start_date: Union[str, datetime, pd.Timestamp],
11
+ end_date: Union[str, datetime, pd.Timestamp],
12
+ xlims: Tuple[float, float],
13
+ ylims: Tuple[float, float],
14
+ out_path: Union[str, Path] = Path("./raw"),
15
+ source: str = "CAWCR",
16
+ model: str = "default",
17
+ prefix: Optional[str] = None,
18
+ verbose: bool = False,
19
+ variables: Optional[List[str]] = None,
20
+ skip_check: bool = False,
21
+ **kwargs,
22
+ ) -> BatchDownloadResult:
23
+ """Download Wave Data
24
+
25
+ Users should call this function, not the individual downloader classes directly.
26
+
27
+ This module will download wave data from several possible sources to facilitate
28
+ TUFLOW FV and SWAN modelling.
29
+
30
+ The following sources have been implemented:
31
+ - `CAWCR` - CSIRO Ocean wave hindcast forced using NCEP's CFSR Atmospheric Model
32
+ - `glob_24m` - Global domain at 24' resolution
33
+ - `aus_10m` - Australia ribbon model domain at 10' resolution
34
+ - `aus_4m` - Australia ribbon model domain at 4' resolution
35
+ - `pac_10m` - Pacific island domain at 10' resolution
36
+ - `pac_4m` - Pacific island domain at 4' resolution
37
+ - `Copernicus` - Various models from the Copernicus Marine Service
38
+ - `GLO` - Global wave model
39
+ - `ERA5` - Global 0.5deg Wave model (WAM) from the European Centre for
40
+ Medium-Range Weather Forecasts (ECMWF)
41
+
42
+ Args:
43
+ start_date: Start date. The string format is `%Y-%m-%d` (e.g., '2011-01-01')
44
+ end_date: End date. The string format is `%Y-%m-%d` (e.g., '2011-02-01')
45
+ xlims: Minimum and maximum longitude, as floats. e.g., (115, 120)
46
+ ylims: Minimum and maximum latitude, as floats. e.g., (-40, -35)
47
+ out_path: Output directory for data files
48
+ source: Data source to download. One of {'CAWCR', 'Copernicus', 'ERA5'}
49
+ model: Choice of model, depending on "source"
50
+ prefix: Extra file name prefix
51
+ verbose: Print extra program information
52
+ variables: List of variables to download
53
+ skip_check: Skip user confirmation
54
+ **kwargs: Additional arguments
55
+
56
+ Returns:
57
+ BatchDownloadResult: Results of the download operation
58
+ """
59
+
60
+ if source.lower() == "cawcr":
61
+ from tfv_get_tools.providers.wave.cawcr import DownloadCAWCR
62
+
63
+ downloader = DownloadCAWCR(
64
+ start_date=start_date,
65
+ end_date=end_date,
66
+ xlims=xlims,
67
+ ylims=ylims,
68
+ out_path=out_path,
69
+ model=model,
70
+ prefix=prefix,
71
+ verbose=verbose,
72
+ variables=variables,
73
+ skip_check=skip_check,
74
+ **kwargs
75
+ )
76
+ return downloader.execute_download()
77
+
78
+ elif source.lower() == "copernicus":
79
+ from tfv_get_tools.providers.wave.copernicus_wave import DownloadCopernicusWave
80
+
81
+ downloader = DownloadCopernicusWave(
82
+ start_date=start_date,
83
+ end_date=end_date,
84
+ xlims=xlims,
85
+ ylims=ylims,
86
+ out_path=out_path,
87
+ model=model,
88
+ prefix=prefix,
89
+ verbose=verbose,
90
+ variables=variables,
91
+ skip_check=skip_check,
92
+ **kwargs
93
+ )
94
+ return downloader.execute_download()
95
+
96
+ elif source.lower() == "era5":
97
+ from tfv_get_tools.providers.wave.era5 import DownloadERA5Wave
98
+
99
+ downloader = DownloadERA5Wave(
100
+ start_date=start_date,
101
+ end_date=end_date,
102
+ xlims=xlims,
103
+ ylims=ylims,
104
+ out_path=out_path,
105
+ model=model,
106
+ prefix=prefix,
107
+ verbose=verbose,
108
+ variables=variables,
109
+ skip_check=skip_check,
110
+ **kwargs
111
+ )
112
+ return downloader.execute_download()
113
+
114
+ else:
115
+ raise ValueError(f'Unrecognised source {source}. Must be one of: CAWCR, Copernicus, ERA5')
116
+
117
+
118
+
119
+ def MergeWave(
120
+ in_path: Path = Path("./raw"),
121
+ out_path: Path = Path("."),
122
+ fname: str = None,
123
+ source: str = 'CAWCR',
124
+ model: str = 'default',
125
+ time_start: str = None,
126
+ time_end: str = None,
127
+ reproject: int = None,
128
+ local_tz: Tuple[float, str] = None,
129
+ wrapto360=False,
130
+ ):
131
+ """
132
+ Merge raw downloaded wave datafiles into a single netcdf file.
133
+
134
+ **Use the same `source` and `model` that was supplied to the Downloader function**
135
+
136
+ Args:
137
+ in_path (Path, optional): Directory of the raw ocean data-files. Defaults to Path(".").
138
+ out_path (Path, optional): Output directory for the merged ocean netcdf and (opt) the fvc. Defaults to Path(".").
139
+ fname (str, optional): Merged ocean netcdf filename. Defaults to None.
140
+ source (str, optional): Source to be merged, defaults to "CAWCR".
141
+ model (str, optional): Model for source to be merged. Defaults to 'default' (which is 'glob_24m' for CAWCR).
142
+ time_start (str, optional): Start time limit of the merged dataset (str: "YYYY-mm-dd HH:MM"). Defaults to None.
143
+ time_end (str, optional): End time limit of the merged dataset (str: "YYYY-mm-dd HH:MM"). Defaults to None.
144
+ reproject (int, optional): Optionally reproject based, based on EPSG code. Defaults to None.
145
+ local_tz: (Tuple(float, str): optional): Add local timezone format is a tuple with Offset[float] and Label[str]
146
+ source: (str, optional): Ocean data source. Defaults to Hycom.
147
+ wrapto360: (bool, optional): Optionally wrap longitude to (0, 360) rather than (-180, 180). Defaults to False.
148
+ """
149
+
150
+ args = tuple()
151
+
152
+ kwargs = dict(
153
+ in_path=in_path,
154
+ out_path=out_path,
155
+ fname=fname,
156
+ source=source,
157
+ model=model,
158
+ time_start=time_start,
159
+ time_end=time_end,
160
+ reproject=reproject,
161
+ local_tz=local_tz,
162
+ pad_dry=False,
163
+ wrapto360=wrapto360,
164
+ )
165
+
166
+ if 'write_fvc' in kwargs:
167
+ print('Writing an FVC include file is not implemented for wave data. Skipping this flag')
168
+
169
+ if source.lower() == "cawcr":
170
+ from tfv_get_tools.providers.wave.cawcr import MergeCAWCR
171
+ MergeCAWCR(*args, **kwargs)
172
+
173
+ elif source.lower() == "copernicus":
174
+ from tfv_get_tools.providers.wave.copernicus_wave import MergeCopernicusWave
175
+ MergeCopernicusWave(*args, **kwargs)
176
+
177
+ elif source.lower() == "era5":
178
+ from tfv_get_tools.providers.wave.era5 import MergeERA5Wave
179
+ MergeERA5Wave(*args, **kwargs)