ras-commander 0.65.0__py3-none-any.whl → 0.67.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/RasPlan.py CHANGED
@@ -1,1469 +1,1488 @@
1
- """
2
- RasPlan - Operations for handling plan files in HEC-RAS projects
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
- 3. Obtain the logger using: logger = logging.getLogger(__name__)
18
-
19
- Example:
20
- @log_call
21
- def my_function():
22
- logger = logging.getLogger(__name__)
23
- logger.debug("Additional debug information")
24
- # Function logic here
25
-
26
-
27
- -----
28
-
29
- All of the methods in this class are static and are designed to be used without instantiation.
30
-
31
- List of Functions in RasPlan:
32
- - set_geom(): Set the geometry for a specified plan
33
- - set_steady(): Apply a steady flow file to a plan file
34
- - set_unsteady(): Apply an unsteady flow file to a plan file
35
- - set_num_cores(): Update the maximum number of cores to use
36
- - set_geom_preprocessor(): Update geometry preprocessor settings
37
- - clone_plan(): Create a new plan file based on a template
38
- - clone_unsteady(): Copy unsteady flow files from a template
39
- - clone_steady(): Copy steady flow files from a template
40
- - clone_geom(): Copy geometry files from a template
41
- - get_next_number(): Determine the next available number from a list
42
- - get_plan_value(): Retrieve a specific value from a plan file
43
- - get_results_path(): Get the results file path for a plan
44
- - get_plan_path(): Get the full path for a plan number
45
- - get_flow_path(): Get the full path for a flow number
46
- - get_unsteady_path(): Get the full path for an unsteady number
47
- - get_geom_path(): Get the full path for a geometry number
48
- - update_run_flags(): Update various run flags in a plan file
49
- - update_plan_intervals(): Update computation and output intervals
50
- - update_plan_description(): Update the description in a plan file
51
- - read_plan_description(): Read the description from a plan file
52
- - update_simulation_date(): Update simulation start and end dates
53
- - get_shortid(): Get the Short Identifier from a plan file
54
- - set_shortid(): Set the Short Identifier in a plan file
55
- - get_plan_title(): Get the Plan Title from a plan file
56
- - set_plan_title(): Set the Plan Title in a plan file
57
-
58
-
59
- """
60
- import os
61
- import re
62
- import logging
63
- from pathlib import Path
64
- import shutil
65
- from typing import Union, Optional
66
- import pandas as pd
67
- from .RasPrj import RasPrj, ras
68
- from .RasUtils import RasUtils
69
- from pathlib import Path
70
- from typing import Union, Any
71
- from datetime import datetime
72
-
73
- import logging
74
- import re
75
- from .LoggingConfig import get_logger
76
- from .Decorators import log_call
77
-
78
- logger = get_logger(__name__)
79
-
80
- class RasPlan:
81
- """
82
- A class for operations on HEC-RAS plan files.
83
- """
84
-
85
- @staticmethod
86
- @log_call
87
- def set_geom(plan_number: Union[str, int], new_geom: Union[str, int], ras_object=None) -> pd.DataFrame:
88
- """
89
- Set the geometry for the specified plan.
90
-
91
- Parameters:
92
- plan_number (Union[str, int]): The plan number to update.
93
- new_geom (Union[str, int]): The new geometry number to set.
94
- ras_object: An optional RAS object instance.
95
-
96
- Returns:
97
- pd.DataFrame: The updated geometry DataFrame.
98
-
99
- Example:
100
- updated_geom_df = RasPlan.set_geom('02', '03')
101
-
102
- Note:
103
- This function updates the ras object's dataframes after modifying the project structure.
104
- """
105
- ras_obj = ras_object or ras
106
- ras_obj.check_initialized()
107
-
108
- plan_number = str(plan_number).zfill(2)
109
- new_geom = str(new_geom).zfill(2)
110
-
111
- # Update all dataframes
112
- ras_obj.plan_df = ras_obj.get_plan_entries()
113
- ras_obj.geom_df = ras_obj.get_geom_entries()
114
- ras_obj.flow_df = ras_obj.get_flow_entries()
115
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
116
-
117
- if new_geom not in ras_obj.geom_df['geom_number'].values:
118
- logger.error(f"Geometry {new_geom} not found in project.")
119
- raise ValueError(f"Geometry {new_geom} not found in project.")
120
-
121
- # Update all geometry-related columns
122
- mask = ras_obj.plan_df['plan_number'] == plan_number
123
- ras_obj.plan_df.loc[mask, 'geometry_number'] = new_geom
124
- ras_obj.plan_df.loc[mask, 'Geom File'] = new_geom
125
- geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom}"
126
- ras_obj.plan_df.loc[mask, 'Geom Path'] = str(geom_path)
127
-
128
- # Update project file and reinitialize
129
- RasUtils.update_file(ras_obj.prj_file, RasPlan._update_geom_in_file, plan_number, new_geom)
130
- ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
131
-
132
- return ras_obj.plan_df
133
-
134
- @staticmethod
135
- def _update_geom_in_file(lines, plan_number, new_geom):
136
- plan_pattern = re.compile(rf"^Plan File=p{plan_number}", re.IGNORECASE)
137
- geom_pattern = re.compile(r"^Geom File=g\d+", re.IGNORECASE)
138
-
139
- for i, line in enumerate(lines):
140
- if plan_pattern.match(line):
141
- for j in range(i+1, len(lines)):
142
- if geom_pattern.match(lines[j]):
143
- lines[j] = f"Geom File=g{new_geom}\n"
144
- logger.info(f"Updated Geom File in project file to g{new_geom} for plan {plan_number}")
145
- break
146
- break
147
- return lines
148
-
149
- @staticmethod
150
- @log_call
151
- def set_steady(plan_number: str, new_steady_flow_number: str, ras_object=None):
152
- """
153
- Apply a steady flow file to a plan file.
154
-
155
- Parameters:
156
- plan_number (str): Plan number (e.g., '02')
157
- new_steady_flow_number (str): Steady flow number to apply (e.g., '01')
158
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
159
-
160
- Returns:
161
- None
162
-
163
- Raises:
164
- ValueError: If the specified steady flow number is not found in the project file
165
- FileNotFoundError: If the specified plan file is not found
166
-
167
- Example:
168
- >>> RasPlan.set_steady('02', '01')
169
-
170
- Note:
171
- This function updates the ras object's dataframes after modifying the project structure.
172
- """
173
- ras_obj = ras_object or ras
174
- ras_obj.check_initialized()
175
-
176
- ras_obj.flow_df = ras_obj.get_flow_entries()
177
-
178
- if new_steady_flow_number not in ras_obj.flow_df['flow_number'].values:
179
- raise ValueError(f"Steady flow number {new_steady_flow_number} not found in project file.")
180
-
181
- plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
182
- if not plan_file_path:
183
- raise FileNotFoundError(f"Plan file not found: {plan_number}")
184
-
185
- RasUtils.update_file(plan_file_path, RasPlan._update_steady_in_file, new_steady_flow_number)
186
-
187
- # Update dataframes and additional columns
188
- ras_obj.plan_df = ras_obj.get_plan_entries()
189
- mask = ras_obj.plan_df['plan_number'] == plan_number
190
- flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_steady_flow_number}"
191
- ras_obj.plan_df.loc[mask, 'Flow File'] = new_steady_flow_number
192
- ras_obj.plan_df.loc[mask, 'Flow Path'] = str(flow_path)
193
- ras_obj.plan_df.loc[mask, 'unsteady_number'] = None
194
-
195
- # Update remaining dataframes
196
- ras_obj.geom_df = ras_obj.get_geom_entries()
197
- ras_obj.flow_df = ras_obj.get_flow_entries()
198
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
199
-
200
- @staticmethod
201
- def _update_steady_in_file(lines, new_steady_flow_number):
202
- return [f"Flow File=f{new_steady_flow_number}\n" if line.startswith("Flow File=f") else line for line in lines]
203
-
204
- @staticmethod
205
- @log_call
206
- def set_unsteady(plan_number: str, new_unsteady_flow_number: str, ras_object=None):
207
- """
208
- Apply an unsteady flow file to a plan file.
209
-
210
- Parameters:
211
- plan_number (str): Plan number (e.g., '04')
212
- new_unsteady_flow_number (str): Unsteady flow number to apply (e.g., '01')
213
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
214
-
215
- Returns:
216
- None
217
-
218
- Raises:
219
- ValueError: If the specified unsteady number is not found in the project file
220
- FileNotFoundError: If the specified plan file is not found
221
-
222
- Example:
223
- >>> RasPlan.set_unsteady('04', '01')
224
-
225
- Note:
226
- This function updates the ras object's dataframes after modifying the project structure.
227
- """
228
- ras_obj = ras_object or ras
229
- ras_obj.check_initialized()
230
-
231
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
232
-
233
- if new_unsteady_flow_number not in ras_obj.unsteady_df['unsteady_number'].values:
234
- raise ValueError(f"Unsteady number {new_unsteady_flow_number} not found in project file.")
235
-
236
- plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
237
- if not plan_file_path:
238
- raise FileNotFoundError(f"Plan file not found: {plan_number}")
239
-
240
- try:
241
- RasUtils.update_file(plan_file_path, lambda lines: [
242
- f"Flow File=u{new_unsteady_flow_number}\n" if line.startswith("Flow File=") else line
243
- for line in lines
244
- ])
245
-
246
- # Update dataframes and additional columns
247
- ras_obj.plan_df = ras_obj.get_plan_entries()
248
- mask = ras_obj.plan_df['plan_number'] == plan_number
249
- flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_flow_number}"
250
- ras_obj.plan_df.loc[mask, 'Flow File'] = new_unsteady_flow_number
251
- ras_obj.plan_df.loc[mask, 'Flow Path'] = str(flow_path)
252
- ras_obj.plan_df.loc[mask, 'unsteady_number'] = new_unsteady_flow_number
253
-
254
- # Update remaining dataframes
255
- ras_obj.geom_df = ras_obj.get_geom_entries()
256
- ras_obj.flow_df = ras_obj.get_flow_entries()
257
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
258
-
259
- except Exception as e:
260
- raise Exception(f"Failed to update unsteady flow file: {e}")
261
-
262
- @staticmethod
263
- @log_call
264
- def set_num_cores(plan_number, num_cores, ras_object=None):
265
- """
266
- Update the maximum number of cores to use in the HEC-RAS plan file.
267
-
268
- Parameters:
269
- plan_number (str): Plan number (e.g., '02') or full path to the plan file
270
- num_cores (int): Maximum number of cores to use
271
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
272
-
273
- Returns:
274
- None
275
-
276
- Number of cores is controlled by the following parameters in the plan file corresponding to 1D, 2D, Pipe Systems and Pump Stations:
277
- UNET D1 Cores=
278
- UNET D2 Cores=
279
- PS Cores=
280
-
281
- Where a value of "0" is used for "All Available" cores, and values of 1 or more are used to specify the number of cores to use.
282
- For complex 1D/2D models with pipe systems, a more complex approach may be needed to optimize performance. (Suggest writing a custom function based on this code).
283
- This function simply sets the "num_cores" parameter for ALL instances of the above parameters in the plan file.
284
-
285
-
286
- Notes on setting num_cores in HEC-RAS:
287
- The recommended setting for num_cores is 2 (most efficient) to 8 (most performant)
288
- More details in the HEC-Commander Repository Blog "Benchmarking is All You Need"
289
- https://github.com/billk-FM/HEC-Commander/blob/main/Blog/7._Benchmarking_Is_All_You_Need.md
290
-
291
- Microsoft Windows has a maximum of 64 cores that can be allocated to a single Ras.exe process.
292
-
293
- Example:
294
- >>> # Using plan number
295
- >>> RasPlan.set_num_cores('02', 4)
296
- >>> # Using full path to plan file
297
- >>> RasPlan.set_num_cores('/path/to/project.p02', 4)
298
-
299
- Note:
300
- This function updates the ras object's dataframes after modifying the project structure.
301
- """
302
- ras_obj = ras_object or ras
303
- ras_obj.check_initialized()
304
-
305
- plan_file_path = RasUtils.get_plan_path(plan_number, ras_obj)
306
- if not plan_file_path:
307
- raise FileNotFoundError(f"Plan file not found: {plan_number}. Please provide a valid plan number or path.")
308
-
309
- def update_num_cores(lines):
310
- updated_lines = []
311
- for line in lines:
312
- if any(param in line for param in ["UNET D1 Cores=", "UNET D2 Cores=", "PS Cores="]):
313
- param_name = line.split("=")[0]
314
- updated_lines.append(f"{param_name}= {num_cores}\n")
315
- else:
316
- updated_lines.append(line)
317
- return updated_lines
318
-
319
- try:
320
- RasUtils.update_file(plan_file_path, update_num_cores)
321
- except Exception as e:
322
- raise IOError(f"Failed to update number of cores in plan file: {e}")
323
-
324
- # Update the ras object's dataframes
325
- ras_obj.plan_df = ras_obj.get_plan_entries()
326
- ras_obj.geom_df = ras_obj.get_geom_entries()
327
- ras_obj.flow_df = ras_obj.get_flow_entries()
328
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
329
-
330
- @staticmethod
331
- @log_call
332
- def set_geom_preprocessor(file_path, run_htab, use_ib_tables, ras_object=None):
333
- """
334
- Update the simulation plan file to modify the `Run HTab` and `UNET Use Existing IB Tables` settings.
335
-
336
- Parameters:
337
- file_path (str): Path to the simulation plan file (.p06 or similar) that you want to modify.
338
- run_htab (int): Value for the `Run HTab` setting:
339
- - `0` : Do not run the geometry preprocessor, use existing geometry tables.
340
- - `-1` : Run the geometry preprocessor, forcing a recomputation of the geometry tables.
341
- use_ib_tables (int): Value for the `UNET Use Existing IB Tables` setting:
342
- - `0` : Use existing interpolation/boundary (IB) tables without recomputing them.
343
- - `-1` : Do not use existing IB tables, force a recomputation.
344
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
345
-
346
- Returns:
347
- None
348
-
349
- Raises:
350
- ValueError: If `run_htab` or `use_ib_tables` are not integers or not within the accepted values (`0` or `-1`).
351
- FileNotFoundError: If the specified file does not exist.
352
- IOError: If there is an error reading or writing the file.
353
-
354
- Example:
355
- >>> RasPlan.set_geom_preprocessor('/path/to/project.p06', run_htab=-1, use_ib_tables=0)
356
-
357
- Note:
358
- This function updates the ras object's dataframes after modifying the project structure.
359
- """
360
- ras_obj = ras_object or ras
361
- ras_obj.check_initialized()
362
-
363
- if run_htab not in [-1, 0]:
364
- raise ValueError("Invalid value for `Run HTab`. Expected `0` or `-1`.")
365
- if use_ib_tables not in [-1, 0]:
366
- raise ValueError("Invalid value for `UNET Use Existing IB Tables`. Expected `0` or `-1`.")
367
-
368
- def update_geom_preprocessor(lines, run_htab, use_ib_tables):
369
- updated_lines = []
370
- for line in lines:
371
- if line.lstrip().startswith("Run HTab="):
372
- updated_lines.append(f"Run HTab= {run_htab} \n")
373
- elif line.lstrip().startswith("UNET Use Existing IB Tables="):
374
- updated_lines.append(f"UNET Use Existing IB Tables= {use_ib_tables} \n")
375
- else:
376
- updated_lines.append(line)
377
- return updated_lines
378
-
379
- try:
380
- RasUtils.update_file(file_path, update_geom_preprocessor, run_htab, use_ib_tables)
381
- except FileNotFoundError:
382
- raise FileNotFoundError(f"The file '{file_path}' does not exist.")
383
- except IOError as e:
384
- raise IOError(f"An error occurred while reading or writing the file: {e}")
385
-
386
- # Update the ras object's dataframes
387
- ras_obj.plan_df = ras_obj.get_plan_entries()
388
- ras_obj.geom_df = ras_obj.get_geom_entries()
389
- ras_obj.flow_df = ras_obj.get_flow_entries()
390
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
391
-
392
- @staticmethod
393
- @log_call
394
- def get_results_path(plan_number: str, ras_object=None) -> Optional[str]:
395
- """
396
- Retrieve the results file path for a given HEC-RAS plan number.
397
-
398
- Args:
399
- plan_number (str): The HEC-RAS plan number for which to find the results path.
400
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
401
-
402
- Returns:
403
- Optional[str]: The full path to the results file if found and the file exists, or None if not found.
404
-
405
- Raises:
406
- RuntimeError: If the project is not initialized.
407
-
408
- Example:
409
- >>> ras_plan = RasPlan()
410
- >>> results_path = ras_plan.get_results_path('01')
411
- >>> if results_path:
412
- ... print(f"Results file found at: {results_path}")
413
- ... else:
414
- ... print("Results file not found.")
415
- """
416
- ras_obj = ras_object or ras
417
- ras_obj.check_initialized()
418
-
419
- # Update the plan dataframe in the ras instance to ensure it is current
420
- ras_obj.plan_df = ras_obj.get_plan_entries()
421
-
422
- # Ensure plan_number is a string
423
- plan_number = str(plan_number).zfill(2)
424
-
425
- plan_entry = ras_obj.plan_df[ras_obj.plan_df['plan_number'] == plan_number]
426
- if not plan_entry.empty:
427
- results_path = plan_entry['HDF_Results_Path'].iloc[0]
428
- if results_path and Path(results_path).exists():
429
- return results_path
430
- else:
431
- return None
432
- else:
433
- return None
434
-
435
- @staticmethod
436
- @log_call
437
- def get_plan_path(plan_number: str, ras_object=None) -> Optional[str]:
438
- """
439
- Return the full path for a given plan number.
440
-
441
- This method ensures that the latest plan entries are included by refreshing
442
- the plan dataframe before searching for the requested plan number.
443
-
444
- Args:
445
- plan_number (str): The plan number to search for.
446
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
447
-
448
- Returns:
449
- Optional[str]: The full path of the plan file if found, None otherwise.
450
-
451
- Raises:
452
- RuntimeError: If the project is not initialized.
453
-
454
- Example:
455
- >>> ras_plan = RasPlan()
456
- >>> plan_path = ras_plan.get_plan_path('01')
457
- >>> if plan_path:
458
- ... print(f"Plan file found at: {plan_path}")
459
- ... else:
460
- ... print("Plan file not found.")
461
- """
462
- ras_obj = ras_object or ras
463
- ras_obj.check_initialized()
464
-
465
- # Use updated plan dataframe
466
- plan_df = ras_obj.get_plan_entries()
467
-
468
- plan_path = plan_df[plan_df['plan_number'] == plan_number]
469
-
470
- if not plan_path.empty:
471
- full_path = plan_path['full_path'].iloc[0]
472
- return full_path
473
- else:
474
- return None
475
-
476
- @staticmethod
477
- @log_call
478
- def get_flow_path(flow_number: str, ras_object=None) -> Optional[str]:
479
- """
480
- Return the full path for a given flow number.
481
-
482
- Args:
483
- flow_number (str): The flow number to search for.
484
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
485
-
486
- Returns:
487
- Optional[str]: The full path of the flow file if found, None otherwise.
488
-
489
- Raises:
490
- RuntimeError: If the project is not initialized.
491
-
492
- Example:
493
- >>> ras_plan = RasPlan()
494
- >>> flow_path = ras_plan.get_flow_path('01')
495
- >>> if flow_path:
496
- ... print(f"Flow file found at: {flow_path}")
497
- ... else:
498
- ... print("Flow file not found.")
499
- """
500
- ras_obj = ras_object or ras
501
- ras_obj.check_initialized()
502
-
503
- # Use updated flow dataframe
504
- ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
505
-
506
- flow_path = ras_obj.flow_df[ras_obj.flow_df['flow_number'] == flow_number]
507
- if not flow_path.empty:
508
- full_path = flow_path['full_path'].iloc[0]
509
- return full_path
510
- else:
511
- return None
512
-
513
- @staticmethod
514
- @log_call
515
- def get_unsteady_path(unsteady_number: str, ras_object=None) -> Optional[str]:
516
- """
517
- Return the full path for a given unsteady number.
518
-
519
- Args:
520
- unsteady_number (str): The unsteady number to search for.
521
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
522
-
523
- Returns:
524
- Optional[str]: The full path of the unsteady file if found, None otherwise.
525
-
526
- Raises:
527
- RuntimeError: If the project is not initialized.
528
-
529
- Example:
530
- >>> ras_plan = RasPlan()
531
- >>> unsteady_path = ras_plan.get_unsteady_path('01')
532
- >>> if unsteady_path:
533
- ... print(f"Unsteady file found at: {unsteady_path}")
534
- ... else:
535
- ... print("Unsteady file not found.")
536
- """
537
- ras_obj = ras_object or ras
538
- ras_obj.check_initialized()
539
-
540
- # Use updated unsteady dataframe
541
- ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
542
-
543
- unsteady_path = ras_obj.unsteady_df[ras_obj.unsteady_df['unsteady_number'] == unsteady_number]
544
- if not unsteady_path.empty:
545
- full_path = unsteady_path['full_path'].iloc[0]
546
- return full_path
547
- else:
548
- return None
549
-
550
- @staticmethod
551
- @log_call
552
- def get_geom_path(geom_number: str, ras_object=None) -> Optional[str]:
553
- """
554
- Return the full path for a given geometry number.
555
-
556
- Args:
557
- geom_number (str): The geometry number to search for.
558
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
559
-
560
- Returns:
561
- Optional[str]: The full path of the geometry file if found, None otherwise.
562
-
563
- Raises:
564
- RuntimeError: If the project is not initialized.
565
-
566
- Example:
567
- >>> ras_plan = RasPlan()
568
- >>> geom_path = ras_plan.get_geom_path('01')
569
- >>> if geom_path:
570
- ... print(f"Geometry file found at: {geom_path}")
571
- ... else:
572
- ... print("Geometry file not found.")
573
- """
574
- ras_obj = ras_object or ras
575
- ras_obj.check_initialized()
576
-
577
- # Use updated geom dataframe
578
- ras_obj.geom_df = ras_obj.get_prj_entries('Geom')
579
-
580
- geom_path = ras_obj.geom_df[ras_obj.geom_df['geom_number'] == geom_number]
581
- if not geom_path.empty:
582
- full_path = geom_path['full_path'].iloc[0]
583
- return full_path
584
- else:
585
- return None
586
-
587
- # Clone Functions to copy unsteady, flow, and geometry files from templates
588
-
589
- @staticmethod
590
- @log_call
591
- def clone_plan(template_plan, new_plan_shortid=None, ras_object=None):
592
- """
593
- Create a new plan file based on a template and update the project file.
594
-
595
- Parameters:
596
- template_plan (str): Plan number to use as template (e.g., '01')
597
- new_plan_shortid (str, optional): New short identifier for the plan file
598
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
599
-
600
- Returns:
601
- str: New plan number
602
-
603
- Example:
604
- >>> ras_plan = RasPlan()
605
- >>> new_plan_number = ras_plan.clone_plan('01', new_plan_shortid='New Plan')
606
- >>> print(f"New plan created with number: {new_plan_number}")
607
-
608
- Note:
609
- This function updates the ras object's dataframes after modifying the project structure.
610
- """
611
- ras_obj = ras_object or ras
612
- ras_obj.check_initialized()
613
-
614
- ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
615
- new_plan_num = RasPlan.get_next_number(ras_obj.plan_df['plan_number'])
616
-
617
- template_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{template_plan}"
618
- new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
619
-
620
- # Clone file and update project file
621
- RasUtils.clone_file(template_plan_path, new_plan_path,
622
- lambda lines: [
623
- f"Short Identifier={new_plan_shortid or (match.group(1) + '_copy')[:24]}\n"
624
- if (match := re.match(r'^Short Identifier=(.*)$', line.strip(), re.IGNORECASE))
625
- else line
626
- for line in lines
627
- ])
628
-
629
- RasUtils.update_project_file(ras_obj.prj_file, 'Plan', new_plan_num, ras_object=ras_obj)
630
- ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
631
-
632
- # Copy additional columns from template to new plan
633
- template_row = ras_obj.plan_df[ras_obj.plan_df['plan_number'] == template_plan].iloc[0]
634
- for col in ['Geom File', 'Geom Path', 'Flow File', 'Flow Path']:
635
- if col in template_row.index:
636
- ras_obj.plan_df.loc[ras_obj.plan_df['plan_number'] == new_plan_num, col] = template_row[col]
637
-
638
- # Update all dataframes
639
- ras_obj.plan_df = ras_obj.get_plan_entries()
640
- ras_obj.geom_df = ras_obj.get_geom_entries()
641
- ras_obj.flow_df = ras_obj.get_flow_entries()
642
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
643
-
644
- return new_plan_num
645
-
646
- @staticmethod
647
- @log_call
648
- def clone_unsteady(template_unsteady, ras_object=None):
649
- """
650
- Copy unsteady flow files from a template, find the next unsteady number,
651
- and update the project file accordingly.
652
-
653
- Parameters:
654
- template_unsteady (str): Unsteady flow number to be used as a template (e.g., '01')
655
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
656
-
657
- Returns:
658
- str: New unsteady flow number (e.g., '03')
659
-
660
- Example:
661
- >>> ras_plan = RasPlan()
662
- >>> new_unsteady_num = ras_plan.clone_unsteady('01')
663
- >>> print(f"New unsteady flow file created: u{new_unsteady_num}")
664
-
665
- Note:
666
- This function updates the ras object's dataframes after modifying the project structure.
667
- """
668
- ras_obj = ras_object or ras
669
- ras_obj.check_initialized()
670
-
671
- # Update unsteady entries without reinitializing the entire project
672
- ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
673
-
674
- new_unsteady_num = RasPlan.get_next_number(ras_obj.unsteady_df['unsteady_number'])
675
- template_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}"
676
- new_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}"
677
-
678
- # Use RasUtils to clone the file
679
- RasUtils.clone_file(template_unsteady_path, new_unsteady_path)
680
-
681
- # Copy the corresponding .hdf file if it exists
682
- template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}.hdf"
683
- new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}.hdf"
684
- if template_hdf_path.exists():
685
- shutil.copy(template_hdf_path, new_hdf_path)
686
-
687
- # Use RasUtils to update the project file
688
- RasUtils.update_project_file(ras_obj.prj_file, 'Unsteady', new_unsteady_num, ras_object=ras_obj)
689
-
690
- # Re-initialize the ras global object
691
- ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
692
-
693
- ras_obj.plan_df = ras_obj.get_plan_entries()
694
- ras_obj.geom_df = ras_obj.get_geom_entries()
695
- ras_obj.flow_df = ras_obj.get_flow_entries()
696
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
697
-
698
- return new_unsteady_num
699
-
700
-
701
- @staticmethod
702
- @log_call
703
- def clone_steady(template_flow, ras_object=None):
704
- """
705
- Copy steady flow files from a template, find the next flow number,
706
- and update the project file accordingly.
707
-
708
- Parameters:
709
- template_flow (str): Flow number to be used as a template (e.g., '01')
710
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
711
-
712
- Returns:
713
- str: New flow number (e.g., '03')
714
-
715
- Example:
716
- >>> ras_plan = RasPlan()
717
- >>> new_flow_num = ras_plan.clone_steady('01')
718
- >>> print(f"New steady flow file created: f{new_flow_num}")
719
-
720
- Note:
721
- This function updates the ras object's dataframes after modifying the project structure.
722
- """
723
- ras_obj = ras_object or ras
724
- ras_obj.check_initialized()
725
-
726
- # Update flow entries without reinitializing the entire project
727
- ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
728
-
729
- new_flow_num = RasPlan.get_next_number(ras_obj.flow_df['flow_number'])
730
- template_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{template_flow}"
731
- new_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_flow_num}"
732
-
733
- # Use RasUtils to clone the file
734
- RasUtils.clone_file(template_flow_path, new_flow_path)
735
-
736
- # Use RasUtils to update the project file
737
- RasUtils.update_project_file(ras_obj.prj_file, 'Flow', new_flow_num, ras_object=ras_obj)
738
-
739
- # Re-initialize the ras global object
740
- ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
741
-
742
- ras_obj.plan_df = ras_obj.get_plan_entries()
743
- ras_obj.geom_df = ras_obj.get_geom_entries()
744
- ras_obj.flow_df = ras_obj.get_flow_entries()
745
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
746
-
747
- return new_flow_num
748
-
749
- @staticmethod
750
- @log_call
751
- def clone_geom(template_geom, ras_object=None):
752
- """
753
- Copy geometry files from a template, find the next geometry number,
754
- and update the project file accordingly.
755
-
756
- Parameters:
757
- template_geom (str): Geometry number to be used as a template (e.g., '01')
758
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
759
-
760
- Returns:
761
- str: New geometry number (e.g., '03')
762
-
763
- Note:
764
- This function updates the ras object's dataframes after modifying the project structure.
765
- """
766
- ras_obj = ras_object or ras
767
- ras_obj.check_initialized()
768
-
769
- # Update geometry entries without reinitializing the entire project
770
- ras_obj.geom_df = ras_obj.get_prj_entries('Geom')
771
-
772
- new_geom_num = RasPlan.get_next_number(ras_obj.geom_df['geom_number'])
773
- template_geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{template_geom}"
774
- new_geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom_num}"
775
-
776
- # Use RasUtils to clone the file
777
- RasUtils.clone_file(template_geom_path, new_geom_path)
778
-
779
- # Handle HDF file copy
780
- template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{template_geom}.hdf"
781
- new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom_num}.hdf"
782
- if template_hdf_path.is_file():
783
- RasUtils.clone_file(template_hdf_path, new_hdf_path)
784
-
785
- # Use RasUtils to update the project file
786
- RasUtils.update_project_file(ras_obj.prj_file, 'Geom', new_geom_num, ras_object=ras_obj)
787
-
788
- # Update all dataframes in the ras object
789
- ras_obj.plan_df = ras_obj.get_plan_entries()
790
- ras_obj.geom_df = ras_obj.get_geom_entries()
791
- ras_obj.flow_df = ras_obj.get_flow_entries()
792
- ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
793
-
794
- return new_geom_num
795
-
796
- @staticmethod
797
- @log_call
798
- def get_next_number(existing_numbers):
799
- """
800
- Determine the next available number from a list of existing numbers.
801
-
802
- Parameters:
803
- existing_numbers (list): List of existing numbers as strings
804
-
805
- Returns:
806
- str: Next available number as a zero-padded string
807
-
808
- Example:
809
- >>> existing_numbers = ['01', '02', '04']
810
- >>> RasPlan.get_next_number(existing_numbers)
811
- '03'
812
- >>> existing_numbers = ['01', '02', '03']
813
- >>> RasPlan.get_next_number(existing_numbers)
814
- '04'
815
- """
816
- existing_numbers = sorted(int(num) for num in existing_numbers)
817
- next_number = 1
818
- for num in existing_numbers:
819
- if num == next_number:
820
- next_number += 1
821
- else:
822
- break
823
- return f"{next_number:02d}"
824
-
825
- @staticmethod
826
- @log_call
827
- def get_plan_value(
828
- plan_number_or_path: Union[str, Path],
829
- key: str,
830
- ras_object=None
831
- ) -> Any:
832
- """
833
- Retrieve a specific value from a HEC-RAS plan file.
834
-
835
- Parameters:
836
- plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
837
- key (str): The key to retrieve from the plan file
838
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
839
-
840
- Returns:
841
- Any: The value associated with the specified key
842
-
843
- Raises:
844
- ValueError: If the plan file is not found
845
- IOError: If there's an error reading the plan file
846
-
847
- Available keys and their expected types:
848
- - 'Computation Interval' (str): Time value for computational time step (e.g., '5SEC', '2MIN')
849
- - 'DSS File' (str): Name of the DSS file used
850
- - 'Flow File' (str): Name of the flow input file
851
- - 'Friction Slope Method' (int): Method selection for friction slope (e.g., 1, 2)
852
- - 'Geom File' (str): Name of the geometry input file
853
- - 'Mapping Interval' (str): Time interval for mapping output
854
- - 'Plan File' (str): Name of the plan file
855
- - 'Plan Title' (str): Title of the simulation plan
856
- - 'Program Version' (str): Version number of HEC-RAS
857
- - 'Run HTab' (int): Flag to run HTab module (-1 or 1)
858
- - 'Run Post Process' (int): Flag to run post-processing (-1 or 1)
859
- - 'Run Sediment' (int): Flag to run sediment transport module (0 or 1)
860
- - 'Run UNET' (int): Flag to run unsteady network module (-1 or 1)
861
- - 'Run WQNET' (int): Flag to run water quality module (0 or 1)
862
- - 'Short Identifier' (str): Short name or ID for the plan
863
- - 'Simulation Date' (str): Start and end dates/times for simulation
864
- - 'UNET D1 Cores' (int): Number of cores used in 1D calculations
865
- - 'UNET D2 Cores' (int): Number of cores used in 2D calculations
866
- - 'PS Cores' (int): Number of cores used in parallel simulation
867
- - 'UNET Use Existing IB Tables' (int): Flag for using existing internal boundary tables (-1, 0, or 1)
868
- - 'UNET 1D Methodology' (str): 1D calculation methodology
869
- - 'UNET D2 Solver Type' (str): 2D solver type
870
- - 'UNET D2 Name' (str): Name of the 2D area
871
- - 'Run RASMapper' (int): Flag to run RASMapper for floodplain mapping (-1 for off, 0 for on)
872
-
873
- Note:
874
- Writing Multi line keys like 'Description' are not supported by this function.
875
-
876
- Example:
877
- >>> computation_interval = RasPlan.get_plan_value("01", "Computation Interval")
878
- >>> print(f"Computation interval: {computation_interval}")
879
- """
880
- ras_obj = ras_object or ras
881
- ras_obj.check_initialized()
882
-
883
- supported_plan_keys = {
884
- 'Description', 'Computation Interval', 'DSS File', 'Flow File', 'Friction Slope Method',
885
- 'Geom File', 'Mapping Interval', 'Plan File', 'Plan Title', 'Program Version',
886
- 'Run HTab', 'Run Post Process', 'Run Sediment', 'Run UNET', 'Run WQNET',
887
- 'Short Identifier', 'Simulation Date', 'UNET D1 Cores', 'UNET D2 Cores', 'PS Cores',
888
- 'UNET Use Existing IB Tables', 'UNET 1D Methodology', 'UNET D2 Solver Type',
889
- 'UNET D2 Name', 'Run RASMapper', 'Run HTab', 'Run UNet', 'Run PostProcess',
890
- 'Run WQNet', 'UNET P2 Cores', 'Run Post Process', 'Run WQNET'
891
- }
892
-
893
- if key not in supported_plan_keys:
894
- logger = logging.getLogger(__name__)
895
- logger.warning(f"Unknown key: {key}. Valid keys are: {', '.join(supported_plan_keys)}\n Add more keys and explanations in get_plan_value() as needed.")
896
-
897
- plan_file_path = Path(plan_number_or_path)
898
- if not plan_file_path.is_file():
899
- plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object=ras_obj)
900
- if plan_file_path is None or not Path(plan_file_path).exists():
901
- raise ValueError(f"Plan file not found: {plan_file_path}")
902
-
903
- try:
904
- with open(plan_file_path, 'r') as file:
905
- content = file.read()
906
- except IOError as e:
907
- logger = logging.getLogger(__name__)
908
- logger.error(f"Error reading plan file {plan_file_path}: {e}")
909
- raise
910
-
911
- # Handle core settings specially to convert to integers
912
- core_keys = {'UNET D1 Cores', 'UNET D2 Cores', 'PS Cores'}
913
- if key in core_keys:
914
- pattern = f"{key}=(.*)"
915
- match = re.search(pattern, content)
916
- if match:
917
- try:
918
- return int(match.group(1).strip())
919
- except ValueError:
920
- logger = logging.getLogger(__name__)
921
- logger.error(f"Could not convert {key} value to integer")
922
- return None
923
- else:
924
- logger = logging.getLogger(__name__)
925
- logger.error(f"Key '{key}' not found in the plan file.")
926
- return None
927
- elif key == 'Description':
928
- match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
929
- return match.group(1).strip() if match else None
930
- else:
931
- pattern = f"{key}=(.*)"
932
- match = re.search(pattern, content)
933
- if match:
934
- return match.group(1).strip()
935
- else:
936
- logger = logging.getLogger(__name__)
937
- logger.error(f"Key '{key}' not found in the plan file.")
938
- return None
939
-
940
-
941
- # NEW FUNCTIONS THAT NEED TESTING AND EXAMPLES
942
-
943
-
944
- @staticmethod
945
- @log_call
946
- def update_run_flags(
947
- plan_number_or_path: Union[str, Path],
948
- geometry_preprocessor: bool = None,
949
- unsteady_flow_simulation: bool = None,
950
- run_sediment: bool = None,
951
- post_processor: bool = None,
952
- floodplain_mapping: bool = None,
953
- ras_object=None
954
- ) -> None:
955
- """
956
- Update the run flags in a HEC-RAS plan file.
957
-
958
- Parameters:
959
- plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
960
- geometry_preprocessor (bool, optional): Flag for Geometry Preprocessor
961
- unsteady_flow_simulation (bool, optional): Flag for Unsteady Flow Simulation
962
- run_sediment (bool, optional): Flag for run_sediment
963
- post_processor (bool, optional): Flag for Post Processor
964
- floodplain_mapping (bool, optional): Flag for Floodplain Mapping
965
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
966
-
967
- Raises:
968
- ValueError: If the plan file is not found
969
- IOError: If there's an error reading or writing the plan file
970
-
971
- Example:
972
- >>> RasPlan.update_run_flags("01", geometry_preprocessor=True, unsteady_flow_simulation=True, run_sediment=False, post_processor=True, floodplain_mapping=False)
973
- """
974
- ras_obj = ras_object or ras
975
- ras_obj.check_initialized()
976
-
977
- plan_file_path = Path(plan_number_or_path)
978
- if not plan_file_path.is_file():
979
- plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object=ras_obj)
980
- if plan_file_path is None or not Path(plan_file_path).exists():
981
- raise ValueError(f"Plan file not found: {plan_file_path}")
982
-
983
- flag_mapping = {
984
- 'geometry_preprocessor': ('Run HTab', geometry_preprocessor),
985
- 'unsteady_flow_simulation': ('Run UNet', unsteady_flow_simulation),
986
- 'run_sediment': ('Run run_sediment', run_sediment),
987
- 'post_processor': ('Run PostProcess', post_processor),
988
- 'floodplain_mapping': ('Run RASMapper', floodplain_mapping)
989
- }
990
-
991
- try:
992
- with open(plan_file_path, 'r') as file:
993
- lines = file.readlines()
994
-
995
- for i, line in enumerate(lines):
996
- for key, (file_key, value) in flag_mapping.items():
997
- if value is not None and line.strip().startswith(file_key):
998
- lines[i] = f"{file_key}= {1 if value else 0}\n"
999
-
1000
- with open(plan_file_path, 'w') as file:
1001
- file.writelines(lines)
1002
-
1003
- logger = logging.getLogger(__name__)
1004
- logger.info(f"Successfully updated run flags in plan file: {plan_file_path}")
1005
-
1006
- except IOError as e:
1007
- logger = logging.getLogger(__name__)
1008
- logger.error(f"Error updating run flags in plan file {plan_file_path}: {e}")
1009
- raise
1010
-
1011
-
1012
-
1013
- @staticmethod
1014
- @log_call
1015
- def update_plan_intervals(
1016
- plan_number_or_path: Union[str, Path],
1017
- computation_interval: Optional[str] = None,
1018
- output_interval: Optional[str] = None,
1019
- instantaneous_interval: Optional[str] = None,
1020
- mapping_interval: Optional[str] = None,
1021
- ras_object=None
1022
- ) -> None:
1023
- """
1024
- Update the computation and output intervals in a HEC-RAS plan file.
1025
-
1026
- Parameters:
1027
- plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
1028
- computation_interval (Optional[str]): The new computation interval. Valid entries include:
1029
- '1SEC', '2SEC', '3SEC', '4SEC', '5SEC', '6SEC', '10SEC', '15SEC', '20SEC', '30SEC',
1030
- '1MIN', '2MIN', '3MIN', '4MIN', '5MIN', '6MIN', '10MIN', '15MIN', '20MIN', '30MIN',
1031
- '1HOUR', '2HOUR', '3HOUR', '4HOUR', '6HOUR', '8HOUR', '12HOUR', '1DAY'
1032
- output_interval (Optional[str]): The new output interval. Valid entries are the same as computation_interval.
1033
- instantaneous_interval (Optional[str]): The new instantaneous interval. Valid entries are the same as computation_interval.
1034
- mapping_interval (Optional[str]): The new mapping interval. Valid entries are the same as computation_interval.
1035
- ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
1036
-
1037
- Raises:
1038
- ValueError: If the plan file is not found or if an invalid interval is provided
1039
- IOError: If there's an error reading or writing the plan file
1040
-
1041
- Note: This function does not check if the intervals are equal divisors. Ensure you use valid values from HEC-RAS.
1042
-
1043
- Example:
1044
- >>> RasPlan.update_plan_intervals("01", computation_interval="5SEC", output_interval="1MIN", instantaneous_interval="1HOUR", mapping_interval="5MIN")
1045
- >>> RasPlan.update_plan_intervals("/path/to/plan.p01", computation_interval="10SEC", output_interval="30SEC")
1046
- """
1047
- ras_obj = ras_object or ras
1048
- ras_obj.check_initialized()
1049
-
1050
- plan_file_path = Path(plan_number_or_path)
1051
- if not plan_file_path.is_file():
1052
- plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object=ras_obj)
1053
- if plan_file_path is None or not Path(plan_file_path).exists():
1054
- raise ValueError(f"Plan file not found: {plan_file_path}")
1055
-
1056
- valid_intervals = [
1057
- '1SEC', '2SEC', '3SEC', '4SEC', '5SEC', '6SEC', '10SEC', '15SEC', '20SEC', '30SEC',
1058
- '1MIN', '2MIN', '3MIN', '4MIN', '5MIN', '6MIN', '10MIN', '15MIN', '20MIN', '30MIN',
1059
- '1HOUR', '2HOUR', '3HOUR', '4HOUR', '6HOUR', '8HOUR', '12HOUR', '1DAY'
1060
- ]
1061
-
1062
- interval_mapping = {
1063
- 'Computation Interval': computation_interval,
1064
- 'Output Interval': output_interval,
1065
- 'Instantaneous Interval': instantaneous_interval,
1066
- 'Mapping Interval': mapping_interval
1067
- }
1068
-
1069
- try:
1070
- with open(plan_file_path, 'r') as file:
1071
- lines = file.readlines()
1072
-
1073
- for i, line in enumerate(lines):
1074
- for key, value in interval_mapping.items():
1075
- if value is not None:
1076
- if value.upper() not in valid_intervals:
1077
- raise ValueError(f"Invalid {key}: {value}. Must be one of {valid_intervals}")
1078
- if line.strip().startswith(key):
1079
- lines[i] = f"{key}={value.upper()}\n"
1080
-
1081
- with open(plan_file_path, 'w') as file:
1082
- file.writelines(lines)
1083
-
1084
- logger = logging.getLogger(__name__)
1085
- logger.info(f"Successfully updated intervals in plan file: {plan_file_path}")
1086
-
1087
- except IOError as e:
1088
- logger = logging.getLogger(__name__)
1089
- logger.error(f"Error updating intervals in plan file {plan_file_path}: {e}")
1090
- raise
1091
-
1092
-
1093
- @log_call
1094
- def update_plan_description(plan_number_or_path: Union[str, Path], description: str, ras_object: Optional['RasPrj'] = None) -> None:
1095
- """
1096
- Update the description block in a HEC-RAS plan file.
1097
-
1098
- Args:
1099
- plan_number_or_path (Union[str, Path]): The plan number or full path to the plan file
1100
- description (str): The new description text to set
1101
- ras_object (Optional[RasPrj]): Specific RAS object to use. If None, uses the global ras instance.
1102
-
1103
- Raises:
1104
- ValueError: If the plan file is not found
1105
- IOError: If there's an error reading or writing the plan file
1106
- """
1107
- logger = get_logger(__name__)
1108
- ras_obj = ras_object or ras
1109
- ras_obj.check_initialized()
1110
-
1111
- plan_file_path = Path(plan_number_or_path)
1112
- if not plan_file_path.is_file():
1113
- plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
1114
- if not plan_file_path.exists():
1115
- logger.error(f"Plan file not found: {plan_file_path}")
1116
- raise ValueError(f"Plan file not found: {plan_file_path}")
1117
-
1118
- try:
1119
- with open(plan_file_path, 'r') as file:
1120
- content = file.read()
1121
-
1122
- # Find the description block
1123
- desc_pattern = r'Begin DESCRIPTION.*?END DESCRIPTION'
1124
- new_desc_block = f'Begin DESCRIPTION\n{description}\nEND DESCRIPTION'
1125
-
1126
- if re.search(desc_pattern, content, re.DOTALL):
1127
- # Replace existing description block
1128
- new_content = re.sub(desc_pattern, new_desc_block, content, flags=re.DOTALL)
1129
- else:
1130
- # Add new description block at the start of the file
1131
- new_content = new_desc_block + '\n' + content
1132
-
1133
- # Write the updated content back to the file
1134
- with open(plan_file_path, 'w') as file:
1135
- file.write(new_content)
1136
-
1137
- logger.info(f"Updated description in plan file: {plan_file_path}")
1138
-
1139
- # Update the dataframes in the RAS object to reflect changes
1140
- if ras_object:
1141
- ras_object.plan_df = ras_object.get_plan_entries()
1142
- ras_object.geom_df = ras_object.get_geom_entries()
1143
- ras_object.flow_df = ras_object.get_flow_entries()
1144
- ras_object.unsteady_df = ras_object.get_unsteady_entries()
1145
-
1146
- except IOError as e:
1147
- logger.error(f"Error updating plan description in {plan_file_path}: {e}")
1148
- raise
1149
- except Exception as e:
1150
- logger.error(f"Unexpected error updating plan description: {e}")
1151
- raise
1152
-
1153
- @staticmethod
1154
- @log_call
1155
- def read_plan_description(plan_number_or_path: Union[str, Path], ras_object: Optional['RasPrj'] = None) -> str:
1156
- """
1157
- Read the description from the plan file.
1158
-
1159
- Args:
1160
- plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1161
- ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1162
-
1163
- Returns:
1164
- str: The description from the plan file.
1165
-
1166
- Raises:
1167
- ValueError: If the plan file is not found.
1168
- IOError: If there's an error reading from the plan file.
1169
- """
1170
- logger = logging.getLogger(__name__)
1171
-
1172
- plan_file_path = Path(plan_number_or_path)
1173
- if not plan_file_path.is_file():
1174
- plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object)
1175
- if plan_file_path is None or not Path(plan_file_path).exists():
1176
- raise ValueError(f"Plan file not found: {plan_file_path}")
1177
-
1178
- try:
1179
- with open(plan_file_path, 'r') as file:
1180
- lines = file.readlines()
1181
- except IOError as e:
1182
- logger.error(f"Error reading plan file {plan_file_path}: {e}")
1183
- raise
1184
-
1185
- description_lines = []
1186
- in_description = False
1187
- description_found = False
1188
- for line in lines:
1189
- if line.strip() == "BEGIN DESCRIPTION:":
1190
- in_description = True
1191
- description_found = True
1192
- elif line.strip() == "END DESCRIPTION:":
1193
- break
1194
- elif in_description:
1195
- description_lines.append(line.strip())
1196
-
1197
- if not description_found:
1198
- logger.warning(f"No description found in plan file: {plan_file_path}")
1199
- return ""
1200
-
1201
- description = '\n'.join(description_lines)
1202
- logger.info(f"Read description from plan file: {plan_file_path}")
1203
- return description
1204
-
1205
-
1206
-
1207
-
1208
- @staticmethod
1209
- @log_call
1210
- def update_simulation_date(plan_number_or_path: Union[str, Path], start_date: datetime, end_date: datetime, ras_object: Optional['RasPrj'] = None) -> None:
1211
- """
1212
- Update the simulation date for a given plan.
1213
-
1214
- Args:
1215
- plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1216
- start_date (datetime): The start date and time for the simulation.
1217
- end_date (datetime): The end date and time for the simulation.
1218
- ras_object (Optional['RasPrj']): The RAS project object. Defaults to None.
1219
-
1220
- Raises:
1221
- ValueError: If the plan file is not found or if there's an error updating the file.
1222
- """
1223
-
1224
- # Get the plan file path
1225
- plan_file_path = Path(plan_number_or_path)
1226
- if not plan_file_path.is_file():
1227
- plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object)
1228
- if plan_file_path is None or not Path(plan_file_path).exists():
1229
- raise ValueError(f"Plan file not found: {plan_file_path}")
1230
-
1231
- # Format the dates
1232
- formatted_date = f"{start_date.strftime('%d%b%Y').upper()},{start_date.strftime('%H%M')},{end_date.strftime('%d%b%Y').upper()},{end_date.strftime('%H%M')}"
1233
-
1234
- try:
1235
- # Read the file
1236
- with open(plan_file_path, 'r') as file:
1237
- lines = file.readlines()
1238
-
1239
- # Update the Simulation Date line
1240
- updated = False
1241
- for i, line in enumerate(lines):
1242
- if line.startswith("Simulation Date="):
1243
- lines[i] = f"Simulation Date={formatted_date}\n"
1244
- updated = True
1245
- break
1246
-
1247
- # If Simulation Date line not found, add it at the end
1248
- if not updated:
1249
- lines.append(f"Simulation Date={formatted_date}\n")
1250
-
1251
- # Write the updated content back to the file
1252
- with open(plan_file_path, 'w') as file:
1253
- file.writelines(lines)
1254
-
1255
- logger.info(f"Updated simulation date in plan file: {plan_file_path}")
1256
-
1257
- except IOError as e:
1258
- logger.error(f"Error updating simulation date in plan file {plan_file_path}: {e}")
1259
- raise ValueError(f"Error updating simulation date: {e}")
1260
-
1261
- # Refresh RasPrj dataframes
1262
- if ras_object:
1263
- ras_object.plan_df = ras_object.get_plan_entries()
1264
- ras_object.unsteady_df = ras_object.get_unsteady_entries()
1265
-
1266
- @staticmethod
1267
- @log_call
1268
- def get_shortid(plan_number_or_path: Union[str, Path], ras_object=None) -> str:
1269
- """
1270
- Get the Short Identifier from a HEC-RAS plan file.
1271
-
1272
- Args:
1273
- plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1274
- ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1275
-
1276
- Returns:
1277
- str: The Short Identifier from the plan file.
1278
-
1279
- Raises:
1280
- ValueError: If the plan file is not found.
1281
- IOError: If there's an error reading from the plan file.
1282
-
1283
- Example:
1284
- >>> shortid = RasPlan.get_shortid('01')
1285
- >>> print(f"Plan's Short Identifier: {shortid}")
1286
- """
1287
- logger = get_logger(__name__)
1288
- ras_obj = ras_object or ras
1289
- ras_obj.check_initialized()
1290
-
1291
- # Get the Short Identifier using get_plan_value
1292
- shortid = RasPlan.get_plan_value(plan_number_or_path, "Short Identifier", ras_obj)
1293
-
1294
- if shortid is None:
1295
- logger.warning(f"Short Identifier not found in plan: {plan_number_or_path}")
1296
- return ""
1297
-
1298
- logger.info(f"Retrieved Short Identifier: {shortid}")
1299
- return shortid
1300
-
1301
- @staticmethod
1302
- @log_call
1303
- def set_shortid(plan_number_or_path: Union[str, Path], new_shortid: str, ras_object=None) -> None:
1304
- """
1305
- Set the Short Identifier in a HEC-RAS plan file.
1306
-
1307
- Args:
1308
- plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1309
- new_shortid (str): The new Short Identifier to set (max 24 characters).
1310
- ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1311
-
1312
- Raises:
1313
- ValueError: If the plan file is not found or if new_shortid is too long.
1314
- IOError: If there's an error updating the plan file.
1315
-
1316
- Example:
1317
- >>> RasPlan.set_shortid('01', 'NewShortIdentifier')
1318
- """
1319
- logger = get_logger(__name__)
1320
- ras_obj = ras_object or ras
1321
- ras_obj.check_initialized()
1322
-
1323
- # Ensure new_shortid is not too long (HEC-RAS limits short identifiers to 24 characters)
1324
- if len(new_shortid) > 24:
1325
- logger.warning(f"Short Identifier too long (24 char max). Truncating: {new_shortid}")
1326
- new_shortid = new_shortid[:24]
1327
-
1328
- # Get the plan file path
1329
- plan_file_path = Path(plan_number_or_path)
1330
- if not plan_file_path.is_file():
1331
- plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_obj)
1332
- if not plan_file_path.exists():
1333
- logger.error(f"Plan file not found: {plan_file_path}")
1334
- raise ValueError(f"Plan file not found: {plan_file_path}")
1335
-
1336
- try:
1337
- # Read the file
1338
- with open(plan_file_path, 'r') as file:
1339
- lines = file.readlines()
1340
-
1341
- # Update the Short Identifier line
1342
- updated = False
1343
- for i, line in enumerate(lines):
1344
- if line.startswith("Short Identifier="):
1345
- lines[i] = f"Short Identifier={new_shortid}\n"
1346
- updated = True
1347
- break
1348
-
1349
- # If Short Identifier line not found, add it after Plan Title
1350
- if not updated:
1351
- for i, line in enumerate(lines):
1352
- if line.startswith("Plan Title="):
1353
- lines.insert(i+1, f"Short Identifier={new_shortid}\n")
1354
- updated = True
1355
- break
1356
-
1357
- # If Plan Title not found either, add at the beginning
1358
- if not updated:
1359
- lines.insert(0, f"Short Identifier={new_shortid}\n")
1360
-
1361
- # Write the updated content back to the file
1362
- with open(plan_file_path, 'w') as file:
1363
- file.writelines(lines)
1364
-
1365
- logger.info(f"Updated Short Identifier in plan file to: {new_shortid}")
1366
-
1367
- except IOError as e:
1368
- logger.error(f"Error updating Short Identifier in plan file {plan_file_path}: {e}")
1369
- raise ValueError(f"Error updating Short Identifier: {e}")
1370
-
1371
- # Refresh RasPrj dataframes if ras_object provided
1372
- if ras_object:
1373
- ras_object.plan_df = ras_object.get_plan_entries()
1374
-
1375
- @staticmethod
1376
- @log_call
1377
- def get_plan_title(plan_number_or_path: Union[str, Path], ras_object=None) -> str:
1378
- """
1379
- Get the Plan Title from a HEC-RAS plan file.
1380
-
1381
- Args:
1382
- plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1383
- ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1384
-
1385
- Returns:
1386
- str: The Plan Title from the plan file.
1387
-
1388
- Raises:
1389
- ValueError: If the plan file is not found.
1390
- IOError: If there's an error reading from the plan file.
1391
-
1392
- Example:
1393
- >>> title = RasPlan.get_plan_title('01')
1394
- >>> print(f"Plan Title: {title}")
1395
- """
1396
- logger = get_logger(__name__)
1397
- ras_obj = ras_object or ras
1398
- ras_obj.check_initialized()
1399
-
1400
- # Get the Plan Title using get_plan_value
1401
- title = RasPlan.get_plan_value(plan_number_or_path, "Plan Title", ras_obj)
1402
-
1403
- if title is None:
1404
- logger.warning(f"Plan Title not found in plan: {plan_number_or_path}")
1405
- return ""
1406
-
1407
- logger.info(f"Retrieved Plan Title: {title}")
1408
- return title
1409
-
1410
- @staticmethod
1411
- @log_call
1412
- def set_plan_title(plan_number_or_path: Union[str, Path], new_title: str, ras_object=None) -> None:
1413
- """
1414
- Set the Plan Title in a HEC-RAS plan file.
1415
-
1416
- Args:
1417
- plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1418
- new_title (str): The new Plan Title to set.
1419
- ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1420
-
1421
- Raises:
1422
- ValueError: If the plan file is not found.
1423
- IOError: If there's an error updating the plan file.
1424
-
1425
- Example:
1426
- >>> RasPlan.set_plan_title('01', 'Updated Plan Scenario')
1427
- """
1428
- logger = get_logger(__name__)
1429
- ras_obj = ras_object or ras
1430
- ras_obj.check_initialized()
1431
-
1432
- # Get the plan file path
1433
- plan_file_path = Path(plan_number_or_path)
1434
- if not plan_file_path.is_file():
1435
- plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_obj)
1436
- if not plan_file_path.exists():
1437
- logger.error(f"Plan file not found: {plan_file_path}")
1438
- raise ValueError(f"Plan file not found: {plan_file_path}")
1439
-
1440
- try:
1441
- # Read the file
1442
- with open(plan_file_path, 'r') as file:
1443
- lines = file.readlines()
1444
-
1445
- # Update the Plan Title line
1446
- updated = False
1447
- for i, line in enumerate(lines):
1448
- if line.startswith("Plan Title="):
1449
- lines[i] = f"Plan Title={new_title}\n"
1450
- updated = True
1451
- break
1452
-
1453
- # If Plan Title line not found, add it at the beginning
1454
- if not updated:
1455
- lines.insert(0, f"Plan Title={new_title}\n")
1456
-
1457
- # Write the updated content back to the file
1458
- with open(plan_file_path, 'w') as file:
1459
- file.writelines(lines)
1460
-
1461
- logger.info(f"Updated Plan Title in plan file to: {new_title}")
1462
-
1463
- except IOError as e:
1464
- logger.error(f"Error updating Plan Title in plan file {plan_file_path}: {e}")
1465
- raise ValueError(f"Error updating Plan Title: {e}")
1466
-
1467
- # Refresh RasPrj dataframes if ras_object provided
1468
- if ras_object:
1
+ """
2
+ RasPlan - Operations for handling plan files in HEC-RAS projects
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
+ 3. Obtain the logger using: logger = logging.getLogger(__name__)
18
+
19
+ Example:
20
+ @log_call
21
+ def my_function():
22
+ logger = logging.getLogger(__name__)
23
+ logger.debug("Additional debug information")
24
+ # Function logic here
25
+
26
+
27
+ -----
28
+
29
+ All of the methods in this class are static and are designed to be used without instantiation.
30
+
31
+ List of Functions in RasPlan:
32
+ - set_geom(): Set the geometry for a specified plan
33
+ - set_steady(): Apply a steady flow file to a plan file
34
+ - set_unsteady(): Apply an unsteady flow file to a plan file
35
+ - set_num_cores(): Update the maximum number of cores to use
36
+ - set_geom_preprocessor(): Update geometry preprocessor settings
37
+ - clone_plan(): Create a new plan file based on a template
38
+ - clone_unsteady(): Copy unsteady flow files from a template
39
+ - clone_steady(): Copy steady flow files from a template
40
+ - clone_geom(): Copy geometry files from a template
41
+ - get_next_number(): Determine the next available number from a list
42
+ - get_plan_value(): Retrieve a specific value from a plan file
43
+ - get_results_path(): Get the results file path for a plan
44
+ - get_plan_path(): Get the full path for a plan number
45
+ - get_flow_path(): Get the full path for a flow number
46
+ - get_unsteady_path(): Get the full path for an unsteady number
47
+ - get_geom_path(): Get the full path for a geometry number
48
+ - update_run_flags(): Update various run flags in a plan file
49
+ - update_plan_intervals(): Update computation and output intervals
50
+ - update_plan_description(): Update the description in a plan file
51
+ - read_plan_description(): Read the description from a plan file
52
+ - update_simulation_date(): Update simulation start and end dates
53
+ - get_shortid(): Get the Short Identifier from a plan file
54
+ - set_shortid(): Set the Short Identifier in a plan file
55
+ - get_plan_title(): Get the Plan Title from a plan file
56
+ - set_plan_title(): Set the Plan Title in a plan file
57
+
58
+
59
+
60
+ """
61
+ import os
62
+ import re
63
+ import logging
64
+ from pathlib import Path
65
+ import shutil
66
+ from typing import Union, Optional
67
+ import pandas as pd
68
+ from .RasPrj import RasPrj, ras
69
+ from .RasUtils import RasUtils
70
+ from pathlib import Path
71
+ from typing import Union, Any
72
+ from datetime import datetime
73
+
74
+ import logging
75
+ import re
76
+ from .LoggingConfig import get_logger
77
+ from .Decorators import log_call
78
+
79
+ logger = get_logger(__name__)
80
+
81
+ class RasPlan:
82
+ """
83
+ A class for operations on HEC-RAS plan files.
84
+ """
85
+
86
+ @staticmethod
87
+ @log_call
88
+ def set_geom(plan_number: Union[str, int], new_geom: Union[str, int], ras_object=None) -> pd.DataFrame:
89
+ """
90
+ Set the geometry for the specified plan.
91
+
92
+ Parameters:
93
+ plan_number (Union[str, int]): The plan number to update.
94
+ new_geom (Union[str, int]): The new geometry number to set.
95
+ ras_object: An optional RAS object instance.
96
+
97
+ Returns:
98
+ pd.DataFrame: The updated geometry DataFrame.
99
+
100
+ Example:
101
+ updated_geom_df = RasPlan.set_geom('02', '03')
102
+
103
+ Note:
104
+ This function updates the ras object's dataframes after modifying the project structure.
105
+ """
106
+ ras_obj = ras_object or ras
107
+ ras_obj.check_initialized()
108
+
109
+ plan_number = str(plan_number).zfill(2)
110
+ new_geom = str(new_geom).zfill(2)
111
+
112
+ # Update all dataframes
113
+ ras_obj.plan_df = ras_obj.get_plan_entries()
114
+ ras_obj.geom_df = ras_obj.get_geom_entries()
115
+ ras_obj.flow_df = ras_obj.get_flow_entries()
116
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
117
+
118
+ if new_geom not in ras_obj.geom_df['geom_number'].values:
119
+ logger.error(f"Geometry {new_geom} not found in project.")
120
+ raise ValueError(f"Geometry {new_geom} not found in project.")
121
+
122
+ # Update all geometry-related columns
123
+ mask = ras_obj.plan_df['plan_number'] == plan_number
124
+ ras_obj.plan_df.loc[mask, 'geom_number'] = new_geom
125
+ ras_obj.plan_df.loc[mask, 'Geom File'] = f"g{new_geom}"
126
+ geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom}"
127
+ ras_obj.plan_df.loc[mask, 'Geom Path'] = str(geom_path)
128
+
129
+ logger.info(f"Geometry for plan {plan_number} set to {new_geom}")
130
+ logger.debug("Updated plan DataFrame:")
131
+ logger.debug(ras_obj.plan_df)
132
+
133
+ # Update project file and reinitialize
134
+ RasUtils.update_file(ras_obj.prj_file, RasPlan._update_geom_in_file, plan_number, new_geom)
135
+ ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
136
+
137
+ return ras_obj.plan_df
138
+
139
+ @staticmethod
140
+ def _update_geom_in_file(lines, plan_number, new_geom):
141
+ plan_pattern = re.compile(rf"^Plan File=p{plan_number}", re.IGNORECASE)
142
+ geom_pattern = re.compile(r"^Geom File=g\d+", re.IGNORECASE)
143
+
144
+ for i, line in enumerate(lines):
145
+ if plan_pattern.match(line):
146
+ for j in range(i+1, len(lines)):
147
+ if geom_pattern.match(lines[j]):
148
+ lines[j] = f"Geom File=g{new_geom}\n"
149
+ logger.info(f"Updated Geom File in project file to g{new_geom} for plan {plan_number}")
150
+ break
151
+ break
152
+ return lines
153
+
154
+ @staticmethod
155
+ @log_call
156
+ def set_steady(plan_number: str, new_steady_flow_number: str, ras_object=None):
157
+ """
158
+ Apply a steady flow file to a plan file.
159
+
160
+ Parameters:
161
+ plan_number (str): Plan number (e.g., '02')
162
+ new_steady_flow_number (str): Steady flow number to apply (e.g., '01')
163
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
164
+
165
+ Returns:
166
+ None
167
+
168
+ Raises:
169
+ ValueError: If the specified steady flow number is not found in the project file
170
+ FileNotFoundError: If the specified plan file is not found
171
+
172
+ Example:
173
+ >>> RasPlan.set_steady('02', '01')
174
+
175
+ Note:
176
+ This function updates the ras object's dataframes after modifying the project structure.
177
+ """
178
+ ras_obj = ras_object or ras
179
+ ras_obj.check_initialized()
180
+
181
+ ras_obj.flow_df = ras_obj.get_flow_entries()
182
+
183
+ if new_steady_flow_number not in ras_obj.flow_df['flow_number'].values:
184
+ raise ValueError(f"Steady flow number {new_steady_flow_number} not found in project file.")
185
+
186
+ plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
187
+ if not plan_file_path:
188
+ raise FileNotFoundError(f"Plan file not found: {plan_number}")
189
+
190
+ try:
191
+ RasUtils.update_file(plan_file_path, RasPlan._update_steady_in_file, new_steady_flow_number)
192
+
193
+ # Update all dataframes
194
+ ras_obj.plan_df = ras_obj.get_plan_entries()
195
+
196
+ # Update flow-related columns
197
+ mask = ras_obj.plan_df['plan_number'] == plan_number
198
+ flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_steady_flow_number}"
199
+ ras_obj.plan_df.loc[mask, 'Flow File'] = f"f{new_steady_flow_number}"
200
+ ras_obj.plan_df.loc[mask, 'Flow Path'] = str(flow_path)
201
+ ras_obj.plan_df.loc[mask, 'unsteady_number'] = None
202
+
203
+ # Update remaining dataframes
204
+ ras_obj.geom_df = ras_obj.get_geom_entries()
205
+ ras_obj.flow_df = ras_obj.get_flow_entries()
206
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
207
+
208
+ except Exception as e:
209
+ raise IOError(f"Failed to update steady flow file: {e}")
210
+
211
+ @staticmethod
212
+ def _update_steady_in_file(lines, new_steady_flow_number):
213
+ return [f"Flow File=f{new_steady_flow_number}\n" if line.startswith("Flow File=f") else line for line in lines]
214
+
215
+ @staticmethod
216
+ @log_call
217
+ def set_unsteady(plan_number: str, new_unsteady_flow_number: str, ras_object=None):
218
+ """
219
+ Apply an unsteady flow file to a plan file.
220
+
221
+ Parameters:
222
+ plan_number (str): Plan number (e.g., '04')
223
+ new_unsteady_flow_number (str): Unsteady flow number to apply (e.g., '01')
224
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
225
+
226
+ Returns:
227
+ None
228
+
229
+ Raises:
230
+ ValueError: If the specified unsteady number is not found in the project file
231
+ FileNotFoundError: If the specified plan file is not found
232
+
233
+ Example:
234
+ >>> RasPlan.set_unsteady('04', '01')
235
+
236
+ Note:
237
+ This function updates the ras object's dataframes after modifying the project structure.
238
+ """
239
+ ras_obj = ras_object or ras
240
+ ras_obj.check_initialized()
241
+
242
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
243
+
244
+ if new_unsteady_flow_number not in ras_obj.unsteady_df['unsteady_number'].values:
245
+ raise ValueError(f"Unsteady number {new_unsteady_flow_number} not found in project file.")
246
+
247
+ plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
248
+ if not plan_file_path:
249
+ raise FileNotFoundError(f"Plan file not found: {plan_number}")
250
+
251
+ try:
252
+ RasUtils.update_file(plan_file_path, RasPlan._update_unsteady_in_file, new_unsteady_flow_number)
253
+
254
+ # Update all dataframes
255
+ ras_obj.plan_df = ras_obj.get_plan_entries()
256
+
257
+ # Update flow-related columns
258
+ mask = ras_obj.plan_df['plan_number'] == plan_number
259
+ flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_flow_number}"
260
+ ras_obj.plan_df.loc[mask, 'Flow File'] = f"u{new_unsteady_flow_number}"
261
+ ras_obj.plan_df.loc[mask, 'Flow Path'] = str(flow_path)
262
+ ras_obj.plan_df.loc[mask, 'unsteady_number'] = new_unsteady_flow_number
263
+
264
+ # Update remaining dataframes
265
+ ras_obj.geom_df = ras_obj.get_geom_entries()
266
+ ras_obj.flow_df = ras_obj.get_flow_entries()
267
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
268
+
269
+ except Exception as e:
270
+ raise IOError(f"Failed to update unsteady flow file: {e}")
271
+
272
+ @staticmethod
273
+ def _update_unsteady_in_file(lines, new_unsteady_flow_number):
274
+ return [f"Unsteady File=u{new_unsteady_flow_number}\n" if line.startswith("Unsteady File=u") else line for line in lines]
275
+
276
+ @staticmethod
277
+ @log_call
278
+ def set_num_cores(plan_number, num_cores, ras_object=None):
279
+ """
280
+ Update the maximum number of cores to use in the HEC-RAS plan file.
281
+
282
+ Parameters:
283
+ plan_number (str): Plan number (e.g., '02') or full path to the plan file
284
+ num_cores (int): Maximum number of cores to use
285
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
286
+
287
+ Returns:
288
+ None
289
+
290
+ Number of cores is controlled by the following parameters in the plan file corresponding to 1D, 2D, Pipe Systems and Pump Stations:
291
+ UNET D1 Cores=
292
+ UNET D2 Cores=
293
+ PS Cores=
294
+
295
+ Where a value of "0" is used for "All Available" cores, and values of 1 or more are used to specify the number of cores to use.
296
+ For complex 1D/2D models with pipe systems, a more complex approach may be needed to optimize performance. (Suggest writing a custom function based on this code).
297
+ This function simply sets the "num_cores" parameter for ALL instances of the above parameters in the plan file.
298
+
299
+
300
+ Notes on setting num_cores in HEC-RAS:
301
+ The recommended setting for num_cores is 2 (most efficient) to 8 (most performant)
302
+ More details in the HEC-Commander Repository Blog "Benchmarking is All You Need"
303
+ https://github.com/billk-FM/HEC-Commander/blob/main/Blog/7._Benchmarking_Is_All_You_Need.md
304
+
305
+ Microsoft Windows has a maximum of 64 cores that can be allocated to a single Ras.exe process.
306
+
307
+ Example:
308
+ >>> # Using plan number
309
+ >>> RasPlan.set_num_cores('02', 4)
310
+ >>> # Using full path to plan file
311
+ >>> RasPlan.set_num_cores('/path/to/project.p02', 4)
312
+
313
+ Note:
314
+ This function updates the ras object's dataframes after modifying the project structure.
315
+ """
316
+ ras_obj = ras_object or ras
317
+ ras_obj.check_initialized()
318
+
319
+ plan_file_path = RasUtils.get_plan_path(plan_number, ras_obj)
320
+ if not plan_file_path:
321
+ raise FileNotFoundError(f"Plan file not found: {plan_number}. Please provide a valid plan number or path.")
322
+
323
+ def update_num_cores(lines):
324
+ updated_lines = []
325
+ for line in lines:
326
+ if any(param in line for param in ["UNET D1 Cores=", "UNET D2 Cores=", "PS Cores="]):
327
+ param_name = line.split("=")[0]
328
+ updated_lines.append(f"{param_name}= {num_cores}\n")
329
+ else:
330
+ updated_lines.append(line)
331
+ return updated_lines
332
+
333
+ try:
334
+ RasUtils.update_file(plan_file_path, update_num_cores)
335
+ except Exception as e:
336
+ raise IOError(f"Failed to update number of cores in plan file: {e}")
337
+
338
+ # Update the ras object's dataframes
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
+ @staticmethod
345
+ @log_call
346
+ def set_geom_preprocessor(file_path, run_htab, use_ib_tables, ras_object=None):
347
+ """
348
+ Update the simulation plan file to modify the `Run HTab` and `UNET Use Existing IB Tables` settings.
349
+
350
+ Parameters:
351
+ file_path (str): Path to the simulation plan file (.p06 or similar) that you want to modify.
352
+ run_htab (int): Value for the `Run HTab` setting:
353
+ - `0` : Do not run the geometry preprocessor, use existing geometry tables.
354
+ - `-1` : Run the geometry preprocessor, forcing a recomputation of the geometry tables.
355
+ use_ib_tables (int): Value for the `UNET Use Existing IB Tables` setting:
356
+ - `0` : Use existing interpolation/boundary (IB) tables without recomputing them.
357
+ - `-1` : Do not use existing IB tables, force a recomputation.
358
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
359
+
360
+ Returns:
361
+ None
362
+
363
+ Raises:
364
+ ValueError: If `run_htab` or `use_ib_tables` are not integers or not within the accepted values (`0` or `-1`).
365
+ FileNotFoundError: If the specified file does not exist.
366
+ IOError: If there is an error reading or writing the file.
367
+
368
+ Example:
369
+ >>> RasPlan.set_geom_preprocessor('/path/to/project.p06', run_htab=-1, use_ib_tables=0)
370
+
371
+ Note:
372
+ This function updates the ras object's dataframes after modifying the project structure.
373
+ """
374
+ ras_obj = ras_object or ras
375
+ ras_obj.check_initialized()
376
+
377
+ if run_htab not in [-1, 0]:
378
+ raise ValueError("Invalid value for `Run HTab`. Expected `0` or `-1`.")
379
+ if use_ib_tables not in [-1, 0]:
380
+ raise ValueError("Invalid value for `UNET Use Existing IB Tables`. Expected `0` or `-1`.")
381
+
382
+ def update_geom_preprocessor(lines, run_htab, use_ib_tables):
383
+ updated_lines = []
384
+ for line in lines:
385
+ if line.lstrip().startswith("Run HTab="):
386
+ updated_lines.append(f"Run HTab= {run_htab} \n")
387
+ elif line.lstrip().startswith("UNET Use Existing IB Tables="):
388
+ updated_lines.append(f"UNET Use Existing IB Tables= {use_ib_tables} \n")
389
+ else:
390
+ updated_lines.append(line)
391
+ return updated_lines
392
+
393
+ try:
394
+ RasUtils.update_file(file_path, update_geom_preprocessor, run_htab, use_ib_tables)
395
+ except FileNotFoundError:
396
+ raise FileNotFoundError(f"The file '{file_path}' does not exist.")
397
+ except IOError as e:
398
+ raise IOError(f"An error occurred while reading or writing the file: {e}")
399
+
400
+ # Update the ras object's dataframes
401
+ ras_obj.plan_df = ras_obj.get_plan_entries()
402
+ ras_obj.geom_df = ras_obj.get_geom_entries()
403
+ ras_obj.flow_df = ras_obj.get_flow_entries()
404
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
405
+
406
+ @staticmethod
407
+ @log_call
408
+ def get_results_path(plan_number: str, ras_object=None) -> Optional[str]:
409
+ """
410
+ Retrieve the results file path for a given HEC-RAS plan number.
411
+
412
+ Args:
413
+ plan_number (str): The HEC-RAS plan number for which to find the results path.
414
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
415
+
416
+ Returns:
417
+ Optional[str]: The full path to the results file if found and the file exists, or None if not found.
418
+
419
+ Raises:
420
+ RuntimeError: If the project is not initialized.
421
+
422
+ Example:
423
+ >>> ras_plan = RasPlan()
424
+ >>> results_path = ras_plan.get_results_path('01')
425
+ >>> if results_path:
426
+ ... print(f"Results file found at: {results_path}")
427
+ ... else:
428
+ ... print("Results file not found.")
429
+ """
430
+ ras_obj = ras_object or ras
431
+ ras_obj.check_initialized()
432
+
433
+ # Update the plan dataframe in the ras instance to ensure it is current
434
+ ras_obj.plan_df = ras_obj.get_plan_entries()
435
+
436
+ # Ensure plan_number is a string
437
+ plan_number = str(plan_number).zfill(2)
438
+
439
+ plan_entry = ras_obj.plan_df[ras_obj.plan_df['plan_number'] == plan_number]
440
+ if not plan_entry.empty:
441
+ results_path = plan_entry['HDF_Results_Path'].iloc[0]
442
+ if results_path and Path(results_path).exists():
443
+ return results_path
444
+ else:
445
+ return None
446
+ else:
447
+ return None
448
+
449
+ @staticmethod
450
+ @log_call
451
+ def get_plan_path(plan_number: str, ras_object=None) -> Optional[str]:
452
+ """
453
+ Return the full path for a given plan number.
454
+
455
+ This method ensures that the latest plan entries are included by refreshing
456
+ the plan dataframe before searching for the requested plan number.
457
+
458
+ Args:
459
+ plan_number (str): The plan number to search for.
460
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
461
+
462
+ Returns:
463
+ Optional[str]: The full path of the plan file if found, None otherwise.
464
+
465
+ Raises:
466
+ RuntimeError: If the project is not initialized.
467
+
468
+ Example:
469
+ >>> ras_plan = RasPlan()
470
+ >>> plan_path = ras_plan.get_plan_path('01')
471
+ >>> if plan_path:
472
+ ... print(f"Plan file found at: {plan_path}")
473
+ ... else:
474
+ ... print("Plan file not found.")
475
+ """
476
+ ras_obj = ras_object or ras
477
+ ras_obj.check_initialized()
478
+
479
+ plan_df = ras_obj.get_plan_entries()
480
+
481
+ plan_path = plan_df[plan_df['plan_number'] == plan_number]
482
+
483
+ if not plan_path.empty:
484
+ if 'full_path' in plan_path.columns and not pd.isna(plan_path['full_path'].iloc[0]):
485
+ return plan_path['full_path'].iloc[0]
486
+ else:
487
+ # Fallback to constructing path
488
+ return str(ras_obj.project_folder / f"{ras_obj.project_name}.p{plan_number}")
489
+ return None
490
+
491
+ @staticmethod
492
+ @log_call
493
+ def get_flow_path(flow_number: str, ras_object=None) -> Optional[str]:
494
+ """
495
+ Return the full path for a given flow number.
496
+
497
+ Args:
498
+ flow_number (str): The flow number to search for.
499
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
500
+
501
+ Returns:
502
+ Optional[str]: The full path of the flow file if found, None otherwise.
503
+
504
+ Raises:
505
+ RuntimeError: If the project is not initialized.
506
+
507
+ Example:
508
+ >>> ras_plan = RasPlan()
509
+ >>> flow_path = ras_plan.get_flow_path('01')
510
+ >>> if flow_path:
511
+ ... print(f"Flow file found at: {flow_path}")
512
+ ... else:
513
+ ... print("Flow file not found.")
514
+ """
515
+ ras_obj = ras_object or ras
516
+ ras_obj.check_initialized()
517
+
518
+ # Use updated flow dataframe
519
+ ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
520
+
521
+ flow_path = ras_obj.flow_df[ras_obj.flow_df['flow_number'] == flow_number]
522
+ if not flow_path.empty:
523
+ full_path = flow_path['full_path'].iloc[0]
524
+ return full_path
525
+ else:
526
+ return None
527
+
528
+ @staticmethod
529
+ @log_call
530
+ def get_unsteady_path(unsteady_number: str, ras_object=None) -> Optional[str]:
531
+ """
532
+ Return the full path for a given unsteady number.
533
+
534
+ Args:
535
+ unsteady_number (str): The unsteady number to search for.
536
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
537
+
538
+ Returns:
539
+ Optional[str]: The full path of the unsteady file if found, None otherwise.
540
+
541
+ Raises:
542
+ RuntimeError: If the project is not initialized.
543
+
544
+ Example:
545
+ >>> ras_plan = RasPlan()
546
+ >>> unsteady_path = ras_plan.get_unsteady_path('01')
547
+ >>> if unsteady_path:
548
+ ... print(f"Unsteady file found at: {unsteady_path}")
549
+ ... else:
550
+ ... print("Unsteady file not found.")
551
+ """
552
+ ras_obj = ras_object or ras
553
+ ras_obj.check_initialized()
554
+
555
+ # Use updated unsteady dataframe
556
+ ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
557
+
558
+ unsteady_path = ras_obj.unsteady_df[ras_obj.unsteady_df['unsteady_number'] == unsteady_number]
559
+ if not unsteady_path.empty:
560
+ full_path = unsteady_path['full_path'].iloc[0]
561
+ return full_path
562
+ else:
563
+ return None
564
+
565
+ @staticmethod
566
+ @log_call
567
+ def get_geom_path(geom_number: str, ras_object=None) -> Optional[str]:
568
+ """
569
+ Return the full path for a given geometry number.
570
+
571
+ Args:
572
+ geom_number (str): The geometry number to search for.
573
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
574
+
575
+ Returns:
576
+ Optional[str]: The full path of the geometry file if found, None otherwise.
577
+
578
+ Raises:
579
+ RuntimeError: If the project is not initialized.
580
+
581
+ Example:
582
+ >>> ras_plan = RasPlan()
583
+ >>> geom_path = ras_plan.get_geom_path('01')
584
+ >>> if geom_path:
585
+ ... print(f"Geometry file found at: {geom_path}")
586
+ ... else:
587
+ ... print("Geometry file not found.")
588
+ """
589
+ ras_obj = ras_object or ras
590
+ ras_obj.check_initialized()
591
+
592
+ # Use updated geom dataframe
593
+ ras_obj.geom_df = ras_obj.get_prj_entries('Geom')
594
+
595
+ geom_path = ras_obj.geom_df[ras_obj.geom_df['geom_number'] == geom_number]
596
+ if not geom_path.empty:
597
+ full_path = geom_path['full_path'].iloc[0]
598
+ return full_path
599
+ else:
600
+ return None
601
+
602
+ # Clone Functions to copy unsteady, flow, and geometry files from templates
603
+
604
+ @staticmethod
605
+ @log_call
606
+ def clone_plan(template_plan, new_plan_shortid=None, ras_object=None):
607
+ """
608
+ Create a new plan file based on a template and update the project file.
609
+
610
+ Parameters:
611
+ template_plan (str): Plan number to use as template (e.g., '01')
612
+ new_plan_shortid (str, optional): New short identifier for the plan file
613
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
614
+
615
+ Returns:
616
+ str: New plan number
617
+
618
+ Example:
619
+ >>> ras_plan = RasPlan()
620
+ >>> new_plan_number = ras_plan.clone_plan('01', new_plan_shortid='New Plan')
621
+ >>> print(f"New plan created with number: {new_plan_number}")
622
+
623
+ Note:
624
+ This function updates the ras object's dataframes after modifying the project structure.
625
+ """
626
+ ras_obj = ras_object or ras
627
+ ras_obj.check_initialized()
628
+
629
+ # Update plan entries without reinitializing the entire project
630
+ ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
631
+
632
+ new_plan_num = RasPlan.get_next_number(ras_obj.plan_df['plan_number'])
633
+ template_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{template_plan}"
634
+ new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
635
+
636
+ def update_shortid(lines):
637
+ shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
638
+ for i, line in enumerate(lines):
639
+ match = shortid_pattern.match(line.strip())
640
+ if match:
641
+ current_shortid = match.group(1)
642
+ if new_plan_shortid is None:
643
+ new_shortid = (current_shortid + "_copy")[:24]
644
+ else:
645
+ new_shortid = new_plan_shortid[:24]
646
+ lines[i] = f"Short Identifier={new_shortid}\n"
647
+ break
648
+ return lines
649
+
650
+ # Use RasUtils to clone the file and update the short identifier
651
+ RasUtils.clone_file(template_plan_path, new_plan_path, update_shortid)
652
+
653
+ # Use RasUtils to update the project file
654
+ RasUtils.update_project_file(ras_obj.prj_file, 'Plan', new_plan_num, ras_object=ras_obj)
655
+
656
+ # Re-initialize the ras global object
657
+ ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
658
+
659
+ ras_obj.plan_df = ras_obj.get_plan_entries()
660
+ ras_obj.geom_df = ras_obj.get_geom_entries()
661
+ ras_obj.flow_df = ras_obj.get_flow_entries()
662
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
663
+
664
+ return new_plan_num
665
+
666
+ @staticmethod
667
+ @log_call
668
+ def clone_unsteady(template_unsteady, ras_object=None):
669
+ """
670
+ Copy unsteady flow files from a template, find the next unsteady number,
671
+ and update the project file accordingly.
672
+
673
+ Parameters:
674
+ template_unsteady (str): Unsteady flow number to be used as a template (e.g., '01')
675
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
676
+
677
+ Returns:
678
+ str: New unsteady flow number (e.g., '03')
679
+
680
+ Example:
681
+ >>> ras_plan = RasPlan()
682
+ >>> new_unsteady_num = ras_plan.clone_unsteady('01')
683
+ >>> print(f"New unsteady flow file created: u{new_unsteady_num}")
684
+
685
+ Note:
686
+ This function updates the ras object's dataframes after modifying the project structure.
687
+ """
688
+ ras_obj = ras_object or ras
689
+ ras_obj.check_initialized()
690
+
691
+ # Update unsteady entries without reinitializing the entire project
692
+ ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
693
+
694
+ new_unsteady_num = RasPlan.get_next_number(ras_obj.unsteady_df['unsteady_number'])
695
+ template_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}"
696
+ new_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}"
697
+
698
+ # Use RasUtils to clone the file
699
+ RasUtils.clone_file(template_unsteady_path, new_unsteady_path)
700
+
701
+ # Copy the corresponding .hdf file if it exists
702
+ template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}.hdf"
703
+ new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}.hdf"
704
+ if template_hdf_path.exists():
705
+ shutil.copy(template_hdf_path, new_hdf_path)
706
+
707
+ # Use RasUtils to update the project file
708
+ RasUtils.update_project_file(ras_obj.prj_file, 'Unsteady', new_unsteady_num, ras_object=ras_obj)
709
+
710
+ # Re-initialize the ras global object
711
+ ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
712
+
713
+ ras_obj.plan_df = ras_obj.get_plan_entries()
714
+ ras_obj.geom_df = ras_obj.get_geom_entries()
715
+ ras_obj.flow_df = ras_obj.get_flow_entries()
716
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
717
+
718
+ return new_unsteady_num
719
+
720
+
721
+ @staticmethod
722
+ @log_call
723
+ def clone_steady(template_flow, ras_object=None):
724
+ """
725
+ Copy steady flow files from a template, find the next flow number,
726
+ and update the project file accordingly.
727
+
728
+ Parameters:
729
+ template_flow (str): Flow number to be used as a template (e.g., '01')
730
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
731
+
732
+ Returns:
733
+ str: New flow number (e.g., '03')
734
+
735
+ Example:
736
+ >>> ras_plan = RasPlan()
737
+ >>> new_flow_num = ras_plan.clone_steady('01')
738
+ >>> print(f"New steady flow file created: f{new_flow_num}")
739
+
740
+ Note:
741
+ This function updates the ras object's dataframes after modifying the project structure.
742
+ """
743
+ ras_obj = ras_object or ras
744
+ ras_obj.check_initialized()
745
+
746
+ # Update flow entries without reinitializing the entire project
747
+ ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
748
+
749
+ new_flow_num = RasPlan.get_next_number(ras_obj.flow_df['flow_number'])
750
+ template_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{template_flow}"
751
+ new_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_flow_num}"
752
+
753
+ # Use RasUtils to clone the file
754
+ RasUtils.clone_file(template_flow_path, new_flow_path)
755
+
756
+ # Use RasUtils to update the project file
757
+ RasUtils.update_project_file(ras_obj.prj_file, 'Flow', new_flow_num, ras_object=ras_obj)
758
+
759
+ # Re-initialize the ras global object
760
+ ras_obj.initialize(ras_obj.project_folder, ras_obj.ras_exe_path)
761
+
762
+ ras_obj.plan_df = ras_obj.get_plan_entries()
763
+ ras_obj.geom_df = ras_obj.get_geom_entries()
764
+ ras_obj.flow_df = ras_obj.get_flow_entries()
765
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
766
+
767
+ return new_flow_num
768
+
769
+ @staticmethod
770
+ @log_call
771
+ def clone_geom(template_geom, ras_object=None):
772
+ """
773
+ Copy geometry files from a template, find the next geometry number,
774
+ and update the project file accordingly.
775
+
776
+ Parameters:
777
+ template_geom (str): Geometry number to be used as a template (e.g., '01')
778
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
779
+
780
+ Returns:
781
+ str: New geometry number (e.g., '03')
782
+
783
+ Note:
784
+ This function updates the ras object's dataframes after modifying the project structure.
785
+ """
786
+ ras_obj = ras_object or ras
787
+ ras_obj.check_initialized()
788
+
789
+ # Update geometry entries without reinitializing the entire project
790
+ ras_obj.geom_df = ras_obj.get_prj_entries('Geom')
791
+
792
+ new_geom_num = RasPlan.get_next_number(ras_obj.geom_df['geom_number'])
793
+ template_geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{template_geom}"
794
+ new_geom_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom_num}"
795
+
796
+ # Use RasUtils to clone the file
797
+ RasUtils.clone_file(template_geom_path, new_geom_path)
798
+
799
+ # Handle HDF file copy
800
+ template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{template_geom}.hdf"
801
+ new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{new_geom_num}.hdf"
802
+ if template_hdf_path.is_file():
803
+ RasUtils.clone_file(template_hdf_path, new_hdf_path)
804
+
805
+ # Use RasUtils to update the project file
806
+ RasUtils.update_project_file(ras_obj.prj_file, 'Geom', new_geom_num, ras_object=ras_obj)
807
+
808
+ # Update all dataframes in the ras object
809
+ ras_obj.plan_df = ras_obj.get_plan_entries()
810
+ ras_obj.geom_df = ras_obj.get_geom_entries()
811
+ ras_obj.flow_df = ras_obj.get_flow_entries()
812
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
813
+
814
+ return new_geom_num
815
+
816
+ @staticmethod
817
+ @log_call
818
+ def get_next_number(existing_numbers):
819
+ """
820
+ Determine the next available number from a list of existing numbers.
821
+
822
+ Parameters:
823
+ existing_numbers (list): List of existing numbers as strings
824
+
825
+ Returns:
826
+ str: Next available number as a zero-padded string
827
+
828
+ Example:
829
+ >>> existing_numbers = ['01', '02', '04']
830
+ >>> RasPlan.get_next_number(existing_numbers)
831
+ '03'
832
+ >>> existing_numbers = ['01', '02', '03']
833
+ >>> RasPlan.get_next_number(existing_numbers)
834
+ '04'
835
+ """
836
+ existing_numbers = sorted(int(num) for num in existing_numbers)
837
+ next_number = 1
838
+ for num in existing_numbers:
839
+ if num == next_number:
840
+ next_number += 1
841
+ else:
842
+ break
843
+ return f"{next_number:02d}"
844
+
845
+ @staticmethod
846
+ @log_call
847
+ def get_plan_value(
848
+ plan_number_or_path: Union[str, Path],
849
+ key: str,
850
+ ras_object=None
851
+ ) -> Any:
852
+ """
853
+ Retrieve a specific value from a HEC-RAS plan file.
854
+
855
+ Parameters:
856
+ plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
857
+ key (str): The key to retrieve from the plan file
858
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
859
+
860
+ Returns:
861
+ Any: The value associated with the specified key
862
+
863
+ Raises:
864
+ ValueError: If the plan file is not found
865
+ IOError: If there's an error reading the plan file
866
+
867
+ Available keys and their expected types:
868
+ - 'Computation Interval' (str): Time value for computational time step (e.g., '5SEC', '2MIN')
869
+ - 'DSS File' (str): Name of the DSS file used
870
+ - 'Flow File' (str): Name of the flow input file
871
+ - 'Friction Slope Method' (int): Method selection for friction slope (e.g., 1, 2)
872
+ - 'Geom File' (str): Name of the geometry input file
873
+ - 'Mapping Interval' (str): Time interval for mapping output
874
+ - 'Plan File' (str): Name of the plan file
875
+ - 'Plan Title' (str): Title of the simulation plan
876
+ - 'Program Version' (str): Version number of HEC-RAS
877
+ - 'Run HTab' (int): Flag to run HTab module (-1 or 1)
878
+ - 'Run Post Process' (int): Flag to run post-processing (-1 or 1)
879
+ - 'Run Sediment' (int): Flag to run sediment transport module (0 or 1)
880
+ - 'Run UNET' (int): Flag to run unsteady network module (-1 or 1)
881
+ - 'Run WQNET' (int): Flag to run water quality module (0 or 1)
882
+ - 'Short Identifier' (str): Short name or ID for the plan
883
+ - 'Simulation Date' (str): Start and end dates/times for simulation
884
+ - 'UNET D1 Cores' (int): Number of cores used in 1D calculations
885
+ - 'UNET D2 Cores' (int): Number of cores used in 2D calculations
886
+ - 'PS Cores' (int): Number of cores used in parallel simulation
887
+ - 'UNET Use Existing IB Tables' (int): Flag for using existing internal boundary tables (-1, 0, or 1)
888
+ - 'UNET 1D Methodology' (str): 1D calculation methodology
889
+ - 'UNET D2 Solver Type' (str): 2D solver type
890
+ - 'UNET D2 Name' (str): Name of the 2D area
891
+ - 'Run RASMapper' (int): Flag to run RASMapper for floodplain mapping (-1 for off, 0 for on)
892
+
893
+ Note:
894
+ Writing Multi line keys like 'Description' are not supported by this function.
895
+
896
+ Example:
897
+ >>> computation_interval = RasPlan.get_plan_value("01", "Computation Interval")
898
+ >>> print(f"Computation interval: {computation_interval}")
899
+ """
900
+ ras_obj = ras_object or ras
901
+ ras_obj.check_initialized()
902
+
903
+ supported_plan_keys = {
904
+ 'Description', 'Computation Interval', 'DSS File', 'Flow File', 'Friction Slope Method',
905
+ 'Geom File', 'Mapping Interval', 'Plan File', 'Plan Title', 'Program Version',
906
+ 'Run HTab', 'Run Post Process', 'Run Sediment', 'Run UNET', 'Run WQNET',
907
+ 'Short Identifier', 'Simulation Date', 'UNET D1 Cores', 'UNET D2 Cores', 'PS Cores',
908
+ 'UNET Use Existing IB Tables', 'UNET 1D Methodology', 'UNET D2 Solver Type',
909
+ 'UNET D2 Name', 'Run RASMapper', 'Run HTab', 'Run UNET'
910
+ }
911
+
912
+ if key not in supported_plan_keys:
913
+ logger = logging.getLogger(__name__)
914
+ logger.warning(f"Unknown key: {key}. Valid keys are: {', '.join(supported_plan_keys)}\n Add more keys and explanations in get_plan_value() as needed.")
915
+
916
+ plan_file_path = Path(plan_number_or_path)
917
+ if not plan_file_path.is_file():
918
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object=ras_obj)
919
+ if plan_file_path is None or not Path(plan_file_path).exists():
920
+ raise ValueError(f"Plan file not found: {plan_file_path}")
921
+
922
+ try:
923
+ with open(plan_file_path, 'r') as file:
924
+ content = file.read()
925
+ except IOError as e:
926
+ logger = logging.getLogger(__name__)
927
+ logger.error(f"Error reading plan file {plan_file_path}: {e}")
928
+ raise
929
+
930
+ # Handle core settings specially to convert to integers
931
+ core_keys = {'UNET D1 Cores', 'UNET D2 Cores', 'PS Cores'}
932
+ if key in core_keys:
933
+ pattern = f"{key}=(.*)"
934
+ match = re.search(pattern, content)
935
+ if match:
936
+ try:
937
+ return int(match.group(1).strip())
938
+ except ValueError:
939
+ logger = logging.getLogger(__name__)
940
+ logger.error(f"Could not convert {key} value to integer")
941
+ return None
942
+ else:
943
+ logger = logging.getLogger(__name__)
944
+ logger.error(f"Key '{key}' not found in the plan file.")
945
+ return None
946
+ elif key == 'Description':
947
+ match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
948
+ return match.group(1).strip() if match else None
949
+ else:
950
+ pattern = f"{key}=(.*)"
951
+ match = re.search(pattern, content)
952
+ if match:
953
+ return match.group(1).strip()
954
+ else:
955
+ logger = logging.getLogger(__name__)
956
+ logger.error(f"Key '{key}' not found in the plan file.")
957
+ return None
958
+
959
+
960
+ # NEW FUNCTIONS THAT NEED TESTING AND EXAMPLES
961
+
962
+
963
+ @staticmethod
964
+ @log_call
965
+ def update_run_flags(
966
+ plan_number_or_path: Union[str, Path],
967
+ geometry_preprocessor: bool = None,
968
+ unsteady_flow_simulation: bool = None,
969
+ run_sediment: bool = None,
970
+ post_processor: bool = None,
971
+ floodplain_mapping: bool = None,
972
+ ras_object=None
973
+ ) -> None:
974
+ """
975
+ Update the run flags in a HEC-RAS plan file.
976
+
977
+ Parameters:
978
+ plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
979
+ geometry_preprocessor (bool, optional): Flag for Geometry Preprocessor
980
+ unsteady_flow_simulation (bool, optional): Flag for Unsteady Flow Simulation
981
+ run_sediment (bool, optional): Flag for run_sediment
982
+ post_processor (bool, optional): Flag for Post Processor
983
+ floodplain_mapping (bool, optional): Flag for Floodplain Mapping
984
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
985
+
986
+ Raises:
987
+ ValueError: If the plan file is not found
988
+ IOError: If there's an error reading or writing the plan file
989
+
990
+ Example:
991
+ >>> RasPlan.update_run_flags("01", geometry_preprocessor=True, unsteady_flow_simulation=True, run_sediment=False, post_processor=True, floodplain_mapping=False)
992
+ """
993
+ ras_obj = ras_object or ras
994
+ ras_obj.check_initialized()
995
+
996
+ plan_file_path = Path(plan_number_or_path)
997
+ if not plan_file_path.is_file():
998
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object=ras_obj)
999
+ if plan_file_path is None or not Path(plan_file_path).exists():
1000
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1001
+
1002
+ flag_mapping = {
1003
+ 'geometry_preprocessor': ('Run HTab', geometry_preprocessor),
1004
+ 'unsteady_flow_simulation': ('Run UNet', unsteady_flow_simulation),
1005
+ 'run_sediment': ('Run run_sediment', run_sediment),
1006
+ 'post_processor': ('Run PostProcess', post_processor),
1007
+ 'floodplain_mapping': ('Run RASMapper', floodplain_mapping)
1008
+ }
1009
+
1010
+ try:
1011
+ with open(plan_file_path, 'r') as file:
1012
+ lines = file.readlines()
1013
+
1014
+ for i, line in enumerate(lines):
1015
+ for key, (file_key, value) in flag_mapping.items():
1016
+ if value is not None and line.strip().startswith(file_key):
1017
+ lines[i] = f"{file_key}= {1 if value else 0}\n"
1018
+
1019
+ with open(plan_file_path, 'w') as file:
1020
+ file.writelines(lines)
1021
+
1022
+ logger = logging.getLogger(__name__)
1023
+ logger.info(f"Successfully updated run flags in plan file: {plan_file_path}")
1024
+
1025
+ except IOError as e:
1026
+ logger = logging.getLogger(__name__)
1027
+ logger.error(f"Error updating run flags in plan file {plan_file_path}: {e}")
1028
+ raise
1029
+
1030
+
1031
+
1032
+ @staticmethod
1033
+ @log_call
1034
+ def update_plan_intervals(
1035
+ plan_number_or_path: Union[str, Path],
1036
+ computation_interval: Optional[str] = None,
1037
+ output_interval: Optional[str] = None,
1038
+ instantaneous_interval: Optional[str] = None,
1039
+ mapping_interval: Optional[str] = None,
1040
+ ras_object=None
1041
+ ) -> None:
1042
+ """
1043
+ Update the computation and output intervals in a HEC-RAS plan file.
1044
+
1045
+ Parameters:
1046
+ plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
1047
+ computation_interval (Optional[str]): The new computation interval. Valid entries include:
1048
+ '1SEC', '2SEC', '3SEC', '4SEC', '5SEC', '6SEC', '10SEC', '15SEC', '20SEC', '30SEC',
1049
+ '1MIN', '2MIN', '3MIN', '4MIN', '5MIN', '6MIN', '10MIN', '15MIN', '20MIN', '30MIN',
1050
+ '1HOUR', '2HOUR', '3HOUR', '4HOUR', '6HOUR', '8HOUR', '12HOUR', '1DAY'
1051
+ output_interval (Optional[str]): The new output interval. Valid entries are the same as computation_interval.
1052
+ instantaneous_interval (Optional[str]): The new instantaneous interval. Valid entries are the same as computation_interval.
1053
+ mapping_interval (Optional[str]): The new mapping interval. Valid entries are the same as computation_interval.
1054
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
1055
+
1056
+ Raises:
1057
+ ValueError: If the plan file is not found or if an invalid interval is provided
1058
+ IOError: If there's an error reading or writing the plan file
1059
+
1060
+ Note: This function does not check if the intervals are equal divisors. Ensure you use valid values from HEC-RAS.
1061
+
1062
+ Example:
1063
+ >>> RasPlan.update_plan_intervals("01", computation_interval="5SEC", output_interval="1MIN", instantaneous_interval="1HOUR", mapping_interval="5MIN")
1064
+ >>> RasPlan.update_plan_intervals("/path/to/plan.p01", computation_interval="10SEC", output_interval="30SEC")
1065
+ """
1066
+ ras_obj = ras_object or ras
1067
+ ras_obj.check_initialized()
1068
+
1069
+ plan_file_path = Path(plan_number_or_path)
1070
+ if not plan_file_path.is_file():
1071
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object=ras_obj)
1072
+ if plan_file_path is None or not Path(plan_file_path).exists():
1073
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1074
+
1075
+ valid_intervals = [
1076
+ '1SEC', '2SEC', '3SEC', '4SEC', '5SEC', '6SEC', '10SEC', '15SEC', '20SEC', '30SEC',
1077
+ '1MIN', '2MIN', '3MIN', '4MIN', '5MIN', '6MIN', '10MIN', '15MIN', '20MIN', '30MIN',
1078
+ '1HOUR', '2HOUR', '3HOUR', '4HOUR', '6HOUR', '8HOUR', '12HOUR', '1DAY'
1079
+ ]
1080
+
1081
+ interval_mapping = {
1082
+ 'Computation Interval': computation_interval,
1083
+ 'Output Interval': output_interval,
1084
+ 'Instantaneous Interval': instantaneous_interval,
1085
+ 'Mapping Interval': mapping_interval
1086
+ }
1087
+
1088
+ try:
1089
+ with open(plan_file_path, 'r') as file:
1090
+ lines = file.readlines()
1091
+
1092
+ for i, line in enumerate(lines):
1093
+ for key, value in interval_mapping.items():
1094
+ if value is not None:
1095
+ if value.upper() not in valid_intervals:
1096
+ raise ValueError(f"Invalid {key}: {value}. Must be one of {valid_intervals}")
1097
+ if line.strip().startswith(key):
1098
+ lines[i] = f"{key}={value.upper()}\n"
1099
+
1100
+ with open(plan_file_path, 'w') as file:
1101
+ file.writelines(lines)
1102
+
1103
+ logger = logging.getLogger(__name__)
1104
+ logger.info(f"Successfully updated intervals in plan file: {plan_file_path}")
1105
+
1106
+ except IOError as e:
1107
+ logger = logging.getLogger(__name__)
1108
+ logger.error(f"Error updating intervals in plan file {plan_file_path}: {e}")
1109
+ raise
1110
+
1111
+
1112
+ @log_call
1113
+ def update_plan_description(plan_number_or_path: Union[str, Path], description: str, ras_object: Optional['RasPrj'] = None) -> None:
1114
+ """
1115
+ Update the description block in a HEC-RAS plan file.
1116
+
1117
+ Args:
1118
+ plan_number_or_path (Union[str, Path]): The plan number or full path to the plan file
1119
+ description (str): The new description text to set
1120
+ ras_object (Optional[RasPrj]): Specific RAS object to use. If None, uses the global ras instance.
1121
+
1122
+ Raises:
1123
+ ValueError: If the plan file is not found
1124
+ IOError: If there's an error reading or writing the plan file
1125
+ """
1126
+ logger = get_logger(__name__)
1127
+ ras_obj = ras_object or ras
1128
+ ras_obj.check_initialized()
1129
+
1130
+ plan_file_path = Path(plan_number_or_path)
1131
+ if not plan_file_path.is_file():
1132
+ plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
1133
+ if not plan_file_path.exists():
1134
+ logger.error(f"Plan file not found: {plan_file_path}")
1135
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1136
+
1137
+ try:
1138
+ with open(plan_file_path, 'r') as file:
1139
+ content = file.read()
1140
+
1141
+ # Find the description block
1142
+ desc_pattern = r'Begin DESCRIPTION.*?END DESCRIPTION'
1143
+ new_desc_block = f'Begin DESCRIPTION\n{description}\nEND DESCRIPTION'
1144
+
1145
+ if re.search(desc_pattern, content, re.DOTALL):
1146
+ # Replace existing description block
1147
+ new_content = re.sub(desc_pattern, new_desc_block, content, flags=re.DOTALL)
1148
+ else:
1149
+ # Add new description block at the start of the file
1150
+ new_content = new_desc_block + '\n' + content
1151
+
1152
+ # Write the updated content back to the file
1153
+ with open(plan_file_path, 'w') as file:
1154
+ file.write(new_content)
1155
+
1156
+ logger.info(f"Updated description in plan file: {plan_file_path}")
1157
+
1158
+ # Update the dataframes in the RAS object to reflect changes
1159
+ if ras_object:
1160
+ ras_object.plan_df = ras_object.get_plan_entries()
1161
+ ras_object.geom_df = ras_object.get_geom_entries()
1162
+ ras_object.flow_df = ras_object.get_flow_entries()
1163
+ ras_object.unsteady_df = ras_object.get_unsteady_entries()
1164
+
1165
+ except IOError as e:
1166
+ logger.error(f"Error updating plan description in {plan_file_path}: {e}")
1167
+ raise
1168
+ except Exception as e:
1169
+ logger.error(f"Unexpected error updating plan description: {e}")
1170
+ raise
1171
+
1172
+ @staticmethod
1173
+ @log_call
1174
+ def read_plan_description(plan_number_or_path: Union[str, Path], ras_object: Optional['RasPrj'] = None) -> str:
1175
+ """
1176
+ Read the description from the plan file.
1177
+
1178
+ Args:
1179
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1180
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1181
+
1182
+ Returns:
1183
+ str: The description from the plan file.
1184
+
1185
+ Raises:
1186
+ ValueError: If the plan file is not found.
1187
+ IOError: If there's an error reading from the plan file.
1188
+ """
1189
+ logger = logging.getLogger(__name__)
1190
+
1191
+ plan_file_path = Path(plan_number_or_path)
1192
+ if not plan_file_path.is_file():
1193
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object)
1194
+ if plan_file_path is None or not Path(plan_file_path).exists():
1195
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1196
+
1197
+ try:
1198
+ with open(plan_file_path, 'r') as file:
1199
+ lines = file.readlines()
1200
+ except IOError as e:
1201
+ logger.error(f"Error reading plan file {plan_file_path}: {e}")
1202
+ raise
1203
+
1204
+ description_lines = []
1205
+ in_description = False
1206
+ description_found = False
1207
+ for line in lines:
1208
+ if line.strip() == "BEGIN DESCRIPTION:":
1209
+ in_description = True
1210
+ description_found = True
1211
+ elif line.strip() == "END DESCRIPTION:":
1212
+ break
1213
+ elif in_description:
1214
+ description_lines.append(line.strip())
1215
+
1216
+ if not description_found:
1217
+ logger.warning(f"No description found in plan file: {plan_file_path}")
1218
+ return ""
1219
+
1220
+ description = '\n'.join(description_lines)
1221
+ logger.info(f"Read description from plan file: {plan_file_path}")
1222
+ return description
1223
+
1224
+
1225
+
1226
+
1227
+ @staticmethod
1228
+ @log_call
1229
+ def update_simulation_date(plan_number_or_path: Union[str, Path], start_date: datetime, end_date: datetime, ras_object: Optional['RasPrj'] = None) -> None:
1230
+ """
1231
+ Update the simulation date for a given plan.
1232
+
1233
+ Args:
1234
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1235
+ start_date (datetime): The start date and time for the simulation.
1236
+ end_date (datetime): The end date and time for the simulation.
1237
+ ras_object (Optional['RasPrj']): The RAS project object. Defaults to None.
1238
+
1239
+ Raises:
1240
+ ValueError: If the plan file is not found or if there's an error updating the file.
1241
+ """
1242
+
1243
+ # Get the plan file path
1244
+ plan_file_path = Path(plan_number_or_path)
1245
+ if not plan_file_path.is_file():
1246
+ plan_file_path = RasPlan.get_plan_path(plan_number_or_path, ras_object)
1247
+ if plan_file_path is None or not Path(plan_file_path).exists():
1248
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1249
+
1250
+ # Format the dates
1251
+ formatted_date = f"{start_date.strftime('%d%b%Y').upper()},{start_date.strftime('%H%M')},{end_date.strftime('%d%b%Y').upper()},{end_date.strftime('%H%M')}"
1252
+
1253
+ try:
1254
+ # Read the file
1255
+ with open(plan_file_path, 'r') as file:
1256
+ lines = file.readlines()
1257
+
1258
+ # Update the Simulation Date line
1259
+ updated = False
1260
+ for i, line in enumerate(lines):
1261
+ if line.startswith("Simulation Date="):
1262
+ lines[i] = f"Simulation Date={formatted_date}\n"
1263
+ updated = True
1264
+ break
1265
+
1266
+ # If Simulation Date line not found, add it at the end
1267
+ if not updated:
1268
+ lines.append(f"Simulation Date={formatted_date}\n")
1269
+
1270
+ # Write the updated content back to the file
1271
+ with open(plan_file_path, 'w') as file:
1272
+ file.writelines(lines)
1273
+
1274
+ logger.info(f"Updated simulation date in plan file: {plan_file_path}")
1275
+
1276
+ except IOError as e:
1277
+ logger.error(f"Error updating simulation date in plan file {plan_file_path}: {e}")
1278
+ raise ValueError(f"Error updating simulation date: {e}")
1279
+
1280
+ # Refresh RasPrj dataframes
1281
+ if ras_object:
1282
+ ras_object.plan_df = ras_object.get_plan_entries()
1283
+ ras_object.unsteady_df = ras_object.get_unsteady_entries()
1284
+
1285
+ @staticmethod
1286
+ @log_call
1287
+ def get_shortid(plan_number_or_path: Union[str, Path], ras_object=None) -> str:
1288
+ """
1289
+ Get the Short Identifier from a HEC-RAS plan file.
1290
+
1291
+ Args:
1292
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1293
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1294
+
1295
+ Returns:
1296
+ str: The Short Identifier from the plan file.
1297
+
1298
+ Raises:
1299
+ ValueError: If the plan file is not found.
1300
+ IOError: If there's an error reading from the plan file.
1301
+
1302
+ Example:
1303
+ >>> shortid = RasPlan.get_shortid('01')
1304
+ >>> print(f"Plan's Short Identifier: {shortid}")
1305
+ """
1306
+ logger = get_logger(__name__)
1307
+ ras_obj = ras_object or ras
1308
+ ras_obj.check_initialized()
1309
+
1310
+ # Get the Short Identifier using get_plan_value
1311
+ shortid = RasPlan.get_plan_value(plan_number_or_path, "Short Identifier", ras_obj)
1312
+
1313
+ if shortid is None:
1314
+ logger.warning(f"Short Identifier not found in plan: {plan_number_or_path}")
1315
+ return ""
1316
+
1317
+ logger.info(f"Retrieved Short Identifier: {shortid}")
1318
+ return shortid
1319
+
1320
+ @staticmethod
1321
+ @log_call
1322
+ def set_shortid(plan_number_or_path: Union[str, Path], new_shortid: str, ras_object=None) -> None:
1323
+ """
1324
+ Set the Short Identifier in a HEC-RAS plan file.
1325
+
1326
+ Args:
1327
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1328
+ new_shortid (str): The new Short Identifier to set (max 24 characters).
1329
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1330
+
1331
+ Raises:
1332
+ ValueError: If the plan file is not found or if new_shortid is too long.
1333
+ IOError: If there's an error updating the plan file.
1334
+
1335
+ Example:
1336
+ >>> RasPlan.set_shortid('01', 'NewShortIdentifier')
1337
+ """
1338
+ logger = get_logger(__name__)
1339
+ ras_obj = ras_object or ras
1340
+ ras_obj.check_initialized()
1341
+
1342
+ # Ensure new_shortid is not too long (HEC-RAS limits short identifiers to 24 characters)
1343
+ if len(new_shortid) > 24:
1344
+ logger.warning(f"Short Identifier too long (24 char max). Truncating: {new_shortid}")
1345
+ new_shortid = new_shortid[:24]
1346
+
1347
+ # Get the plan file path
1348
+ plan_file_path = Path(plan_number_or_path)
1349
+ if not plan_file_path.is_file():
1350
+ plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_obj)
1351
+ if not plan_file_path.exists():
1352
+ logger.error(f"Plan file not found: {plan_file_path}")
1353
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1354
+
1355
+ try:
1356
+ # Read the file
1357
+ with open(plan_file_path, 'r') as file:
1358
+ lines = file.readlines()
1359
+
1360
+ # Update the Short Identifier line
1361
+ updated = False
1362
+ for i, line in enumerate(lines):
1363
+ if line.startswith("Short Identifier="):
1364
+ lines[i] = f"Short Identifier={new_shortid}\n"
1365
+ updated = True
1366
+ break
1367
+
1368
+ # If Short Identifier line not found, add it after Plan Title
1369
+ if not updated:
1370
+ for i, line in enumerate(lines):
1371
+ if line.startswith("Plan Title="):
1372
+ lines.insert(i+1, f"Short Identifier={new_shortid}\n")
1373
+ updated = True
1374
+ break
1375
+
1376
+ # If Plan Title not found either, add at the beginning
1377
+ if not updated:
1378
+ lines.insert(0, f"Short Identifier={new_shortid}\n")
1379
+
1380
+ # Write the updated content back to the file
1381
+ with open(plan_file_path, 'w') as file:
1382
+ file.writelines(lines)
1383
+
1384
+ logger.info(f"Updated Short Identifier in plan file to: {new_shortid}")
1385
+
1386
+ except IOError as e:
1387
+ logger.error(f"Error updating Short Identifier in plan file {plan_file_path}: {e}")
1388
+ raise ValueError(f"Error updating Short Identifier: {e}")
1389
+
1390
+ # Refresh RasPrj dataframes if ras_object provided
1391
+ if ras_object:
1392
+ ras_object.plan_df = ras_object.get_plan_entries()
1393
+
1394
+ @staticmethod
1395
+ @log_call
1396
+ def get_plan_title(plan_number_or_path: Union[str, Path], ras_object=None) -> str:
1397
+ """
1398
+ Get the Plan Title from a HEC-RAS plan file.
1399
+
1400
+ Args:
1401
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1402
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1403
+
1404
+ Returns:
1405
+ str: The Plan Title from the plan file.
1406
+
1407
+ Raises:
1408
+ ValueError: If the plan file is not found.
1409
+ IOError: If there's an error reading from the plan file.
1410
+
1411
+ Example:
1412
+ >>> title = RasPlan.get_plan_title('01')
1413
+ >>> print(f"Plan Title: {title}")
1414
+ """
1415
+ logger = get_logger(__name__)
1416
+ ras_obj = ras_object or ras
1417
+ ras_obj.check_initialized()
1418
+
1419
+ # Get the Plan Title using get_plan_value
1420
+ title = RasPlan.get_plan_value(plan_number_or_path, "Plan Title", ras_obj)
1421
+
1422
+ if title is None:
1423
+ logger.warning(f"Plan Title not found in plan: {plan_number_or_path}")
1424
+ return ""
1425
+
1426
+ logger.info(f"Retrieved Plan Title: {title}")
1427
+ return title
1428
+
1429
+ @staticmethod
1430
+ @log_call
1431
+ def set_plan_title(plan_number_or_path: Union[str, Path], new_title: str, ras_object=None) -> None:
1432
+ """
1433
+ Set the Plan Title in a HEC-RAS plan file.
1434
+
1435
+ Args:
1436
+ plan_number_or_path (Union[str, Path]): The plan number or path to the plan file.
1437
+ new_title (str): The new Plan Title to set.
1438
+ ras_object (Optional[RasPrj]): The RAS project object. If None, uses the global 'ras' object.
1439
+
1440
+ Raises:
1441
+ ValueError: If the plan file is not found.
1442
+ IOError: If there's an error updating the plan file.
1443
+
1444
+ Example:
1445
+ >>> RasPlan.set_plan_title('01', 'Updated Plan Scenario')
1446
+ """
1447
+ logger = get_logger(__name__)
1448
+ ras_obj = ras_object or ras
1449
+ ras_obj.check_initialized()
1450
+
1451
+ # Get the plan file path
1452
+ plan_file_path = Path(plan_number_or_path)
1453
+ if not plan_file_path.is_file():
1454
+ plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_obj)
1455
+ if not plan_file_path.exists():
1456
+ logger.error(f"Plan file not found: {plan_file_path}")
1457
+ raise ValueError(f"Plan file not found: {plan_file_path}")
1458
+
1459
+ try:
1460
+ # Read the file
1461
+ with open(plan_file_path, 'r') as file:
1462
+ lines = file.readlines()
1463
+
1464
+ # Update the Plan Title line
1465
+ updated = False
1466
+ for i, line in enumerate(lines):
1467
+ if line.startswith("Plan Title="):
1468
+ lines[i] = f"Plan Title={new_title}\n"
1469
+ updated = True
1470
+ break
1471
+
1472
+ # If Plan Title line not found, add it at the beginning
1473
+ if not updated:
1474
+ lines.insert(0, f"Plan Title={new_title}\n")
1475
+
1476
+ # Write the updated content back to the file
1477
+ with open(plan_file_path, 'w') as file:
1478
+ file.writelines(lines)
1479
+
1480
+ logger.info(f"Updated Plan Title in plan file to: {new_title}")
1481
+
1482
+ except IOError as e:
1483
+ logger.error(f"Error updating Plan Title in plan file {plan_file_path}: {e}")
1484
+ raise ValueError(f"Error updating Plan Title: {e}")
1485
+
1486
+ # Refresh RasPrj dataframes if ras_object provided
1487
+ if ras_object:
1469
1488
  ras_object.plan_df = ras_object.get_plan_entries()