ras-commander 0.35.0__py3-none-any.whl → 0.36.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ras_commander/RasCmdr.py +360 -332
- ras_commander/RasExamples.py +113 -80
- ras_commander/RasGeo.py +38 -28
- ras_commander/RasGpt.py +142 -0
- ras_commander/RasHdf.py +170 -253
- ras_commander/RasPlan.py +115 -166
- ras_commander/RasPrj.py +212 -141
- ras_commander/RasUnsteady.py +37 -22
- ras_commander/RasUtils.py +98 -82
- ras_commander/__init__.py +11 -13
- ras_commander/logging_config.py +80 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/METADATA +15 -11
- ras_commander-0.36.0.dist-info/RECORD +17 -0
- ras_commander-0.35.0.dist-info/RECORD +0 -15
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.36.0.dist-info}/top_level.txt +0 -0
    
        ras_commander/RasPlan.py
    CHANGED
    
    | @@ -1,3 +1,29 @@ | |
| 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 | 
            +
            import os
         | 
| 1 27 | 
             
            import re
         | 
| 2 28 | 
             
            import logging
         | 
| 3 29 | 
             
            from pathlib import Path
         | 
| @@ -6,28 +32,20 @@ from typing import Union, Optional | |
| 6 32 | 
             
            import pandas as pd
         | 
| 7 33 | 
             
            from .RasPrj import RasPrj, ras
         | 
| 8 34 | 
             
            from .RasUtils import RasUtils
         | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 35 | 
             
            from pathlib import Path
         | 
| 12 36 | 
             
            from typing import Union, Any
         | 
| 13 37 | 
             
            import logging
         | 
| 14 38 | 
             
            import re
         | 
| 39 | 
            +
            from ras_commander import get_logger
         | 
| 40 | 
            +
            from ras_commander.logging_config import log_call
         | 
| 15 41 |  | 
| 16 | 
            -
             | 
| 17 | 
            -
            # Configure logging
         | 
| 18 | 
            -
            logging.basicConfig(
         | 
| 19 | 
            -
                level=logging.INFO,
         | 
| 20 | 
            -
                format='%(asctime)s - %(levelname)s - %(message)s',
         | 
| 21 | 
            -
                handlers=[
         | 
| 22 | 
            -
                    logging.StreamHandler()
         | 
| 23 | 
            -
                ]
         | 
| 24 | 
            -
            )
         | 
| 42 | 
            +
            logger = get_logger(__name__)
         | 
| 25 43 |  | 
| 26 44 | 
             
            class RasPlan:
         | 
| 27 45 | 
             
                """
         | 
| 28 46 | 
             
                A class for operations on HEC-RAS plan files.
         | 
| 29 47 | 
             
                """
         | 
| 30 | 
            -
             | 
| 48 | 
            +
                @log_call
         | 
| 31 49 | 
             
                @staticmethod
         | 
| 32 50 | 
             
                def set_geom(plan_number: Union[str, int], new_geom: Union[str, int], ras_object=None) -> pd.DataFrame:
         | 
| 33 51 | 
             
                    """
         | 
| @@ -110,6 +128,7 @@ class RasPlan: | |
| 110 128 | 
             
                    return ras_obj.plan_df
         | 
| 111 129 |  | 
| 112 130 | 
             
                @staticmethod
         | 
| 131 | 
            +
                @log_call
         | 
| 113 132 | 
             
                def set_steady(plan_number: str, new_steady_flow_number: str, ras_object=None):
         | 
| 114 133 | 
             
                    """
         | 
| 115 134 | 
             
                    Apply a steady flow file to a plan file.
         | 
| @@ -132,7 +151,6 @@ class RasPlan: | |
| 132 151 | 
             
                    Note:
         | 
| 133 152 | 
             
                        This function updates the ras object's dataframes after modifying the project structure.
         | 
| 134 153 | 
             
                    """
         | 
| 135 | 
            -
                    logging.info(f"Setting steady flow file to {new_steady_flow_number} in Plan {plan_number}")
         | 
| 136 154 | 
             
                    ras_obj = ras_object or ras
         | 
| 137 155 | 
             
                    ras_obj.check_initialized()
         | 
| 138 156 |  | 
| @@ -140,27 +158,23 @@ class RasPlan: | |
| 140 158 | 
             
                    ras_obj.flow_df = ras_obj.get_flow_entries()
         | 
| 141 159 |  | 
| 142 160 | 
             
                    if new_steady_flow_number not in ras_obj.flow_df['flow_number'].values:
         | 
| 143 | 
            -
                        logging.error(f"Steady flow number {new_steady_flow_number} not found in project file.")
         | 
| 144 161 | 
             
                        raise ValueError(f"Steady flow number {new_steady_flow_number} not found in project file.")
         | 
| 145 162 |  | 
| 146 163 | 
             
                    # Resolve the full path of the plan file
         | 
| 147 164 | 
             
                    plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
         | 
| 148 165 | 
             
                    if not plan_file_path:
         | 
| 149 | 
            -
                        logging.error(f"Plan file not found: {plan_number}")
         | 
| 150 166 | 
             
                        raise FileNotFoundError(f"Plan file not found: {plan_number}")
         | 
| 151 167 |  | 
| 152 168 | 
             
                    try:
         | 
| 153 169 | 
             
                        with open(plan_file_path, 'r') as f:
         | 
| 154 170 | 
             
                            lines = f.readlines()
         | 
| 155 171 | 
             
                    except FileNotFoundError:
         | 
| 156 | 
            -
                         | 
| 157 | 
            -
                        raise
         | 
| 172 | 
            +
                        raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
         | 
| 158 173 |  | 
| 159 174 | 
             
                    with open(plan_file_path, 'w') as f:
         | 
| 160 175 | 
             
                        for line in lines:
         | 
| 161 176 | 
             
                            if line.startswith("Flow File=f"):
         | 
| 162 177 | 
             
                                f.write(f"Flow File=f{new_steady_flow_number}\n")
         | 
| 163 | 
            -
                                logging.info(f"Updated Flow File in {plan_file_path} to f{new_steady_flow_number}")
         | 
| 164 178 | 
             
                            else:
         | 
| 165 179 | 
             
                                f.write(line)
         | 
| 166 180 |  | 
| @@ -171,6 +185,7 @@ class RasPlan: | |
| 171 185 | 
             
                    ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
         | 
| 172 186 |  | 
| 173 187 | 
             
                @staticmethod
         | 
| 188 | 
            +
                @log_call
         | 
| 174 189 | 
             
                def set_unsteady(plan_number: str, new_unsteady_flow_number: str, ras_object=None):
         | 
| 175 190 | 
             
                    """
         | 
| 176 191 | 
             
                    Apply an unsteady flow file to a plan file.
         | 
| @@ -193,8 +208,6 @@ class RasPlan: | |
| 193 208 | 
             
                    Note:
         | 
| 194 209 | 
             
                        This function updates the ras object's dataframes after modifying the project structure.
         | 
