ras-commander 0.33.0__py3-none-any.whl → 0.34.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/RasCmdr.py +163 -131
- ras_commander/RasExamples.py +96 -46
- ras_commander/RasGeo.py +27 -6
- ras_commander/RasHdf.py +248 -0
- ras_commander/RasPlan.py +391 -437
- ras_commander/RasPrj.py +396 -64
- ras_commander/RasUnsteady.py +24 -4
- ras_commander/RasUtils.py +352 -51
- ras_commander/__init__.py +4 -1
- ras_commander-0.34.0.dist-info/METADATA +263 -0
- ras_commander-0.34.0.dist-info/RECORD +15 -0
- ras_commander-0.33.0.dist-info/METADATA +0 -5
- ras_commander-0.33.0.dist-info/RECORD +0 -14
- {ras_commander-0.33.0.dist-info → ras_commander-0.34.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.33.0.dist-info → ras_commander-0.34.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.33.0.dist-info → ras_commander-0.34.0.dist-info}/top_level.txt +0 -0
ras_commander/RasPrj.py
CHANGED
@@ -15,23 +15,37 @@ By default, the RasPrj class is initialized with the global 'ras' object.
|
|
15
15
|
However, you can create multiple RasPrj instances to manage multiple projects.
|
16
16
|
Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
|
17
17
|
"""
|
18
|
-
# Example Terminal Output for RasPrj Functions:
|
19
|
-
# print(f"\n----- INSERT TEXT HERE -----\n")
|
20
18
|
|
19
|
+
# Example Terminal Output for RasPrj Functions:
|
20
|
+
# logging.info("----- INSERT TEXT HERE -----")
|
21
|
+
import re
|
21
22
|
from pathlib import Path
|
22
23
|
import pandas as pd
|
23
|
-
import
|
24
|
+
import logging
|
25
|
+
from typing import Union, Any, List, Dict, Tuple
|
26
|
+
|
27
|
+
|
28
|
+
# Configure logging
|
29
|
+
logging.basicConfig(
|
30
|
+
level=logging.INFO,
|
31
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
32
|
+
handlers=[
|
33
|
+
logging.StreamHandler()
|
34
|
+
]
|
35
|
+
)
|
24
36
|
|
25
37
|
class RasPrj:
|
26
38
|
def __init__(self):
|
27
39
|
self.initialized = False
|
40
|
+
self.boundaries_df = None # New attribute to store boundary conditions
|
28
41
|
|
29
42
|
def initialize(self, project_folder, ras_exe_path):
|
30
43
|
"""
|
31
44
|
Initialize a RasPrj instance.
|
32
45
|
|
33
46
|
This method sets up the RasPrj instance with the given project folder and RAS executable path.
|
34
|
-
It finds the project file, loads project data,
|
47
|
+
It finds the project file, loads project data, sets the initialization flag, and now also
|
48
|
+
extracts boundary conditions.
|
35
49
|
|
36
50
|
Args:
|
37
51
|
project_folder (str or Path): Path to the HEC-RAS project folder.
|
@@ -46,13 +60,17 @@ class RasPrj:
|
|
46
60
|
self.project_folder = Path(project_folder)
|
47
61
|
self.prj_file = self.find_ras_prj(self.project_folder)
|
48
62
|
if self.prj_file is None:
|
63
|
+
logging.error(f"No HEC-RAS project file found in {self.project_folder}")
|
49
64
|
raise ValueError(f"No HEC-RAS project file found in {self.project_folder}")
|
50
65
|
self.project_name = Path(self.prj_file).stem
|
51
66
|
self.ras_exe_path = ras_exe_path
|
52
67
|
self._load_project_data()
|
68
|
+
self.boundaries_df = self.get_boundary_conditions() # Extract boundary conditions
|
53
69
|
self.initialized = True
|
54
|
-
|
55
|
-
|
70
|
+
logging.info(f"Initialization complete for project: {self.project_name}")
|
71
|
+
logging.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
|
72
|
+
f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
|
73
|
+
f"Boundary conditions: {len(self.boundaries_df)}")
|
56
74
|
|
57
75
|
def _load_project_data(self):
|
58
76
|
"""
|
@@ -67,6 +85,56 @@ class RasPrj:
|
|
67
85
|
self.unsteady_df = self._get_prj_entries('Unsteady')
|
68
86
|
self.geom_df = self._get_prj_entries('Geom')
|
69
87
|
|
88
|
+
def _parse_plan_file(self, plan_file_path):
|
89
|
+
"""
|
90
|
+
Parse a plan file and extract critical information.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
plan_file_path (Path): Path to the plan file.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
dict: Dictionary containing extracted plan information.
|
97
|
+
"""
|
98
|
+
plan_info = {}
|
99
|
+
with open(plan_file_path, 'r') as file:
|
100
|
+
content = file.read()
|
101
|
+
|
102
|
+
# Extract description
|
103
|
+
description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
104
|
+
if description_match:
|
105
|
+
plan_info['description'] = description_match.group(1).strip()
|
106
|
+
|
107
|
+
# Extract other critical information
|
108
|
+
patterns = {
|
109
|
+
'computation_interval': r'Computation Interval=(.+)',
|
110
|
+
'dss_file': r'DSS File=(.+)',
|
111
|
+
'flow_file': r'Flow File=(.+)',
|
112
|
+
'friction_slope_method': r'Friction Slope Method=(.+)',
|
113
|
+
'geom_file': r'Geom File=(.+)',
|
114
|
+
'mapping_interval': r'Mapping Interval=(.+)',
|
115
|
+
'plan_title': r'Plan Title=(.+)',
|
116
|
+
'program_version': r'Program Version=(.+)',
|
117
|
+
'run_htab': r'Run HTab=(.+)',
|
118
|
+
'run_post_process': r'Run PostProcess=(.+)',
|
119
|
+
'run_sediment': r'Run Sediment=(.+)',
|
120
|
+
'run_unet': r'Run UNet=(.+)',
|
121
|
+
'run_wqnet': r'Run WQNet=(.+)',
|
122
|
+
'short_identifier': r'Short Identifier=(.+)',
|
123
|
+
'simulation_date': r'Simulation Date=(.+)',
|
124
|
+
'unet_d1_cores': r'UNET D1 Cores=(.+)',
|
125
|
+
'unet_use_existing_ib_tables': r'UNET Use Existing IB Tables=(.+)',
|
126
|
+
'unet_1d_methodology': r'UNET 1D Methodology=(.+)',
|
127
|
+
'unet_d2_solver_type': r'UNET D2 SolverType=(.+)',
|
128
|
+
'unet_d2_name': r'UNET D2 Name=(.+)'
|
129
|
+
}
|
130
|
+
|
131
|
+
for key, pattern in patterns.items():
|
132
|
+
match = re.search(pattern, content)
|
133
|
+
if match:
|
134
|
+
plan_info[key] = match.group(1).strip()
|
135
|
+
|
136
|
+
return plan_info
|
137
|
+
|
70
138
|
def _get_prj_entries(self, entry_type):
|
71
139
|
"""
|
72
140
|
Extract entries of a specific type from the HEC-RAS project file.
|
@@ -79,40 +147,77 @@ class RasPrj:
|
|
79
147
|
|
80
148
|
Note:
|
81
149
|
This method reads the project file and extracts entries matching the specified type.
|
82
|
-
For '
|
150
|
+
For 'Unsteady' entries, it parses additional information from the unsteady file.
|
83
151
|
"""
|
84
|
-
# Initialize an empty list to store entries
|
85
152
|
entries = []
|
86
|
-
# Create a regex pattern to match the specific entry type
|
87
153
|
pattern = re.compile(rf"{entry_type} File=(\w+)")
|
88
154
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
155
|
+
try:
|
156
|
+
with open(self.prj_file, 'r') as file:
|
157
|
+
for line in file:
|
158
|
+
match = pattern.match(line.strip())
|
159
|
+
if match:
|
160
|
+
file_name = match.group(1)
|
161
|
+
full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
|
162
|
+
entry = {
|
163
|
+
f'{entry_type.lower()}_number': file_name[1:],
|
164
|
+
'full_path': full_path
|
165
|
+
}
|
166
|
+
|
167
|
+
if entry_type == 'Plan':
|
168
|
+
plan_info = self._parse_plan_file(Path(full_path))
|
169
|
+
entry.update(plan_info)
|
170
|
+
|
171
|
+
# Add HDF results path if it exists
|
172
|
+
hdf_results_path = self.project_folder / f"{self.project_name}.p{file_name[1:]}.hdf"
|
173
|
+
entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
|
174
|
+
|
175
|
+
if entry_type == 'Unsteady':
|
176
|
+
unsteady_info = self._parse_unsteady_file(Path(full_path))
|
177
|
+
entry.update(unsteady_info)
|
178
|
+
|
179
|
+
entries.append(entry)
|
180
|
+
except Exception as e:
|
181
|
+
logging.exception(f"Failed to read project file {self.prj_file}: {e}")
|
182
|
+
raise
|
183
|
+
|
114
184
|
return pd.DataFrame(entries)
|
115
185
|
|
186
|
+
def _parse_unsteady_file(self, unsteady_file_path):
|
187
|
+
"""
|
188
|
+
Parse an unsteady flow file and extract critical information.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
unsteady_file_path (Path): Path to the unsteady flow file.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
dict: Dictionary containing extracted unsteady flow information.
|
195
|
+
"""
|
196
|
+
unsteady_info = {}
|
197
|
+
with open(unsteady_file_path, 'r') as file:
|
198
|
+
content = file.read()
|
199
|
+
|
200
|
+
# Extract critical information
|
201
|
+
patterns = {
|
202
|
+
'flow_title': r'Flow Title=(.+)',
|
203
|
+
'program_version': r'Program Version=(.+)',
|
204
|
+
'use_restart': r'Use Restart=(.+)',
|
205
|
+
'precipitation_mode': r'Precipitation Mode=(.+)',
|
206
|
+
'wind_mode': r'Wind Mode=(.+)',
|
207
|
+
'precipitation_bc_mode': r'Met BC=Precipitation\|Mode=(.+)',
|
208
|
+
'evapotranspiration_bc_mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
|
209
|
+
'precipitation_expanded_view': r'Met BC=Precipitation\|Expanded View=(.+)',
|
210
|
+
'precipitation_constant_units': r'Met BC=Precipitation\|Constant Units=(.+)',
|
211
|
+
'precipitation_gridded_source': r'Met BC=Precipitation\|Gridded Source=(.+)'
|
212
|
+
}
|
213
|
+
|
214
|
+
for key, pattern in patterns.items():
|
215
|
+
match = re.search(pattern, content)
|
216
|
+
if match:
|
217
|
+
unsteady_info[key] = match.group(1).strip()
|
218
|
+
|
219
|
+
return unsteady_info
|
220
|
+
|
116
221
|
@property
|
117
222
|
def is_initialized(self):
|
118
223
|
"""
|
@@ -131,6 +236,7 @@ class RasPrj:
|
|
131
236
|
RuntimeError: If the project has not been initialized.
|
132
237
|
"""
|
133
238
|
if not self.initialized:
|
239
|
+
logging.error("Project not initialized. Call init_ras_project() first.")
|
134
240
|
raise RuntimeError("Project not initialized. Call init_ras_project() first.")
|
135
241
|
|
136
242
|
@staticmethod
|
@@ -148,17 +254,26 @@ class RasPrj:
|
|
148
254
|
prj_files = list(folder_path.glob("*.prj"))
|
149
255
|
rasmap_files = list(folder_path.glob("*.rasmap"))
|
150
256
|
if len(prj_files) == 1:
|
257
|
+
logging.info(f"Single .prj file found: {prj_files[0]}")
|
151
258
|
return prj_files[0].resolve()
|
152
259
|
if len(prj_files) > 1:
|
153
260
|
if len(rasmap_files) == 1:
|
154
261
|
base_filename = rasmap_files[0].stem
|
155
262
|
prj_file = folder_path / f"{base_filename}.prj"
|
156
|
-
|
263
|
+
if prj_file.exists():
|
264
|
+
logging.info(f"Matched .prj file based on .rasmap: {prj_file}")
|
265
|
+
return prj_file.resolve()
|
157
266
|
for prj_file in prj_files:
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
267
|
+
try:
|
268
|
+
with open(prj_file, 'r') as file:
|
269
|
+
content = file.read()
|
270
|
+
if "Proj Title=" in content:
|
271
|
+
logging.info(f".prj file with 'Proj Title=' found: {prj_file}")
|
272
|
+
return prj_file.resolve()
|
273
|
+
except Exception as e:
|
274
|
+
logging.warning(f"Failed to read .prj file {prj_file}: {e}")
|
275
|
+
continue
|
276
|
+
logging.warning("No suitable .prj file found after all checks.")
|
162
277
|
return None
|
163
278
|
|
164
279
|
def get_project_name(self):
|
@@ -255,35 +370,238 @@ class RasPrj:
|
|
255
370
|
# Filter the plan_df to include only entries with existing HDF results
|
256
371
|
hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
|
257
372
|
|
258
|
-
# If no HDF entries are found,
|
373
|
+
# If no HDF entries are found, log the information
|
259
374
|
if hdf_entries.empty:
|
375
|
+
logging.info("No HDF entries found.")
|
260
376
|
return pd.DataFrame(columns=self.plan_df.columns)
|
261
377
|
|
378
|
+
logging.info(f"Found {len(hdf_entries)} HDF entries.")
|
262
379
|
return hdf_entries
|
263
380
|
|
264
381
|
def print_data(self):
|
265
|
-
"""Print all RAS Object data for this instance.
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
382
|
+
"""Print all RAS Object data for this instance."""
|
383
|
+
self.check_initialized()
|
384
|
+
logging.info(f"--- Data for {self.project_name} ---")
|
385
|
+
logging.info(f"Project folder: {self.project_folder}")
|
386
|
+
logging.info(f"PRJ file: {self.prj_file}")
|
387
|
+
logging.info(f"HEC-RAS executable: {self.ras_exe_path}")
|
388
|
+
logging.info("Plan files:")
|
389
|
+
logging.info(f"\n{self.plan_df}")
|
390
|
+
logging.info("Flow files:")
|
391
|
+
logging.info(f"\n{self.flow_df}")
|
392
|
+
logging.info("Unsteady flow files:")
|
393
|
+
logging.info(f"\n{self.unsteady_df}")
|
394
|
+
logging.info("Geometry files:")
|
395
|
+
logging.info(f"\n{self.geom_df}")
|
396
|
+
logging.info("HDF entries:")
|
397
|
+
logging.info(f"\n{self.get_hdf_entries()}")
|
398
|
+
logging.info("Boundary conditions:")
|
399
|
+
logging.info(f"\n{self.boundaries_df}")
|
400
|
+
logging.info("----------------------------")
|
401
|
+
|
402
|
+
|
403
|
+
@staticmethod
|
404
|
+
def get_plan_value(
|
405
|
+
plan_number_or_path: Union[str, Path],
|
406
|
+
key: str,
|
407
|
+
ras_object=None
|
408
|
+
) -> Any:
|
409
|
+
"""
|
410
|
+
Retrieve a specific value from a HEC-RAS plan file.
|
411
|
+
|
412
|
+
Parameters:
|
413
|
+
plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
|
414
|
+
key (str): The key to retrieve from the plan file
|
415
|
+
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
416
|
+
|
417
|
+
Returns:
|
418
|
+
Any: The value associated with the specified key
|
419
|
+
|
420
|
+
Raises:
|
421
|
+
ValueError: If an invalid key is provided or if the plan file is not found
|
422
|
+
IOError: If there's an error reading the plan file
|
423
|
+
|
424
|
+
Note: See the docstring of update_plan_file for a full list of available keys and their types.
|
425
|
+
|
426
|
+
Example:
|
427
|
+
>>> computation_interval = RasUtils.get_plan_value("01", "computation_interval")
|
428
|
+
>>> print(f"Computation interval: {computation_interval}")
|
429
|
+
"""
|
430
|
+
ras_obj = ras_object or ras
|
431
|
+
ras_obj.check_initialized()
|
432
|
+
|
433
|
+
valid_keys = {
|
434
|
+
'description', 'computation_interval', 'dss_file', 'flow_file', 'friction_slope_method',
|
435
|
+
'geom_file', 'mapping_interval', 'plan_file', 'plan_title', 'program_version',
|
436
|
+
'run_htab', 'run_post_process', 'run_sediment', 'run_unet', 'run_wqnet',
|
437
|
+
'short_identifier', 'simulation_date', 'unet_d1_cores', 'unet_use_existing_ib_tables',
|
438
|
+
'unet_1d_methodology', 'unet_d2_solver_type', 'unet_d2_name'
|
439
|
+
}
|
440
|
+
|
441
|
+
if key not in valid_keys:
|
442
|
+
raise ValueError(f"Invalid key: {key}. Valid keys are: {', '.join(valid_keys)}")
|
443
|
+
|
444
|
+
plan_file_path = Path(plan_number_or_path)
|
445
|
+
if not plan_file_path.is_file():
|
446
|
+
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
|
447
|
+
if not plan_file_path.exists():
|
448
|
+
raise ValueError(f"Plan file not found: {plan_file_path}")
|
449
|
+
|
450
|
+
try:
|
451
|
+
with open(plan_file_path, 'r') as file:
|
452
|
+
content = file.read()
|
453
|
+
except IOError as e:
|
454
|
+
logging.error(f"Error reading plan file {plan_file_path}: {e}")
|
455
|
+
raise
|
456
|
+
|
457
|
+
if key == 'description':
|
458
|
+
import re
|
459
|
+
match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
460
|
+
return match.group(1).strip() if match else None
|
461
|
+
else:
|
462
|
+
pattern = f"{key.replace('_', ' ').title()}=(.*)"
|
463
|
+
import re
|
464
|
+
match = re.search(pattern, content)
|
465
|
+
return match.group(1).strip() if match else None
|
466
|
+
|
467
|
+
def get_boundary_conditions(self) -> pd.DataFrame:
|
468
|
+
"""
|
469
|
+
Extract boundary conditions from unsteady flow files and create a DataFrame.
|
470
|
+
|
471
|
+
This method parses unsteady flow files to extract boundary condition information.
|
472
|
+
It creates a DataFrame with structured data for known boundary condition types
|
473
|
+
and parameters, and associates this information with the corresponding unsteady flow file.
|
474
|
+
|
475
|
+
Note:
|
476
|
+
Any lines in the boundary condition blocks that are not explicitly parsed and
|
477
|
+
incorporated into the DataFrame are captured in a multi-line string. This string
|
478
|
+
is logged at the DEBUG level for each boundary condition. This feature is crucial
|
479
|
+
for developers incorporating new boundary condition types or parameters, as it
|
480
|
+
allows them to see what information might be missing from the current parsing logic.
|
481
|
+
|
482
|
+
Returns:
|
483
|
+
pd.DataFrame: A DataFrame containing detailed boundary condition information,
|
484
|
+
linked to the unsteady flow files.
|
485
|
+
|
486
|
+
Usage:
|
487
|
+
To see the unparsed lines, set the logging level to DEBUG before calling this method:
|
488
|
+
|
489
|
+
import logging
|
490
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
491
|
+
|
492
|
+
boundaries_df = ras_project.get_boundary_conditions()
|
493
|
+
"""
|
494
|
+
boundary_data = []
|
495
|
+
|
496
|
+
for _, row in self.unsteady_df.iterrows():
|
497
|
+
unsteady_file_path = row['full_path']
|
498
|
+
unsteady_number = row['unsteady_number']
|
499
|
+
|
500
|
+
with open(unsteady_file_path, 'r') as file:
|
501
|
+
content = file.read()
|
502
|
+
|
503
|
+
bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
|
504
|
+
|
505
|
+
for i, block in enumerate(bc_blocks, 1):
|
506
|
+
bc_info, unparsed_lines = self._parse_boundary_condition(block, unsteady_number, i)
|
507
|
+
boundary_data.append(bc_info)
|
508
|
+
|
509
|
+
if unparsed_lines:
|
510
|
+
logging.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
511
|
+
|
512
|
+
boundaries_df = pd.DataFrame(boundary_data)
|
513
|
+
|
514
|
+
# Merge with unsteady_df to get relevant unsteady flow file information
|
515
|
+
merged_df = pd.merge(boundaries_df, self.unsteady_df,
|
516
|
+
left_on='unsteady_number', right_on='unsteady_number', how='left')
|
517
|
+
|
518
|
+
return merged_df
|
519
|
+
|
520
|
+
def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
|
521
|
+
lines = block.split('\n')
|
522
|
+
bc_info = {
|
523
|
+
'unsteady_number': unsteady_number,
|
524
|
+
'boundary_condition_number': bc_number
|
525
|
+
}
|
526
|
+
|
527
|
+
parsed_lines = set()
|
528
|
+
|
529
|
+
# Parse Boundary Location
|
530
|
+
boundary_location = lines[0].split('=')[1].strip()
|
531
|
+
fields = [field.strip() for field in boundary_location.split(',')]
|
532
|
+
bc_info.update({
|
533
|
+
'river_reach_name': fields[0] if len(fields) > 0 else '',
|
534
|
+
'river_station': fields[1] if len(fields) > 1 else '',
|
535
|
+
'storage_area_name': fields[2] if len(fields) > 2 else '',
|
536
|
+
'pump_station_name': fields[3] if len(fields) > 3 else ''
|
537
|
+
})
|
538
|
+
parsed_lines.add(0)
|
539
|
+
|
540
|
+
# Determine BC Type
|
541
|
+
bc_types = {
|
542
|
+
'Flow Hydrograph=': 'Flow Hydrograph',
|
543
|
+
'Lateral Inflow Hydrograph=': 'Lateral Inflow Hydrograph',
|
544
|
+
'Uniform Lateral Inflow Hydrograph=': 'Uniform Lateral Inflow Hydrograph',
|
545
|
+
'Stage Hydrograph=': 'Stage Hydrograph',
|
546
|
+
'Friction Slope=': 'Normal Depth',
|
547
|
+
'Gate Name=': 'Gate Opening'
|
548
|
+
}
|
549
|
+
|
550
|
+
bc_info['bc_type'] = 'Unknown'
|
551
|
+
bc_info['hydrograph_type'] = None
|
552
|
+
for i, line in enumerate(lines[1:], 1):
|
553
|
+
for key, bc_type in bc_types.items():
|
554
|
+
if line.startswith(key):
|
555
|
+
bc_info['bc_type'] = bc_type
|
556
|
+
if 'Hydrograph' in bc_type:
|
557
|
+
bc_info['hydrograph_type'] = bc_type
|
558
|
+
parsed_lines.add(i)
|
559
|
+
break
|
560
|
+
if bc_info['bc_type'] != 'Unknown':
|
561
|
+
break
|
562
|
+
|
563
|
+
# Parse other fields
|
564
|
+
known_fields = ['Interval', 'DSS Path', 'Use DSS', 'Use Fixed Start Time', 'Fixed Start Date/Time',
|
565
|
+
'Is Critical Boundary', 'Critical Boundary Flow', 'DSS File']
|
566
|
+
for i, line in enumerate(lines):
|
567
|
+
if '=' in line:
|
568
|
+
key, value = line.split('=', 1)
|
569
|
+
key = key.strip()
|
570
|
+
if key in known_fields:
|
571
|
+
bc_info[key] = value.strip()
|
572
|
+
parsed_lines.add(i)
|
573
|
+
|
574
|
+
# Handle hydrograph values
|
575
|
+
bc_info['hydrograph_num_values'] = 0
|
576
|
+
if bc_info['hydrograph_type']:
|
577
|
+
hydrograph_key = f"{bc_info['hydrograph_type']}="
|
578
|
+
hydrograph_line = next((line for i, line in enumerate(lines) if line.startswith(hydrograph_key)), None)
|
579
|
+
if hydrograph_line:
|
580
|
+
hydrograph_index = lines.index(hydrograph_line)
|
581
|
+
values_count = int(hydrograph_line.split('=')[1].strip())
|
582
|
+
bc_info['hydrograph_num_values'] = values_count
|
583
|
+
if values_count > 0:
|
584
|
+
values = ' '.join(lines[hydrograph_index + 1:]).split()[:values_count]
|
585
|
+
bc_info['hydrograph_values'] = values
|
586
|
+
parsed_lines.update(range(hydrograph_index, hydrograph_index + (values_count // 5) + 2))
|
587
|
+
|
588
|
+
# Collect unparsed lines
|
589
|
+
unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
|
590
|
+
|
591
|
+
return bc_info, unparsed_lines
|
282
592
|
|
283
593
|
|
284
594
|
# Create a global instance named 'ras'
|
285
595
|
ras = RasPrj()
|
286
596
|
|
597
|
+
# END OF CLASS DEFINITION
|
598
|
+
|
599
|
+
|
600
|
+
|
601
|
+
|
602
|
+
# START OF FUNCTION DEFINITIONS
|
603
|
+
|
604
|
+
|
287
605
|
def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
|
288
606
|
"""
|
289
607
|
Initialize a RAS project.
|
@@ -334,21 +652,21 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
|
|
334
652
|
"""
|
335
653
|
|
336
654
|
if not Path(ras_project_folder).exists():
|
655
|
+
logging.error(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
|
337
656
|
raise FileNotFoundError(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
|
338
657
|
|
339
658
|
ras_exe_path = get_ras_exe(ras_version)
|
340
659
|
|
341
660
|
if ras_instance is None:
|
342
|
-
|
661
|
+
logging.info("Initializing global 'ras' object via init_ras_project function.")
|
343
662
|
ras_instance = ras
|
344
663
|
elif not isinstance(ras_instance, RasPrj):
|
345
|
-
|
664
|
+
logging.error("Provided ras_instance is not an instance of RasPrj.")
|
346
665
|
raise TypeError("ras_instance must be an instance of RasPrj or None.")
|
347
666
|
|
348
667
|
# Initialize the RasPrj instance
|
349
668
|
ras_instance.initialize(ras_project_folder, ras_exe_path)
|
350
669
|
|
351
|
-
#print(f"\n-----HEC-RAS project initialized via init_ras_project function: {ras_instance.project_name}-----\n")
|
352
670
|
return ras_instance
|
353
671
|
|
354
672
|
|
@@ -375,13 +693,16 @@ def get_ras_exe(ras_version):
|
|
375
693
|
hecras_path = Path(ras_version)
|
376
694
|
|
377
695
|
if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
|
696
|
+
logging.info(f"HEC-RAS executable found at specified path: {hecras_path}")
|
378
697
|
return str(hecras_path)
|
379
698
|
|
380
699
|
if ras_version in ras_version_numbers:
|
381
700
|
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
|
382
701
|
if default_path.is_file():
|
702
|
+
logging.info(f"HEC-RAS executable found at default path: {default_path}")
|
383
703
|
return str(default_path)
|
384
704
|
else:
|
705
|
+
logging.error(f"HEC-RAS executable not found at the expected path: {default_path}")
|
385
706
|
raise FileNotFoundError(f"HEC-RAS executable not found at the expected path: {default_path}")
|
386
707
|
|
387
708
|
try:
|
@@ -389,12 +710,23 @@ def get_ras_exe(ras_version):
|
|
389
710
|
if version_float > max(float(v) for v in ras_version_numbers):
|
390
711
|
newer_version_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
|
391
712
|
if newer_version_path.is_file():
|
713
|
+
logging.info(f"Newer version of HEC-RAS executable found at: {newer_version_path}")
|
392
714
|
return str(newer_version_path)
|
393
715
|
else:
|
394
|
-
|
716
|
+
logging.error("Newer version of HEC-RAS was specified, but the executable was not found.")
|
717
|
+
raise FileNotFoundError(
|
718
|
+
f"Newer version of HEC-RAS was specified. Check the version number or pass the full Ras.exe path as the function argument instead of the version number. The script looked for the executable at: {newer_version_path}"
|
719
|
+
)
|
395
720
|
except ValueError:
|
396
721
|
pass
|
397
722
|
|
398
|
-
|
399
|
-
|
400
|
-
|
723
|
+
logging.error(
|
724
|
+
f"Invalid HEC-RAS version or path: {ras_version}. "
|
725
|
+
f"Please provide a valid version number from {ras_version_numbers} "
|
726
|
+
"or a full path to the HEC-RAS executable."
|
727
|
+
)
|
728
|
+
raise ValueError(
|
729
|
+
f"Invalid HEC-RAS version or path: {ras_version}. "
|
730
|
+
f"Please provide a valid version number from {ras_version_numbers} "
|
731
|
+
"or a full path to the HEC-RAS executable."
|
732
|
+
)
|
ras_commander/RasUnsteady.py
CHANGED
@@ -3,8 +3,20 @@ Operations for handling unsteady flow files in HEC-RAS projects.
|
|
3
3
|
"""
|
4
4
|
from pathlib import Path
|
5
5
|
from .RasPrj import ras
|
6
|
+
import logging
|
6
7
|
import re
|
7
8
|
|
9
|
+
# Configure logging at the module level
|
10
|
+
logging.basicConfig(
|
11
|
+
level=logging.INFO, # Set to DEBUG for more detailed output
|
12
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
13
|
+
handlers=[
|
14
|
+
logging.StreamHandler(), # Logs to console
|
15
|
+
# Uncomment the next line to enable logging to a file
|
16
|
+
# logging.FileHandler('ras_unsteady.log')
|
17
|
+
]
|
18
|
+
)
|
19
|
+
|
8
20
|
class RasUnsteady:
|
9
21
|
"""
|
10
22
|
Class for all operations related to HEC-RAS unsteady flow files.
|
@@ -46,28 +58,36 @@ class RasUnsteady:
|
|
46
58
|
try:
|
47
59
|
with open(unsteady_path, 'r') as f:
|
48
60
|
lines = f.readlines()
|
61
|
+
logging.debug(f"Successfully read unsteady flow file: {unsteady_path}")
|
49
62
|
except FileNotFoundError:
|
63
|
+
logging.error(f"Unsteady flow file not found: {unsteady_path}")
|
50
64
|
raise FileNotFoundError(f"Unsteady flow file not found: {unsteady_path}")
|
51
65
|
except PermissionError:
|
66
|
+
logging.error(f"Permission denied when reading unsteady flow file: {unsteady_path}")
|
52
67
|
raise PermissionError(f"Permission denied when reading unsteady flow file: {unsteady_path}")
|
53
68
|
|
54
69
|
updated = False
|
55
70
|
for i, line in enumerate(lines):
|
56
71
|
for param, new_value in modifications.items():
|
57
72
|
if line.startswith(f"{param}="):
|
73
|
+
old_value = line.strip().split('=')[1]
|
58
74
|
lines[i] = f"{param}={new_value}\n"
|
59
75
|
updated = True
|
60
|
-
|
76
|
+
logging.info(f"Updated {param} from {old_value} to {new_value}")
|
77
|
+
|
61
78
|
if updated:
|
62
79
|
try:
|
63
80
|
with open(unsteady_path, 'w') as f:
|
64
81
|
f.writelines(lines)
|
82
|
+
logging.debug(f"Successfully wrote modifications to unsteady flow file: {unsteady_path}")
|
65
83
|
except PermissionError:
|
84
|
+
logging.error(f"Permission denied when writing to unsteady flow file: {unsteady_path}")
|
66
85
|
raise PermissionError(f"Permission denied when writing to unsteady flow file: {unsteady_path}")
|
67
86
|
except IOError as e:
|
87
|
+
logging.error(f"Error writing to unsteady flow file: {unsteady_path}. {str(e)}")
|
68
88
|
raise IOError(f"Error writing to unsteady flow file: {unsteady_path}. {str(e)}")
|
69
|
-
|
89
|
+
logging.info(f"Applied modifications to {unsteady_file}")
|
70
90
|
else:
|
71
|
-
|
72
|
-
|
91
|
+
logging.warning(f"No matching parameters found in {unsteady_file}")
|
92
|
+
|
73
93
|
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|