ras-commander 0.44.0__py3-none-any.whl → 0.46.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/RasPlan.py CHANGED
@@ -34,6 +34,8 @@ from .RasPrj import RasPrj, ras
34
34
  from .RasUtils import RasUtils
35
35
  from pathlib import Path
36
36
  from typing import Union, Any
37
+ from datetime import datetime
38
+
37
39
  import logging
38
40
  import re
39
41
  from .LoggingConfig import get_logger
@@ -216,6 +218,8 @@ class RasPlan:
216
218
  @staticmethod
217
219
  def _update_unsteady_in_file(lines, new_unsteady_flow_number):
218
220
  return [f"Unsteady File=u{new_unsteady_flow_number}\n" if line.startswith("Unsteady File=u") else line for line in lines]
221
+
222
+
219
223
  @staticmethod
220
224
  @log_call
221
225
  def set_num_cores(plan_number, num_cores, ras_object=None):
@@ -870,47 +874,174 @@ class RasPlan:
870
874
  logger.error(f"Key '{key}' not found in the plan file.")
871
875
  return None
872
876
 
877
+
878
+ # NEW FUNCTIONS THAT NEED TESTING AND EXAMPLES
879
+
880
+
873
881
  @staticmethod
874
882
  @log_call
875
- def update_plan_value(
883
+ def update_run_flags(
876
884
  plan_number_or_path: Union[str, Path],
877
- key: str,
878
- value: Any,
885
+ geometry_preprocessor: bool = None,
886
+ unsteady_flow_simulation: bool = None,
887
+ run_sediment: bool = None,
888
+ post_processor: bool = None,
889
+ floodplain_mapping: bool = None,
879
890
  ras_object=None
880
891
  ) -> None:
881
892
  """
882
- Update a specific key-value pair in a HEC-RAS plan file.
893
+ Update the run flags in a HEC-RAS plan file.
883
894
 
884
895
  Parameters:
885
896
  plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
886
- key (str): The key to update in the plan file
887
- value (Any): The new value to set for the key
897
+ geometry_preprocessor (bool, optional): Flag for Geometry Preprocessor
898
+ unsteady_flow_simulation (bool, optional): Flag for Unsteady Flow Simulation
899
+ run_sediment (bool, optional): Flag for run_sediment
900
+ post_processor (bool, optional): Flag for Post Processor
901
+ floodplain_mapping (bool, optional): Flag for Floodplain Mapping
888
902
  ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
889
903
 
890
904
  Raises:
891
905
  ValueError: If the plan file is not found
892
906
  IOError: If there's an error reading or writing the plan file
893
907
 
894
- Note: See the docstring of get_plan_value for a full list of available keys and their types.
908
+ Example:
909
+ >>> RasPlan.update_run_flags("01", geometry_preprocessor=True, unsteady_flow_simulation=True, run_sediment=False, post_processor=True, floodplain_mapping=False)
910
+ """
911
+ ras_obj = ras_object or ras
912
+ ras_obj.check_initialized()
913
+
914
+ plan_file_path = Path(plan_number_or_path)
915
+ if not plan_file_path.is_file():
916
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object=ras_obj)
917
+ if plan_file_path is None or not Path(plan_file_path).exists():
918
+ raise ValueError(f"Plan file not found: {plan_file_path}")
919
+
920
+ flag_mapping = {
921
+ 'geometry_preprocessor': ('Run HTab', geometry_preprocessor),
922
+ 'unsteady_flow_simulation': ('Run UNet', unsteady_flow_simulation),
923
+ 'run_sediment': ('Run run_sediment', run_sediment),
924
+ 'post_processor': ('Run PostProcess', post_processor),
925
+ 'floodplain_mapping': ('Run RASMapper', floodplain_mapping)
926
+ }
927
+
928
+ try:
929
+ with open(plan_file_path, 'r') as file:
930
+ lines = file.readlines()
931
+
932
+ for i, line in enumerate(lines):
933
+ for key, (file_key, value) in flag_mapping.items():
934
+ if value is not None and line.strip().startswith(file_key):
935
+ lines[i] = f"{file_key}= {1 if value else 0}\n"
936
+
937
+ with open(plan_file_path, 'w') as file:
938
+ file.writelines(lines)
939
+
940
+ logger = logging.getLogger(__name__)
941
+ logger.info(f"Successfully updated run flags in plan file: {plan_file_path}")
942
+
943
+ except IOError as e:
944
+ logger = logging.getLogger(__name__)
945
+ logger.error(f"Error updating run flags in plan file {plan_file_path}: {e}")
946
+ raise
947
+
948
+
949
+
950
+ @staticmethod
951
+ @log_call
952
+ def update_plan_intervals(
953
+ plan_number_or_path: Union[str, Path],
954
+ computation_interval: Optional[str] = None,
955
+ output_interval: Optional[str] = None,
956
+ instantaneous_interval: Optional[str] = None,
957
+ mapping_interval: Optional[str] = None,
958
+ ras_object=None
959
+ ) -> None:
960
+ """
961
+ Update the computation and output intervals in a HEC-RAS plan file.
962
+
963
+ Parameters:
964
+ plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
965
+ computation_interval (Optional[str]): The new computation interval. Valid entries include:
966
+ '1SEC', '2SEC', '3SEC', '4SEC', '5SEC', '6SEC', '10SEC', '15SEC', '20SEC', '30SEC',
967
+ '1MIN', '2MIN', '3MIN', '4MIN', '5MIN', '6MIN', '10MIN', '15MIN', '20MIN', '30MIN',
968
+ '1HOUR', '2HOUR', '3HOUR', '4HOUR', '6HOUR', '8HOUR', '12HOUR', '1DAY'
969
+ output_interval (Optional[str]): The new output interval. Valid entries are the same as computation_interval.
970
+ instantaneous_interval (Optional[str]): The new instantaneous interval. Valid entries are the same as computation_interval.
971
+ mapping_interval (Optional[str]): The new mapping interval. Valid entries are the same as computation_interval.
972
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
973
+
974
+ Raises:
975
+ ValueError: If the plan file is not found or if an invalid interval is provided
976
+ IOError: If there's an error reading or writing the plan file
977
+
978
+ Note: This function does not check if the intervals are equal divisors. Ensure you use valid values from HEC-RAS.
895
979
 
896
980
  Example:
897
- >>> RasPlan.update_plan_value("01", "computation_interval", "10SEC")
898
- >>> RasPlan.update_plan_value("/path/to/plan.p01", "run_htab", 1)
899
- >>> RasPlan.update_plan_value("01", "run_rasmapper", 0) # Turn on Floodplain Mapping
981
+ >>> RasPlan.update_plan_intervals("01", computation_interval="5SEC", output_interval="1MIN", instantaneous_interval="1HOUR", mapping_interval="5MIN")
982
+ >>> RasPlan.update_plan_intervals("/path/to/plan.p01", computation_interval="10SEC", output_interval="30SEC")
900
983
  """
901
984
  ras_obj = ras_object or ras
902
985
  ras_obj.check_initialized()
903
986
 
904
- supported_plan_keys = {
905
- 'Description', 'Computation Interval', 'DSS File', 'Flow File', 'Friction Slope Method',
906
- 'Geom File', 'Mapping Interval', 'Plan File', 'Plan Title', 'Program Version',
907
- 'Run HTAB', 'Run Post Process', 'Run Sediment', 'Run UNET', 'Run WQNET',
908
- 'Short Identifier', 'Simulation Date', 'UNET D1 Cores', 'UNET Use Existing IB Tables',
909
- 'UNET 1D Methodology', 'UNET D2 Solver Type', 'UNET D2 Name', 'Run RASMapper'
987
+ plan_file_path = Path(plan_number_or_path)
988
+ if not plan_file_path.is_file():
989
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object=ras_obj)
990
+ if plan_file_path is None or not Path(plan_file_path).exists():
991
+ raise ValueError(f"Plan file not found: {plan_file_path}")
992
+
993
+ valid_intervals = [
994
+ '1SEC', '2SEC', '3SEC', '4SEC', '5SEC', '6SEC', '10SEC', '15SEC', '20SEC', '30SEC',
995
+ '1MIN', '2MIN', '3MIN', '4MIN', '5MIN', '6MIN', '10MIN', '15MIN', '20MIN', '30MIN',
996
+ '1HOUR', '2HOUR', '3HOUR', '4HOUR', '6HOUR', '8HOUR', '12HOUR', '1DAY'
997
+ ]
998
+
999
+ interval_mapping = {
1000
+ 'Computation Interval': computation_interval,
1001
+ 'Output Interval': output_interval,
1002
+ 'Instantaneous Interval': instantaneous_interval,
1003
+ 'Mapping Interval': mapping_interval
910
1004
  }
1005
+
1006
+ try:
1007
+ with open(plan_file_path, 'r') as file:
1008
+ lines = file.readlines()
1009
+
1010
+ for i, line in enumerate(lines):
1011
+ for key, value in interval_mapping.items():
1012
+ if value is not None:
1013
+ if value.upper() not in valid_intervals:
1014
+ raise ValueError(f"Invalid {key}: {value}. Must be one of {valid_intervals}")
1015
+ if line.strip().startswith(key):
1016
+ lines[i] = f"{key}={value.upper()}\n"
1017
+
1018
+ with open(plan_file_path, 'w') as file:
1019
+ file.writelines(lines)
1020
+
1021
+ logger = logging.getLogger(__name__)
1022
+ logger.info(f"Successfully updated intervals in plan file: {plan_file_path}")
1023
+
1024
+ except IOError as e:
1025
+ logger = logging.getLogger(__name__)
1026
+ logger.error(f"Error updating intervals in plan file {plan_file_path}: {e}")
1027
+ raise
1028
+
1029
+
1030
+ @log_call
1031
+ def update_plan_description(plan_number_or_path: Union[str, Path], description: str, ras_object: Optional['RasPrj'] = None) -> None:
1032
+ """
1033
+ Update the description in the plan file.
1034
+
1035
+ Args:
1036
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1037
+ description (str): The new description to be written to the plan file.
1038
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1039
+
1040
+ Raises:
1041
+ ValueError: If the plan file is not found.
1042
+ IOError: If there's an error reading from or writing to the plan file.
1043
+ """
911
1044
  logger = logging.getLogger(__name__)
912
- if key not in supported_plan_keys:
913
- logger.warning(f"Unknown key: {key}. Valid keys are: {', '.join(supported_plan_keys)}")
914
1045
 
915
1046
  plan_file_path = Path(plan_number_or_path)
916
1047
  if not plan_file_path.is_file():
@@ -925,43 +1056,165 @@ class RasPlan:
925
1056
  logger.error(f"Error reading plan file {plan_file_path}: {e}")
926
1057
  raise
927
1058
 
928
- # Special handling for description
929
- if key == 'description':
930
- description_start = None
931
- description_end = None
932
- for i, line in enumerate(lines):
933
- if line.strip() == 'Begin DESCRIPTION':
934
- description_start = i
935
- elif line.strip() == 'END DESCRIPTION':
936
- description_end = i
937
- break
938
- if description_start is not None and description_end is not None:
939
- lines[description_start+1:description_end] = [f"{value}\n"]
1059
+ start_index = None
1060
+ end_index = None
1061
+ comp_interval_index = None
1062
+ for i, line in enumerate(lines):
1063
+ if line.strip() == "BEGIN DESCRIPTION:":
1064
+ start_index = i
1065
+ elif line.strip() == "END DESCRIPTION:":
1066
+ end_index = i
1067
+ elif line.strip().startswith("Computation Interval="):
1068
+ comp_interval_index = i
1069
+
1070
+ if start_index is not None and end_index is not None:
1071
+ # Description exists, update it
1072
+ new_lines = lines[:start_index + 1]
1073
+ if description:
1074
+ new_lines.extend(description.split('\n'))
940
1075
  else:
941
- lines.append(f"Begin DESCRIPTION\n{value}\nEND DESCRIPTION\n")
1076
+ new_lines.append('\n')
1077
+ new_lines.extend(lines[end_index:])
942
1078
  else:
943
- # For other keys
944
- pattern = f"{key.replace('_', ' ').title()}="
1079
+ # Description doesn't exist, insert before Computation Interval
1080
+ if comp_interval_index is None:
1081
+ logger.warning("Neither description tags nor Computation Interval found in plan file. Appending to end of file.")
1082
+ comp_interval_index = len(lines)
1083
+
1084
+ new_lines = lines[:comp_interval_index]
1085
+ new_lines.append("BEGIN DESCRIPTION:\n")
1086
+ if description:
1087
+ new_lines.extend(f"{line}\n" for line in description.split('\n'))
1088
+ else:
1089
+ new_lines.append('\n')
1090
+ new_lines.append("END DESCRIPTION:\n")
1091
+ new_lines.extend(lines[comp_interval_index:])
1092
+
1093
+ try:
1094
+ with open(plan_file_path, 'w') as file:
1095
+ file.writelines(new_lines)
1096
+ logger.info(f"Updated description in plan file: {plan_file_path}")
1097
+ except IOError as e:
1098
+ logger.error(f"Error writing to plan file {plan_file_path}: {e}")
1099
+ raise
1100
+
1101
+ # Refresh RasPrj dataframes
1102
+ if ras_object:
1103
+ ras_object.plan_df = ras_object.get_plan_entries()
1104
+ ras_object.geom_df = ras_object.get_geom_entries()
1105
+ ras_object.flow_df = ras_object.get_flow_entries()
1106
+ ras_object.unsteady_df = ras_object.get_unsteady_entries()
1107
+
1108
+ @staticmethod
1109
+ @log_call
1110
+ def read_plan_description(plan_number_or_path: Union[str, Path], ras_object: Optional['RasPrj'] = None) -> str:
1111
+ """
1112
+ Read the description from the plan file.
1113
+
1114
+ Args:
1115
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1116
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1117
+
1118
+ Returns:
1119
+ str: The description from the plan file.
1120
+
1121
+ Raises:
1122
+ ValueError: If the plan file is not found.
1123
+ IOError: If there's an error reading from the plan file.
1124
+ """
1125
+ logger = logging.getLogger(__name__)
1126
+
1127
+ plan_file_path = Path(plan_number_or_path)
1128
+ if not plan_file_path.is_file():
1129
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object)
1130
+ if plan_file_path is None or not Path(plan_file_path).exists():
1131
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1132
+
1133
+ try:
1134
+ with open(plan_file_path, 'r') as file:
1135
+ lines = file.readlines()
1136
+ except IOError as e:
1137
+ logger.error(f"Error reading plan file {plan_file_path}: {e}")
1138
+ raise
1139
+
1140
+ description_lines = []
1141
+ in_description = False
1142
+ description_found = False
1143
+ for line in lines:
1144
+ if line.strip() == "BEGIN DESCRIPTION:":
1145
+ in_description = True
1146
+ description_found = True
1147
+ elif line.strip() == "END DESCRIPTION:":
1148
+ break
1149
+ elif in_description:
1150
+ description_lines.append(line.strip())
1151
+
1152
+ if not description_found:
1153
+ logger.warning(f"No description found in plan file: {plan_file_path}")
1154
+ return ""
1155
+
1156
+ description = '\n'.join(description_lines)
1157
+ logger.info(f"Read description from plan file: {plan_file_path}")
1158
+ return description
1159
+
1160
+
1161
+
1162
+
1163
+ @staticmethod
1164
+ @log_call
1165
+ def update_simulation_date(plan_number_or_path: Union[str, Path], start_date: datetime, end_date: datetime, ras_object: Optional['RasPrj'] = None) -> None:
1166
+ """
1167
+ Update the simulation date for a given plan.
1168
+
1169
+ Args:
1170
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1171
+ start_date (datetime): The start date and time for the simulation.
1172
+ end_date (datetime): The end date and time for the simulation.
1173
+ ras_object (Optional['RasPrj']): The RAS project object. Defaults to None.
1174
+
1175
+ Raises:
1176
+ ValueError: If the plan file is not found or if there's an error updating the file.
1177
+ """
1178
+
1179
+ # Get the plan file path
1180
+ plan_file_path = Path(plan_number_or_path)
1181
+ if not plan_file_path.is_file():
1182
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object)
1183
+ if plan_file_path is None or not Path(plan_file_path).exists():
1184
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1185
+
1186
+ # Format the dates
1187
+ formatted_date = f"{start_date.strftime('%d%b%Y').upper()},{start_date.strftime('%H%M')},{end_date.strftime('%d%b%Y').upper()},{end_date.strftime('%H%M')}"
1188
+
1189
+ try:
1190
+ # Read the file
1191
+ with open(plan_file_path, 'r') as file:
1192
+ lines = file.readlines()
1193
+
1194
+ # Update the Simulation Date line
945
1195
  updated = False