| 195 210 | 
             
                    """
         | 
| 196 | 
            -
                    logging.info(f"Setting unsteady flow file to {new_unsteady_flow_number} in Plan {plan_number}")
         | 
| 197 | 
            -
                    
         | 
| 198 211 | 
             
                    ras_obj = ras_object or ras
         | 
| 199 212 | 
             
                    ras_obj.check_initialized()
         | 
| 200 213 |  | 
| @@ -202,21 +215,17 @@ class RasPlan: | |
| 202 215 | 
             
                    ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
         | 
| 203 216 |  | 
| 204 217 | 
             
                    if new_unsteady_flow_number not in ras_obj.unsteady_df['unsteady_number'].values:
         | 
| 205 | 
            -
                        logging.error(f"Unsteady number {new_unsteady_flow_number} not found in project file.")
         | 
| 206 218 | 
             
                        raise ValueError(f"Unsteady number {new_unsteady_flow_number} not found in project file.")
         | 
| 207 219 |  | 
| 208 220 | 
             
                    # Get the full path of the plan file
         | 
| 209 221 | 
             
                    plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
         | 
| 210 222 | 
             
                    if not plan_file_path:
         | 
| 211 | 
            -
                        logging.error(f"Plan file not found: {plan_number}")
         | 
| 212 223 | 
             
                        raise FileNotFoundError(f"Plan file not found: {plan_number}")
         | 
| 213 224 |  | 
| 214 225 | 
             
                    try:
         | 
| 215 226 | 
             
                        RasUtils.update_plan_file(plan_file_path, 'Unsteady', new_unsteady_flow_number)
         | 
| 216 | 
            -
                        logging.info(f"Updated unsteady flow file in {plan_file_path} to u{new_unsteady_flow_number}")
         | 
| 217 227 | 
             
                    except Exception as e:
         | 
| 218 | 
            -
                         | 
| 219 | 
            -
                        raise
         | 
| 228 | 
            +
                        raise Exception(f"Failed to update unsteady flow file: {e}")
         | 
| 220 229 |  | 
| 221 230 | 
             
                    # Update the ras object's dataframes
         | 
| 222 231 | 
             
                    ras_obj.plan_df = ras_obj.get_plan_entries()
         | 
| @@ -225,6 +234,7 @@ class RasPlan: | |
| 225 234 | 
             
                    ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
         | 
| 226 235 |  | 
| 227 236 | 
             
                @staticmethod
         | 
| 237 | 
            +
                @log_call
         | 
| 228 238 | 
             
                def set_num_cores(plan_number, num_cores, ras_object=None):
         | 
| 229 239 | 
             
                    """
         | 
| 230 240 | 
             
                    Update the maximum number of cores to use in the HEC-RAS plan file.
         | 
| @@ -253,8 +263,6 @@ class RasPlan: | |
| 253 263 | 
             
                    Note:
         | 
| 254 264 | 
             
                        This function updates the ras object's dataframes after modifying the project structure.
         | 
| 255 265 | 
             
                    """
         | 
| 256 | 
            -
                    logging.info(f"Setting num_cores to {num_cores} in Plan {plan_number}")
         | 
| 257 | 
            -
                    
         | 
| 258 266 | 
             
                    ras_obj = ras_object or ras
         | 
| 259 267 | 
             
                    ras_obj.check_initialized()
         | 
| 260 268 |  | 
| @@ -262,7 +270,6 @@ class RasPlan: | |
| 262 270 | 
             
                    if Path(plan_number).is_file():
         | 
| 263 271 | 
             
                        plan_file_path = Path(plan_number)
         | 
| 264 272 | 
             
                        if not plan_file_path.exists():
         | 
| 265 | 
            -
                            logging.error(f"Plan file not found: {plan_file_path}. Please provide a valid plan number or path.")
         | 
| 266 273 | 
             
                            raise FileNotFoundError(f"Plan file not found: {plan_file_path}. Please provide a valid plan number or path.")
         | 
| 267 274 | 
             
                    else:
         | 
| 268 275 | 
             
                        # Update the plan dataframe in the ras instance to ensure it is current
         | 
| @@ -271,7 +278,6 @@ class RasPlan: | |
| 271 278 | 
             
                        # Get the full path of the plan file
         | 
| 272 279 | 
             
                        plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
         | 
| 273 280 | 
             
                        if not plan_file_path:
         | 
| 274 | 
            -
                            logging.error(f"Plan file not found: {plan_number}. Please provide a valid plan number or path.")
         | 
| 275 281 | 
             
                            raise FileNotFoundError(f"Plan file not found: {plan_number}. Please provide a valid plan number or path.")
         | 
| 276 282 |  | 
| 277 283 | 
             
                    cores_pattern = re.compile(r"(UNET D1 Cores= )\d+")
         | 
| @@ -279,17 +285,14 @@ class RasPlan: | |
| 279 285 | 
             
                        with open(plan_file_path, 'r') as file:
         | 
| 280 286 | 
             
                            content = file.read()
         | 
| 281 287 | 
             
                    except FileNotFoundError:
         | 
| 282 | 
            -
                         | 
| 283 | 
            -
                        raise
         | 
| 288 | 
            +
                        raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
         | 
| 284 289 |  | 
| 285 290 | 
             
                    new_content = cores_pattern.sub(rf"\g<1>{num_cores}", content)
         | 
| 286 291 | 
             
                    try:
         | 
| 287 292 | 
             
                        with open(plan_file_path, 'w') as file:
         | 
| 288 293 | 
             
                            file.write(new_content)
         | 
| 289 | 
            -
                        logging.info(f"Updated {plan_file_path} with {num_cores} cores.")
         | 
| 290 294 | 
             
                    except IOError as e:
         | 
| 291 | 
            -
                         | 
| 292 | 
            -
                        raise
         | 
| 295 | 
            +
                        raise IOError(f"Failed to write to plan file: {e}")
         | 
| 293 296 |  | 
| 294 297 | 
             
                    # Update the ras object's dataframes
         | 
| 295 298 | 
             
                    ras_obj.plan_df = ras_obj.get_plan_entries()
         | 
| @@ -298,6 +301,7 @@ class RasPlan: | |
| 298 301 | 
             
                    ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
         | 
| 299 302 |  | 
| 300 303 | 
             
                @staticmethod
         | 
| 304 | 
            +
                @log_call
         | 
| 301 305 | 
             
                def set_geom_preprocessor(file_path, run_htab, use_ib_tables, ras_object=None):
         | 
| 302 306 | 
             
                    """
         | 
| 303 307 | 
             
                    Update the simulation plan file to modify the `Run HTab` and `UNET Use Existing IB Tables` settings.
         | 
| @@ -330,37 +334,27 @@ class RasPlan: | |
| 330 334 | 
             
                    ras_obj.check_initialized()
         | 
| 331 335 |  | 
| 332 336 | 
             
                    if run_htab not in [-1, 0]:
         | 
| 333 | 
            -
                        logging.error("Invalid value for `Run HTab`. Expected `0` or `-1`.")
         | 
| 334 337 | 
             
                        raise ValueError("Invalid value for `Run HTab`. Expected `0` or `-1`.")
         | 
| 335 338 | 
             
                    if use_ib_tables not in [-1, 0]:
         | 
| 336 | 
            -
                        logging.error("Invalid value for `UNET Use Existing IB Tables`. Expected `0` or `-1`.")
         | 
| 337 339 | 
             
                        raise ValueError("Invalid value for `UNET Use Existing IB Tables`. Expected `0` or `-1`.")
         | 
| 338 340 | 
             
                    try:
         | 
| 339 | 
            -
                        logging.info(f"Reading the file: {file_path}")
         | 
