ras-commander 0.51.0__py3-none-any.whl → 0.53.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/HdfXsec.py CHANGED
@@ -5,7 +5,7 @@ Attribution: A substantial amount of code in this file is sourced or derived
5
5
  from the https://github.com/fema-ffrd/rashdf library,
6
6
  released under MIT license and Copyright (c) 2024 fema-ffrd
7
7
 
8
- The file has been forked and modified for use in RAS Commander.
8
+ This source code has been forked and modified for use in RAS Commander.
9
9
 
10
10
  -----
11
11
 
@@ -99,7 +99,7 @@ class HdfXsec:
99
99
  """
100
100
  try:
101
101
  with h5py.File(hdf_path, 'r') as hdf:
102
- # Extract datasets
102
+ # Extract required datasets
103
103
  poly_info = hdf['/Geometry/Cross Sections/Polyline Info'][:]
104
104
  poly_parts = hdf['/Geometry/Cross Sections/Polyline Parts'][:]
105
105
  poly_points = hdf['/Geometry/Cross Sections/Polyline Points'][:]
@@ -182,43 +182,61 @@ class HdfXsec:
182
182
  else:
183
183
  ineffective_blocks.append([])
184
184
 
185
- # Create GeoDataFrame
186
- if geometries:
187
- # Create DataFrame from attributes
188
- data = {
189
- 'geometry': geometries,
190
- 'station_elevation': station_elevations,
191
- 'mannings_n': mannings_n,
192
- 'ineffective_blocks': ineffective_blocks,
193
- 'River': [x['River'].decode('utf-8').strip() for x in xs_attrs],
194
- 'Reach': [x['Reach'].decode('utf-8').strip() for x in xs_attrs],
195
- 'RS': [x['RS'].decode('utf-8').strip() for x in xs_attrs],
196
- 'Name': [x['Name'].decode('utf-8').strip() for x in xs_attrs],
197
- 'Description': [x['Description'].decode('utf-8').strip() for x in xs_attrs],
198
- 'Len Left': xs_attrs['Len Left'],
199
- 'Len Channel': xs_attrs['Len Channel'],
200
- 'Len Right': xs_attrs['Len Right'],
201
- 'Left Bank': xs_attrs['Left Bank'],
202
- 'Right Bank': xs_attrs['Right Bank'],
203
- 'Friction Mode': [x['Friction Mode'].decode('utf-8').strip() for x in xs_attrs],
204
- 'Contr': xs_attrs['Contr'],
205
- 'Expan': xs_attrs['Expan'],
206
- 'Left Levee Sta': xs_attrs['Left Levee Sta'],
207
- 'Left Levee Elev': xs_attrs['Left Levee Elev'],
208
- 'Right Levee Sta': xs_attrs['Right Levee Sta'],
209
- 'Right Levee Elev': xs_attrs['Right Levee Elev'],
210
- 'HP Count': xs_attrs['HP Count'],
211
- 'HP Start Elev': xs_attrs['HP Start Elev'],
212
- 'HP Vert Incr': xs_attrs['HP Vert Incr'],
213
- 'HP LOB Slices': xs_attrs['HP LOB Slices'],
214
- 'HP Chan Slices': xs_attrs['HP Chan Slices'],
215
- 'HP ROB Slices': xs_attrs['HP ROB Slices'],
216
- 'Ineff Block Mode': xs_attrs['Ineff Block Mode'],
217
- 'Obstr Block Mode': xs_attrs['Obstr Block Mode'],
218
- 'Default Centerline': xs_attrs['Default Centerline'],
219
- 'Last Edited': [x['Last Edited'].decode('utf-8').strip() for x in xs_attrs]
220
- }
185
+ # Create base dictionary with required fields
186
+ data = {
187
+ 'geometry': geometries,
188
+ 'station_elevation': station_elevations,
189
+ 'mannings_n': mannings_n,
190
+ 'ineffective_blocks': ineffective_blocks,
191
+ }
192
+
193
+ # Define field mappings with default values
194
+ field_mappings = {
195
+ 'River': ('River', ''),
196
+ 'Reach': ('Reach', ''),
197
+ 'RS': ('RS', ''),
198
+ 'Name': ('Name', ''),
199
+ 'Description': ('Description', ''),
200
+ 'Len Left': ('Len Left', 0.0),
201
+ 'Len Channel': ('Len Channel', 0.0),
202
+ 'Len Right': ('Len Right', 0.0),
203
+ 'Left Bank': ('Left Bank', 0.0),
204
+ 'Right Bank': ('Right Bank', 0.0),
205
+ 'Friction Mode': ('Friction Mode', ''),
206
+ 'Contr': ('Contr', 0.0),
207
+ 'Expan': ('Expan', 0.0),
208
+ 'Left Levee Sta': ('Left Levee Sta', None),
209
+ 'Left Levee Elev': ('Left Levee Elev', None),
210
+ 'Right Levee Sta': ('Right Levee Sta', None),
211
+ 'Right Levee Elev': ('Right Levee Elev', None),
212
+ 'HP Count': ('HP Count', 0),
213
+ 'HP Start Elev': ('HP Start Elev', 0.0),
214
+ 'HP Vert Incr': ('HP Vert Incr', 0.0),
215
+ 'HP LOB Slices': ('HP LOB Slices', 0),
216
+ 'HP Chan Slices': ('HP Chan Slices', 0),
217
+ 'HP ROB Slices': ('HP ROB Slices', 0),
218
+ 'Ineff Block Mode': ('Ineff Block Mode', 0),
219
+ 'Obstr Block Mode': ('Obstr Block Mode', 0),
220
+ 'Default Centerline': ('Default Centerline', 0),
221
+ 'Last Edited': ('Last Edited', '')
222
+ }
221
223
 
224
+ # Add fields that exist in xs_attrs
225
+ for field_name, (attr_name, default_value) in field_mappings.items():
226
+ if attr_name in xs_attrs.dtype.names:
227
+ if xs_attrs[attr_name].dtype.kind == 'S':
228
+ # Handle string fields
229
+ data[field_name] = [x[attr_name].decode('utf-8').strip()
230
+ for x in xs_attrs]
231
+ else:
232
+ # Handle numeric fields
233
+ data[field_name] = xs_attrs[attr_name]
234
+ else:
235
+ # Use default value if field doesn't exist
236
+ data[field_name] = [default_value] * len(geometries)
237
+ logger.debug(f"Field {attr_name} not found in attributes, using default value")
238
+
239
+ if geometries:
222
240
  gdf = gpd.GeoDataFrame(data)
223
241
 
224
242
  # Set CRS if available
@@ -233,7 +251,7 @@ class HdfXsec:
233
251
  return gpd.GeoDataFrame()
234
252
 
235
253
  except Exception as e:
236
- logging.error(f"Error processing cross-section data: {str(e)}")
254
+ logger.error(f"Error processing cross-section data: {str(e)}")
237
255
  return gpd.GeoDataFrame()
238
256
 
239
257
  @staticmethod
@@ -391,7 +409,7 @@ class HdfXsec:
391
409
  result_gdf.at[idx, 'points'] = points
392
410
 
393
411
  # Add stationing direction based on upstream/downstream info
394
- if row['upstream_type'] == 'Junction' and row['downstream_type'] != 'Junction':
412
+ if row['US Type'] == 'Junction' and row['DS Type'] != 'Junction':
395
413
  # Reverse stationing if upstream is junction
396
414
  result_gdf.at[idx, 'station_start'] = total_length
397
415
  result_gdf.at[idx, 'station_end'] = 0.0
@@ -23,7 +23,8 @@ def setup_logging(log_file=None, log_level=logging.INFO):
23
23
 
24
24
  # Define log format
25
25
  log_format = logging.Formatter(
26
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
26
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
+ datefmt='%Y-%m-%d %H:%M:%S'
27
28
  )
28
29
 
29
30
  # Configure console handler
ras_commander/RasCmdr.py CHANGED
@@ -271,13 +271,13 @@ class RasCmdr:
271
271
  logger.info(f"Created worker folder: {worker_folder}")
272
272
 
273
273
  try:
274
- ras_instance = RasPrj()
275
- worker_ras_instance = init_ras_project(
274
+ worker_ras = RasPrj()
275
+ worker_ras_object = init_ras_project(
276
276
  ras_project_folder=worker_folder,
277
277
  ras_version=ras_obj.ras_exe_path,
278
- ras_instance=ras_instance
278
+ ras_object=worker_ras
279
279
  )
280
- worker_ras_objects[worker_id] = worker_ras_instance
280
+ worker_ras_objects[worker_id] = worker_ras_object
281
281
  except Exception as e:
282
282
  logger.critical(f"Failed to initialize RAS project for worker {worker_id}: {str(e)}")
283
283
  worker_ras_objects[worker_id] = None
@@ -317,28 +317,53 @@ class RasCmdr:
317
317
  continue
318
318
  worker_folder = Path(worker_ras.project_folder)
319
319
  try:
320
- for item in worker_folder.iterdir():
321
- dest_path = final_dest_folder / item.name
322
- if dest_path.exists():
323
- if dest_path.is_dir():
324
- shutil.rmtree(dest_path)
325
- logger.debug(f"Removed existing directory at {dest_path}")
326
- else:
327
- dest_path.unlink()
328
- logger.debug(f"Removed existing file at {dest_path}")
329
- shutil.move(str(item), final_dest_folder)
330
- logger.debug(f"Moved {item} to {final_dest_folder}")
331
- shutil.rmtree(worker_folder)
332
- logger.info(f"Removed worker folder: {worker_folder}")
320
+ # First, close any open resources in the worker RAS object
321
+ worker_ras.close() if hasattr(worker_ras, 'close') else None
322
+
323
+ # Add a small delay to ensure file handles are released
324
+ time.sleep(1)
325
+
326
+ # Move files with retry mechanism
327
+ max_retries = 3
328
+ for retry in range(max_retries):
329
+ try:
330
+ for item in worker_folder.iterdir():
331
+ dest_path = final_dest_folder / item.name
332
+ if dest_path.exists():
333
+ if dest_path.is_dir():
334
+ shutil.rmtree(dest_path)
335
+ else:
336
+ dest_path.unlink()
337
+ # Use copy instead of move for more reliability
338
+ if item.is_dir():
339
+ shutil.copytree(item, dest_path)
340
+ else:
341
+ shutil.copy2(item, dest_path)
342
+
343
+ # Add another small delay before removal
344
+ time.sleep(1)
345
+
346
+ # Try to remove the worker folder
347
+ if worker_folder.exists():
348
+ shutil.rmtree(worker_folder)
349
+ break # If successful, break the retry loop
350
+
351
+ except PermissionError as pe:
352
+ if retry == max_retries - 1: # If this was the last retry
353
+ logger.error(f"Failed to move/remove files after {max_retries} attempts: {str(pe)}")
354
+ raise
355
+ time.sleep(2 ** retry) # Exponential backoff
356
+ continue
357
+
333
358
  except Exception as e:
334
359
  logger.error(f"Error moving results from {worker_folder} to {final_dest_folder}: {str(e)}")
335
360
 
336
361
  try:
337
- final_dest_folder_ras_obj = RasPrj()
362
+ final_dest_folder_ras = RasPrj()
338
363
  final_dest_folder_ras_obj = init_ras_project(
339
364
  ras_project_folder=final_dest_folder,
340
365
  ras_version=ras_obj.ras_exe_path,
341
- ras_instance=final_dest_folder_ras_obj
366
+ ras_object=final_dest_folder_ras
342
367
  )
343
368
  final_dest_folder_ras_obj.check_initialized()
344
369
  except Exception as e:
@@ -379,7 +404,7 @@ class RasCmdr:
379
404
  other two compute_ functions. Per the original HEC-RAS test flag, it creates a separate test folder,
380
405
  copies the project there, and executes the specified plans in sequential order.
381
406
 
382
- For most purposes, just copying a the project folder, initing that new folder, then running each plan
407
+ For most purposes, just copying the project folder, initing that new folder, then running each plan
383
408
  with compute_plan is a simpler and more flexible approach. This is shown in the examples provided
384
409
  in the ras-commander library.
385
410
 
ras_commander/RasPlan.py CHANGED
@@ -249,7 +249,6 @@ class RasPlan:
249
249
  def _update_unsteady_in_file(lines, new_unsteady_flow_number):
250
250
  return [f"Unsteady File=u{new_unsteady_flow_number}\n" if line.startswith("Unsteady File=u") else line for line in lines]
251
251
 
252
-
253
252
  @staticmethod
254
253
  @log_call
255
254
  def set_num_cores(plan_number, num_cores, ras_object=None):
@@ -264,6 +263,16 @@ class RasPlan:
264
263
  Returns:
265
264
  None
266
265
 
266
+ Number of cores is controlled by the following parameters in the plan file corresponding to 1D, 2D, Pipe Systems and Pump Stations:
267
+ UNET D1 Cores=
268
+ UNET D2 Cores=
269
+ PS Cores=
270
+
271
+ Where a value of "0" is used for "All Available" cores, and values of 1 or more are used to specify the number of cores to use.
272
+ For complex 1D/2D models with pipe systems, a more complex approach may be needed to optimize performance. (Suggest writing a custom function based on this code).
273
+ This function simply sets the "num_cores" parameter for ALL instances of the above parameters in the plan file.
274
+
275
+
267
276
  Notes on setting num_cores in HEC-RAS:
268
277
  The recommended setting for num_cores is 2 (most efficient) to 8 (most performant)
269
278
  More details in the HEC-Commander Repository Blog "Benchmarking is All You Need"
@@ -290,9 +299,9 @@ class RasPlan:
290
299
  def update_num_cores(lines):
291
300
  updated_lines = []
292
301
  for line in lines:
293
- if "UNET D1 Cores=" in line:
294
- parts = line.split("=")
295
- updated_lines.append(f"{parts[0]}= {num_cores}\n")
302
+ if any(param in line for param in ["UNET D1 Cores=", "UNET D2 Cores=", "PS Cores="]):
303
+ param_name = line.split("=")[0]
304
+ updated_lines.append(f"{param_name}= {num_cores}\n")
296
305
  else:
297
306
  updated_lines.append(line)
298
307
  return updated_lines
@@ -840,7 +849,7 @@ class RasPlan:
840
849
  - 'Plan File' (str): Name of the plan file
841
850
  - 'Plan Title' (str): Title of the simulation plan
842
851
  - 'Program Version' (str): Version number of HEC-RAS
843
- - 'Run HTAB' (int): Flag to run HTab module (-1 or 1)
852
+ - 'Run HTab' (int): Flag to run HTab module (-1 or 1)
844
853
  - 'Run Post Process' (int): Flag to run post-processing (-1 or 1)
845
854
  - 'Run Sediment' (int): Flag to run sediment transport module (0 or 1)
846
855
  - 'Run UNET' (int): Flag to run unsteady network module (-1 or 1)
@@ -848,13 +857,14 @@ class RasPlan:
848
857
  - 'Short Identifier' (str): Short name or ID for the plan
849
858
  - 'Simulation Date' (str): Start and end dates/times for simulation
850
859
  - 'UNET D1 Cores' (int): Number of cores used in 1D calculations
860
+ - 'UNET D2 Cores' (int): Number of cores used in 2D calculations
861
+ - 'PS Cores' (int): Number of cores used in parallel simulation
851
862
  - 'UNET Use Existing IB Tables' (int): Flag for using existing internal boundary tables (-1, 0, or 1)
852
863
  - 'UNET 1D Methodology' (str): 1D calculation methodology
853
864
  - 'UNET D2 Solver Type' (str): 2D solver type
854
865
  - 'UNET D2 Name' (str): Name of the 2D area
855
866
  - 'Run RASMapper' (int): Flag to run RASMapper for floodplain mapping (-1 for off, 0 for on)
856
867
 
857
-
858
868
  Note:
859
869
  Writing Multi line keys like 'Description' are not supported by this function.
860
870
 
@@ -868,9 +878,10 @@ class RasPlan:
868
878
  supported_plan_keys = {
869
879
  'Description', 'Computation Interval', 'DSS File', 'Flow File', 'Friction Slope Method',
870
880
  'Geom File', 'Mapping Interval', 'Plan File', 'Plan Title', 'Program Version',
871
- 'Run HTAB', 'Run Post Process', 'Run Sediment', 'Run UNET', 'Run WQNET',
872
- 'Short Identifier', 'Simulation Date', 'UNET D1 Cores', 'UNET Use Existing IB Tables',
873
- 'UNET 1D Methodology', 'UNET D2 Solver Type', 'UNET D2 Name', 'Run RASMapper'
881
+ 'Run HTab', 'Run Post Process', 'Run Sediment', 'Run UNET', 'Run WQNET',
882
+ 'Short Identifier', 'Simulation Date', 'UNET D1 Cores', 'UNET D2 Cores', 'PS Cores',
883
+ 'UNET Use Existing IB Tables', 'UNET 1D Methodology', 'UNET D2 Solver Type',
884
+ 'UNET D2 Name', 'Run RASMapper', 'Run HTab', 'Run UNET'
874
885
  }
875
886
 
876
887
  if key not in supported_plan_keys:
@@ -891,7 +902,23 @@ class RasPlan:
891
902
  logger.error(f"Error reading plan file {plan_file_path}: {e}")
892
903
  raise
893
904
 
894
- if key == 'Description':
905
+ # Handle core settings specially to convert to integers
906
+ core_keys = {'UNET D1 Cores', 'UNET D2 Cores', 'PS Cores'}
907
+ if key in core_keys:
908
+ pattern = f"{key}=(.*)"
909
+ match = re.search(pattern, content)
910
+ if match:
911
+ try:
912
+ return int(match.group(1).strip())
913
+ except ValueError:
914
+ logger = logging.getLogger(__name__)
915
+ logger.error(f"Could not convert {key} value to integer")
916
+ return None
917
+ else:
918
+ logger = logging.getLogger(__name__)
919
+ logger.error(f"Key '{key}' not found in the plan file.")
920
+ return None
921
+ elif key == 'Description':
895
922
  match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
896
923
  return match.group(1).strip() if match else None
897
924
  else:
@@ -1060,81 +1087,63 @@ class RasPlan:
1060
1087
  @log_call
1061
1088
  def update_plan_description(plan_number_or_path: Union[str, Path], description: str, ras_object: Optional['RasPrj'] = None) -> None:
1062
1089
  """
