ras-commander 0.75.0__py3-none-any.whl → 0.77.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/Decorators.py +256 -256
- ras_commander/HdfInfiltration.py +1529 -1529
- ras_commander/HdfResultsPlan.py +380 -380
- ras_commander/RasExamples.py +423 -423
- ras_commander/RasGeo.py +1 -1
- ras_commander/RasPlan.py +1536 -1536
- ras_commander/RasPrj.py +1478 -1469
- ras_commander/__init__.py +1 -1
- {ras_commander-0.75.0.dist-info → ras_commander-0.77.0.dist-info}/METADATA +16 -10
- {ras_commander-0.75.0.dist-info → ras_commander-0.77.0.dist-info}/RECORD +13 -13
- {ras_commander-0.75.0.dist-info → ras_commander-0.77.0.dist-info}/WHEEL +1 -1
- {ras_commander-0.75.0.dist-info → ras_commander-0.77.0.dist-info}/licenses/LICENSE +0 -0
- {ras_commander-0.75.0.dist-info → ras_commander-0.77.0.dist-info}/top_level.txt +0 -0
ras_commander/RasPrj.py
CHANGED
@@ -1,1469 +1,1478 @@
|
|
1
|
-
"""
|
2
|
-
RasPrj.py - Manages HEC-RAS projects within the ras-commander library
|
3
|
-
|
4
|
-
This module provides a class for managing HEC-RAS projects.
|
5
|
-
|
6
|
-
Classes:
|
7
|
-
RasPrj: A class for managing HEC-RAS projects.
|
8
|
-
|
9
|
-
Functions:
|
10
|
-
init_ras_project: Initialize a RAS project.
|
11
|
-
get_ras_exe: Determine the HEC-RAS executable path based on the input.
|
12
|
-
|
13
|
-
DEVELOPER NOTE:
|
14
|
-
This class is used to initialize a RAS project and is used in conjunction with the RasCmdr class to manage the execution of RAS plans.
|
15
|
-
By default, the RasPrj class is initialized with the global 'ras' object.
|
16
|
-
However, you can create multiple RasPrj instances to manage multiple projects.
|
17
|
-
Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
|
18
|
-
|
19
|
-
This module is part of the ras-commander library and uses a centralized logging configuration.
|
20
|
-
|
21
|
-
Logging Configuration:
|
22
|
-
- The logging is set up in the logging_config.py file.
|
23
|
-
- A @log_call decorator is available to automatically log function calls.
|
24
|
-
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
25
|
-
- Logs are written to both console and a rotating file handler.
|
26
|
-
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
27
|
-
- The default log level is INFO.
|
28
|
-
|
29
|
-
To use logging in this module:
|
30
|
-
1. Use the @log_call decorator for automatic function call logging.
|
31
|
-
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
32
|
-
|
33
|
-
|
34
|
-
Example:
|
35
|
-
@log_call
|
36
|
-
def my_function():
|
37
|
-
|
38
|
-
logger.debug("Additional debug information")
|
39
|
-
# Function logic here
|
40
|
-
|
41
|
-
-----
|
42
|
-
|
43
|
-
All of the methods in this class are class methods and are designed to be used with instances of the class.
|
44
|
-
|
45
|
-
List of Functions in RasPrj:
|
46
|
-
- initialize()
|
47
|
-
- _load_project_data()
|
48
|
-
- _get_geom_file_for_plan()
|
49
|
-
- _parse_plan_file()
|
50
|
-
- _parse_unsteady_file()
|
51
|
-
- _get_prj_entries()
|
52
|
-
- _parse_boundary_condition()
|
53
|
-
- is_initialized (property)
|
54
|
-
- check_initialized()
|
55
|
-
- find_ras_prj()
|
56
|
-
- get_project_name()
|
57
|
-
- get_prj_entries()
|
58
|
-
- get_plan_entries()
|
59
|
-
- get_flow_entries()
|
60
|
-
- get_unsteady_entries()
|
61
|
-
- get_geom_entries()
|
62
|
-
- get_hdf_entries()
|
63
|
-
- print_data()
|
64
|
-
- get_plan_value()
|
65
|
-
- get_boundary_conditions()
|
66
|
-
|
67
|
-
Functions in RasPrj that are not part of the class:
|
68
|
-
- init_ras_project()
|
69
|
-
- get_ras_exe()
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
"""
|
75
|
-
import os
|
76
|
-
import re
|
77
|
-
from pathlib import Path
|
78
|
-
import pandas as pd
|
79
|
-
from typing import Union, Any, List, Dict, Tuple
|
80
|
-
import logging
|
81
|
-
from ras_commander.LoggingConfig import get_logger
|
82
|
-
from ras_commander.Decorators import log_call
|
83
|
-
|
84
|
-
logger = get_logger(__name__)
|
85
|
-
|
86
|
-
def read_file_with_fallback_encoding(file_path, encodings=['utf-8', 'latin1', 'cp1252', 'iso-8859-1']):
|
87
|
-
"""
|
88
|
-
Attempt to read a file using multiple encodings.
|
89
|
-
|
90
|
-
Args:
|
91
|
-
file_path (str or Path): Path to the file to read
|
92
|
-
encodings (list): List of encodings to try, in order of preference
|
93
|
-
|
94
|
-
Returns:
|
95
|
-
tuple: (content, encoding) or (None, None) if all encodings fail
|
96
|
-
"""
|
97
|
-
for encoding in encodings:
|
98
|
-
try:
|
99
|
-
with open(file_path, 'r', encoding=encoding) as file:
|
100
|
-
content = file.read()
|
101
|
-
return content, encoding
|
102
|
-
except UnicodeDecodeError:
|
103
|
-
continue
|
104
|
-
except Exception as e:
|
105
|
-
logger.error(f"Error reading file {file_path} with {encoding} encoding: {e}")
|
106
|
-
continue
|
107
|
-
|
108
|
-
logger.error(f"Failed to read file {file_path} with any of the attempted encodings: {encodings}")
|
109
|
-
return None, None
|
110
|
-
|
111
|
-
class RasPrj:
|
112
|
-
|
113
|
-
def __init__(self):
|
114
|
-
self.initialized = False
|
115
|
-
self.boundaries_df = None # New attribute to store boundary conditions
|
116
|
-
self.suppress_logging = False # Add suppress_logging as instance variable
|
117
|
-
|
118
|
-
@log_call
|
119
|
-
def initialize(self, project_folder, ras_exe_path, suppress_logging=True):
|
120
|
-
"""
|
121
|
-
Initialize a RasPrj instance with project folder and RAS executable path.
|
122
|
-
|
123
|
-
IMPORTANT: External users should use init_ras_project() function instead of this method.
|
124
|
-
This method is intended for internal use only.
|
125
|
-
|
126
|
-
Args:
|
127
|
-
project_folder (str or Path): Path to the HEC-RAS project folder.
|
128
|
-
ras_exe_path (str or Path): Path to the HEC-RAS executable.
|
129
|
-
suppress_logging (bool, default=True): If True, suppresses initialization logging messages.
|
130
|
-
|
131
|
-
Raises:
|
132
|
-
ValueError: If no HEC-RAS project file is found in the specified folder.
|
133
|
-
|
134
|
-
Note:
|
135
|
-
This method sets up the RasPrj instance by:
|
136
|
-
1. Finding the project file (.prj)
|
137
|
-
2. Loading project data (plans, geometries, flows)
|
138
|
-
3. Extracting boundary conditions
|
139
|
-
4. Setting the initialization flag
|
140
|
-
5. Loading RASMapper data (.rasmap)
|
141
|
-
"""
|
142
|
-
self.suppress_logging = suppress_logging # Store suppress_logging state
|
143
|
-
self.project_folder = Path(project_folder)
|
144
|
-
self.prj_file = self.find_ras_prj(self.project_folder)
|
145
|
-
if self.prj_file is None:
|
146
|
-
logger.error(f"No HEC-RAS project file found in {self.project_folder}")
|
147
|
-
raise ValueError(f"No HEC-RAS project file found in {self.project_folder}. Please check the path and try again.")
|
148
|
-
self.project_name = Path(self.prj_file).stem
|
149
|
-
self.ras_exe_path = ras_exe_path
|
150
|
-
|
151
|
-
# Set initialized to True before loading project data
|
152
|
-
self.initialized = True
|
153
|
-
|
154
|
-
# Now load the project data
|
155
|
-
self._load_project_data()
|
156
|
-
self.boundaries_df = self.get_boundary_conditions()
|
157
|
-
|
158
|
-
# Load RASMapper data if available
|
159
|
-
try:
|
160
|
-
# Import here to avoid circular imports
|
161
|
-
from .RasMap import RasMap
|
162
|
-
self.rasmap_df = RasMap.initialize_rasmap_df(self)
|
163
|
-
except ImportError:
|
164
|
-
logger.warning("RasMap module not available. RASMapper data will not be loaded.")
|
165
|
-
self.rasmap_df = pd.DataFrame(columns=['projection_path', 'profile_lines_path', 'soil_layer_path',
|
166
|
-
'infiltration_hdf_path', 'landcover_hdf_path', 'terrain_hdf_path',
|
167
|
-
'current_settings'])
|
168
|
-
except Exception as e:
|
169
|
-
logger.error(f"Error initializing RASMapper data: {e}")
|
170
|
-
self.rasmap_df = pd.DataFrame(columns=['projection_path', 'profile_lines_path', 'soil_layer_path',
|
171
|
-
'infiltration_hdf_path', 'landcover_hdf_path', 'terrain_hdf_path',
|
172
|
-
'current_settings'])
|
173
|
-
|
174
|
-
if not suppress_logging:
|
175
|
-
logger.info(f"Initialization complete for project: {self.project_name}")
|
176
|
-
logger.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
|
177
|
-
f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
|
178
|
-
f"Boundary conditions: {len(self.boundaries_df)}")
|
179
|
-
logger.info(f"Geometry HDF files found: {self.plan_df['Geom_File'].notna().sum()}")
|
180
|
-
logger.info(f"RASMapper data loaded: {not self.rasmap_df.empty}")
|
181
|
-
|
182
|
-
@log_call
|
183
|
-
def _load_project_data(self):
|
184
|
-
"""
|
185
|
-
Load project data from the HEC-RAS project file.
|
186
|
-
|
187
|
-
This internal method:
|
188
|
-
1. Initializes DataFrames for plan, flow, unsteady, and geometry entries
|
189
|
-
2. Ensures all required columns are present with appropriate default values
|
190
|
-
3. Sets file paths for all components (geometries, flows, plans)
|
191
|
-
|
192
|
-
Raises:
|
193
|
-
Exception: If there's an error loading or processing project data.
|
194
|
-
"""
|
195
|
-
try:
|
196
|
-
# Load data frames
|
197
|
-
self.unsteady_df = self._get_prj_entries('Unsteady')
|
198
|
-
self.plan_df = self._get_prj_entries('Plan')
|
199
|
-
self.flow_df = self._get_prj_entries('Flow')
|
200
|
-
self.geom_df = self.get_geom_entries()
|
201
|
-
|
202
|
-
# Ensure required columns exist
|
203
|
-
self._ensure_required_columns()
|
204
|
-
|
205
|
-
# Set paths for geometry and flow files
|
206
|
-
self._set_file_paths()
|
207
|
-
|
208
|
-
# Make sure all plan paths are properly set
|
209
|
-
self._set_plan_paths()
|
210
|
-
|
211
|
-
except Exception as e:
|
212
|
-
logger.error(f"Error loading project data: {e}")
|
213
|
-
raise
|
214
|
-
|
215
|
-
def _ensure_required_columns(self):
|
216
|
-
"""Ensure all required columns exist in plan_df."""
|
217
|
-
required_columns = [
|
218
|
-
'plan_number', 'unsteady_number', 'geometry_number',
|
219
|
-
'Geom File', 'Geom Path', 'Flow File', 'Flow Path', 'full_path'
|
220
|
-
]
|
221
|
-
|
222
|
-
for col in required_columns:
|
223
|
-
if col not in self.plan_df.columns:
|
224
|
-
self.plan_df[col] = None
|
225
|
-
|
226
|
-
if not self.plan_df['full_path'].any():
|
227
|
-
self.plan_df['full_path'] = self.plan_df['plan_number'].apply(
|
228
|
-
lambda x: str(self.project_folder / f"{self.project_name}.p{x}")
|
229
|
-
)
|
230
|
-
|
231
|
-
def _set_file_paths(self):
|
232
|
-
"""Set geometry and flow paths in plan_df."""
|
233
|
-
for idx, row in self.plan_df.iterrows():
|
234
|
-
try:
|
235
|
-
self._set_geom_path(idx, row)
|
236
|
-
self._set_flow_path(idx, row)
|
237
|
-
|
238
|
-
if not self.suppress_logging:
|
239
|
-
logger.info(f"Plan {row['plan_number']} paths set up")
|
240
|
-
except Exception as e:
|
241
|
-
logger.error(f"Error processing plan file {row['plan_number']}: {e}")
|
242
|
-
|
243
|
-
def _set_geom_path(self, idx: int, row: pd.Series):
|
244
|
-
"""Set geometry path for a plan entry."""
|
245
|
-
if pd.notna(row['Geom File']):
|
246
|
-
geom_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
|
247
|
-
self.plan_df.at[idx, 'Geom Path'] = str(geom_path)
|
248
|
-
|
249
|
-
def _set_flow_path(self, idx: int, row: pd.Series):
|
250
|
-
"""Set flow path for a plan entry."""
|
251
|
-
if pd.notna(row['Flow File']):
|
252
|
-
prefix = 'u' if pd.notna(row['unsteady_number']) else 'f'
|
253
|
-
flow_path = self.project_folder / f"{self.project_name}.{prefix}{row['Flow File']}"
|
254
|
-
self.plan_df.at[idx, 'Flow Path'] = str(flow_path)
|
255
|
-
|
256
|
-
def _set_plan_paths(self):
|
257
|
-
"""Set full path information for plan files and their associated geometry and flow files."""
|
258
|
-
if self.plan_df.empty:
|
259
|
-
logger.debug("Plan DataFrame is empty, no paths to set")
|
260
|
-
return
|
261
|
-
|
262
|
-
# Ensure full path is set for all plan entries
|
263
|
-
if 'full_path' not in self.plan_df.columns or self.plan_df['full_path'].isna().any():
|
264
|
-
self.plan_df['full_path'] = self.plan_df['plan_number'].apply(
|
265
|
-
lambda x: str(self.project_folder / f"{self.project_name}.p{x}")
|
266
|
-
)
|
267
|
-
|
268
|
-
# Create the Geom Path and Flow Path columns if they don't exist
|
269
|
-
if 'Geom Path' not in self.plan_df.columns:
|
270
|
-
self.plan_df['Geom Path'] = None
|
271
|
-
if 'Flow Path' not in self.plan_df.columns:
|
272
|
-
self.plan_df['Flow Path'] = None
|
273
|
-
|
274
|
-
# Update paths for each plan entry
|
275
|
-
for idx, row in self.plan_df.iterrows():
|
276
|
-
try:
|
277
|
-
# Set geometry path if Geom File exists and Geom Path is missing or invalid
|
278
|
-
if pd.notna(row['Geom File']):
|
279
|
-
geom_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
|
280
|
-
self.plan_df.at[idx, 'Geom Path'] = str(geom_path)
|
281
|
-
|
282
|
-
# Set flow path if Flow File exists and Flow Path is missing or invalid
|
283
|
-
if pd.notna(row['Flow File']):
|
284
|
-
# Determine the prefix (u for unsteady, f for steady flow)
|
285
|
-
prefix = 'u' if pd.notna(row['unsteady_number']) else 'f'
|
286
|
-
flow_path = self.project_folder / f"{self.project_name}.{prefix}{row['Flow File']}"
|
287
|
-
self.plan_df.at[idx, 'Flow Path'] = str(flow_path)
|
288
|
-
|
289
|
-
if not self.suppress_logging:
|
290
|
-
logger.debug(f"Plan {row['plan_number']} paths set up")
|
291
|
-
except Exception as e:
|
292
|
-
logger.error(f"Error setting paths for plan {row.get('plan_number', idx)}: {e}")
|
293
|
-
|
294
|
-
def _get_geom_file_for_plan(self, plan_number):
|
295
|
-
"""
|
296
|
-
Get the geometry file path for a given plan number.
|
297
|
-
|
298
|
-
Args:
|
299
|
-
plan_number (str): The plan number to find the geometry file for.
|
300
|
-
|
301
|
-
Returns:
|
302
|
-
str: The full path to the geometry HDF file, or None if not found.
|
303
|
-
"""
|
304
|
-
plan_file_path = self.project_folder / f"{self.project_name}.p{plan_number}"
|
305
|
-
content, encoding = read_file_with_fallback_encoding(plan_file_path)
|
306
|
-
|
307
|
-
if content is None:
|
308
|
-
return None
|
309
|
-
|
310
|
-
try:
|
311
|
-
for line in content.splitlines():
|
312
|
-
if line.startswith("Geom File="):
|
313
|
-
geom_file = line.strip().split('=')[1]
|
314
|
-
geom_hdf_path = self.project_folder / f"{self.project_name}.{geom_file}.hdf"
|
315
|
-
if geom_hdf_path.exists():
|
316
|
-
return str(geom_hdf_path)
|
317
|
-
else:
|
318
|
-
return None
|
319
|
-
except Exception as e:
|
320
|
-
logger.error(f"Error reading plan file for geometry: {e}")
|
321
|
-
return None
|
322
|
-
|
323
|
-
|
324
|
-
@staticmethod
|
325
|
-
@log_call
|
326
|
-
def get_plan_value(
|
327
|
-
plan_number_or_path: Union[str, Path],
|
328
|
-
key: str,
|
329
|
-
ras_object=None
|
330
|
-
) -> Any:
|
331
|
-
"""
|
332
|
-
Retrieve a specific value from a HEC-RAS plan file.
|
333
|
-
|
334
|
-
Parameters:
|
335
|
-
plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
|
336
|
-
key (str): The key to retrieve from the plan file
|
337
|
-
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
338
|
-
|
339
|
-
Returns:
|
340
|
-
Any: The value associated with the specified key
|
341
|
-
|
342
|
-
Raises:
|
343
|
-
ValueError: If the plan file is not found
|
344
|
-
IOError: If there's an error reading the plan file
|
345
|
-
"""
|
346
|
-
logger = get_logger(__name__)
|
347
|
-
ras_obj = ras_object or ras
|
348
|
-
ras_obj.check_initialized()
|
349
|
-
|
350
|
-
# These must exactly match the keys in supported_plan_keys from _parse_plan_file
|
351
|
-
valid_keys = {
|
352
|
-
'Computation Interval',
|
353
|
-
'DSS File',
|
354
|
-
'Flow File',
|
355
|
-
'Friction Slope Method',
|
356
|
-
'Geom File',
|
357
|
-
'Mapping Interval',
|
358
|
-
'Plan Title',
|
359
|
-
'Program Version',
|
360
|
-
'Run HTab',
|
361
|
-
'Run PostProcess',
|
362
|
-
'Run Sediment',
|
363
|
-
'Run UNet',
|
364
|
-
'Run WQNet',
|
365
|
-
'Short Identifier',
|
366
|
-
'Simulation Date',
|
367
|
-
'UNET D1 Cores',
|
368
|
-
'UNET D2 Cores',
|
369
|
-
'PS Cores',
|
370
|
-
'UNET Use Existing IB Tables',
|
371
|
-
'UNET 1D Methodology',
|
372
|
-
'UNET D2 SolverType',
|
373
|
-
'UNET D2 Name',
|
374
|
-
'description' # Special case for description block
|
375
|
-
}
|
376
|
-
|
377
|
-
if key not in valid_keys:
|
378
|
-
logger.warning(f"Unknown key: {key}. Valid keys are: {', '.join(sorted(valid_keys))}")
|
379
|
-
return None
|
380
|
-
|
381
|
-
plan_file_path = Path(plan_number_or_path)
|
382
|
-
if not plan_file_path.is_file():
|
383
|
-
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
|
384
|
-
if not plan_file_path.exists():
|
385
|
-
logger.error(f"Plan file not found: {plan_file_path}")
|
386
|
-
raise ValueError(f"Plan file not found: {plan_file_path}")
|
387
|
-
|
388
|
-
try:
|
389
|
-
with open(plan_file_path, 'r') as file:
|
390
|
-
content = file.read()
|
391
|
-
except IOError as e:
|
392
|
-
logger.error(f"Error reading plan file {plan_file_path}: {e}")
|
393
|
-
raise
|
394
|
-
|
395
|
-
if key == 'description':
|
396
|
-
match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
397
|
-
return match.group(1).strip() if match else None
|
398
|
-
else:
|
399
|
-
pattern = f"{key}=(.*)"
|
400
|
-
match = re.search(pattern, content)
|
401
|
-
if match:
|
402
|
-
value = match.group(1).strip()
|
403
|
-
# Convert core values to integers
|
404
|
-
if key in ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']:
|
405
|
-
try:
|
406
|
-
return int(value)
|
407
|
-
except ValueError:
|
408
|
-
logger.warning(f"Could not convert {key} value '{value}' to integer")
|
409
|
-
return None
|
410
|
-
return value
|
411
|
-
|
412
|
-
# Use DEBUG level for missing core values, ERROR for other missing keys
|
413
|
-
if key in ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']:
|
414
|
-
logger.debug(f"Core setting '{key}' not found in plan file")
|
415
|
-
else:
|
416
|
-
logger.error(f"Key '{key}' not found in the plan file")
|
417
|
-
return None
|
418
|
-
|
419
|
-
def _parse_plan_file(self, plan_file_path):
|
420
|
-
"""
|
421
|
-
Parse a plan file and extract critical information.
|
422
|
-
|
423
|
-
Args:
|
424
|
-
plan_file_path (Path): Path to the plan file.
|
425
|
-
|
426
|
-
Returns:
|
427
|
-
dict: Dictionary containing extracted plan information.
|
428
|
-
"""
|
429
|
-
plan_info = {}
|
430
|
-
content, encoding = read_file_with_fallback_encoding(plan_file_path)
|
431
|
-
|
432
|
-
if content is None:
|
433
|
-
logger.error(f"Could not read plan file {plan_file_path} with any supported encoding")
|
434
|
-
return plan_info
|
435
|
-
|
436
|
-
try:
|
437
|
-
# Extract description
|
438
|
-
description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
439
|
-
if description_match:
|
440
|
-
plan_info['description'] = description_match.group(1).strip()
|
441
|
-
|
442
|
-
# BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the plan file keys.
|
443
|
-
|
444
|
-
# Extract other critical information
|
445
|
-
supported_plan_keys = {
|
446
|
-
'Computation Interval': r'Computation Interval=(.+)',
|
447
|
-
'DSS File': r'DSS File=(.+)',
|
448
|
-
'Flow File': r'Flow File=(.+)',
|
449
|
-
'Friction Slope Method': r'Friction Slope Method=(.+)',
|
450
|
-
'Geom File': r'Geom File=(.+)',
|
451
|
-
'Mapping Interval': r'Mapping Interval=(.+)',
|
452
|
-
'Plan Title': r'Plan Title=(.+)',
|
453
|
-
'Program Version': r'Program Version=(.+)',
|
454
|
-
'Run HTab': r'Run HTab=(.+)',
|
455
|
-
'Run PostProcess': r'Run PostProcess=(.+)',
|
456
|
-
'Run Sediment': r'Run Sediment=(.+)',
|
457
|
-
'Run UNet': r'Run UNet=(.+)',
|
458
|
-
'Run WQNet': r'Run WQNet=(.+)',
|
459
|
-
'Short Identifier': r'Short Identifier=(.+)',
|
460
|
-
'Simulation Date': r'Simulation Date=(.+)',
|
461
|
-
'UNET D1 Cores': r'UNET D1 Cores=(.+)',
|
462
|
-
'UNET D2 Cores': r'UNET D2 Cores=(.+)',
|
463
|
-
'PS Cores': r'PS Cores=(.+)',
|
464
|
-
'UNET Use Existing IB Tables': r'UNET Use Existing IB Tables=(.+)',
|
465
|
-
'UNET 1D Methodology': r'UNET 1D Methodology=(.+)',
|
466
|
-
'UNET D2 SolverType': r'UNET D2 SolverType=(.+)',
|
467
|
-
'UNET D2 Name': r'UNET D2 Name=(.+)'
|
468
|
-
}
|
469
|
-
|
470
|
-
# END Exception to Style Guide
|
471
|
-
|
472
|
-
# First, explicitly set None for core values
|
473
|
-
core_keys = ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']
|
474
|
-
for key in core_keys:
|
475
|
-
plan_info[key] = None
|
476
|
-
|
477
|
-
for key, pattern in supported_plan_keys.items():
|
478
|
-
match = re.search(pattern, content)
|
479
|
-
if match:
|
480
|
-
value = match.group(1).strip()
|
481
|
-
# Convert core values to integers if they exist
|
482
|
-
if key in core_keys and value:
|
483
|
-
try:
|
484
|
-
value = int(value)
|
485
|
-
except ValueError:
|
486
|
-
logger.warning(f"Could not convert {key} value '{value}' to integer in plan file {plan_file_path}")
|
487
|
-
value = None
|
488
|
-
plan_info[key] = value
|
489
|
-
elif key in core_keys:
|
490
|
-
logger.debug(f"Core setting '{key}' not found in plan file {plan_file_path}")
|
491
|
-
|
492
|
-
logger.debug(f"Parsed plan file: {plan_file_path} using {encoding} encoding")
|
493
|
-
except Exception as e:
|
494
|
-
logger.error(f"Error parsing plan file {plan_file_path}: {e}")
|
495
|
-
|
496
|
-
return plan_info
|
497
|
-
|
498
|
-
@log_call
|
499
|
-
def _get_prj_entries(self, entry_type):
|
500
|
-
"""
|
501
|
-
Extract entries of a specific type from the HEC-RAS project file.
|
502
|
-
|
503
|
-
Args:
|
504
|
-
entry_type (str): The type of entry to extract (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
|
505
|
-
|
506
|
-
Returns:
|
507
|
-
pd.DataFrame: A DataFrame containing the extracted entries.
|
508
|
-
|
509
|
-
Raises:
|
510
|
-
Exception: If there's an error reading or processing the project file.
|
511
|
-
"""
|
512
|
-
entries = []
|
513
|
-
pattern = re.compile(rf"{entry_type} File=(\w+)")
|
514
|
-
|
515
|
-
try:
|
516
|
-
with open(self.prj_file, 'r', encoding='utf-8') as file:
|
517
|
-
for line in file:
|
518
|
-
match = pattern.match(line.strip())
|
519
|
-
if match:
|
520
|
-
file_name = match.group(1)
|
521
|
-
full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
|
522
|
-
entry_number = file_name[1:]
|
523
|
-
|
524
|
-
entry = {
|
525
|
-
f'{entry_type.lower()}_number': entry_number,
|
526
|
-
'full_path': full_path
|
527
|
-
}
|
528
|
-
|
529
|
-
# Handle Unsteady entries
|
530
|
-
if entry_type == 'Unsteady':
|
531
|
-
entry.update(self._process_unsteady_entry(entry_number, full_path))
|
532
|
-
else:
|
533
|
-
entry.update(self._process_default_entry())
|
534
|
-
|
535
|
-
# Handle Plan entries
|
536
|
-
if entry_type == 'Plan':
|
537
|
-
entry.update(self._process_plan_entry(entry_number, full_path))
|
538
|
-
|
539
|
-
entries.append(entry)
|
540
|
-
|
541
|
-
df = pd.DataFrame(entries)
|
542
|
-
return self._format_dataframe(df, entry_type)
|
543
|
-
|
544
|
-
except Exception as e:
|
545
|
-
logger.error(f"Error in _get_prj_entries for {entry_type}: {e}")
|
546
|
-
raise
|
547
|
-
|
548
|
-
def _process_unsteady_entry(self, entry_number: str, full_path: str) -> dict:
|
549
|
-
"""Process unsteady entry data."""
|
550
|
-
entry = {'unsteady_number': entry_number}
|
551
|
-
unsteady_info = self._parse_unsteady_file(Path(full_path))
|
552
|
-
entry.update(unsteady_info)
|
553
|
-
return entry
|
554
|
-
|
555
|
-
def _process_default_entry(self) -> dict:
|
556
|
-
"""Process default entry data."""
|
557
|
-
return {
|
558
|
-
'unsteady_number': None,
|
559
|
-
'geometry_number': None
|
560
|
-
}
|
561
|
-
|
562
|
-
def _process_plan_entry(self, entry_number: str, full_path: str) -> dict:
|
563
|
-
"""Process plan entry data."""
|
564
|
-
entry = {}
|
565
|
-
plan_info = self._parse_plan_file(Path(full_path))
|
566
|
-
|
567
|
-
if plan_info:
|
568
|
-
entry.update(self._process_flow_file(plan_info))
|
569
|
-
entry.update(self._process_geom_file(plan_info))
|
570
|
-
|
571
|
-
# Add remaining plan info
|
572
|
-
for key, value in plan_info.items():
|
573
|
-
if key not in ['Flow File', 'Geom File']:
|
574
|
-
entry[key] = value
|
575
|
-
|
576
|
-
# Add HDF results path
|
577
|
-
hdf_results_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
|
578
|
-
entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
|
579
|
-
|
580
|
-
return entry
|
581
|
-
|
582
|
-
def _process_flow_file(self, plan_info: dict) -> dict:
|
583
|
-
"""Process flow file information from plan info."""
|
584
|
-
flow_file = plan_info.get('Flow File')
|
585
|
-
if flow_file and flow_file.startswith('u'):
|
586
|
-
return {
|
587
|
-
'unsteady_number': flow_file[1:],
|
588
|
-
'Flow File': flow_file[1:]
|
589
|
-
}
|
590
|
-
return {
|
591
|
-
'unsteady_number': None,
|
592
|
-
'Flow File': flow_file[1:] if flow_file and flow_file.startswith('f') else None
|
593
|
-
}
|
594
|
-
|
595
|
-
def _process_geom_file(self, plan_info: dict) -> dict:
|
596
|
-
"""Process geometry file information from plan info."""
|
597
|
-
geom_file = plan_info.get('Geom File')
|
598
|
-
if geom_file and geom_file.startswith('g'):
|
599
|
-
return {
|
600
|
-
'geometry_number': geom_file[1:],
|
601
|
-
'Geom File': geom_file[1:]
|
602
|
-
}
|
603
|
-
return {
|
604
|
-
'geometry_number': None,
|
605
|
-
'Geom File': None
|
606
|
-
}
|
607
|
-
|
608
|
-
def _parse_unsteady_file(self, unsteady_file_path):
|
609
|
-
"""
|
610
|
-
Parse an unsteady flow file and extract critical information.
|
611
|
-
|
612
|
-
Args:
|
613
|
-
unsteady_file_path (Path): Path to the unsteady flow file.
|
614
|
-
|
615
|
-
Returns:
|
616
|
-
dict: Dictionary containing extracted unsteady flow information.
|
617
|
-
"""
|
618
|
-
unsteady_info = {}
|
619
|
-
content, encoding = read_file_with_fallback_encoding(unsteady_file_path)
|
620
|
-
|
621
|
-
if content is None:
|
622
|
-
return unsteady_info
|
623
|
-
|
624
|
-
try:
|
625
|
-
# BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the unsteady file keys.
|
626
|
-
|
627
|
-
supported_unsteady_keys = {
|
628
|
-
'Flow Title': r'Flow Title=(.+)',
|
629
|
-
'Program Version': r'Program Version=(.+)',
|
630
|
-
'Use Restart': r'Use Restart=(.+)',
|
631
|
-
'Precipitation Mode': r'Precipitation Mode=(.+)',
|
632
|
-
'Wind Mode': r'Wind Mode=(.+)',
|
633
|
-
'Met BC=Precipitation|Mode': r'Met BC=Precipitation\|Mode=(.+)',
|
634
|
-
'Met BC=Evapotranspiration|Mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
|
635
|
-
'Met BC=Precipitation|Expanded View': r'Met BC=Precipitation\|Expanded View=(.+)',
|
636
|
-
'Met BC=Precipitation|Constant Units': r'Met BC=Precipitation\|Constant Units=(.+)',
|
637
|
-
'Met BC=Precipitation|Gridded Source': r'Met BC=Precipitation\|Gridded Source=(.+)'
|
638
|
-
}
|
639
|
-
|
640
|
-
# END Exception to Style Guide
|
641
|
-
|
642
|
-
for key, pattern in supported_unsteady_keys.items():
|
643
|
-
match = re.search(pattern, content)
|
644
|
-
if match:
|
645
|
-
unsteady_info[key] = match.group(1).strip()
|
646
|
-
|
647
|
-
except Exception as e:
|
648
|
-
logger.error(f"Error parsing unsteady file {unsteady_file_path}: {e}")
|
649
|
-
|
650
|
-
return unsteady_info
|
651
|
-
|
652
|
-
@property
|
653
|
-
def is_initialized(self):
|
654
|
-
"""
|
655
|
-
Check if the RasPrj instance has been initialized.
|
656
|
-
|
657
|
-
Returns:
|
658
|
-
bool: True if the instance has been initialized, False otherwise.
|
659
|
-
"""
|
660
|
-
return self.initialized
|
661
|
-
|
662
|
-
@log_call
|
663
|
-
def check_initialized(self):
|
664
|
-
"""
|
665
|
-
Ensure that the RasPrj instance has been initialized before operations.
|
666
|
-
|
667
|
-
Raises:
|
668
|
-
RuntimeError: If the project has not been initialized with init_ras_project().
|
669
|
-
|
670
|
-
Note:
|
671
|
-
This method is called by other methods to validate the project state before
|
672
|
-
performing operations. Users typically don't need to call this directly.
|
673
|
-
"""
|
674
|
-
if not self.initialized:
|
675
|
-
raise RuntimeError("Project not initialized. Call init_ras_project() first.")
|
676
|
-
|
677
|
-
@staticmethod
|
678
|
-
@log_call
|
679
|
-
def find_ras_prj(folder_path):
|
680
|
-
"""
|
681
|
-
Find the appropriate HEC-RAS project file (.prj) in the given folder.
|
682
|
-
|
683
|
-
This method uses several strategies to locate the correct project file:
|
684
|
-
1. If only one .prj file exists, it is selected
|
685
|
-
2. If multiple .prj files exist, it tries to match with .rasmap file names
|
686
|
-
3. As a last resort, it scans files for "Proj Title=" content
|
687
|
-
|
688
|
-
Args:
|
689
|
-
folder_path (str or Path): Path to the folder containing HEC-RAS files.
|
690
|
-
|
691
|
-
Returns:
|
692
|
-
Path: The full path of the selected .prj file or None if no suitable file is found.
|
693
|
-
|
694
|
-
Example:
|
695
|
-
>>> project_file = RasPrj.find_ras_prj("/path/to/ras_project")
|
696
|
-
>>> if project_file:
|
697
|
-
... print(f"Found project file: {project_file}")
|
698
|
-
... else:
|
699
|
-
... print("No project file found")
|
700
|
-
"""
|
701
|
-
folder_path = Path(folder_path)
|
702
|
-
prj_files = list(folder_path.glob("*.prj"))
|
703
|
-
rasmap_files = list(folder_path.glob("*.rasmap"))
|
704
|
-
if len(prj_files) == 1:
|
705
|
-
return prj_files[0].resolve()
|
706
|
-
if len(prj_files) > 1:
|
707
|
-
if len(rasmap_files) == 1:
|
708
|
-
base_filename = rasmap_files[0].stem
|
709
|
-
prj_file = folder_path / f"{base_filename}.prj"
|
710
|
-
if prj_file.exists():
|
711
|
-
return prj_file.resolve()
|
712
|
-
for prj_file in prj_files:
|
713
|
-
try:
|
714
|
-
with open(prj_file, 'r') as file:
|
715
|
-
content = file.read()
|
716
|
-
if "Proj Title=" in content:
|
717
|
-
return prj_file.resolve()
|
718
|
-
except Exception:
|
719
|
-
continue
|
720
|
-
return None
|
721
|
-
|
722
|
-
|
723
|
-
@log_call
|
724
|
-
def get_project_name(self):
|
725
|
-
"""
|
726
|
-
Get the name of the HEC-RAS project (without file extension).
|
727
|
-
|
728
|
-
Returns:
|
729
|
-
str: The name of the project.
|
730
|
-
|
731
|
-
Raises:
|
732
|
-
RuntimeError: If the project has not been initialized.
|
733
|
-
|
734
|
-
Example:
|
735
|
-
>>> project_name = ras.get_project_name()
|
736
|
-
>>> print(f"Working with project: {project_name}")
|
737
|
-
"""
|
738
|
-
self.check_initialized()
|
739
|
-
return self.project_name
|
740
|
-
|
741
|
-
@log_call
|
742
|
-
def get_prj_entries(self, entry_type):
|
743
|
-
"""
|
744
|
-
Get entries of a specific type from the HEC-RAS project.
|
745
|
-
|
746
|
-
This method extracts files of the specified type from the project file,
|
747
|
-
parses their content, and returns a structured DataFrame.
|
748
|
-
|
749
|
-
Args:
|
750
|
-
entry_type (str): The type of entry to retrieve ('Plan', 'Flow', 'Unsteady', or 'Geom').
|
751
|
-
|
752
|
-
Returns:
|
753
|
-
pd.DataFrame: A DataFrame containing the requested entries with appropriate columns.
|
754
|
-
|
755
|
-
Raises:
|
756
|
-
RuntimeError: If the project has not been initialized.
|
757
|
-
|
758
|
-
Example:
|
759
|
-
>>> # Get all geometry files in the project
|
760
|
-
>>> geom_entries = ras.get_prj_entries('Geom')
|
761
|
-
>>> print(f"Project contains {len(geom_entries)} geometry files")
|
762
|
-
|
763
|
-
Note:
|
764
|
-
This is a generic method. For specific file types, use the dedicated methods:
|
765
|
-
get_plan_entries(), get_flow_entries(), get_unsteady_entries(), get_geom_entries()
|
766
|
-
"""
|
767
|
-
self.check_initialized()
|
768
|
-
return self._get_prj_entries(entry_type)
|
769
|
-
|
770
|
-
@log_call
|
771
|
-
def get_plan_entries(self):
|
772
|
-
"""
|
773
|
-
Get all plan entries from the HEC-RAS project.
|
774
|
-
|
775
|
-
Returns a DataFrame containing all plan files (.p*) in the project
|
776
|
-
with their associated properties, paths and settings.
|
777
|
-
|
778
|
-
Returns:
|
779
|
-
pd.DataFrame: A DataFrame with columns including 'plan_number', 'full_path',
|
780
|
-
'unsteady_number', 'geometry_number', etc.
|
781
|
-
|
782
|
-
Raises:
|
783
|
-
RuntimeError: If the project has not been initialized.
|
784
|
-
|
785
|
-
Example:
|
786
|
-
>>> plan_entries = ras.get_plan_entries()
|
787
|
-
>>> print(f"Project contains {len(plan_entries)} plan files")
|
788
|
-
>>> # Display the first plan's properties
|
789
|
-
>>> if not plan_entries.empty:
|
790
|
-
... print(plan_entries.iloc[0])
|
791
|
-
"""
|
792
|
-
self.check_initialized()
|
793
|
-
return self._get_prj_entries('Plan')
|
794
|
-
|
795
|
-
@log_call
|
796
|
-
def get_flow_entries(self):
|
797
|
-
"""
|
798
|
-
Get all flow entries from the HEC-RAS project.
|
799
|
-
|
800
|
-
Returns a DataFrame containing all flow files (.f*) in the project
|
801
|
-
with their associated properties and paths.
|
802
|
-
|
803
|
-
Returns:
|
804
|
-
pd.DataFrame: A DataFrame with columns including 'flow_number', 'full_path', etc.
|
805
|
-
|
806
|
-
Raises:
|
807
|
-
RuntimeError: If the project has not been initialized.
|
808
|
-
|
809
|
-
Example:
|
810
|
-
>>> flow_entries = ras.get_flow_entries()
|
811
|
-
>>> print(f"Project contains {len(flow_entries)} flow files")
|
812
|
-
>>> # Display the first flow file's properties
|
813
|
-
>>> if not flow_entries.empty:
|
814
|
-
... print(flow_entries.iloc[0])
|
815
|
-
"""
|
816
|
-
self.check_initialized()
|
817
|
-
return self._get_prj_entries('Flow')
|
818
|
-
|
819
|
-
@log_call
|
820
|
-
def get_unsteady_entries(self):
|
821
|
-
"""
|
822
|
-
Get all unsteady flow entries from the HEC-RAS project.
|
823
|
-
|
824
|
-
Returns a DataFrame containing all unsteady flow files (.u*) in the project
|
825
|
-
with their associated properties and paths.
|
826
|
-
|
827
|
-
Returns:
|
828
|
-
pd.DataFrame: A DataFrame with columns including 'unsteady_number', 'full_path', etc.
|
829
|
-
|
830
|
-
Raises:
|
831
|
-
RuntimeError: If the project has not been initialized.
|
832
|
-
|
833
|
-
Example:
|
834
|
-
>>> unsteady_entries = ras.get_unsteady_entries()
|
835
|
-
>>> print(f"Project contains {len(unsteady_entries)} unsteady flow files")
|
836
|
-
>>> # Display the first unsteady file's properties
|
837
|
-
>>> if not unsteady_entries.empty:
|
838
|
-
... print(unsteady_entries.iloc[0])
|
839
|
-
"""
|
840
|
-
self.check_initialized()
|
841
|
-
return self._get_prj_entries('Unsteady')
|
842
|
-
|
843
|
-
@log_call
|
844
|
-
def get_geom_entries(self):
|
845
|
-
"""
|
846
|
-
Get all geometry entries from the HEC-RAS project.
|
847
|
-
|
848
|
-
Returns a DataFrame containing all geometry files (.g*) in the project
|
849
|
-
with their associated properties, paths and HDF links.
|
850
|
-
|
851
|
-
Returns:
|
852
|
-
pd.DataFrame: A DataFrame with columns including 'geom_number', 'full_path',
|
853
|
-
'hdf_path', etc.
|
854
|
-
|
855
|
-
Raises:
|
856
|
-
RuntimeError: If the project has not been initialized.
|
857
|
-
|
858
|
-
Example:
|
859
|
-
>>> geom_entries = ras.get_geom_entries()
|
860
|
-
>>> print(f"Project contains {len(geom_entries)} geometry files")
|
861
|
-
>>> # Display the first geometry file's properties
|
862
|
-
>>> if not geom_entries.empty:
|
863
|
-
... print(geom_entries.iloc[0])
|
864
|
-
"""
|
865
|
-
self.check_initialized()
|
866
|
-
geom_pattern = re.compile(r'Geom File=(\w+)')
|
867
|
-
geom_entries = []
|
868
|
-
|
869
|
-
try:
|
870
|
-
with open(self.prj_file, 'r') as f:
|
871
|
-
for line in f:
|
872
|
-
match = geom_pattern.search(line)
|
873
|
-
if match:
|
874
|
-
geom_entries.append(match.group(1))
|
875
|
-
|
876
|
-
geom_df = pd.DataFrame({'geom_file': geom_entries})
|
877
|
-
geom_df['geom_number'] = geom_df['geom_file'].str.extract(r'(\d+)$')
|
878
|
-
geom_df['full_path'] = geom_df['geom_file'].apply(lambda x: str(self.project_folder / f"{self.project_name}.{x}"))
|
879
|
-
geom_df['hdf_path'] = geom_df['full_path'] + ".hdf"
|
880
|
-
|
881
|
-
if not self.suppress_logging: # Only log if suppress_logging is False
|
882
|
-
logger.info(f"Found {len(geom_df)} geometry entries")
|
883
|
-
return geom_df
|
884
|
-
except Exception as e:
|
885
|
-
logger.error(f"Error reading geometry entries from project file: {e}")
|
886
|
-
raise
|
887
|
-
|
888
|
-
@log_call
|
889
|
-
def get_hdf_entries(self):
|
890
|
-
"""
|
891
|
-
Get all plan entries that have associated HDF results files.
|
892
|
-
|
893
|
-
This method identifies which plans have been successfully computed
|
894
|
-
and have HDF results available for further analysis.
|
895
|
-
|
896
|
-
Returns:
|
897
|
-
pd.DataFrame: A DataFrame containing plan entries with HDF results.
|
898
|
-
Returns an empty DataFrame if no results are found.
|
899
|
-
|
900
|
-
Raises:
|
901
|
-
RuntimeError: If the project has not been initialized.
|
902
|
-
|
903
|
-
Example:
|
904
|
-
>>> hdf_entries = ras.get_hdf_entries()
|
905
|
-
>>> if hdf_entries.empty:
|
906
|
-
... print("No computed results found. Run simulations first.")
|
907
|
-
... else:
|
908
|
-
... print(f"Found results for {len(hdf_entries)} plans")
|
909
|
-
|
910
|
-
Note:
|
911
|
-
This is useful for identifying which plans have been successfully computed
|
912
|
-
and can be used for further results analysis.
|
913
|
-
"""
|
914
|
-
self.check_initialized()
|
915
|
-
|
916
|
-
hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
|
917
|
-
|
918
|
-
if hdf_entries.empty:
|
919
|
-
return pd.DataFrame(columns=self.plan_df.columns)
|
920
|
-
|
921
|
-
return hdf_entries
|
922
|
-
|
923
|
-
|
924
|
-
@log_call
|
925
|
-
def print_data(self):
|
926
|
-
"""
|
927
|
-
Print a comprehensive summary of all RAS Object data for this instance.
|
928
|
-
|
929
|
-
This method outputs:
|
930
|
-
- Project information (name, folder, file paths)
|
931
|
-
- Summary of plans, flows, geometries, and unsteady files
|
932
|
-
- HDF results availability
|
933
|
-
- Boundary conditions
|
934
|
-
|
935
|
-
Useful for debugging, validation, and exploring project structure.
|
936
|
-
|
937
|
-
Raises:
|
938
|
-
RuntimeError: If the project has not been initialized.
|
939
|
-
|
940
|
-
Example:
|
941
|
-
>>> ras.print_data() # Displays complete project overview
|
942
|
-
"""
|
943
|
-
self.check_initialized()
|
944
|
-
logger.info(f"--- Data for {self.project_name} ---")
|
945
|
-
logger.info(f"Project folder: {self.project_folder}")
|
946
|
-
logger.info(f"PRJ file: {self.prj_file}")
|
947
|
-
logger.info(f"HEC-RAS executable: {self.ras_exe_path}")
|
948
|
-
logger.info("Plan files:")
|
949
|
-
logger.info(f"\n{self.plan_df}")
|
950
|
-
logger.info("Flow files:")
|
951
|
-
logger.info(f"\n{self.flow_df}")
|
952
|
-
logger.info("Unsteady flow files:")
|
953
|
-
logger.info(f"\n{self.unsteady_df}")
|
954
|
-
logger.info("Geometry files:")
|
955
|
-
logger.info(f"\n{self.geom_df}")
|
956
|
-
logger.info("HDF entries:")
|
957
|
-
logger.info(f"\n{self.get_hdf_entries()}")
|
958
|
-
logger.info("Boundary conditions:")
|
959
|
-
logger.info(f"\n{self.boundaries_df}")
|
960
|
-
logger.info("----------------------------")
|
961
|
-
|
962
|
-
@log_call
|
963
|
-
def get_boundary_conditions(self) -> pd.DataFrame:
|
964
|
-
"""
|
965
|
-
Extract boundary conditions from unsteady flow files into a structured DataFrame.
|
966
|
-
|
967
|
-
This method:
|
968
|
-
1. Parses all unsteady flow files to extract boundary condition information
|
969
|
-
2. Creates a structured DataFrame with boundary locations, types and parameters
|
970
|
-
3. Links boundary conditions to their respective unsteady flow files
|
971
|
-
|
972
|
-
Supported boundary condition types include:
|
973
|
-
- Flow Hydrograph
|
974
|
-
- Stage Hydrograph
|
975
|
-
- Normal Depth
|
976
|
-
- Lateral Inflow Hydrograph
|
977
|
-
- Uniform Lateral Inflow Hydrograph
|
978
|
-
- Gate Opening
|
979
|
-
|
980
|
-
Returns:
|
981
|
-
pd.DataFrame: A DataFrame containing detailed boundary condition information.
|
982
|
-
Returns an empty DataFrame if no unsteady flow files are present.
|
983
|
-
|
984
|
-
Example:
|
985
|
-
>>> boundaries = ras.get_boundary_conditions()
|
986
|
-
>>> if not boundaries.empty:
|
987
|
-
... print(f"Found {len(boundaries)} boundary conditions")
|
988
|
-
... # Show flow hydrographs only
|
989
|
-
... flow_hydrographs = boundaries[boundaries['bc_type'] == 'Flow Hydrograph']
|
990
|
-
... print(f"Project has {len(flow_hydrographs)} flow hydrographs")
|
991
|
-
|
992
|
-
Note:
|
993
|
-
To see unparsed boundary condition lines for debugging, set logging to DEBUG:
|
994
|
-
import logging
|
995
|
-
logging.getLogger().setLevel(logging.DEBUG)
|
996
|
-
"""
|
997
|
-
boundary_data = []
|
998
|
-
|
999
|
-
# Check if unsteady_df is empty
|
1000
|
-
if self.unsteady_df.empty:
|
1001
|
-
logger.info("No unsteady flow files found in the project.")
|
1002
|
-
return pd.DataFrame() # Return an empty DataFrame
|
1003
|
-
|
1004
|
-
for _, row in self.unsteady_df.iterrows():
|
1005
|
-
unsteady_file_path = row['full_path']
|
1006
|
-
unsteady_number = row['unsteady_number']
|
1007
|
-
|
1008
|
-
try:
|
1009
|
-
with open(unsteady_file_path, 'r') as file:
|
1010
|
-
content = file.read()
|
1011
|
-
except IOError as e:
|
1012
|
-
logger.error(f"Error reading unsteady file {unsteady_file_path}: {e}")
|
1013
|
-
continue
|
1014
|
-
|
1015
|
-
bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
|
1016
|
-
|
1017
|
-
for i, block in enumerate(bc_blocks, 1):
|
1018
|
-
bc_info, unparsed_lines = self._parse_boundary_condition(block, unsteady_number, i)
|
1019
|
-
boundary_data.append(bc_info)
|
1020
|
-
|
1021
|
-
if unparsed_lines:
|
1022
|
-
logger.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
1023
|
-
|
1024
|
-
if not boundary_data:
|
1025
|
-
logger.info("No boundary conditions found in unsteady flow files.")
|
1026
|
-
return pd.DataFrame() # Return an empty DataFrame if no boundary conditions were found
|
1027
|
-
|
1028
|
-
boundaries_df = pd.DataFrame(boundary_data)
|
1029
|
-
|
1030
|
-
# Merge with unsteady_df to get relevant unsteady flow file information
|
1031
|
-
merged_df = pd.merge(boundaries_df, self.unsteady_df,
|
1032
|
-
left_on='unsteady_number', right_on='unsteady_number', how='left')
|
1033
|
-
|
1034
|
-
return merged_df
|
1035
|
-
|
1036
|
-
def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
|
1037
|
-
lines = block.split('\n')
|
1038
|
-
bc_info = {
|
1039
|
-
'unsteady_number': unsteady_number,
|
1040
|
-
'boundary_condition_number': bc_number
|
1041
|
-
}
|
1042
|
-
|
1043
|
-
parsed_lines = set()
|
1044
|
-
|
1045
|
-
# Parse Boundary Location
|
1046
|
-
boundary_location = lines[0].split('=')[1].strip()
|
1047
|
-
fields = [field.strip() for field in boundary_location.split(',')]
|
1048
|
-
bc_info.update({
|
1049
|
-
'river_reach_name': fields[0] if len(fields) > 0 else '',
|
1050
|
-
'river_station': fields[1] if len(fields) > 1 else '',
|
1051
|
-
'storage_area_name': fields[2] if len(fields) > 2 else '',
|
1052
|
-
'pump_station_name': fields[3] if len(fields) > 3 else ''
|
1053
|
-
})
|
1054
|
-
parsed_lines.add(0)
|
1055
|
-
|
1056
|
-
# Determine BC Type
|
1057
|
-
bc_types = {
|
1058
|
-
'Flow Hydrograph=': 'Flow Hydrograph',
|
1059
|
-
'Lateral Inflow Hydrograph=': 'Lateral Inflow Hydrograph',
|
1060
|
-
'Uniform Lateral Inflow Hydrograph=': 'Uniform Lateral Inflow Hydrograph',
|
1061
|
-
'Stage Hydrograph=': 'Stage Hydrograph',
|
1062
|
-
'Friction Slope=': 'Normal Depth',
|
1063
|
-
'Gate Name=': 'Gate Opening'
|
1064
|
-
}
|
1065
|
-
|
1066
|
-
bc_info['bc_type'] = 'Unknown'
|
1067
|
-
bc_info['hydrograph_type'] = None
|
1068
|
-
for i, line in enumerate(lines[1:], 1):
|
1069
|
-
for key, bc_type in bc_types.items():
|
1070
|
-
if line.startswith(key):
|
1071
|
-
bc_info['bc_type'] = bc_type
|
1072
|
-
if 'Hydrograph' in bc_type:
|
1073
|
-
bc_info['hydrograph_type'] = bc_type
|
1074
|
-
parsed_lines.add(i)
|
1075
|
-
break
|
1076
|
-
if bc_info['bc_type'] != 'Unknown':
|
1077
|
-
break
|
1078
|
-
|
1079
|
-
# Parse other fields
|
1080
|
-
known_fields = ['Interval', 'DSS Path', 'Use DSS', 'Use Fixed Start Time', 'Fixed Start Date/Time',
|
1081
|
-
'Is Critical Boundary', 'Critical Boundary Flow', 'DSS File']
|
1082
|
-
for i, line in enumerate(lines):
|
1083
|
-
if '=' in line:
|
1084
|
-
key, value = line.split('=', 1)
|
1085
|
-
key = key.strip()
|
1086
|
-
if key in known_fields:
|
1087
|
-
bc_info[key] = value.strip()
|
1088
|
-
parsed_lines.add(i)
|
1089
|
-
|
1090
|
-
# Handle hydrograph values
|
1091
|
-
bc_info['hydrograph_num_values'] = 0
|
1092
|
-
if bc_info['hydrograph_type']:
|
1093
|
-
hydrograph_key = f"{bc_info['hydrograph_type']}="
|
1094
|
-
hydrograph_line = next((line for i, line in enumerate(lines) if line.startswith(hydrograph_key)), None)
|
1095
|
-
if hydrograph_line:
|
1096
|
-
hydrograph_index = lines.index(hydrograph_line)
|
1097
|
-
values_count = int(hydrograph_line.split('=')[1].strip())
|
1098
|
-
bc_info['hydrograph_num_values'] = values_count
|
1099
|
-
if values_count > 0:
|
1100
|
-
values = ' '.join(lines[hydrograph_index + 1:]).split()[:values_count]
|
1101
|
-
bc_info['hydrograph_values'] = values
|
1102
|
-
parsed_lines.update(range(hydrograph_index, hydrograph_index + (values_count // 5) + 2))
|
1103
|
-
|
1104
|
-
# Collect unparsed lines
|
1105
|
-
unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
|
1106
|
-
|
1107
|
-
if unparsed_lines:
|
1108
|
-
logger.debug(f"Unparsed lines for boundary condition {bc_number} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
1109
|
-
|
1110
|
-
return bc_info, unparsed_lines
|
1111
|
-
|
1112
|
-
@log_call
|
1113
|
-
def _format_dataframe(self, df, entry_type):
|
1114
|
-
"""
|
1115
|
-
Format the DataFrame according to the desired column structure.
|
1116
|
-
|
1117
|
-
Args:
|
1118
|
-
df (pd.DataFrame): The DataFrame to format.
|
1119
|
-
entry_type (str): The type of entry (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
|
1120
|
-
|
1121
|
-
Returns:
|
1122
|
-
pd.DataFrame: The formatted DataFrame.
|
1123
|
-
"""
|
1124
|
-
if df.empty:
|
1125
|
-
return df
|
1126
|
-
|
1127
|
-
if entry_type == 'Plan':
|
1128
|
-
# Set required column order
|
1129
|
-
first_cols = ['plan_number', 'unsteady_number', 'geometry_number']
|
1130
|
-
|
1131
|
-
# Standard plan key columns in the exact order specified
|
1132
|
-
plan_key_cols = [
|
1133
|
-
'Plan Title', 'Program Version', 'Short Identifier', 'Simulation Date',
|
1134
|
-
'Std Step Tol', 'Computation Interval', 'Output Interval', 'Instantaneous Interval',
|
1135
|
-
'Mapping Interval', 'Run HTab', 'Run UNet', 'Run Sediment', 'Run PostProcess',
|
1136
|
-
'Run WQNet', 'Run RASMapper', 'UNET Use Existing IB Tables', 'HDF_Results_Path',
|
1137
|
-
'UNET 1D Methodology', 'Write IC File', 'Write IC File at Fixed DateTime',
|
1138
|
-
'IC Time', 'Write IC File Reoccurance', 'Write IC File at Sim End'
|
1139
|
-
]
|
1140
|
-
|
1141
|
-
# Additional convenience columns
|
1142
|
-
file_path_cols = ['Geom File', 'Geom Path', 'Flow File', 'Flow Path']
|
1143
|
-
|
1144
|
-
# Special columns that must be preserved
|
1145
|
-
special_cols = ['HDF_Results_Path']
|
1146
|
-
|
1147
|
-
# Build the final column list
|
1148
|
-
all_cols = first_cols.copy()
|
1149
|
-
|
1150
|
-
# Add plan key columns if they exist
|
1151
|
-
for col in plan_key_cols:
|
1152
|
-
if col in df.columns and col not in all_cols and col not in special_cols:
|
1153
|
-
all_cols.append(col)
|
1154
|
-
|
1155
|
-
# Add any remaining columns not explicitly specified
|
1156
|
-
other_cols = [col for col in df.columns if col not in all_cols + file_path_cols + special_cols + ['full_path']]
|
1157
|
-
all_cols.extend(other_cols)
|
1158
|
-
|
1159
|
-
# Add HDF_Results_Path if it exists (ensure it comes before file paths)
|
1160
|
-
for special_col in special_cols:
|
1161
|
-
if special_col in df.columns and special_col not in all_cols:
|
1162
|
-
all_cols.append(special_col)
|
1163
|
-
|
1164
|
-
# Add file path columns at the end
|
1165
|
-
all_cols.extend(file_path_cols)
|
1166
|
-
|
1167
|
-
# Rename plan_number column
|
1168
|
-
df = df.rename(columns={f'{entry_type.lower()}_number': 'plan_number'})
|
1169
|
-
|
1170
|
-
# Fill in missing columns with None
|
1171
|
-
for col in all_cols:
|
1172
|
-
if col not in df.columns:
|
1173
|
-
df[col] = None
|
1174
|
-
|
1175
|
-
# Make sure full_path column is preserved and included
|
1176
|
-
if 'full_path' in df.columns and 'full_path' not in all_cols:
|
1177
|
-
all_cols.append('full_path')
|
1178
|
-
|
1179
|
-
# Return DataFrame with specified column order
|
1180
|
-
cols_to_return = [col for col in all_cols if col in df.columns]
|
1181
|
-
return df[cols_to_return]
|
1182
|
-
|
1183
|
-
return df
|
1184
|
-
|
1185
|
-
@log_call
|
1186
|
-
def _get_prj_entries(self, entry_type):
|
1187
|
-
"""
|
1188
|
-
Extract entries of a specific type from the HEC-RAS project file.
|
1189
|
-
"""
|
1190
|
-
entries = []
|
1191
|
-
pattern = re.compile(rf"{entry_type} File=(\w+)")
|
1192
|
-
|
1193
|
-
try:
|
1194
|
-
with open(self.prj_file, 'r') as file:
|
1195
|
-
for line in file:
|
1196
|
-
match = pattern.match(line.strip())
|
1197
|
-
if match:
|
1198
|
-
file_name = match.group(1)
|
1199
|
-
full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
|
1200
|
-
entry = self._create_entry(entry_type, file_name, full_path)
|
1201
|
-
entries.append(entry)
|
1202
|
-
|
1203
|
-
return self._format_dataframe(pd.DataFrame(entries), entry_type)
|
1204
|
-
|
1205
|
-
except Exception as e:
|
1206
|
-
logger.error(f"Error in _get_prj_entries for {entry_type}: {e}")
|
1207
|
-
raise
|
1208
|
-
|
1209
|
-
def _create_entry(self, entry_type, file_name, full_path):
|
1210
|
-
"""Helper method to create entry dictionary."""
|
1211
|
-
entry_number = file_name[1:]
|
1212
|
-
entry = {
|
1213
|
-
f'{entry_type.lower()}_number': entry_number,
|
1214
|
-
'full_path': full_path,
|
1215
|
-
'unsteady_number': None,
|
1216
|
-
'geometry_number': None
|
1217
|
-
}
|
1218
|
-
|
1219
|
-
if entry_type == 'Unsteady':
|
1220
|
-
entry['unsteady_number'] = entry_number
|
1221
|
-
entry.update(self._parse_unsteady_file(Path(full_path)))
|
1222
|
-
elif entry_type == 'Plan':
|
1223
|
-
self._update_plan_entry(entry, entry_number, full_path)
|
1224
|
-
|
1225
|
-
return entry
|
1226
|
-
|
1227
|
-
def _update_plan_entry(self, entry, entry_number, full_path):
|
1228
|
-
"""Helper method to update plan entry with additional information."""
|
1229
|
-
plan_info = self._parse_plan_file(Path(full_path))
|
1230
|
-
if plan_info:
|
1231
|
-
# Handle Flow File
|
1232
|
-
flow_file = plan_info.get('Flow File')
|
1233
|
-
if flow_file:
|
1234
|
-
if flow_file.startswith('u'):
|
1235
|
-
entry.update({'unsteady_number': flow_file[1:], 'Flow File': flow_file[1:]})
|
1236
|
-
else:
|
1237
|
-
entry['Flow File'] = flow_file[1:] if flow_file.startswith('f') else None
|
1238
|
-
|
1239
|
-
# Handle Geom File
|
1240
|
-
geom_file = plan_info.get('Geom File')
|
1241
|
-
if geom_file and geom_file.startswith('g'):
|
1242
|
-
entry.update({'geometry_number': geom_file[1:], 'Geom File': geom_file[1:]})
|
1243
|
-
|
1244
|
-
# Add remaining plan info
|
1245
|
-
entry.update({k: v for k, v in plan_info.items() if k not in ['Flow File', 'Geom File']})
|
1246
|
-
|
1247
|
-
# Add HDF results path
|
1248
|
-
hdf_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
|
1249
|
-
entry['HDF_Results_Path'] = str(hdf_path) if hdf_path.exists() else None
|
1250
|
-
|
1251
|
-
|
1252
|
-
# Create a global instance named 'ras'
|
1253
|
-
# Defining the global instance allows the init_ras_project function to initialize the project.
|
1254
|
-
# This only happens on the library initialization, not when the user calls init_ras_project.
|
1255
|
-
ras = RasPrj()
|
1256
|
-
|
1257
|
-
# END OF CLASS DEFINITION
|
1258
|
-
|
1259
|
-
|
1260
|
-
# START OF FUNCTION DEFINITIONS
|
1261
|
-
|
1262
|
-
@log_call
|
1263
|
-
def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
|
1264
|
-
"""
|
1265
|
-
Initialize a RAS project for use with the ras-commander library.
|
1266
|
-
|
1267
|
-
This is the primary function for setting up a HEC-RAS project. It:
|
1268
|
-
1. Finds the project file (.prj) in the specified folder
|
1269
|
-
2. Identifies the appropriate HEC-RAS executable
|
1270
|
-
3. Loads project data (plans, geometries, flows)
|
1271
|
-
4. Creates dataframes containing project components
|
1272
|
-
|
1273
|
-
Args:
|
1274
|
-
ras_project_folder (str or Path): The path to the RAS project folder.
|
1275
|
-
ras_version (str, optional): The version of RAS to use (e.g., "6.6")
|
1276
|
-
|
1277
|
-
|
1278
|
-
ras_object (RasPrj, optional): If None, updates the global 'ras' object.
|
1279
|
-
If a RasPrj instance, updates that instance.
|
1280
|
-
If any other value, creates and returns a new RasPrj instance.
|
1281
|
-
|
1282
|
-
Returns:
|
1283
|
-
RasPrj: An initialized RasPrj instance.
|
1284
|
-
|
1285
|
-
Raises:
|
1286
|
-
FileNotFoundError: If the specified project folder doesn't exist.
|
1287
|
-
ValueError: If no HEC-RAS project file is found in the folder.
|
1288
|
-
|
1289
|
-
Example:
|
1290
|
-
>>> # Initialize using the global 'ras' object (most common)
|
1291
|
-
>>> init_ras_project("/path/to/project", "6.6")
|
1292
|
-
>>> print(f"Initialized project: {ras.project_name}")
|
1293
|
-
>>>
|
1294
|
-
>>> # Create a new RasPrj instance
|
1295
|
-
>>> my_project = init_ras_project("/path/to/project", "6.6", "new")
|
1296
|
-
>>> print(f"Created project instance: {my_project.project_name}")
|
1297
|
-
"""
|
1298
|
-
project_folder = Path(ras_project_folder)
|
1299
|
-
if not project_folder.exists():
|
1300
|
-
logger.error(f"The specified RAS project folder does not exist: {project_folder}")
|
1301
|
-
raise FileNotFoundError(f"The specified RAS project folder does not exist: {project_folder}. Please check the path and try again.")
|
1302
|
-
|
1303
|
-
# Determine which RasPrj instance to use
|
1304
|
-
if ras_object is None:
|
1305
|
-
# Use the global 'ras' object
|
1306
|
-
logger.debug("Initializing global 'ras' object via init_ras_project function.")
|
1307
|
-
ras_object = ras
|
1308
|
-
elif not isinstance(ras_object, RasPrj):
|
1309
|
-
# Create a new RasPrj instance
|
1310
|
-
logger.debug("Creating a new RasPrj instance.")
|
1311
|
-
ras_object = RasPrj()
|
1312
|
-
|
1313
|
-
ras_exe_path = None
|
1314
|
-
|
1315
|
-
# Use version specified by user if provided
|
1316
|
-
if ras_version is not None:
|
1317
|
-
ras_exe_path = get_ras_exe(ras_version)
|
1318
|
-
if ras_exe_path == "Ras.exe" and ras_version != "Ras.exe":
|
1319
|
-
logger.warning(f"HEC-RAS Version {ras_version} was not found. Running HEC-RAS will fail.")
|
1320
|
-
else:
|
1321
|
-
# No version specified, try to detect from plan files
|
1322
|
-
detected_version = None
|
1323
|
-
logger.info("No HEC-RAS Version Specified.Attempting to detect HEC-RAS version from plan files.")
|
1324
|
-
|
1325
|
-
# Look for .pXX files in project folder
|
1326
|
-
logger.info(f"Searching for plan files in {project_folder}")
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
for
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
""
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
|
1429
|
-
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1444
|
-
|
1445
|
-
|
1446
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
1453
|
-
|
1454
|
-
|
1455
|
-
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
if
|
1460
|
-
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{
|
1461
|
-
if default_path.is_file():
|
1462
|
-
logger.debug(f"HEC-RAS executable found at path
|
1463
|
-
return str(default_path)
|
1464
|
-
|
1465
|
-
|
1466
|
-
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1
|
+
"""
|
2
|
+
RasPrj.py - Manages HEC-RAS projects within the ras-commander library
|
3
|
+
|
4
|
+
This module provides a class for managing HEC-RAS projects.
|
5
|
+
|
6
|
+
Classes:
|
7
|
+
RasPrj: A class for managing HEC-RAS projects.
|
8
|
+
|
9
|
+
Functions:
|
10
|
+
init_ras_project: Initialize a RAS project.
|
11
|
+
get_ras_exe: Determine the HEC-RAS executable path based on the input.
|
12
|
+
|
13
|
+
DEVELOPER NOTE:
|
14
|
+
This class is used to initialize a RAS project and is used in conjunction with the RasCmdr class to manage the execution of RAS plans.
|
15
|
+
By default, the RasPrj class is initialized with the global 'ras' object.
|
16
|
+
However, you can create multiple RasPrj instances to manage multiple projects.
|
17
|
+
Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
|
18
|
+
|
19
|
+
This module is part of the ras-commander library and uses a centralized logging configuration.
|
20
|
+
|
21
|
+
Logging Configuration:
|
22
|
+
- The logging is set up in the logging_config.py file.
|
23
|
+
- A @log_call decorator is available to automatically log function calls.
|
24
|
+
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
25
|
+
- Logs are written to both console and a rotating file handler.
|
26
|
+
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
27
|
+
- The default log level is INFO.
|
28
|
+
|
29
|
+
To use logging in this module:
|
30
|
+
1. Use the @log_call decorator for automatic function call logging.
|
31
|
+
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
32
|
+
|
33
|
+
|
34
|
+
Example:
|
35
|
+
@log_call
|
36
|
+
def my_function():
|
37
|
+
|
38
|
+
logger.debug("Additional debug information")
|
39
|
+
# Function logic here
|
40
|
+
|
41
|
+
-----
|
42
|
+
|
43
|
+
All of the methods in this class are class methods and are designed to be used with instances of the class.
|
44
|
+
|
45
|
+
List of Functions in RasPrj:
|
46
|
+
- initialize()
|
47
|
+
- _load_project_data()
|
48
|
+
- _get_geom_file_for_plan()
|
49
|
+
- _parse_plan_file()
|
50
|
+
- _parse_unsteady_file()
|
51
|
+
- _get_prj_entries()
|
52
|
+
- _parse_boundary_condition()
|
53
|
+
- is_initialized (property)
|
54
|
+
- check_initialized()
|
55
|
+
- find_ras_prj()
|
56
|
+
- get_project_name()
|
57
|
+
- get_prj_entries()
|
58
|
+
- get_plan_entries()
|
59
|
+
- get_flow_entries()
|
60
|
+
- get_unsteady_entries()
|
61
|
+
- get_geom_entries()
|
62
|
+
- get_hdf_entries()
|
63
|
+
- print_data()
|
64
|
+
- get_plan_value()
|
65
|
+
- get_boundary_conditions()
|
66
|
+
|
67
|
+
Functions in RasPrj that are not part of the class:
|
68
|
+
- init_ras_project()
|
69
|
+
- get_ras_exe()
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
"""
|
75
|
+
import os
|
76
|
+
import re
|
77
|
+
from pathlib import Path
|
78
|
+
import pandas as pd
|
79
|
+
from typing import Union, Any, List, Dict, Tuple
|
80
|
+
import logging
|
81
|
+
from ras_commander.LoggingConfig import get_logger
|
82
|
+
from ras_commander.Decorators import log_call
|
83
|
+
|
84
|
+
logger = get_logger(__name__)
|
85
|
+
|
86
|
+
def read_file_with_fallback_encoding(file_path, encodings=['utf-8', 'latin1', 'cp1252', 'iso-8859-1']):
|
87
|
+
"""
|
88
|
+
Attempt to read a file using multiple encodings.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
file_path (str or Path): Path to the file to read
|
92
|
+
encodings (list): List of encodings to try, in order of preference
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
tuple: (content, encoding) or (None, None) if all encodings fail
|
96
|
+
"""
|
97
|
+
for encoding in encodings:
|
98
|
+
try:
|
99
|
+
with open(file_path, 'r', encoding=encoding) as file:
|
100
|
+
content = file.read()
|
101
|
+
return content, encoding
|
102
|
+
except UnicodeDecodeError:
|
103
|
+
continue
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Error reading file {file_path} with {encoding} encoding: {e}")
|
106
|
+
continue
|
107
|
+
|
108
|
+
logger.error(f"Failed to read file {file_path} with any of the attempted encodings: {encodings}")
|
109
|
+
return None, None
|
110
|
+
|
111
|
+
class RasPrj:
|
112
|
+
|
113
|
+
def __init__(self):
|
114
|
+
self.initialized = False
|
115
|
+
self.boundaries_df = None # New attribute to store boundary conditions
|
116
|
+
self.suppress_logging = False # Add suppress_logging as instance variable
|
117
|
+
|
118
|
+
@log_call
|
119
|
+
def initialize(self, project_folder, ras_exe_path, suppress_logging=True):
|
120
|
+
"""
|
121
|
+
Initialize a RasPrj instance with project folder and RAS executable path.
|
122
|
+
|
123
|
+
IMPORTANT: External users should use init_ras_project() function instead of this method.
|
124
|
+
This method is intended for internal use only.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
project_folder (str or Path): Path to the HEC-RAS project folder.
|
128
|
+
ras_exe_path (str or Path): Path to the HEC-RAS executable.
|
129
|
+
suppress_logging (bool, default=True): If True, suppresses initialization logging messages.
|
130
|
+
|
131
|
+
Raises:
|
132
|
+
ValueError: If no HEC-RAS project file is found in the specified folder.
|
133
|
+
|
134
|
+
Note:
|
135
|
+
This method sets up the RasPrj instance by:
|
136
|
+
1. Finding the project file (.prj)
|
137
|
+
2. Loading project data (plans, geometries, flows)
|
138
|
+
3. Extracting boundary conditions
|
139
|
+
4. Setting the initialization flag
|
140
|
+
5. Loading RASMapper data (.rasmap)
|
141
|
+
"""
|
142
|
+
self.suppress_logging = suppress_logging # Store suppress_logging state
|
143
|
+
self.project_folder = Path(project_folder)
|
144
|
+
self.prj_file = self.find_ras_prj(self.project_folder)
|
145
|
+
if self.prj_file is None:
|
146
|
+
logger.error(f"No HEC-RAS project file found in {self.project_folder}")
|
147
|
+
raise ValueError(f"No HEC-RAS project file found in {self.project_folder}. Please check the path and try again.")
|
148
|
+
self.project_name = Path(self.prj_file).stem
|
149
|
+
self.ras_exe_path = ras_exe_path
|
150
|
+
|
151
|
+
# Set initialized to True before loading project data
|
152
|
+
self.initialized = True
|
153
|
+
|
154
|
+
# Now load the project data
|
155
|
+
self._load_project_data()
|
156
|
+
self.boundaries_df = self.get_boundary_conditions()
|
157
|
+
|
158
|
+
# Load RASMapper data if available
|
159
|
+
try:
|
160
|
+
# Import here to avoid circular imports
|
161
|
+
from .RasMap import RasMap
|
162
|
+
self.rasmap_df = RasMap.initialize_rasmap_df(self)
|
163
|
+
except ImportError:
|
164
|
+
logger.warning("RasMap module not available. RASMapper data will not be loaded.")
|
165
|
+
self.rasmap_df = pd.DataFrame(columns=['projection_path', 'profile_lines_path', 'soil_layer_path',
|
166
|
+
'infiltration_hdf_path', 'landcover_hdf_path', 'terrain_hdf_path',
|
167
|
+
'current_settings'])
|
168
|
+
except Exception as e:
|
169
|
+
logger.error(f"Error initializing RASMapper data: {e}")
|
170
|
+
self.rasmap_df = pd.DataFrame(columns=['projection_path', 'profile_lines_path', 'soil_layer_path',
|
171
|
+
'infiltration_hdf_path', 'landcover_hdf_path', 'terrain_hdf_path',
|
172
|
+
'current_settings'])
|
173
|
+
|
174
|
+
if not suppress_logging:
|
175
|
+
logger.info(f"Initialization complete for project: {self.project_name}")
|
176
|
+
logger.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
|
177
|
+
f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
|
178
|
+
f"Boundary conditions: {len(self.boundaries_df)}")
|
179
|
+
logger.info(f"Geometry HDF files found: {self.plan_df['Geom_File'].notna().sum()}")
|
180
|
+
logger.info(f"RASMapper data loaded: {not self.rasmap_df.empty}")
|
181
|
+
|
182
|
+
@log_call
|
183
|
+
def _load_project_data(self):
|
184
|
+
"""
|
185
|
+
Load project data from the HEC-RAS project file.
|
186
|
+
|
187
|
+
This internal method:
|
188
|
+
1. Initializes DataFrames for plan, flow, unsteady, and geometry entries
|
189
|
+
2. Ensures all required columns are present with appropriate default values
|
190
|
+
3. Sets file paths for all components (geometries, flows, plans)
|
191
|
+
|
192
|
+
Raises:
|
193
|
+
Exception: If there's an error loading or processing project data.
|
194
|
+
"""
|
195
|
+
try:
|
196
|
+
# Load data frames
|
197
|
+
self.unsteady_df = self._get_prj_entries('Unsteady')
|
198
|
+
self.plan_df = self._get_prj_entries('Plan')
|
199
|
+
self.flow_df = self._get_prj_entries('Flow')
|
200
|
+
self.geom_df = self.get_geom_entries()
|
201
|
+
|
202
|
+
# Ensure required columns exist
|
203
|
+
self._ensure_required_columns()
|
204
|
+
|
205
|
+
# Set paths for geometry and flow files
|
206
|
+
self._set_file_paths()
|
207
|
+
|
208
|
+
# Make sure all plan paths are properly set
|
209
|
+
self._set_plan_paths()
|
210
|
+
|
211
|
+
except Exception as e:
|
212
|
+
logger.error(f"Error loading project data: {e}")
|
213
|
+
raise
|
214
|
+
|
215
|
+
def _ensure_required_columns(self):
|
216
|
+
"""Ensure all required columns exist in plan_df."""
|
217
|
+
required_columns = [
|
218
|
+
'plan_number', 'unsteady_number', 'geometry_number',
|
219
|
+
'Geom File', 'Geom Path', 'Flow File', 'Flow Path', 'full_path'
|
220
|
+
]
|
221
|
+
|
222
|
+
for col in required_columns:
|
223
|
+
if col not in self.plan_df.columns:
|
224
|
+
self.plan_df[col] = None
|
225
|
+
|
226
|
+
if not self.plan_df['full_path'].any():
|
227
|
+
self.plan_df['full_path'] = self.plan_df['plan_number'].apply(
|
228
|
+
lambda x: str(self.project_folder / f"{self.project_name}.p{x}")
|
229
|
+
)
|
230
|
+
|
231
|
+
def _set_file_paths(self):
|
232
|
+
"""Set geometry and flow paths in plan_df."""
|
233
|
+
for idx, row in self.plan_df.iterrows():
|
234
|
+
try:
|
235
|
+
self._set_geom_path(idx, row)
|
236
|
+
self._set_flow_path(idx, row)
|
237
|
+
|
238
|
+
if not self.suppress_logging:
|
239
|
+
logger.info(f"Plan {row['plan_number']} paths set up")
|
240
|
+
except Exception as e:
|
241
|
+
logger.error(f"Error processing plan file {row['plan_number']}: {e}")
|
242
|
+
|
243
|
+
def _set_geom_path(self, idx: int, row: pd.Series):
|
244
|
+
"""Set geometry path for a plan entry."""
|
245
|
+
if pd.notna(row['Geom File']):
|
246
|
+
geom_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
|
247
|
+
self.plan_df.at[idx, 'Geom Path'] = str(geom_path)
|
248
|
+
|
249
|
+
def _set_flow_path(self, idx: int, row: pd.Series):
|
250
|
+
"""Set flow path for a plan entry."""
|
251
|
+
if pd.notna(row['Flow File']):
|
252
|
+
prefix = 'u' if pd.notna(row['unsteady_number']) else 'f'
|
253
|
+
flow_path = self.project_folder / f"{self.project_name}.{prefix}{row['Flow File']}"
|
254
|
+
self.plan_df.at[idx, 'Flow Path'] = str(flow_path)
|
255
|
+
|
256
|
+
def _set_plan_paths(self):
|
257
|
+
"""Set full path information for plan files and their associated geometry and flow files."""
|
258
|
+
if self.plan_df.empty:
|
259
|
+
logger.debug("Plan DataFrame is empty, no paths to set")
|
260
|
+
return
|
261
|
+
|
262
|
+
# Ensure full path is set for all plan entries
|
263
|
+
if 'full_path' not in self.plan_df.columns or self.plan_df['full_path'].isna().any():
|
264
|
+
self.plan_df['full_path'] = self.plan_df['plan_number'].apply(
|
265
|
+
lambda x: str(self.project_folder / f"{self.project_name}.p{x}")
|
266
|
+
)
|
267
|
+
|
268
|
+
# Create the Geom Path and Flow Path columns if they don't exist
|
269
|
+
if 'Geom Path' not in self.plan_df.columns:
|
270
|
+
self.plan_df['Geom Path'] = None
|
271
|
+
if 'Flow Path' not in self.plan_df.columns:
|
272
|
+
self.plan_df['Flow Path'] = None
|
273
|
+
|
274
|
+
# Update paths for each plan entry
|
275
|
+
for idx, row in self.plan_df.iterrows():
|
276
|
+
try:
|
277
|
+
# Set geometry path if Geom File exists and Geom Path is missing or invalid
|
278
|
+
if pd.notna(row['Geom File']):
|
279
|
+
geom_path = self.project_folder / f"{self.project_name}.g{row['Geom File']}"
|
280
|
+
self.plan_df.at[idx, 'Geom Path'] = str(geom_path)
|
281
|
+
|
282
|
+
# Set flow path if Flow File exists and Flow Path is missing or invalid
|
283
|
+
if pd.notna(row['Flow File']):
|
284
|
+
# Determine the prefix (u for unsteady, f for steady flow)
|
285
|
+
prefix = 'u' if pd.notna(row['unsteady_number']) else 'f'
|
286
|
+
flow_path = self.project_folder / f"{self.project_name}.{prefix}{row['Flow File']}"
|
287
|
+
self.plan_df.at[idx, 'Flow Path'] = str(flow_path)
|
288
|
+
|
289
|
+
if not self.suppress_logging:
|
290
|
+
logger.debug(f"Plan {row['plan_number']} paths set up")
|
291
|
+
except Exception as e:
|
292
|
+
logger.error(f"Error setting paths for plan {row.get('plan_number', idx)}: {e}")
|
293
|
+
|
294
|
+
def _get_geom_file_for_plan(self, plan_number):
|
295
|
+
"""
|
296
|
+
Get the geometry file path for a given plan number.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
plan_number (str): The plan number to find the geometry file for.
|
300
|
+
|
301
|
+
Returns:
|
302
|
+
str: The full path to the geometry HDF file, or None if not found.
|
303
|
+
"""
|
304
|
+
plan_file_path = self.project_folder / f"{self.project_name}.p{plan_number}"
|
305
|
+
content, encoding = read_file_with_fallback_encoding(plan_file_path)
|
306
|
+
|
307
|
+
if content is None:
|
308
|
+
return None
|
309
|
+
|
310
|
+
try:
|
311
|
+
for line in content.splitlines():
|
312
|
+
if line.startswith("Geom File="):
|
313
|
+
geom_file = line.strip().split('=')[1]
|
314
|
+
geom_hdf_path = self.project_folder / f"{self.project_name}.{geom_file}.hdf"
|
315
|
+
if geom_hdf_path.exists():
|
316
|
+
return str(geom_hdf_path)
|
317
|
+
else:
|
318
|
+
return None
|
319
|
+
except Exception as e:
|
320
|
+
logger.error(f"Error reading plan file for geometry: {e}")
|
321
|
+
return None
|
322
|
+
|
323
|
+
|
324
|
+
@staticmethod
|
325
|
+
@log_call
|
326
|
+
def get_plan_value(
|
327
|
+
plan_number_or_path: Union[str, Path],
|
328
|
+
key: str,
|
329
|
+
ras_object=None
|
330
|
+
) -> Any:
|
331
|
+
"""
|
332
|
+
Retrieve a specific value from a HEC-RAS plan file.
|
333
|
+
|
334
|
+
Parameters:
|
335
|
+
plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
|
336
|
+
key (str): The key to retrieve from the plan file
|
337
|
+
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
Any: The value associated with the specified key
|
341
|
+
|
342
|
+
Raises:
|
343
|
+
ValueError: If the plan file is not found
|
344
|
+
IOError: If there's an error reading the plan file
|
345
|
+
"""
|
346
|
+
logger = get_logger(__name__)
|
347
|
+
ras_obj = ras_object or ras
|
348
|
+
ras_obj.check_initialized()
|
349
|
+
|
350
|
+
# These must exactly match the keys in supported_plan_keys from _parse_plan_file
|
351
|
+
valid_keys = {
|
352
|
+
'Computation Interval',
|
353
|
+
'DSS File',
|
354
|
+
'Flow File',
|
355
|
+
'Friction Slope Method',
|
356
|
+
'Geom File',
|
357
|
+
'Mapping Interval',
|
358
|
+
'Plan Title',
|
359
|
+
'Program Version',
|
360
|
+
'Run HTab',
|
361
|
+
'Run PostProcess',
|
362
|
+
'Run Sediment',
|
363
|
+
'Run UNet',
|
364
|
+
'Run WQNet',
|
365
|
+
'Short Identifier',
|
366
|
+
'Simulation Date',
|
367
|
+
'UNET D1 Cores',
|
368
|
+
'UNET D2 Cores',
|
369
|
+
'PS Cores',
|
370
|
+
'UNET Use Existing IB Tables',
|
371
|
+
'UNET 1D Methodology',
|
372
|
+
'UNET D2 SolverType',
|
373
|
+
'UNET D2 Name',
|
374
|
+
'description' # Special case for description block
|
375
|
+
}
|
376
|
+
|
377
|
+
if key not in valid_keys:
|
378
|
+
logger.warning(f"Unknown key: {key}. Valid keys are: {', '.join(sorted(valid_keys))}")
|
379
|
+
return None
|
380
|
+
|
381
|
+
plan_file_path = Path(plan_number_or_path)
|
382
|
+
if not plan_file_path.is_file():
|
383
|
+
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
|
384
|
+
if not plan_file_path.exists():
|
385
|
+
logger.error(f"Plan file not found: {plan_file_path}")
|
386
|
+
raise ValueError(f"Plan file not found: {plan_file_path}")
|
387
|
+
|
388
|
+
try:
|
389
|
+
with open(plan_file_path, 'r') as file:
|
390
|
+
content = file.read()
|
391
|
+
except IOError as e:
|
392
|
+
logger.error(f"Error reading plan file {plan_file_path}: {e}")
|
393
|
+
raise
|
394
|
+
|
395
|
+
if key == 'description':
|
396
|
+
match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
397
|
+
return match.group(1).strip() if match else None
|
398
|
+
else:
|
399
|
+
pattern = f"{key}=(.*)"
|
400
|
+
match = re.search(pattern, content)
|
401
|
+
if match:
|
402
|
+
value = match.group(1).strip()
|
403
|
+
# Convert core values to integers
|
404
|
+
if key in ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']:
|
405
|
+
try:
|
406
|
+
return int(value)
|
407
|
+
except ValueError:
|
408
|
+
logger.warning(f"Could not convert {key} value '{value}' to integer")
|
409
|
+
return None
|
410
|
+
return value
|
411
|
+
|
412
|
+
# Use DEBUG level for missing core values, ERROR for other missing keys
|
413
|
+
if key in ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']:
|
414
|
+
logger.debug(f"Core setting '{key}' not found in plan file")
|
415
|
+
else:
|
416
|
+
logger.error(f"Key '{key}' not found in the plan file")
|
417
|
+
return None
|
418
|
+
|
419
|
+
def _parse_plan_file(self, plan_file_path):
|
420
|
+
"""
|
421
|
+
Parse a plan file and extract critical information.
|
422
|
+
|
423
|
+
Args:
|
424
|
+
plan_file_path (Path): Path to the plan file.
|
425
|
+
|
426
|
+
Returns:
|
427
|
+
dict: Dictionary containing extracted plan information.
|
428
|
+
"""
|
429
|
+
plan_info = {}
|
430
|
+
content, encoding = read_file_with_fallback_encoding(plan_file_path)
|
431
|
+
|
432
|
+
if content is None:
|
433
|
+
logger.error(f"Could not read plan file {plan_file_path} with any supported encoding")
|
434
|
+
return plan_info
|
435
|
+
|
436
|
+
try:
|
437
|
+
# Extract description
|
438
|
+
description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
439
|
+
if description_match:
|
440
|
+
plan_info['description'] = description_match.group(1).strip()
|
441
|
+
|
442
|
+
# BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the plan file keys.
|
443
|
+
|
444
|
+
# Extract other critical information
|
445
|
+
supported_plan_keys = {
|
446
|
+
'Computation Interval': r'Computation Interval=(.+)',
|
447
|
+
'DSS File': r'DSS File=(.+)',
|
448
|
+
'Flow File': r'Flow File=(.+)',
|
449
|
+
'Friction Slope Method': r'Friction Slope Method=(.+)',
|
450
|
+
'Geom File': r'Geom File=(.+)',
|
451
|
+
'Mapping Interval': r'Mapping Interval=(.+)',
|
452
|
+
'Plan Title': r'Plan Title=(.+)',
|
453
|
+
'Program Version': r'Program Version=(.+)',
|
454
|
+
'Run HTab': r'Run HTab=(.+)',
|
455
|
+
'Run PostProcess': r'Run PostProcess=(.+)',
|
456
|
+
'Run Sediment': r'Run Sediment=(.+)',
|
457
|
+
'Run UNet': r'Run UNet=(.+)',
|
458
|
+
'Run WQNet': r'Run WQNet=(.+)',
|
459
|
+
'Short Identifier': r'Short Identifier=(.+)',
|
460
|
+
'Simulation Date': r'Simulation Date=(.+)',
|
461
|
+
'UNET D1 Cores': r'UNET D1 Cores=(.+)',
|
462
|
+
'UNET D2 Cores': r'UNET D2 Cores=(.+)',
|
463
|
+
'PS Cores': r'PS Cores=(.+)',
|
464
|
+
'UNET Use Existing IB Tables': r'UNET Use Existing IB Tables=(.+)',
|
465
|
+
'UNET 1D Methodology': r'UNET 1D Methodology=(.+)',
|
466
|
+
'UNET D2 SolverType': r'UNET D2 SolverType=(.+)',
|
467
|
+
'UNET D2 Name': r'UNET D2 Name=(.+)'
|
468
|
+
}
|
469
|
+
|
470
|
+
# END Exception to Style Guide
|
471
|
+
|
472
|
+
# First, explicitly set None for core values
|
473
|
+
core_keys = ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']
|
474
|
+
for key in core_keys:
|
475
|
+
plan_info[key] = None
|
476
|
+
|
477
|
+
for key, pattern in supported_plan_keys.items():
|
478
|
+
match = re.search(pattern, content)
|
479
|
+
if match:
|
480
|
+
value = match.group(1).strip()
|
481
|
+
# Convert core values to integers if they exist
|
482
|
+
if key in core_keys and value:
|
483
|
+
try:
|
484
|
+
value = int(value)
|
485
|
+
except ValueError:
|
486
|
+
logger.warning(f"Could not convert {key} value '{value}' to integer in plan file {plan_file_path}")
|
487
|
+
value = None
|
488
|
+
plan_info[key] = value
|
489
|
+
elif key in core_keys:
|
490
|
+
logger.debug(f"Core setting '{key}' not found in plan file {plan_file_path}")
|
491
|
+
|
492
|
+
logger.debug(f"Parsed plan file: {plan_file_path} using {encoding} encoding")
|
493
|
+
except Exception as e:
|
494
|
+
logger.error(f"Error parsing plan file {plan_file_path}: {e}")
|
495
|
+
|
496
|
+
return plan_info
|
497
|
+
|
498
|
+
@log_call
|
499
|
+
def _get_prj_entries(self, entry_type):
|
500
|
+
"""
|
501
|
+
Extract entries of a specific type from the HEC-RAS project file.
|
502
|
+
|
503
|
+
Args:
|
504
|
+
entry_type (str): The type of entry to extract (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
|
505
|
+
|
506
|
+
Returns:
|
507
|
+
pd.DataFrame: A DataFrame containing the extracted entries.
|
508
|
+
|
509
|
+
Raises:
|
510
|
+
Exception: If there's an error reading or processing the project file.
|
511
|
+
"""
|
512
|
+
entries = []
|
513
|
+
pattern = re.compile(rf"{entry_type} File=(\w+)")
|
514
|
+
|
515
|
+
try:
|
516
|
+
with open(self.prj_file, 'r', encoding='utf-8') as file:
|
517
|
+
for line in file:
|
518
|
+
match = pattern.match(line.strip())
|
519
|
+
if match:
|
520
|
+
file_name = match.group(1)
|
521
|
+
full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
|
522
|
+
entry_number = file_name[1:]
|
523
|
+
|
524
|
+
entry = {
|
525
|
+
f'{entry_type.lower()}_number': entry_number,
|
526
|
+
'full_path': full_path
|
527
|
+
}
|
528
|
+
|
529
|
+
# Handle Unsteady entries
|
530
|
+
if entry_type == 'Unsteady':
|
531
|
+
entry.update(self._process_unsteady_entry(entry_number, full_path))
|
532
|
+
else:
|
533
|
+
entry.update(self._process_default_entry())
|
534
|
+
|
535
|
+
# Handle Plan entries
|
536
|
+
if entry_type == 'Plan':
|
537
|
+
entry.update(self._process_plan_entry(entry_number, full_path))
|
538
|
+
|
539
|
+
entries.append(entry)
|
540
|
+
|
541
|
+
df = pd.DataFrame(entries)
|
542
|
+
return self._format_dataframe(df, entry_type)
|
543
|
+
|
544
|
+
except Exception as e:
|
545
|
+
logger.error(f"Error in _get_prj_entries for {entry_type}: {e}")
|
546
|
+
raise
|
547
|
+
|
548
|
+
def _process_unsteady_entry(self, entry_number: str, full_path: str) -> dict:
|
549
|
+
"""Process unsteady entry data."""
|
550
|
+
entry = {'unsteady_number': entry_number}
|
551
|
+
unsteady_info = self._parse_unsteady_file(Path(full_path))
|
552
|
+
entry.update(unsteady_info)
|
553
|
+
return entry
|
554
|
+
|
555
|
+
def _process_default_entry(self) -> dict:
|
556
|
+
"""Process default entry data."""
|
557
|
+
return {
|
558
|
+
'unsteady_number': None,
|
559
|
+
'geometry_number': None
|
560
|
+
}
|
561
|
+
|
562
|
+
def _process_plan_entry(self, entry_number: str, full_path: str) -> dict:
|
563
|
+
"""Process plan entry data."""
|
564
|
+
entry = {}
|
565
|
+
plan_info = self._parse_plan_file(Path(full_path))
|
566
|
+
|
567
|
+
if plan_info:
|
568
|
+
entry.update(self._process_flow_file(plan_info))
|
569
|
+
entry.update(self._process_geom_file(plan_info))
|
570
|
+
|
571
|
+
# Add remaining plan info
|
572
|
+
for key, value in plan_info.items():
|
573
|
+
if key not in ['Flow File', 'Geom File']:
|
574
|
+
entry[key] = value
|
575
|
+
|
576
|
+
# Add HDF results path
|
577
|
+
hdf_results_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
|
578
|
+
entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
|
579
|
+
|
580
|
+
return entry
|
581
|
+
|
582
|
+
def _process_flow_file(self, plan_info: dict) -> dict:
|
583
|
+
"""Process flow file information from plan info."""
|
584
|
+
flow_file = plan_info.get('Flow File')
|
585
|
+
if flow_file and flow_file.startswith('u'):
|
586
|
+
return {
|
587
|
+
'unsteady_number': flow_file[1:],
|
588
|
+
'Flow File': flow_file[1:]
|
589
|
+
}
|
590
|
+
return {
|
591
|
+
'unsteady_number': None,
|
592
|
+
'Flow File': flow_file[1:] if flow_file and flow_file.startswith('f') else None
|
593
|
+
}
|
594
|
+
|
595
|
+
def _process_geom_file(self, plan_info: dict) -> dict:
|
596
|
+
"""Process geometry file information from plan info."""
|
597
|
+
geom_file = plan_info.get('Geom File')
|
598
|
+
if geom_file and geom_file.startswith('g'):
|
599
|
+
return {
|
600
|
+
'geometry_number': geom_file[1:],
|
601
|
+
'Geom File': geom_file[1:]
|
602
|
+
}
|
603
|
+
return {
|
604
|
+
'geometry_number': None,
|
605
|
+
'Geom File': None
|
606
|
+
}
|
607
|
+
|
608
|
+
def _parse_unsteady_file(self, unsteady_file_path):
|
609
|
+
"""
|
610
|
+
Parse an unsteady flow file and extract critical information.
|
611
|
+
|
612
|
+
Args:
|
613
|
+
unsteady_file_path (Path): Path to the unsteady flow file.
|
614
|
+
|
615
|
+
Returns:
|
616
|
+
dict: Dictionary containing extracted unsteady flow information.
|
617
|
+
"""
|
618
|
+
unsteady_info = {}
|
619
|
+
content, encoding = read_file_with_fallback_encoding(unsteady_file_path)
|
620
|
+
|
621
|
+
if content is None:
|
622
|
+
return unsteady_info
|
623
|
+
|
624
|
+
try:
|
625
|
+
# BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the unsteady file keys.
|
626
|
+
|
627
|
+
supported_unsteady_keys = {
|
628
|
+
'Flow Title': r'Flow Title=(.+)',
|
629
|
+
'Program Version': r'Program Version=(.+)',
|
630
|
+
'Use Restart': r'Use Restart=(.+)',
|
631
|
+
'Precipitation Mode': r'Precipitation Mode=(.+)',
|
632
|
+
'Wind Mode': r'Wind Mode=(.+)',
|
633
|
+
'Met BC=Precipitation|Mode': r'Met BC=Precipitation\|Mode=(.+)',
|
634
|
+
'Met BC=Evapotranspiration|Mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
|
635
|
+
'Met BC=Precipitation|Expanded View': r'Met BC=Precipitation\|Expanded View=(.+)',
|
636
|
+
'Met BC=Precipitation|Constant Units': r'Met BC=Precipitation\|Constant Units=(.+)',
|
637
|
+
'Met BC=Precipitation|Gridded Source': r'Met BC=Precipitation\|Gridded Source=(.+)'
|
638
|
+
}
|
639
|
+
|
640
|
+
# END Exception to Style Guide
|
641
|
+
|
642
|
+
for key, pattern in supported_unsteady_keys.items():
|
643
|
+
match = re.search(pattern, content)
|
644
|
+
if match:
|
645
|
+
unsteady_info[key] = match.group(1).strip()
|
646
|
+
|
647
|
+
except Exception as e:
|
648
|
+
logger.error(f"Error parsing unsteady file {unsteady_file_path}: {e}")
|
649
|
+
|
650
|
+
return unsteady_info
|
651
|
+
|
652
|
+
@property
|
653
|
+
def is_initialized(self):
|
654
|
+
"""
|
655
|
+
Check if the RasPrj instance has been initialized.
|
656
|
+
|
657
|
+
Returns:
|
658
|
+
bool: True if the instance has been initialized, False otherwise.
|
659
|
+
"""
|
660
|
+
return self.initialized
|
661
|
+
|
662
|
+
@log_call
|
663
|
+
def check_initialized(self):
|
664
|
+
"""
|
665
|
+
Ensure that the RasPrj instance has been initialized before operations.
|
666
|
+
|
667
|
+
Raises:
|
668
|
+
RuntimeError: If the project has not been initialized with init_ras_project().
|
669
|
+
|
670
|
+
Note:
|
671
|
+
This method is called by other methods to validate the project state before
|
672
|
+
performing operations. Users typically don't need to call this directly.
|
673
|
+
"""
|
674
|
+
if not self.initialized:
|
675
|
+
raise RuntimeError("Project not initialized. Call init_ras_project() first.")
|
676
|
+
|
677
|
+
@staticmethod
|
678
|
+
@log_call
|
679
|
+
def find_ras_prj(folder_path):
|
680
|
+
"""
|
681
|
+
Find the appropriate HEC-RAS project file (.prj) in the given folder.
|
682
|
+
|
683
|
+
This method uses several strategies to locate the correct project file:
|
684
|
+
1. If only one .prj file exists, it is selected
|
685
|
+
2. If multiple .prj files exist, it tries to match with .rasmap file names
|
686
|
+
3. As a last resort, it scans files for "Proj Title=" content
|
687
|
+
|
688
|
+
Args:
|
689
|
+
folder_path (str or Path): Path to the folder containing HEC-RAS files.
|
690
|
+
|
691
|
+
Returns:
|
692
|
+
Path: The full path of the selected .prj file or None if no suitable file is found.
|
693
|
+
|
694
|
+
Example:
|
695
|
+
>>> project_file = RasPrj.find_ras_prj("/path/to/ras_project")
|
696
|
+
>>> if project_file:
|
697
|
+
... print(f"Found project file: {project_file}")
|
698
|
+
... else:
|
699
|
+
... print("No project file found")
|
700
|
+
"""
|
701
|
+
folder_path = Path(folder_path)
|
702
|
+
prj_files = list(folder_path.glob("*.prj"))
|
703
|
+
rasmap_files = list(folder_path.glob("*.rasmap"))
|
704
|
+
if len(prj_files) == 1:
|
705
|
+
return prj_files[0].resolve()
|
706
|
+
if len(prj_files) > 1:
|
707
|
+
if len(rasmap_files) == 1:
|
708
|
+
base_filename = rasmap_files[0].stem
|
709
|
+
prj_file = folder_path / f"{base_filename}.prj"
|
710
|
+
if prj_file.exists():
|
711
|
+
return prj_file.resolve()
|
712
|
+
for prj_file in prj_files:
|
713
|
+
try:
|
714
|
+
with open(prj_file, 'r') as file:
|
715
|
+
content = file.read()
|
716
|
+
if "Proj Title=" in content:
|
717
|
+
return prj_file.resolve()
|
718
|
+
except Exception:
|
719
|
+
continue
|
720
|
+
return None
|
721
|
+
|
722
|
+
|
723
|
+
@log_call
|
724
|
+
def get_project_name(self):
|
725
|
+
"""
|
726
|
+
Get the name of the HEC-RAS project (without file extension).
|
727
|
+
|
728
|
+
Returns:
|
729
|
+
str: The name of the project.
|
730
|
+
|
731
|
+
Raises:
|
732
|
+
RuntimeError: If the project has not been initialized.
|
733
|
+
|
734
|
+
Example:
|
735
|
+
>>> project_name = ras.get_project_name()
|
736
|
+
>>> print(f"Working with project: {project_name}")
|
737
|
+
"""
|
738
|
+
self.check_initialized()
|
739
|
+
return self.project_name
|
740
|
+
|
741
|
+
@log_call
|
742
|
+
def get_prj_entries(self, entry_type):
|
743
|
+
"""
|
744
|
+
Get entries of a specific type from the HEC-RAS project.
|
745
|
+
|
746
|
+
This method extracts files of the specified type from the project file,
|
747
|
+
parses their content, and returns a structured DataFrame.
|
748
|
+
|
749
|
+
Args:
|
750
|
+
entry_type (str): The type of entry to retrieve ('Plan', 'Flow', 'Unsteady', or 'Geom').
|
751
|
+
|
752
|
+
Returns:
|
753
|
+
pd.DataFrame: A DataFrame containing the requested entries with appropriate columns.
|
754
|
+
|
755
|
+
Raises:
|
756
|
+
RuntimeError: If the project has not been initialized.
|
757
|
+
|
758
|
+
Example:
|
759
|
+
>>> # Get all geometry files in the project
|
760
|
+
>>> geom_entries = ras.get_prj_entries('Geom')
|
761
|
+
>>> print(f"Project contains {len(geom_entries)} geometry files")
|
762
|
+
|
763
|
+
Note:
|
764
|
+
This is a generic method. For specific file types, use the dedicated methods:
|
765
|
+
get_plan_entries(), get_flow_entries(), get_unsteady_entries(), get_geom_entries()
|
766
|
+
"""
|
767
|
+
self.check_initialized()
|
768
|
+
return self._get_prj_entries(entry_type)
|
769
|
+
|
770
|
+
@log_call
|
771
|
+
def get_plan_entries(self):
|
772
|
+
"""
|
773
|
+
Get all plan entries from the HEC-RAS project.
|
774
|
+
|
775
|
+
Returns a DataFrame containing all plan files (.p*) in the project
|
776
|
+
with their associated properties, paths and settings.
|
777
|
+
|
778
|
+
Returns:
|
779
|
+
pd.DataFrame: A DataFrame with columns including 'plan_number', 'full_path',
|
780
|
+
'unsteady_number', 'geometry_number', etc.
|
781
|
+
|
782
|
+
Raises:
|
783
|
+
RuntimeError: If the project has not been initialized.
|
784
|
+
|
785
|
+
Example:
|
786
|
+
>>> plan_entries = ras.get_plan_entries()
|
787
|
+
>>> print(f"Project contains {len(plan_entries)} plan files")
|
788
|
+
>>> # Display the first plan's properties
|
789
|
+
>>> if not plan_entries.empty:
|
790
|
+
... print(plan_entries.iloc[0])
|
791
|
+
"""
|
792
|
+
self.check_initialized()
|
793
|
+
return self._get_prj_entries('Plan')
|
794
|
+
|
795
|
+
@log_call
|
796
|
+
def get_flow_entries(self):
|
797
|
+
"""
|
798
|
+
Get all flow entries from the HEC-RAS project.
|
799
|
+
|
800
|
+
Returns a DataFrame containing all flow files (.f*) in the project
|
801
|
+
with their associated properties and paths.
|
802
|
+
|
803
|
+
Returns:
|
804
|
+
pd.DataFrame: A DataFrame with columns including 'flow_number', 'full_path', etc.
|
805
|
+
|
806
|
+
Raises:
|
807
|
+
RuntimeError: If the project has not been initialized.
|
808
|
+
|
809
|
+
Example:
|
810
|
+
>>> flow_entries = ras.get_flow_entries()
|
811
|
+
>>> print(f"Project contains {len(flow_entries)} flow files")
|
812
|
+
>>> # Display the first flow file's properties
|
813
|
+
>>> if not flow_entries.empty:
|
814
|
+
... print(flow_entries.iloc[0])
|
815
|
+
"""
|
816
|
+
self.check_initialized()
|
817
|
+
return self._get_prj_entries('Flow')
|
818
|
+
|
819
|
+
@log_call
|
820
|
+
def get_unsteady_entries(self):
|
821
|
+
"""
|
822
|
+
Get all unsteady flow entries from the HEC-RAS project.
|
823
|
+
|
824
|
+
Returns a DataFrame containing all unsteady flow files (.u*) in the project
|
825
|
+
with their associated properties and paths.
|
826
|
+
|
827
|
+
Returns:
|
828
|
+
pd.DataFrame: A DataFrame with columns including 'unsteady_number', 'full_path', etc.
|
829
|
+
|
830
|
+
Raises:
|
831
|
+
RuntimeError: If the project has not been initialized.
|
832
|
+
|
833
|
+
Example:
|
834
|
+
>>> unsteady_entries = ras.get_unsteady_entries()
|
835
|
+
>>> print(f"Project contains {len(unsteady_entries)} unsteady flow files")
|
836
|
+
>>> # Display the first unsteady file's properties
|
837
|
+
>>> if not unsteady_entries.empty:
|
838
|
+
... print(unsteady_entries.iloc[0])
|
839
|
+
"""
|
840
|
+
self.check_initialized()
|
841
|
+
return self._get_prj_entries('Unsteady')
|
842
|
+
|
843
|
+
@log_call
|
844
|
+
def get_geom_entries(self):
|
845
|
+
"""
|
846
|
+
Get all geometry entries from the HEC-RAS project.
|
847
|
+
|
848
|
+
Returns a DataFrame containing all geometry files (.g*) in the project
|
849
|
+
with their associated properties, paths and HDF links.
|
850
|
+
|
851
|
+
Returns:
|
852
|
+
pd.DataFrame: A DataFrame with columns including 'geom_number', 'full_path',
|
853
|
+
'hdf_path', etc.
|
854
|
+
|
855
|
+
Raises:
|
856
|
+
RuntimeError: If the project has not been initialized.
|
857
|
+
|
858
|
+
Example:
|
859
|
+
>>> geom_entries = ras.get_geom_entries()
|
860
|
+
>>> print(f"Project contains {len(geom_entries)} geometry files")
|
861
|
+
>>> # Display the first geometry file's properties
|
862
|
+
>>> if not geom_entries.empty:
|
863
|
+
... print(geom_entries.iloc[0])
|
864
|
+
"""
|
865
|
+
self.check_initialized()
|
866
|
+
geom_pattern = re.compile(r'Geom File=(\w+)')
|
867
|
+
geom_entries = []
|
868
|
+
|
869
|
+
try:
|
870
|
+
with open(self.prj_file, 'r') as f:
|
871
|
+
for line in f:
|
872
|
+
match = geom_pattern.search(line)
|
873
|
+
if match:
|
874
|
+
geom_entries.append(match.group(1))
|
875
|
+
|
876
|
+
geom_df = pd.DataFrame({'geom_file': geom_entries})
|
877
|
+
geom_df['geom_number'] = geom_df['geom_file'].str.extract(r'(\d+)$')
|
878
|
+
geom_df['full_path'] = geom_df['geom_file'].apply(lambda x: str(self.project_folder / f"{self.project_name}.{x}"))
|
879
|
+
geom_df['hdf_path'] = geom_df['full_path'] + ".hdf"
|
880
|
+
|
881
|
+
if not self.suppress_logging: # Only log if suppress_logging is False
|
882
|
+
logger.info(f"Found {len(geom_df)} geometry entries")
|
883
|
+
return geom_df
|
884
|
+
except Exception as e:
|
885
|
+
logger.error(f"Error reading geometry entries from project file: {e}")
|
886
|
+
raise
|
887
|
+
|
888
|
+
@log_call
|
889
|
+
def get_hdf_entries(self):
|
890
|
+
"""
|
891
|
+
Get all plan entries that have associated HDF results files.
|
892
|
+
|
893
|
+
This method identifies which plans have been successfully computed
|
894
|
+
and have HDF results available for further analysis.
|
895
|
+
|
896
|
+
Returns:
|
897
|
+
pd.DataFrame: A DataFrame containing plan entries with HDF results.
|
898
|
+
Returns an empty DataFrame if no results are found.
|
899
|
+
|
900
|
+
Raises:
|
901
|
+
RuntimeError: If the project has not been initialized.
|
902
|
+
|
903
|
+
Example:
|
904
|
+
>>> hdf_entries = ras.get_hdf_entries()
|
905
|
+
>>> if hdf_entries.empty:
|
906
|
+
... print("No computed results found. Run simulations first.")
|
907
|
+
... else:
|
908
|
+
... print(f"Found results for {len(hdf_entries)} plans")
|
909
|
+
|
910
|
+
Note:
|
911
|
+
This is useful for identifying which plans have been successfully computed
|
912
|
+
and can be used for further results analysis.
|
913
|
+
"""
|
914
|
+
self.check_initialized()
|
915
|
+
|
916
|
+
hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
|
917
|
+
|
918
|
+
if hdf_entries.empty:
|
919
|
+
return pd.DataFrame(columns=self.plan_df.columns)
|
920
|
+
|
921
|
+
return hdf_entries
|
922
|
+
|
923
|
+
|
924
|
+
@log_call
|
925
|
+
def print_data(self):
|
926
|
+
"""
|
927
|
+
Print a comprehensive summary of all RAS Object data for this instance.
|
928
|
+
|
929
|
+
This method outputs:
|
930
|
+
- Project information (name, folder, file paths)
|
931
|
+
- Summary of plans, flows, geometries, and unsteady files
|
932
|
+
- HDF results availability
|
933
|
+
- Boundary conditions
|
934
|
+
|
935
|
+
Useful for debugging, validation, and exploring project structure.
|
936
|
+
|
937
|
+
Raises:
|
938
|
+
RuntimeError: If the project has not been initialized.
|
939
|
+
|
940
|
+
Example:
|
941
|
+
>>> ras.print_data() # Displays complete project overview
|
942
|
+
"""
|
943
|
+
self.check_initialized()
|
944
|
+
logger.info(f"--- Data for {self.project_name} ---")
|
945
|
+
logger.info(f"Project folder: {self.project_folder}")
|
946
|
+
logger.info(f"PRJ file: {self.prj_file}")
|
947
|
+
logger.info(f"HEC-RAS executable: {self.ras_exe_path}")
|
948
|
+
logger.info("Plan files:")
|
949
|
+
logger.info(f"\n{self.plan_df}")
|
950
|
+
logger.info("Flow files:")
|
951
|
+
logger.info(f"\n{self.flow_df}")
|
952
|
+
logger.info("Unsteady flow files:")
|
953
|
+
logger.info(f"\n{self.unsteady_df}")
|
954
|
+
logger.info("Geometry files:")
|
955
|
+
logger.info(f"\n{self.geom_df}")
|
956
|
+
logger.info("HDF entries:")
|
957
|
+
logger.info(f"\n{self.get_hdf_entries()}")
|
958
|
+
logger.info("Boundary conditions:")
|
959
|
+
logger.info(f"\n{self.boundaries_df}")
|
960
|
+
logger.info("----------------------------")
|
961
|
+
|
962
|
+
@log_call
|
963
|
+
def get_boundary_conditions(self) -> pd.DataFrame:
|
964
|
+
"""
|
965
|
+
Extract boundary conditions from unsteady flow files into a structured DataFrame.
|
966
|
+
|
967
|
+
This method:
|
968
|
+
1. Parses all unsteady flow files to extract boundary condition information
|
969
|
+
2. Creates a structured DataFrame with boundary locations, types and parameters
|
970
|
+
3. Links boundary conditions to their respective unsteady flow files
|
971
|
+
|
972
|
+
Supported boundary condition types include:
|
973
|
+
- Flow Hydrograph
|
974
|
+
- Stage Hydrograph
|
975
|
+
- Normal Depth
|
976
|
+
- Lateral Inflow Hydrograph
|
977
|
+
- Uniform Lateral Inflow Hydrograph
|
978
|
+
- Gate Opening
|
979
|
+
|
980
|
+
Returns:
|
981
|
+
pd.DataFrame: A DataFrame containing detailed boundary condition information.
|
982
|
+
Returns an empty DataFrame if no unsteady flow files are present.
|
983
|
+
|
984
|
+
Example:
|
985
|
+
>>> boundaries = ras.get_boundary_conditions()
|
986
|
+
>>> if not boundaries.empty:
|
987
|
+
... print(f"Found {len(boundaries)} boundary conditions")
|
988
|
+
... # Show flow hydrographs only
|
989
|
+
... flow_hydrographs = boundaries[boundaries['bc_type'] == 'Flow Hydrograph']
|
990
|
+
... print(f"Project has {len(flow_hydrographs)} flow hydrographs")
|
991
|
+
|
992
|
+
Note:
|
993
|
+
To see unparsed boundary condition lines for debugging, set logging to DEBUG:
|
994
|
+
import logging
|
995
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
996
|
+
"""
|
997
|
+
boundary_data = []
|
998
|
+
|
999
|
+
# Check if unsteady_df is empty
|
1000
|
+
if self.unsteady_df.empty:
|
1001
|
+
logger.info("No unsteady flow files found in the project.")
|
1002
|
+
return pd.DataFrame() # Return an empty DataFrame
|
1003
|
+
|
1004
|
+
for _, row in self.unsteady_df.iterrows():
|
1005
|
+
unsteady_file_path = row['full_path']
|
1006
|
+
unsteady_number = row['unsteady_number']
|
1007
|
+
|
1008
|
+
try:
|
1009
|
+
with open(unsteady_file_path, 'r') as file:
|
1010
|
+
content = file.read()
|
1011
|
+
except IOError as e:
|
1012
|
+
logger.error(f"Error reading unsteady file {unsteady_file_path}: {e}")
|
1013
|
+
continue
|
1014
|
+
|
1015
|
+
bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
|
1016
|
+
|
1017
|
+
for i, block in enumerate(bc_blocks, 1):
|
1018
|
+
bc_info, unparsed_lines = self._parse_boundary_condition(block, unsteady_number, i)
|
1019
|
+
boundary_data.append(bc_info)
|
1020
|
+
|
1021
|
+
if unparsed_lines:
|
1022
|
+
logger.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
1023
|
+
|
1024
|
+
if not boundary_data:
|
1025
|
+
logger.info("No boundary conditions found in unsteady flow files.")
|
1026
|
+
return pd.DataFrame() # Return an empty DataFrame if no boundary conditions were found
|
1027
|
+
|
1028
|
+
boundaries_df = pd.DataFrame(boundary_data)
|
1029
|
+
|
1030
|
+
# Merge with unsteady_df to get relevant unsteady flow file information
|
1031
|
+
merged_df = pd.merge(boundaries_df, self.unsteady_df,
|
1032
|
+
left_on='unsteady_number', right_on='unsteady_number', how='left')
|
1033
|
+
|
1034
|
+
return merged_df
|
1035
|
+
|
1036
|
+
def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
|
1037
|
+
lines = block.split('\n')
|
1038
|
+
bc_info = {
|
1039
|
+
'unsteady_number': unsteady_number,
|
1040
|
+
'boundary_condition_number': bc_number
|
1041
|
+
}
|
1042
|
+
|
1043
|
+
parsed_lines = set()
|
1044
|
+
|
1045
|
+
# Parse Boundary Location
|
1046
|
+
boundary_location = lines[0].split('=')[1].strip()
|
1047
|
+
fields = [field.strip() for field in boundary_location.split(',')]
|
1048
|
+
bc_info.update({
|
1049
|
+
'river_reach_name': fields[0] if len(fields) > 0 else '',
|
1050
|
+
'river_station': fields[1] if len(fields) > 1 else '',
|
1051
|
+
'storage_area_name': fields[2] if len(fields) > 2 else '',
|
1052
|
+
'pump_station_name': fields[3] if len(fields) > 3 else ''
|
1053
|
+
})
|
1054
|
+
parsed_lines.add(0)
|
1055
|
+
|
1056
|
+
# Determine BC Type
|
1057
|
+
bc_types = {
|
1058
|
+
'Flow Hydrograph=': 'Flow Hydrograph',
|
1059
|
+
'Lateral Inflow Hydrograph=': 'Lateral Inflow Hydrograph',
|
1060
|
+
'Uniform Lateral Inflow Hydrograph=': 'Uniform Lateral Inflow Hydrograph',
|
1061
|
+
'Stage Hydrograph=': 'Stage Hydrograph',
|
1062
|
+
'Friction Slope=': 'Normal Depth',
|
1063
|
+
'Gate Name=': 'Gate Opening'
|
1064
|
+
}
|
1065
|
+
|
1066
|
+
bc_info['bc_type'] = 'Unknown'
|
1067
|
+
bc_info['hydrograph_type'] = None
|
1068
|
+
for i, line in enumerate(lines[1:], 1):
|
1069
|
+
for key, bc_type in bc_types.items():
|
1070
|
+
if line.startswith(key):
|
1071
|
+
bc_info['bc_type'] = bc_type
|
1072
|
+
if 'Hydrograph' in bc_type:
|
1073
|
+
bc_info['hydrograph_type'] = bc_type
|
1074
|
+
parsed_lines.add(i)
|
1075
|
+
break
|
1076
|
+
if bc_info['bc_type'] != 'Unknown':
|
1077
|
+
break
|
1078
|
+
|
1079
|
+
# Parse other fields
|
1080
|
+
known_fields = ['Interval', 'DSS Path', 'Use DSS', 'Use Fixed Start Time', 'Fixed Start Date/Time',
|
1081
|
+
'Is Critical Boundary', 'Critical Boundary Flow', 'DSS File']
|
1082
|
+
for i, line in enumerate(lines):
|
1083
|
+
if '=' in line:
|
1084
|
+
key, value = line.split('=', 1)
|
1085
|
+
key = key.strip()
|
1086
|
+
if key in known_fields:
|
1087
|
+
bc_info[key] = value.strip()
|
1088
|
+
parsed_lines.add(i)
|
1089
|
+
|
1090
|
+
# Handle hydrograph values
|
1091
|
+
bc_info['hydrograph_num_values'] = 0
|
1092
|
+
if bc_info['hydrograph_type']:
|
1093
|
+
hydrograph_key = f"{bc_info['hydrograph_type']}="
|
1094
|
+
hydrograph_line = next((line for i, line in enumerate(lines) if line.startswith(hydrograph_key)), None)
|
1095
|
+
if hydrograph_line:
|
1096
|
+
hydrograph_index = lines.index(hydrograph_line)
|
1097
|
+
values_count = int(hydrograph_line.split('=')[1].strip())
|
1098
|
+
bc_info['hydrograph_num_values'] = values_count
|
1099
|
+
if values_count > 0:
|
1100
|
+
values = ' '.join(lines[hydrograph_index + 1:]).split()[:values_count]
|
1101
|
+
bc_info['hydrograph_values'] = values
|
1102
|
+
parsed_lines.update(range(hydrograph_index, hydrograph_index + (values_count // 5) + 2))
|
1103
|
+
|
1104
|
+
# Collect unparsed lines
|
1105
|
+
unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
|
1106
|
+
|
1107
|
+
if unparsed_lines:
|
1108
|
+
logger.debug(f"Unparsed lines for boundary condition {bc_number} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
1109
|
+
|
1110
|
+
return bc_info, unparsed_lines
|
1111
|
+
|
1112
|
+
@log_call
|
1113
|
+
def _format_dataframe(self, df, entry_type):
|
1114
|
+
"""
|
1115
|
+
Format the DataFrame according to the desired column structure.
|
1116
|
+
|
1117
|
+
Args:
|
1118
|
+
df (pd.DataFrame): The DataFrame to format.
|
1119
|
+
entry_type (str): The type of entry (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
|
1120
|
+
|
1121
|
+
Returns:
|
1122
|
+
pd.DataFrame: The formatted DataFrame.
|
1123
|
+
"""
|
1124
|
+
if df.empty:
|
1125
|
+
return df
|
1126
|
+
|
1127
|
+
if entry_type == 'Plan':
|
1128
|
+
# Set required column order
|
1129
|
+
first_cols = ['plan_number', 'unsteady_number', 'geometry_number']
|
1130
|
+
|
1131
|
+
# Standard plan key columns in the exact order specified
|
1132
|
+
plan_key_cols = [
|
1133
|
+
'Plan Title', 'Program Version', 'Short Identifier', 'Simulation Date',
|
1134
|
+
'Std Step Tol', 'Computation Interval', 'Output Interval', 'Instantaneous Interval',
|
1135
|
+
'Mapping Interval', 'Run HTab', 'Run UNet', 'Run Sediment', 'Run PostProcess',
|
1136
|
+
'Run WQNet', 'Run RASMapper', 'UNET Use Existing IB Tables', 'HDF_Results_Path',
|
1137
|
+
'UNET 1D Methodology', 'Write IC File', 'Write IC File at Fixed DateTime',
|
1138
|
+
'IC Time', 'Write IC File Reoccurance', 'Write IC File at Sim End'
|
1139
|
+
]
|
1140
|
+
|
1141
|
+
# Additional convenience columns
|
1142
|
+
file_path_cols = ['Geom File', 'Geom Path', 'Flow File', 'Flow Path']
|
1143
|
+
|
1144
|
+
# Special columns that must be preserved
|
1145
|
+
special_cols = ['HDF_Results_Path']
|
1146
|
+
|
1147
|
+
# Build the final column list
|
1148
|
+
all_cols = first_cols.copy()
|
1149
|
+
|
1150
|
+
# Add plan key columns if they exist
|
1151
|
+
for col in plan_key_cols:
|
1152
|
+
if col in df.columns and col not in all_cols and col not in special_cols:
|
1153
|
+
all_cols.append(col)
|
1154
|
+
|
1155
|
+
# Add any remaining columns not explicitly specified
|
1156
|
+
other_cols = [col for col in df.columns if col not in all_cols + file_path_cols + special_cols + ['full_path']]
|
1157
|
+
all_cols.extend(other_cols)
|
1158
|
+
|
1159
|
+
# Add HDF_Results_Path if it exists (ensure it comes before file paths)
|
1160
|
+
for special_col in special_cols:
|
1161
|
+
if special_col in df.columns and special_col not in all_cols:
|
1162
|
+
all_cols.append(special_col)
|
1163
|
+
|
1164
|
+
# Add file path columns at the end
|
1165
|
+
all_cols.extend(file_path_cols)
|
1166
|
+
|
1167
|
+
# Rename plan_number column
|
1168
|
+
df = df.rename(columns={f'{entry_type.lower()}_number': 'plan_number'})
|
1169
|
+
|
1170
|
+
# Fill in missing columns with None
|
1171
|
+
for col in all_cols:
|
1172
|
+
if col not in df.columns:
|
1173
|
+
df[col] = None
|
1174
|
+
|
1175
|
+
# Make sure full_path column is preserved and included
|
1176
|
+
if 'full_path' in df.columns and 'full_path' not in all_cols:
|
1177
|
+
all_cols.append('full_path')
|
1178
|
+
|
1179
|
+
# Return DataFrame with specified column order
|
1180
|
+
cols_to_return = [col for col in all_cols if col in df.columns]
|
1181
|
+
return df[cols_to_return]
|
1182
|
+
|
1183
|
+
return df
|
1184
|
+
|
1185
|
+
@log_call
|
1186
|
+
def _get_prj_entries(self, entry_type):
|
1187
|
+
"""
|
1188
|
+
Extract entries of a specific type from the HEC-RAS project file.
|
1189
|
+
"""
|
1190
|
+
entries = []
|
1191
|
+
pattern = re.compile(rf"{entry_type} File=(\w+)")
|
1192
|
+
|
1193
|
+
try:
|
1194
|
+
with open(self.prj_file, 'r') as file:
|
1195
|
+
for line in file:
|
1196
|
+
match = pattern.match(line.strip())
|
1197
|
+
if match:
|
1198
|
+
file_name = match.group(1)
|
1199
|
+
full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
|
1200
|
+
entry = self._create_entry(entry_type, file_name, full_path)
|
1201
|
+
entries.append(entry)
|
1202
|
+
|
1203
|
+
return self._format_dataframe(pd.DataFrame(entries), entry_type)
|
1204
|
+
|
1205
|
+
except Exception as e:
|
1206
|
+
logger.error(f"Error in _get_prj_entries for {entry_type}: {e}")
|
1207
|
+
raise
|
1208
|
+
|
1209
|
+
def _create_entry(self, entry_type, file_name, full_path):
|
1210
|
+
"""Helper method to create entry dictionary."""
|
1211
|
+
entry_number = file_name[1:]
|
1212
|
+
entry = {
|
1213
|
+
f'{entry_type.lower()}_number': entry_number,
|
1214
|
+
'full_path': full_path,
|
1215
|
+
'unsteady_number': None,
|
1216
|
+
'geometry_number': None
|
1217
|
+
}
|
1218
|
+
|
1219
|
+
if entry_type == 'Unsteady':
|
1220
|
+
entry['unsteady_number'] = entry_number
|
1221
|
+
entry.update(self._parse_unsteady_file(Path(full_path)))
|
1222
|
+
elif entry_type == 'Plan':
|
1223
|
+
self._update_plan_entry(entry, entry_number, full_path)
|
1224
|
+
|
1225
|
+
return entry
|
1226
|
+
|
1227
|
+
def _update_plan_entry(self, entry, entry_number, full_path):
|
1228
|
+
"""Helper method to update plan entry with additional information."""
|
1229
|
+
plan_info = self._parse_plan_file(Path(full_path))
|
1230
|
+
if plan_info:
|
1231
|
+
# Handle Flow File
|
1232
|
+
flow_file = plan_info.get('Flow File')
|
1233
|
+
if flow_file:
|
1234
|
+
if flow_file.startswith('u'):
|
1235
|
+
entry.update({'unsteady_number': flow_file[1:], 'Flow File': flow_file[1:]})
|
1236
|
+
else:
|
1237
|
+
entry['Flow File'] = flow_file[1:] if flow_file.startswith('f') else None
|
1238
|
+
|
1239
|
+
# Handle Geom File
|
1240
|
+
geom_file = plan_info.get('Geom File')
|
1241
|
+
if geom_file and geom_file.startswith('g'):
|
1242
|
+
entry.update({'geometry_number': geom_file[1:], 'Geom File': geom_file[1:]})
|
1243
|
+
|
1244
|
+
# Add remaining plan info
|
1245
|
+
entry.update({k: v for k, v in plan_info.items() if k not in ['Flow File', 'Geom File']})
|
1246
|
+
|
1247
|
+
# Add HDF results path
|
1248
|
+
hdf_path = self.project_folder / f"{self.project_name}.p{entry_number}.hdf"
|
1249
|
+
entry['HDF_Results_Path'] = str(hdf_path) if hdf_path.exists() else None
|
1250
|
+
|
1251
|
+
|
1252
|
+
# Create a global instance named 'ras'
|
1253
|
+
# Defining the global instance allows the init_ras_project function to initialize the project.
|
1254
|
+
# This only happens on the library initialization, not when the user calls init_ras_project.
|
1255
|
+
ras = RasPrj()
|
1256
|
+
|
1257
|
+
# END OF CLASS DEFINITION
|
1258
|
+
|
1259
|
+
|
1260
|
+
# START OF FUNCTION DEFINITIONS
|
1261
|
+
|
1262
|
+
@log_call
|
1263
|
+
def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
|
1264
|
+
"""
|
1265
|
+
Initialize a RAS project for use with the ras-commander library.
|
1266
|
+
|
1267
|
+
This is the primary function for setting up a HEC-RAS project. It:
|
1268
|
+
1. Finds the project file (.prj) in the specified folder
|
1269
|
+
2. Identifies the appropriate HEC-RAS executable
|
1270
|
+
3. Loads project data (plans, geometries, flows)
|
1271
|
+
4. Creates dataframes containing project components
|
1272
|
+
|
1273
|
+
Args:
|
1274
|
+
ras_project_folder (str or Path): The path to the RAS project folder.
|
1275
|
+
ras_version (str, optional): The version of RAS to use (e.g., "6.6") OR
|
1276
|
+
a full path to the Ras.exe file (e.g., "D:/Programs/HEC/HEC-RAS/6.6/Ras.exe").
|
1277
|
+
If None, will attempt to detect from plan files.
|
1278
|
+
ras_object (RasPrj, optional): If None, updates the global 'ras' object.
|
1279
|
+
If a RasPrj instance, updates that instance.
|
1280
|
+
If any other value, creates and returns a new RasPrj instance.
|
1281
|
+
|
1282
|
+
Returns:
|
1283
|
+
RasPrj: An initialized RasPrj instance.
|
1284
|
+
|
1285
|
+
Raises:
|
1286
|
+
FileNotFoundError: If the specified project folder doesn't exist.
|
1287
|
+
ValueError: If no HEC-RAS project file is found in the folder.
|
1288
|
+
|
1289
|
+
Example:
|
1290
|
+
>>> # Initialize using the global 'ras' object (most common)
|
1291
|
+
>>> init_ras_project("/path/to/project", "6.6")
|
1292
|
+
>>> print(f"Initialized project: {ras.project_name}")
|
1293
|
+
>>>
|
1294
|
+
>>> # Create a new RasPrj instance
|
1295
|
+
>>> my_project = init_ras_project("/path/to/project", "6.6", "new")
|
1296
|
+
>>> print(f"Created project instance: {my_project.project_name}")
|
1297
|
+
"""
|
1298
|
+
project_folder = Path(ras_project_folder)
|
1299
|
+
if not project_folder.exists():
|
1300
|
+
logger.error(f"The specified RAS project folder does not exist: {project_folder}")
|
1301
|
+
raise FileNotFoundError(f"The specified RAS project folder does not exist: {project_folder}. Please check the path and try again.")
|
1302
|
+
|
1303
|
+
# Determine which RasPrj instance to use
|
1304
|
+
if ras_object is None:
|
1305
|
+
# Use the global 'ras' object
|
1306
|
+
logger.debug("Initializing global 'ras' object via init_ras_project function.")
|
1307
|
+
ras_object = ras
|
1308
|
+
elif not isinstance(ras_object, RasPrj):
|
1309
|
+
# Create a new RasPrj instance
|
1310
|
+
logger.debug("Creating a new RasPrj instance.")
|
1311
|
+
ras_object = RasPrj()
|
1312
|
+
|
1313
|
+
ras_exe_path = None
|
1314
|
+
|
1315
|
+
# Use version specified by user if provided
|
1316
|
+
if ras_version is not None:
|
1317
|
+
ras_exe_path = get_ras_exe(ras_version)
|
1318
|
+
if ras_exe_path == "Ras.exe" and ras_version != "Ras.exe":
|
1319
|
+
logger.warning(f"HEC-RAS Version {ras_version} was not found. Running HEC-RAS will fail.")
|
1320
|
+
else:
|
1321
|
+
# No version specified, try to detect from plan files
|
1322
|
+
detected_version = None
|
1323
|
+
logger.info("No HEC-RAS Version Specified.Attempting to detect HEC-RAS version from plan files.")
|
1324
|
+
|
1325
|
+
# Look for .pXX files in project folder
|
1326
|
+
logger.info(f"Searching for plan files in {project_folder}")
|
1327
|
+
# Search for any file with .p01 through .p99 extension, regardless of base name
|
1328
|
+
plan_files = list(project_folder.glob("*.p[0-9][0-9]"))
|
1329
|
+
|
1330
|
+
if not plan_files:
|
1331
|
+
logger.info(f"No plan files found in {project_folder}")
|
1332
|
+
|
1333
|
+
for plan_file in plan_files:
|
1334
|
+
logger.info(f"Found plan file: {plan_file.name}")
|
1335
|
+
content, encoding = read_file_with_fallback_encoding(plan_file)
|
1336
|
+
|
1337
|
+
if not content:
|
1338
|
+
logger.info(f"Could not read content from {plan_file.name}")
|
1339
|
+
continue
|
1340
|
+
|
1341
|
+
logger.info(f"Successfully read plan file with {encoding} encoding")
|
1342
|
+
|
1343
|
+
# Look for Program Version in plan file
|
1344
|
+
for line in content.splitlines():
|
1345
|
+
if line.startswith("Program Version="):
|
1346
|
+
version = line.split("=")[1].strip()
|
1347
|
+
logger.info(f"Found Program Version={version} in {plan_file.name}")
|
1348
|
+
|
1349
|
+
# Replace 00 in version string if present
|
1350
|
+
if "00" in version:
|
1351
|
+
version = version.replace("00", "0")
|
1352
|
+
|
1353
|
+
# Try to get RAS executable for this version
|
1354
|
+
test_exe_path = get_ras_exe(version)
|
1355
|
+
logger.info(f"Checking RAS executable path: {test_exe_path}")
|
1356
|
+
|
1357
|
+
if test_exe_path != "Ras.exe":
|
1358
|
+
detected_version = version
|
1359
|
+
ras_exe_path = test_exe_path
|
1360
|
+
logger.debug(f"Found valid HEC-RAS version {version} in plan file {plan_file.name}")
|
1361
|
+
break
|
1362
|
+
else:
|
1363
|
+
logger.info(f"Version {version} not found in default installation path")
|
1364
|
+
|
1365
|
+
if detected_version:
|
1366
|
+
break
|
1367
|
+
|
1368
|
+
if not detected_version:
|
1369
|
+
logger.error("No valid HEC-RAS version found in any plan files.")
|
1370
|
+
ras_exe_path = "Ras.exe"
|
1371
|
+
logger.warning("No valid HEC-RAS version was detected. Running HEC-RAS will fail.")
|
1372
|
+
|
1373
|
+
# Initialize or re-initialize with the determined executable path
|
1374
|
+
ras_object.initialize(project_folder, ras_exe_path)
|
1375
|
+
|
1376
|
+
# Always update the global ras object as well
|
1377
|
+
if ras_object is not ras:
|
1378
|
+
ras.initialize(project_folder, ras_exe_path)
|
1379
|
+
logger.debug("Global 'ras' object also updated to match the new project.")
|
1380
|
+
|
1381
|
+
logger.debug(f"Project initialized. Project folder: {ras_object.project_folder}")
|
1382
|
+
logger.debug(f"Using HEC-RAS executable: {ras_exe_path}")
|
1383
|
+
return ras_object
|
1384
|
+
|
1385
|
+
@log_call
|
1386
|
+
def get_ras_exe(ras_version=None):
|
1387
|
+
"""
|
1388
|
+
Determine the HEC-RAS executable path based on the input.
|
1389
|
+
|
1390
|
+
This function attempts to find the HEC-RAS executable in the following order:
|
1391
|
+
1. If ras_version is a valid file path to an .exe file, use that path directly
|
1392
|
+
(useful for non-standard installations or non-C: drive installations)
|
1393
|
+
2. If ras_version is a known version number, use default installation path (on C: drive)
|
1394
|
+
3. If global 'ras' object has ras_exe_path, use that
|
1395
|
+
4. As a fallback, return "Ras.exe" but log an error
|
1396
|
+
|
1397
|
+
Args:
|
1398
|
+
ras_version (str, optional): Either a version number (e.g., "6.6") or
|
1399
|
+
a full path to the HEC-RAS executable
|
1400
|
+
(e.g., "D:/Programs/HEC/HEC-RAS/6.6/Ras.exe").
|
1401
|
+
|
1402
|
+
Returns:
|
1403
|
+
str: The full path to the HEC-RAS executable or "Ras.exe" if not found.
|
1404
|
+
|
1405
|
+
Note:
|
1406
|
+
- HEC-RAS version numbers include: "6.6", "6.5", "6.4.1", "6.3", etc.
|
1407
|
+
- The default installation path follows: C:/Program Files (x86)/HEC/HEC-RAS/{version}/Ras.exe
|
1408
|
+
- For non-standard installations, provide the full path to Ras.exe
|
1409
|
+
- Returns "Ras.exe" if no valid path is found, with error logged
|
1410
|
+
- Allows the library to function even without HEC-RAS installed
|
1411
|
+
"""
|
1412
|
+
if ras_version is None:
|
1413
|
+
if hasattr(ras, 'ras_exe_path') and ras.ras_exe_path:
|
1414
|
+
logger.debug(f"Using HEC-RAS executable from global 'ras' object: {ras.ras_exe_path}")
|
1415
|
+
return ras.ras_exe_path
|
1416
|
+
else:
|
1417
|
+
default_path = "Ras.exe"
|
1418
|
+
logger.debug(f"No HEC-RAS version specified and global 'ras' object not initialized or missing ras_exe_path.")
|
1419
|
+
logger.warning(f"HEC-RAS is not installed or version not specified. Running HEC-RAS will fail unless a valid installed version is specified.")
|
1420
|
+
return default_path
|
1421
|
+
|
1422
|
+
ras_version_numbers = [
|
1423
|
+
"6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
|
1424
|
+
"5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
|
1425
|
+
"4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
|
1426
|
+
]
|
1427
|
+
|
1428
|
+
# Check if input is a direct path to an executable
|
1429
|
+
hecras_path = Path(ras_version)
|
1430
|
+
if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
|
1431
|
+
logger.debug(f"HEC-RAS executable found at specified path: {hecras_path}")
|
1432
|
+
return str(hecras_path)
|
1433
|
+
|
1434
|
+
# Check known version numbers
|
1435
|
+
if str(ras_version) in ras_version_numbers:
|
1436
|
+
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
|
1437
|
+
if default_path.is_file():
|
1438
|
+
logger.debug(f"HEC-RAS executable found at default path: {default_path}")
|
1439
|
+
return str(default_path)
|
1440
|
+
else:
|
1441
|
+
error_msg = f"HEC-RAS Version {ras_version} is not found at expected path. Running HEC-RAS will fail unless a valid installed version is specified."
|
1442
|
+
logger.error(error_msg)
|
1443
|
+
return "Ras.exe"
|
1444
|
+
|
1445
|
+
# Try to handle other version formats (e.g., just the number without dots)
|
1446
|
+
try:
|
1447
|
+
# First check if it's a direct version number
|
1448
|
+
version_str = str(ras_version)
|
1449
|
+
|
1450
|
+
# Check for paths like "C:/Path/To/Ras.exe"
|
1451
|
+
if os.path.sep in version_str and version_str.lower().endswith('.exe'):
|
1452
|
+
exe_path = Path(version_str)
|
1453
|
+
if exe_path.is_file():
|
1454
|
+
logger.debug(f"HEC-RAS executable found at specified path: {exe_path}")
|
1455
|
+
return str(exe_path)
|
1456
|
+
|
1457
|
+
# Try to find a matching version from our list
|
1458
|
+
for known_version in ras_version_numbers:
|
1459
|
+
if version_str in known_version or known_version.replace('.', '') == version_str:
|
1460
|
+
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{known_version}/Ras.exe")
|
1461
|
+
if default_path.is_file():
|
1462
|
+
logger.debug(f"HEC-RAS executable found at default path: {default_path}")
|
1463
|
+
return str(default_path)
|
1464
|
+
|
1465
|
+
# Check if it's a newer version
|
1466
|
+
if '.' in version_str:
|
1467
|
+
major_version = int(version_str.split('.')[0])
|
1468
|
+
if major_version >= 6:
|
1469
|
+
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{version_str}/Ras.exe")
|
1470
|
+
if default_path.is_file():
|
1471
|
+
logger.debug(f"HEC-RAS executable found at path for newer version: {default_path}")
|
1472
|
+
return str(default_path)
|
1473
|
+
except Exception as e:
|
1474
|
+
logger.error(f"Error parsing version or finding path: {e}")
|
1475
|
+
|
1476
|
+
error_msg = f"HEC-RAS Version {ras_version} is not recognized or installed. Running HEC-RAS will fail unless a valid installed version is specified."
|
1477
|
+
logger.error(error_msg)
|
1478
|
+
return "Ras.exe"
|