| 340 341 | 
             
                        with open(file_path, 'r') as file:
         | 
| 341 342 | 
             
                            lines = file.readlines()
         | 
| 342 | 
            -
                        logging.info("Updating the file with new settings...")
         | 
| 343 343 | 
             
                        updated_lines = []
         | 
| 344 344 | 
             
                        for line in lines:
         | 
| 345 345 | 
             
                            if line.lstrip().startswith("Run HTab="):
         | 
| 346 346 | 
             
                                updated_line = f"Run HTab= {run_htab} \n"
         | 
| 347 347 | 
             
                                updated_lines.append(updated_line)
         | 
| 348 | 
            -
                                logging.info(f"Updated 'Run HTab' to {run_htab}")
         | 
| 349 348 | 
             
                            elif line.lstrip().startswith("UNET Use Existing IB Tables="):
         | 
| 350 349 | 
             
                                updated_line = f"UNET Use Existing IB Tables= {use_ib_tables} \n"
         | 
| 351 350 | 
             
                                updated_lines.append(updated_line)
         | 
| 352 | 
            -
                                logging.info(f"Updated 'UNET Use Existing IB Tables' to {use_ib_tables}")
         | 
| 353 351 | 
             
                            else:
         | 
| 354 352 | 
             
                                updated_lines.append(line)
         | 
| 355 | 
            -
                        logging.info(f"Writing the updated settings back to the file: {file_path}")
         | 
| 356 353 | 
             
                        with open(file_path, 'w') as file:
         | 
| 357 354 | 
             
                            file.writelines(updated_lines)
         | 
| 358 | 
            -
                        logging.info("File update completed successfully.")
         | 
| 359 355 | 
             
                    except FileNotFoundError:
         | 
| 360 | 
            -
                        logging.error(f"The file '{file_path}' does not exist.")
         | 
| 361 356 | 
             
                        raise FileNotFoundError(f"The file '{file_path}' does not exist.")
         | 
| 362 357 | 
             
                    except IOError as e:
         | 
| 363 | 
            -
                        logging.error(f"An error occurred while reading or writing the file: {e}")
         | 
| 364 358 | 
             
                        raise IOError(f"An error occurred while reading or writing the file: {e}")
         | 
| 365 359 |  | 
| 366 360 | 
             
                    # Update the ras object's dataframes
         | 
| @@ -369,9 +363,8 @@ class RasPlan: | |
| 369 363 | 
             
                    ras_obj.flow_df = ras_obj.get_flow_entries()
         | 
| 370 364 | 
             
                    ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
         | 
| 371 365 |  | 
| 372 | 
            -
                # Get Functions to retrieve file paths for plan, flow, unsteady, geometry and results files
         | 
| 373 | 
            -
             | 
| 374 366 | 
             
                @staticmethod
         | 
| 367 | 
            +
                @log_call
         | 
| 375 368 | 
             
                def get_results_path(plan_number: str, ras_object=None) -> Optional[str]:
         | 
| 376 369 | 
             
                    """
         | 
| 377 370 | 
             
                    Retrieve the results file path for a given HEC-RAS plan number.
         | 
| @@ -403,24 +396,18 @@ class RasPlan: | |
| 403 396 | 
             
                    # Ensure plan_number is a string
         | 
| 404 397 | 
             
                    plan_number = str(plan_number).zfill(2)
         | 
| 405 398 |  | 
| 406 | 
            -
                    # Log the plan dataframe for debugging
         | 
| 407 | 
            -
                    logging.debug("Plan DataFrame:")
         | 
| 408 | 
            -
                    logging.debug(ras_obj.plan_df)
         | 
| 409 | 
            -
                    
         | 
| 410 399 | 
             
                    plan_entry = ras_obj.plan_df[ras_obj.plan_df['plan_number'] == plan_number]
         | 
| 411 400 | 
             
                    if not plan_entry.empty:
         | 
| 412 401 | 
             
                        results_path = plan_entry['HDF_Results_Path'].iloc[0]
         | 
| 413 402 | 
             
                        if results_path and Path(results_path).exists():
         | 
| 414 | 
            -
                            logging.info(f"Results file for Plan number {plan_number} exists at: {results_path}")
         | 
| 415 403 | 
             
                            return results_path
         | 
| 416 404 | 
             
                        else:
         | 
| 417 | 
            -
                            logging.warning(f"Results file for Plan number {plan_number} does not exist.")
         | 
| 418 405 | 
             
                            return None
         | 
| 419 406 | 
             
                    else:
         | 
| 420 | 
            -
                        logging.warning(f"Plan number {plan_number} not found in the entries.")
         | 
| 421 407 | 
             
                        return None
         | 
| 422 408 |  | 
| 423 409 | 
             
                @staticmethod
         | 
| 410 | 
            +
                @log_call
         | 
| 424 411 | 
             
                def get_plan_path(plan_number: str, ras_object=None) -> Optional[str]:
         | 
| 425 412 | 
             
                    """
         | 
| 426 413 | 
             
                    Return the full path for a given plan number.
         | 
| @@ -456,13 +443,12 @@ class RasPlan: | |
| 456 443 |  | 
| 457 444 | 
             
                    if not plan_path.empty:
         | 
| 458 445 | 
             
                        full_path = plan_path['full_path'].iloc[0]
         | 
| 459 | 
            -
                        logging.info(f"Plan file for Plan number {plan_number} found at: {full_path}")
         | 
| 460 446 | 
             
                        return full_path
         | 
| 461 447 | 
             
                    else:
         | 
| 462 | 
            -
                        logging.warning(f"Plan number {plan_number} not found in the updated plan entries.")
         | 
| 463 448 | 
             
                        return None
         | 
| 464 449 |  | 
| 465 450 | 
             
                @staticmethod
         | 
| 451 | 
            +
                @log_call
         | 
| 466 452 | 
             
                def get_flow_path(flow_number: str, ras_object=None) -> Optional[str]:
         | 
| 467 453 | 
             
                    """
         | 
| 468 454 | 
             
                    Return the full path for a given flow number.
         | 
| @@ -494,13 +480,12 @@ class RasPlan: | |
| 494 480 | 
             
                    flow_path = ras_obj.flow_df[ras_obj.flow_df['flow_number'] == flow_number]
         | 
| 495 481 | 
             
                    if not flow_path.empty:
         | 
| 496 482 | 
             
                        full_path = flow_path['full_path'].iloc[0]
         | 
| 497 | 
            -
                        logging.info(f"Flow file for Flow number {flow_number} found at: {full_path}")
         | 
| 498 483 | 
             
                        return full_path
         | 
| 499 484 | 
             
                    else:
         | 
| 500 | 
            -
                        logging.warning(f"Flow number {flow_number} not found in the updated flow entries.")
         | 
| 501 485 | 
             
                        return None
         | 
| 502 486 |  | 
| 503 487 | 
             
                @staticmethod
         | 
| 488 | 
            +
                @log_call
         | 
| 504 489 | 
             
                def get_unsteady_path(unsteady_number: str, ras_object=None) -> Optional[str]:
         | 
| 505 490 | 
             
                    """
         | 
| 506 491 | 
             
                    Return the full path for a given unsteady number.
         | 
