ras-commander 0.35.0__py3-none-any.whl → 0.36.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 +360 -332
- ras_commander/RasExamples.py +113 -80
- ras_commander/RasGeo.py +38 -28
- ras_commander/RasGpt.py +142 -0
- ras_commander/RasHdf.py +170 -253
- ras_commander/RasPlan.py +115 -166
- ras_commander/RasPrj.py +212 -141
- ras_commander/RasUnsteady.py +37 -22
- ras_commander/RasUtils.py +98 -82
- ras_commander/__init__.py +11 -13
- ras_commander/logging_config.py +80 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/METADATA +15 -11
- ras_commander-0.36.0.dist-info/RECORD +17 -0
- ras_commander-0.35.0.dist-info/RECORD +0 -15
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/top_level.txt +0 -0
ras_commander/RasPrj.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
"""
|
1
|
+
"""
|
2
|
+
RasPrj.py - Manages HEC-RAS projects within the ras-commander library
|
2
3
|
|
3
4
|
This module provides a class for managing HEC-RAS projects.
|
4
5
|
|
@@ -14,31 +15,46 @@ This class is used to initialize a RAS project and is used in conjunction with t
|
|
14
15
|
By default, the RasPrj class is initialized with the global 'ras' object.
|
15
16
|
However, you can create multiple RasPrj instances to manage multiple projects.
|
16
17
|
Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
|
17
|
-
"""
|
18
18
|
|
19
|
-
|
20
|
-
|
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
|
+
import os
|
21
42
|
import re
|
22
43
|
from pathlib import Path
|
23
44
|
import pandas as pd
|
24
|
-
import logging
|
25
45
|
from typing import Union, Any, List, Dict, Tuple
|
46
|
+
import logging
|
47
|
+
from ras_commander.logging_config import get_logger, log_call
|
26
48
|
|
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
|
-
)
|
49
|
+
logger = get_logger(__name__)
|
36
50
|
|
37
51
|
class RasPrj:
|
52
|
+
|
38
53
|
def __init__(self):
|
39
54
|
self.initialized = False
|
40
55
|
self.boundaries_df = None # New attribute to store boundary conditions
|
41
56
|
|
57
|
+
@log_call
|
42
58
|
def initialize(self, project_folder, ras_exe_path):
|
43
59
|
"""
|
44
60
|
Initialize a RasPrj instance.
|
@@ -60,18 +76,19 @@ class RasPrj:
|
|
60
76
|
self.project_folder = Path(project_folder)
|
61
77
|
self.prj_file = self.find_ras_prj(self.project_folder)
|
62
78
|
if self.prj_file is None:
|
63
|
-
|
79
|
+
logger.error(f"No HEC-RAS project file found in {self.project_folder}")
|
64
80
|
raise ValueError(f"No HEC-RAS project file found in {self.project_folder}")
|
65
81
|
self.project_name = Path(self.prj_file).stem
|
66
82
|
self.ras_exe_path = ras_exe_path
|
67
83
|
self._load_project_data()
|
68
84
|
self.boundaries_df = self.get_boundary_conditions() # Extract boundary conditions
|
69
85
|
self.initialized = True
|
70
|
-
|
71
|
-
|
86
|
+
logger.info(f"Initialization complete for project: {self.project_name}")
|
87
|
+
logger.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
|
72
88
|
f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
|
73
89
|
f"Boundary conditions: {len(self.boundaries_df)}")
|
74
90
|
|
91
|
+
@log_call
|
75
92
|
def _load_project_data(self):
|
76
93
|
"""
|
77
94
|
Load project data from the HEC-RAS project file.
|
@@ -85,6 +102,7 @@ class RasPrj:
|
|
85
102
|
self.unsteady_df = self._get_prj_entries('Unsteady')
|
86
103
|
self.geom_df = self._get_prj_entries('Geom')
|
87
104
|
|
105
|
+
@log_call
|
88
106
|
def _parse_plan_file(self, plan_file_path):
|
89
107
|
"""
|
90
108
|
Parse a plan file and extract critical information.
|
@@ -96,45 +114,57 @@ class RasPrj:
|
|
96
114
|
dict: Dictionary containing extracted plan information.
|
97
115
|
"""
|
98
116
|
plan_info = {}
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
117
|
+
try:
|
118
|
+
with open(plan_file_path, 'r') as file:
|
119
|
+
content = file.read()
|
120
|
+
|
121
|
+
# Extract description
|
122
|
+
description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
123
|
+
if description_match:
|
124
|
+
plan_info['description'] = description_match.group(1).strip()
|
125
|
+
|
126
|
+
# BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the plan file keys.
|
127
|
+
|
128
|
+
# Extract other critical information
|
129
|
+
supported_plan_keys = {
|
130
|
+
'Computation Interval': r'Computation Interval=(.+)',
|
131
|
+
'DSS File': r'DSS File=(.+)',
|
132
|
+
'Flow File': r'Flow File=(.+)',
|
133
|
+
'Friction Slope Method': r'Friction Slope Method=(.+)',
|
134
|
+
'Geom File': r'Geom File=(.+)',
|
135
|
+
'Mapping Interval': r'Mapping Interval=(.+)',
|
136
|
+
'Plan Title': r'Plan Title=(.+)',
|
137
|
+
'Program Version': r'Program Version=(.+)',
|
138
|
+
'Run HTab': r'Run HTab=(.+)',
|
139
|
+
'Run PostProcess': r'Run PostProcess=(.+)',
|
140
|
+
'Run Sediment': r'Run Sediment=(.+)',
|
141
|
+
'Run UNet': r'Run UNet=(.+)',
|
142
|
+
'Run WQNet': r'Run WQNet=(.+)',
|
143
|
+
'Short Identifier': r'Short Identifier=(.+)',
|
144
|
+
'Simulation Date': r'Simulation Date=(.+)',
|
145
|
+
'UNET D1 Cores': r'UNET D1 Cores=(.+)',
|
146
|
+
'UNET Use Existing IB Tables': r'UNET Use Existing IB Tables=(.+)',
|
147
|
+
'UNET 1D Methodology': r'UNET 1D Methodology=(.+)',
|
148
|
+
'UNET D2 SolverType': r'UNET D2 SolverType=(.+)',
|
149
|
+
'UNET D2 Name': r'UNET D2 Name=(.+)'
|
150
|
+
}
|
151
|
+
|
152
|
+
# END Exception to Style Guide
|
153
|
+
|
154
|
+
for key, pattern in supported_plan_keys.items():
|
155
|
+
match = re.search(pattern, content)
|
156
|
+
if match:
|
157
|
+
plan_info[key] = match.group(1).strip()
|
130
158
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
plan_info[key] = match.group(1).strip()
|
159
|
+
logger.debug(f"Parsed plan file: {plan_file_path}")
|
160
|
+
except Exception as e:
|
161
|
+
logger.exception(f"Error parsing plan file {plan_file_path}: {e}")
|
135
162
|
|
136
163
|
return plan_info
|
137
164
|
|
165
|
+
|
166
|
+
|
167
|
+
@log_call
|
138
168
|
def _get_prj_entries(self, entry_type):
|
139
169
|
"""
|
140
170
|
Extract entries of a specific type from the HEC-RAS project file.
|
@@ -168,7 +198,6 @@ class RasPrj:
|
|
168
198
|
plan_info = self._parse_plan_file(Path(full_path))
|
169
199
|
entry.update(plan_info)
|
170
200
|
|
171
|
-
# Add HDF results path if it exists
|
172
201
|
hdf_results_path = self.project_folder / f"{self.project_name}.p{file_name[1:]}.hdf"
|
173
202
|
entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
|
174
203
|
|
@@ -178,11 +207,11 @@ class RasPrj:
|
|
178
207
|
|
179
208
|
entries.append(entry)
|
180
209
|
except Exception as e:
|
181
|
-
logging.exception(f"Failed to read project file {self.prj_file}: {e}")
|
182
210
|
raise
|
183
211
|
|
184
212
|
return pd.DataFrame(entries)
|
185
213
|
|
214
|
+
@log_call
|
186
215
|
def _parse_unsteady_file(self, unsteady_file_path):
|
187
216
|
"""
|
188
217
|
Parse an unsteady flow file and extract critical information.
|
@@ -197,21 +226,24 @@ class RasPrj:
|
|
197
226
|
with open(unsteady_file_path, 'r') as file:
|
198
227
|
content = file.read()
|
199
228
|
|
200
|
-
#
|
201
|
-
|
202
|
-
|
203
|
-
'
|
204
|
-
'
|
205
|
-
'
|
206
|
-
'
|
207
|
-
'
|
208
|
-
'
|
209
|
-
'
|
210
|
-
'
|
211
|
-
'
|
229
|
+
# BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the unsteady file keys.
|
230
|
+
|
231
|
+
supported_unsteady_keys = {
|
232
|
+
'Flow Title': r'Flow Title=(.+)',
|
233
|
+
'Program Version': r'Program Version=(.+)',
|
234
|
+
'Use Restart': r'Use Restart=(.+)',
|
235
|
+
'Precipitation Mode': r'Precipitation Mode=(.+)',
|
236
|
+
'Wind Mode': r'Wind Mode=(.+)',
|
237
|
+
'Met BC=Precipitation|Mode': r'Met BC=Precipitation\|Mode=(.+)',
|
238
|
+
'Met BC=Evapotranspiration|Mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
|
239
|
+
'Met BC=Precipitation|Expanded View': r'Met BC=Precipitation\|Expanded View=(.+)',
|
240
|
+
'Met BC=Precipitation|Constant Units': r'Met BC=Precipitation\|Constant Units=(.+)',
|
241
|
+
'Met BC=Precipitation|Gridded Source': r'Met BC=Precipitation\|Gridded Source=(.+)'
|
212
242
|
}
|
213
243
|
|
214
|
-
|
244
|
+
# END Exception to Style Guide
|
245
|
+
|
246
|
+
for key, pattern in supported_unsteady_keys.items():
|
215
247
|
match = re.search(pattern, content)
|
216
248
|
if match:
|
217
249
|
unsteady_info[key] = match.group(1).strip()
|
@@ -228,6 +260,7 @@ class RasPrj:
|
|
228
260
|
"""
|
229
261
|
return self.initialized
|
230
262
|
|
263
|
+
@log_call
|
231
264
|
def check_initialized(self):
|
232
265
|
"""
|
233
266
|
Ensure that the RasPrj instance has been initialized.
|
@@ -236,10 +269,10 @@ class RasPrj:
|
|
236
269
|
RuntimeError: If the project has not been initialized.
|
237
270
|
"""
|
238
271
|
if not self.initialized:
|
239
|
-
logging.error("Project not initialized. Call init_ras_project() first.")
|
240
272
|
raise RuntimeError("Project not initialized. Call init_ras_project() first.")
|
241
273
|
|
242
274
|
@staticmethod
|
275
|
+
@log_call
|
243
276
|
def find_ras_prj(folder_path):
|
244
277
|
"""
|
245
278
|
Find the appropriate HEC-RAS project file (.prj) in the given folder.
|
@@ -254,28 +287,25 @@ class RasPrj:
|
|
254
287
|
prj_files = list(folder_path.glob("*.prj"))
|
255
288
|
rasmap_files = list(folder_path.glob("*.rasmap"))
|
256
289
|
if len(prj_files) == 1:
|
257
|
-
logging.info(f"Single .prj file found: {prj_files[0]}")
|
258
290
|
return prj_files[0].resolve()
|
259
291
|
if len(prj_files) > 1:
|
260
292
|
if len(rasmap_files) == 1:
|
261
293
|
base_filename = rasmap_files[0].stem
|
262
294
|
prj_file = folder_path / f"{base_filename}.prj"
|
263
295
|
if prj_file.exists():
|
264
|
-
logging.info(f"Matched .prj file based on .rasmap: {prj_file}")
|
265
296
|
return prj_file.resolve()
|
266
297
|
for prj_file in prj_files:
|
267
298
|
try:
|
268
299
|
with open(prj_file, 'r') as file:
|
269
300
|
content = file.read()
|
270
301
|
if "Proj Title=" in content:
|
271
|
-
logging.info(f".prj file with 'Proj Title=' found: {prj_file}")
|
272
302
|
return prj_file.resolve()
|
273
|
-
except Exception
|
274
|
-
logging.warning(f"Failed to read .prj file {prj_file}: {e}")
|
303
|
+
except Exception:
|
275
304
|
continue
|
276
|
-
logging.warning("No suitable .prj file found after all checks.")
|
277
305
|
return None
|
278
306
|
|
307
|
+
|
308
|
+
@log_call
|
279
309
|
def get_project_name(self):
|
280
310
|
"""
|
281
311
|
Get the name of the HEC-RAS project.
|
@@ -289,6 +319,7 @@ class RasPrj:
|
|
289
319
|
self.check_initialized()
|
290
320
|
return self.project_name
|
291
321
|
|
322
|
+
@log_call
|
292
323
|
def get_prj_entries(self, entry_type):
|
293
324
|
"""
|
294
325
|
Get entries of a specific type from the HEC-RAS project.
|
@@ -305,6 +336,7 @@ class RasPrj:
|
|
305
336
|
self.check_initialized()
|
306
337
|
return self._get_prj_entries(entry_type)
|
307
338
|
|
339
|
+
@log_call
|
308
340
|
def get_plan_entries(self):
|
309
341
|
"""
|
310
342
|
Get all plan entries from the HEC-RAS project.
|
@@ -318,6 +350,7 @@ class RasPrj:
|
|
318
350
|
self.check_initialized()
|
319
351
|
return self._get_prj_entries('Plan')
|
320
352
|
|
353
|
+
@log_call
|
321
354
|
def get_flow_entries(self):
|
322
355
|
"""
|
323
356
|
Get all flow entries from the HEC-RAS project.
|
@@ -331,6 +364,7 @@ class RasPrj:
|
|
331
364
|
self.check_initialized()
|
332
365
|
return self._get_prj_entries('Flow')
|
333
366
|
|
367
|
+
@log_call
|
334
368
|
def get_unsteady_entries(self):
|
335
369
|
"""
|
336
370
|
Get all unsteady flow entries from the HEC-RAS project.
|
@@ -344,6 +378,7 @@ class RasPrj:
|
|
344
378
|
self.check_initialized()
|
345
379
|
return self._get_prj_entries('Unsteady')
|
346
380
|
|
381
|
+
@log_call
|
347
382
|
def get_geom_entries(self):
|
348
383
|
"""
|
349
384
|
Get all geometry entries from the HEC-RAS project.
|
@@ -357,6 +392,7 @@ class RasPrj:
|
|
357
392
|
self.check_initialized()
|
358
393
|
return self._get_prj_entries('Geom')
|
359
394
|
|
395
|
+
@log_call
|
360
396
|
def get_hdf_entries(self):
|
361
397
|
"""
|
362
398
|
Get HDF entries for plans that have results.
|
@@ -367,40 +403,38 @@ class RasPrj:
|
|
367
403
|
"""
|
368
404
|
self.check_initialized()
|
369
405
|
|
370
|
-
# Filter the plan_df to include only entries with existing HDF results
|
371
406
|
hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
|
372
407
|
|
373
|
-
# If no HDF entries are found, log the information
|
374
408
|
if hdf_entries.empty:
|
375
|
-
logging.info("No HDF entries found.")
|
376
409
|
return pd.DataFrame(columns=self.plan_df.columns)
|
377
410
|
|
378
|
-
logging.info(f"Found {len(hdf_entries)} HDF entries.")
|
379
411
|
return hdf_entries
|
380
412
|
|
413
|
+
|
414
|
+
@log_call
|
381
415
|
def print_data(self):
|
382
416
|
"""Print all RAS Object data for this instance."""
|
383
417
|
self.check_initialized()
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
418
|
+
logger.info(f"--- Data for {self.project_name} ---")
|
419
|
+
logger.info(f"Project folder: {self.project_folder}")
|
420
|
+
logger.info(f"PRJ file: {self.prj_file}")
|
421
|
+
logger.info(f"HEC-RAS executable: {self.ras_exe_path}")
|
422
|
+
logger.info("Plan files:")
|
423
|
+
logger.info(f"\n{self.plan_df}")
|
424
|
+
logger.info("Flow files:")
|
425
|
+
logger.info(f"\n{self.flow_df}")
|
426
|
+
logger.info("Unsteady flow files:")
|
427
|
+
logger.info(f"\n{self.unsteady_df}")
|
428
|
+
logger.info("Geometry files:")
|
429
|
+
logger.info(f"\n{self.geom_df}")
|
430
|
+
logger.info("HDF entries:")
|
431
|
+
logger.info(f"\n{self.get_hdf_entries()}")
|
432
|
+
logger.info("Boundary conditions:")
|
433
|
+
logger.info(f"\n{self.boundaries_df}")
|
434
|
+
logger.info("----------------------------")
|
402
435
|
|
403
436
|
@staticmethod
|
437
|
+
@log_call
|
404
438
|
def get_plan_value(
|
405
439
|
plan_number_or_path: Union[str, Path],
|
406
440
|
key: str,
|
@@ -427,6 +461,7 @@ class RasPrj:
|
|
427
461
|
>>> computation_interval = RasUtils.get_plan_value("01", "computation_interval")
|
428
462
|
>>> print(f"Computation interval: {computation_interval}")
|
429
463
|
"""
|
464
|
+
logger = getLogger(__name__)
|
430
465
|
ras_obj = ras_object or ras
|
431
466
|
ras_obj.check_initialized()
|
432
467
|
|
@@ -439,19 +474,21 @@ class RasPrj:
|
|
439
474
|
}
|
440
475
|
|
441
476
|
if key not in valid_keys:
|
477
|
+
logger.error(f"Invalid key: {key}")
|
442
478
|
raise ValueError(f"Invalid key: {key}. Valid keys are: {', '.join(valid_keys)}")
|
443
479
|
|
444
480
|
plan_file_path = Path(plan_number_or_path)
|
445
481
|
if not plan_file_path.is_file():
|
446
482
|
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
|
447
483
|
if not plan_file_path.exists():
|
484
|
+
logger.error(f"Plan file not found: {plan_file_path}")
|
448
485
|
raise ValueError(f"Plan file not found: {plan_file_path}")
|
449
486
|
|
450
487
|
try:
|
451
488
|
with open(plan_file_path, 'r') as file:
|
452
489
|
content = file.read()
|
453
490
|
except IOError as e:
|
454
|
-
|
491
|
+
logger.error(f"Error reading plan file {plan_file_path}: {e}")
|
455
492
|
raise
|
456
493
|
|
457
494
|
if key == 'description':
|
@@ -464,6 +501,7 @@ class RasPrj:
|
|
464
501
|
match = re.search(pattern, content)
|
465
502
|
return match.group(1).strip() if match else None
|
466
503
|
|
504
|
+
@log_call
|
467
505
|
def get_boundary_conditions(self) -> pd.DataFrame:
|
468
506
|
"""
|
469
507
|
Extract boundary conditions from unsteady flow files and create a DataFrame.
|
@@ -478,27 +516,39 @@ class RasPrj:
|
|
478
516
|
is logged at the DEBUG level for each boundary condition. This feature is crucial
|
479
517
|
for developers incorporating new boundary condition types or parameters, as it
|
480
518
|
allows them to see what information might be missing from the current parsing logic.
|
519
|
+
If no unsteady flow files are present, it returns an empty DataFrame.
|
481
520
|
|
482
521
|
Returns:
|
483
522
|
pd.DataFrame: A DataFrame containing detailed boundary condition information,
|
484
|
-
|
523
|
+
linked to the unsteady flow files.
|
485
524
|
|
486
525
|
Usage:
|
487
526
|
To see the unparsed lines, set the logging level to DEBUG before calling this method:
|
488
527
|
|
489
528
|
import logging
|
490
|
-
|
529
|
+
getLogger().setLevel(logging.DEBUG)
|
491
530
|
|
492
531
|
boundaries_df = ras_project.get_boundary_conditions()
|
532
|
+
linked to the unsteady flow files. Returns an empty DataFrame if
|
533
|
+
no unsteady flow files are present.
|
493
534
|
"""
|
494
535
|
boundary_data = []
|
495
536
|
|
537
|
+
# Check if unsteady_df is empty
|
538
|
+
if self.unsteady_df.empty:
|
539
|
+
logger.info("No unsteady flow files found in the project.")
|
540
|
+
return pd.DataFrame() # Return an empty DataFrame
|
541
|
+
|
496
542
|
for _, row in self.unsteady_df.iterrows():
|
497
543
|
unsteady_file_path = row['full_path']
|
498
544
|
unsteady_number = row['unsteady_number']
|
499
545
|
|
500
|
-
|
501
|
-
|
546
|
+
try:
|
547
|
+
with open(unsteady_file_path, 'r') as file:
|
548
|
+
content = file.read()
|
549
|
+
except IOError as e:
|
550
|
+
logger.error(f"Error reading unsteady file {unsteady_file_path}: {e}")
|
551
|
+
continue
|
502
552
|
|
503
553
|
bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
|
504
554
|
|
@@ -507,7 +557,11 @@ class RasPrj:
|
|
507
557
|
boundary_data.append(bc_info)
|
508
558
|
|
509
559
|
if unparsed_lines:
|
510
|
-
|
560
|
+
logger.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
561
|
+
|
562
|
+
if not boundary_data:
|
563
|
+
logger.info("No boundary conditions found in unsteady flow files.")
|
564
|
+
return pd.DataFrame() # Return an empty DataFrame if no boundary conditions were found
|
511
565
|
|
512
566
|
boundaries_df = pd.DataFrame(boundary_data)
|
513
567
|
|
@@ -517,6 +571,7 @@ class RasPrj:
|
|
517
571
|
|
518
572
|
return merged_df
|
519
573
|
|
574
|
+
@log_call
|
520
575
|
def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
|
521
576
|
lines = block.split('\n')
|
522
577
|
bc_info = {
|
@@ -588,21 +643,25 @@ class RasPrj:
|
|
588
643
|
# Collect unparsed lines
|
589
644
|
unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
|
590
645
|
|
646
|
+
if unparsed_lines:
|
647
|
+
logger.debug(f"Unparsed lines for boundary condition {bc_number} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
648
|
+
|
591
649
|
return bc_info, unparsed_lines
|
592
650
|
|
593
651
|
|
594
652
|
# Create a global instance named 'ras'
|
653
|
+
# Defining the global instance allows the init_ras_project function to initialize the project.
|
654
|
+
# This only happens on the library initialization, not when the user calls init_ras_project.
|
595
655
|
ras = RasPrj()
|
596
656
|
|
597
657
|
# END OF CLASS DEFINITION
|
598
658
|
|
599
659
|
|
600
|
-
|
601
|
-
|
602
660
|
# START OF FUNCTION DEFINITIONS
|
603
661
|
|
604
662
|
|
605
|
-
|
663
|
+
@log_call
|
664
|
+
def init_ras_project(ras_project_folder, ras_version=None, ras_instance=None):
|
606
665
|
"""
|
607
666
|
Initialize a RAS project.
|
608
667
|
|
@@ -616,9 +675,13 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
|
|
616
675
|
-----------
|
617
676
|
ras_project_folder : str
|
618
677
|
The path to the RAS project folder.
|
619
|
-
ras_version : str
|
678
|
+
ras_version : str, optional
|
620
679
|
The version of RAS to use (e.g., "6.5").
|
621
680
|
The version can also be a full path to the Ras.exe file. (Useful when calling ras objects for folder copies.)
|
681
|
+
If None, the function will attempt to use the version from the global 'ras' object or a default path.
|
682
|
+
You MUST specify a version number via init at some point or ras will not run.
|
683
|
+
Once the ras_version is specified once it should auto-fill from the global 'ras' object.
|
684
|
+
The RAS Commander Library Assistant can ignore this argument since it does not have Ras.exe present, but all of other operations are fully working.
|
622
685
|
ras_instance : RasPrj, optional
|
623
686
|
An instance of RasPrj to initialize. If None, the global 'ras' instance is used.
|
624
687
|
|
@@ -630,12 +693,12 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
|
|
630
693
|
Usage:
|
631
694
|
------
|
632
695
|
1. For general use with a single project:
|
633
|
-
init_ras_project("/path/to/project"
|
696
|
+
init_ras_project("/path/to/project")
|
634
697
|
# Use the global 'ras' object after initialization
|
635
698
|
|
636
699
|
2. For managing multiple projects:
|
637
700
|
project1 = init_ras_project("/path/to/project1", "6.5", ras_instance=RasPrj())
|
638
|
-
project2 = init_ras_project("/path/to/project2",
|
701
|
+
project2 = init_ras_project("/path/to/project2", ras_instance=RasPrj())
|
639
702
|
|
640
703
|
Notes:
|
641
704
|
------
|
@@ -644,51 +707,70 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
|
|
644
707
|
- Be consistent in your approach: stick to either the global 'ras' object
|
645
708
|
or custom instances throughout your script or application.
|
646
709
|
- Document your choice of approach clearly in your code.
|
710
|
+
- If ras_version is not provided, the function will attempt to use the version
|
711
|
+
from the global 'ras' object or a default path.
|
647
712
|
|
648
713
|
Warnings:
|
649
714
|
---------
|
650
715
|
Avoid mixing use of the global 'ras' object and custom instances to prevent
|
651
716
|
confusion and potential bugs.
|
652
717
|
"""
|
653
|
-
logging.info(f"Initializing project in folder: {ras_project_folder}")
|
654
|
-
logging.info(f"Using ras_instance with id: {id(ras_instance)}")
|
655
|
-
|
656
|
-
|
657
|
-
|
658
718
|
if not Path(ras_project_folder).exists():
|
659
|
-
|
719
|
+
logger.error(f"The specified RAS project folder does not exist: {ras_project_folder}")
|
660
720
|
raise FileNotFoundError(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
|
661
721
|
|
662
722
|
ras_exe_path = get_ras_exe(ras_version)
|
663
723
|
|
664
724
|
if ras_instance is None:
|
665
|
-
|
725
|
+
logger.info("Initializing global 'ras' object via init_ras_project function.")
|
666
726
|
ras_instance = ras
|
667
727
|
elif not isinstance(ras_instance, RasPrj):
|
668
|
-
|
728
|
+
logger.error("Provided ras_instance is not an instance of RasPrj.")
|
669
729
|
raise TypeError("ras_instance must be an instance of RasPrj or None.")
|
670
730
|
|
671
731
|
# Initialize the RasPrj instance
|
672
732
|
ras_instance.initialize(ras_project_folder, ras_exe_path)
|
673
733
|
|
674
|
-
|
734
|
+
logger.info(f"Project initialized. ras_instance project folder: {ras_instance.project_folder}")
|
675
735
|
return ras_instance
|
676
736
|
|
677
|
-
|
678
|
-
def get_ras_exe(ras_version):
|
737
|
+
@log_call
|
738
|
+
def get_ras_exe(ras_version=None):
|
679
739
|
"""
|
680
740
|
Determine the HEC-RAS executable path based on the input.
|
681
741
|
|
682
742
|
Args:
|
683
|
-
ras_version (str): Either a version number or a full path to the HEC-RAS executable.
|
743
|
+
ras_version (str, optional): Either a version number or a full path to the HEC-RAS executable.
|
744
|
+
If None, the function will attempt to use the version from the global 'ras' object
|
745
|
+
or a default path.
|
684
746
|
|
685
747
|
Returns:
|
686
748
|
str: The full path to the HEC-RAS executable.
|
687
749
|
|
688
750
|
Raises:
|
689
751
|
ValueError: If the input is neither a valid version number nor a valid file path.
|
690
|
-
|
752
|
+
|
753
|
+
Notes:
|
754
|
+
- If ras_version is not provided, the function will first check the global 'ras' object for a path.
|
755
|
+
- If the global 'ras' object is not initialized or doesn't have a path, a default path will be used.
|
756
|
+
- The default path allows the library to function in environments without HEC-RAS installed.
|
757
|
+
- This enables the HEC-Commander GPT to operate without stopping, even if HEC-RAS is not present.
|
758
|
+
- End users MUST use logging to check for operational errors, as full code stops prevent the GPT from running.
|
691
759
|
"""
|
760
|
+
# If ras_version is not provided, use the version of the global 'ras' object.
|
761
|
+
# If the global 'ras' object is not initialized, use the default path: default_path = Path("/path/to/Ras.exe")
|
762
|
+
# This default path allows the library to function in environments without HEC-RAS installed.
|
763
|
+
# It enables our HEC-Commander GPT to operate without stopping, even though HEC-RAS is not present.
|
764
|
+
# End users MUST use logging to check for operational errors, as full code stops prevent the GPT from running.
|
765
|
+
if ras_version is None:
|
766
|
+
if hasattr(ras, 'ras_exe_path') and ras.ras_exe_path:
|
767
|
+
logger.info(f"Using HEC-RAS executable from global 'ras' object: {ras.ras_exe_path}")
|
768
|
+
return ras.ras_exe_path
|
769
|
+
else:
|
770
|
+
default_path = Path("Ras.exe")
|
771
|
+
logger.warning(f"No HEC-RAS version specified and global 'ras' object not initialized or missing ras_exe_path. Using default path: {default_path}. The RAS Commander (ras-commander) Library Assistant can ignore this error since it does not have HEC-RAS installed.")
|
772
|
+
return str(default_path)
|
773
|
+
|
692
774
|
ras_version_numbers = [
|
693
775
|
"6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
|
694
776
|
"5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
|
@@ -698,41 +780,30 @@ def get_ras_exe(ras_version):
|
|
698
780
|
hecras_path = Path(ras_version)
|
699
781
|
|
700
782
|
if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
|
701
|
-
|
783
|
+
logger.info(f"HEC-RAS executable found at specified path: {hecras_path}")
|
702
784
|
return str(hecras_path)
|
703
785
|
|
704
786
|
if ras_version in ras_version_numbers:
|
705
787
|
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
|
706
788
|
if default_path.is_file():
|
707
|
-
|
789
|
+
logger.info(f"HEC-RAS executable found at default path: {default_path}")
|
708
790
|
return str(default_path)
|
709
791
|
else:
|
710
|
-
|
711
|
-
raise FileNotFoundError(f"HEC-RAS executable not found at the expected path: {default_path}")
|
792
|
+
logger.critical(f"HEC-RAS executable not found at the expected path: {default_path}")
|
712
793
|
|
713
794
|
try:
|
714
795
|
version_float = float(ras_version)
|
715
796
|
if version_float > max(float(v) for v in ras_version_numbers):
|
716
797
|
newer_version_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
|
717
798
|
if newer_version_path.is_file():
|
718
|
-
|
799
|
+
logger.info(f"Newer version of HEC-RAS executable found at: {newer_version_path}")
|
719
800
|
return str(newer_version_path)
|
720
801
|
else:
|
721
|
-
|
722
|
-
raise FileNotFoundError(
|
723
|
-
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}"
|
724
|
-
)
|
802
|
+
logger.critical("Newer version of HEC-RAS was specified, but the executable was not found.")
|
725
803
|
except ValueError:
|
726
804
|
pass
|
727
805
|
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
)
|
733
|
-
raise ValueError(
|
734
|
-
f"Invalid HEC-RAS version or path: {ras_version}. "
|
735
|
-
f"Please provide a valid version number from {ras_version_numbers} "
|
736
|
-
"or a full path to the HEC-RAS executable."
|
737
|
-
)
|
738
|
-
|
806
|
+
logger.error(f"Invalid HEC-RAS version or path: {ras_version}, returning default path: {default_path}")
|
807
|
+
#raise ValueError(f"Invalid HEC-RAS version or path: {ras_version}") # don't raise an error here, just return the default path
|
808
|
+
return str(default_path)
|
809
|
+
|