cht_utils 2.0.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.
cht_utils/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ __version__ = "2.0.0"
2
+
3
+ """CHT utilities — common tools for the Deltares Coastal Hazard Toolkit.
4
+
5
+ Subpackages
6
+ -----------
7
+ fileio
8
+ File I/O for Deltares formats (INI, Tekal, PLI/POL), YAML, JSON-JS, XML.
9
+ fileops
10
+ File and directory operations (copy, move, delete, list).
11
+ cog
12
+ Cloud-Optimized GeoTIFF conversion (from GeoTIFF, NetCDF, XYZ).
13
+ physics
14
+ Coastal and wave physics (dispersion, shoaling, runup, wave decomposition).
15
+ remote
16
+ Remote file transfer (AWS S3, SSH/SFTP).
17
+ probabilistic
18
+ Ensemble post-processing and probability maps.
19
+ maps
20
+ Floodmap and Topography map visualization.
21
+
22
+ Top-level modules
23
+ -----------------
24
+ colors
25
+ Colormap registration, rendering, and conversion.
26
+ interpolation
27
+ 2-D interpolation on regular and unstructured grids.
28
+ """
@@ -0,0 +1,6 @@
1
+ """Cloud-Optimized GeoTIFF (COG) conversion utilities."""
2
+
3
+ from cht_utils.cog.geotiff_to_cog import geotiff_to_cog as geotiff_to_cog
4
+ from cht_utils.cog.geotiff_to_cog import is_cog as is_cog
5
+ from cht_utils.cog.netcdf_to_cog import netcdf_to_cog as netcdf_to_cog
6
+ from cht_utils.cog.xyz_to_cog import xyz_to_cog as xyz_to_cog
@@ -0,0 +1,79 @@
1
+ """Convert GeoTIFF files to Cloud-Optimized GeoTIFF (COG) format."""
2
+
3
+ import shutil
4
+
5
+ import rasterio
6
+ from rasterio.shutil import copy as rio_copy
7
+
8
+
9
+ def is_cog(tif_path: str) -> bool:
10
+ """Check if a GeoTIFF is Cloud-Optimized.
11
+
12
+ Verifies that the file is tiled and has overviews.
13
+
14
+ Parameters
15
+ ----------
16
+ tif_path : str
17
+ Path to the GeoTIFF file.
18
+
19
+ Returns
20
+ -------
21
+ bool
22
+ ``True`` if the file is a COG.
23
+ """
24
+ try:
25
+ with rasterio.open(tif_path) as src:
26
+ if not src.is_tiled:
27
+ return False
28
+ overviews = src.overviews(1)
29
+ return len(overviews) > 0
30
+ except Exception as e:
31
+ print(f"Error checking COG: {e}")
32
+ return False
33
+
34
+
35
+ def geotiff_to_cog(
36
+ geotiff_path: str,
37
+ output_cog_path: str,
38
+ resampling: str = "average",
39
+ ) -> bool:
40
+ """Convert a GeoTIFF to Cloud-Optimized GeoTIFF.
41
+
42
+ If the input is already a COG, it is simply copied.
43
+
44
+ Parameters
45
+ ----------
46
+ geotiff_path : str
47
+ Path to the input GeoTIFF.
48
+ output_cog_path : str
49
+ Path for the output COG.
50
+ resampling : str
51
+ Resampling method for overviews (default: ``"average"``).
52
+
53
+ Returns
54
+ -------
55
+ bool
56
+ ``True`` on success, ``False`` on error.
57
+ """
58
+ try:
59
+ if is_cog(geotiff_path):
60
+ print(f"{geotiff_path} is already a COG! Copying to {output_cog_path}")
61
+ shutil.copy(geotiff_path, output_cog_path)
62
+ return True
63
+
64
+ with rasterio.open(geotiff_path) as src:
65
+ rio_copy(
66
+ src,
67
+ output_cog_path,
68
+ driver="COG",
69
+ compress="deflate",
70
+ blocksize=512,
71
+ overview_resampling=resampling,
72
+ )
73
+
74
+ print(f"COG saved to: {output_cog_path}")
75
+ return True
76
+
77
+ except Exception as e:
78
+ print(f"Error during conversion: {e}")
79
+ return False
@@ -0,0 +1,85 @@
1
+ """Convert NetCDF variables to Cloud-Optimized GeoTIFF (COG) format."""
2
+
3
+ from typing import Optional
4
+
5
+ import xarray as xr
6
+ from pyproj import CRS
7
+
8
+
9
+ def netcdf_to_cog(
10
+ netcdf_path: str,
11
+ variable_name: str,
12
+ output_cog_path: str,
13
+ time_index: Optional[int] = None,
14
+ ) -> bool:
15
+ """Convert a NetCDF variable to a Cloud-Optimized GeoTIFF.
16
+
17
+ Assumes EPSG:4326 if no CRS information is found in the dataset.
18
+
19
+ Parameters
20
+ ----------
21
+ netcdf_path : str
22
+ Path to the NetCDF file.
23
+ variable_name : str
24
+ Name of the variable to extract.
25
+ output_cog_path : str
26
+ Output path for the COG.
27
+ time_index : int or None
28
+ Optional time dimension index to select.
29
+
30
+ Returns
31
+ -------
32
+ bool
33
+ ``True`` on success, ``False`` on error.
34
+ """
35
+ try:
36
+ ds = xr.open_dataset(netcdf_path)
37
+
38
+ if variable_name not in ds:
39
+ raise ValueError(
40
+ f"Variable '{variable_name}' not found in the NetCDF file."
41
+ )
42
+ da = ds[variable_name]
43
+
44
+ if "time" in da.dims and time_index is not None:
45
+ da = da.isel(time=time_index)
46
+
47
+ crs = _get_crs(ds)
48
+ crs_str = f"EPSG:{crs.to_epsg()}"
49
+
50
+ if crs.is_projected:
51
+ da.rio.set_spatial_dims(x_dim="x", y_dim="y", inplace=True)
52
+ else:
53
+ da.rio.set_spatial_dims(x_dim="lon", y_dim="lat", inplace=True)
54
+
55
+ da.rio.write_crs(crs_str, inplace=True)
56
+ da.rio.to_raster(
57
+ output_cog_path,
58
+ driver="COG",
59
+ compress="deflate",
60
+ blocksize=512,
61
+ overview_resampling="average",
62
+ dtype=da.dtype,
63
+ )
64
+ print(f"COG saved to: {output_cog_path}")
65
+ return True
66
+
67
+ except Exception as e:
68
+ print(f"Error during conversion: {e}")
69
+ return False
70
+
71
+
72
+ def _get_crs(ds: xr.Dataset) -> CRS:
73
+ """Extract CRS from a dataset, defaulting to EPSG:4326."""
74
+ if "crs" not in ds:
75
+ return CRS.from_epsg(4326)
76
+
77
+ for attr_name in ("crs_wkt", "spatial_ref"):
78
+ wkt = ds["crs"].attrs.get(attr_name)
79
+ if wkt:
80
+ crs = CRS.from_wkt(wkt)
81
+ if crs.to_epsg() is None and "NAD83" in crs.name:
82
+ return CRS.from_epsg(4269)
83
+ return crs
84
+
85
+ return CRS.from_epsg(4326)
@@ -0,0 +1,86 @@
1
+ """Convert XYZ point-cloud data to Cloud-Optimized GeoTIFF (COG) format."""
2
+
3
+ import os
4
+ from typing import Union
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import xarray as xr
9
+ from pyproj import CRS
10
+ from rasterio.enums import Resampling
11
+ from rasterio.transform import from_origin
12
+
13
+
14
+ def xyz_to_cog(
15
+ xyz_path: str,
16
+ output_cog_path: str,
17
+ crs: Union[int, CRS] = 4326,
18
+ ) -> bool:
19
+ """Grid scattered XYZ data and write as a COG.
20
+
21
+ Parameters
22
+ ----------
23
+ xyz_path : str
24
+ Path to a whitespace-separated XYZ file (columns: x, y, z).
25
+ output_cog_path : str
26
+ Output COG file path.
27
+ crs : int or pyproj.CRS
28
+ Coordinate reference system (default: EPSG:4326).
29
+
30
+ Returns
31
+ -------
32
+ bool
33
+ ``True`` on success, ``False`` on error.
34
+ """
35
+ try:
36
+ df = pd.read_csv(xyz_path, sep=r"\s+", names=["x", "y", "z"])
37
+
38
+ x_unique = np.sort(df["x"].unique())
39
+ y_unique = np.sort(df["y"].unique())
40
+
41
+ dx = np.round(np.min(np.diff(x_unique)), 6)
42
+ dy = np.round(np.min(np.diff(y_unique)), 6)
43
+
44
+ z_grid = np.full((len(y_unique), len(x_unique)), np.nan)
45
+ x_to_idx = {x: i for i, x in enumerate(x_unique)}
46
+ y_to_idx = {y: i for i, y in enumerate(y_unique)}
47
+
48
+ for row in df.itertuples(index=False):
49
+ z_grid[y_to_idx[row.y], x_to_idx[row.x]] = row.z
50
+
51
+ da = xr.DataArray(
52
+ data=z_grid,
53
+ dims=["y", "x"],
54
+ coords={"x": x_unique, "y": y_unique},
55
+ name="topography",
56
+ )
57
+
58
+ if y_unique[0] > y_unique[-1]:
59
+ da = da.sortby("y")
60
+
61
+ transform = from_origin(x_unique[0] - dx / 2, y_unique[-1] + dy / 2, dx, dy)
62
+ da.rio.write_transform(transform, inplace=True)
63
+
64
+ if isinstance(crs, int):
65
+ crs_string = f"EPSG:{crs}"
66
+ elif isinstance(crs, CRS):
67
+ crs_string = f"EPSG:{crs.to_epsg()}"
68
+ else:
69
+ crs_string = str(crs)
70
+
71
+ da.rio.write_crs(crs_string, inplace=True)
72
+ da.rio.to_raster(
73
+ output_cog_path,
74
+ driver="COG",
75
+ compress="deflate",
76
+ dtype="float32",
77
+ nodata=np.nan,
78
+ resampling=Resampling.average,
79
+ )
80
+
81
+ print(f"Saved to: {os.path.abspath(output_cog_path)}")
82
+ return True
83
+
84
+ except Exception as e:
85
+ print(f"Error: {e}")
86
+ return False
@@ -0,0 +1,6 @@
1
+ """Colormap utilities for reading, registering, and rendering colormaps."""
2
+
3
+ from cht_utils.colors.colors import cm2png as cm2png
4
+ from cht_utils.colors.colors import read_color_maps as read_color_maps
5
+ from cht_utils.colors.colors import read_colormap as read_colormap
6
+ from cht_utils.colors.colors import rgb2hex as rgb2hex
@@ -0,0 +1,117 @@
1
+ """Colormap utilities for reading, registering, and rendering colormaps."""
2
+
3
+ import os
4
+ from typing import List, Tuple
5
+
6
+ import matplotlib as mpl
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ def read_color_maps(path_name: str) -> List[str]:
13
+ """Read all colormaps from a folder and register them with matplotlib.
14
+
15
+ Each ``.txt`` file in *path_name* is expected to contain whitespace-separated
16
+ RGB values (one row per colour, values in 0-1 range).
17
+
18
+ Parameters
19
+ ----------
20
+ path_name : str
21
+ Directory containing colormap text files.
22
+
23
+ Returns
24
+ -------
25
+ List[str]
26
+ Full list of registered matplotlib colormap names.
27
+ """
28
+ for file in os.listdir(path_name):
29
+ if file.endswith(".txt"):
30
+ name = os.path.splitext(file)[0]
31
+ rgb = read_colormap(os.path.join(path_name, file))
32
+ cmap = mpl.colors.ListedColormap(rgb, name=name)
33
+ mpl.colormaps.register(cmap=cmap)
34
+ return plt.colormaps()
35
+
36
+
37
+ def cm2png(
38
+ cmap: mpl.colors.Colormap,
39
+ file_name: str = "colorbar.png",
40
+ orientation: str = "horizontal",
41
+ vmin: float = 0.0,
42
+ vmax: float = 1.0,
43
+ legend_title: str = "",
44
+ legend_label: str = "",
45
+ units: str = "",
46
+ unit_string: str = "",
47
+ decimals: int = -1,
48
+ ) -> None:
49
+ """Render a colormap to a PNG image.
50
+
51
+ Parameters
52
+ ----------
53
+ cmap : matplotlib.colors.Colormap
54
+ Colormap to render.
55
+ file_name : str
56
+ Output PNG path.
57
+ orientation : str
58
+ ``"horizontal"`` or ``"vertical"``.
59
+ vmin, vmax : float
60
+ Value range for the colour scale.
61
+ legend_label : str
62
+ Label shown alongside the colorbar.
63
+ """
64
+ if orientation == "horizontal":
65
+ fig = plt.figure(figsize=(2.5, 1))
66
+ ax = fig.add_axes([0.05, 0.80, 0.9, 0.15])
67
+ else:
68
+ fig = plt.figure(figsize=(1, 2.5))
69
+ ax = fig.add_axes([0.80, 0.05, 0.15, 0.90])
70
+
71
+ norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
72
+ cb = mpl.colorbar.ColorbarBase(
73
+ ax, cmap=cmap, norm=norm, orientation=orientation, label=legend_label
74
+ )
75
+ cb.ax.tick_params(labelsize=6)
76
+
77
+ fig.savefig(file_name, dpi=150, bbox_inches="tight")
78
+ plt.close(fig)
79
+
80
+
81
+ def read_colormap(file_name: str) -> np.ndarray:
82
+ """Read a colormap from a whitespace-separated RGB text file.
83
+
84
+ Parameters
85
+ ----------
86
+ file_name : str
87
+ Path to the colormap file.
88
+
89
+ Returns
90
+ -------
91
+ np.ndarray
92
+ Array of shape ``(N, 3)`` with RGB values.
93
+ """
94
+ df = pd.read_csv(
95
+ file_name,
96
+ index_col=False,
97
+ header=None,
98
+ sep=r"\s+",
99
+ names=["r", "g", "b"],
100
+ )
101
+ return df.to_numpy()
102
+
103
+
104
+ def rgb2hex(rgb: Tuple[int, int, int]) -> str:
105
+ """Convert an RGB tuple to a hex colour string.
106
+
107
+ Parameters
108
+ ----------
109
+ rgb : Tuple[int, int, int]
110
+ Red, green, blue values (0-255).
111
+
112
+ Returns
113
+ -------
114
+ str
115
+ Six-character hex string (no leading ``#``).
116
+ """
117
+ return "%02x%02x%02x" % rgb
@@ -0,0 +1,21 @@
1
+ """File I/O utilities for various Deltares and common data formats."""
2
+
3
+ from cht_utils.fileio.deltares_ini import IniStruct as IniStruct
4
+ from cht_utils.fileio.deltares_ini import Keyword as Keyword
5
+ from cht_utils.fileio.deltares_ini import Section as Section
6
+ from cht_utils.fileio.json_js import read_json_js as read_json_js
7
+ from cht_utils.fileio.json_js import write_csv_js as write_csv_js
8
+ from cht_utils.fileio.json_js import write_json_js as write_json_js
9
+ from cht_utils.fileio.pli_file import gdf2pli as gdf2pli
10
+ from cht_utils.fileio.pli_file import gdf2pol as gdf2pol
11
+ from cht_utils.fileio.pli_file import pli2gdf as pli2gdf
12
+ from cht_utils.fileio.pli_file import pol2gdf as pol2gdf
13
+ from cht_utils.fileio.pli_file import read_pli_file as read_pli_file
14
+ from cht_utils.fileio.tekal import tekal as tekal
15
+ from cht_utils.fileio.tekal import tekalblock as tekalblock
16
+ from cht_utils.fileio.xml import dict2xml as dict2xml
17
+ from cht_utils.fileio.xml import get_value as get_xml_value # noqa: F401
18
+ from cht_utils.fileio.xml import obj2xml as obj2xml
19
+ from cht_utils.fileio.xml import xml2obj as xml2obj
20
+ from cht_utils.fileio.yaml import dict2yaml as dict2yaml
21
+ from cht_utils.fileio.yaml import yaml2dict as yaml2dict