| @@ -532,13 +517,12 @@ class RasPlan: | |
| 532 517 | 
             
                    unsteady_path = ras_obj.unsteady_df[ras_obj.unsteady_df['unsteady_number'] == unsteady_number]
         | 
| 533 518 | 
             
                    if not unsteady_path.empty:
         | 
| 534 519 | 
             
                        full_path = unsteady_path['full_path'].iloc[0]
         | 
| 535 | 
            -
                        logging.info(f"Unsteady file for Unsteady number {unsteady_number} found at: {full_path}")
         | 
| 536 520 | 
             
                        return full_path
         | 
| 537 521 | 
             
                    else:
         | 
| 538 | 
            -
                        logging.warning(f"Unsteady number {unsteady_number} not found in the updated unsteady entries.")
         | 
| 539 522 | 
             
                        return None
         | 
| 540 523 |  | 
| 541 524 | 
             
                @staticmethod
         | 
| 525 | 
            +
                @log_call
         | 
| 542 526 | 
             
                def get_geom_path(geom_number: str, ras_object=None) -> Optional[str]:
         | 
| 543 527 | 
             
                    """
         | 
| 544 528 | 
             
                    Return the full path for a given geometry number.
         | 
| @@ -570,15 +554,14 @@ class RasPlan: | |
| 570 554 | 
             
                    geom_path = ras_obj.geom_df[ras_obj.geom_df['geom_number'] == geom_number]
         | 
| 571 555 | 
             
                    if not geom_path.empty:
         | 
| 572 556 | 
             
                        full_path = geom_path['full_path'].iloc[0]
         | 
| 573 | 
            -
                        logging.info(f"Geometry file for Geom number {geom_number} found at: {full_path}")
         | 
| 574 557 | 
             
                        return full_path
         | 
| 575 558 | 
             
                    else:
         | 
| 576 | 
            -
                        logging.warning(f"Geometry number {geom_number} not found in the updated geometry entries.")
         | 
| 577 559 | 
             
                        return None
         | 
| 578 560 |  | 
| 579 561 | 
             
                # Clone Functions to copy unsteady, flow, and geometry files from templates
         | 
| 580 562 |  | 
| 581 563 | 
             
                @staticmethod
         | 
| 564 | 
            +
                @log_call
         | 
| 582 565 | 
             
                def clone_plan(template_plan, new_plan_shortid=None, ras_object=None):
         | 
| 583 566 | 
             
                    """
         | 
| 584 567 | 
             
                    Create a new plan file based on a template and update the project file.
         | 
| @@ -610,18 +593,15 @@ class RasPlan: | |
| 610 593 | 
             
                    new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
         | 
| 611 594 |  | 
| 612 595 | 
             
                    if not template_plan_path.exists():
         | 
| 613 | 
            -
                        logging.error(f"Template plan file '{template_plan_path}' does not exist.")
         | 
| 614 596 | 
             
                        raise FileNotFoundError(f"Template plan file '{template_plan_path}' does not exist.")
         | 
| 615 597 |  | 
| 616 598 | 
             
                    shutil.copy(template_plan_path, new_plan_path)
         | 
| 617 | 
            -
                    logging.info(f"Copied {template_plan_path} to {new_plan_path}")
         | 
| 618 599 |  | 
| 619 600 | 
             
                    try:
         | 
| 620 601 | 
             
                        with open(new_plan_path, 'r') as f:
         | 
| 621 602 | 
             
                            plan_lines = f.readlines()
         | 
| 622 603 | 
             
                    except FileNotFoundError:
         | 
| 623 | 
            -
                         | 
| 624 | 
            -
                        raise
         | 
| 604 | 
            +
                        raise FileNotFoundError(f"New plan file not found after copying: {new_plan_path}")
         | 
| 625 605 |  | 
| 626 606 | 
             
                    shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
         | 
| 627 607 | 
             
                    for i, line in enumerate(plan_lines):
         | 
| @@ -633,23 +613,19 @@ class RasPlan: | |
| 633 613 | 
             
                            else:
         | 
| 634 614 | 
             
                                new_shortid = new_plan_shortid[:24]
         | 
| 635 615 | 
             
                            plan_lines[i] = f"Short Identifier={new_shortid}\n"
         | 
| 636 | 
            -
                            logging.info(f"Updated 'Short Identifier' to '{new_shortid}' in {new_plan_path}")
         | 
| 637 616 | 
             
                            break
         | 
| 638 617 |  | 
| 639 618 | 
             
                    try:
         | 
| 640 619 | 
             
                        with open(new_plan_path, 'w') as f:
         | 
| 641 620 | 
             
                            f.writelines(plan_lines)
         | 
| 642 | 
            -
                        logging.info(f"Updated short identifier in {new_plan_path}")
         | 
| 643 621 | 
             
                    except IOError as e:
         | 
| 644 | 
            -
                         | 
| 645 | 
            -
                        raise
         | 
| 622 | 
            +
                        raise IOError(f"Failed to write updated short identifier to {new_plan_path}: {e}")
         | 
| 646 623 |  | 
| 647 624 | 
             
                    try:
         | 
| 648 625 | 
             
                        with open(ras_obj.prj_file, 'r') as f:
         | 
| 649 626 | 
             
                            lines = f.readlines()
         | 
| 650 627 | 
             
                    except FileNotFoundError:
         | 
| 651 | 
            -
                         | 
| 652 | 
            -
                        raise
         | 
| 628 | 
            +
                        raise FileNotFoundError(f"Project file not found: {ras_obj.prj_file}")
         | 
| 653 629 |  | 
| 654 630 | 
             
                    # Prepare the new Plan File entry line
         | 
| 655 631 | 
             
                    new_plan_line = f"Plan File=p{new_plan_num}\n"
         | 
| @@ -669,27 +645,22 @@ class RasPlan: | |
| 669 645 |  | 
| 670 646 | 
             
                    if insertion_index is not None:
         | 
| 671 647 | 
             
                        lines.insert(insertion_index, new_plan_line)
         | 
| 672 | 
            -
                        logging.info(f"Inserted new plan line at index {insertion_index}")
         | 
| 673 648 | 
             
                    else:
         | 
| 674 649 | 
             
                        # Try to insert after the last Plan File entry
         | 
| 675 650 | 
             
                        plan_indices = [i for i, line in enumerate(lines) if plan_file_pattern.match(line.strip())]
         | 
| 676 651 | 
             
                        if plan_indices:
         | 
| 677 652 | 
             
                            last_plan_index = plan_indices[-1]
         | 
| 678 653 | 
             
                            lines.insert(last_plan_index + 1, new_plan_line)
         | 
| 679 | 
            -
                            logging.info(f"Inserted new plan line after index {last_plan_index}")
         | 
| 680 654 | 
             
                        else:
         | 
| 681 655 | 
             
                            # Append at the end if no Plan File entries exist
         | 
| 682 656 | 
             
                            lines.append(new_plan_line)
         | 
| 683 | 
            -
                            logging.info(f"Appended new plan line at the end of the project file")
         | 
| 684 657 |  | 
| 685 658 | 
             
                    try:
         | 
| 686 659 | 
             
                        # Write the updated lines back to the project file
         | 
| 687 660 | 
             
                        with open(ras_obj.prj_file, 'w') as f:
         | 
| 688 661 | 
             
                            f.writelines(lines)
         | 