946
1196
  for i, line in enumerate(lines):
947
- if line.startswith(pattern):
948
- lines[i] = f"{pattern}{value}\n"
1197
+ if line.startswith("Simulation Date="):
1198
+ lines[i] = f"Simulation Date={formatted_date}\n"
949
1199
  updated = True
950
1200
  break
1201
+
1202
+ # If Simulation Date line not found, add it at the end
951
1203
  if not updated:
952
- logger.error(f"Key '{key}' not found in the plan file.")
953
- return
1204
+ lines.append(f"Simulation Date={formatted_date}\n")
954
1205
 
955
- try:
1206
+ # Write the updated content back to the file
956
1207
  with open(plan_file_path, 'w') as file:
957
1208
  file.writelines(lines)
958
- logger.info(f"Updated {key} in plan file: {plan_file_path}")
1209
+
1210
+ logger.info(f"Updated simulation date in plan file: {plan_file_path}")
1211
+
959
1212
  except IOError as e:
960
- logger.error(f"Error writing to plan file {plan_file_path}: {e}")
961
- raise
1213
+ logger.error(f"Error updating simulation date in plan file {plan_file_path}: {e}")
1214
+ raise ValueError(f"Error updating simulation date: {e}")
962
1215
 
963
1216
  # Refresh RasPrj dataframes
964
- ras_obj.plan_df = ras_obj.get_plan_entries()
965
- ras_obj.geom_df = ras_obj.get_geom_entries()
966
- ras_obj.flow_df = ras_obj.get_flow_entries()
967
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
1217
+ if ras_object:
1218
+ ras_object.plan_df = ras_object.get_plan_entries()
1219
+ ras_object.unsteady_df = ras_object.get_unsteady_entries()
1220
+
@@ -0,0 +1,21 @@
1
+ """
2
+ RasToGo module provides functions to interface HEC-RAS with go-consequences.
3
+ This module helps prepare and format RAS data for use with go-consequences.
4
+ """
5
+
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Any, Tuple, Union
9
+ import pandas as pd
10
+ import numpy as np
11
+
12
+ from .Decorators import log_call, standardize_input
13
+ from .LoggingConfig import setup_logging, get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ class RasToGo:
18
+ """Class containing functions to interface HEC-RAS with go-consequences."""
19
+
20
+ #@staticmethod
21
+ #@log_call