ras-commander 0.34.0__py3-none-any.whl → 0.36.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ras_commander/RasCmdr.py CHANGED
@@ -1,8 +1,27 @@
1
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
- """
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.
5
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
+ """
6
25
  import os
7
26
  import subprocess
8
27
  import shutil
@@ -28,25 +47,35 @@ from threading import Lock, Thread, current_thread
28
47
  from concurrent.futures import ThreadPoolExecutor, as_completed
29
48
  from itertools import cycle
30
49
  from typing import Union, List, Optional, Dict
31
-
32
-
33
- # Configure logging
34
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
35
-
36
- # TO DO:
37
- # 1. Alternate Run Mode for compute_plan and compute_parallel: Using Powershell to execute the HEC-RAS command and hide the RAS window and all child windows.
38
- # If this is implemented, and the plan has a popup, then the plan will not execute. This is a deal breaker for many scenarios, and should only be used
39
- # as a special option for those who don't want to deal with the popups, or want to run in the background. This option should be limited to non-commercial use.
40
- # 2. Implment compute_plan_remote to go along with compute_plan. This will be a compute_plan that is run on a remote machine via a psexec command.
41
- # First, we will use the keyring package to securely store the remote machine username and password.
42
- # Second, we will implement the psexec command to execute the HEC-RAS command on the remote machine.
43
- # Each machine will need to be initialized as a remote_worker object, which will store the machine name, username, password, ras_exe_path, local folder path and other relevant info.
44
- # A separate RasRemote class will be created to handle the creation of the remote_worker objects and the necessary abstractions.
45
- # The compute_plan_remote function will live in RasCmdr, and will be a thin abstraction above the RasRemote class, since the functions will be simliar to the existing compute_plan functions, but specific to remote execution.
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)
46
73
 
47
74
 
48
75
  class RasCmdr:
76
+
49
77
  @staticmethod
