ras-commander 0.61.0__py3-none-any.whl → 0.65.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.
ras_commander/HdfBndry.py CHANGED
@@ -122,6 +122,12 @@ class HdfBndry:
122
122
  -------
123
123
  gpd.GeoDataFrame
124
124
  A GeoDataFrame containing the breaklines.
125
+
126
+ Notes
127
+ -----
128
+ - Zero-length breaklines are logged and skipped.
129
+ - Single-point breaklines are logged and skipped.
130
+ - These invalid breaklines should be removed in RASMapper to prevent potential issues.
125
131
  """
126
132
  try:
127
133
  with h5py.File(hdf_path, 'r') as hdf_file:
@@ -129,17 +135,95 @@ class HdfBndry:
129
135
  if breaklines_path not in hdf_file:
130
136
  logger.warning(f"Breaklines path '{breaklines_path}' not found in HDF file.")
131
137
  return gpd.GeoDataFrame()
138
+
132
139
  bl_line_data = hdf_file[breaklines_path]
133
- bl_line_ids = range(bl_line_data["Attributes"][()].shape[0])
134
- names = np.vectorize(HdfUtils.convert_ras_string)(
135
- bl_line_data["Attributes"][()]["Name"]
136
- )
137
- geoms = HdfBase.get_polylines_from_parts(hdf_path, breaklines_path)
140
+ attributes = bl_line_data["Attributes"][()]
141
+
142
+ # Initialize lists to store valid breakline data
143
+ valid_ids = []
144
+ valid_names = []
145
+ valid_geoms = []
146
+
147
+ # Track invalid breaklines for summary
148
+ zero_length_count = 0
149
+ single_point_count = 0
150
+ other_error_count = 0
151
+
152
+ # Process each breakline
153
+ for idx, (pnt_start, pnt_cnt, part_start, part_cnt) in enumerate(bl_line_data["Polyline Info"][()]):
154
+ name = HdfUtils.convert_ras_string(attributes["Name"][idx])
155
+
156
+ # Check for zero-length breaklines
157
+ if pnt_cnt == 0:
158
+ zero_length_count += 1
159
+ logger.debug(f"Zero-length breakline found (FID: {idx}, Name: {name})")
160
+ continue
161
+
162
+ # Check for single-point breaklines
163
+ if pnt_cnt == 1:
164
+ single_point_count += 1
165
+ logger.debug(f"Single-point breakline found (FID: {idx}, Name: {name})")
166
+ continue
167
+
168
+ try:
169
+ points = bl_line_data["Polyline Points"][()][pnt_start:pnt_start + pnt_cnt]
170
+
171
+ # Additional validation of points array
172
+ if len(points) < 2:
173
+ single_point_count += 1
174
+ logger.debug(f"Invalid point count in breakline (FID: {idx}, Name: {name})")
175
+ continue
176
+
177
+ if part_cnt == 1:
178
+ geom = LineString(points)
179
+ else:
180
+ parts = bl_line_data["Polyline Parts"][()][part_start:part_start + part_cnt]
181
+ geom = MultiLineString([
182
+ points[part_pnt_start:part_pnt_start + part_pnt_cnt]
183
+ for part_pnt_start, part_pnt_cnt in parts
184
+ if part_pnt_cnt > 1 # Skip single-point parts
185
+ ])
186
+ # Skip if no valid parts remain
187
+ if len(geom.geoms) == 0:
188
+ other_error_count += 1
189
+ logger.debug(f"No valid parts in multipart breakline (FID: {idx}, Name: {name})")
190
+ continue
191
+
192
+ valid_ids.append(idx)
193
+ valid_names.append(name)
194
+ valid_geoms.append(geom)
195
+
196
+ except Exception as e:
197
+ other_error_count += 1
198
+ logger.debug(f"Error processing breakline {idx}: {str(e)}")
199
+ continue
200
+
201
+ # Log summary of invalid breaklines
202
+ total_invalid = zero_length_count + single_point_count + other_error_count
203
+ if total_invalid > 0:
204
+ logger.info(
205
+ f"Breakline processing summary:\n"
206
+ f"- Zero-length breaklines: {zero_length_count}\n"
207
+ f"- Single-point breaklines: {single_point_count}\n"
208
+ f"- Other invalid breaklines: {other_error_count}\n"
209
+ f"Consider removing these invalid breaklines using RASMapper."
210
+ )
211
+
212
+ # Create GeoDataFrame with valid breaklines
213
+ if not valid_ids:
214
+ logger.warning("No valid breaklines found in the HDF file.")
215
+ return gpd.GeoDataFrame()
216
+
138
217
  return gpd.GeoDataFrame(
139
- {"bl_id": bl_line_ids, "Name": names, "geometry": geoms},
218
+ {
219
+ "bl_id": valid_ids,
220
+ "Name": valid_names,
221
+ "geometry": valid_geoms
222
+ },
140
223
  geometry="geometry",
141
- crs=HdfBase.get_projection(hdf_file),
224
+ crs=HdfBase.get_projection(hdf_file)
142
225
  )
226
+
143
227
  except Exception as e:
144
228
  logger.error(f"Error reading breaklines: {str(e)}")
145
229
  return gpd.GeoDataFrame()
@@ -47,7 +47,7 @@ from pathlib import Path
47
47
  import pandas as pd
48
48
  import geopandas as gpd
49
49
  import h5py
50
- from rasterstats import zonal_stats
50
+
51
51
  from .Decorators import log_call, standardize_input
52
52
 
53
53
  class HdfInfiltration:
@@ -232,6 +232,12 @@ class HdfInfiltration:
232
232
  Returns:
233
233
  DataFrame with soil statistics including percentages and areas
234
234
  """
