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