78
+ @log_call
50
79
  def compute_plan(
51
80
  plan_number,
52
81
  dest_folder=None,
@@ -74,251 +103,252 @@ class RasCmdr:
74
103
  Raises:
75
104
  ValueError: If the specified dest_folder already exists and is not empty, and overwrite_dest is False.
76
105
  """
77
- ras_obj = ras_object or ras
78
- ras_obj.check_initialized()
79
-
80
- if dest_folder is not None:
81
- dest_folder = Path(ras_obj.project_folder).parent / dest_folder if isinstance(dest_folder, str) else Path(dest_folder)
82
-
83
- if dest_folder.exists():
84
- if overwrite_dest:
85
- shutil.rmtree(dest_folder)
86
- logging.info(f"Destination folder '{dest_folder}' exists. Overwriting as per overwrite_dest=True.")
87
- elif any(dest_folder.iterdir()):
88
- error_msg = f"Destination folder '{dest_folder}' exists and is not empty. Use overwrite_dest=True to overwrite."
89
- logging.error(error_msg)
90
- raise ValueError(error_msg)
91
-
92
- dest_folder.mkdir(parents=True, exist_ok=True)
93
- shutil.copytree(ras_obj.project_folder, dest_folder, dirs_exist_ok=True)
94
- logging.info(f"Copied project folder to destination: {dest_folder}")
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()
95
110
 
96
- compute_ras = RasPrj()
97
- compute_ras.initialize(dest_folder, ras_obj.ras_exe_path)
98
- compute_prj_path = compute_ras.prj_file
99
- else:
100
- compute_ras = ras_obj
101
- compute_prj_path = ras_obj.prj_file
102
-
103
- # Determine the plan path
104
- 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)
105
-
106
- if not compute_prj_path or not compute_plan_path:
107
- logging.error(f"Could not find project file or plan file for plan {plan_number}")
108
- return False
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
109
133
 
110
- # Clear geometry preprocessor files if requested
111
- if clear_geompre:
112
- try:
113
- RasGeo.clear_geompre_files(compute_plan_path, ras_object=compute_ras)
114
- logging.info(f"Cleared geometry preprocessor files for plan: {plan_number}")
115
- except Exception as e:
116
- logging.error(f"Error clearing geometry preprocessor files for plan {plan_number}: {str(e)}")
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)
117
136
 
118
- # Set the number of cores if specified
119
- if num_cores is not None:
120
- try:
121
- RasPlan.set_num_cores(compute_plan_path, num_cores=num_cores, ras_object=compute_ras)
122
- logging.info(f"Set number of cores to {num_cores} for plan: {plan_number}")
123
- except Exception as e:
124
- logging.error(f"Error setting number of cores for plan {plan_number}: {str(e)}")
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
125
140
 
126
- # Prepare the command for HEC-RAS execution
127
- cmd = f'"{compute_ras.ras_exe_path}" -c "{compute_prj_path}" "{compute_plan_path}"'
128
- logging.info("Running HEC-RAS from the Command Line:")
129
- logging.info(f"Running command: {cmd}")
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)}")
130
148
 
131
- # Execute the HEC-RAS command
132
- start_time = time.time()
133
- try:
134
- subprocess.run(cmd, check=True, shell=True, capture_output=True, text=True)
135
- end_time = time.time()
136
- run_time = end_time - start_time
137
- logging.info(f"HEC-RAS execution completed for plan: {plan_number}")
138
- logging.info(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
139
- return True
140
- except subprocess.CalledProcessError as e:
141
- end_time = time.time()
142
- run_time = end_time - start_time
143
- logging.error(f"Error running plan: {plan_number}")
144
- logging.error(f"Error message: {e.output}")
145
- logging.info(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
146
- return False
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}")
147
161
 
148
- ras_obj = ras_object or ras
149
- ras_obj.plan_df = ras_obj.get_plan_entries()
150
- ras_obj.geom_df = ras_obj.get_geom_entries()
151
- ras_obj.flow_df = ras_obj.get_flow_entries()
152
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
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()
153
188
 
154
189
 
155
190
 
156
191
  @staticmethod
192
+ @log_call
193
+ @staticmethod
194
+ @log_call
157
195
  def compute_parallel(
158
196
  plan_number: Union[str, List[str], None] = None,
159
197
  max_workers: int = 2,
160
198
  num_cores: int = 2,
161
199
  clear_geompre: bool = False,
162
- ras_object: Optional['RasPrj'] = None, # Type hinting as string to avoid NameError
200
+ ras_object: Optional['RasPrj'] = None,
163
201
  dest_folder: Union[str, Path, None] = None,
164
202
  overwrite_dest: bool = False
165
203
  ) -> Dict[str, bool]:
166
204
  """
167
- [Docstring remains unchanged]
168
- """
169
- ras_obj = ras_object or ras # Assuming 'ras' is a global RasPrj instance
170
- ras_obj.check_initialized()
205
+ Compute multiple HEC-RAS plans in parallel.
171
206
 
172
- project_folder = Path(ras_obj.project_folder)
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.
173
215
 
174
- if dest_folder is not None:
175
- dest_folder_path = Path(dest_folder)
176
- if dest_folder_path.exists():
177
- if overwrite_dest:
178
- shutil.rmtree(dest_folder_path)
179
- logging.info(f"Destination folder '{dest_folder_path}' exists. Overwriting as per overwrite_dest=True.")
180
- elif any(dest_folder_path.iterdir()):
181
- error_msg = f"Destination folder '{dest_folder_path}' exists and is not empty. Use overwrite_dest=True to overwrite."
182
- logging.error(error_msg)
183
- raise ValueError(error_msg)
184
- dest_folder_path.mkdir(parents=True, exist_ok=True)
185
- shutil.copytree(project_folder, dest_folder_path, dirs_exist_ok=True)
186
- logging.info(f"Copied project folder to destination: {dest_folder_path}")
187
- project_folder = dest_folder_path
188
-
189
- if plan_number:
190
- if isinstance(plan_number, str):
191
- plan_number = [plan_number]
192
- ras_obj.plan_df = ras_obj.plan_df[ras_obj.plan_df['plan_number'].isin(plan_number)]
193
- logging.info(f"Filtered plans to execute: {plan_number}")
194
-
195
- num_plans = len(ras_obj.plan_df)
196
- max_workers = min(max_workers, num_plans) if num_plans > 0 else 1
197
- logging.info(f"Adjusted max_workers to {max_workers} based on the number of plans: {num_plans}")
198
-
199
- # Clean up existing worker folders and create new ones
200
- worker_ras_objects = {}
201
- for worker_id in range(1, max_workers + 1):
202
- worker_folder = project_folder.parent / f"{project_folder.name} [Worker {worker_id}]"
203
- if worker_folder.exists():
204
- shutil.rmtree(worker_folder)
205
- logging.info(f"Removed existing worker folder: {worker_folder}")
206
- shutil.copytree(project_folder, worker_folder)
207
- logging.info(f"Created worker folder: {worker_folder}")
208
-
209
- # Instantiate RasPrj properly
210
- ras_instance = RasPrj() # Add necessary parameters if required
211
- worker_ras_instance = init_ras_project(
212
- ras_project_folder=worker_folder,
213
- ras_version=ras_obj.ras_exe_path,
214
- ras_instance=ras_instance # Pass the instance instead of a string
215
- )
216
- worker_ras_objects[worker_id] = worker_ras_instance
217
-
218
- # Distribute plans among workers in a round-robin fashion
219
- worker_cycle = cycle(range(1, max_workers + 1))
220
- plan_assignments = [(next(worker_cycle), plan_num) for plan_num in ras_obj.plan_df['plan_number']]
221
-
222
- # Initialize ThreadPoolExecutor without tracking individual plan success
223
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
224
- # Submit all plan executions to the executor
225
- futures = [
226
- executor.submit(
227
- RasCmdr.compute_plan,
228
- plan_num,
229
- ras_object=worker_ras_objects[worker_id],
230
- clear_geompre=clear_geompre,
231
- num_cores=num_cores
232
- )
233
- for worker_id, plan_num in plan_assignments
234
- ]
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}")
235
258
 
236
- # Optionally, you can log when each plan starts and completes
237
- for future, (worker_id, plan_num) in zip(as_completed(futures), plan_assignments):
238
259
  try:
239
- future.result() # We don't need the success flag here
240
- logging.info(f"Plan {plan_num} executed in worker {worker_id}")
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
241
267
  except Exception as e:
242
- logging.error(f"Plan {plan_num} failed in worker {worker_id}: {str(e)}")
243
- # Depending on requirements, you might want to handle retries or mark these plans differently
244
-
245
- # Consolidate results
246
- final_dest_folder = dest_folder_path if dest_folder is not None else project_folder.parent / f"{project_folder.name} [Computed]"
247
- final_dest_folder.mkdir(parents=True, exist_ok=True)
248
- logging.info(f"Final destination for computed results: {final_dest_folder}")
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)}")
249
321
 
250
- for worker_ras in worker_ras_objects.values():
251
- worker_folder = Path(worker_ras.project_folder)
252
322
  try:
253
- for item in worker_folder.iterdir():
254
- dest_path = final_dest_folder / item.name
255
- if dest_path.exists():
256
- if dest_path.is_dir():
257
- shutil.rmtree(dest_path)
258
- logging.debug(f"Removed existing directory at {dest_path}")
259
- else:
260
- dest_path.unlink()
261
- logging.debug(f"Removed existing file at {dest_path}")
262
- shutil.move(str(item), final_dest_folder)
263
- logging.debug(f"Moved {item} to {final_dest_folder}")
264
- shutil.rmtree(worker_folder)
265
- logging.info(f"Removed worker folder: {worker_folder}")
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()
266
330
  except Exception as e:
267
- logging.error(f"Error moving results from {worker_folder} to {final_dest_folder}: {str(e)}")
331
+ logger.critical(f"Failed to initialize RasPrj for final destination: {str(e)}")
268
332
 
269
- # Initialize a new RasPrj object for the final destination
270
- try:
271
- # Create a new RasPrj instance
272
- final_dest_folder_ras_obj = RasPrj()
273
-
274
- # Initialize it using init_ras_project
275
- final_dest_folder_ras_obj = init_ras_project(
276
- ras_project_folder=final_dest_folder,
277
- ras_version=ras_obj.ras_exe_path,
278
- ras_instance=final_dest_folder_ras_obj
279
- )
280
-
281
- # Now we can check if it's initialized
282
- final_dest_folder_ras_obj.check_initialized()
283
- except Exception as e:
284
- logging.error(f"Failed to initialize RasPrj for final destination: {str(e)}")
285
- raise
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}")
286
337
 
287
- # Retrieve plan entries and check for HDF results
288
- try:
289
- plan_entries = final_dest_folder_ras_obj.get_prj_entries('Plan')
290
- except Exception as e:
291
- logging.error(f"Failed to retrieve plan entries from final RasPrj: {str(e)}")
292
- raise
293
-
294
- execution_results: Dict[str, bool] = {}
295
- for _, row in ras_obj.plan_df.iterrows():
296
- plan_num = row['plan_number']
297
- # Find the corresponding entry in plan_entries
298
- entry = plan_entries[plan_entries['plan_number'] == plan_num]
299
- if not entry.empty:
300
- hdf_path = entry.iloc[0].get('HDF_Results_Path')
301
- success = hdf_path is not None and Path(hdf_path).exists()
302
- else:
303
- success = False
304
- execution_results[plan_num] = success
305
-
306
- # Print execution results for each plan
307
- logging.info("\nExecution Results:")
308
- for plan_num, success in execution_results.items():
309
- status = 'Successful' if success else 'Failed'
310
- logging.info(f"Plan {plan_num}: {status} \n(HDF_Results_Path: {hdf_path})")
311
-
312
- ras_obj = ras_object or ras
313
- ras_obj.plan_df = ras_obj.get_plan_entries()
314
- ras_obj.geom_df = ras_obj.get_geom_entries()
315
- ras_obj.flow_df = ras_obj.get_flow_entries()
316
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
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()
317
343
 
318
- return execution_results
319
-
344
+ return execution_results
345
+
346
+ except Exception as e:
347
+ logger.critical(f"Error in compute_parallel: {str(e)}")
348
+ return {}
320
349
 
321
350
  @staticmethod
351
+ @log_call
322
352
  def compute_test_mode(
323
353
  plan_number=None,
324
354
  dest_folder_suffix="[Test]",
@@ -328,11 +358,11 @@ class RasCmdr:
328
358
  overwrite_dest=False
329
359
  ):
330
360
  """
331
- Execute HEC-RAS plans in test mode. This is a re-creation of the HEC-RAS command line -test flag,
361
+ Execute HEC-RAS plans in test mode. This is a re-creation of the HEC-RAS command line -test flag,
332
362
  which does not work in recent versions of HEC-RAS.
333
363
 
334
364
  As a special-purpose function that emulates the original -test flag, it operates differently than the
335
- other two compute_ functions. Per the original HEC-RAS test flag, it creates a separate test folder,
365
+ other two compute_ functions. Per the original HEC-RAS test flag, it creates a separate test folder,
336
366
  copies the project there, and executes the specified plans in sequential order.
337
367
 
338
368
  For most purposes, just copying a the project folder, initing that new folder, then running each plan
@@ -353,7 +383,7 @@ class RasCmdr:
353
383
  overwrite_dest (bool, optional): If True, overwrite the destination folder if it exists. Defaults to False.
354
384
 
355
385
  Returns:
356
- None
386
+ Dict[str, bool]: Dictionary of plan numbers and their execution success status.
357
387
 
358
388
  Example:
359
389
  Run all plans: RasCommander.compute_test_mode()
@@ -372,110 +402,109 @@ class RasCmdr:
372
402
  - Because copying the project is implicit, only a dest_folder_suffix option is provided.
373
403
  - For more flexible run management, use the compute_parallel or compute_sequential functions.
374
404
  """
375
-
376
- # This line of code is used to initialize the RasPrj object with the default "ras" object if no specific object is provided.
377
- ras_obj = ras_object or ras
378
- # This line of code is used to check if the RasPrj object is initialized.
379
- ras_obj.check_initialized()
380
-
381
- logging.info("Starting the compute_test_mode...")
382
-
383
- # Use the project folder from the ras object
384
- project_folder = ras_obj.project_folder
385
-
386
- # Check if the project folder exists
387
- if not project_folder.exists():
388
- logging.error(f"Project folder '{project_folder}' does not exist.")
389
- return
390
-
391
- # Create test folder with the specified suffix in the same directory as the project folder
392
- compute_folder = project_folder.parent / f"{project_folder.name} {dest_folder_suffix}"
393
- logging.info(f"Creating the test folder: {compute_folder}...")
394
-
395
- # Check if the compute folder exists and is empty
396
- if compute_folder.exists():
397
- if overwrite_dest:
398
- shutil.rmtree(compute_folder)
399
- logging.info(f"Compute folder '{compute_folder}' exists. Overwriting as per overwrite_dest=True.")
400
- elif any(compute_folder.iterdir()):
401
- error_msg = (
402
- f"Compute folder '{compute_folder}' exists and is not empty. "
403
- "Use overwrite_dest=True to overwrite."
404
- )
405
- logging.error(error_msg)
406
- raise ValueError(error_msg)
407
- else:
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
+
408
432
  try:
409
433
  shutil.copytree(project_folder, compute_folder)
410
- logging.info(f"Copied project folder to compute folder: {compute_folder}")
411
- except FileNotFoundError:
412
- logging.error(f"Unable to copy project folder. Source folder '{project_folder}' not found.")
413
- return
414
- except PermissionError:
415
- logging.error(f"Permission denied when trying to create or copy to '{compute_folder}'.")
416
- return
434
+ logger.info(f"Copied project folder to compute folder: {compute_folder}")
417
435
  except Exception as e:
418
- logging.error(f"Error occurred while copying project folder: {str(e)}")
419
- return
436
+ logger.critical(f"Error occurred while copying project folder: {str(e)}")
437
+ return {}
420
438
 
421
- # Initialize a new RAS project in the compute folder
422
- try:
423
- compute_ras = RasPrj()
424
- compute_ras.initialize(compute_folder, ras_obj.ras_exe_path)
425
- compute_prj_path = compute_ras.prj_file
426
- logging.info(f"Initialized RAS project in compute folder: {compute_prj_path}")
427
- except Exception as e:
428
- logging.error(f"Error initializing RAS project in compute folder: {str(e)}")
429
- return
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 {}
430
447
 
431
- if not compute_prj_path:
432
- logging.error("Project file not found.")
433
- return
448
+ if not compute_prj_path:
449
+ logger.error("Project file not found.")
450
+ return {}
434
451
 
435
- # Get plan entries
436
- logging.info("Getting plan entries...")
437
- try:
438
- ras_compute_plan_entries = compute_ras.plan_df
439
- logging.info("Retrieved plan entries successfully.")
440
- except Exception as e:
441
- logging.error(f"Error retrieving plan entries: {str(e)}")
442
- return
443
-
444
- if plan_number:
445
- if isinstance(plan_number, str):
446
- plan_number = [plan_number]
447
- ras_compute_plan_entries = ras_compute_plan_entries[
448
- ras_compute_plan_entries['plan_number'].isin(plan_number)
449
- ]
450
- logging.info(f"Filtered plans to execute: {plan_number}")
451
-
452
- logging.info("Running selected plans sequentially...")
453
- for _, plan in ras_compute_plan_entries.iterrows():
454
- plan_number = plan["plan_number"]
455
- start_time = time.time()
452
+ logger.info("Getting plan entries...")
456
453
  try:
457
- success = RasCmdr.compute_plan(
458
- plan_number,
459
- ras_object=compute_ras,
460
- clear_geompre=clear_geompre,
461
- num_cores=num_cores
462
- )
463
- if success:
464
- logging.info(f"Successfully computed plan {plan_number}")
465
- else:
466
- logging.error(f"Failed to compute plan {plan_number}")
454
+ ras_compute_plan_entries = compute_ras.plan_df
455
+ logger.info("Retrieved plan entries successfully.")
467
456
  except Exception as e:
468
- logging.error(f"Error computing plan {plan_number}: {str(e)}")
469
- end_time = time.time()
470
- run_time = end_time - start_time
471
- logging.info(f"Total run time for plan {plan_number}: {run_time:.2f} seconds")
472
-
473
- logging.info("All selected plans have been executed.")
474
- logging.info("compute_test_mode completed.")
475
-
476
- ras_obj = ras_object or ras
477
- ras_obj.plan_df = ras_obj.get_plan_entries()
478
- ras_obj.geom_df = ras_obj.get_geom_entries()
479
- ras_obj.flow_df = ras_obj.get_flow_entries()
480
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
481
-
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 {}