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.
@@ -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
- logger.info(f"All available projects: {', '.join(projects)}")
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 Manning\'s n Value'])
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 Manning\'s n Value'])
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
- 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)
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
- project_folder = Path(ras_project_folder)
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.77.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 manages RASMapper configuration data
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=9CxqIB8GwA5o81p-N8vu93kxGMOZLQBfrOb58DGjEHQ,17959
21
- ras_commander/RasGeo.py,sha256=Yiqnbl2L-W_UeT-x-ZHrNKKJ6kIxLyxpfx2fzxleB8s,21675
22
- ras_commander/RasMap.py,sha256=4cVzaaQure-CXdXB1BY29iE20S00eldUqoL96PvJPbw,10635
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=ab10DeR11_edA54vL1DOptVXlIHNxLXOX6hN8I_Xr3M,63359
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.77.0.dist-info/licenses/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
29
- ras_commander-0.77.0.dist-info/METADATA,sha256=WHdVmyFOjbMEAmtHOxWbAWEUt7fSvsNlffXpCkfrRxw,27940
30
- ras_commander-0.77.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
31
- ras_commander-0.77.0.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
32
- ras_commander-0.77.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5