ras-commander 0.77.0__py3-none-any.whl → 0.78.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/RasExamples.py +121 -2
- ras_commander/RasGeo.py +2 -2
- ras_commander/RasMap.py +467 -252
- ras_commander/RasPrj.py +2 -1
- {ras_commander-0.77.0.dist-info → ras_commander-0.78.0.dist-info}/METADATA +2 -3
- {ras_commander-0.77.0.dist-info → ras_commander-0.78.0.dist-info}/RECORD +9 -9
- {ras_commander-0.77.0.dist-info → ras_commander-0.78.0.dist-info}/WHEEL +1 -1
- {ras_commander-0.77.0.dist-info → ras_commander-0.78.0.dist-info}/licenses/LICENSE +0 -0
- {ras_commander-0.77.0.dist-info → ras_commander-0.78.0.dist-info}/top_level.txt +0 -0
ras_commander/RasExamples.py
CHANGED
@@ -69,6 +69,12 @@ class RasExamples:
|
|
69
69
|
examples_dir = base_dir
|
70
70
|
projects_dir = examples_dir / 'example_projects'
|
71
71
|
csv_file_path = examples_dir / 'example_projects.csv'
|
72
|
+
|
73
|
+
# Special projects that are not in the main zip file
|
74
|
+
SPECIAL_PROJECTS = {
|
75
|
+
'NewOrleansMetro': 'https://www.hec.usace.army.mil/confluence/rasdocs/hgt/files/latest/299502039/299502111/1/1747692522764/NewOrleansMetroPipesExample.zip',
|
76
|
+
'BeaverLake': 'https://www.hec.usace.army.mil/confluence/rasdocs/hgt/files/latest/299501780/299502090/1/1747692179014/BeaverLake-SWMM-Import-Solution.zip'
|
77
|
+
}
|
72
78
|
|
73
79
|
_folder_df = None
|
74
80
|
_zip_file_path = None
|
@@ -148,6 +154,17 @@ class RasExamples:
|
|
148
154
|
extracted_paths = []
|
149
155
|
|
150
156
|
for project_name in project_names:
|
157
|
+
# Check if this is a special project
|
158
|
+
if project_name in cls.SPECIAL_PROJECTS:
|
159
|
+
try:
|
160
|
+
special_path = cls._extract_special_project(project_name)
|
161
|
+
extracted_paths.append(special_path)
|
162
|
+
continue
|
163
|
+
except Exception as e:
|
164
|
+
logger.error(f"Failed to extract special project '{project_name}': {e}")
|
165
|
+
continue
|
166
|
+
|
167
|
+
# Regular project extraction logic
|
151
168
|
logger.info("----- RasExamples Extracting Project -----")
|
152
169
|
logger.info(f"Extracting project '{project_name}'")
|
153
170
|
project_path = cls.projects_dir
|
@@ -319,6 +336,9 @@ class RasExamples:
|
|
319
336
|
def list_projects(cls, category=None):
|
320
337
|
"""
|
321
338
|
List all projects or projects in a specific category.
|
339
|
+
|
340
|
+
Note: Special projects (NewOrleansMetro, BeaverLake) are also available but not listed
|
341
|
+
in categories as they are downloaded separately.
|
322
342
|
"""
|
323
343
|
if cls._folder_df is None:
|
324
344
|
logger.warning("No projects available. Make sure the zip file is properly loaded.")
|
@@ -328,7 +348,10 @@ class RasExamples:
|
|
328
348
|
logger.info(f"Projects in category '{category}': {', '.join(projects)}")
|
329
349
|
else:
|
330
350
|
projects = cls._folder_df['Project'].unique()
|
331
|
-
|
351
|
+
# Add special projects to the list
|
352
|
+
all_projects = list(projects) + list(cls.SPECIAL_PROJECTS.keys())
|
353
|
+
logger.info(f"All available projects: {', '.join(all_projects)}")
|
354
|
+
return all_projects
|
332
355
|
return projects.tolist()
|
333
356
|
|
334
357
|
@classmethod
|
@@ -421,4 +444,100 @@ class RasExamples:
|
|
421
444
|
raise ValueError(f"Invalid size string: {size_str}")
|
422
445
|
|
423
446
|
number, unit = float(re.findall(r'[\d\.]+', size_str)[0]), re.findall(r'[BKMGT]B?', size_str)[0]
|
424
|
-
return int(number * units[unit])
|
447
|
+
return int(number * units[unit])
|
448
|
+
|
449
|
+
@classmethod
|
450
|
+
def _extract_special_project(cls, project_name: str) -> Path:
|
451
|
+
"""
|
452
|
+
Download and extract special projects that are not in the main zip file.
|
453
|
+
|
454
|
+
Args:
|
455
|
+
project_name: Name of the special project ('NewOrleansMetro' or 'BeaverLake')
|
456
|
+
|
457
|
+
Returns:
|
458
|
+
Path: Path to the extracted project directory
|
459
|
+
|
460
|
+
Raises:
|
461
|
+
ValueError: If the project is not a recognized special project
|
462
|
+
"""
|
463
|
+
if project_name not in cls.SPECIAL_PROJECTS:
|
464
|
+
raise ValueError(f"'{project_name}' is not a recognized special project")
|
465
|
+
|
466
|
+
logger.info(f"----- RasExamples Extracting Special Project -----")
|
467
|
+
logger.info(f"Extracting special project '{project_name}'")
|
468
|
+
|
469
|
+
# Create the project directory
|
470
|
+
project_path = cls.projects_dir / project_name
|
471
|
+
|
472
|
+
# Check if already exists
|
473
|
+
if project_path.exists():
|
474
|
+
logger.info(f"Special project '{project_name}' already exists. Deleting existing folder...")
|
475
|
+
try:
|
476
|
+
shutil.rmtree(project_path)
|
477
|
+
logger.info(f"Existing folder for project '{project_name}' has been deleted.")
|
478
|
+
except Exception as e:
|
479
|
+
logger.error(f"Failed to delete existing project folder '{project_name}': {e}")
|
480
|
+
raise
|
481
|
+
|
482
|
+
# Create the project directory
|
483
|
+
project_path.mkdir(parents=True, exist_ok=True)
|
484
|
+
|
485
|
+
# Download the zip file
|
486
|
+
url = cls.SPECIAL_PROJECTS[project_name]
|
487
|
+
zip_file_path = cls.projects_dir / f"{project_name}_temp.zip"
|
488
|
+
|
489
|
+
logger.info(f"Downloading special project from: {url}")
|
490
|
+
logger.info("This may take a few moments...")
|
491
|
+
|
492
|
+
try:
|
493
|
+
response = requests.get(url, stream=True, timeout=300)
|
494
|
+
response.raise_for_status()
|
495
|
+
|
496
|
+
# Get total file size if available
|
497
|
+
total_size = int(response.headers.get('content-length', 0))
|
498
|
+
|
499
|
+
# Download with progress bar
|
500
|
+
with open(zip_file_path, 'wb') as file:
|
501
|
+
if total_size > 0:
|
502
|
+
with tqdm(
|
503
|
+
desc=f"Downloading {project_name}",
|
504
|
+
total=total_size,
|
505
|
+
unit='iB',
|
506
|
+
unit_scale=True,
|
507
|
+
unit_divisor=1024,
|
508
|
+
) as progress_bar:
|
509
|
+
for chunk in response.iter_content(chunk_size=8192):
|
510
|
+
size = file.write(chunk)
|
511
|
+
progress_bar.update(size)
|
512
|
+
else:
|
513
|
+
# No content length, download without progress bar
|
514
|
+
for chunk in response.iter_content(chunk_size=8192):
|
515
|
+
file.write(chunk)
|
516
|
+
|
517
|
+
logger.info(f"Downloaded special project zip file to {zip_file_path}")
|
518
|
+
|
519
|
+
except requests.exceptions.RequestException as e:
|
520
|
+
logger.error(f"Failed to download special project '{project_name}': {e}")
|
521
|
+
if zip_file_path.exists():
|
522
|
+
zip_file_path.unlink()
|
523
|
+
raise
|
524
|
+
|
525
|
+
# Extract the zip file directly to the project directory
|
526
|
+
try:
|
527
|
+
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
|
528
|
+
# Extract directly to the project directory (no internal folder structure)
|
529
|
+
zip_ref.extractall(project_path)
|
530
|
+
logger.info(f"Successfully extracted special project '{project_name}' to {project_path}")
|
531
|
+
|
532
|
+
except Exception as e:
|
533
|
+
logger.error(f"Failed to extract special project '{project_name}': {e}")
|
534
|
+
if project_path.exists():
|
535
|
+
shutil.rmtree(project_path)
|
536
|
+
raise
|
537
|
+
finally:
|
538
|
+
# Clean up the temporary zip file
|
539
|
+
if zip_file_path.exists():
|
540
|
+
zip_file_path.unlink()
|
541
|
+
logger.debug(f"Removed temporary zip file: {zip_file_path}")
|
542
|
+
|
543
|
+
return project_path
|
ras_commander/RasGeo.py
CHANGED
@@ -206,10 +206,10 @@ class RasGeo:
|
|
206
206
|
|
207
207
|
# Create DataFrame
|
208
208
|
if base_table_rows:
|
209
|
-
df = pd.DataFrame(base_table_rows, columns=['Table Number', 'Land Cover Name', 'Base
|
209
|
+
df = pd.DataFrame(base_table_rows, columns=['Table Number', 'Land Cover Name', 'Base Mannings n Value'])
|
210
210
|
return df
|
211
211
|
else:
|
212
|
-
return pd.DataFrame(columns=['Table Number', 'Land Cover Name', 'Base
|
212
|
+
return pd.DataFrame(columns=['Table Number', 'Land Cover Name', 'Base Mannings n Value'])
|
213
213
|
|
214
214
|
|
215
215
|
@staticmethod
|
ras_commander/RasMap.py
CHANGED
@@ -1,252 +1,467 @@
|
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
-
|
12
|
-
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
import
|
20
|
-
import
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
from
|
26
|
-
|
27
|
-
from .
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
'
|
66
|
-
'
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
'
|
78
|
-
'
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
return
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
'
|
195
|
-
'
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
'
|
249
|
-
'
|
250
|
-
|
251
|
-
|
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
|
+
It also includes functions to automate the post-processing of stored maps.
|
7
|
+
|
8
|
+
This module is part of the ras-commander library and uses a centralized logging configuration.
|
9
|
+
|
10
|
+
Logging Configuration:
|
11
|
+
- The logging is set up in the logging_config.py file.
|
12
|
+
- A @log_call decorator is available to automatically log function calls.
|
13
|
+
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
14
|
+
|
15
|
+
Classes:
|
16
|
+
RasMap: Class for parsing and accessing HEC-RAS mapper configuration.
|
17
|
+
"""
|
18
|
+
|
19
|
+
import os
|
20
|
+
import re
|
21
|
+
import xml.etree.ElementTree as ET
|
22
|
+
from pathlib import Path
|
23
|
+
import pandas as pd
|
24
|
+
import shutil
|
25
|
+
from typing import Union, Optional, Dict, List, Any
|
26
|
+
|
27
|
+
from .RasPrj import ras
|
28
|
+
from .RasPlan import RasPlan
|
29
|
+
from .RasCmdr import RasCmdr
|
30
|
+
from .LoggingConfig import get_logger
|
31
|
+
from .Decorators import log_call
|
32
|
+
|
33
|
+
logger = get_logger(__name__)
|
34
|
+
|
35
|
+
class RasMap:
|
36
|
+
"""
|
37
|
+
Class for parsing and accessing information from HEC-RAS mapper configuration files (.rasmap).
|
38
|
+
|
39
|
+
This class provides methods to extract paths to terrain, soil, land cover data,
|
40
|
+
and various project settings from the .rasmap file associated with a HEC-RAS project.
|
41
|
+
It also includes functionality to automate the post-processing of stored maps.
|
42
|
+
"""
|
43
|
+
|
44
|
+
@staticmethod
|
45
|
+
@log_call
|
46
|
+
def parse_rasmap(rasmap_path: Union[str, Path], ras_object=None) -> pd.DataFrame:
|
47
|
+
"""
|
48
|
+
Parse a .rasmap file and extract relevant information.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
rasmap_path (Union[str, Path]): Path to the .rasmap file.
|
52
|
+
ras_object: Optional RAS object instance.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
pd.DataFrame: DataFrame containing extracted information from the .rasmap file.
|
56
|
+
"""
|
57
|
+
ras_obj = ras_object or ras
|
58
|
+
ras_obj.check_initialized()
|
59
|
+
|
60
|
+
rasmap_path = Path(rasmap_path)
|
61
|
+
if not rasmap_path.exists():
|
62
|
+
logger.error(f"RASMapper file not found: {rasmap_path}")
|
63
|
+
# Create a single row DataFrame with all empty values
|
64
|
+
return pd.DataFrame({
|
65
|
+
'projection_path': [None],
|
66
|
+
'profile_lines_path': [[]],
|
67
|
+
'soil_layer_path': [[]],
|
68
|
+
'infiltration_hdf_path': [[]],
|
69
|
+
'landcover_hdf_path': [[]],
|
70
|
+
'terrain_hdf_path': [[]],
|
71
|
+
'current_settings': [{}]
|
72
|
+
})
|
73
|
+
|
74
|
+
try:
|
75
|
+
# Initialize data for the DataFrame - just one row with lists
|
76
|
+
data = {
|
77
|
+
'projection_path': [None],
|
78
|
+
'profile_lines_path': [[]],
|
79
|
+
'soil_layer_path': [[]],
|
80
|
+
'infiltration_hdf_path': [[]],
|
81
|
+
'landcover_hdf_path': [[]],
|
82
|
+
'terrain_hdf_path': [[]],
|
83
|
+
'current_settings': [{}]
|
84
|
+
}
|
85
|
+
|
86
|
+
# Read the file content
|
87
|
+
with open(rasmap_path, 'r', encoding='utf-8') as f:
|
88
|
+
xml_content = f.read()
|
89
|
+
|
90
|
+
# Check if it's a valid XML file
|
91
|
+
if not xml_content.strip().startswith('<'):
|
92
|
+
logger.error(f"File does not appear to be valid XML: {rasmap_path}")
|
93
|
+
return pd.DataFrame(data)
|
94
|
+
|
95
|
+
# Parse the XML file
|
96
|
+
try:
|
97
|
+
tree = ET.parse(rasmap_path)
|
98
|
+
root = tree.getroot()
|
99
|
+
except ET.ParseError as e:
|
100
|
+
logger.error(f"Error parsing XML in {rasmap_path}: {e}")
|
101
|
+
return pd.DataFrame(data)
|
102
|
+
|
103
|
+
# Helper function to convert relative paths to absolute paths
|
104
|
+
def to_absolute_path(relative_path: str) -> str:
|
105
|
+
if not relative_path:
|
106
|
+
return None
|
107
|
+
# Remove any leading .\ or ./
|
108
|
+
relative_path = relative_path.lstrip('.\\').lstrip('./')
|
109
|
+
# Convert to absolute path relative to project folder
|
110
|
+
return str(ras_obj.project_folder / relative_path)
|
111
|
+
|
112
|
+
# Extract projection path
|
113
|
+
try:
|
114
|
+
projection_elem = root.find(".//RASProjectionFilename")
|
115
|
+
if projection_elem is not None and 'Filename' in projection_elem.attrib:
|
116
|
+
data['projection_path'][0] = to_absolute_path(projection_elem.attrib['Filename'])
|
117
|
+
except Exception as e:
|
118
|
+
logger.warning(f"Error extracting projection path: {e}")
|
119
|
+
|
120
|
+
# Extract profile lines path
|
121
|
+
try:
|
122
|
+
profile_lines_elem = root.find(".//Features/Layer[@Name='Profile Lines']")
|
123
|
+
if profile_lines_elem is not None and 'Filename' in profile_lines_elem.attrib:
|
124
|
+
data['profile_lines_path'][0].append(to_absolute_path(profile_lines_elem.attrib['Filename']))
|
125
|
+
except Exception as e:
|
126
|
+
logger.warning(f"Error extracting profile lines path: {e}")
|
127
|
+
|
128
|
+
# Extract soil layer paths
|
129
|
+
try:
|
130
|
+
soil_layers = root.findall(".//Layer[@Name='Hydrologic Soil Groups']")
|
131
|
+
for layer in soil_layers:
|
132
|
+
if 'Filename' in layer.attrib:
|
133
|
+
data['soil_layer_path'][0].append(to_absolute_path(layer.attrib['Filename']))
|
134
|
+
except Exception as e:
|
135
|
+
logger.warning(f"Error extracting soil layer paths: {e}")
|
136
|
+
|
137
|
+
# Extract infiltration HDF paths
|
138
|
+
try:
|
139
|
+
infiltration_layers = root.findall(".//Layer[@Name='Infiltration']")
|
140
|
+
for layer in infiltration_layers:
|
141
|
+
if 'Filename' in layer.attrib:
|
142
|
+
data['infiltration_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
|
143
|
+
except Exception as e:
|
144
|
+
logger.warning(f"Error extracting infiltration HDF paths: {e}")
|
145
|
+
|
146
|
+
# Extract landcover HDF paths
|
147
|
+
try:
|
148
|
+
landcover_layers = root.findall(".//Layer[@Name='LandCover']")
|
149
|
+
for layer in landcover_layers:
|
150
|
+
if 'Filename' in layer.attrib:
|
151
|
+
data['landcover_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
|
152
|
+
except Exception as e:
|
153
|
+
logger.warning(f"Error extracting landcover HDF paths: {e}")
|
154
|
+
|
155
|
+
# Extract terrain HDF paths
|
156
|
+
try:
|
157
|
+
terrain_layers = root.findall(".//Terrains/Layer")
|
158
|
+
for layer in terrain_layers:
|
159
|
+
if 'Filename' in layer.attrib:
|
160
|
+
data['terrain_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
|
161
|
+
except Exception as e:
|
162
|
+
logger.warning(f"Error extracting terrain HDF paths: {e}")
|
163
|
+
|
164
|
+
# Extract current settings
|
165
|
+
current_settings = {}
|
166
|
+
try:
|
167
|
+
settings_elem = root.find(".//CurrentSettings")
|
168
|
+
if settings_elem is not None:
|
169
|
+
# Extract ProjectSettings
|
170
|
+
project_settings_elem = settings_elem.find("ProjectSettings")
|
171
|
+
if project_settings_elem is not None:
|
172
|
+
for child in project_settings_elem:
|
173
|
+
current_settings[child.tag] = child.text
|
174
|
+
|
175
|
+
# Extract Folders
|
176
|
+
folders_elem = settings_elem.find("Folders")
|
177
|
+
if folders_elem is not None:
|
178
|
+
for child in folders_elem:
|
179
|
+
current_settings[child.tag] = child.text
|
180
|
+
|
181
|
+
data['current_settings'][0] = current_settings
|
182
|
+
except Exception as e:
|
183
|
+
logger.warning(f"Error extracting current settings: {e}")
|
184
|
+
|
185
|
+
# Create DataFrame
|
186
|
+
df = pd.DataFrame(data)
|
187
|
+
logger.info(f"Successfully parsed RASMapper file: {rasmap_path}")
|
188
|
+
return df
|
189
|
+
|
190
|
+
except Exception as e:
|
191
|
+
logger.error(f"Unexpected error processing RASMapper file {rasmap_path}: {e}")
|
192
|
+
# Create a single row DataFrame with all empty values
|
193
|
+
return pd.DataFrame({
|
194
|
+
'projection_path': [None],
|
195
|
+
'profile_lines_path': [[]],
|
196
|
+
'soil_layer_path': [[]],
|
197
|
+
'infiltration_hdf_path': [[]],
|
198
|
+
'landcover_hdf_path': [[]],
|
199
|
+
'terrain_hdf_path': [[]],
|
200
|
+
'current_settings': [{}]
|
201
|
+
})
|
202
|
+
|
203
|
+
@staticmethod
|
204
|
+
@log_call
|
205
|
+
def get_rasmap_path(ras_object=None) -> Optional[Path]:
|
206
|
+
"""
|
207
|
+
Get the path to the .rasmap file based on the current project.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
ras_object: Optional RAS object instance.
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
Optional[Path]: Path to the .rasmap file if found, None otherwise.
|
214
|
+
"""
|
215
|
+
ras_obj = ras_object or ras
|
216
|
+
ras_obj.check_initialized()
|
217
|
+
|
218
|
+
project_name = ras_obj.project_name
|
219
|
+
project_folder = ras_obj.project_folder
|
220
|
+
rasmap_path = project_folder / f"{project_name}.rasmap"
|
221
|
+
|
222
|
+
if not rasmap_path.exists():
|
223
|
+
logger.warning(f"RASMapper file not found: {rasmap_path}")
|
224
|
+
return None
|
225
|
+
|
226
|
+
return rasmap_path
|
227
|
+
|
228
|
+
@staticmethod
|
229
|
+
@log_call
|
230
|
+
def initialize_rasmap_df(ras_object=None) -> pd.DataFrame:
|
231
|
+
"""
|
232
|
+
Initialize the rasmap_df as part of project initialization.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
ras_object: Optional RAS object instance.
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
pd.DataFrame: DataFrame containing information from the .rasmap file.
|
239
|
+
"""
|
240
|
+
ras_obj = ras_object or ras
|
241
|
+
ras_obj.check_initialized()
|
242
|
+
|
243
|
+
rasmap_path = RasMap.get_rasmap_path(ras_obj)
|
244
|
+
if rasmap_path is None:
|
245
|
+
logger.warning("No .rasmap file found for this project. Creating empty rasmap_df.")
|
246
|
+
# Create a single row DataFrame with all empty values
|
247
|
+
return pd.DataFrame({
|
248
|
+
'projection_path': [None],
|
249
|
+
'profile_lines_path': [[]],
|
250
|
+
'soil_layer_path': [[]],
|
251
|
+
'infiltration_hdf_path': [[]],
|
252
|
+
'landcover_hdf_path': [[]],
|
253
|
+
'terrain_hdf_path': [[]],
|
254
|
+
'current_settings': [{}]
|
255
|
+
})
|
256
|
+
|
257
|
+
return RasMap.parse_rasmap(rasmap_path, ras_obj)
|
258
|
+
|
259
|
+
@staticmethod
|
260
|
+
@log_call
|
261
|
+
def get_terrain_names(rasmap_path: Union[str, Path]) -> List[str]:
|
262
|
+
"""
|
263
|
+
Extracts terrain layer names from a given .rasmap file.
|
264
|
+
|
265
|
+
Args:
|
266
|
+
rasmap_path (Union[str, Path]): Path to the .rasmap file.
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
List[str]: A list of terrain names.
|
270
|
+
|
271
|
+
Raises:
|
272
|
+
FileNotFoundError: If the rasmap file does not exist.
|
273
|
+
ValueError: If the file is not a valid XML or lacks a 'Terrains' section.
|
274
|
+
"""
|
275
|
+
rasmap_path = Path(rasmap_path)
|
276
|
+
if not rasmap_path.is_file():
|
277
|
+
raise FileNotFoundError(f"The file '{rasmap_path}' does not exist.")
|
278
|
+
|
279
|
+
try:
|
280
|
+
tree = ET.parse(rasmap_path)
|
281
|
+
root = tree.getroot()
|
282
|
+
except ET.ParseError as e:
|
283
|
+
raise ValueError(f"Failed to parse the RASMAP file. Ensure it is a valid XML file. Error: {e}")
|
284
|
+
|
285
|
+
terrains_element = root.find('Terrains')
|
286
|
+
if terrains_element is None:
|
287
|
+
logger.warning("The RASMAP file does not contain a 'Terrains' section.")
|
288
|
+
return []
|
289
|
+
|
290
|
+
terrain_names = [layer.get('Name') for layer in terrains_element.findall('Layer') if layer.get('Name')]
|
291
|
+
logger.info(f"Extracted terrain names: {terrain_names}")
|
292
|
+
return terrain_names
|
293
|
+
|
294
|
+
|
295
|
+
@staticmethod
|
296
|
+
@log_call
|
297
|
+
def postprocess_stored_maps(
|
298
|
+
plan_number: Union[str, List[str]],
|
299
|
+
specify_terrain: Optional[str] = None,
|
300
|
+
layers: Union[str, List[str]] = None,
|
301
|
+
ras_object: Optional[Any] = None
|
302
|
+
) -> bool:
|
303
|
+
"""
|
304
|
+
Automates the generation of stored floodplain map outputs (e.g., .tif files).
|
305
|
+
|
306
|
+
This function modifies the plan and .rasmap files to generate floodplain maps
|
307
|
+
for one or more plans, then restores the original files.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
plan_number (Union[str, List[str]]): Plan number(s) to generate maps for.
|
311
|
+
specify_terrain (Optional[str]): The name of a specific terrain to use.
|
312
|
+
layers (Union[str, List[str]], optional): A list of map layers to generate.
|
313
|
+
Defaults to ['WSEL', 'Velocity', 'Depth'].
|
314
|
+
ras_object (Optional[Any]): The RAS project object.
|
315
|
+
|
316
|
+
Returns:
|
317
|
+
bool: True if the process completed successfully, False otherwise.
|
318
|
+
"""
|
319
|
+
ras_obj = ras_object or ras
|
320
|
+
ras_obj.check_initialized()
|
321
|
+
|
322
|
+
if layers is None:
|
323
|
+
layers = ['WSEL', 'Velocity', 'Depth']
|
324
|
+
elif isinstance(layers, str):
|
325
|
+
layers = [layers]
|
326
|
+
|
327
|
+
# Convert plan_number to list if it's a string
|
328
|
+
plan_number_list = [plan_number] if isinstance(plan_number, str) else plan_number
|
329
|
+
|
330
|
+
rasmap_path = ras_obj.project_folder / f"{ras_obj.project_name}.rasmap"
|
331
|
+
rasmap_backup_path = rasmap_path.with_suffix(f"{rasmap_path.suffix}.storedmap.bak")
|
332
|
+
|
333
|
+
# Store plan paths and their backups
|
334
|
+
plan_paths = []
|
335
|
+
plan_backup_paths = []
|
336
|
+
for plan_num in plan_number_list:
|
337
|
+
plan_path = Path(RasPlan.get_plan_path(plan_num, ras_obj))
|
338
|
+
plan_backup_path = plan_path.with_suffix(f"{plan_path.suffix}.storedmap.bak")
|
339
|
+
plan_paths.append(plan_path)
|
340
|
+
plan_backup_paths.append(plan_backup_path)
|
341
|
+
|
342
|
+
def _create_map_element(name, map_type, profile_name="Max"):
|
343
|
+
map_params = {
|
344
|
+
"MapType": map_type,
|
345
|
+
"OutputMode": "Stored Current Terrain",
|
346
|
+
"ProfileIndex": "2147483647",
|
347
|
+
"ProfileName": profile_name
|
348
|
+
}
|
349
|
+
if specify_terrain:
|
350
|
+
map_params["Terrain"] = specify_terrain
|
351
|
+
|
352
|
+
layer_elem = ET.Element('Layer', Name=name, Type="RASResultsMap", Checked="True")
|
353
|
+
map_params_elem = ET.SubElement(layer_elem, 'MapParameters')
|
354
|
+
for k, v in map_params.items():
|
355
|
+
map_params_elem.set(k, str(v))
|
356
|
+
return layer_elem
|
357
|
+
|
358
|
+
try:
|
359
|
+
# --- 1. Backup and Modify Plan Files ---
|
360
|
+
for plan_num, plan_path, plan_backup_path in zip(plan_number_list, plan_paths, plan_backup_paths):
|
361
|
+
logger.info(f"Backing up plan file {plan_path} to {plan_backup_path}")
|
362
|
+
shutil.copy2(plan_path, plan_backup_path)
|
363
|
+
|
364
|
+
logger.info(f"Updating plan run flags for floodplain mapping for plan {plan_num}...")
|
365
|
+
RasPlan.update_run_flags(
|
366
|
+
plan_num,
|
367
|
+
geometry_preprocessor=False,
|
368
|
+
unsteady_flow_simulation=False,
|
369
|
+
post_processor=False,
|
370
|
+
floodplain_mapping=True, # Note: True maps to 0, which means "Run"
|
371
|
+
ras_object=ras_obj
|
372
|
+
)
|
373
|
+
|
374
|
+
# --- 2. Backup and Modify RASMAP File ---
|
375
|
+
logger.info(f"Backing up rasmap file {rasmap_path} to {rasmap_backup_path}")
|
376
|
+
shutil.copy2(rasmap_path, rasmap_backup_path)
|
377
|
+
|
378
|
+
tree = ET.parse(rasmap_path)
|
379
|
+
root = tree.getroot()
|
380
|
+
|
381
|
+
results_section = root.find('Results')
|
382
|
+
if results_section is None:
|
383
|
+
raise ValueError(f"No <Results> section found in {rasmap_path}")
|
384
|
+
|
385
|
+
# Process each plan's results layer
|
386
|
+
for plan_num in plan_number_list:
|
387
|
+
plan_hdf_part = f".p{plan_num}.hdf"
|
388
|
+
results_layer = None
|
389
|
+
for layer in results_section.findall("Layer[@Type='RASResults']"):
|
390
|
+
filename = layer.get("Filename")
|
391
|
+
if filename and plan_hdf_part.lower() in filename.lower():
|
392
|
+
results_layer = layer
|
393
|
+
break
|
394
|
+
|
395
|
+
if results_layer is None:
|
396
|
+
logger.warning(f"Could not find RASResults layer for plan ending in '{plan_hdf_part}' in {rasmap_path}")
|
397
|
+
continue
|
398
|
+
|
399
|
+
map_definitions = {"WSEL": "elevation", "Velocity": "velocity", "Depth": "depth"}
|
400
|
+
for layer_name in layers:
|
401
|
+
if layer_name in map_definitions:
|
402
|
+
map_type = map_definitions[layer_name]
|
403
|
+
layername_attr = "Water Surface" if layer_name == "WSEL" else None
|
404
|
+
map_elem = _create_map_element(layer_name, map_type)
|
405
|
+
if layername_attr:
|
406
|
+
map_elem.find("MapParameters").set("LayerName", layername_attr)
|
407
|
+
results_layer.append(map_elem)
|
408
|
+
logger.info(f"Added '{layer_name}' stored map to results layer for plan {plan_num}.")
|
409
|
+
|
410
|
+
if specify_terrain:
|
411
|
+
terrains_elem = root.find('Terrains')
|
412
|
+
if terrains_elem is not None:
|
413
|
+
for layer in list(terrains_elem):
|
414
|
+
if layer.get('Name') != specify_terrain:
|
415
|
+
terrains_elem.remove(layer)
|
416
|
+
logger.info(f"Filtered terrains, keeping only '{specify_terrain}'.")
|
417
|
+
|
418
|
+
tree.write(rasmap_path, encoding='utf-8', xml_declaration=True)
|
419
|
+
|
420
|
+
# --- 3. Execute HEC-RAS ---
|
421
|
+
logger.info("Opening HEC-RAS...")
|
422
|
+
ras_exe = ras_obj.ras_exe_path
|
423
|
+
prj_path = f'"{str(ras_obj.prj_file)}"'
|
424
|
+
command = f"{ras_exe} {prj_path}"
|
425
|
+
|
426
|
+
try:
|
427
|
+
import sys
|
428
|
+
import subprocess
|
429
|
+
if sys.platform == "win32":
|
430
|
+
hecras_process = subprocess.Popen(command)
|
431
|
+
else:
|
432
|
+
hecras_process = subprocess.Popen([ras_exe, prj_path])
|
433
|
+
|
434
|
+
logger.info(f"HEC-RAS opened with Process ID: {hecras_process.pid}")
|
435
|
+
logger.info(f"Please run plan(s) {', '.join(plan_number_list)} using the 'Compute Multiple' window in HEC-RAS to generate floodplain mapping results.")
|
436
|
+
|
437
|
+
# Wait for HEC-RAS to close
|
438
|
+
logger.info("Waiting for HEC-RAS to close...")
|
439
|
+
hecras_process.wait()
|
440
|
+
logger.info("HEC-RAS has closed")
|
441
|
+
|
442
|
+
success = True
|
443
|
+
|
444
|
+
except Exception as e:
|
445
|
+
logger.error(f"Failed to launch HEC-RAS: {e}")
|
446
|
+
success = False
|
447
|
+
|
448
|
+
if not success:
|
449
|
+
logger.error("Floodplain mapping computation failed.")
|
450
|
+
return False
|
451
|
+
|
452
|
+
logger.info("Floodplain mapping computation successful.")
|
453
|
+
return True
|
454
|
+
|
455
|
+
except Exception as e:
|
456
|
+
logger.error(f"Error in postprocess_stored_maps: {e}")
|
457
|
+
return False
|
458
|
+
|
459
|
+
finally:
|
460
|
+
# --- 4. Restore Files ---
|
461
|
+
for plan_path, plan_backup_path in zip(plan_paths, plan_backup_paths):
|
462
|
+
if plan_backup_path.exists():
|
463
|
+
logger.info(f"Restoring original plan file from {plan_backup_path}")
|
464
|
+
shutil.move(plan_backup_path, plan_path)
|
465
|
+
if rasmap_backup_path.exists():
|
466
|
+
logger.info(f"Restoring original rasmap file from {rasmap_backup_path}")
|
467
|
+
shutil.move(rasmap_backup_path, rasmap_path)
|
ras_commander/RasPrj.py
CHANGED
@@ -1295,7 +1295,8 @@ def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
|
|
1295
1295
|
>>> my_project = init_ras_project("/path/to/project", "6.6", "new")
|
1296
1296
|
>>> print(f"Created project instance: {my_project.project_name}")
|
1297
1297
|
"""
|
1298
|
-
|
1298
|
+
# Convert to absolute path immediately to ensure consistent path handling
|
1299
|
+
project_folder = Path(ras_project_folder).resolve()
|
1299
1300
|
if not project_folder.exists():
|
1300
1301
|
logger.error(f"The specified RAS project folder does not exist: {project_folder}")
|
1301
1302
|
raise FileNotFoundError(f"The specified RAS project folder does not exist: {project_folder}. Please check the path and try again.")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ras-commander
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.78.0
|
4
4
|
Summary: A Python library for automating HEC-RAS 6.x operations
|
5
5
|
Home-page: https://github.com/gpt-cmdr/ras-commander
|
6
6
|
Author: William M. Katzenmeyer, P.E., C.F.M.
|
@@ -291,7 +291,7 @@ This is useful for comparing different river systems, running scenario analyses
|
|
291
291
|
- `RasGeo`: Handles operations related to geometry files
|
292
292
|
- `RasUnsteady`: Manages unsteady flow file operations
|
293
293
|
- `RasUtils`: Contains utility functions for file operations and data management
|
294
|
-
- `RasMap`: Parses and
|
294
|
+
- `RasMap`: Parses RASMapper configuration files and automates floodplain mapping
|
295
295
|
- `RasExamples`: Manages and loads HEC-RAS example projects
|
296
296
|
|
297
297
|
#### HDF Data Access Classes
|
@@ -511,7 +511,6 @@ Additionally, we would like to acknowledge the following notable contributions a
|
|
511
511
|
|
512
512
|
- Development of additional HDF functions for detailed analysis and mapping of HEC-RAS results within the RasHdf class.
|
513
513
|
- Development of the prototype `RasCmdr` class for executing HEC-RAS simulations.
|
514
|
-
- Optimization examples and methods from (INSERT REFERENCE) for use in the Ras-Commander library examples
|
515
514
|
|
516
515
|
2. Attribution: The [`pyHMT2D`](https://github.com/psu-efd/pyHMT2D/) project by Xiaofeng Liu, which provided insights into HDF file handling methods for HEC-RAS outputs. Many of the functions in the [Ras_2D_Data.py](https://github.com/psu-efd/pyHMT2D/blob/main/pyHMT2D/Hydraulic_Models_Data/RAS_2D/RAS_2D_Data.py) file were adapted with AI for use in RAS Commander.
|
517
516
|
|
@@ -17,16 +17,16 @@ ras_commander/HdfUtils.py,sha256=VkIKAXBrLwTlk2VtXSO-W3RU-NHpfHbE1QcZUZgl-t8,152
|
|
17
17
|
ras_commander/HdfXsec.py,sha256=flREnFFrIZu4SSKGRQeX9w3SS49q0UWPJnq4zO7DbUM,27342
|
18
18
|
ras_commander/LoggingConfig.py,sha256=gWe5K5XTmMQpSczsTysAqpC9my24i_IyM8dvD85fxYg,2704
|
19
19
|
ras_commander/RasCmdr.py,sha256=37GnchoQ0fIAkPnssnCr1mRUXY8gm-hIMTmuHZlnYP8,34591
|
20
|
-
ras_commander/RasExamples.py,sha256=
|
21
|
-
ras_commander/RasGeo.py,sha256=
|
22
|
-
ras_commander/RasMap.py,sha256=
|
20
|
+
ras_commander/RasExamples.py,sha256=oOI9id_98anyrTqqpnYbbs5VX7q1E0P-cC4wzqT3OTg,23411
|
21
|
+
ras_commander/RasGeo.py,sha256=u2HUkFrPnMc6Z7jD4PHM4YV3nwTZqMi4QIoNDQ7Rjug,21671
|
22
|
+
ras_commander/RasMap.py,sha256=OqUaMmNRw6opVBxkCAYd--pEE1gZE-Y5SPyoaX2ulMg,19791
|
23
23
|
ras_commander/RasPlan.py,sha256=ogIpLqawXTsjLnKRZTqzZydn_EFVJZFZZGgHvJ_t_-c,65408
|
24
|
-
ras_commander/RasPrj.py,sha256=
|
24
|
+
ras_commander/RasPrj.py,sha256=oXPWuNC_403mUfqyysXyBVul0Xtz_0SKDxZdj4fvNYU,63448
|
25
25
|
ras_commander/RasUnsteady.py,sha256=TO08CT2GC4G5rcXO_Wbia2t4PhiWRu9-nC9F0IW7Gyo,37187
|
26
26
|
ras_commander/RasUtils.py,sha256=0fm4IIs0LH1dgDj3pGd66mR82DhWLEkRKUvIo2M_5X0,35886
|
27
27
|
ras_commander/__init__.py,sha256=byL-4pG2AwLeZXa1nFd3srZe9Pu96V52Y5Dujcu3mM8,2039
|
28
|
-
ras_commander-0.
|
29
|
-
ras_commander-0.
|
30
|
-
ras_commander-0.
|
31
|
-
ras_commander-0.
|
32
|
-
ras_commander-0.
|
28
|
+
ras_commander-0.78.0.dist-info/licenses/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
|
29
|
+
ras_commander-0.78.0.dist-info/METADATA,sha256=-AIKb7auKcOkGb6vDogfaKr4eCdhFZIrml4kPiZbpcM,27855
|
30
|
+
ras_commander-0.78.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
31
|
+
ras_commander-0.78.0.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
|
32
|
+
ras_commander-0.78.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|