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.
- ras_commander/HdfInfiltration.py +696 -416
- ras_commander/RasCmdr.py +189 -40
- ras_commander/RasGeo.py +407 -21
- ras_commander/RasMap.py +252 -0
- ras_commander/RasPlan.py +28 -25
- ras_commander/RasPrj.py +243 -79
- ras_commander/RasUnsteady.py +162 -68
- ras_commander/__init__.py +1 -1
- {ras_commander-0.70.0.dist-info → ras_commander-0.72.0.dist-info}/METADATA +15 -5
- {ras_commander-0.70.0.dist-info → ras_commander-0.72.0.dist-info}/RECORD +13 -15
- {ras_commander-0.70.0.dist-info → ras_commander-0.72.0.dist-info}/WHEEL +1 -1
- ras_commander/RasGpt.py +0 -27
- ras_commander/RasMapper.py +0 -24
- ras_commander/RasToGo.py +0 -37
- {ras_commander-0.70.0.dist-info → ras_commander-0.72.0.dist-info/licenses}/LICENSE +0 -0
- {ras_commander-0.70.0.dist-info → ras_commander-0.72.0.dist-info}/top_level.txt +0 -0
ras_commander/RasMap.py
ADDED
@@ -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
|
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
|
-
#
|
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
|
-
|
975
|
+
|
973
976
|
|
974
977
|
|
975
978
|
@staticmethod
|