ras-commander 0.1.6__py2.py3-none-any.whl → 0.21.0__py2.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/README.md +187 -0
- ras_commander/RasCommander.py +456 -0
- ras_commander/RasExamples.py +304 -0
- ras_commander/RasGeo.py +83 -0
- ras_commander/RasPlan.py +1216 -0
- ras_commander/RasPrj.py +400 -0
- ras_commander/RasUnsteady.py +53 -0
- ras_commander/RasUtils.py +283 -0
- ras_commander/__init__.py +33 -0
- ras_commander-0.21.0.dist-info/METADATA +342 -0
- ras_commander-0.21.0.dist-info/RECORD +14 -0
- {ras_commander-0.1.6.dist-info → ras_commander-0.21.0.dist-info}/WHEEL +1 -1
- ras_commander/_version.py +0 -16
- ras_commander/execution.py +0 -315
- ras_commander/file_operations.py +0 -173
- ras_commander/geometry_operations.py +0 -184
- ras_commander/plan_operations.py +0 -307
- ras_commander/project_config.py +0 -64
- ras_commander/project_init.py +0 -174
- ras_commander/project_management.py +0 -227
- ras_commander/project_setup.py +0 -15
- ras_commander/unsteady_operations.py +0 -172
- ras_commander/utilities.py +0 -195
- ras_commander-0.1.6.dist-info/METADATA +0 -133
- ras_commander-0.1.6.dist-info/RECORD +0 -17
- {ras_commander-0.1.6.dist-info → ras_commander-0.21.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.1.6.dist-info → ras_commander-0.21.0.dist-info}/top_level.txt +0 -0
ras_commander/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
"""
|
2
|
+
# Developer's README
|
3
|
+
|
4
|
+
These notes should be followed by any developer who wants to use this library orcontribute to this project.
|
5
|
+
|
6
|
+
-----
|
7
|
+
|
8
|
+
|
9
|
+
# Developer's README for ras_commander
|
10
|
+
|
11
|
+
## Project Overview
|
12
|
+
|
13
|
+
ras_commander is a Python library for automating HEC-RAS operations. It provides a set of classes and functions to interact with HEC-RAS project files, execute simulations, and manage project data.
|
14
|
+
|
15
|
+
## Project Structure
|
16
|
+
|
17
|
+
The library is organized into several key modules:
|
18
|
+
|
19
|
+
- `RasPrj.py`: Handles project initialization and manages project-level information.
|
20
|
+
- `RasCommander.py`: Manages execution of HEC-RAS simulations.
|
21
|
+
- `RasPlan.py`: Provides functions for modifying and updating plan files.
|
22
|
+
- `RasGeo.py`: Handles operations related to geometry files.
|
23
|
+
- `RasUnsteady.py`: Manages unsteady flow file operations.
|
24
|
+
- `RasUtils.py`: Contains utility functions for file operations and data management.
|
25
|
+
|
26
|
+
## Key Concepts
|
27
|
+
|
28
|
+
### RAS Instance Management
|
29
|
+
|
30
|
+
The library supports both a global `ras` instance and the ability to create multiple instances for different projects:
|
31
|
+
|
32
|
+
- Use the global `ras` instance for simple, single-project scenarios.
|
33
|
+
- Create multiple `RasPrj` instances for working with multiple projects simultaneously.
|
34
|
+
|
35
|
+
### Function Design
|
36
|
+
|
37
|
+
Most functions in the library follow this pattern:
|
38
|
+
|
39
|
+
```python
|
40
|
+
def some_function(param1, param2, ras_object=None):
|
41
|
+
ras_obj = ras_object or ras
|
42
|
+
ras_obj.check_initialized()
|
43
|
+
# Function implementation
|
44
|
+
```
|
45
|
+
|
46
|
+
This design allows for flexibility in using either the global instance or a specific project instance.
|
47
|
+
|
48
|
+
## ras_commander Best Practices
|
49
|
+
|
50
|
+
1. Always check if a project is initialized before performing operations:
|
51
|
+
```python
|
52
|
+
ras_obj.check_initialized()
|
53
|
+
```
|
54
|
+
|
55
|
+
2. Use the `ras_object` parameter in functions to specify which project instance to use.
|
56
|
+
|
57
|
+
3. For complex projects with multiple HEC-RAS folders, prefer passing explicit `ras_object` instances to functions for clarity.
|
58
|
+
|
59
|
+
4. Use type hints and descriptive variable names to improve code readability.
|
60
|
+
|
61
|
+
5. Handle exceptions appropriately, especially for file operations and HEC-RAS interactions.
|
62
|
+
|
63
|
+
6. When adding new functionality, consider its placement within the existing class structure.
|
64
|
+
|
65
|
+
7. Update the `__init__.py` file when adding new modules or significant functionality.
|
66
|
+
|
67
|
+
## Testing
|
68
|
+
|
69
|
+
- Write unit tests for all new functions and methods.
|
70
|
+
- Ensure tests cover both single-project and multi-project scenarios.
|
71
|
+
- Use the `unittest` framework for consistency with existing tests.
|
72
|
+
|
73
|
+
## Documentation
|
74
|
+
|
75
|
+
- Keep docstrings up-to-date with any changes to function signatures or behavior.
|
76
|
+
- Update the main README.md file when adding new features or changing existing functionality.
|
77
|
+
- Consider adding or updating example scripts in the `examples/` directory for new features.
|
78
|
+
- Build a notebook first! We have AI to help us integrate functions into the library once we have a working example.
|
79
|
+
|
80
|
+
|
81
|
+
## Performance Considerations
|
82
|
+
|
83
|
+
- For parallel execution of plans, refer to the "Benchmarking is All You Need" blog post in the HEC-Commander repository for guidance on optimal core usage.
|
84
|
+
|
85
|
+
## Abbreviations
|
86
|
+
|
87
|
+
Consistently use these abbreviations throughout the codebase:
|
88
|
+
|
89
|
+
- ras: HEC-RAS
|
90
|
+
- prj: Project
|
91
|
+
- geom: Geometry
|
92
|
+
- pre: Preprocessor
|
93
|
+
- geompre: Geometry Preprocessor
|
94
|
+
- num: Number
|
95
|
+
- init: Initialize
|
96
|
+
- XS: Cross Section
|
97
|
+
- DSS: Data Storage System
|
98
|
+
- GIS: Geographic Information System
|
99
|
+
- BC: Boundary Condition
|
100
|
+
- IC: Initial Condition
|
101
|
+
- TW: Tailwater
|
102
|
+
|
103
|
+
## Future Development
|
104
|
+
|
105
|
+
Refer to the "Future Development Roadmap" for planned enhancements and features to be implemented.
|
106
|
+
|
107
|
+
By following these guidelines, we can maintain consistency, readability, and reliability across the ras_commander library.
|
108
|
+
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
# Understanding and Using RAS Instances in ras_commander
|
131
|
+
|
132
|
+
The `RasPrj` class now supports both a global instance named `ras` and the ability to create multiple instances for different projects.
|
133
|
+
|
134
|
+
Key points about RAS instances:
|
135
|
+
|
136
|
+
1. **Global Instance**: A default global instance named `ras` is still available for backwards compatibility and simple use cases.
|
137
|
+
2. **Multiple Instances**: Users can create and manage multiple `RasPrj` instances for different projects.
|
138
|
+
3. **Flexible Function Calls**: Most functions now accept an optional `ras_object` parameter, allowing use of specific project instances.
|
139
|
+
4. **Consistent State**: Each instance maintains its own project state, ensuring data consistency within each project context.
|
140
|
+
|
141
|
+
## Using RAS Instances
|
142
|
+
|
143
|
+
### Global Instance
|
144
|
+
For simple, single-project scenarios:
|
145
|
+
|
146
|
+
```python
|
147
|
+
from ras_commander import ras, init_ras_project
|
148
|
+
|
149
|
+
# Initialize the global instance
|
150
|
+
init_ras_project("/path/to/project", "6.5")
|
151
|
+
|
152
|
+
# Use the global instance
|
153
|
+
print(f"Working with project: {ras.project_name}")
|
154
|
+
plan_file = ras.get_plan_path("01")
|
155
|
+
```
|
156
|
+
|
157
|
+
### Multiple Instances
|
158
|
+
For working with multiple projects:
|
159
|
+
|
160
|
+
```python
|
161
|
+
from ras_commander import RasPrj, init_ras_project
|
162
|
+
|
163
|
+
# Create and initialize separate instances
|
164
|
+
project1 = init_ras_project("/path/to/project1", "6.5")
|
165
|
+
project2 = init_ras_project("/path/to/project2", "6.5")
|
166
|
+
|
167
|
+
# Use specific instances in function calls
|
168
|
+
RasPlan.set_geom("01", "02", ras_object=project1)
|
169
|
+
RasPlan.set_geom("01", "03", ras_object=project2)
|
170
|
+
```
|
171
|
+
|
172
|
+
### Best Practices
|
173
|
+
1. Always check if a project is initialized before using:
|
174
|
+
```python
|
175
|
+
def my_function(ras_object=None):
|
176
|
+
ras_obj = ras_object or ras
|
177
|
+
ras_obj.check_initialized()
|
178
|
+
# Proceed with operations using ras_obj
|
179
|
+
```
|
180
|
+
|
181
|
+
2. Use the `ras_object` parameter in functions to specify which project instance to use.
|
182
|
+
|
183
|
+
3. For any advance usage with multiple projects, you shouldprefer passing explicit `ras_object` instances to functions for clarity and to avoid unintended use of the global instance.
|
184
|
+
|
185
|
+
By supporting both a global instance and multiple instances, ras_commander provides flexibility for various usage scenarios while maintaining simplicity for basic use cases.
|
186
|
+
|
187
|
+
"""
|
@@ -0,0 +1,456 @@
|
|
1
|
+
"""
|
2
|
+
Execution operations for running HEC-RAS simulations using subprocess.
|
3
|
+
Based on the HEC-Commander project's "Command Line is All You Need" approach, leveraging the -c compute flag to run HEC-RAS and orchestrating changes directly in the RAS input files to achieve automation outcomes.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import os
|
7
|
+
import subprocess
|
8
|
+
import shutil
|
9
|
+
from pathlib import Path
|
10
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
11
|
+
from .RasPrj import ras, RasPrj, init_ras_project, get_ras_exe
|
12
|
+
from .RasPlan import RasPlan
|
13
|
+
from .RasGeo import RasGeo
|
14
|
+
from .RasUtils import RasUtils
|
15
|
+
import subprocess
|
16
|
+
import os
|
17
|
+
import logging
|
18
|
+
import time
|
19
|
+
import pandas as pd
|
20
|
+
from threading import Thread, Lock
|
21
|
+
import queue
|
22
|
+
from pathlib import Path
|
23
|
+
import shutil
|
24
|
+
import queue
|
25
|
+
from threading import Thread, Lock
|
26
|
+
import time
|
27
|
+
|
28
|
+
|
29
|
+
class RasCommander:
|
30
|
+
@staticmethod
|
31
|
+
def compute_plan(
|
32
|
+
plan_number,
|
33
|
+
compute_folder=None,
|
34
|
+
ras_object=None
|
35
|
+
):
|
36
|
+
"""
|
37
|
+
Execute a HEC-RAS plan.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
plan_number (str, Path): The plan number to execute (e.g., "01", "02") or the full path to the plan file.
|
41
|
+
compute_folder (str, Path, optional): Name of the folder or full path for computation.
|
42
|
+
If a string is provided, it will be created in the same parent directory as the project folder.
|
43
|
+
If a full path is provided, it will be used as is.
|
44
|
+
If the compute_folder already exists, a ValueError will be raised to prevent overwriting.
|
45
|
+
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
bool: True if the execution was successful, False otherwise.
|
49
|
+
|
50
|
+
Raises:
|
51
|
+
ValueError: If the specified compute_folder already exists and is not empty.
|
52
|
+
|
53
|
+
Example:
|
54
|
+
# Execute plan "01" in the current project folder
|
55
|
+
RasCommander.compute_plan("01")
|
56
|
+
|
57
|
+
# Execute plan "02" in a new compute folder
|
58
|
+
RasCommander.compute_plan("02", compute_folder="ComputeRun1")
|
59
|
+
|
60
|
+
# Execute a specific plan file in a new compute folder
|
61
|
+
RasCommander.compute_plan(r"C:\path\to\plan.p01.hdf", compute_folder="ComputeRun2")
|
62
|
+
|
63
|
+
Notes:
|
64
|
+
When using a compute_folder:
|
65
|
+
- A new RasPrj object is created for the computation.
|
66
|
+
- The entire project is copied to the new folder before execution.
|
67
|
+
- Results will be stored in the new folder, preserving the original project.
|
68
|
+
"""
|
69
|
+
# Initialize RasPrj object with the default "ras" object if no specific object is provided
|
70
|
+
ras_obj = ras_object or ras
|
71
|
+
ras_obj.check_initialized()
|
72
|
+
|
73
|
+
# Determine the compute folder path and plan path
|
74
|
+
if compute_folder is not None:
|
75
|
+
compute_folder = Path(ras_obj.project_folder).parent / compute_folder if isinstance(compute_folder, str) else Path(compute_folder)
|
76
|
+
|
77
|
+
# Check if the compute folder exists and is empty
|
78
|
+
if compute_folder.exists() and any(compute_folder.iterdir()):
|
79
|
+
raise ValueError(f"Compute folder '{compute_folder}' exists and is not empty. Please ensure the compute folder is empty before proceeding.")
|
80
|
+
elif not compute_folder.exists():
|
81
|
+
shutil.copytree(ras_obj.project_folder, compute_folder)
|
82
|
+
|
83
|
+
# Initialize a new RAS project in the compute folder
|
84
|
+
compute_ras = RasPrj()
|
85
|
+
compute_ras.initialize(compute_folder, ras_obj.ras_exe_path)
|
86
|
+
compute_prj_path = compute_ras.prj_file
|
87
|
+
else:
|
88
|
+
compute_ras = ras_obj
|
89
|
+
compute_prj_path = ras_obj.prj_file
|
90
|
+
|
91
|
+
# Determine the plan path
|
92
|
+
compute_plan_path = Path(plan_number) if isinstance(plan_number, (str, Path)) and Path(plan_number).is_file() else RasPlan.get_plan_path(plan_number, compute_ras)
|
93
|
+
|
94
|
+
if not compute_prj_path or not compute_plan_path:
|
95
|
+
print(f"Error: Could not find project file or plan file for plan {plan_number}")
|
96
|
+
return False
|
97
|
+
|
98
|
+
# Prepare the command for HEC-RAS execution
|
99
|
+
cmd = f'"{compute_ras.ras_exe_path}" -c "{compute_prj_path}" "{compute_plan_path}"'
|
100
|
+
print("Running HEC-RAS from the Command Line:")
|
101
|
+
print(f"Running command: {cmd}")
|
102
|
+
|
103
|
+
# Execute the HEC-RAS command
|
104
|
+
start_time = time.time()
|
105
|
+
try:
|
106
|
+
subprocess.run(cmd, check=True, shell=True, capture_output=True, text=True)
|
107
|
+
end_time = time.time()
|
108
|
+
run_time = end_time - start_time
|
109
|
+
print(f"HEC-RAS execution completed for plan: {plan_number}")
|
110
|
+
print(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
|
111
|
+
return True
|
112
|
+
except subprocess.CalledProcessError as e:
|
113
|
+
end_time = time.time()
|
114
|
+
run_time = end_time - start_time
|
115
|
+
print(f"Error running plan: {plan_number}")
|
116
|
+
print(f"Error message: {e.output}")
|
117
|
+
print(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
|
118
|
+
return False
|
119
|
+
|
120
|
+
ras_obj = ras_object or ras
|
121
|
+
ras_obj.plan_df = ras_obj.get_plan_entries()
|
122
|
+
ras_obj.geom_df = ras_obj.get_geom_entries()
|
123
|
+
ras_obj.flow_df = ras_obj.get_flow_entries()
|
124
|
+
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
125
|
+
|
126
|
+
|
127
|
+
def compute_test_mode(
|
128
|
+
plan_numbers=None,
|
129
|
+
folder_suffix="[Test]",
|
130
|
+
clear_geompre=False,
|
131
|
+
max_cores=None,
|
132
|
+
ras_object=None
|
133
|
+
):
|
134
|
+
"""
|
135
|
+
Execute HEC-RAS plans in test mode.
|
136
|
+
|
137
|
+
This function creates a separate test folder, copies the project there, and executes the specified plans.
|
138
|
+
It allows for isolated testing without affecting the original project files.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
plan_numbers (list of str, optional): List of plan numbers to execute.
|
142
|
+
If None, all plans will be executed. Default is None.
|
143
|
+
folder_suffix (str, optional): Suffix to append to the test folder name.
|
144
|
+
Defaults to "[Test]".
|
145
|
+
clear_geompre (bool, optional): Whether to clear geometry preprocessor files.
|
146
|
+
Defaults to False.
|
147
|
+
max_cores (int, optional): Maximum number of cores to use for each plan.
|
148
|
+
If None, the current setting is not changed. Default is None.
|
149
|
+
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
150
|
+
|
151
|
+
Returns:
|
152
|
+
None
|
153
|
+
|
154
|
+
Example:
|
155
|
+
Run all plans: RasCommander.compute_test_mode()
|
156
|
+
Run specific plans: RasCommander.compute_test_mode(plan_numbers=["01", "03", "05"])
|
157
|
+
Run plans with a custom folder suffix: RasCommander.compute_test_mode(folder_suffix="[TestRun]")
|
158
|
+
Run plans and clear geometry preprocessor files: RasCommander.compute_test_mode(clear_geompre=True)
|
159
|
+
Run plans with a specific number of cores: RasCommander.compute_test_mode(max_cores=4)
|
160
|
+
|
161
|
+
Notes:
|
162
|
+
- This function executes plans in a separate folder for isolated testing.
|
163
|
+
- If plan_numbers is not provided, all plans in the project will be executed.
|
164
|
+
- The function does not change the geometry preprocessor and IB tables settings.
|
165
|
+
- To force recomputing of geometry preprocessor and IB tables, use the clear_geompre=True option.
|
166
|
+
- Plans are executed sequentially.
|
167
|
+
"""
|
168
|
+
|
169
|
+
# This line of code is used to initialize the RasPrj object with the default "ras" object if no specific object is provided.
|
170
|
+
ras_obj = ras_object or ras
|
171
|
+
# This line of code is used to check if the RasPrj object is initialized.
|
172
|
+
ras_obj.check_initialized()
|
173
|
+
|
174
|
+
print("Starting the compute_test_mode...")
|
175
|
+
|
176
|
+
# Use the project folder from the ras object
|
177
|
+
project_folder = ras_obj.project_folder
|
178
|
+
|
179
|
+
# Check if the project folder exists
|
180
|
+
if not project_folder.exists():
|
181
|
+
print(f"Error: Project folder '{project_folder}' does not exist.")
|
182
|
+
return
|
183
|
+
|
184
|
+
# Create test folder with the specified suffix in the same directory as the project folder
|
185
|
+
compute_folder = project_folder.parent / f"{project_folder.name} {folder_suffix}"
|
186
|
+
print(f"Creating the test folder: {compute_folder}...")
|
187
|
+
|
188
|
+
# Check if the compute folder exists and is empty
|
189
|
+
if compute_folder.exists():
|
190
|
+
if any(compute_folder.iterdir()):
|
191
|
+
raise ValueError(
|
192
|
+
f"Compute folder '{compute_folder}' exists and is not empty. "
|
193
|
+
"Please ensure the compute folder is empty before proceeding."
|
194
|
+
)
|
195
|
+
else:
|
196
|
+
try:
|
197
|
+
shutil.copytree(project_folder, compute_folder)
|
198
|
+
except FileNotFoundError:
|
199
|
+
print(f"Error: Unable to copy project folder. Source folder '{project_folder}' not found.")
|
200
|
+
return
|
201
|
+
except PermissionError:
|
202
|
+
print(f"Error: Permission denied when trying to create or copy to '{compute_folder}'.")
|
203
|
+
return
|
204
|
+
except Exception as e:
|
205
|
+
print(f"Error occurred while copying project folder: {str(e)}")
|
206
|
+
return
|
207
|
+
|
208
|
+
# Initialize a new RAS project in the compute folder
|
209
|
+
try:
|
210
|
+
compute_ras = RasPrj()
|
211
|
+
compute_ras.initialize(compute_folder, ras_obj.ras_exe_path)
|
212
|
+
compute_prj_path = compute_ras.prj_file
|
213
|
+
except Exception as e:
|
214
|
+
print(f"Error initializing RAS project in compute folder: {str(e)}")
|
215
|
+
return
|
216
|
+
|
217
|
+
if not compute_prj_path:
|
218
|
+
print("Project file not found.")
|
219
|
+
return
|
220
|
+
print(f"Project file found: {compute_prj_path}")
|
221
|
+
|
222
|
+
# Get plan entries
|
223
|
+
print("Getting plan entries...")
|
224
|
+
try:
|
225
|
+
ras_compute_plan_entries = compute_ras.plan_df
|
226
|
+
print("Retrieved plan entries successfully.")
|
227
|
+
except Exception as e:
|
228
|
+
print(f"Error retrieving plan entries: {str(e)}")
|
229
|
+
return
|
230
|
+
|
231
|
+
# Filter plans if plan_numbers is provided
|
232
|
+
if plan_numbers:
|
233
|
+
ras_compute_plan_entries = ras_compute_plan_entries[
|
234
|
+
ras_compute_plan_entries['plan_number'].isin(plan_numbers)
|
235
|
+
]
|
236
|
+
print(f"Filtered plans to execute: {plan_numbers}")
|
237
|
+
|
238
|
+
# Optimize by iterating once to clear geompre files and set max cores
|
239
|
+
if clear_geompre or max_cores is not None:
|
240
|
+
print("Processing geometry preprocessor files and core settings...")
|
241
|
+
for plan_file in ras_compute_plan_entries['full_path']:
|
242
|
+
if clear_geompre:
|
243
|
+
try:
|
244
|
+
RasGeo.clear_geompre_files(plan_file)
|
245
|
+
print(f"Cleared geometry preprocessor files for {plan_file}")
|
246
|
+
except Exception as e:
|
247
|
+
print(f"Error clearing geometry preprocessor files for {plan_file}: {str(e)}")
|
248
|
+
if max_cores is not None:
|
249
|
+
try:
|
250
|
+
RasPlan.set_num_cores(plan_file, num_cores=max_cores)
|
251
|
+
print(f"Set max cores to {max_cores} for {plan_file}")
|
252
|
+
except Exception as e:
|
253
|
+
print(f"Error setting max cores for {plan_file}: {str(e)}")
|
254
|
+
print("Geometry preprocessor files and core settings processed successfully.")
|
255
|
+
|
256
|
+
# Run plans sequentially
|
257
|
+
print("Running selected plans sequentially...")
|
258
|
+
for _, plan in ras_compute_plan_entries.iterrows():
|
259
|
+
plan_number = plan["plan_number"]
|
260
|
+
start_time = time.time()
|
261
|
+
try:
|
262
|
+
RasCommander.compute_plan(plan_number, ras_object=compute_ras)
|
263
|
+
except Exception as e:
|
264
|
+
print(f"Error computing plan {plan_number}: {str(e)}")
|
265
|
+
end_time = time.time()
|
266
|
+
run_time = end_time - start_time
|
267
|
+
print(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
|
268
|
+
|
269
|
+
print("All selected plans have been executed.")
|
270
|
+
print("compute_test_mode completed.")
|
271
|
+
|
272
|
+
ras_obj = ras_object or ras
|
273
|
+
ras_obj.plan_df = ras_obj.get_plan_entries()
|
274
|
+
ras_obj.geom_df = ras_obj.get_geom_entries()
|
275
|
+
ras_obj.flow_df = ras_obj.get_flow_entries()
|
276
|
+
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
277
|
+
|
278
|
+
@staticmethod
|
279
|
+
def compute_parallel(
|
280
|
+
plan_numbers: list[str] | None = None,
|
281
|
+
max_workers: int = 2,
|
282
|
+
cores_per_run: int = 2,
|
283
|
+
ras_object: RasPrj | None = None,
|
284
|
+
dest_folder: str | Path | None = None
|
285
|
+
) -> dict[str, bool]:
|
286
|
+
"""
|
287
|
+
Execute HEC-RAS plans in parallel using multiple worker threads.
|
288
|
+
|
289
|
+
This function creates separate worker folders, copies the project to each, and executes the specified plans
|
290
|
+
in parallel. It allows for isolated and concurrent execution of multiple plans.
|
291
|
+
|
292
|
+
Args:
|
293
|
+
plan_numbers (list[str], optional): List of plan numbers to execute.
|
294
|
+
If None, all plans will be executed. Default is None.
|
295
|
+
max_workers (int, optional): Maximum number of worker threads to use.
|
296
|
+
Default is 2.
|
297
|
+
cores_per_run (int, optional): Number of cores to use for each plan execution.
|
298
|
+
Default is 2.
|
299
|
+
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
300
|
+
dest_folder (str | Path, optional): Destination folder for the final computed results.
|
301
|
+
If None, results will be stored in a "[Computed]" folder next to the original project.
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
dict[str, bool]: A dictionary with plan numbers as keys and boolean values indicating success (True) or failure (False).
|
305
|
+
|
306
|
+
Raises:
|
307
|
+
ValueError: If the destination folder exists and is not empty.
|
308
|
+
FileNotFoundError: If a plan file is not found.
|
309
|
+
|
310
|
+
Notes:
|
311
|
+
- This function creates separate folders for each worker to ensure isolated execution.
|
312
|
+
- Each worker uses its own RAS object to prevent conflicts.
|
313
|
+
- Plans are distributed among workers using a queue to ensure efficient parallel processing.
|
314
|
+
- The function automatically handles cleanup and consolidation of results after execution.
|
315
|
+
|
316
|
+
Revision Notes:
|
317
|
+
- Removed redundant variable initializations.
|
318
|
+
- Streamlined worker folder creation and RAS object initialization.
|
319
|
+
- Optimized the consolidation of results from worker folders.
|
320
|
+
- Removed debug print statements for cleaner execution logs.
|
321
|
+
"""
|
322
|
+
ras_obj = ras_object or ras
|
323
|
+
ras_obj.check_initialized()
|
324
|
+
|
325
|
+
project_folder = ras_obj.project_folder
|
326
|
+
|
327
|
+
if dest_folder is not None:
|
328
|
+
dest_folder_path = Path(dest_folder)
|
329
|
+
if dest_folder_path.exists():
|
330
|
+
raise ValueError(
|
331
|
+
f"\nError: Destination folder already exists: '{dest_folder_path}'\n"
|
332
|
+
f"To prevent accidental overwriting of results, this operation cannot proceed.\n"
|
333
|
+
f"Please take one of the following actions:\n"
|
334
|
+
f"1. Delete the folder manually and run the operation again.\n"
|
335
|
+
f"2. Use a different destination folder name.\n"
|
336
|
+
f"3. Programmatically delete the folder before calling compute_parallel, like this:\n"
|
337
|
+
f" if Path('{dest_folder_path}').exists():\n"
|
338
|
+
f" shutil.rmtree('{dest_folder_path}')\n"
|
339
|
+
f"This safety measure ensures that you don't inadvertently overwrite existing results."
|
340
|
+
)
|
341
|
+
shutil.copytree(project_folder, dest_folder_path)
|
342
|
+
project_folder = dest_folder_path # Update project_folder to the new destination
|
343
|
+
|
344
|
+
if plan_numbers:
|
345
|
+
if isinstance(plan_numbers, str):
|
346
|
+
plan_numbers = [plan_numbers]
|
347
|
+
ras_obj.plan_df = ras_obj.plan_df[ras_obj.plan_df['plan_number'].isin(plan_numbers)]
|
348
|
+
|
349
|
+
num_plans = len(ras_obj.plan_df)
|
350
|
+
max_workers = min(max_workers, num_plans) if num_plans > 0 else 1
|
351
|
+
print(f"Adjusted max_workers to {max_workers} based on the number of plans: {num_plans}")
|
352
|
+
|
353
|
+
# Clean up existing worker folders
|
354
|
+
for worker_id in range(1, max_workers + 1):
|
355
|
+
worker_folder = project_folder.parent / f"{project_folder.name} [Worker {worker_id}]"
|
356
|
+
if worker_folder.exists():
|
357
|
+
shutil.rmtree(worker_folder)
|
358
|
+
print(f"Removed existing worker folder: {worker_folder}")
|
359
|
+
|
360
|
+
# Create worker folders and initialize RAS objects
|
361
|
+
worker_ras_objects = {}
|
362
|
+
for worker_id in range(1, max_workers + 1):
|
363
|
+
worker_folder = project_folder.parent / f"{project_folder.name} [Worker {worker_id}]"
|
364
|
+
shutil.copytree(project_folder, worker_folder)
|
365
|
+
|
366
|
+
worker_ras_instance = RasPrj()
|
367
|
+
worker_ras_instance = init_ras_project(
|
368
|
+
ras_project_folder=worker_folder,
|
369
|
+
ras_version=ras_obj.ras_exe_path,
|
370
|
+
ras_instance=worker_ras_instance
|
371
|
+
)
|
372
|
+
worker_ras_objects[worker_id] = worker_ras_instance
|
373
|
+
|
374
|
+
# Prepare plan queue with plan numbers
|
375
|
+
plan_queue = queue.Queue()
|
376
|
+
for plan_number in ras_obj.plan_df['plan_number']:
|
377
|
+
plan_queue.put(plan_number)
|
378
|
+
|
379
|
+
# Initialize results dictionary and thread locks
|
380
|
+
execution_results: dict[str, bool] = {}
|
381
|
+
results_lock = Lock()
|
382
|
+
queue_lock = Lock()
|
383
|
+
|
384
|
+
def worker_thread(worker_id: int):
|
385
|
+
worker_ras_obj = worker_ras_objects[worker_id]
|
386
|
+
while True:
|
387
|
+
with queue_lock:
|
388
|
+
if plan_queue.empty():
|
389
|
+
break
|
390
|
+
plan_number = plan_queue.get()
|
391
|
+
|
392
|
+
try:
|
393
|
+
plan_path = RasPlan.get_plan_path(plan_number, ras_object=worker_ras_obj)
|
394
|
+
if not plan_path:
|
395
|
+
raise FileNotFoundError(f"Plan file not found: {plan_number}")
|
396
|
+
|
397
|
+
RasPlan.set_num_cores(plan_number, cores_per_run, ras_object=worker_ras_obj)
|
398
|
+
|
399
|
+
print(f"Worker {worker_id} executing plan {plan_number}")
|
400
|
+
|
401
|
+
success = RasCommander.compute_plan(plan_number, ras_object=worker_ras_obj)
|
402
|
+
|
403
|
+
with results_lock:
|
404
|
+
execution_results[plan_number] = success
|
405
|
+
print(f"Completed: Plan {plan_number} in worker {worker_id}")
|
406
|
+
except Exception as e:
|
407
|
+
with results_lock:
|
408
|
+
execution_results[plan_number] = False
|
409
|
+
print(f"Failed: Plan {plan_number} in worker {worker_id}. Error: {str(e)}")
|
410
|
+
|
411
|
+
# Start worker threads
|
412
|
+
worker_threads = []
|
413
|
+
for worker_id in range(1, max_workers + 1):
|
414
|
+
worker_ras_instance = worker_ras_objects[worker_id]
|
415
|
+
worker_ras_instance.plan_df = worker_ras_instance.get_plan_entries()
|
416
|
+
worker_ras_instance.geom_df = worker_ras_instance.get_geom_entries()
|
417
|
+
worker_ras_instance.flow_df = worker_ras_instance.get_flow_entries()
|
418
|
+
worker_ras_instance.unsteady_df = worker_ras_instance.get_unsteady_entries()
|
419
|
+
|
420
|
+
thread = Thread(target=worker_thread, args=(worker_id,))
|
421
|
+
thread.start()
|
422
|
+
worker_threads.append(thread)
|
423
|
+
print(f"Started worker thread {worker_id}")
|
424
|
+
|
425
|
+
# Wait for all threads to complete
|
426
|
+
for worker_id, thread in enumerate(worker_threads, 1):
|
427
|
+
thread.join()
|
428
|
+
print(f"Worker thread {worker_id} has completed.")
|
429
|
+
|
430
|
+
# Consolidate results
|
431
|
+
final_dest_folder = dest_folder_path if dest_folder is not None else project_folder.parent / f"{project_folder.name} [Computed]"
|
432
|
+
final_dest_folder.mkdir(exist_ok=True)
|
433
|
+
print(f"Final destination for computed results: {final_dest_folder}")
|
434
|
+
|
435
|
+
for worker_id, worker_ras in worker_ras_objects.items():
|
436
|
+
worker_folder = worker_ras.project_folder
|
437
|
+
try:
|
438
|
+
for item in worker_folder.iterdir():
|
439
|
+
dest_path = final_dest_folder / item.name
|
440
|
+
if dest_path.exists():
|
441
|
+
if dest_path.is_dir():
|
442
|
+
shutil.rmtree(dest_path)
|
443
|
+
else:
|
444
|
+
dest_path.unlink()
|
445
|
+
shutil.move(str(item), final_dest_folder)
|
446
|
+
shutil.rmtree(worker_folder)
|
447
|
+
print(f"Moved results and removed worker folder: {worker_folder}")
|
448
|
+
except Exception as e:
|
449
|
+
print(f"Error moving results from {worker_folder} to {final_dest_folder}: {str(e)}")
|
450
|
+
|
451
|
+
# Print execution results for each plan
|
452
|
+
print("\nExecution Results:")
|
453
|
+
for plan_number, success in execution_results.items():
|
454
|
+
print(f"Plan {plan_number}: {'Successful' if success else 'Failed'}")
|
455
|
+
|
456
|
+
return execution_results
|