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