1063
- Update the description in the plan file.
1090
+ Update the description block in a HEC-RAS plan file.
1064
1091
 
1065
1092
  Args:
1066
- plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1067
- description (str): The new description to be written to the plan file.
1068
- ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1093
+ plan_number_or_path (Union[str, Path]): The plan number or full path to the plan file
1094
+ description (str): The new description text to set
1095
+ ras_object (Optional[RasPrj]): Specific RAS object to use. If None, uses the global ras instance.
1069
1096
 
1070
1097
  Raises:
1071
- ValueError: If the plan file is not found.
1072
- IOError: If there's an error reading from or writing to the plan file.
1098
+ ValueError: If the plan file is not found
1099
+ IOError: If there's an error reading or writing the plan file
1073
1100
  """
1074
- logger = logging.getLogger(__name__)
1101
+ logger = get_logger(__name__)
1102
+ ras_obj = ras_object or ras
1103
+ ras_obj.check_initialized()
1075
1104
 
1076
1105
  plan_file_path = Path(plan_number_or_path)
1077
1106
  if not plan_file_path.is_file():
1078
- plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object)
1079
- if plan_file_path is None or not Path(plan_file_path).exists():
1107
+ plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
1108
+ if not plan_file_path.exists():
1109
+ logger.error(f"Plan file not found: {plan_file_path}")
1080
1110
  raise ValueError(f"Plan file not found: {plan_file_path}")
1081
1111
 
1082
1112
  try:
1083
1113
  with open(plan_file_path, 'r') as file:
1084
- lines = file.readlines()
1085
- except IOError as e:
1086
- logger.error(f"Error reading plan file {plan_file_path}: {e}")
1087
- raise
1114
+ content = file.read()
1088
1115
 
1089
- start_index = None
1090
- end_index = None
1091
- comp_interval_index = None
1092
- for i, line in enumerate(lines):
1093
- if line.strip() == "BEGIN DESCRIPTION:":
1094
- start_index = i
1095
- elif line.strip() == "END DESCRIPTION:":
1096
- end_index = i
1097
- elif line.strip().startswith("Computation Interval="):
1098
- comp_interval_index = i
1099
-
1100
- if start_index is not None and end_index is not None:
1101
- # Description exists, update it
1102
- new_lines = lines[:start_index + 1]
1103
- if description:
1104
- new_lines.extend(description.split('\n'))
1105
- else:
1106
- new_lines.append('\n')
1107
- new_lines.extend(lines[end_index:])
1108
- else:
1109
- # Description doesn't exist, insert before Computation Interval
1110
- if comp_interval_index is None:
1111
- logger.warning("Neither description tags nor Computation Interval found in plan file. Appending to end of file.")
1112
- comp_interval_index = len(lines)
1113
-
1114
- new_lines = lines[:comp_interval_index]
1115
- new_lines.append("BEGIN DESCRIPTION:\n")
1116
- if description:
1117
- new_lines.extend(f"{line}\n" for line in description.split('\n'))
1116
+ # Find the description block
1117
+ desc_pattern = r'Begin DESCRIPTION.*?END DESCRIPTION'
1118
+ new_desc_block = f'Begin DESCRIPTION\n{description}\nEND DESCRIPTION'
1119
+
1120
+ if re.search(desc_pattern, content, re.DOTALL):
1121
+ # Replace existing description block
1122
+ new_content = re.sub(desc_pattern, new_desc_block, content, flags=re.DOTALL)
1118
1123
  else:
1119
- new_lines.append('\n')
1120
- new_lines.append("END DESCRIPTION:\n")
1121
- new_lines.extend(lines[comp_interval_index:])
1124
+ # Add new description block at the start of the file
1125
+ new_content = new_desc_block + '\n' + content
1122
1126
 
1123
- try:
1127
+ # Write the updated content back to the file
1124
1128
  with open(plan_file_path, 'w') as file:
1125
- file.writelines(new_lines)
1129
+ file.write(new_content)
1130
+
1126
1131
  logger.info(f"Updated description in plan file: {plan_file_path}")
1132
+
1133
+ # Update the dataframes in the RAS object to reflect changes
1134
+ if ras_object:
1135
+ ras_object.plan_df = ras_object.get_plan_entries()
1136
+ ras_object.geom_df = ras_object.get_geom_entries()
1137
+ ras_object.flow_df = ras_object.get_flow_entries()
1138
+ ras_object.unsteady_df = ras_object.get_unsteady_entries()
1139
+
1127
1140
  except IOError as e:
1128
- logger.error(f"Error writing to plan file {plan_file_path}: {e}")
1141
+ logger.error(f"Error updating plan description in {plan_file_path}: {e}")
1142
+ raise
1143
+ except Exception as e:
1144
+ logger.error(f"Unexpected error updating plan description: {e}")
1129
1145
  raise
1130
1146
 
1131
- # Refresh RasPrj dataframes
1132
- if ras_object:
1133
- ras_object.plan_df = ras_object.get_plan_entries()
1134
- ras_object.geom_df = ras_object.get_geom_entries()
1135
- ras_object.flow_df = ras_object.get_flow_entries()
1136
- ras_object.unsteady_df = ras_object.get_unsteady_entries()
1137
-
1138
1147
  @staticmethod
1139
1148
  @log_call
1140
1149
  def read_plan_description(plan_number_or_path: Union[str, Path], ras_object: Optional['RasPrj'] = None) -> str: