ras-commander 0.70.0__py3-none-any.whl → 0.72.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.
@@ -0,0 +1,252 @@
1
+ """
2
+ RasMap - Parses HEC-RAS mapper configuration files (.rasmap)
3
+
4
+ This module provides functionality to extract and organize information from
5
+ HEC-RAS mapper configuration files, including paths to terrain, soil, and land cover data.
6
+
7
+ This module is part of the ras-commander library and uses a centralized logging configuration.
8
+
9
+ Logging Configuration:
10
+ - The logging is set up in the logging_config.py file.
11
+ - A @log_call decorator is available to automatically log function calls.
12
+ - Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
13
+
14
+ Classes:
15
+ RasMap: Class for parsing and accessing HEC-RAS mapper configuration.
16
+ """
17
+
18
+ import os
19
+ import re
20
+ import xml.etree.ElementTree as ET
21
+ from pathlib import Path
22
+ import pandas as pd
23
+ from typing import Union, Optional, Dict, List, Any
24
+
25
+ from .RasPrj import ras
26
+ from .LoggingConfig import get_logger
27
+ from .Decorators import log_call
28
+
29
+ logger = get_logger(__name__)
30
+
31
+ class RasMap:
32
+ """
33
+ Class for parsing and accessing information from HEC-RAS mapper configuration files (.rasmap).
34
+
35
+ This class provides methods to extract paths to terrain, soil, land cover data,
36
+ and various project settings from the .rasmap file associated with a HEC-RAS project.
37
+ """
38
+
39
+ @staticmethod
40
+ @log_call
41
+ def parse_rasmap(rasmap_path: Union[str, Path], ras_object=None) -> pd.DataFrame:
42
+ """
43
+ Parse a .rasmap file and extract relevant information.
44
+
45
+ Args:
46
+ rasmap_path (Union[str, Path]): Path to the .rasmap file.
47
+ ras_object: Optional RAS object instance.
48
+
49
+ Returns:
50
+ pd.DataFrame: DataFrame containing extracted information from the .rasmap file.
51
+ """
52
+ ras_obj = ras_object or ras
53
+ ras_obj.check_initialized()
54
+
55
+ rasmap_path = Path(rasmap_path)
56
+ if not rasmap_path.exists():
57
+ logger.error(f"RASMapper file not found: {rasmap_path}")
58
+ # Create a single row DataFrame with all empty values
59
+ return pd.DataFrame({
60
+ 'projection_path': [None],
61
+ 'profile_lines_path': [[]],
62
+ 'soil_layer_path': [[]],
63
+ 'infiltration_hdf_path': [[]],
64
+ 'landcover_hdf_path': [[]],
65
+ 'terrain_hdf_path': [[]],
66
+ 'current_settings': [{}]
67
+ })
68
+
69
+ try:
70
+ # Initialize data for the DataFrame - just one row with lists
71
+ data = {
72
+ 'projection_path': [None],
73
+ 'profile_lines_path': [[]],
74
+ 'soil_layer_path': [[]],
75
+ 'infiltration_hdf_path': [[]],
76
+ 'landcover_hdf_path': [[]],
77
+ 'terrain_hdf_path': [[]],
78
+ 'current_settings': [{}]
79
+ }
80
+
81
+ # Read the file content
82
+ with open(rasmap_path, 'r', encoding='utf-8') as f:
83
+ xml_content = f.read()
84
+
85
+ # Check if it's a valid XML file
86
+ if not xml_content.strip().startswith('<'):
87
+ logger.error(f"File does not appear to be valid XML: {rasmap_path}")
88
+ return pd.DataFrame(data)
89
+
90
+ # Parse the XML file
91
+ try:
92
+ tree = ET.parse(rasmap_path)
93
+ root = tree.getroot()
94
+ except ET.ParseError as e:
95
+ logger.error(f"Error parsing XML in {rasmap_path}: {e}")
96
+ return pd.DataFrame(data)
97
+
98
+ # Helper function to convert relative paths to absolute paths
99
+ def to_absolute_path(relative_path: str) -> str:
100
+ if not relative_path:
101
+ return None
102
+ # Remove any leading .\ or ./
103
+ relative_path = relative_path.lstrip('.\\').lstrip('./')
104
+ # Convert to absolute path relative to project folder
105
+ return str(ras_obj.project_folder / relative_path)
106
+
107
+ # Extract projection path
108
+ try:
109
+ projection_elem = root.find(".//RASProjectionFilename")
110
+ if projection_elem is not None and 'Filename' in projection_elem.attrib:
111
+ data['projection_path'][0] = to_absolute_path(projection_elem.attrib['Filename'])
112
+ except Exception as e:
113
+ logger.warning(f"Error extracting projection path: {e}")
114
+
115
+ # Extract profile lines path
116
+ try:
117
+ profile_lines_elem = root.find(".//Features/Layer[@Name='Profile Lines']")
118
+ if profile_lines_elem is not None and 'Filename' in profile_lines_elem.attrib:
119
+ data['profile_lines_path'][0].append(to_absolute_path(profile_lines_elem.attrib['Filename']))
120
+ except Exception as e:
121
+ logger.warning(f"Error extracting profile lines path: {e}")
122
+
123
+ # Extract soil layer paths
124
+ try:
125
+ soil_layers = root.findall(".//Layer[@Name='Hydrologic Soil Groups']")
126
+ for layer in soil_layers:
127
+ if 'Filename' in layer.attrib:
128
+ data['soil_layer_path'][0].append(to_absolute_path(layer.attrib['Filename']))
129
+ except Exception as e:
130
+ logger.warning(f"Error extracting soil layer paths: {e}")
131
+
132
+ # Extract infiltration HDF paths
133
+ try:
134
+ infiltration_layers = root.findall(".//Layer[@Name='Infiltration']")
135
+ for layer in infiltration_layers:
136
+ if 'Filename' in layer.attrib:
137
+ data['infiltration_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
138
+ except Exception as e:
139
+ logger.warning(f"Error extracting infiltration HDF paths: {e}")
140
+
141
+ # Extract landcover HDF paths
142
+ try:
143
+ landcover_layers = root.findall(".//Layer[@Name='LandCover']")
144
+ for layer in landcover_layers:
145
+ if 'Filename' in layer.attrib:
146
+ data['landcover_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
147
+ except Exception as e:
148
+ logger.warning(f"Error extracting landcover HDF paths: {e}")
149
+
150
+ # Extract terrain HDF paths
151
+ try:
152
+ terrain_layers = root.findall(".//Terrains/Layer")
153
+ for layer in terrain_layers:
154
+ if 'Filename' in layer.attrib:
155
+ data['terrain_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
156
+ except Exception as e:
157
+ logger.warning(f"Error extracting terrain HDF paths: {e}")
158
+
159
+ # Extract current settings
160
+ current_settings = {}
161
+ try:
162
+ settings_elem = root.find(".//CurrentSettings")
163
+ if settings_elem is not None:
164
+ # Extract ProjectSettings
165
+ project_settings_elem = settings_elem.find("ProjectSettings")
166
+ if project_settings_elem is not None:
167
+ for child in project_settings_elem:
168
+ current_settings[child.tag] = child.text
169
+
170
+ # Extract Folders
171
+ folders_elem = settings_elem.find("Folders")
172
+ if folders_elem is not None:
173
+ for child in folders_elem:
174
+ current_settings[child.tag] = child.text
175
+
176
+ data['current_settings'][0] = current_settings
177
+ except Exception as e:
178
+ logger.warning(f"Error extracting current settings: {e}")
179
+
180
+ # Create DataFrame
181
+ df = pd.DataFrame(data)
182
+ logger.info(f"Successfully parsed RASMapper file: {rasmap_path}")
183
+ return df
184
+
185
+ except Exception as e:
186
+ logger.error(f"Unexpected error processing RASMapper file {rasmap_path}: {e}")
187
+ # Create a single row DataFrame with all empty values
188
+ return pd.DataFrame({
189
+ 'projection_path': [None],
190
+ 'profile_lines_path': [[]],
191
+ 'soil_layer_path': [[]],
192
+ 'infiltration_hdf_path': [[]],
193
+ 'landcover_hdf_path': [[]],
194
+ 'terrain_hdf_path': [[]],
195
+ 'current_settings': [{}]
196
+ })
197
+
198
+ @staticmethod
199
+ @log_call
200
+ def get_rasmap_path(ras_object=None) -> Optional[Path]:
201
+ """
202
+ Get the path to the .rasmap file based on the current project.
203
+
204
+ Args:
205
+ ras_object: Optional RAS object instance.
206
+
207
+ Returns:
208
+ Optional[Path]: Path to the .rasmap file if found, None otherwise.
209
+ """
210
+ ras_obj = ras_object or ras
211
+ ras_obj.check_initialized()
212
+
213
+ project_name = ras_obj.project_name
214
+ project_folder = ras_obj.project_folder
215
+ rasmap_path = project_folder / f"{project_name}.rasmap"
216
+
217
+ if not rasmap_path.exists():
218
+ logger.warning(f"RASMapper file not found: {rasmap_path}")
219
+ return None
220
+
221
+ return rasmap_path
222
+
223
+ @staticmethod
224
+ @log_call
225
+ def initialize_rasmap_df(ras_object=None) -> pd.DataFrame:
226
+ """
227
+ Initialize the rasmap_df as part of project initialization.
228
+
229
+ Args:
230
+ ras_object: Optional RAS object instance.
231
+
232
+ Returns:
233
+ pd.DataFrame: DataFrame containing information from the .rasmap file.
234
+ """
235
+ ras_obj = ras_object or ras
236
+ ras_obj.check_initialized()
237
+
238
+ rasmap_path = RasMap.get_rasmap_path(ras_obj)
239
+ if rasmap_path is None:
240
+ logger.warning("No .rasmap file found for this project. Creating empty rasmap_df.")
241
+ # Create a single row DataFrame with all empty values
242
+ return pd.DataFrame({
243
+ 'projection_path': [None],
244
+ 'profile_lines_path': [[]],
245
+ 'soil_layer_path': [[]],
246
+ 'infiltration_hdf_path': [[]],
247
+ 'landcover_hdf_path': [[]],
248
+ 'terrain_hdf_path': [[]],
249
+ 'current_settings': [{}]
250
+ })
251
+
252
+ return RasMap.parse_rasmap(rasmap_path, ras_obj)
ras_commander/RasPlan.py CHANGED
@@ -87,7 +87,7 @@ class RasPlan:
87
87
  @log_call
88
88
  def set_geom(plan_number: Union[str, int], new_geom: Union[str, int], ras_object=None) -> pd.DataFrame:
89
89
  """
90
- Set the geometry for the specified plan.
90
+ Set the geometry for the specified plan by updating only the plan file.
91
91
 
92
92
  Parameters:
93
93
  plan_number (Union[str, int]): The plan number to update.
@@ -101,7 +101,8 @@ class RasPlan:
101
101
  updated_geom_df = RasPlan.set_geom('02', '03')
102
102
 
103
103
  Note:
104
- This function updates the ras object's dataframes after modifying the project structure.
104
+ This function updates the Geom File= line in the plan file and
105
+ updates the ras object's dataframes without modifying the PRJ file.
105
106
  """
106
107
  ras_obj = ras_object or ras
107
108
  ras_obj.check_initialized()
@@ -112,16 +113,37 @@ class RasPlan:
112
113
  # Update all dataframes
113
114
  ras_obj.plan_df = ras_obj.get_plan_entries()
114
115
  ras_obj.geom_df = ras_obj.get_geom_entries()
115
- ras_obj.flow_df = ras_obj.get_flow_entries()
116
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
117
116
 
118
117
  if new_geom not in ras_obj.geom_df['geom_number'].values:
119
118
  logger.error(f"Geometry {new_geom} not found in project.")
120
119
  raise ValueError(f"Geometry {new_geom} not found in project.")
121
120
 
122
- # Update all geometry-related columns
121
+ # Get the plan file path
122
+ plan_file_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{plan_number}"
123
+ if not plan_file_path.exists():
124
+ logger.error(f"Plan file not found: {plan_file_path}")
125
+ raise ValueError(f"Plan file not found: {plan_file_path}")
126
+
127
+ # Read the plan file and update the Geom File line
128
+ try:
129
+ with open(plan_file_path, 'r') as file:
130
+ lines = file.readlines()
131
+
132
+ for i, line in enumerate(lines):
133
+ if line.startswith("Geom File="):
134
+ lines[i] = f"Geom File=g{new_geom}\n"
135
+ logger.info(f"Updated Geom File in plan file to g{new_geom} for plan {plan_number}")
136
+ break
137
+
138
+ with open(plan_file_path, 'w') as file:
139
+ file.writelines(lines)
140
+ except Exception as e:
141
+ logger.error(f"Error updating plan file: {e}")
142
+ raise
143
+ # Update the plan_df without reinitializing
123
144
  mask = ras_obj.plan_df['plan_number'] == plan_number
124
145
  ras_obj.plan_df.loc[mask, 'geom_number'] = new_geom
146
+ ras_obj.plan_df.loc[mask, 'geometry_number'] = new_geom # Update geometry_number column
125
147
  ras_obj.plan_df.loc[mask, 'Geom File'] = f"g{new_geom}"
126
148
  geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom}"
127
149
  ras_obj.plan_df.loc[mask, 'Geom Path'] = str(geom_path)
@@ -130,27 +152,8 @@ class RasPlan:
130
152
  logger.debug("Updated plan DataFrame:")
131
153
  logger.debug(ras_obj.plan_df)
132
154
 
133
- # Update project file and reinitialize
134
- RasUtils.update_file(ras_obj.prj_file, RasPlan._update_geom_in_file, plan_number, new_geom)
135
- ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
136
-
137
155
  return ras_obj.plan_df
138
156
 
139
- @staticmethod
140
- def _update_geom_in_file(lines, plan_number, new_geom):
141
- plan_pattern = re.compile(rf"^Plan File=p{plan_number}", re.IGNORECASE)
142
- geom_pattern = re.compile(r"^Geom File=g\d+", re.IGNORECASE)
143
-
144
- for i, line in enumerate(lines):
145
- if plan_pattern.match(line):
146
- for j in range(i+1, len(lines)):
147
- if geom_pattern.match(lines[j]):
148
- lines[j] = f"Geom File=g{new_geom}\n"
149
- logger.info(f"Updated Geom File in project file to g{new_geom} for plan {plan_number}")
150
- break
151
- break
152
- return lines
153
-
154
157
  @staticmethod
155
158
  @log_call
156
159
  def set_steady(plan_number: str, new_steady_flow_number: str, ras_object=None):
@@ -969,7 +972,7 @@ class RasPlan:
969
972
  return None
970
973
 
971
974
 
972
- # NEW FUNCTIONS THAT NEED TESTING AND EXAMPLES
975
+
973
976
 
974
977
 
975
978
  @staticmethod