From fe5aeb52e44294c073ef9651aea70f3df612c59e Mon Sep 17 00:00:00 2001 From: Stevenray Janke Date: Wed, 24 Apr 2024 17:05:05 -0400 Subject: [PATCH 1/5] plan plan.py organization --- src/rashdf/plan.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/rashdf/plan.py b/src/rashdf/plan.py index 3e007ed..3a072a2 100644 --- a/src/rashdf/plan.py +++ b/src/rashdf/plan.py @@ -1,9 +1,31 @@ from .geom import RasGeomHdf - +from .utils import * +from typing import Dict from geopandas import GeoDataFrame class RasPlanHdf(RasGeomHdf): + def get_plan_attrs(self) -> Dict: + raise NotImplementedError + + def get_plan_info_attrs(self) -> Dict: + raise NotImplementedError + + def get_plan_param_attrs(self) -> Dict: + raise NotImplementedError + + def get_meteorology_precip_attrs(self) -> Dict: + raise NotImplementedError + + def get_results_unsteady_attrs(self) -> Dict: + raise NotImplementedError + + def get_results_summary_attrs(self) -> Dict: + raise NotImplementedError + + def get_results_volume_accounting_attrs(self) -> Dict: + raise NotImplementedError + def enroachment_points(self) -> GeoDataFrame: raise NotImplementedError From bccd086b4236dc9a98a3b1f1ddc1b32b2821a25d Mon Sep 17 00:00:00 2001 From: Stevenray Janke Date: Mon, 29 Apr 2024 15:21:57 -0400 Subject: [PATCH 2/5] Add attribute retrieval functions --- src/rashdf/base.py | 39 +++++++++++++++-- src/rashdf/geom.py | 103 +++++++++++++++++++++++++++++++++----------- src/rashdf/plan.py | 68 ++++++++++++++++++++++++----- src/rashdf/utils.py | 53 +++++++++++++++++++++-- 4 files changed, 221 insertions(+), 42 deletions(-) diff --git a/src/rashdf/base.py b/src/rashdf/base.py index 4b1aaab..6a3544a 100644 --- a/src/rashdf/base.py +++ b/src/rashdf/base.py @@ -1,4 +1,6 @@ import h5py +from .utils import hdf5_attrs_to_dict +from typing import Dict class RasHdf(h5py.File): @@ -6,7 +8,7 @@ class RasHdf(h5py.File): def __init__(self, name: str, **kwargs): """Open a HEC-RAS HDF file. - + Parameters ---------- name : str @@ -14,10 +16,10 @@ def __init__(self, name: str, **kwargs): kwargs : dict Additional keyword arguments to pass to h5py.File """ - super().__init__(name, mode="r", **kwargs) + super().__init__(name, **kwargs) @classmethod - def open_uri(cls, uri: str, fsspec_kwargs: dict = {}, h5py_kwargs: dict = {}) -> 'RasHdf': + def open_uri(cls, uri: str, fsspec_kwargs: dict = {}, h5py_kwargs: dict = {}) -> "RasHdf": """Open a HEC-RAS HDF file from a URI. Parameters @@ -41,5 +43,34 @@ def open_uri(cls, uri: str, fsspec_kwargs: dict = {}, h5py_kwargs: dict = {}) -> >>> results_hdf = RasHdf.open_uri("s3://my-bucket/results.hdf") """ import fsspec + remote_file = fsspec.open(uri, mode="rb", **fsspec_kwargs) - return cls(remote_file.open(), **h5py_kwargs) \ No newline at end of file + return cls(remote_file.open(), **h5py_kwargs) + + def get_attrs(self, attr_path: str) -> Dict: + """Convert attributes from a HEC-RAS HDF file into a Python dictionary for a given attribute path. + + Parameters + ---------- + attr_path (str): The path within the HEC-RAS HDF file where the desired attributes are located (Ex. "Plan Data/Plan Parameters"). + + Returns + ------- + plan_attrs (dict): Dictionary filled with attributes at given path, if attributes exist at that path. + """ + attr_object = self.get(attr_path) + + if attr_object: + return hdf5_attrs_to_dict(attr_object.attrs) + + return {} + + def get_root_attrs(self): + """Returns attributes at root level of HEC-RAS HDF file. + + Returns + ------- + dict + Dictionary filled with HEC-RAS HDF root attributes. + """ + return self.get_attrs("/") diff --git a/src/rashdf/geom.py b/src/rashdf/geom.py index 5209655..b2ea5a7 100644 --- a/src/rashdf/geom.py +++ b/src/rashdf/geom.py @@ -1,5 +1,5 @@ from .base import RasHdf -from .utils import convert_ras_hdf_string +from .utils import convert_ras_hdf_string, get_first_hdf_group, hdf5_attrs_to_dict import numpy as np import pandas as pd @@ -12,10 +12,16 @@ class RasGeomHdf(RasHdf): + def __init__(self, name: str): + super().__init__(name) + self.geom_path = "Geometry" + self.geom_structures_path = "Geometry/Structures" + self.flow_area_2d_path = "Geometry/2D Flow Areas" + def projection(self) -> Optional[CRS]: """Return the projection of the RAS geometry as a pyproj.CRS object. - + Returns ------- CRS @@ -27,11 +33,11 @@ def projection(self) -> Optional[CRS]: if type(proj_wkt) == bytes or type(proj_wkt) == np.bytes_: proj_wkt = proj_wkt.decode("utf-8") return CRS.from_wkt(proj_wkt) - + def mesh_area_names(self) -> list: - """Return a list of the 2D mesh area names of + """Return a list of the 2D mesh area names of the RAS geometry. - + Returns ------- list @@ -43,7 +49,7 @@ def mesh_area_names(self) -> list: def mesh_areas(self) -> GeoDataFrame: """Return 2D flow area perimeter polygons. - + Returns ------- GeoDataFrame @@ -53,11 +59,13 @@ def mesh_areas(self) -> GeoDataFrame: if not mesh_area_names: return GeoDataFrame() mesh_area_polygons = [Polygon(self[f"/Geometry/2D Flow Areas/{n}/Perimeter"][()]) for n in mesh_area_names] - return GeoDataFrame({"mesh_name" : mesh_area_names, "geometry" : mesh_area_polygons}, geometry="geometry", crs=self.projection()) + return GeoDataFrame( + {"mesh_name": mesh_area_names, "geometry": mesh_area_polygons}, geometry="geometry", crs=self.projection() + ) def mesh_cell_polygons(self) -> GeoDataFrame: """Return the 2D flow mesh cell polygons. - + Returns ------- GeoDataFrame @@ -69,30 +77,38 @@ def mesh_cell_polygons(self) -> GeoDataFrame: face_gdf = self.mesh_cell_faces() - cell_dict = {"mesh_name":[], "cell_id":[], "geometry":[]} + cell_dict = {"mesh_name": [], "cell_id": [], "geometry": []} for i, mesh_name in enumerate(mesh_area_names): cell_cnt = self["/Geometry/2D Flow Areas/Cell Info"][()][i][1] cell_ids = list(range(cell_cnt)) cell_face_info = self[f"/Geometry/2D Flow Areas/{mesh_name}/Cells Face and Orientation Info"][()] - cell_face_values = self[f"/Geometry/2D Flow Areas/{mesh_name}/Cells Face and Orientation Values"][()][:,0] + cell_face_values = self[f"/Geometry/2D Flow Areas/{mesh_name}/Cells Face and Orientation Values"][()][:, 0] face_id_lists = list( np.vectorize( - lambda cell_id: str(cell_face_values[cell_face_info[cell_id][0]:cell_face_info[cell_id][0]+cell_face_info[cell_id][1]]) + lambda cell_id: str( + cell_face_values[ + cell_face_info[cell_id][0] : cell_face_info[cell_id][0] + cell_face_info[cell_id][1] + ] + ) )(cell_ids) ) - mesh_faces = face_gdf[face_gdf.mesh_name == mesh_name][["face_id", "geometry"]].set_index("face_id").to_numpy() - cell_dict["mesh_name"] += [mesh_name]*cell_cnt + mesh_faces = ( + face_gdf[face_gdf.mesh_name == mesh_name][["face_id", "geometry"]].set_index("face_id").to_numpy() + ) + cell_dict["mesh_name"] += [mesh_name] * cell_cnt cell_dict["cell_id"] += cell_ids cell_dict["geometry"] += list( np.vectorize( - lambda face_id_list: polygonize(np.ravel(mesh_faces[np.array(face_id_list.strip("[]").split()).astype(int)])).geoms[0] + lambda face_id_list: polygonize( + np.ravel(mesh_faces[np.array(face_id_list.strip("[]").split()).astype(int)]) + ).geoms[0] )(face_id_lists) ) return GeoDataFrame(cell_dict, geometry="geometry", crs=self.projection()) def mesh_cell_points(self) -> GeoDataFrame: """Return the 2D flow mesh cell points. - + Returns ------- GeoDataFrame @@ -101,18 +117,20 @@ def mesh_cell_points(self) -> GeoDataFrame: mesh_area_names = self.mesh_area_names() if not mesh_area_names: return GeoDataFrame() - pnt_dict = {"mesh_name":[], "cell_id":[], "geometry":[]} + pnt_dict = {"mesh_name": [], "cell_id": [], "geometry": []} for i, mesh_name in enumerate(mesh_area_names): starting_row, count = self["/Geometry/2D Flow Areas/Cell Info"][()][i] - cell_pnt_coords = self["/Geometry/2D Flow Areas/Cell Points"][()][starting_row:starting_row+count] - pnt_dict["mesh_name"] += [mesh_name]*cell_pnt_coords.shape[0] + cell_pnt_coords = self["/Geometry/2D Flow Areas/Cell Points"][()][starting_row : starting_row + count] + pnt_dict["mesh_name"] += [mesh_name] * cell_pnt_coords.shape[0] pnt_dict["cell_id"] += range(count) - pnt_dict["geometry"] += list(np.vectorize(lambda coords: Point(coords), signature="(n)->()")(cell_pnt_coords)) + pnt_dict["geometry"] += list( + np.vectorize(lambda coords: Point(coords), signature="(n)->()")(cell_pnt_coords) + ) return GeoDataFrame(pnt_dict, geometry="geometry", crs=self.projection()) def mesh_cell_faces(self) -> GeoDataFrame: """Return the 2D flow mesh cell faces. - + Returns ------- GeoDataFrame @@ -121,7 +139,7 @@ def mesh_cell_faces(self) -> GeoDataFrame: mesh_area_names = self.mesh_area_names() if not mesh_area_names: return GeoDataFrame() - face_dict = {"mesh_name":[], "face_id":[], "geometry":[]} + face_dict = {"mesh_name": [], "face_id": [], "geometry": []} for mesh_name in mesh_area_names: facepoints_index = self[f"/Geometry/2D Flow Areas/{mesh_name}/Faces FacePoint Indexes"][()] facepoints_coordinates = self[f"/Geometry/2D Flow Areas/{mesh_name}/FacePoints Coordinate"][()] @@ -129,18 +147,55 @@ def mesh_cell_faces(self) -> GeoDataFrame: faces_perimeter_values = self[f"/Geometry/2D Flow Areas/{mesh_name}/Faces Perimeter Values"][()] face_id = -1 for pnt_a_index, pnt_b_index in facepoints_index: - face_id+=1 + face_id += 1 face_dict["mesh_name"].append(mesh_name) face_dict["face_id"].append(face_id) coordinates = list() coordinates.append(facepoints_coordinates[pnt_a_index]) starting_row, count = faces_perimeter_info[face_id] if count > 0: - coordinates += list(faces_perimeter_values[starting_row:starting_row+count]) + coordinates += list(faces_perimeter_values[starting_row : starting_row + count]) coordinates.append(facepoints_coordinates[pnt_b_index]) face_dict["geometry"].append(LineString(coordinates)) return GeoDataFrame(face_dict, geometry="geometry", crs=self.projection()) + def get_geom_attrs(self): + """Returns base geometry attributes from a HEC-RAS HDF geom file. + + Returns + ------- + dict + Dictionary filled with base geometry attributes. + """ + return self.get_attrs(self.geom_path) + + def get_geom_structures_attrs(self): + """Returns geometry structures attributes from a HEC-RAS HDF geom file. + + Returns + ------- + dict + Dictionary filled with geometry structures attributes. + """ + return self.get_attrs(self.geom_structures_path) + + def get_geom_2d_flow_area_attrs(self): + """Returns geometry 2d flow area attributes from a HEC-RAS HDF geom file. + + Returns + ------- + dict + Dictionary filled with geometry 2d flow area attributes. + """ + try: + d2_flow_area = get_first_hdf_group(self.get(self.flow_area_2d_path)) + except AttributeError: + raise AttributeError(f"Unable to get 2D Flow Area; {self.flow_area_2d_path} group not found in HDF5 file.") + + d2_flow_area_attrs = hdf5_attrs_to_dict(d2_flow_area.attrs) + + return d2_flow_area_attrs + def bc_lines(self) -> GeoDataFrame: raise NotImplementedError @@ -188,7 +243,7 @@ def flowpaths(self) -> GeoDataFrame: def bank_points(self) -> GeoDataFrame: raise NotImplementedError - + def bank_lines(self) -> GeoDataFrame: raise NotImplementedError diff --git a/src/rashdf/plan.py b/src/rashdf/plan.py index 3a072a2..cb6acdc 100644 --- a/src/rashdf/plan.py +++ b/src/rashdf/plan.py @@ -1,31 +1,79 @@ from .geom import RasGeomHdf -from .utils import * +from .base import RasHdf from typing import Dict from geopandas import GeoDataFrame -class RasPlanHdf(RasGeomHdf): +class RasPlanHdf(RasHdf): - def get_plan_attrs(self) -> Dict: - raise NotImplementedError + def __init__(self, name: str): + super().__init__(name) + self.plan_info_path = "Plan Data/Plan Information" + self.plan_params_path = "Plan Data/Plan Parameters" + self.meteorology_precip_path = "Event Conditions/Meteorology/Precipitation" + self.results_unsteady_path = "Results/Unsteady" + self.results_summary_path = "Results/Unsteady/Summary" + self.volume_accounting_path = "Results/Unsteady/Summary/Volume Accounting" def get_plan_info_attrs(self) -> Dict: - raise NotImplementedError + """Returns plan information attributes from a HEC-RAS HDF plan file. + + Returns + ------- + dict + Dictionary filled with plan information attributes. + """ + return self.get_attrs(self.plan_info_path) def get_plan_param_attrs(self) -> Dict: - raise NotImplementedError + """Returns plan parameter attributes from a HEC-RAS HDF plan file. + + Returns + ------- + dict + Dictionary filled with plan parameter attributes. + """ + return self.get_attrs(self.plan_params_path) def get_meteorology_precip_attrs(self) -> Dict: - raise NotImplementedError + """Returns precipitation attributes from a HEC-RAS HDF plan file. + + Returns + ------- + dict + Dictionary filled with precipitation attributes. + """ + return self.get_attrs(self.meteorology_precip_path) def get_results_unsteady_attrs(self) -> Dict: - raise NotImplementedError + """Returns unsteady attributes from a HEC-RAS HDF plan file. + + Returns + ------- + dict + Dictionary filled with unsteady attributes. + """ + return self.get_attrs(self.results_unsteady_path) def get_results_summary_attrs(self) -> Dict: - raise NotImplementedError + """Returns results summary attributes from a HEC-RAS HDF plan file. + + Returns + ------- + dict + Dictionary filled with results summary attributes. + """ + return self.get_attrs(self.results_summary_path) def get_results_volume_accounting_attrs(self) -> Dict: - raise NotImplementedError + """Returns volume accounting attributes from a HEC-RAS HDF plan file. + + Returns + ------- + dict + Dictionary filled with volume accounting attributes. + """ + return self.get_attrs(self.volume_accounting_path) def enroachment_points(self) -> GeoDataFrame: raise NotImplementedError diff --git a/src/rashdf/utils.py b/src/rashdf/utils.py index 218ed28..7eeae60 100644 --- a/src/rashdf/utils.py +++ b/src/rashdf/utils.py @@ -1,6 +1,6 @@ import numpy as np - -from typing import Any, List, Tuple, Union +import h5py +from typing import Any, List, Tuple, Union, Optional from datetime import datetime, timedelta import re @@ -40,7 +40,7 @@ def parse_ras_simulation_window_datetime(datetime_str) -> datetime: def parse_run_time_window(window: str) -> Tuple[datetime, datetime]: """ Parse a run time window string into a tuple of datetime objects. - + Parameters ---------- window (str): The run time window string to be parsed. @@ -167,4 +167,49 @@ def convert_ras_hdf_value( # Convert all other types to string else: - return str(value) \ No newline at end of file + return str(value) + + +def hdf5_attrs_to_dict(attrs: dict, prefix: str = None) -> dict: + """ + Convert a dictionary of attributes from an HDF5 file into a Python dictionary. + + Parameters: + ---------- + attrs (dict): The attributes to be converted. + prefix (str, optional): An optional prefix to prepend to the keys. + + Returns: + ---------- + dict: A dictionary with the converted attributes. + """ + results = {} + for k, v in attrs.items(): + value = convert_ras_hdf_value(v) + if prefix: + key = f"{prefix}:{k}" + else: + key = k + results[key] = value + return results + + +def get_first_hdf_group(parent_group: h5py.Group) -> Optional[h5py.Group]: + """ + Get the first HDF5 group from a parent group. + + This function iterates over the items in the parent group and returns the first item that is an instance of + h5py.Group. If no such item is found, it returns None. + + Parameters: + ---------- + parent_group (h5py.Group): The parent group to search in. + + Returns: + ---------- + Optional[h5py.Group]: The first HDF5 group in the parent group, or None if no group is found. + """ + for _, item in parent_group.items(): + if isinstance(item, h5py.Group): + return item + return None From 3c936e3ff4971b7268d9d82f43e288f0cd362620 Mon Sep 17 00:00:00 2001 From: Stevenray Janke Date: Tue, 30 Apr 2024 16:23:17 -0400 Subject: [PATCH 3/5] Add unit tests, move constants to class properties --- src/rashdf/base.py | 2 +- src/rashdf/geom.py | 18 +++++------ src/rashdf/plan.py | 28 ++++++++--------- tests/test_geom.py | 47 +++++++++++++++++++++++++++- tests/test_plan.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 tests/test_plan.py diff --git a/src/rashdf/base.py b/src/rashdf/base.py index 6a3544a..e57a723 100644 --- a/src/rashdf/base.py +++ b/src/rashdf/base.py @@ -16,7 +16,7 @@ def __init__(self, name: str, **kwargs): kwargs : dict Additional keyword arguments to pass to h5py.File """ - super().__init__(name, **kwargs) + super().__init__(name, mode="r", **kwargs) @classmethod def open_uri(cls, uri: str, fsspec_kwargs: dict = {}, h5py_kwargs: dict = {}) -> "RasHdf": diff --git a/src/rashdf/geom.py b/src/rashdf/geom.py index b2ea5a7..8d1a0a4 100644 --- a/src/rashdf/geom.py +++ b/src/rashdf/geom.py @@ -11,12 +11,12 @@ class RasGeomHdf(RasHdf): + GEOM_PATH = "Geometry" + GEOM_STRUCTURES_PATH = f"{GEOM_PATH}/Structures" + FLOW_AREA_2D_PATH = f"{GEOM_PATH}/2D Flow Areas" - def __init__(self, name: str): - super().__init__(name) - self.geom_path = "Geometry" - self.geom_structures_path = "Geometry/Structures" - self.flow_area_2d_path = "Geometry/2D Flow Areas" + def __init__(self, name: str, **kwargs): + super().__init__(name, **kwargs) def projection(self) -> Optional[CRS]: """Return the projection of the RAS geometry as a @@ -167,7 +167,7 @@ def get_geom_attrs(self): dict Dictionary filled with base geometry attributes. """ - return self.get_attrs(self.geom_path) + return self.get_attrs(self.GEOM_PATH) def get_geom_structures_attrs(self): """Returns geometry structures attributes from a HEC-RAS HDF geom file. @@ -177,7 +177,7 @@ def get_geom_structures_attrs(self): dict Dictionary filled with geometry structures attributes. """ - return self.get_attrs(self.geom_structures_path) + return self.get_attrs(self.GEOM_STRUCTURES_PATH) def get_geom_2d_flow_area_attrs(self): """Returns geometry 2d flow area attributes from a HEC-RAS HDF geom file. @@ -188,9 +188,9 @@ def get_geom_2d_flow_area_attrs(self): Dictionary filled with geometry 2d flow area attributes. """ try: - d2_flow_area = get_first_hdf_group(self.get(self.flow_area_2d_path)) + d2_flow_area = get_first_hdf_group(self.get(self.FLOW_AREA_2D_PATH)) except AttributeError: - raise AttributeError(f"Unable to get 2D Flow Area; {self.flow_area_2d_path} group not found in HDF5 file.") + raise AttributeError(f"Unable to get 2D Flow Area; {self.FLOW_AREA_2D_PATH} group not found in HDF5 file.") d2_flow_area_attrs = hdf5_attrs_to_dict(d2_flow_area.attrs) diff --git a/src/rashdf/plan.py b/src/rashdf/plan.py index cb6acdc..880808e 100644 --- a/src/rashdf/plan.py +++ b/src/rashdf/plan.py @@ -5,15 +5,15 @@ class RasPlanHdf(RasHdf): + PLAN_INFO_PATH = "Plan Data/Plan Information" + PLAN_PARAMS_PATH = "Plan Data/Plan Parameters" + PRECIP_PATH = "Event Conditions/Meteorology/Precipitation" + RESULTS_UNSTEADY_PATH = "Results/Unsteady" + RESULTS_SUMMARY_PATH = f"{RESULTS_UNSTEADY_PATH}/Summary" + VOLUME_ACCOUNTING_PATH = f"{RESULTS_UNSTEADY_PATH}/Volume Accounting" - def __init__(self, name: str): - super().__init__(name) - self.plan_info_path = "Plan Data/Plan Information" - self.plan_params_path = "Plan Data/Plan Parameters" - self.meteorology_precip_path = "Event Conditions/Meteorology/Precipitation" - self.results_unsteady_path = "Results/Unsteady" - self.results_summary_path = "Results/Unsteady/Summary" - self.volume_accounting_path = "Results/Unsteady/Summary/Volume Accounting" + def __init__(self, name: str, **kwargs): + super().__init__(name, **kwargs) def get_plan_info_attrs(self) -> Dict: """Returns plan information attributes from a HEC-RAS HDF plan file. @@ -23,7 +23,7 @@ def get_plan_info_attrs(self) -> Dict: dict Dictionary filled with plan information attributes. """ - return self.get_attrs(self.plan_info_path) + return self.get_attrs(self.PLAN_INFO_PATH) def get_plan_param_attrs(self) -> Dict: """Returns plan parameter attributes from a HEC-RAS HDF plan file. @@ -33,7 +33,7 @@ def get_plan_param_attrs(self) -> Dict: dict Dictionary filled with plan parameter attributes. """ - return self.get_attrs(self.plan_params_path) + return self.get_attrs(self.PLAN_PARAMS_PATH) def get_meteorology_precip_attrs(self) -> Dict: """Returns precipitation attributes from a HEC-RAS HDF plan file. @@ -43,7 +43,7 @@ def get_meteorology_precip_attrs(self) -> Dict: dict Dictionary filled with precipitation attributes. """ - return self.get_attrs(self.meteorology_precip_path) + return self.get_attrs(self.PRECIP_PATH) def get_results_unsteady_attrs(self) -> Dict: """Returns unsteady attributes from a HEC-RAS HDF plan file. @@ -53,7 +53,7 @@ def get_results_unsteady_attrs(self) -> Dict: dict Dictionary filled with unsteady attributes. """ - return self.get_attrs(self.results_unsteady_path) + return self.get_attrs(self.RESULTS_UNSTEADY_PATH) def get_results_summary_attrs(self) -> Dict: """Returns results summary attributes from a HEC-RAS HDF plan file. @@ -63,7 +63,7 @@ def get_results_summary_attrs(self) -> Dict: dict Dictionary filled with results summary attributes. """ - return self.get_attrs(self.results_summary_path) + return self.get_attrs(self.RESULTS_SUMMARY_PATH) def get_results_volume_accounting_attrs(self) -> Dict: """Returns volume accounting attributes from a HEC-RAS HDF plan file. @@ -73,7 +73,7 @@ def get_results_volume_accounting_attrs(self) -> Dict: dict Dictionary filled with volume accounting attributes. """ - return self.get_attrs(self.volume_accounting_path) + return self.get_attrs(self.VOLUME_ACCOUNTING_PATH) def enroachment_points(self) -> GeoDataFrame: raise NotImplementedError diff --git a/tests/test_geom.py b/tests/test_geom.py index 13342b2..f8dfd74 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -6,6 +6,7 @@ TEST_DATA = Path("./tests/data") + def test_projection(tmp_path): wkt = 'PROJCS["Albers_Conic_Equal_Area",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Albers"],PARAMETER["false_easting",0.0],PARAMETER["false_northing",0.0],PARAMETER["central_meridian",-96.0],PARAMETER["standard_parallel_1",29.5],PARAMETER["standard_parallel_2",45.5],PARAMETER["latitude_of_origin",37.5],UNIT["Foot_US",0.3048006096012192]]' # Create a dummy HDF file @@ -16,31 +17,75 @@ def test_projection(tmp_path): # Test the projection assert ras_hdf.projection() == CRS.from_wkt(wkt) + def test_mesh_area_names(): geom = TEST_DATA / "ras/Muncie.g05.hdf" with RasGeomHdf(geom) as ghdf: assert ghdf.mesh_area_names() == ["2D Interior Area", "Perimeter_NW"] + def test_mesh_areas(): geom = TEST_DATA / "ras/Muncie.g05.hdf" with RasGeomHdf(geom) as ghdf: with open(TEST_DATA / "json/mesh_areas.json") as json: assert ghdf.mesh_areas().to_json() == json.read() + def test_mesh_cell_faces(): geom = TEST_DATA / "ras/Muncie.g05.hdf" with RasGeomHdf(geom) as ghdf: with open(TEST_DATA / "json/mesh_cell_faces.json") as json: assert ghdf.mesh_cell_faces().to_json() == json.read() + def test_mesh_cell_points(): geom = TEST_DATA / "ras/Muncie.g05.hdf" with RasGeomHdf(geom) as ghdf: with open(TEST_DATA / "json/mesh_cell_points.json") as json: assert ghdf.mesh_cell_points().to_json() == json.read() + def test_mesh_cell_polygons(): geom = TEST_DATA / "ras/Muncie.g05.hdf" with RasGeomHdf(geom) as ghdf: with open(TEST_DATA / "json/mesh_cell_polygons.json") as json: - assert ghdf.mesh_cell_polygons().to_json() == json.read() \ No newline at end of file + assert ghdf.mesh_cell_polygons().to_json() == json.read() + + +def test_get_geom_attrs(tmp_path): + attrs_to_set = {"test_attribute1": "test_str1", "test_attribute2": 500} + + with h5py.File(tmp_path / "test.hdf", "w") as f: + geom_group = f.create_group(RasGeomHdf.GEOM_PATH) + for key, value in attrs_to_set.items(): + geom_group.attrs[key] = value + + ras_hdf = RasGeomHdf(tmp_path / "test.hdf") + + assert ras_hdf.get_geom_attrs() == attrs_to_set + + +def test_get_geom_structures_attrs(tmp_path): + attrs_to_set = {"test_attribute1": "test_str1", "test_attribute2": 500} + + with h5py.File(tmp_path / "test.hdf", "w") as f: + structures_group = f.create_group(RasGeomHdf.GEOM_STRUCTURES_PATH) + for key, value in attrs_to_set.items(): + structures_group.attrs[key] = value + + ras_hdf = RasGeomHdf(tmp_path / "test.hdf") + + assert ras_hdf.get_geom_structures_attrs() == attrs_to_set + + +def test_get_geom_2d_flow_area_attrs(tmp_path): + attrs_to_set = {"test_attribute1": "test_str1", "test_attribute2": 500} + + with h5py.File(tmp_path / "test.hdf", "w") as f: + flow_area_group = f.create_group(f"{RasGeomHdf.FLOW_AREA_2D_PATH}/group") + for key, value in attrs_to_set.items(): + flow_area_group.attrs[key] = value + + ras_hdf = RasGeomHdf(tmp_path / "test.hdf") + + assert ras_hdf.get_geom_2d_flow_area_attrs() == attrs_to_set diff --git a/tests/test_plan.py b/tests/test_plan.py new file mode 100644 index 0000000..4f97aca --- /dev/null +++ b/tests/test_plan.py @@ -0,0 +1,77 @@ +from src.rashdf import RasPlanHdf + +import h5py + +attrs_to_set = {"test_attribute1": "test_str1", "test_attribute2": 500} + + +def test_get_plan_info_attrs(tmp_path): + + with h5py.File(tmp_path / "test.hdf", "w") as f: + group = f.create_group(RasPlanHdf.PLAN_INFO_PATH) + for key, value in attrs_to_set.items(): + group.attrs[key] = value + + ras_plan_hdf = RasPlanHdf(tmp_path / "test.hdf") + + assert ras_plan_hdf.get_plan_info_attrs() == attrs_to_set + + +def test_get_plan_param_attrs(tmp_path): + + with h5py.File(tmp_path / "test.hdf", "w") as f: + group = f.create_group(RasPlanHdf.PLAN_PARAMS_PATH) + for key, value in attrs_to_set.items(): + group.attrs[key] = value + + ras_plan_hdf = RasPlanHdf(tmp_path / "test.hdf") + + assert ras_plan_hdf.get_plan_param_attrs() == attrs_to_set + + +def test_get_meteorology_precip_attrs(tmp_path): + + with h5py.File(tmp_path / "test.hdf", "w") as f: + group = f.create_group(RasPlanHdf.PRECIP_PATH) + for key, value in attrs_to_set.items(): + group.attrs[key] = value + + ras_plan_hdf = RasPlanHdf(tmp_path / "test.hdf") + + assert ras_plan_hdf.get_meteorology_precip_attrs() == attrs_to_set + + +def test_get_results_unsteady_attrs(tmp_path): + + with h5py.File(tmp_path / "test.hdf", "w") as f: + group = f.create_group(RasPlanHdf.RESULTS_UNSTEADY_PATH) + for key, value in attrs_to_set.items(): + group.attrs[key] = value + + ras_plan_hdf = RasPlanHdf(tmp_path / "test.hdf") + + assert ras_plan_hdf.get_results_unsteady_attrs() == attrs_to_set + + +def test_get_results_summary_attrs(tmp_path): + + with h5py.File(tmp_path / "test.hdf", "w") as f: + group = f.create_group(RasPlanHdf.RESULTS_SUMMARY_PATH) + for key, value in attrs_to_set.items(): + group.attrs[key] = value + + ras_plan_hdf = RasPlanHdf(tmp_path / "test.hdf") + + assert ras_plan_hdf.get_results_summary_attrs() == attrs_to_set + + +def test_get_results_volume_accounting_attrs(tmp_path): + + with h5py.File(tmp_path / "test.hdf", "w") as f: + group = f.create_group(RasPlanHdf.VOLUME_ACCOUNTING_PATH) + for key, value in attrs_to_set.items(): + group.attrs[key] = value + + ras_plan_hdf = RasPlanHdf(tmp_path / "test.hdf") + + assert ras_plan_hdf.get_results_volume_accounting_attrs() == attrs_to_set From b89318595cf5712f164f2a39f10cadaead4bb552 Mon Sep 17 00:00:00 2001 From: Stevenray Janke Date: Wed, 1 May 2024 10:16:02 -0400 Subject: [PATCH 4/5] Change RasPlanHdf inherited class to RasGeomHdf, name changes --- src/rashdf/geom.py | 6 +++--- src/rashdf/plan.py | 11 +++++------ tests/test_plan.py | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/rashdf/geom.py b/src/rashdf/geom.py index 8d1a0a4..e47b84a 100644 --- a/src/rashdf/geom.py +++ b/src/rashdf/geom.py @@ -160,7 +160,7 @@ def mesh_cell_faces(self) -> GeoDataFrame: return GeoDataFrame(face_dict, geometry="geometry", crs=self.projection()) def get_geom_attrs(self): - """Returns base geometry attributes from a HEC-RAS HDF geom file. + """Returns base geometry attributes from a HEC-RAS HDF file. Returns ------- @@ -170,7 +170,7 @@ def get_geom_attrs(self): return self.get_attrs(self.GEOM_PATH) def get_geom_structures_attrs(self): - """Returns geometry structures attributes from a HEC-RAS HDF geom file. + """Returns geometry structures attributes from a HEC-RAS HDF file. Returns ------- @@ -180,7 +180,7 @@ def get_geom_structures_attrs(self): return self.get_attrs(self.GEOM_STRUCTURES_PATH) def get_geom_2d_flow_area_attrs(self): - """Returns geometry 2d flow area attributes from a HEC-RAS HDF geom file. + """Returns geometry 2d flow area attributes from a HEC-RAS HDF file. Returns ------- diff --git a/src/rashdf/plan.py b/src/rashdf/plan.py index 880808e..bd90cc6 100644 --- a/src/rashdf/plan.py +++ b/src/rashdf/plan.py @@ -1,15 +1,14 @@ from .geom import RasGeomHdf -from .base import RasHdf from typing import Dict from geopandas import GeoDataFrame -class RasPlanHdf(RasHdf): +class RasPlanHdf(RasGeomHdf): PLAN_INFO_PATH = "Plan Data/Plan Information" PLAN_PARAMS_PATH = "Plan Data/Plan Parameters" PRECIP_PATH = "Event Conditions/Meteorology/Precipitation" RESULTS_UNSTEADY_PATH = "Results/Unsteady" - RESULTS_SUMMARY_PATH = f"{RESULTS_UNSTEADY_PATH}/Summary" + RESULTS_UNSTEADY_SUMMARY_PATH = f"{RESULTS_UNSTEADY_PATH}/Summary" VOLUME_ACCOUNTING_PATH = f"{RESULTS_UNSTEADY_PATH}/Volume Accounting" def __init__(self, name: str, **kwargs): @@ -55,15 +54,15 @@ def get_results_unsteady_attrs(self) -> Dict: """ return self.get_attrs(self.RESULTS_UNSTEADY_PATH) - def get_results_summary_attrs(self) -> Dict: - """Returns results summary attributes from a HEC-RAS HDF plan file. + def get_results_unsteady_summary_attrs(self) -> Dict: + """Returns results unsteady summary attributes from a HEC-RAS HDF plan file. Returns ------- dict Dictionary filled with results summary attributes. """ - return self.get_attrs(self.RESULTS_SUMMARY_PATH) + return self.get_attrs(self.RESULTS_UNSTEADY_SUMMARY_PATH) def get_results_volume_accounting_attrs(self) -> Dict: """Returns volume accounting attributes from a HEC-RAS HDF plan file. diff --git a/tests/test_plan.py b/tests/test_plan.py index 4f97aca..6e1aec3 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -53,16 +53,16 @@ def test_get_results_unsteady_attrs(tmp_path): assert ras_plan_hdf.get_results_unsteady_attrs() == attrs_to_set -def test_get_results_summary_attrs(tmp_path): +def test_get_results_unsteady_summary_attrs(tmp_path): with h5py.File(tmp_path / "test.hdf", "w") as f: - group = f.create_group(RasPlanHdf.RESULTS_SUMMARY_PATH) + group = f.create_group(RasPlanHdf.RESULTS_UNSTEADY_SUMMARY_PATH) for key, value in attrs_to_set.items(): group.attrs[key] = value ras_plan_hdf = RasPlanHdf(tmp_path / "test.hdf") - assert ras_plan_hdf.get_results_summary_attrs() == attrs_to_set + assert ras_plan_hdf.get_results_unsteady_summary_attrs() == attrs_to_set def test_get_results_volume_accounting_attrs(tmp_path): From dd8128cd3ce1334093452b6eee6b47e188ed82f9 Mon Sep 17 00:00:00 2001 From: Stevenray Janke Date: Wed, 1 May 2024 17:10:35 -0400 Subject: [PATCH 5/5] Reformat --- src/rashdf/base.py | 4 +- src/rashdf/geom.py | 93 +++++++++++++++++++++++++++++++++++----------- tests/test_plan.py | 6 --- 3 files changed, 74 insertions(+), 29 deletions(-) diff --git a/src/rashdf/base.py b/src/rashdf/base.py index e57a723..15f3ddf 100644 --- a/src/rashdf/base.py +++ b/src/rashdf/base.py @@ -19,7 +19,9 @@ def __init__(self, name: str, **kwargs): super().__init__(name, mode="r", **kwargs) @classmethod - def open_uri(cls, uri: str, fsspec_kwargs: dict = {}, h5py_kwargs: dict = {}) -> "RasHdf": + def open_uri( + cls, uri: str, fsspec_kwargs: dict = {}, h5py_kwargs: dict = {} + ) -> "RasHdf": """Open a HEC-RAS HDF file from a URI. Parameters diff --git a/src/rashdf/geom.py b/src/rashdf/geom.py index d373e41..c67624c 100644 --- a/src/rashdf/geom.py +++ b/src/rashdf/geom.py @@ -51,7 +51,12 @@ def mesh_area_names(self) -> List[str]: """ if "/Geometry/2D Flow Areas" not in self: return list() - return list([convert_ras_hdf_string(n) for n in self["/Geometry/2D Flow Areas/Attributes"][()]["Name"]]) + return list( + [ + convert_ras_hdf_string(n) + for n in self["/Geometry/2D Flow Areas/Attributes"][()]["Name"] + ] + ) def mesh_areas(self) -> GeoDataFrame: """Return 2D flow area perimeter polygons. @@ -64,7 +69,10 @@ def mesh_areas(self) -> GeoDataFrame: mesh_area_names = self.mesh_area_names() if not mesh_area_names: return GeoDataFrame() - mesh_area_polygons = [Polygon(self[f"/Geometry/2D Flow Areas/{n}/Perimeter"][()]) for n in mesh_area_names] + mesh_area_polygons = [ + Polygon(self[f"/Geometry/2D Flow Areas/{n}/Perimeter"][()]) + for n in mesh_area_names + ] return GeoDataFrame( {"mesh_name": mesh_area_names, "geometry": mesh_area_polygons}, geometry="geometry", @@ -89,26 +97,37 @@ def mesh_cell_polygons(self) -> GeoDataFrame: for i, mesh_name in enumerate(mesh_area_names): cell_cnt = self["/Geometry/2D Flow Areas/Cell Info"][()][i][1] cell_ids = list(range(cell_cnt)) - cell_face_info = self[f"/Geometry/2D Flow Areas/{mesh_name}/Cells Face and Orientation Info"][()] - cell_face_values = self[f"/Geometry/2D Flow Areas/{mesh_name}/Cells Face and Orientation Values"][()][:, 0] + cell_face_info = self[ + f"/Geometry/2D Flow Areas/{mesh_name}/Cells Face and Orientation Info" + ][()] + cell_face_values = self[ + f"/Geometry/2D Flow Areas/{mesh_name}/Cells Face and Orientation Values" + ][()][:, 0] face_id_lists = list( np.vectorize( lambda cell_id: str( cell_face_values[ - cell_face_info[cell_id][0] : cell_face_info[cell_id][0] + cell_face_info[cell_id][1] + cell_face_info[cell_id][0] : cell_face_info[cell_id][0] + + cell_face_info[cell_id][1] ] ) )(cell_ids) ) mesh_faces = ( - face_gdf[face_gdf.mesh_name == mesh_name][["face_id", "geometry"]].set_index("face_id").to_numpy() + face_gdf[face_gdf.mesh_name == mesh_name][["face_id", "geometry"]] + .set_index("face_id") + .to_numpy() ) cell_dict["mesh_name"] += [mesh_name] * cell_cnt cell_dict["cell_id"] += cell_ids cell_dict["geometry"] += list( np.vectorize( lambda face_id_list: polygonize( - np.ravel(mesh_faces[np.array(face_id_list.strip("[]").split()).astype(int)]) + np.ravel( + mesh_faces[ + np.array(face_id_list.strip("[]").split()).astype(int) + ] + ) ).geoms[0] )(face_id_lists) ) @@ -128,11 +147,15 @@ def mesh_cell_points(self) -> GeoDataFrame: pnt_dict = {"mesh_name": [], "cell_id": [], "geometry": []} for i, mesh_name in enumerate(mesh_area_names): starting_row, count = self["/Geometry/2D Flow Areas/Cell Info"][()][i] - cell_pnt_coords = self["/Geometry/2D Flow Areas/Cell Points"][()][starting_row : starting_row + count] + cell_pnt_coords = self["/Geometry/2D Flow Areas/Cell Points"][()][ + starting_row : starting_row + count + ] pnt_dict["mesh_name"] += [mesh_name] * cell_pnt_coords.shape[0] pnt_dict["cell_id"] += range(count) pnt_dict["geometry"] += list( - np.vectorize(lambda coords: Point(coords), signature="(n)->()")(cell_pnt_coords) + np.vectorize(lambda coords: Point(coords), signature="(n)->()")( + cell_pnt_coords + ) ) return GeoDataFrame(pnt_dict, geometry="geometry", crs=self.projection()) @@ -149,10 +172,18 @@ def mesh_cell_faces(self) -> GeoDataFrame: return GeoDataFrame() face_dict = {"mesh_name": [], "face_id": [], "geometry": []} for mesh_name in mesh_area_names: - facepoints_index = self[f"/Geometry/2D Flow Areas/{mesh_name}/Faces FacePoint Indexes"][()] - facepoints_coordinates = self[f"/Geometry/2D Flow Areas/{mesh_name}/FacePoints Coordinate"][()] - faces_perimeter_info = self[f"/Geometry/2D Flow Areas/{mesh_name}/Faces Perimeter Info"][()] - faces_perimeter_values = self[f"/Geometry/2D Flow Areas/{mesh_name}/Faces Perimeter Values"][()] + facepoints_index = self[ + f"/Geometry/2D Flow Areas/{mesh_name}/Faces FacePoint Indexes" + ][()] + facepoints_coordinates = self[ + f"/Geometry/2D Flow Areas/{mesh_name}/FacePoints Coordinate" + ][()] + faces_perimeter_info = self[ + f"/Geometry/2D Flow Areas/{mesh_name}/Faces Perimeter Info" + ][()] + faces_perimeter_values = self[ + f"/Geometry/2D Flow Areas/{mesh_name}/Faces Perimeter Values" + ][()] face_id = -1 for pnt_a_index, pnt_b_index in facepoints_index: face_id += 1 @@ -162,7 +193,9 @@ def mesh_cell_faces(self) -> GeoDataFrame: coordinates.append(facepoints_coordinates[pnt_a_index]) starting_row, count = faces_perimeter_info[face_id] if count > 0: - coordinates += list(faces_perimeter_values[starting_row : starting_row + count]) + coordinates += list( + faces_perimeter_values[starting_row : starting_row + count] + ) coordinates.append(facepoints_coordinates[pnt_b_index]) face_dict["geometry"].append(LineString(coordinates)) return GeoDataFrame(face_dict, geometry="geometry", crs=self.projection()) @@ -198,7 +231,9 @@ def get_geom_2d_flow_area_attrs(self): try: d2_flow_area = get_first_hdf_group(self.get(self.FLOW_AREA_2D_PATH)) except AttributeError: - raise AttributeError(f"Unable to get 2D Flow Area; {self.FLOW_AREA_2D_PATH} group not found in HDF5 file.") + raise AttributeError( + f"Unable to get 2D Flow Area; {self.FLOW_AREA_2D_PATH} group not found in HDF5 file." + ) d2_flow_area_attrs = hdf5_attrs_to_dict(d2_flow_area.attrs) @@ -221,12 +256,18 @@ def bc_lines(self) -> GeoDataFrame: mesh_names = v_conv_str(bc_line_data["Attributes"][()]["SA-2D"]) types = v_conv_str(bc_line_data["Attributes"][()]["Type"]) geoms = list() - for pnt_start, pnt_cnt, part_start, part_cnt in bc_line_data["Polyline Info"][()]: - points = bc_line_data["Polyline Points"][()][pnt_start : pnt_start + pnt_cnt] + for pnt_start, pnt_cnt, part_start, part_cnt in bc_line_data["Polyline Info"][ + () + ]: + points = bc_line_data["Polyline Points"][()][ + pnt_start : pnt_start + pnt_cnt + ] if part_cnt == 1: geoms.append(LineString(points)) else: - parts = bc_line_data["Polyline Parts"][()][part_start : part_start + part_cnt] + parts = bc_line_data["Polyline Parts"][()][ + part_start : part_start + part_cnt + ] geoms.append( MultiLineString( list( @@ -259,14 +300,22 @@ def breaklines(self) -> GeoDataFrame: return GeoDataFrame() bl_line_data = self["/Geometry/2D Flow Area Break Lines"] bl_line_ids = range(bl_line_data["Attributes"][()].shape[0]) - names = np.vectorize(convert_ras_hdf_string)(bl_line_data["Attributes"][()]["Name"]) + names = np.vectorize(convert_ras_hdf_string)( + bl_line_data["Attributes"][()]["Name"] + ) geoms = list() - for pnt_start, pnt_cnt, part_start, part_cnt in bl_line_data["Polyline Info"][()]: - points = bl_line_data["Polyline Points"][()][pnt_start : pnt_start + pnt_cnt] + for pnt_start, pnt_cnt, part_start, part_cnt in bl_line_data["Polyline Info"][ + () + ]: + points = bl_line_data["Polyline Points"][()][ + pnt_start : pnt_start + pnt_cnt + ] if part_cnt == 1: geoms.append(LineString(points)) else: - parts = bl_line_data["Polyline Parts"][()][part_start : part_start + part_cnt] + parts = bl_line_data["Polyline Parts"][()][ + part_start : part_start + part_cnt + ] geoms.append( MultiLineString( list( diff --git a/tests/test_plan.py b/tests/test_plan.py index 6e1aec3..fe98f21 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -6,7 +6,6 @@ def test_get_plan_info_attrs(tmp_path): - with h5py.File(tmp_path / "test.hdf", "w") as f: group = f.create_group(RasPlanHdf.PLAN_INFO_PATH) for key, value in attrs_to_set.items(): @@ -18,7 +17,6 @@ def test_get_plan_info_attrs(tmp_path): def test_get_plan_param_attrs(tmp_path): - with h5py.File(tmp_path / "test.hdf", "w") as f: group = f.create_group(RasPlanHdf.PLAN_PARAMS_PATH) for key, value in attrs_to_set.items(): @@ -30,7 +28,6 @@ def test_get_plan_param_attrs(tmp_path): def test_get_meteorology_precip_attrs(tmp_path): - with h5py.File(tmp_path / "test.hdf", "w") as f: group = f.create_group(RasPlanHdf.PRECIP_PATH) for key, value in attrs_to_set.items(): @@ -42,7 +39,6 @@ def test_get_meteorology_precip_attrs(tmp_path): def test_get_results_unsteady_attrs(tmp_path): - with h5py.File(tmp_path / "test.hdf", "w") as f: group = f.create_group(RasPlanHdf.RESULTS_UNSTEADY_PATH) for key, value in attrs_to_set.items(): @@ -54,7 +50,6 @@ def test_get_results_unsteady_attrs(tmp_path): def test_get_results_unsteady_summary_attrs(tmp_path): - with h5py.File(tmp_path / "test.hdf", "w") as f: group = f.create_group(RasPlanHdf.RESULTS_UNSTEADY_SUMMARY_PATH) for key, value in attrs_to_set.items(): @@ -66,7 +61,6 @@ def test_get_results_unsteady_summary_attrs(tmp_path): def test_get_results_volume_accounting_attrs(tmp_path): - with h5py.File(tmp_path / "test.hdf", "w") as f: group = f.create_group(RasPlanHdf.VOLUME_ACCOUNTING_PATH) for key, value in attrs_to_set.items():