ras-commander 0.35.0__tar.gz → 0.36.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.
Files changed (24) hide show
  1. {ras_commander-0.35.0/ras_commander.egg-info → ras_commander-0.36.0}/PKG-INFO +15 -11
  2. {ras_commander-0.35.0 → ras_commander-0.36.0}/README.md +12 -8
  3. ras_commander-0.36.0/ras_commander/RasCmdr.py +510 -0
  4. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander/RasExamples.py +113 -80
  5. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander/RasGeo.py +38 -28
  6. ras_commander-0.36.0/ras_commander/RasGpt.py +142 -0
  7. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander/RasHdf.py +170 -253
  8. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander/RasPlan.py +115 -166
  9. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander/RasPrj.py +212 -141
  10. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander/RasUnsteady.py +37 -22
  11. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander/RasUtils.py +98 -82
  12. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander/__init__.py +11 -13
  13. ras_commander-0.36.0/ras_commander/logging_config.py +80 -0
  14. {ras_commander-0.35.0 → ras_commander-0.36.0/ras_commander.egg-info}/PKG-INFO +15 -11
  15. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander.egg-info/SOURCES.txt +2 -1
  16. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander.egg-info/requires.txt +2 -2
  17. {ras_commander-0.35.0 → ras_commander-0.36.0}/setup.py +4 -4
  18. ras_commander-0.35.0/ras_commander/RasCmdr.py +0 -482
  19. ras_commander-0.35.0/ras_commander/_version.py +0 -16
  20. {ras_commander-0.35.0 → ras_commander-0.36.0}/LICENSE +0 -0
  21. {ras_commander-0.35.0 → ras_commander-0.36.0}/pyproject.toml +0 -0
  22. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander.egg-info/dependency_links.txt +0 -0
  23. {ras_commander-0.35.0 → ras_commander-0.36.0}/ras_commander.egg-info/top_level.txt +0 -0
  24. {ras_commander-0.35.0 → ras_commander-0.36.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ras-commander
3
- Version: 0.35.0
3
+ Version: 0.36.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
@@ -12,8 +12,8 @@ Classifier: Operating System :: OS Independent
12
12
  Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: pandas>=1.0.0
16
- Requires-Dist: numpy>=1.18.0
15
+ Requires-Dist: pandas>=2.0
16
+ Requires-Dist: numpy>=2.0
17
17
  Requires-Dist: h5py>=3.1.0
18
18
  Requires-Dist: requests>=2.25.0
19
19
  Requires-Dist: scipy>=1.5.0
@@ -33,14 +33,21 @@ Requires-Dist: twine>=3.3.0; extra == "dev"
33
33
  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
34
 
35
35
  ## Contributors:
36
- William Katzenmeyer, P.E., C.F.M. - billk@fenstermaker.com
36
+ William Katzenmeyer, P.E., C.F.M.
37
37
 
38
- Sean Micek, P.E., C.F.M. - smicek@fenstermaker.com
38
+ Sean Micek, P.E., C.F.M.
39
39
 
40
- Aaron Nichols, P.E., C.F.M. - anichols@fenstermaker.com
40
+ Aaron Nichols, P.E., C.F.M.
41
41
 
42
42
  (Additional Contributors Here)
43
43
 
44
+ ## Don't Ask Me, Ask ChatGPT!
45
+
46
+ 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.
47
+
48
+ 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.
49
+
50
+ 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
51
 
45
52
  ## Background
46
53
  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 +307,8 @@ Additionally, we would like to acknowledge the following notable contributions a
300
307
 
301
308
  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
309
 
303
- Xiaofeng Liu, Ph.D., P.E.
304
- Associate Professor
305
- Department of Civil and Environmental Engineering
306
- Institute of Computational and Data Sciences
307
- Penn State University
310
+ Xiaofeng Liu, Ph.D., P.E., Associate Professor, Department of Civil and Environmental Engineering
311
+ Institute of Computational and Data Sciences, Penn State University
308
312
 
309
313
  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
314
 
@@ -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. - billk@fenstermaker.com
6
+ William Katzenmeyer, P.E., C.F.M.
7
7
 
8
- Sean Micek, P.E., C.F.M. - smicek@fenstermaker.com
8
+ Sean Micek, P.E., C.F.M.
9
9
 
10
- Aaron Nichols, P.E., C.F.M. - anichols@fenstermaker.com
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
- Associate Professor
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 {}