ras-commander 0.35.0__tar.gz → 0.37.0__tar.gz
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-0.35.0/ras_commander.egg-info → ras_commander-0.37.0}/PKG-INFO +22 -19
- {ras_commander-0.35.0 → ras_commander-0.37.0}/README.md +12 -8
- ras_commander-0.37.0/ras_commander/RasCmdr.py +510 -0
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander/RasExamples.py +113 -80
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander/RasGeo.py +38 -28
- ras_commander-0.37.0/ras_commander/RasGpt.py +142 -0
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander/RasHdf.py +170 -253
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander/RasPlan.py +115 -166
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander/RasPrj.py +212 -141
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander/RasUnsteady.py +37 -22
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander/RasUtils.py +98 -82
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander/__init__.py +11 -13
- ras_commander-0.37.0/ras_commander/logging_config.py +80 -0
- {ras_commander-0.35.0 → ras_commander-0.37.0/ras_commander.egg-info}/PKG-INFO +22 -19
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander.egg-info/SOURCES.txt +2 -1
- ras_commander-0.37.0/ras_commander.egg-info/requires.txt +16 -0
- {ras_commander-0.35.0 → ras_commander-0.37.0}/setup.py +12 -12
- ras_commander-0.35.0/ras_commander/RasCmdr.py +0 -482
- ras_commander-0.35.0/ras_commander/_version.py +0 -16
- ras_commander-0.35.0/ras_commander.egg-info/requires.txt +0 -16
- {ras_commander-0.35.0 → ras_commander-0.37.0}/LICENSE +0 -0
- {ras_commander-0.35.0 → ras_commander-0.37.0}/pyproject.toml +0 -0
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander.egg-info/dependency_links.txt +0 -0
- {ras_commander-0.35.0 → ras_commander-0.37.0}/ras_commander.egg-info/top_level.txt +0 -0
- {ras_commander-0.35.0 → ras_commander-0.37.0}/setup.cfg +0 -0
@@ -1,19 +1,18 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ras-commander
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.37.0
|
4
4
|
Summary: A Python library for automating HEC-RAS operations
|
5
5
|
Home-page: https://github.com/billk-FM/ras-commander
|
6
6
|
Author: William M. Katzenmeyer
|
7
7
|
Author-email: billk@fenstermaker.com
|
8
|
-
Classifier: Programming Language :: Python :: 3.10
|
9
8
|
Classifier: Programming Language :: Python :: 3.11
|
10
9
|
Classifier: License :: OSI Approved :: MIT License
|
11
10
|
Classifier: Operating System :: OS Independent
|
12
|
-
Requires-Python: >=3.
|
11
|
+
Requires-Python: >=3.11
|
13
12
|
Description-Content-Type: text/markdown
|
14
13
|
License-File: LICENSE
|
15
|
-
Requires-Dist: pandas>=
|
16
|
-
Requires-Dist: numpy>=
|
14
|
+
Requires-Dist: pandas>=2.0
|
15
|
+
Requires-Dist: numpy>=2.0
|
17
16
|
Requires-Dist: h5py>=3.1.0
|
18
17
|
Requires-Dist: requests>=2.25.0
|
19
18
|
Requires-Dist: scipy>=1.5.0
|
@@ -21,26 +20,33 @@ Requires-Dist: matplotlib>=3.3.0
|
|
21
20
|
Requires-Dist: tqdm>=4.50.0
|
22
21
|
Requires-Dist: psutil>=5.7.0
|
23
22
|
Provides-Extra: dev
|
24
|
-
Requires-Dist: pytest>=
|
25
|
-
Requires-Dist: flake8>=
|
26
|
-
Requires-Dist: black>=
|
27
|
-
Requires-Dist: sphinx>=
|
28
|
-
Requires-Dist: sphinx-rtd-theme>=0.
|
29
|
-
Requires-Dist: twine>=
|
23
|
+
Requires-Dist: pytest>=8.3.3; extra == "dev"
|
24
|
+
Requires-Dist: flake8>=7.1.1; extra == "dev"
|
25
|
+
Requires-Dist: black>=24.8.0; extra == "dev"
|
26
|
+
Requires-Dist: sphinx>=7.4.7; extra == "dev"
|
27
|
+
Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == "dev"
|
28
|
+
Requires-Dist: twine>=5.1.1; extra == "dev"
|
30
29
|
|
31
30
|
# RAS Commander (ras-commander)
|
32
31
|
|
33
32
|
RAS Commander is a Python library for automating HEC-RAS operations, providing a set of tools to interact with HEC-RAS project files, execute simulations, and manage project data. This library is an evolution of the RASCommander 1.0 Python Notebook Application previously released under the [HEC-Commander tools repository](https://github.com/billk-FM/HEC-Commander).
|
34
33
|
|
35
34
|
## Contributors:
|
36
|
-
William Katzenmeyer, P.E., C.F.M.
|
35
|
+
William Katzenmeyer, P.E., C.F.M.
|
37
36
|
|
38
|
-
Sean Micek, P.E., C.F.M.
|
37
|
+
Sean Micek, P.E., C.F.M.
|
39
38
|
|
40
|
-
Aaron Nichols, P.E., C.F.M.
|
39
|
+
Aaron Nichols, P.E., C.F.M.
|
41
40
|
|
42
41
|
(Additional Contributors Here)
|
43
42
|
|
43
|
+
## Don't Ask Me, Ask ChatGPT!
|
44
|
+
|
45
|
+
Before you read any further, you can [chat directly with ChatGPT on this topic.](https://chatgpt.com/g/g-TZRPR3oAO-ras-commander-library-assistant) Ask it anything, and it will use its tools to answer your questions and help you learn. You can even upload your own plan, unsteady and HDF files to inspect and help determine how to automate your workflows or visualize your results.
|
46
|
+
|
47
|
+
There are also [AI Assistant Knowledge Bases](https://github.com/billk-FM/ras-commander/tree/main/ai_tools/assistant_knowledge_bases) with various versions available to directly use with large context LLM models such as Anthropic's Claude, Google Gemini and OpenAI's GPT4o and o1 models.
|
48
|
+
|
49
|
+
FUTURE: TEMPLATES are available to use with AI Assistant Notebooks to build your own automation tools. When used with large context models, these templates allow you to ask GPT to build a workflow from scratch to automate your projects.
|
44
50
|
|
45
51
|
## Background
|
46
52
|
The ras-commander library emerged from the initial test-bed of AI-driven coding represented by the HEC-Commander tools Python notebooks. These notebooks served as a proof of concept, demonstrating the value proposition of automating HEC-RAS operations. The transition from notebooks to a structured library aims to provide a more robust, maintainable, and extensible solution for water resources engineers.
|
@@ -300,11 +306,8 @@ Additionally, we would like to acknowledge the following notable contributions a
|
|
300
306
|
|
301
307
|
2. Attribution: The [`pyHMT2D`](https://github.com/psu-efd/pyHMT2D/) project by Xiaofeng Liu, which provided insights into HDF file handling methods for HEC-RAS outputs. Many of the functions in the [Ras_2D_Data.py](https://github.com/psu-efd/pyHMT2D/blob/main/pyHMT2D/Hydraulic_Models_Data/RAS_2D/RAS_2D_Data.py) file were adapted with AI for use in RAS Commander.
|
302
308
|
|
303
|
-
Xiaofeng Liu, Ph.D., P.E
|
304
|
-
|
305
|
-
Department of Civil and Environmental Engineering
|
306
|
-
Institute of Computational and Data Sciences
|
307
|
-
Penn State University
|
309
|
+
Xiaofeng Liu, Ph.D., P.E., Associate Professor, Department of Civil and Environmental Engineering
|
310
|
+
Institute of Computational and Data Sciences, Penn State University
|
308
311
|
|
309
312
|
These acknowledgments recognize the contributions and inspirations that have helped shape RAS Commander, ensuring proper attribution for the ideas and code that have influenced its development.
|
310
313
|
|
@@ -3,14 +3,21 @@
|
|
3
3
|
RAS Commander is a Python library for automating HEC-RAS operations, providing a set of tools to interact with HEC-RAS project files, execute simulations, and manage project data. This library is an evolution of the RASCommander 1.0 Python Notebook Application previously released under the [HEC-Commander tools repository](https://github.com/billk-FM/HEC-Commander).
|
4
4
|
|
5
5
|
## Contributors:
|
6
|
-
William Katzenmeyer, P.E., C.F.M.
|
6
|
+
William Katzenmeyer, P.E., C.F.M.
|
7
7
|
|
8
|
-
Sean Micek, P.E., C.F.M.
|
8
|
+
Sean Micek, P.E., C.F.M.
|
9
9
|
|
10
|
-
Aaron Nichols, P.E., C.F.M.
|
10
|
+
Aaron Nichols, P.E., C.F.M.
|
11
11
|
|
12
12
|
(Additional Contributors Here)
|
13
13
|
|
14
|
+
## Don't Ask Me, Ask ChatGPT!
|
15
|
+
|
16
|
+
Before you read any further, you can [chat directly with ChatGPT on this topic.](https://chatgpt.com/g/g-TZRPR3oAO-ras-commander-library-assistant) Ask it anything, and it will use its tools to answer your questions and help you learn. You can even upload your own plan, unsteady and HDF files to inspect and help determine how to automate your workflows or visualize your results.
|
17
|
+
|
18
|
+
There are also [AI Assistant Knowledge Bases](https://github.com/billk-FM/ras-commander/tree/main/ai_tools/assistant_knowledge_bases) with various versions available to directly use with large context LLM models such as Anthropic's Claude, Google Gemini and OpenAI's GPT4o and o1 models.
|
19
|
+
|
20
|
+
FUTURE: TEMPLATES are available to use with AI Assistant Notebooks to build your own automation tools. When used with large context models, these templates allow you to ask GPT to build a workflow from scratch to automate your projects.
|
14
21
|
|
15
22
|
## Background
|
16
23
|
The ras-commander library emerged from the initial test-bed of AI-driven coding represented by the HEC-Commander tools Python notebooks. These notebooks served as a proof of concept, demonstrating the value proposition of automating HEC-RAS operations. The transition from notebooks to a structured library aims to provide a more robust, maintainable, and extensible solution for water resources engineers.
|
@@ -270,11 +277,8 @@ Additionally, we would like to acknowledge the following notable contributions a
|
|
270
277
|
|
271
278
|
2. Attribution: The [`pyHMT2D`](https://github.com/psu-efd/pyHMT2D/) project by Xiaofeng Liu, which provided insights into HDF file handling methods for HEC-RAS outputs. Many of the functions in the [Ras_2D_Data.py](https://github.com/psu-efd/pyHMT2D/blob/main/pyHMT2D/Hydraulic_Models_Data/RAS_2D/RAS_2D_Data.py) file were adapted with AI for use in RAS Commander.
|
272
279
|
|
273
|
-
Xiaofeng Liu, Ph.D., P.E
|
274
|
-
|
275
|
-
Department of Civil and Environmental Engineering
|
276
|
-
Institute of Computational and Data Sciences
|
277
|
-
Penn State University
|
280
|
+
Xiaofeng Liu, Ph.D., P.E., Associate Professor, Department of Civil and Environmental Engineering
|
281
|
+
Institute of Computational and Data Sciences, Penn State University
|
278
282
|
|
279
283
|
These acknowledgments recognize the contributions and inspirations that have helped shape RAS Commander, ensuring proper attribution for the ideas and code that have influenced its development.
|
280
284
|
|
@@ -0,0 +1,510 @@
|
|
1
|
+
"""
|
2
|
+
RasCmdr - Execution operations for running HEC-RAS simulations
|
3
|
+
|
4
|
+
This module is part of the ras-commander library and uses a centralized logging configuration.
|
5
|
+
|
6
|
+
Logging Configuration:
|
7
|
+
- The logging is set up in the logging_config.py file.
|
8
|
+
- A @log_call decorator is available to automatically log function calls.
|
9
|
+
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
10
|
+
- Logs are written to both console and a rotating file handler.
|
11
|
+
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
12
|
+
- The default log level is INFO.
|
13
|
+
|
14
|
+
To use logging in this module:
|
15
|
+
1. Use the @log_call decorator for automatic function call logging.
|
16
|
+
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
17
|
+
|
18
|
+
Example:
|
19
|
+
@log_call
|
20
|
+
def my_function():
|
21
|
+
|
22
|
+
logger.debug("Additional debug information")
|
23
|
+
# Function logic here
|
24
|
+
"""
|
25
|
+
import os
|
26
|
+
import subprocess
|
27
|
+
import shutil
|
28
|
+
from pathlib import Path
|
29
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
30
|
+
from .RasPrj import ras, RasPrj, init_ras_project, get_ras_exe
|
31
|
+
from .RasPlan import RasPlan
|
32
|
+
from .RasGeo import RasGeo
|
33
|
+
from .RasUtils import RasUtils
|
34
|
+
import logging
|
35
|
+
import time
|
36
|
+
import queue
|
37
|
+
from threading import Thread, Lock
|
38
|
+
from typing import Union, List, Optional, Dict
|
39
|
+
from pathlib import Path
|
40
|
+
import shutil
|
41
|
+
import logging
|
42
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
43
|
+
from threading import Lock, Thread
|
44
|
+
from itertools import cycle
|
45
|
+
from ras_commander.RasPrj import RasPrj # Ensure RasPrj is imported
|
46
|
+
from threading import Lock, Thread, current_thread
|
47
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
48
|
+
from itertools import cycle
|
49
|
+
from typing import Union, List, Optional, Dict
|
50
|
+
from ras_commander.logging_config import get_logger, log_call
|
51
|
+
|
52
|
+
logger = get_logger(__name__)
|
53
|
+
|
54
|
+
# Module code starts here
|
55
|
+
|
56
|
+
# TODO: Future Enhancements
|
57
|
+
# 1. Alternate Run Mode for compute_plan and compute_parallel:
|
58
|
+
# - Use Powershell to execute HEC-RAS command
|
59
|
+
# - Hide RAS window and all child windows
|
60
|
+
# - Note: This mode may prevent execution if the plan has a popup
|
61
|
+
# - Intended for background runs or popup-free scenarios
|
62
|
+
# - Limit to non-commercial use
|
63
|
+
#
|
64
|
+
# 2. Implement compute_plan_remote:
|
65
|
+
# - Execute compute_plan on a remote machine via psexec
|
66
|
+
# - Use keyring package for secure credential storage
|
67
|
+
# - Implement psexec command for remote HEC-RAS execution
|
68
|
+
# - Create remote_worker objects to store machine details:
|
69
|
+
# (machine name, username, password, ras_exe_path, local folder path, etc.)
|
70
|
+
# - Develop RasRemote class for remote_worker management and abstractions
|
71
|
+
# - Implement compute_plan_remote in RasCmdr as a thin wrapper around RasRemote
|
72
|
+
# (similar to existing compute_plan functions but for remote execution)
|
73
|
+
|
74
|
+
|
75
|
+
class RasCmdr:
|
76
|
+
|
77
|
+
@staticmethod
|
78
|
+
@log_call
|
79
|
+
def compute_plan(
|
80
|
+
plan_number,
|
81
|
+
dest_folder=None,
|
82
|
+
ras_object=None,
|
83
|
+
clear_geompre=False,
|
84
|
+
num_cores=None,
|
85
|
+
overwrite_dest=False
|
86
|
+
):
|
87
|
+
"""
|
88
|
+
Execute a HEC-RAS plan.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
plan_number (str, Path): The plan number to execute (e.g., "01", "02") or the full path to the plan file.
|
92
|
+
dest_folder (str, Path, optional): Name of the folder or full path for computation.
|
93
|
+
If a string is provided, it will be created in the same parent directory as the project folder.
|
94
|
+
If a full path is provided, it will be used as is.
|
95
|
+
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
96
|
+
clear_geompre (bool, optional): Whether to clear geometry preprocessor files. Defaults to False.
|
97
|
+
num_cores (int, optional): Number of cores to use for the plan execution. If None, the current setting is not changed.
|
98
|
+
overwrite_dest (bool, optional): If True, overwrite the destination folder if it exists. Defaults to False.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
bool: True if the execution was successful, False otherwise.
|
102
|
+
|
103
|
+
Raises:
|
104
|
+
ValueError: If the specified dest_folder already exists and is not empty, and overwrite_dest is False.
|
105
|
+
"""
|
106
|
+
try:
|
107
|
+
ras_obj = ras_object if ras_object is not None else ras
|
108
|
+
logger.info(f"Using ras_object with project folder: {ras_obj.project_folder}")
|
109
|
+
ras_obj.check_initialized()
|
110
|
+
|
111
|
+
if dest_folder is not None:
|
112
|
+
dest_folder = Path(ras_obj.project_folder).parent / dest_folder if isinstance(dest_folder, str) else Path(dest_folder)
|
113
|
+
|
114
|
+
if dest_folder.exists():
|
115
|
+
if overwrite_dest:
|
116
|
+
shutil.rmtree(dest_folder)
|
117
|
+
logger.info(f"Destination folder '{dest_folder}' exists. Overwriting as per overwrite_dest=True.")
|
118
|
+
elif any(dest_folder.iterdir()):
|
119
|
+
error_msg = f"Destination folder '{dest_folder}' exists and is not empty. Use overwrite_dest=True to overwrite."
|
120
|
+
logger.error(error_msg)
|
121
|
+
raise ValueError(error_msg)
|
122
|
+
|
123
|
+
dest_folder.mkdir(parents=True, exist_ok=True)
|
124
|
+
shutil.copytree(ras_obj.project_folder, dest_folder, dirs_exist_ok=True)
|
125
|
+
logger.info(f"Copied project folder to destination: {dest_folder}")
|
126
|
+
|
127
|
+
compute_ras = RasPrj()
|
128
|
+
compute_ras.initialize(dest_folder, ras_obj.ras_exe_path)
|
129
|
+
compute_prj_path = compute_ras.prj_file
|
130
|
+
else:
|
131
|
+
compute_ras = ras_obj
|
132
|
+
compute_prj_path = ras_obj.prj_file
|
133
|
+
|
134
|
+
# Determine the plan path
|
135
|
+
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)
|
136
|
+
|
137
|
+
if not compute_prj_path or not compute_plan_path:
|
138
|
+
logger.error(f"Could not find project file or plan file for plan {plan_number}")
|
139
|
+
return False
|
140
|
+
|
141
|
+
# Clear geometry preprocessor files if requested
|
142
|
+
if clear_geompre:
|
143
|
+
try:
|
144
|
+
RasGeo.clear_geompre_files(compute_plan_path, ras_object=compute_ras)
|
145
|
+
logger.info(f"Cleared geometry preprocessor files for plan: {plan_number}")
|
146
|
+
except Exception as e:
|
147
|
+
logger.error(f"Error clearing geometry preprocessor files for plan {plan_number}: {str(e)}")
|
148
|
+
|
149
|
+
# Set the number of cores if specified
|
150
|
+
if num_cores is not None:
|
151
|
+
try:
|
152
|
+
RasPlan.set_num_cores(compute_plan_path, num_cores=num_cores, ras_object=compute_ras)
|
153
|
+
logger.info(f"Set number of cores to {num_cores} for plan: {plan_number}")
|
154
|
+
except Exception as e:
|
155
|
+
logger.error(f"Error setting number of cores for plan {plan_number}: {str(e)}")
|
156
|
+
|
157
|
+
# Prepare the command for HEC-RAS execution
|
158
|
+
cmd = f'"{compute_ras.ras_exe_path}" -c "{compute_prj_path}" "{compute_plan_path}"'
|
159
|
+
logger.info("Running HEC-RAS from the Command Line:")
|
160
|
+
logger.info(f"Running command: {cmd}")
|
161
|
+
|
162
|
+
# Execute the HEC-RAS command
|
163
|
+
start_time = time.time()
|
164
|
+
try:
|
165
|
+
subprocess.run(cmd, check=True, shell=True, capture_output=True, text=True)
|
166
|
+
end_time = time.time()
|
167
|
+
run_time = end_time - start_time
|
168
|
+
logger.info(f"HEC-RAS execution completed for plan: {plan_number}")
|
169
|
+
logger.info(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
|
170
|
+
return True
|
171
|
+
except subprocess.CalledProcessError as e:
|
172
|
+
end_time = time.time()
|
173
|
+
run_time = end_time - start_time
|
174
|
+
logger.error(f"Error running plan: {plan_number}")
|
175
|
+
logger.error(f"Error message: {e.output}")
|
176
|
+
logger.info(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
|
177
|
+
return False
|
178
|
+
except Exception as e:
|
179
|
+
logger.critical(f"Error in compute_plan: {str(e)}")
|
180
|
+
return False
|
181
|
+
finally:
|
182
|
+
# Update the RAS object's dataframes
|
183
|
+
if ras_obj:
|
184
|
+
ras_obj.plan_df = ras_obj.get_plan_entries()
|
185
|
+
ras_obj.geom_df = ras_obj.get_geom_entries()
|
186
|
+
ras_obj.flow_df = ras_obj.get_flow_entries()
|
187
|
+
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
188
|
+
|
189
|
+
|
190
|
+
|
191
|
+
@staticmethod
|
192
|
+
@log_call
|
193
|
+
@staticmethod
|
194
|
+
@log_call
|
195
|
+
def compute_parallel(
|
196
|
+
plan_number: Union[str, List[str], None] = None,
|
197
|
+
max_workers: int = 2,
|
198
|
+
num_cores: int = 2,
|
199
|
+
clear_geompre: bool = False,
|
200
|
+
ras_object: Optional['RasPrj'] = None,
|
201
|
+
dest_folder: Union[str, Path, None] = None,
|
202
|
+
overwrite_dest: bool = False
|
203
|
+
) -> Dict[str, bool]:
|
204
|
+
"""
|
205
|
+
Compute multiple HEC-RAS plans in parallel.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
plan_number (Union[str, List[str], None]): Plan number(s) to compute. If None, all plans are computed.
|
209
|
+
max_workers (int): Maximum number of parallel workers.
|
210
|
+
num_cores (int): Number of cores to use per plan computation.
|
211
|
+
clear_geompre (bool): Whether to clear geometry preprocessor files.
|
212
|
+
ras_object (Optional[RasPrj]): RAS project object. If None, uses global instance.
|
213
|
+
dest_folder (Union[str, Path, None]): Destination folder for computed results.
|
214
|
+
overwrite_dest (bool): Whether to overwrite existing destination folder.
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
Dict[str, bool]: Dictionary of plan numbers and their execution success status.
|
218
|
+
"""
|
219
|
+
try:
|
220
|
+
ras_obj = ras_object or ras
|
221
|
+
ras_obj.check_initialized()
|
222
|
+
|
223
|
+
project_folder = Path(ras_obj.project_folder)
|
224
|
+
|
225
|
+
if dest_folder is not None:
|
226
|
+
dest_folder_path = Path(dest_folder)
|
227
|
+
if dest_folder_path.exists():
|
228
|
+
if overwrite_dest:
|
229
|
+
shutil.rmtree(dest_folder_path)
|
230
|
+
logger.info(f"Destination folder '{dest_folder_path}' exists. Overwriting as per overwrite_dest=True.")
|
231
|
+
elif any(dest_folder_path.iterdir()):
|
232
|
+
error_msg = f"Destination folder '{dest_folder_path}' exists and is not empty. Use overwrite_dest=True to overwrite."
|
233
|
+
logger.error(error_msg)
|
234
|
+
raise ValueError(error_msg)
|
235
|
+
dest_folder_path.mkdir(parents=True, exist_ok=True)
|
236
|
+
shutil.copytree(project_folder, dest_folder_path, dirs_exist_ok=True)
|
237
|
+
logger.info(f"Copied project folder to destination: {dest_folder_path}")
|
238
|
+
project_folder = dest_folder_path
|
239
|
+
|
240
|
+
if plan_number:
|
241
|
+
if isinstance(plan_number, str):
|
242
|
+
plan_number = [plan_number]
|
243
|
+
ras_obj.plan_df = ras_obj.plan_df[ras_obj.plan_df['plan_number'].isin(plan_number)]
|
244
|
+
logger.info(f"Filtered plans to execute: {plan_number}")
|
245
|
+
|
246
|
+
num_plans = len(ras_obj.plan_df)
|
247
|
+
max_workers = min(max_workers, num_plans) if num_plans > 0 else 1
|
248
|
+
logger.info(f"Adjusted max_workers to {max_workers} based on the number of plans: {num_plans}")
|
249
|
+
|
250
|
+
worker_ras_objects = {}
|
251
|
+
for worker_id in range(1, max_workers + 1):
|
252
|
+
worker_folder = project_folder.parent / f"{project_folder.name} [Worker {worker_id}]"
|
253
|
+
if worker_folder.exists():
|
254
|
+
shutil.rmtree(worker_folder)
|
255
|
+
logger.info(f"Removed existing worker folder: {worker_folder}")
|
256
|
+
shutil.copytree(project_folder, worker_folder)
|
257
|
+
logger.info(f"Created worker folder: {worker_folder}")
|
258
|
+
|
259
|
+
try:
|
260
|
+
ras_instance = RasPrj()
|
261
|
+
worker_ras_instance = init_ras_project(
|
262
|
+
ras_project_folder=worker_folder,
|
263
|
+
ras_version=ras_obj.ras_exe_path,
|
264
|
+
ras_instance=ras_instance
|
265
|
+
)
|
266
|
+
worker_ras_objects[worker_id] = worker_ras_instance
|
267
|
+
except Exception as e:
|
268
|
+
logger.critical(f"Failed to initialize RAS project for worker {worker_id}: {str(e)}")
|
269
|
+
worker_ras_objects[worker_id] = None
|
270
|
+
|
271
|
+
worker_cycle = cycle(range(1, max_workers + 1))
|
272
|
+
plan_assignments = [(next(worker_cycle), plan_num) for plan_num in ras_obj.plan_df['plan_number']]
|
273
|
+
|
274
|
+
execution_results: Dict[str, bool] = {}
|
275
|
+
|
276
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
277
|
+
futures = [
|
278
|
+
executor.submit(
|
279
|
+
RasCmdr.compute_plan,
|
280
|
+
plan_num,
|
281
|
+
ras_object=worker_ras_objects[worker_id],
|
282
|
+
clear_geompre=clear_geompre,
|
283
|
+
num_cores=num_cores
|
284
|
+
)
|
285
|
+
for worker_id, plan_num in plan_assignments
|
286
|
+
]
|
287
|
+
|
288
|
+
for future, (worker_id, plan_num) in zip(as_completed(futures), plan_assignments):
|
289
|
+
try:
|
290
|
+
success = future.result()
|
291
|
+
execution_results[plan_num] = success
|
292
|
+
logger.info(f"Plan {plan_num} executed in worker {worker_id}: {'Successful' if success else 'Failed'}")
|
293
|
+
except Exception as e:
|
294
|
+
execution_results[plan_num] = False
|
295
|
+
logger.error(f"Plan {plan_num} failed in worker {worker_id}: {str(e)}")
|
296
|
+
|
297
|
+
final_dest_folder = dest_folder_path if dest_folder is not None else project_folder.parent / f"{project_folder.name} [Computed]"
|
298
|
+
final_dest_folder.mkdir(parents=True, exist_ok=True)
|
299
|
+
logger.info(f"Final destination for computed results: {final_dest_folder}")
|
300
|
+
|
301
|
+
for worker_ras in worker_ras_objects.values():
|
302
|
+
if worker_ras is None:
|
303
|
+
continue
|
304
|
+
worker_folder = Path(worker_ras.project_folder)
|
305
|
+
try:
|
306
|
+
for item in worker_folder.iterdir():
|
307
|
+
dest_path = final_dest_folder / item.name
|
308
|
+
if dest_path.exists():
|
309
|
+
if dest_path.is_dir():
|
310
|
+
shutil.rmtree(dest_path)
|
311
|
+
logger.debug(f"Removed existing directory at {dest_path}")
|
312
|
+
else:
|
313
|
+
dest_path.unlink()
|
314
|
+
logger.debug(f"Removed existing file at {dest_path}")
|
315
|
+
shutil.move(str(item), final_dest_folder)
|
316
|
+
logger.debug(f"Moved {item} to {final_dest_folder}")
|
317
|
+
shutil.rmtree(worker_folder)
|
318
|
+
logger.info(f"Removed worker folder: {worker_folder}")
|
319
|
+
except Exception as e:
|
320
|
+
logger.error(f"Error moving results from {worker_folder} to {final_dest_folder}: {str(e)}")
|
321
|
+
|
322
|
+
try:
|
323
|
+
final_dest_folder_ras_obj = RasPrj()
|
324
|
+
final_dest_folder_ras_obj = init_ras_project(
|
325
|
+
ras_project_folder=final_dest_folder,
|
326
|
+
ras_version=ras_obj.ras_exe_path,
|
327
|
+
ras_instance=final_dest_folder_ras_obj
|
328
|
+
)
|
329
|
+
final_dest_folder_ras_obj.check_initialized()
|
330
|
+
except Exception as e:
|
331
|
+
logger.critical(f"Failed to initialize RasPrj for final destination: {str(e)}")
|
332
|
+
|
333
|
+
logger.info("\nExecution Results:")
|
334
|
+
for plan_num, success in execution_results.items():
|
335
|
+
status = 'Successful' if success else 'Failed'
|
336
|
+
logger.info(f"Plan {plan_num}: {status}")
|
337
|
+
|
338
|
+
ras_obj = ras_object or ras
|
339
|
+
ras_obj.plan_df = ras_obj.get_plan_entries()
|
340
|
+
ras_obj.geom_df = ras_obj.get_geom_entries()
|
341
|
+
ras_obj.flow_df = ras_obj.get_flow_entries()
|
342
|
+
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
343
|
+
|
344
|
+
return execution_results
|
345
|
+
|
346
|
+
except Exception as e:
|
347
|
+
logger.critical(f"Error in compute_parallel: {str(e)}")
|
348
|
+
return {}
|
349
|
+
|
350
|
+
@staticmethod
|
351
|
+
@log_call
|
352
|
+
def compute_test_mode(
|
353
|
+
plan_number=None,
|
354
|
+
dest_folder_suffix="[Test]",
|
355
|
+
clear_geompre=False,
|
356
|
+
num_cores=None,
|
357
|
+
ras_object=None,
|
358
|
+
overwrite_dest=False
|
359
|
+
):
|
360
|
+
"""
|
361
|
+
Execute HEC-RAS plans in test mode. This is a re-creation of the HEC-RAS command line -test flag,
|
362
|
+
which does not work in recent versions of HEC-RAS.
|
363
|
+
|
364
|
+
As a special-purpose function that emulates the original -test flag, it operates differently than the
|
365
|
+
other two compute_ functions. Per the original HEC-RAS test flag, it creates a separate test folder,
|
366
|
+
copies the project there, and executes the specified plans in sequential order.
|
367
|
+
|
368
|
+
For most purposes, just copying a the project folder, initing that new folder, then running each plan
|
369
|
+
with compute_plan is a simpler and more flexible approach. This is shown in the examples provided
|
370
|
+
in the ras-commander library.
|
371
|
+
|
372
|
+
Args:
|
373
|
+
plan_number (str, list[str], optional): Plan number or list of plan numbers to execute.
|
374
|
+
If None, all plans will be executed. Default is None.
|
375
|
+
dest_folder_suffix (str, optional): Suffix to append to the test folder name to create dest_folder.
|
376
|
+
Defaults to "[Test]".
|
377
|
+
dest_folder is always created in the project folder's parent directory.
|
378
|
+
clear_geompre (bool, optional): Whether to clear geometry preprocessor files.
|
379
|
+
Defaults to False.
|
380
|
+
num_cores (int, optional): Maximum number of cores to use for each plan.
|
381
|
+
If None, the current setting is not changed. Default is None.
|
382
|
+
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
383
|
+
overwrite_dest (bool, optional): If True, overwrite the destination folder if it exists. Defaults to False.
|
384
|
+
|
385
|
+
Returns:
|
386
|
+
Dict[str, bool]: Dictionary of plan numbers and their execution success status.
|
387
|
+
|
388
|
+
Example:
|
389
|
+
Run all plans: RasCommander.compute_test_mode()
|
390
|
+
Run a specific plan: RasCommander.compute_test_mode(plan_number="01")
|
391
|
+
Run multiple plans: RasCommander.compute_test_mode(plan_number=["01", "03", "05"])
|
392
|
+
Run plans with a custom folder suffix: RasCommander.compute_test_mode(dest_folder_suffix="[TestRun]")
|
393
|
+
Run plans and clear geometry preprocessor files: RasCommander.compute_test_mode(clear_geompre=True)
|
394
|
+
Run plans with a specific number of cores: RasCommander.compute_test_mode(num_cores=4)
|
395
|
+
|
396
|
+
Notes:
|
397
|
+
- This function executes plans in a separate folder for isolated testing.
|
398
|
+
- If plan_number is not provided, all plans in the project will be executed.
|
399
|
+
- The function does not change the geometry preprocessor and IB tables settings.
|
400
|
+
- To force recomputing of geometry preprocessor and IB tables, use the clear_geompre=True option.
|
401
|
+
- Plans are executed sequentially.
|
402
|
+
- Because copying the project is implicit, only a dest_folder_suffix option is provided.
|
403
|
+
- For more flexible run management, use the compute_parallel or compute_sequential functions.
|
404
|
+
"""
|
405
|
+
try:
|
406
|
+
ras_obj = ras_object or ras
|
407
|
+
ras_obj.check_initialized()
|
408
|
+
|
409
|
+
logger.info("Starting the compute_test_mode...")
|
410
|
+
|
411
|
+
project_folder = Path(ras_obj.project_folder)
|
412
|
+
|
413
|
+
if not project_folder.exists():
|
414
|
+
logger.error(f"Project folder '{project_folder}' does not exist.")
|
415
|
+
return {}
|
416
|
+
|
417
|
+
compute_folder = project_folder.parent / f"{project_folder.name} {dest_folder_suffix}"
|
418
|
+
logger.info(f"Creating the test folder: {compute_folder}...")
|
419
|
+
|
420
|
+
if compute_folder.exists():
|
421
|
+
if overwrite_dest:
|
422
|
+
shutil.rmtree(compute_folder)
|
423
|
+
logger.info(f"Compute folder '{compute_folder}' exists. Overwriting as per overwrite_dest=True.")
|
424
|
+
elif any(compute_folder.iterdir()):
|
425
|
+
error_msg = (
|
426
|
+
f"Compute folder '{compute_folder}' exists and is not empty. "
|
427
|
+
"Use overwrite_dest=True to overwrite."
|
428
|
+
)
|
429
|
+
logger.error(error_msg)
|
430
|
+
raise ValueError(error_msg)
|
431
|
+
|
432
|
+
try:
|
433
|
+
shutil.copytree(project_folder, compute_folder)
|
434
|
+
logger.info(f"Copied project folder to compute folder: {compute_folder}")
|
435
|
+
except Exception as e:
|
436
|
+
logger.critical(f"Error occurred while copying project folder: {str(e)}")
|
437
|
+
return {}
|
438
|
+
|
439
|
+
try:
|
440
|
+
compute_ras = RasPrj()
|
441
|
+
compute_ras.initialize(compute_folder, ras_obj.ras_exe_path)
|
442
|
+
compute_prj_path = compute_ras.prj_file
|
443
|
+
logger.info(f"Initialized RAS project in compute folder: {compute_prj_path}")
|
444
|
+
except Exception as e:
|
445
|
+
logger.critical(f"Error initializing RAS project in compute folder: {str(e)}")
|
446
|
+
return {}
|
447
|
+
|
448
|
+
if not compute_prj_path:
|
449
|
+
logger.error("Project file not found.")
|
450
|
+
return {}
|
451
|
+
|
452
|
+
logger.info("Getting plan entries...")
|
453
|
+
try:
|
454
|
+
ras_compute_plan_entries = compute_ras.plan_df
|
455
|
+
logger.info("Retrieved plan entries successfully.")
|
456
|
+
except Exception as e:
|
457
|
+
logger.critical(f"Error retrieving plan entries: {str(e)}")
|
458
|
+
return {}
|
459
|
+
|
460
|
+
if plan_number:
|
461
|
+
if isinstance(plan_number, str):
|
462
|
+
plan_number = [plan_number]
|
463
|
+
ras_compute_plan_entries = ras_compute_plan_entries[
|
464
|
+
ras_compute_plan_entries['plan_number'].isin(plan_number)
|
465
|
+
]
|
466
|
+
logger.info(f"Filtered plans to execute: {plan_number}")
|
467
|
+
|
468
|
+
execution_results = {}
|
469
|
+
logger.info("Running selected plans sequentially...")
|
470
|
+
for _, plan in ras_compute_plan_entries.iterrows():
|
471
|
+
plan_number = plan["plan_number"]
|
472
|
+
start_time = time.time()
|
473
|
+
try:
|
474
|
+
success = RasCmdr.compute_plan(
|
475
|
+
plan_number,
|
476
|
+
ras_object=compute_ras,
|
477
|
+
clear_geompre=clear_geompre,
|
478
|
+
num_cores=num_cores
|
479
|
+
)
|
480
|
+
execution_results[plan_number] = success
|
481
|
+
if success:
|
482
|
+
logger.info(f"Successfully computed plan {plan_number}")
|
483
|
+
else:
|
484
|
+
logger.error(f"Failed to compute plan {plan_number}")
|
485
|
+
except Exception as e:
|
486
|
+
execution_results[plan_number] = False
|
487
|
+
logger.error(f"Error computing plan {plan_number}: {str(e)}")
|
488
|
+
finally:
|
489
|
+
end_time = time.time()
|
490
|
+
run_time = end_time - start_time
|
491
|
+
logger.info(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
|
492
|
+
|
493
|
+
logger.info("All selected plans have been executed.")
|
494
|
+
logger.info("compute_test_mode completed.")
|
495
|
+
|
496
|
+
logger.info("\nExecution Results:")
|
497
|
+
for plan_num, success in execution_results.items():
|
498
|
+
status = 'Successful' if success else 'Failed'
|
499
|
+
logger.info(f"Plan {plan_num}: {status}")
|
500
|
+
|
501
|
+
ras_obj.plan_df = ras_obj.get_plan_entries()
|
502
|
+
ras_obj.geom_df = ras_obj.get_geom_entries()
|
503
|
+
ras_obj.flow_df = ras_obj.get_flow_entries()
|
504
|
+
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
505
|
+
|
506
|
+
return execution_results
|
507
|
+
|
508
|
+
except Exception as e:
|
509
|
+
logger.critical(f"Error in compute_test_mode: {str(e)}")
|
510
|
+
return {}
|