ras-commander 0.68.0__py3-none-any.whl → 0.71.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 +189 -40
- ras_commander/RasGeo.py +257 -15
- ras_commander/RasPlan.py +41 -26
- ras_commander/RasPrj.py +224 -78
- ras_commander/RasUnsteady.py +162 -68
- ras_commander/__init__.py +1 -1
- {ras_commander-0.68.0.dist-info → ras_commander-0.71.0.dist-info}/METADATA +15 -5
- {ras_commander-0.68.0.dist-info → ras_commander-0.71.0.dist-info}/RECORD +11 -14
- {ras_commander-0.68.0.dist-info → ras_commander-0.71.0.dist-info}/WHEEL +1 -1
- ras_commander/RasGpt.py +0 -27
- ras_commander/RasMapper.py +0 -24
- ras_commander/RasToGo.py +0 -37
- {ras_commander-0.68.0.dist-info → ras_commander-0.71.0.dist-info/licenses}/LICENSE +0 -0
- {ras_commander-0.68.0.dist-info → ras_commander-0.71.0.dist-info}/top_level.txt +0 -0
ras_commander/RasCmdr.py
CHANGED
@@ -99,23 +99,66 @@ class RasCmdr:
|
|
99
99
|
overwrite_dest=False
|
100
100
|
):
|
101
101
|
"""
|
102
|
-
Execute a HEC-RAS plan.
|
102
|
+
Execute a single HEC-RAS plan in a specified location.
|
103
|
+
|
104
|
+
This function runs a HEC-RAS plan by launching the HEC-RAS executable through command line,
|
105
|
+
allowing for destination folder specification, core count control, and geometry preprocessor management.
|
103
106
|
|
104
107
|
Args:
|
105
108
|
plan_number (str, Path): The plan number to execute (e.g., "01", "02") or the full path to the plan file.
|
109
|
+
Recommended to use two-digit strings for plan numbers for consistency (e.g., "01" instead of 1).
|
106
110
|
dest_folder (str, Path, optional): Name of the folder or full path for computation.
|
107
111
|
If a string is provided, it will be created in the same parent directory as the project folder.
|
108
112
|
If a full path is provided, it will be used as is.
|
113
|
+
If None, computation occurs in the original project folder, modifying the original project.
|
109
114
|
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
115
|
+
Useful when working with multiple projects simultaneously.
|
110
116
|
clear_geompre (bool, optional): Whether to clear geometry preprocessor files. Defaults to False.
|
111
|
-
|
117
|
+
Set to True when geometry has been modified to force recomputation of preprocessor files.
|
118
|
+
num_cores (int, optional): Number of cores to use for the plan execution.
|
119
|
+
If None, the current setting in the plan file is not changed.
|
120
|
+
Generally, 2-4 cores provides good performance for most models.
|
112
121
|
overwrite_dest (bool, optional): If True, overwrite the destination folder if it exists. Defaults to False.
|
122
|
+
Set to True to replace an existing destination folder with the same name.
|
113
123
|
|
114
124
|
Returns:
|
115
125
|
bool: True if the execution was successful, False otherwise.
|
116
126
|
|
117
127
|
Raises:
|
118
128
|
ValueError: If the specified dest_folder already exists and is not empty, and overwrite_dest is False.
|
129
|
+
FileNotFoundError: If the plan file or project file cannot be found.
|
130
|
+
PermissionError: If there are issues accessing or writing to the destination folder.
|
131
|
+
subprocess.CalledProcessError: If the HEC-RAS execution fails.
|
132
|
+
|
133
|
+
Examples:
|
134
|
+
# Run a plan in the original project folder
|
135
|
+
RasCmdr.compute_plan("01")
|
136
|
+
|
137
|
+
# Run a plan in a separate folder
|
138
|
+
RasCmdr.compute_plan("01", dest_folder="computation_folder")
|
139
|
+
|
140
|
+
# Run a plan with a specific number of cores
|
141
|
+
RasCmdr.compute_plan("01", num_cores=4)
|
142
|
+
|
143
|
+
# Run a plan in a specific folder, overwriting if it exists
|
144
|
+
RasCmdr.compute_plan("01", dest_folder="computation_folder", overwrite_dest=True)
|
145
|
+
|
146
|
+
# Run a plan in a specific folder with multiple options
|
147
|
+
RasCmdr.compute_plan(
|
148
|
+
"01",
|
149
|
+
dest_folder="computation_folder",
|
150
|
+
num_cores=2,
|
151
|
+
clear_geompre=True,
|
152
|
+
overwrite_dest=True
|
153
|
+
)
|
154
|
+
|
155
|
+
Notes:
|
156
|
+
- For executing multiple plans, consider using compute_parallel() or compute_test_mode().
|
157
|
+
- Setting num_cores appropriately is important for performance:
|
158
|
+
* 1-2 cores: Highest efficiency per core, good for small models
|
159
|
+
* 3-8 cores: Good balance for most models
|
160
|
+
* >8 cores: May have diminishing returns due to overhead
|
161
|
+
- This function updates the RAS object's dataframes (plan_df, geom_df, etc.) after execution.
|
119
162
|
"""
|
120
163
|
try:
|
121
164
|
ras_obj = ras_object if ras_object is not None else ras
|
@@ -202,8 +245,6 @@ class RasCmdr:
|
|
202
245
|
|
203
246
|
|
204
247
|
|
205
|
-
@staticmethod
|
206
|
-
@log_call
|
207
248
|
@staticmethod
|
208
249
|
@log_call
|
209
250
|
def compute_parallel(
|
@@ -216,19 +257,83 @@ class RasCmdr:
|
|
216
257
|
overwrite_dest: bool = False
|
217
258
|
) -> Dict[str, bool]:
|
218
259
|
"""
|
219
|
-
|
260
|
+
Execute multiple HEC-RAS plans in parallel using multiple worker instances.
|
261
|
+
|
262
|
+
This method creates separate worker folders for each parallel process, runs plans
|
263
|
+
in those folders, and then consolidates results to a final destination folder.
|
264
|
+
It's ideal for running independent plans simultaneously to make better use of system resources.
|
220
265
|
|
221
266
|
Args:
|
222
|
-
plan_number (Union[str, List[str], None]): Plan number(s) to compute.
|
223
|
-
|
267
|
+
plan_number (Union[str, List[str], None]): Plan number(s) to compute.
|
268
|
+
If None, all plans in the project are computed.
|
269
|
+
If string, only that plan will be computed.
|
270
|
+
If list, all specified plans will be computed.
|
271
|
+
Recommended to use two-digit strings for plan numbers for consistency (e.g., "01" instead of 1).
|
272
|
+
max_workers (int): Maximum number of parallel workers (separate HEC-RAS instances).
|
273
|
+
Each worker gets a separate folder with a copy of the project.
|
274
|
+
Optimal value depends on CPU cores and memory available.
|
275
|
+
A good starting point is: max_workers = floor(physical_cores / num_cores).
|
224
276
|
num_cores (int): Number of cores to use per plan computation.
|
225
|
-
|
226
|
-
|
277
|
+
Controls computational resources allocated to each individual HEC-RAS instance.
|
278
|
+
For parallel execution, 2-4 cores per worker often provides the best balance.
|
279
|
+
clear_geompre (bool): Whether to clear geometry preprocessor files before computation.
|
280
|
+
Set to True when geometry has been modified to force recomputation.
|
281
|
+
ras_object (Optional[RasPrj]): RAS project object. If None, uses global 'ras' instance.
|
282
|
+
Useful when working with multiple projects simultaneously.
|
227
283
|
dest_folder (Union[str, Path, None]): Destination folder for computed results.
|
284
|
+
If None, creates a "[Computed]" folder adjacent to the project folder.
|
285
|
+
If string, creates folder in the project's parent directory.
|
286
|
+
If Path, uses the exact path provided.
|
228
287
|
overwrite_dest (bool): Whether to overwrite existing destination folder.
|
288
|
+
Set to True to replace an existing destination folder with the same name.
|
229
289
|
|
230
290
|
Returns:
|
231
291
|
Dict[str, bool]: Dictionary of plan numbers and their execution success status.
|
292
|
+
Keys are plan numbers and values are boolean success indicators.
|
293
|
+
|
294
|
+
Raises:
|
295
|
+
ValueError: If the destination folder already exists, is not empty, and overwrite_dest is False.
|
296
|
+
FileNotFoundError: If project files cannot be found.
|
297
|
+
PermissionError: If there are issues accessing or writing to folders.
|
298
|
+
RuntimeError: If worker initialization fails.
|
299
|
+
|
300
|
+
Examples:
|
301
|
+
# Run all plans in parallel with default settings
|
302
|
+
RasCmdr.compute_parallel()
|
303
|
+
|
304
|
+
# Run all plans with 4 workers, 2 cores per worker
|
305
|
+
RasCmdr.compute_parallel(max_workers=4, num_cores=2)
|
306
|
+
|
307
|
+
# Run specific plans in parallel
|
308
|
+
RasCmdr.compute_parallel(plan_number=["01", "03"], max_workers=2)
|
309
|
+
|
310
|
+
# Run all plans with dynamic worker allocation based on system resources
|
311
|
+
import psutil
|
312
|
+
physical_cores = psutil.cpu_count(logical=False)
|
313
|
+
cores_per_worker = 2
|
314
|
+
max_workers = max(1, physical_cores // cores_per_worker)
|
315
|
+
RasCmdr.compute_parallel(max_workers=max_workers, num_cores=cores_per_worker)
|
316
|
+
|
317
|
+
# Run all plans in a specific destination folder
|
318
|
+
RasCmdr.compute_parallel(dest_folder="parallel_results", overwrite_dest=True)
|
319
|
+
|
320
|
+
Notes:
|
321
|
+
- Worker Assignment: Plans are assigned to workers in a round-robin fashion.
|
322
|
+
For example, with 3 workers and 5 plans, assignment would be:
|
323
|
+
Worker 1: Plans 1 & 4, Worker 2: Plans 2 & 5, Worker 3: Plan 3.
|
324
|
+
|
325
|
+
- Resource Management: Each HEC-RAS instance (worker) typically requires:
|
326
|
+
* 2-4 GB of RAM
|
327
|
+
* 2-4 cores for optimal performance
|
328
|
+
|
329
|
+
- When to use parallel vs. sequential:
|
330
|
+
* Parallel: For independent plans, faster overall completion
|
331
|
+
* Sequential: For dependent plans, consistent resource usage, easier debugging
|
332
|
+
|
333
|
+
- The function creates worker folders during execution and consolidates results
|
334
|
+
to the destination folder upon completion.
|
335
|
+
|
336
|
+
- This function updates the RAS object's dataframes (plan_df, geom_df, etc.) after execution.
|
232
337
|
"""
|
233
338
|
try:
|
234
339
|
ras_obj = ras_object or ras
|
@@ -251,11 +356,17 @@ class RasCmdr:
|
|
251
356
|
logger.info(f"Copied project folder to destination: {dest_folder_path}")
|
252
357
|
project_folder = dest_folder_path
|
253
358
|
|
359
|
+
# Store filtered plan numbers separately to ensure only these are executed
|
360
|
+
filtered_plan_numbers = []
|
361
|
+
|
254
362
|
if plan_number:
|
255
363
|
if isinstance(plan_number, str):
|
256
364
|
plan_number = [plan_number]
|
257
365
|
ras_obj.plan_df = ras_obj.plan_df[ras_obj.plan_df['plan_number'].isin(plan_number)]
|
258
|
-
|
366
|
+
filtered_plan_numbers = list(ras_obj.plan_df['plan_number'])
|
367
|
+
logger.info(f"Filtered plans to execute: {filtered_plan_numbers}")
|
368
|
+
else:
|
369
|
+
filtered_plan_numbers = list(ras_obj.plan_df['plan_number'])
|
259
370
|
|
260
371
|
num_plans = len(ras_obj.plan_df)
|
261
372
|
max_workers = min(max_workers, num_plans) if num_plans > 0 else 1
|
@@ -282,8 +393,9 @@ class RasCmdr:
|
|
282
393
|
logger.critical(f"Failed to initialize RAS project for worker {worker_id}: {str(e)}")
|
283
394
|
worker_ras_objects[worker_id] = None
|
284
395
|
|
396
|
+
# Explicitly use the filtered plan numbers for assignments
|
285
397
|
worker_cycle = cycle(range(1, max_workers + 1))
|
286
|
-
plan_assignments = [(next(worker_cycle), plan_num) for plan_num in
|
398
|
+
plan_assignments = [(next(worker_cycle), plan_num) for plan_num in filtered_plan_numbers]
|
287
399
|
|
288
400
|
execution_results: Dict[str, bool] = {}
|
289
401
|
|
@@ -397,49 +509,86 @@ class RasCmdr:
|
|
397
509
|
overwrite_dest=False
|
398
510
|
):
|
399
511
|
"""
|
400
|
-
Execute HEC-RAS plans in
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
copies the project there, and executes the specified plans in sequential order.
|
406
|
-
|
407
|
-
For most purposes, just copying the project folder, initing that new folder, then running each plan
|
408
|
-
with compute_plan is a simpler and more flexible approach. This is shown in the examples provided
|
409
|
-
in the ras-commander library.
|
512
|
+
Execute HEC-RAS plans sequentially in a separate test folder.
|
513
|
+
|
514
|
+
This function creates a separate test folder, copies the project there, and executes
|
515
|
+
the specified plans in sequential order. It's useful for batch processing plans that
|
516
|
+
need to be run in a specific order or when you want to ensure consistent resource usage.
|
410
517
|
|
411
518
|
Args:
|
412
519
|
plan_number (str, list[str], optional): Plan number or list of plan numbers to execute.
|
413
520
|
If None, all plans will be executed. Default is None.
|
414
|
-
|
521
|
+
Recommended to use two-digit strings for plan numbers for consistency (e.g., "01" instead of 1).
|
522
|
+
dest_folder_suffix (str, optional): Suffix to append to the test folder name.
|
415
523
|
Defaults to "[Test]".
|
416
|
-
|
524
|
+
The test folder is always created in the project folder's parent directory.
|
417
525
|
clear_geompre (bool, optional): Whether to clear geometry preprocessor files.
|
418
526
|
Defaults to False.
|
419
|
-
|
420
|
-
|
527
|
+
Set to True when geometry has been modified to force recomputation.
|
528
|
+
num_cores (int, optional): Number of cores to use for each plan.
|
529
|
+
If None, the current setting in the plan file is not changed. Default is None.
|
530
|
+
For sequential execution, 4-8 cores often provides good performance.
|
421
531
|
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
422
|
-
|
532
|
+
Useful when working with multiple projects simultaneously.
|
533
|
+
overwrite_dest (bool, optional): If True, overwrite the destination folder if it exists.
|
534
|
+
Defaults to False.
|
535
|
+
Set to True to replace an existing test folder with the same name.
|
423
536
|
|
424
537
|
Returns:
|
425
538
|
Dict[str, bool]: Dictionary of plan numbers and their execution success status.
|
539
|
+
Keys are plan numbers and values are boolean success indicators.
|
540
|
+
|
541
|
+
Raises:
|
542
|
+
ValueError: If the destination folder already exists, is not empty, and overwrite_dest is False.
|
543
|
+
FileNotFoundError: If project files cannot be found.
|
544
|
+
PermissionError: If there are issues accessing or writing to folders.
|
426
545
|
|
427
|
-
|
428
|
-
Run all plans
|
429
|
-
|
430
|
-
|
431
|
-
Run
|
432
|
-
|
433
|
-
|
546
|
+
Examples:
|
547
|
+
# Run all plans sequentially
|
548
|
+
RasCmdr.compute_test_mode()
|
549
|
+
|
550
|
+
# Run a specific plan
|
551
|
+
RasCmdr.compute_test_mode(plan_number="01")
|
552
|
+
|
553
|
+
# Run multiple specific plans
|
554
|
+
RasCmdr.compute_test_mode(plan_number=["01", "03", "05"])
|
555
|
+
|
556
|
+
# Run plans with a custom folder suffix
|
557
|
+
RasCmdr.compute_test_mode(dest_folder_suffix="[SequentialRun]")
|
558
|
+
|
559
|
+
# Run plans with a specific number of cores
|
560
|
+
RasCmdr.compute_test_mode(num_cores=4)
|
434
561
|
|
562
|
+
# Run specific plans with multiple options
|
563
|
+
RasCmdr.compute_test_mode(
|
564
|
+
plan_number=["01", "02"],
|
565
|
+
dest_folder_suffix="[SpecificSequential]",
|
566
|
+
clear_geompre=True,
|
567
|
+
num_cores=6,
|
568
|
+
overwrite_dest=True
|
569
|
+
)
|
570
|
+
|
435
571
|
Notes:
|
436
|
-
- This function
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
572
|
+
- This function was created to replicate the original HEC-RAS command line -test flag,
|
573
|
+
which does not work in recent versions of HEC-RAS.
|
574
|
+
|
575
|
+
- Key differences from other compute functions:
|
576
|
+
* compute_plan: Runs a single plan, with option for destination folder
|
577
|
+
* compute_parallel: Runs multiple plans simultaneously in worker folders
|
578
|
+
* compute_test_mode: Runs multiple plans sequentially in a single test folder
|
579
|
+
|
580
|
+
- Use cases:
|
581
|
+
* Running plans in a specific order
|
582
|
+
* Ensuring consistent resource usage
|
583
|
+
* Easier debugging (one plan at a time)
|
584
|
+
* Isolated test environment
|
585
|
+
|
586
|
+
- Performance considerations:
|
587
|
+
* Sequential execution is generally slower overall than parallel execution
|
588
|
+
* Each plan gets consistent resource usage
|
589
|
+
* Execution time scales linearly with the number of plans
|
590
|
+
|
591
|
+
- This function updates the RAS object's dataframes (plan_df, geom_df, etc.) after execution.
|
443
592
|
"""
|
444
593
|
try:
|
445
594
|
ras_obj = ras_object or ras
|
ras_commander/RasGeo.py
CHANGED
@@ -37,6 +37,7 @@ List of Functions in RasGeo:
|
|
37
37
|
import os
|
38
38
|
from pathlib import Path
|
39
39
|
from typing import List, Union
|
40
|
+
import pandas as pd # Added pandas import
|
40
41
|
from .RasPlan import RasPlan
|
41
42
|
from .RasPrj import ras
|
42
43
|
from .LoggingConfig import get_logger
|
@@ -56,13 +57,18 @@ class RasGeo:
|
|
56
57
|
ras_object = None
|
57
58
|
) -> None:
|
58
59
|
"""
|
59
|
-
Clear HEC-RAS geometry preprocessor files for specified plan files
|
60
|
-
|
60
|
+
Clear HEC-RAS geometry preprocessor files for specified plan files.
|
61
|
+
|
62
|
+
Geometry preprocessor files (.c* extension) contain computed hydraulic properties derived
|
63
|
+
from the geometry. These should be cleared when the geometry changes to ensure that
|
64
|
+
HEC-RAS recomputes all hydraulic tables with updated geometry information.
|
65
|
+
|
61
66
|
Limitations/Future Work:
|
62
67
|
- This function only deletes the geometry preprocessor file.
|
63
68
|
- It does not clear the IB tables.
|
64
69
|
- It also does not clear geometry preprocessor tables from the geometry HDF.
|
65
|
-
- All of these features will need to be added to reliably remove geometry preprocessor
|
70
|
+
- All of these features will need to be added to reliably remove geometry preprocessor
|
71
|
+
files for 1D and 2D projects.
|
66
72
|
|
67
73
|
Parameters:
|
68
74
|
plan_files (Union[str, Path, List[Union[str, Path]]], optional):
|
@@ -71,20 +77,20 @@ class RasGeo:
|
|
71
77
|
ras_object: An optional RAS object instance.
|
72
78
|
|
73
79
|
Returns:
|
74
|
-
None
|
75
|
-
|
76
|
-
|
77
|
-
#
|
78
|
-
|
80
|
+
None: The function deletes files and updates the ras object's geometry dataframe
|
81
|
+
|
82
|
+
Example:
|
83
|
+
# Clone a plan and geometry
|
84
|
+
new_plan_number = RasPlan.clone_plan("01")
|
85
|
+
new_geom_number = RasPlan.clone_geom("01")
|
79
86
|
|
80
|
-
#
|
81
|
-
|
87
|
+
# Set the new geometry for the cloned plan
|
88
|
+
RasPlan.set_geom(new_plan_number, new_geom_number)
|
89
|
+
plan_path = RasPlan.get_plan_path(new_plan_number)
|
82
90
|
|
83
|
-
# Clear
|
84
|
-
RasGeo.clear_geompre_files(
|
85
|
-
|
86
|
-
Note:
|
87
|
-
This function updates the ras object's geometry dataframe after clearing the preprocessor files.
|
91
|
+
# Clear geometry preprocessor files to ensure clean results
|
92
|
+
RasGeo.clear_geompre_files(plan_path)
|
93
|
+
print(f"Cleared geometry preprocessor files for plan {new_plan_number}")
|
88
94
|
"""
|
89
95
|
ras_obj = ras_object or ras
|
90
96
|
ras_obj.check_initialized()
|
@@ -131,8 +137,244 @@ class RasGeo:
|
|
131
137
|
|
132
138
|
|
133
139
|
|
140
|
+
@staticmethod
|
141
|
+
def get_mannings_override_tables(geom_file_path, ras_object=None):
|
142
|
+
"""
|
143
|
+
Extracts Manning's override region tables from a HEC-RAS geometry file.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
geom_file_path (str or Path): Geometry file path or geometry number (e.g., "01").
|
147
|
+
ras_object (RasPrj, optional): RAS project object for context. Defaults to global 'ras'.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
pd.DataFrame: DataFrame containing Manning's override region tables with columns:
|
151
|
+
- Region Name: Name of the override region
|
152
|
+
- Land Use Type: Land use type or description
|
153
|
+
- Mannings N Value: Manning's n value for the land use type
|
154
|
+
- Polygon Value: Polygon value or ID associated with the region
|
155
|
+
|
156
|
+
Raises:
|
157
|
+
FileNotFoundError: If the geometry file doesn't exist.
|
158
|
+
ValueError: If the geometry file number is invalid.
|
159
|
+
"""
|
160
|
+
# Get the full path to the geometry file if a number was provided
|
161
|
+
if isinstance(geom_file_path, (str, int)) and not str(geom_file_path).endswith('.g'):
|
162
|
+
ras_obj = ras_object or ras
|
163
|
+
ras_obj.check_initialized()
|
164
|
+
geom_file_path = RasPlan.get_geom_path(str(geom_file_path), ras_object=ras_obj)
|
165
|
+
if geom_file_path is None:
|
166
|
+
raise ValueError(f"Geometry file number '{geom_file_path}' not found in project")
|
167
|
+
|
168
|
+
geom_file_path = Path(geom_file_path)
|
169
|
+
if not geom_file_path.exists():
|
170
|
+
raise FileNotFoundError(f"Geometry file not found: {geom_file_path}")
|
171
|
+
|
172
|
+
# Lists for storing data
|
173
|
+
region_names, land_use_types, mannings_values, polygon_values = [], [], [], []
|
174
|
+
|
175
|
+
region_name, table_value, polygon_value = "", 0, ""
|
176
|
+
|
177
|
+
with open(geom_file_path, 'r') as file:
|
178
|
+
lines = file.readlines()
|
179
|
+
|
180
|
+
i = 0 # Initialize line counter
|
181
|
+
while i < len(lines):
|
182
|
+
line = lines[i].strip()
|
183
|
+
|
184
|
+
if "LCMann Region Name=" in line:
|
185
|
+
region_name = line.split("=")[1]
|
186
|
+
i += 1 # Move to the next line
|
187
|
+
continue
|
188
|
+
|
189
|
+
if "LCMann Region Table=" in line:
|
190
|
+
table_value = int(line.split("=")[1])
|
191
|
+
i += 1 # Skip to the next line which starts the table entries
|
192
|
+
for j in range(table_value):
|
193
|
+
if i+j < len(lines):
|
194
|
+
# Handle multiple commas by splitting from the right
|
195
|
+
parts = lines[i+j].strip().rsplit(",", 1)
|
196
|
+
if len(parts) == 2:
|
197
|
+
land_use, mannings = parts
|
198
|
+
try:
|
199
|
+
mannings_float = float(mannings)
|
200
|
+
region_names.append(region_name)
|
201
|
+
land_use_types.append(land_use)
|
202
|
+
mannings_values.append(mannings_float)
|
203
|
+
polygon_values.append(polygon_value) # This will repeat the last polygon_value
|
204
|
+
except ValueError:
|
205
|
+
# Skip if Manning's value is not a valid float
|
206
|
+
pass
|
207
|
+
|
208
|
+
i += table_value # Skip past the table entries
|
209
|
+
continue
|
210
|
+
|
211
|
+
if "LCMann Region Polygon=" in line:
|
212
|
+
polygon_value = line.split("=")[1]
|
213
|
+
i += 1 # Move to the next line
|
214
|
+
continue
|
215
|
+
|
216
|
+
i += 1 # Move to the next line if none of the conditions above are met
|
217
|
+
|
218
|
+
# Create DataFrame
|
219
|
+
mannings_tables = pd.DataFrame({
|
220
|
+
"Region Name": region_names,
|
221
|
+
"Land Use Type": land_use_types,
|
222
|
+
"Mannings N Value": mannings_values,
|
223
|
+
"Polygon Value": polygon_values
|
224
|
+
})
|
225
|
+
|
226
|
+
return mannings_tables
|
134
227
|
|
135
228
|
|
136
229
|
|
230
|
+
@staticmethod
|
231
|
+
@log_call
|
232
|
+
def set_mannings_override_tables(geom_file_path, mannings_df, ras_object=None):
|
233
|
+
"""
|
234
|
+
Updates Manning's override region tables in a HEC-RAS geometry file based on provided dataframe.
|
235
|
+
|
236
|
+
This function takes a dataframe of Manning's values (similar to the one returned by
|
237
|
+
extract_mannings_override_tables) and updates the corresponding values in the geometry file.
|
238
|
+
If Region Name is specified in the dataframe, only updates that specific region.
|
239
|
+
If no Region Name is given for a row, it updates all instances of the Land Use Type
|
240
|
+
across all regions in the geometry file.
|
241
|
+
|
242
|
+
Args:
|
243
|
+
geom_file_path (str or Path): Geometry file path or geometry number (e.g., "01").
|
244
|
+
mannings_df (pd.DataFrame): DataFrame containing Manning's override values with columns:
|
245
|
+
- Land Use Type: Land use type or description (required)
|
246
|
+
- Mannings N Value: Manning's n value for the land use type (required)
|
247
|
+
- Region Name: Name of the override region (optional)
|
248
|
+
ras_object (RasPrj, optional): RAS project object for context. Defaults to global 'ras'.
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
bool: True if successful, False otherwise.
|
252
|
+
|
253
|
+
Raises:
|
254
|
+
FileNotFoundError: If the geometry file doesn't exist.
|
255
|
+
ValueError: If the geometry file number is invalid or required columns are missing.
|
256
|
+
|
257
|
+
Example:
|
258
|
+
# Get existing Manning's tables
|
259
|
+
mannings_tables = RasGeo.extract_mannings_override_tables("01")
|
260
|
+
|
261
|
+
# Update specific values
|
262
|
+
mannings_tables.loc[mannings_tables['Land Use Type'] == 'Open Water', 'Mannings N Value'] = 0.030
|
263
|
+
|
264
|
+
# Update all forest types in all regions
|
265
|
+
forest_updates = pd.DataFrame({
|
266
|
+
'Land Use Type': ['Mixed Forest', 'Deciduous Forest', 'Evergreen Forest'],
|
267
|
+
'Mannings N Value': [0.040, 0.042, 0.045]
|
268
|
+
})
|
269
|
+
|
270
|
+
# Apply the changes
|
271
|
+
RasGeo.set_mannings_override_tables("01", mannings_tables)
|
272
|
+
# Or apply just the forest updates to all regions
|
273
|
+
RasGeo.set_mannings_override_tables("01", forest_updates)
|
274
|
+
"""
|
275
|
+
# Get the full path to the geometry file if a number was provided
|
276
|
+
if isinstance(geom_file_path, (str, int)) and not str(geom_file_path).endswith('.g'):
|
277
|
+
ras_obj = ras_object or ras
|
278
|
+
ras_obj.check_initialized()
|
279
|
+
geom_file_path = RasPlan.get_geom_path(str(geom_file_path), ras_object=ras_obj)
|
280
|
+
if geom_file_path is None:
|
281
|
+
raise ValueError(f"Geometry file number '{geom_file_path}' not found in project")
|
282
|
+
|
283
|
+
geom_file_path = Path(geom_file_path)
|
284
|
+
if not geom_file_path.exists():
|
285
|
+
raise FileNotFoundError(f"Geometry file not found: {geom_file_path}")
|
286
|
+
|
287
|
+
# Verify required columns exist
|
288
|
+
required_columns = ['Land Use Type', 'Mannings N Value']
|
289
|
+
if not all(col in mannings_df.columns for col in required_columns):
|
290
|
+
raise ValueError(f"DataFrame must contain columns: {required_columns}")
|
291
|
+
|
292
|
+
# Create a dictionary for easier lookups
|
293
|
+
update_dict = {}
|
294
|
+
for _, row in mannings_df.iterrows():
|
295
|
+
land_use = row['Land Use Type']
|
296
|
+
manning_value = row['Mannings N Value']
|
297
|
+
region_name = row.get('Region Name', None) # Optional column
|
298
|
+
|
299
|
+
if region_name:
|
300
|
+
if region_name not in update_dict:
|
301
|
+
update_dict[region_name] = {}
|
302
|
+
update_dict[region_name][land_use] = manning_value
|
303
|
+
else:
|
304
|
+
# Special key for updates that apply to all regions
|
305
|
+
if 'ALL_REGIONS' not in update_dict:
|
306
|
+
update_dict['ALL_REGIONS'] = {}
|
307
|
+
update_dict['ALL_REGIONS'][land_use] = manning_value
|
308
|
+
|
309
|
+
logger.info(f"Updating Manning's n values in geometry file: {geom_file_path}")
|
310
|
+
|
311
|
+
# Read the entire file
|
312
|
+
with open(geom_file_path, 'r') as file:
|
313
|
+
lines = file.readlines()
|
314
|
+
|
315
|
+
# Process the file line by line
|
316
|
+
modified_lines = []
|
317
|
+
current_region = None
|
318
|
+
in_table = False
|
319
|
+
table_start_index = -1
|
320
|
+
table_size = 0
|
321
|
+
|
322
|
+
i = 0
|
323
|
+
while i < len(lines):
|
324
|
+
line = lines[i]
|
325
|
+
modified_lines.append(line) # Add line by default, may modify later
|
326
|
+
|
327
|
+
if "LCMann Region Name=" in line:
|
328
|
+
current_region = line.split("=")[1].strip()
|
329
|
+
in_table = False
|
330
|
+
|
331
|
+
elif "LCMann Region Table=" in line:
|
332
|
+
in_table = True
|
333
|
+
table_start_index = len(modified_lines)
|
334
|
+
try:
|
335
|
+
table_size = int(line.split("=")[1].strip())
|
336
|
+
except ValueError:
|
337
|
+
logger.warning(f"Invalid table size at line: {line}")
|
338
|
+
table_size = 0
|
339
|
+
|
340
|
+
elif in_table and table_size > 0:
|
341
|
+
# We're inside a Manning's table
|
342
|
+
land_use_entry = line.strip()
|
343
|
+
if "," in land_use_entry:
|
344
|
+
parts = land_use_entry.rsplit(",", 1)
|
345
|
+
if len(parts) == 2:
|
346
|
+
land_use, _ = parts
|
347
|
+
|
348
|
+
# Check if we should update this entry
|
349
|
+
update_value = None
|
350
|
+
|
351
|
+
# First check region-specific updates
|
352
|
+
if current_region in update_dict and land_use in update_dict[current_region]:
|
353
|
+
update_value = update_dict[current_region][land_use]
|
354
|
+
|
355
|
+
# Then check global updates (ALL_REGIONS)
|
356
|
+
elif 'ALL_REGIONS' in update_dict and land_use in update_dict['ALL_REGIONS']:
|
357
|
+
update_value = update_dict['ALL_REGIONS'][land_use]
|
358
|
+
|
359
|
+
if update_value is not None:
|
360
|
+
# Replace the last entry in modified_lines with updated Manning's value
|
361
|
+
modified_lines[-1] = f"{land_use},{update_value}\n"
|
362
|
+
logger.debug(f"Updated '{land_use}' in region '{current_region}' to {update_value}")
|
363
|
+
|
364
|
+
# Decrement counter for table entries
|
365
|
+
table_size -= 1
|
366
|
+
if table_size == 0:
|
367
|
+
in_table = False
|
368
|
+
|
369
|
+
i += 1
|
370
|
+
|
371
|
+
# Write the file back
|
372
|
+
with open(geom_file_path, 'w') as file:
|
373
|
+
file.writelines(modified_lines)
|
374
|
+
|
375
|
+
logger.info(f"Successfully updated Manning's n values in geometry file: {geom_file_path}")
|
376
|
+
return True
|
377
|
+
|
378
|
+
|
137
379
|
|
138
380
|
|