| 689 | 
            -
                        logging.info(f"Updated {ras_obj.prj_file} with new plan p{new_plan_num}")
         | 
| 690 662 | 
             
                    except IOError as e:
         | 
| 691 | 
            -
                         | 
| 692 | 
            -
                        raise
         | 
| 663 | 
            +
                        raise IOError(f"Failed to write updated project file: {e}")
         | 
| 693 664 |  | 
| 694 665 | 
             
                    new_plan = new_plan_num
         | 
| 695 666 |  | 
| @@ -704,6 +675,7 @@ class RasPlan: | |
| 704 675 | 
             
                    return new_plan
         | 
| 705 676 |  | 
| 706 677 | 
             
                @staticmethod
         | 
| 678 | 
            +
                @log_call
         | 
| 707 679 | 
             
                def clone_unsteady(template_unsteady, ras_object=None):
         | 
| 708 680 | 
             
                    """
         | 
| 709 681 | 
             
                    Copy unsteady flow files from a template, find the next unsteady number,
         | 
| @@ -735,27 +707,21 @@ class RasPlan: | |
| 735 707 | 
             
                    new_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}"
         | 
| 736 708 |  | 
| 737 709 | 
             
                    if not template_unsteady_path.exists():
         | 
| 738 | 
            -
                        logging.error(f"Template unsteady file '{template_unsteady_path}' does not exist.")
         | 
| 739 710 | 
             
                        raise FileNotFoundError(f"Template unsteady file '{template_unsteady_path}' does not exist.")
         | 
| 740 711 |  | 
| 741 712 | 
             
                    shutil.copy(template_unsteady_path, new_unsteady_path)
         | 
| 742 | 
            -
                    logging.info(f"Copied {template_unsteady_path} to {new_unsteady_path}")
         | 
| 743 713 |  | 
| 744 714 | 
             
                    # Copy the corresponding .hdf file if it exists
         | 
| 745 715 | 
             
                    template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}.hdf"
         | 
| 746 716 | 
             
                    new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}.hdf"
         | 
| 747 717 | 
             
                    if template_hdf_path.exists():
         | 
| 748 718 | 
             
                        shutil.copy(template_hdf_path, new_hdf_path)
         | 
| 749 | 
            -
                        logging.info(f"Copied {template_hdf_path} to {new_hdf_path}")
         | 
| 750 | 
            -
                    else:
         | 
| 751 | 
            -
                        logging.warning(f"No corresponding .hdf file found for '{template_unsteady_path}'. Skipping '.hdf' copy.")
         | 
| 752 719 |  | 
| 753 720 | 
             
                    try:
         | 
| 754 721 | 
             
                        with open(ras_obj.prj_file, 'r') as f:
         | 
| 755 722 | 
             
                            lines = f.readlines()
         | 
| 756 723 | 
             
                    except FileNotFoundError:
         | 
| 757 | 
            -
                         | 
| 758 | 
            -
                        raise
         | 
| 724 | 
            +
                        raise FileNotFoundError(f"Project file not found: {ras_obj.prj_file}")
         | 
| 759 725 |  | 
| 760 726 | 
             
                    # Prepare the new Unsteady Flow File entry line
         | 
| 761 727 | 
             
                    new_unsteady_line = f"Unsteady File=u{new_unsteady_num}\n"
         | 
| @@ -775,27 +741,22 @@ class RasPlan: | |
| 775 741 |  | 
| 776 742 | 
             
                    if insertion_index is not None:
         | 
| 777 743 | 
             
                        lines.insert(insertion_index, new_unsteady_line)
         | 
| 778 | 
            -
                        logging.info(f"Inserted new unsteady flow line at index {insertion_index}")
         | 
| 779 744 | 
             
                    else:
         | 
| 780 745 | 
             
                        # Try to insert after the last Unsteady Flow File entry
         | 
| 781 746 | 
             
                        unsteady_indices = [i for i, line in enumerate(lines) if unsteady_file_pattern.match(line.strip())]
         | 
| 782 747 | 
             
                        if unsteady_indices:
         | 
| 783 748 | 
             
                            last_unsteady_index = unsteady_indices[-1]
         | 
| 784 749 | 
             
                            lines.insert(last_unsteady_index + 1, new_unsteady_line)
         | 
| 785 | 
            -
                            logging.info(f"Inserted new unsteady flow line after index {last_unsteady_index}")
         | 
| 786 750 | 
             
                        else:
         | 
| 787 751 | 
             
                            # Append at the end if no Unsteady Flow File entries exist
         | 
| 788 752 | 
             
                            lines.append(new_unsteady_line)
         | 
| 789 | 
            -
                            logging.info(f"Appended new unsteady flow line at the end of the project file")
         | 
| 790 753 |  | 
| 791 754 | 
             
                    try:
         | 
| 792 755 | 
             
                        # Write the updated lines back to the project file
         | 
| 793 756 | 
             
                        with open(ras_obj.prj_file, 'w') as f:
         | 
| 794 757 | 
             
                            f.writelines(lines)
         | 
| 795 | 
            -
                        logging.info(f"Updated {ras_obj.prj_file} with new unsteady flow file u{new_unsteady_num}")
         | 
| 796 758 | 
             
                    except IOError as e:
         | 
| 797 | 
            -
                         | 
| 798 | 
            -
                        raise
         | 
| 759 | 
            +
                        raise IOError(f"Failed to write updated project file: {e}")
         | 
| 799 760 |  | 
| 800 761 | 
             
                    new_unsteady = new_unsteady_num
         | 
| 801 762 |  | 
| @@ -810,6 +771,7 @@ class RasPlan: | |
| 810 771 | 
             
                    return new_unsteady
         | 
| 811 772 |  | 
| 812 773 | 
             
                @staticmethod
         | 
| 774 | 
            +
                @log_call
         | 
| 813 775 | 
             
                def clone_steady(template_flow, ras_object=None):
         | 
| 814 776 | 
             
                    """
         | 
| 815 777 | 
             
                    Copy steady flow files from a template, find the next flow number,
         | 
| @@ -841,19 +803,16 @@ class RasPlan: | |
| 841 803 | 
             
                    new_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_flow_num}"
         | 
| 842 804 |  | 
| 843 805 | 
             
                    if not template_flow_path.exists():
         | 
| 844 | 
            -
                        logging.error(f"Template steady flow file '{template_flow_path}' does not exist.")
         | 
| 845 806 | 
             
                        raise FileNotFoundError(f"Template steady flow file '{template_flow_path}' does not exist.")
         | 
| 846 807 |  | 
| 847 808 | 
             
                    shutil.copy(template_flow_path, new_flow_path)
         | 
| 848 | 
            -
                    logging.info(f"Copied {template_flow_path} to {new_flow_path}")
         | 
| 849 809 |  | 
| 850 810 | 
             
                    # Read the contents of the project file
         | 
| 851 811 | 
             
                    try:
         | 
| 852 812 | 
             
                        with open(ras_obj.prj_file, 'r') as f:
         | 
| 853 813 | 
             
                            lines = f.readlines()
         | 
| 854 814 | 
             
                    except FileNotFoundError:
         | 
| 855 | 
            -
                         | 
| 856 | 
            -
                        raise
         | 
| 815 | 
            +
                        raise FileNotFoundError(f"Project file not found: {ras_obj.prj_file}")
         | 