235
+
236
+ try:
237
+ from rasterstats import zonal_stats
238
+ except ImportError as e:
239
+ logger.error("Failed to import rasterstats. Please run 'pip install rasterstats' and try again.")
240
+ raise e
235
241
  # Initialize areas dictionary
236
242
  mukey_areas = {mukey: 0 for mukey in raster_map.values()}
237
243
 
ras_commander/HdfPlan.py CHANGED
@@ -50,6 +50,8 @@ import pandas as pd
50
50
  from datetime import datetime
51
51
  from pathlib import Path
52
52
  from typing import Dict, List, Optional
53
+ import re
54
+ import numpy as np
53
55
 
54
56
  from .HdfBase import HdfBase
55
57
  from .HdfUtils import HdfUtils
@@ -178,7 +180,7 @@ class HdfPlan:
178
180
  @staticmethod
179
181
  @log_call
180
182
  @standardize_input(file_type='plan_hdf')
181
- def get_plan_parameters(hdf_path: Path) -> Dict:
183
+ def get_plan_parameters(hdf_path: Path) -> pd.DataFrame:
182
184
  """
183
185
  Get plan parameter attributes from a HEC-RAS HDF plan file.
184
186
 
@@ -186,7 +188,10 @@ class HdfPlan:
186
188
  hdf_path (Path): Path to the HEC-RAS plan HDF file.
187
189
 
188
190
  Returns:
189
- Dict: A dictionary containing the plan parameter attributes.
191
+ pd.DataFrame: A DataFrame containing the plan parameters with columns:
192
+ - Parameter: Name of the parameter
193
+ - Value: Value of the parameter (decoded if byte string)
194
+ - Plan: Plan number (01-99) extracted from the filename (ProjectName.pXX.hdf)
190
195
 
191
196
  Raises:
192
197
  ValueError: If there's an error retrieving the plan parameter attributes.
@@ -197,14 +202,48 @@ class HdfPlan:
197
202
  if plan_params_path not in hdf_file:
198
203
  raise ValueError(f"Plan Parameters not found in {hdf_path}")
199
204
 
200
- attrs = {}
205
+ # Extract parameters
206
+ params_dict = {}
201
207
  for key in hdf_file[plan_params_path].attrs.keys():
202
208
  value = hdf_file[plan_params_path].attrs[key]
209
+
210
+ # Handle different types of values
203
211
  if isinstance(value, bytes):
204
212
  value = HdfUtils.convert_ras_string(value)
205
- attrs[key] = value
213
+ elif isinstance(value, np.ndarray):
214
+ # Handle array values
215
+ if value.dtype.kind in {'S', 'a'}: # Array of byte strings
216
+ value = [v.decode('utf-8') if isinstance(v, bytes) else v for v in value]
217
+ else:
218
+ value = value.tolist() # Convert numpy array to list
219
+
220
+ # If it's a single-item list, extract the value
221
+ if len(value) == 1:
222
+ value = value[0]
223
+
224
+ params_dict[key] = value
206
225
 
207
- return attrs
226
+ # Create DataFrame from parameters
227
+ df = pd.DataFrame.from_dict(params_dict, orient='index', columns=['Value'])
228
+ df.index.name = 'Parameter'
229
+ df = df.reset_index()
230
+
231
+ # Extract plan number from filename
232
+ filename = Path(hdf_path).name
233
+ plan_match = re.search(r'\.p(\d{2})\.', filename)
234
+ if plan_match:
235
+ plan_num = plan_match.group(1)
236
+ else:
237
+ plan_num = "00" # Default if no match found
238
+ logger.warning(f"Could not extract plan number from filename: {filename}")
239
+
240
+ df['Plan'] = plan_num
241
+
242
+ # Reorder columns to put Plan first
243
+ df = df[['Plan', 'Parameter', 'Value']]
244
+
245
+ return df
246
+
208
247
  except Exception as e:
209
248
  raise ValueError(f"Failed to get plan parameter attributes: {str(e)}")
210
249
 
@@ -609,12 +609,8 @@ class HdfResultsMesh:
609
609
  Returns
610
610
  -------
611
611
  gpd.GeoDataFrame
612
- A GeoDataFrame containing the summary output data with attributes as metadata.
613
-
614
- Raises
615
- ------
616
- ValueError
617
- If the HDF file cannot be opened or read, or if the requested data is not found.
612
+ A GeoDataFrame containing the summary output data with decoded attributes as metadata.
613
+ Returns empty GeoDataFrame if variable is not found.
618
614
  """
619
615
  try:
620
616
  dfs = []
@@ -623,13 +619,19 @@ class HdfResultsMesh:
623
619
  logger.info(f"Processing summary output for variable: {var}")
624
620
  d2_flow_areas = hdf_file.get("Geometry/2D Flow Areas/Attributes")
625
621
  if d2_flow_areas is None:
622
+ logger.info("No 2D Flow Areas found in HDF file")
626
623
  return gpd.GeoDataFrame()
627
624
 
628
625
  for d2_flow_area in d2_flow_areas[:]:
629
626
  mesh_name = HdfUtils.convert_ras_string(d2_flow_area[0])
630
627
  cell_count = d2_flow_area[-1]
631
628
  logger.debug(f"Processing mesh: {mesh_name} with {cell_count} cells")
632
- group = HdfResultsMesh.get_mesh_summary_output_group(hdf_file, mesh_name, var)
629
+
630
+ try:
631
+ group = HdfResultsMesh.get_mesh_summary_output_group(hdf_file, mesh_name, var)
632
+ except ValueError:
633
+ logger.info(f"Variable '{var}' not present in output file for mesh '{mesh_name}', skipping")
634
+ continue
633
635
 
634
636
  data = group[:]
635
637
  logger.debug(f"Data shape for {var} in {mesh_name}: {data.shape}")
@@ -639,7 +641,7 @@ class HdfResultsMesh:
639
641
  if data.ndim == 2 and data.shape[0] == 2:
640
642
  # Handle 2D datasets (e.g. Maximum Water Surface)
641
643
  row_variables = group.attrs.get('Row Variables', [b'Value', b'Time'])
642
- row_variables = [v.decode('utf-8').strip() for v in row_variables]
644
+ row_variables = [v.decode('utf-8').strip() if isinstance(v, bytes) else v for v in row_variables]
643
645
 
644
646
  df = pd.DataFrame({
645
647
  "mesh_name": [mesh_name] * data.shape[1],
@@ -676,14 +678,22 @@ class HdfResultsMesh:
676
678
  on=['mesh_name', 'cell_id'],
677
679
  how='left')
678
680
 
679
- # Add group attributes as metadata
681
+ # Add group attributes as metadata with proper decoding
680
682
  df.attrs['mesh_name'] = mesh_name
681
683
  for attr_name, attr_value in group.attrs.items():
682
684
  if isinstance(attr_value, bytes):
683
- attr_value = attr_value.decode('utf-8')
685
+ # Decode single byte string
686
+ decoded_value = attr_value.decode('utf-8')
684
687
  elif isinstance(attr_value, np.ndarray):
685
- attr_value = attr_value.tolist()
686
- df.attrs[attr_name] = attr_value
688
+ if attr_value.dtype.kind in {'S', 'a'}: # Array of byte strings
689
+ # Decode array of byte strings
690
+ decoded_value = [v.decode('utf-8') if isinstance(v, bytes) else v for v in attr_value]
691
+ else:
692
+ # Convert other numpy arrays to list
693
+ decoded_value = attr_value.tolist()
694
+ else:
695
+ decoded_value = attr_value
696
+ df.attrs[attr_name] = decoded_value
687
697
 
688
698
  dfs.append(df)
689
699
 
@@ -700,7 +710,7 @@ class HdfResultsMesh:
700
710
  if crs:
701
711
  gdf.set_crs(crs, inplace=True)
702
712
 
703
- # Combine attributes from all meshes
713
+ # Combine attributes from all meshes with decoded values
704
714
  combined_attrs = {}
705
715
  for df in dfs:
706
716
  for key, value in df.attrs.items():
@@ -737,6 +747,6 @@ class HdfResultsMesh:
737
747
  output_path = f"Results/Unsteady/Output/Output Blocks/Base Output/Summary Output/2D Flow Areas/{mesh_name}/{var}"
738
748
  output_item = hdf_file.get(output_path)
739
749
  if output_item is None:
740
- raise ValueError(f"Could not find HDF group or dataset at path '{output_path}'")
750
+ raise ValueError(f"Dataset not found at path '{output_path}'")
741
751
  return output_item
742
752