| 857 816 |  | 
| 858 817 | 
             
                    # Prepare the new Steady Flow File entry line
         | 
| 859 818 | 
             
                    new_flow_line = f"Flow File=f{new_flow_num}\n"
         | 
| @@ -873,27 +832,22 @@ class RasPlan: | |
| 873 832 |  | 
| 874 833 | 
             
                    if insertion_index is not None:
         | 
| 875 834 | 
             
                        lines.insert(insertion_index, new_flow_line)
         | 
| 876 | 
            -
                        logging.info(f"Inserted new steady flow line at index {insertion_index}")
         | 
| 877 835 | 
             
                    else:
         | 
| 878 836 | 
             
                        # Try to insert after the last Steady Flow File entry
         | 
| 879 837 | 
             
                        flow_indices = [i for i, line in enumerate(lines) if flow_file_pattern.match(line.strip())]
         | 
| 880 838 | 
             
                        if flow_indices:
         | 
| 881 839 | 
             
                            last_flow_index = flow_indices[-1]
         | 
| 882 840 | 
             
                            lines.insert(last_flow_index + 1, new_flow_line)
         | 
| 883 | 
            -
                            logging.info(f"Inserted new steady flow line after index {last_flow_index}")
         | 
| 884 841 | 
             
                        else:
         | 
| 885 842 | 
             
                            # Append at the end if no Steady Flow File entries exist
         | 
| 886 843 | 
             
                            lines.append(new_flow_line)
         | 
| 887 | 
            -
                            logging.info(f"Appended new steady flow line at the end of the project file")
         | 
| 888 844 |  | 
| 889 845 | 
             
                    try:
         | 
| 890 846 | 
             
                        # Write the updated lines back to the project file
         | 
| 891 847 | 
             
                        with open(ras_obj.prj_file, 'w') as f:
         | 
| 892 848 | 
             
                            f.writelines(lines)
         | 
| 893 | 
            -
                        logging.info(f"Updated {ras_obj.prj_file} with new steady flow file f{new_flow_num}")
         | 
| 894 849 | 
             
                    except IOError as e:
         | 
| 895 | 
            -
                         | 
| 896 | 
            -
                        raise
         | 
| 850 | 
            +
                        raise IOError(f"Failed to write updated project file: {e}")
         | 
| 897 851 |  | 
| 898 852 | 
             
                    new_steady = new_flow_num
         | 
| 899 853 |  | 
| @@ -908,6 +862,7 @@ class RasPlan: | |
| 908 862 | 
             
                    return new_steady
         | 
| 909 863 |  | 
| 910 864 | 
             
                @staticmethod
         | 
| 865 | 
            +
                @log_call
         | 
| 911 866 | 
             
                def clone_geom(template_geom, ras_object=None):
         | 
| 912 867 | 
             
                    """
         | 
| 913 868 | 
             
                    Copy geometry files from a template, find the next geometry number,
         | 
| @@ -928,13 +883,11 @@ class RasPlan: | |
| 928 883 |  | 
| 929 884 | 
             
                    # Update geometry entries without reinitializing the entire project
         | 
| 930 885 | 
             
                    ras_obj.geom_df = ras_obj.get_prj_entries('Geom')
         | 
| 931 | 
            -
                    logging.debug(f"Updated geometry entries:\n{ras_obj.geom_df}")
         | 
| 932 886 |  | 
| 933 887 | 
             
                    template_geom_filename = f"{ras_obj.project_name}.g{template_geom}"
         | 
| 934 888 | 
             
                    template_geom_path = ras_obj.project_folder / template_geom_filename
         | 
| 935 889 |  | 
| 936 890 | 
             
                    if not template_geom_path.is_file():
         | 
| 937 | 
            -
                        logging.error(f"Template geometry file '{template_geom_path}' does not exist.")
         | 
| 938 891 | 
             
                        raise FileNotFoundError(f"Template geometry file '{template_geom_path}' does not exist.")
         | 
| 939 892 |  | 
| 940 893 | 
             
                    next_geom_number = RasPlan.get_next_number(ras_obj.geom_df['geom_number'])
         | 
| @@ -943,23 +896,18 @@ class RasPlan: | |
| 943 896 | 
             
                    new_geom_path = ras_obj.project_folder / new_geom_filename
         | 
| 944 897 |  | 
| 945 898 | 
             
                    shutil.copyfile(template_geom_path, new_geom_path)
         | 
| 946 | 
            -
                    logging.info(f"Copied '{template_geom_path}' to '{new_geom_path}'.")
         | 
| 947 899 |  | 
| 948 900 | 
             
                    # Handle HDF file copy
         | 
| 949 901 | 
             
                    template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{template_geom}.hdf"
         | 
| 950 902 | 
             
                    new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.g{next_geom_number}.hdf"
         | 
| 951 903 | 
             
                    if template_hdf_path.is_file():
         | 
| 952 904 | 
             
                        shutil.copyfile(template_hdf_path, new_hdf_path)
         | 
| 953 | 
            -
                        logging.info(f"Copied '{template_hdf_path}' to '{new_hdf_path}'.")
         | 
| 954 | 
            -
                    else:
         | 
| 955 | 
            -
                        logging.warning(f"Template geometry HDF file '{template_hdf_path}' does not exist. Skipping '.hdf' copy.")
         | 
| 956 905 |  | 
| 957 906 | 
             
                    try:
         | 
| 958 907 | 
             
                        with open(ras_obj.prj_file, 'r') as file:
         | 
| 959 908 | 
             
                            lines = file.readlines()
         | 
| 960 909 | 
             
                    except FileNotFoundError:
         | 
| 961 | 
            -
                         | 
| 962 | 
            -
                        raise
         | 
| 910 | 
            +
                        raise FileNotFoundError(f"Project file not found: {ras_obj.prj_file}")
         | 
| 963 911 |  | 
| 964 912 | 
             
                    # Prepare the new Geometry File entry line
         | 
| 965 913 | 
             
                    new_geom_line = f"Geom File=g{next_geom_number}\n"
         | 
| @@ -979,27 +927,22 @@ class RasPlan: | |
| 979 927 |  | 
| 980 928 | 
             
                    if insertion_index is not None:
         | 
| 981 929 | 
             
                        lines.insert(insertion_index, new_geom_line)
         | 
| 982 | 
            -
                        logging.info(f"Inserted new geometry line at index {insertion_index}")
         | 
| 983 930 | 
             
                    else:
         | 
| 984 931 | 
             
                        # Try to insert after the last Geometry File entry
         | 
| 985 932 | 
             
                        geom_indices = [i for i, line in enumerate(lines) if geom_file_pattern.match(line.strip())]
         | 
| 986 933 | 
             
                        if geom_indices:
         | 
| 987 934 | 
             
                            last_geom_index = geom_indices[-1]
         | 
| 988 935 | 
             
                            lines.insert(last_geom_index + 1, new_geom_line)
         | 
| 989 | 
            -
                            logging.info(f"Inserted new geometry line after index {last_geom_index}")
         | 
| 990 936 | 
             
                        else:
         | 
| 991 937 | 
             
                            # Append at the end if no Geometry File entries exist
         | 
| 992 938 | 
             
                            lines.append(new_geom_line)
         | 
| 993 | 
            -
                            logging.info(f"Appended new geometry line at the end of the project file")
         | 
| 994 939 |  | 
| 995 940 | 
             
                    try:
         | 
| 996 941 | 
             
                        # Write the updated lines back to the project file
         | 
| 997 942 | 
             
                        with open(ras_obj.prj_file, 'w') as file:
         | 
| 998 943 | 
             
                            file.writelines(lines)
         | 
| 999 | 
            -
                        logging.info(f"Updated {ras_obj.prj_file} with new geometry file g{next_geom_number}")
         | 
| 1000 944 | 
             
                    except IOError as e:
         | 
| 1001 | 
            -
                         | 
| 1002 | 
            -
                        raise
         | 
| 945 | 
            +
                        raise IOError(f"Failed to write updated project file: {e}")
         | 
| 1003 946 |  | 
| 1004 947 | 
             
                    new_geom = next_geom_number
         | 
| 1005 948 |  | 
| @@ -1009,11 +952,10 @@ class RasPlan: | |
| 1009 952 | 
             
                    ras_obj.flow_df = ras_obj.get_flow_entries()
         | 
| 1010 953 | 
             
                    ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
         | 
| 1011 954 |  | 
| 1012 | 
            -
                    logging.debug(f"Updated geometry entries:\n{ras_obj.geom_df}")
         | 
| 1013 | 
            -
             | 
| 1014 955 | 
             
                    return new_geom
         | 
| 1015 956 |  | 
| 1016 957 | 
             
                @staticmethod
         | 
| 958 | 
            +
                @log_call
         | 
| 1017 959 | 
             
                def get_next_number(existing_numbers):
         | 
| 1018 960 | 
             
                    """
         | 
| 1019 961 | 
             
                    Determine the next available number from a list of existing numbers.
         | 
| @@ -1041,8 +983,8 @@ class RasPlan: | |
| 1041 983 | 
             
                            break
         | 
| 1042 984 | 
             
                    return f"{next_number:02d}"
         | 
| 1043 985 |  | 
| 1044 | 
            -
             | 
| 1045 986 | 
             
                @staticmethod
         | 
| 987 | 
            +
                @log_call
         | 
| 1046 988 | 
             
                def get_plan_value(
         | 
| 1047 989 | 
             
                    plan_number_or_path: Union[str, Path],
         | 
| 1048 990 | 
             
                    key: str,
         | 
| @@ -1064,47 +1006,51 @@ class RasPlan: | |
| 1064 1006 | 
             
                    IOError: If there's an error reading the plan file
         | 
| 1065 1007 |  | 
| 1066 1008 | 
             
                    Available keys and their expected types:
         | 
| 1067 | 
            -
                    - ' | 
| 1068 | 
            -
                    - ' | 
| 1069 | 
            -
                    - ' | 
| 1070 | 
            -
                    - ' | 
| 1071 | 
            -
                    - ' | 
| 1072 | 
            -
                    - ' | 
| 1073 | 
            -
                    - ' | 
| 1074 | 
            -
                    - ' | 
| 1075 | 
            -
                    - ' | 
| 1076 | 
            -
                    - ' | 
| 1077 | 
            -
                    - ' | 
| 1078 | 
            -
                    - ' | 
| 1079 | 
            -
                    - ' | 
| 1080 | 
            -
                    - ' | 
| 1081 | 
            -
                    - ' | 
| 1082 | 
            -
                    - ' | 
| 1083 | 
            -
                    - ' | 
| 1084 | 
            -
                    - ' | 
| 1085 | 
            -
                    - ' | 
| 1086 | 
            -
                    - ' | 
| 1087 | 
            -
                    - ' | 
| 1088 | 
            -
                    - ' | 
| 1089 | 
            -
                     | 
| 1009 | 
            +
                    - 'Computation Interval' (str): Time value for computational time step (e.g., '5SEC', '2MIN')
         | 
| 1010 | 
            +
                    - 'DSS File' (str): Name of the DSS file used
         | 
| 1011 | 
            +
                    - 'Flow File' (str): Name of the flow input file
         | 
| 1012 | 
            +
                    - 'Friction Slope Method' (int): Method selection for friction slope (e.g., 1, 2)
         | 
| 1013 | 
            +
                    - 'Geom File' (str): Name of the geometry input file
         | 
| 1014 | 
            +
                    - 'Mapping Interval' (str): Time interval for mapping output
         | 
| 1015 | 
            +
                    - 'Plan File' (str): Name of the plan file
         | 
| 1016 | 
            +
                    - 'Plan Title' (str): Title of the simulation plan
         | 
| 1017 | 
            +
                    - 'Program Version' (str): Version number of HEC-RAS
         | 
| 1018 | 
            +
                    - 'Run HTAB' (int): Flag to run HTab module (-1 or 1)
         | 
| 1019 | 
            +
                    - 'Run Post Process' (int): Flag to run post-processing (-1 or 1)
         | 
| 1020 | 
            +
                    - 'Run Sediment' (int): Flag to run sediment transport module (0 or 1)
         | 
| 1021 | 
            +
                    - 'Run UNET' (int): Flag to run unsteady network module (-1 or 1)
         | 
| 1022 | 
            +
                    - 'Run WQNET' (int): Flag to run water quality module (0 or 1)
         | 
| 1023 | 
            +
                    - 'Short Identifier' (str): Short name or ID for the plan
         | 
| 1024 | 
            +
                    - 'Simulation Date' (str): Start and end dates/times for simulation
         | 
| 1025 | 
            +
                    - 'UNET D1 Cores' (int): Number of cores used in 1D calculations
         | 
| 1026 | 
            +
                    - 'UNET Use Existing IB Tables' (int): Flag for using existing internal boundary tables (-1, 0, or 1)
         | 
| 1027 | 
            +
                    - 'UNET 1D Methodology' (str): 1D calculation methodology
         | 
| 1028 | 
            +
                    - 'UNET D2 Solver Type' (str): 2D solver type
         | 
| 1029 | 
            +
                    - 'UNET D2 Name' (str): Name of the 2D area
         | 
| 1030 | 
            +
                    - 'Run RASMapper' (int): Flag to run RASMapper for floodplain mapping (-1 for off, 0 for on)
         | 
| 1031 | 
            +
                    
         | 
| 1032 | 
            +
                    
         | 
| 1033 | 
            +
                    Note: 
         | 
| 1034 | 
            +
                    Writing Multi line keys like 'Description' are not supported by this function.
         | 
| 1090 1035 |  | 
| 1091 1036 | 
             
                    Example:
         | 
| 1092 | 
            -
                    >>> computation_interval = RasPlan.get_plan_value("01", " | 
| 1037 | 
            +
                    >>> computation_interval = RasPlan.get_plan_value("01", "Computation Interval")
         | 
| 1093 1038 | 
             
                    >>> print(f"Computation interval: {computation_interval}")
         | 
| 1094 1039 | 
             
                    """
         | 
| 1095 1040 | 
             
                    ras_obj = ras_object or ras
         | 
| 1096 1041 | 
             
                    ras_obj.check_initialized()
         | 
| 1097 1042 |  | 
| 1098 | 
            -
                     | 
| 1099 | 
            -
                        ' | 
| 1100 | 
            -
                        ' | 
| 1101 | 
            -
                        ' | 
| 1102 | 
            -
                        ' | 
| 1103 | 
            -
                        ' | 
| 1043 | 
            +
                    supported_plan_keys = {
         | 
| 1044 | 
            +
                        'Description', 'Computation Interval', 'DSS File', 'Flow File', 'Friction Slope Method',
         | 
| 1045 | 
            +
                        'Geom File', 'Mapping Interval', 'Plan File', 'Plan Title', 'Program Version',
         | 
| 1046 | 
            +
                        'Run HTAB', 'Run Post Process', 'Run Sediment', 'Run UNET', 'Run WQNET',
         | 
| 1047 | 
            +
                        'Short Identifier', 'Simulation Date', 'UNET D1 Cores', 'UNET Use Existing IB Tables',
         | 
| 1048 | 
            +
                        'UNET 1D Methodology', 'UNET D2 Solver Type', 'UNET D2 Name', 'Run RASMapper'
         | 
| 1104 1049 | 
             
                    }
         | 
| 1105 1050 |  | 
| 1106 | 
            -
                    if key not in  | 
| 1107 | 
            -
                         | 
| 1051 | 
            +
                    if key not in supported_plan_keys:
         | 
| 1052 | 
            +
                        logger = logging.getLogger(__name__)
         | 
| 1053 | 
            +
                        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.")
         | 
| 1108 1054 |  | 
| 1109 1055 | 
             
                    plan_file_path = Path(plan_number_or_path)
         | 
| 1110 1056 | 
             
                    if not plan_file_path.is_file():
         | 
| @@ -1116,22 +1062,25 @@ class RasPlan: | |
| 1116 1062 | 
             
                        with open(plan_file_path, 'r') as file:
         | 
| 1117 1063 | 
             
                            content = file.read()
         | 
| 1118 1064 | 
             
                    except IOError as e:
         | 
| 1119 | 
            -
                        logging. | 
| 1065 | 
            +
                        logger = logging.getLogger(__name__)
         | 
| 1066 | 
            +
                        logger.error(f"Error reading plan file {plan_file_path}: {e}")
         | 
| 1120 1067 | 
             
                        raise
         | 
| 1121 1068 |  | 
| 1122 | 
            -
                    if key == ' | 
| 1069 | 
            +
                    if key == 'Description':
         | 
| 1123 1070 | 
             
                        match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
         | 
| 1124 1071 | 
             
                        return match.group(1).strip() if match else None
         | 
| 1125 1072 | 
             
                    else:
         | 
| 1126 | 
            -
                        pattern = f"{key | 
| 1073 | 
            +
                        pattern = f"{key}=(.*)"
         | 
| 1127 1074 | 
             
                        match = re.search(pattern, content)
         | 
| 1128 1075 | 
             
                        if match:
         | 
| 1129 1076 | 
             
                            return match.group(1).strip()
         | 
| 1130 1077 | 
             
                        else:
         | 
| 1131 | 
            -
                            logging. | 
| 1078 | 
            +
                            logger = logging.getLogger(__name__)
         | 
| 1079 | 
            +
                            logger.error(f"Key '{key}' not found in the plan file.")
         | 
| 1132 1080 | 
             
                            return None
         | 
| 1133 1081 |  | 
| 1134 1082 | 
             
                @staticmethod
         | 
| 1083 | 
            +
                @log_call
         | 
| 1135 1084 | 
             
                def update_plan_value(
         | 
| 1136 1085 | 
             
                    plan_number_or_path: Union[str, Path],
         | 
| 1137 1086 | 
             
                    key: str,
         | 
| @@ -1161,16 +1110,16 @@ class RasPlan: | |
| 1161 1110 | 
             
                    ras_obj = ras_object or ras
         | 
| 1162 1111 | 
             
                    ras_obj.check_initialized()
         | 
| 1163 1112 |  | 
| 1164 | 
            -
                     | 
| 1165 | 
            -
                        ' | 
| 1166 | 
            -
                        ' | 
| 1167 | 
            -
                        ' | 
| 1168 | 
            -
                        ' | 
| 1169 | 
            -
                        ' | 
| 1113 | 
            +
                    supported_plan_keys = {
         | 
| 1114 | 
            +
                        'Description', 'Computation Interval', 'DSS File', 'Flow File', 'Friction Slope Method',
         | 
| 1115 | 
            +
                        'Geom File', 'Mapping Interval', 'Plan File', 'Plan Title', 'Program Version',
         | 
| 1116 | 
            +
                        'Run HTAB', 'Run Post Process', 'Run Sediment', 'Run UNET', 'Run WQNET',
         | 
| 1117 | 
            +
                        'Short Identifier', 'Simulation Date', 'UNET D1 Cores', 'UNET Use Existing IB Tables',
         | 
| 1118 | 
            +
                        'UNET 1D Methodology', 'UNET D2 Solver Type', 'UNET D2 Name', 'Run RASMapper'
         | 
| 1170 1119 | 
             
                    }
         | 
| 1171 | 
            -
             | 
| 1172 | 
            -
                    if key not in  | 
| 1173 | 
            -
                         | 
| 1120 | 
            +
                    logger = logging.getLogger(__name__)
         | 
| 1121 | 
            +
                    if key not in supported_plan_keys:
         | 
| 1122 | 
            +
                        logger.warning(f"Unknown key: {key}. Valid keys are: {', '.join(supported_plan_keys)}")
         | 
| 1174 1123 |  | 
| 1175 1124 | 
             
                    plan_file_path = Path(plan_number_or_path)
         | 
| 1176 1125 | 
             
                    if not plan_file_path.is_file():
         | 
| @@ -1182,7 +1131,7 @@ class RasPlan: | |
| 1182 1131 | 
             
                        with open(plan_file_path, 'r') as file:
         | 
| 1183 1132 | 
             
                            lines = file.readlines()
         | 
| 1184 1133 | 
             
                    except IOError as e:
         | 
| 1185 | 
            -
                         | 
| 1134 | 
            +
                        logger.error(f"Error reading plan file {plan_file_path}: {e}")
         | 
| 1186 1135 | 
             
                        raise
         | 
| 1187 1136 |  | 
| 1188 1137 | 
             
                    # Special handling for description
         | 
| @@ -1209,15 +1158,15 @@ class RasPlan: | |
| 1209 1158 | 
             
                                updated = True
         | 
| 1210 1159 | 
             
                                break
         | 
| 1211 1160 | 
             
                        if not updated:
         | 
| 1212 | 
            -
                             | 
| 1161 | 
            +
                            logger.error(f"Key '{key}' not found in the plan file.")
         | 
| 1213 1162 | 
             
                            return
         | 
| 1214 1163 |  | 
| 1215 1164 | 
             
                    try:
         | 
| 1216 1165 | 
             
                        with open(plan_file_path, 'w') as file:
         | 
| 1217 1166 | 
             
                            file.writelines(lines)
         | 
| 1218 | 
            -
                         | 
| 1167 | 
            +
                        logger.info(f"Updated {key} in plan file: {plan_file_path}")
         | 
| 1219 1168 | 
             
                    except IOError as e:
         | 
| 1220 | 
            -
                         | 
| 1169 | 
            +
                        logger.error(f"Error writing to plan file {plan_file_path}: {e}")
         | 
| 1221 1170 | 
             
                        raise
         | 
| 1222 1171 |  | 
| 1223 1172 | 
             
                    # Refresh RasPrj dataframes
         |