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/RasPrj.py
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 | 
            -
            """ | 
| 1 | 
            +
            """
         | 
| 2 | 
            +
            RasPrj.py - Manages HEC-RAS projects within the ras-commander library
         | 
| 2 3 |  | 
| 3 4 | 
             
            This module provides a class for managing HEC-RAS projects.
         | 
| 4 5 |  | 
| @@ -14,31 +15,46 @@ This class is used to initialize a RAS project and is used in conjunction with t | |
| 14 15 | 
             
            By default, the RasPrj class is initialized with the global 'ras' object.
         | 
| 15 16 | 
             
            However, you can create multiple RasPrj instances to manage multiple projects.
         | 
| 16 17 | 
             
            Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
         | 
| 17 | 
            -
            """
         | 
| 18 18 |  | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 19 | 
            +
            This module is part of the ras-commander library and uses a centralized logging configuration.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            Logging Configuration:
         | 
| 22 | 
            +
            - The logging is set up in the logging_config.py file.
         | 
| 23 | 
            +
            - A @log_call decorator is available to automatically log function calls.
         | 
| 24 | 
            +
            - Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
         | 
| 25 | 
            +
            - Logs are written to both console and a rotating file handler.
         | 
| 26 | 
            +
            - The default log file is 'ras_commander.log' in the 'logs' directory.
         | 
| 27 | 
            +
            - The default log level is INFO.
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            To use logging in this module:
         | 
| 30 | 
            +
            1. Use the @log_call decorator for automatic function call logging.
         | 
| 31 | 
            +
            2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
         | 
| 32 | 
            +
             | 
| 33 | 
            +
             | 
| 34 | 
            +
            Example:
         | 
| 35 | 
            +
                @log_call
         | 
| 36 | 
            +
                def my_function():
         | 
| 37 | 
            +
                    
         | 
| 38 | 
            +
                    logger.debug("Additional debug information")
         | 
| 39 | 
            +
                    # Function logic here
         | 
| 40 | 
            +
            """
         | 
| 41 | 
            +
            import os
         | 
| 21 42 | 
             
            import re
         | 
| 22 43 | 
             
            from pathlib import Path
         | 
| 23 44 | 
             
            import pandas as pd
         | 
| 24 | 
            -
            import logging
         | 
| 25 45 | 
             
            from typing import Union, Any, List, Dict, Tuple
         | 
| 46 | 
            +
            import logging
         | 
| 47 | 
            +
            from ras_commander.logging_config import get_logger, log_call
         | 
| 26 48 |  | 
| 27 | 
            -
             | 
| 28 | 
            -
            # Configure logging
         | 
| 29 | 
            -
            logging.basicConfig(
         | 
| 30 | 
            -
                level=logging.INFO,
         | 
| 31 | 
            -
                format='%(asctime)s - %(levelname)s - %(message)s',
         | 
| 32 | 
            -
                handlers=[
         | 
| 33 | 
            -
                    logging.StreamHandler()
         | 
| 34 | 
            -
                ]
         | 
| 35 | 
            -
            )
         | 
| 49 | 
            +
            logger = get_logger(__name__)
         | 
| 36 50 |  | 
| 37 51 | 
             
            class RasPrj:
         | 
| 52 | 
            +
                
         | 
| 38 53 | 
             
                def __init__(self):
         | 
| 39 54 | 
             
                    self.initialized = False
         | 
| 40 55 | 
             
                    self.boundaries_df = None  # New attribute to store boundary conditions
         | 
| 41 56 |  | 
| 57 | 
            +
                @log_call
         | 
| 42 58 | 
             
                def initialize(self, project_folder, ras_exe_path):
         | 
| 43 59 | 
             
                    """
         | 
| 44 60 | 
             
                    Initialize a RasPrj instance.
         | 
| @@ -60,18 +76,19 @@ class RasPrj: | |
| 60 76 | 
             
                    self.project_folder = Path(project_folder)
         | 
| 61 77 | 
             
                    self.prj_file = self.find_ras_prj(self.project_folder)
         | 
| 62 78 | 
             
                    if self.prj_file is None:
         | 
| 63 | 
            -
                         | 
| 79 | 
            +
                        logger.error(f"No HEC-RAS project file found in {self.project_folder}")
         | 
| 64 80 | 
             
                        raise ValueError(f"No HEC-RAS project file found in {self.project_folder}")
         | 
| 65 81 | 
             
                    self.project_name = Path(self.prj_file).stem
         | 
| 66 82 | 
             
                    self.ras_exe_path = ras_exe_path
         | 
| 67 83 | 
             
                    self._load_project_data()
         | 
| 68 84 | 
             
                    self.boundaries_df = self.get_boundary_conditions()  # Extract boundary conditions
         | 
| 69 85 | 
             
                    self.initialized = True
         | 
| 70 | 
            -
                     | 
| 71 | 
            -
                     | 
| 86 | 
            +
                    logger.info(f"Initialization complete for project: {self.project_name}")
         | 
| 87 | 
            +
                    logger.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
         | 
| 72 88 | 
             
                                 f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
         | 
| 73 89 | 
             
                                 f"Boundary conditions: {len(self.boundaries_df)}")
         | 
| 74 90 |  | 
| 91 | 
            +
                @log_call
         | 
| 75 92 | 
             
                def _load_project_data(self):
         | 
| 76 93 | 
             
                    """
         | 
| 77 94 | 
             
                    Load project data from the HEC-RAS project file.
         | 
| @@ -85,6 +102,7 @@ class RasPrj: | |
| 85 102 | 
             
                    self.unsteady_df = self._get_prj_entries('Unsteady')
         | 
| 86 103 | 
             
                    self.geom_df = self._get_prj_entries('Geom')
         | 
| 87 104 |  | 
| 105 | 
            +
                @log_call
         | 
| 88 106 | 
             
                def _parse_plan_file(self, plan_file_path):
         | 
| 89 107 | 
             
                    """
         | 
| 90 108 | 
             
                    Parse a plan file and extract critical information.
         | 
| @@ -96,45 +114,57 @@ class RasPrj: | |
| 96 114 | 
             
                        dict: Dictionary containing extracted plan information.
         | 
| 97 115 | 
             
                    """
         | 
| 98 116 | 
             
                    plan_info = {}
         | 
| 99 | 
            -
                     | 
| 100 | 
            -
                         | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
             | 
| 104 | 
            -
             | 
| 105 | 
            -
                             | 
| 106 | 
            -
             | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
                             | 
| 110 | 
            -
                             | 
| 111 | 
            -
                             | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 117 | 
            +
                    try:
         | 
| 118 | 
            +
                        with open(plan_file_path, 'r') as file:
         | 
| 119 | 
            +
                            content = file.read()
         | 
| 120 | 
            +
                            
         | 
| 121 | 
            +
                            # Extract description
         | 
| 122 | 
            +
                            description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
         | 
| 123 | 
            +
                            if description_match:
         | 
| 124 | 
            +
                                plan_info['description'] = description_match.group(1).strip()
         | 
| 125 | 
            +
                            
         | 
| 126 | 
            +
                            # BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the plan file keys.
         | 
| 127 | 
            +
                            
         | 
| 128 | 
            +
                            # Extract other critical information
         | 
| 129 | 
            +
                            supported_plan_keys = {
         | 
| 130 | 
            +
                                'Computation Interval': r'Computation Interval=(.+)',
         | 
| 131 | 
            +
                                'DSS File': r'DSS File=(.+)',
         | 
| 132 | 
            +
                                'Flow File': r'Flow File=(.+)',
         | 
| 133 | 
            +
                                'Friction Slope Method': r'Friction Slope Method=(.+)',
         | 
| 134 | 
            +
                                'Geom File': r'Geom File=(.+)',
         | 
| 135 | 
            +
                                'Mapping Interval': r'Mapping Interval=(.+)',
         | 
| 136 | 
            +
                                'Plan Title': r'Plan Title=(.+)',
         | 
| 137 | 
            +
                                'Program Version': r'Program Version=(.+)',
         | 
| 138 | 
            +
                                'Run HTab': r'Run HTab=(.+)',
         | 
| 139 | 
            +
                                'Run PostProcess': r'Run PostProcess=(.+)',
         | 
| 140 | 
            +
                                'Run Sediment': r'Run Sediment=(.+)',
         | 
| 141 | 
            +
                                'Run UNet': r'Run UNet=(.+)',
         | 
| 142 | 
            +
                                'Run WQNet': r'Run WQNet=(.+)',
         | 
| 143 | 
            +
                                'Short Identifier': r'Short Identifier=(.+)',
         | 
| 144 | 
            +
                                'Simulation Date': r'Simulation Date=(.+)',
         | 
| 145 | 
            +
                                'UNET D1 Cores': r'UNET D1 Cores=(.+)',
         | 
| 146 | 
            +
                                'UNET Use Existing IB Tables': r'UNET Use Existing IB Tables=(.+)',
         | 
| 147 | 
            +
                                'UNET 1D Methodology': r'UNET 1D Methodology=(.+)',
         | 
| 148 | 
            +
                                'UNET D2 SolverType': r'UNET D2 SolverType=(.+)',
         | 
| 149 | 
            +
                                'UNET D2 Name': r'UNET D2 Name=(.+)'
         | 
| 150 | 
            +
                            }
         | 
| 151 | 
            +
                            
         | 
| 152 | 
            +
                            # END Exception to Style Guide
         | 
| 153 | 
            +
                            
         | 
| 154 | 
            +
                            for key, pattern in supported_plan_keys.items():
         | 
| 155 | 
            +
                                match = re.search(pattern, content)
         | 
| 156 | 
            +
                                if match:
         | 
| 157 | 
            +
                                    plan_info[key] = match.group(1).strip()
         | 
| 130 158 |  | 
| 131 | 
            -
                         | 
| 132 | 
            -
             | 
| 133 | 
            -
             | 
| 134 | 
            -
                                plan_info[key] = match.group(1).strip()
         | 
| 159 | 
            +
                        logger.debug(f"Parsed plan file: {plan_file_path}")
         | 
| 160 | 
            +
                    except Exception as e:
         | 
| 161 | 
            +
                        logger.exception(f"Error parsing plan file {plan_file_path}: {e}")
         | 
| 135 162 |  | 
| 136 163 | 
             
                    return plan_info
         | 
| 137 164 |  | 
| 165 | 
            +
             | 
| 166 | 
            +
                
         | 
| 167 | 
            +
                @log_call
         | 
| 138 168 | 
             
                def _get_prj_entries(self, entry_type):
         | 
| 139 169 | 
             
                    """
         | 
| 140 170 | 
             
                    Extract entries of a specific type from the HEC-RAS project file.
         | 
| @@ -168,7 +198,6 @@ class RasPrj: | |
| 168 198 | 
             
                                        plan_info = self._parse_plan_file(Path(full_path))
         | 
| 169 199 | 
             
                                        entry.update(plan_info)
         | 
| 170 200 |  | 
| 171 | 
            -
                                        # Add HDF results path if it exists
         | 
| 172 201 | 
             
                                        hdf_results_path = self.project_folder / f"{self.project_name}.p{file_name[1:]}.hdf"
         | 
| 173 202 | 
             
                                        entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
         | 
| 174 203 |  | 
| @@ -178,11 +207,11 @@ class RasPrj: | |
| 178 207 |  | 
| 179 208 | 
             
                                    entries.append(entry)
         | 
| 180 209 | 
             
                    except Exception as e:
         | 
| 181 | 
            -
                        logging.exception(f"Failed to read project file {self.prj_file}: {e}")
         | 
| 182 210 | 
             
                        raise
         | 
| 183 211 |  | 
| 184 212 | 
             
                    return pd.DataFrame(entries)
         | 
| 185 213 |  | 
| 214 | 
            +
                @log_call
         | 
| 186 215 | 
             
                def _parse_unsteady_file(self, unsteady_file_path):
         | 
| 187 216 | 
             
                    """
         | 
| 188 217 | 
             
                    Parse an unsteady flow file and extract critical information.
         | 
| @@ -197,21 +226,24 @@ class RasPrj: | |
| 197 226 | 
             
                    with open(unsteady_file_path, 'r') as file:
         | 
| 198 227 | 
             
                        content = file.read()
         | 
| 199 228 |  | 
| 200 | 
            -
                        #  | 
| 201 | 
            -
             | 
| 202 | 
            -
             | 
| 203 | 
            -
                            ' | 
| 204 | 
            -
                            ' | 
| 205 | 
            -
                            ' | 
| 206 | 
            -
                            ' | 
| 207 | 
            -
                            ' | 
| 208 | 
            -
                            ' | 
| 209 | 
            -
                            ' | 
| 210 | 
            -
                            ' | 
| 211 | 
            -
                            ' | 
| 229 | 
            +
                        # BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the unsteady file keys.
         | 
| 230 | 
            +
                            
         | 
| 231 | 
            +
                        supported_unsteady_keys = {
         | 
| 232 | 
            +
                            'Flow Title': r'Flow Title=(.+)',
         | 
| 233 | 
            +
                            'Program Version': r'Program Version=(.+)',
         | 
| 234 | 
            +
                            'Use Restart': r'Use Restart=(.+)',
         | 
| 235 | 
            +
                            'Precipitation Mode': r'Precipitation Mode=(.+)',
         | 
| 236 | 
            +
                            'Wind Mode': r'Wind Mode=(.+)',
         | 
| 237 | 
            +
                            'Met BC=Precipitation|Mode': r'Met BC=Precipitation\|Mode=(.+)',
         | 
| 238 | 
            +
                            'Met BC=Evapotranspiration|Mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
         | 
| 239 | 
            +
                            'Met BC=Precipitation|Expanded View': r'Met BC=Precipitation\|Expanded View=(.+)',
         | 
| 240 | 
            +
                            'Met BC=Precipitation|Constant Units': r'Met BC=Precipitation\|Constant Units=(.+)',
         | 
| 241 | 
            +
                            'Met BC=Precipitation|Gridded Source': r'Met BC=Precipitation\|Gridded Source=(.+)'
         | 
| 212 242 | 
             
                        }
         | 
| 213 243 |  | 
| 214 | 
            -
                         | 
| 244 | 
            +
                        # END Exception to Style Guide
         | 
| 245 | 
            +
                        
         | 
| 246 | 
            +
                        for key, pattern in supported_unsteady_keys.items():
         | 
| 215 247 | 
             
                            match = re.search(pattern, content)
         | 
| 216 248 | 
             
                            if match:
         | 
| 217 249 | 
             
                                unsteady_info[key] = match.group(1).strip()
         | 
| @@ -228,6 +260,7 @@ class RasPrj: | |
| 228 260 | 
             
                    """
         | 
| 229 261 | 
             
                    return self.initialized
         | 
| 230 262 |  | 
| 263 | 
            +
                @log_call
         | 
| 231 264 | 
             
                def check_initialized(self):
         | 
| 232 265 | 
             
                    """
         | 
| 233 266 | 
             
                    Ensure that the RasPrj instance has been initialized.
         | 
| @@ -236,10 +269,10 @@ class RasPrj: | |
| 236 269 | 
             
                        RuntimeError: If the project has not been initialized.
         | 
| 237 270 | 
             
                    """
         | 
| 238 271 | 
             
                    if not self.initialized:
         | 
| 239 | 
            -
                        logging.error("Project not initialized. Call init_ras_project() first.")
         | 
| 240 272 | 
             
                        raise RuntimeError("Project not initialized. Call init_ras_project() first.")
         | 
| 241 273 |  | 
| 242 274 | 
             
                @staticmethod
         | 
| 275 | 
            +
                @log_call
         | 
| 243 276 | 
             
                def find_ras_prj(folder_path):
         | 
| 244 277 | 
             
                    """
         | 
| 245 278 | 
             
                    Find the appropriate HEC-RAS project file (.prj) in the given folder.
         | 
| @@ -254,28 +287,25 @@ class RasPrj: | |
| 254 287 | 
             
                    prj_files = list(folder_path.glob("*.prj"))
         | 
| 255 288 | 
             
                    rasmap_files = list(folder_path.glob("*.rasmap"))
         | 
| 256 289 | 
             
                    if len(prj_files) == 1:
         | 
| 257 | 
            -
                        logging.info(f"Single .prj file found: {prj_files[0]}")
         | 
| 258 290 | 
             
                        return prj_files[0].resolve()
         | 
| 259 291 | 
             
                    if len(prj_files) > 1:
         | 
| 260 292 | 
             
                        if len(rasmap_files) == 1:
         | 
| 261 293 | 
             
                            base_filename = rasmap_files[0].stem
         | 
| 262 294 | 
             
                            prj_file = folder_path / f"{base_filename}.prj"
         | 
| 263 295 | 
             
                            if prj_file.exists():
         | 
| 264 | 
            -
                                logging.info(f"Matched .prj file based on .rasmap: {prj_file}")
         | 
| 265 296 | 
             
                                return prj_file.resolve()
         | 
| 266 297 | 
             
                        for prj_file in prj_files:
         | 
| 267 298 | 
             
                            try:
         | 
| 268 299 | 
             
                                with open(prj_file, 'r') as file:
         | 
| 269 300 | 
             
                                    content = file.read()
         | 
| 270 301 | 
             
                                    if "Proj Title=" in content:
         | 
| 271 | 
            -
                                        logging.info(f".prj file with 'Proj Title=' found: {prj_file}")
         | 
| 272 302 | 
             
                                        return prj_file.resolve()
         | 
| 273 | 
            -
                            except Exception | 
| 274 | 
            -
                                logging.warning(f"Failed to read .prj file {prj_file}: {e}")
         | 
| 303 | 
            +
                            except Exception:
         | 
| 275 304 | 
             
                                continue
         | 
| 276 | 
            -
                    logging.warning("No suitable .prj file found after all checks.")
         | 
| 277 305 | 
             
                    return None
         | 
| 278 306 |  | 
| 307 | 
            +
             | 
| 308 | 
            +
                @log_call
         | 
| 279 309 | 
             
                def get_project_name(self):
         | 
| 280 310 | 
             
                    """
         | 
| 281 311 | 
             
                    Get the name of the HEC-RAS project.
         | 
| @@ -289,6 +319,7 @@ class RasPrj: | |
| 289 319 | 
             
                    self.check_initialized()
         | 
| 290 320 | 
             
                    return self.project_name
         | 
| 291 321 |  | 
| 322 | 
            +
                @log_call
         | 
| 292 323 | 
             
                def get_prj_entries(self, entry_type):
         | 
| 293 324 | 
             
                    """
         | 
| 294 325 | 
             
                    Get entries of a specific type from the HEC-RAS project.
         | 
| @@ -305,6 +336,7 @@ class RasPrj: | |
| 305 336 | 
             
                    self.check_initialized()
         | 
| 306 337 | 
             
                    return self._get_prj_entries(entry_type)
         | 
| 307 338 |  | 
| 339 | 
            +
                @log_call
         | 
| 308 340 | 
             
                def get_plan_entries(self):
         | 
| 309 341 | 
             
                    """
         | 
| 310 342 | 
             
                    Get all plan entries from the HEC-RAS project.
         | 
| @@ -318,6 +350,7 @@ class RasPrj: | |
| 318 350 | 
             
                    self.check_initialized()
         | 
| 319 351 | 
             
                    return self._get_prj_entries('Plan')
         | 
| 320 352 |  | 
| 353 | 
            +
                @log_call
         | 
| 321 354 | 
             
                def get_flow_entries(self):
         | 
| 322 355 | 
             
                    """
         | 
| 323 356 | 
             
                    Get all flow entries from the HEC-RAS project.
         | 
| @@ -331,6 +364,7 @@ class RasPrj: | |
| 331 364 | 
             
                    self.check_initialized()
         | 
| 332 365 | 
             
                    return self._get_prj_entries('Flow')
         | 
| 333 366 |  | 
| 367 | 
            +
                @log_call
         | 
| 334 368 | 
             
                def get_unsteady_entries(self):
         | 
| 335 369 | 
             
                    """
         | 
| 336 370 | 
             
                    Get all unsteady flow entries from the HEC-RAS project.
         | 
| @@ -344,6 +378,7 @@ class RasPrj: | |
| 344 378 | 
             
                    self.check_initialized()
         | 
| 345 379 | 
             
                    return self._get_prj_entries('Unsteady')
         | 
| 346 380 |  | 
| 381 | 
            +
                @log_call
         | 
| 347 382 | 
             
                def get_geom_entries(self):
         | 
| 348 383 | 
             
                    """
         | 
| 349 384 | 
             
                    Get all geometry entries from the HEC-RAS project.
         | 
| @@ -357,6 +392,7 @@ class RasPrj: | |
| 357 392 | 
             
                    self.check_initialized()
         | 
| 358 393 | 
             
                    return self._get_prj_entries('Geom')
         | 
| 359 394 |  | 
| 395 | 
            +
                @log_call
         | 
| 360 396 | 
             
                def get_hdf_entries(self):
         | 
| 361 397 | 
             
                    """
         | 
| 362 398 | 
             
                    Get HDF entries for plans that have results.
         | 
| @@ -367,40 +403,38 @@ class RasPrj: | |
| 367 403 | 
             
                    """
         | 
| 368 404 | 
             
                    self.check_initialized()
         | 
| 369 405 |  | 
| 370 | 
            -
                    # Filter the plan_df to include only entries with existing HDF results
         | 
| 371 406 | 
             
                    hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
         | 
| 372 407 |  | 
| 373 | 
            -
                    # If no HDF entries are found, log the information
         | 
| 374 408 | 
             
                    if hdf_entries.empty:
         | 
| 375 | 
            -
                        logging.info("No HDF entries found.")
         | 
| 376 409 | 
             
                        return pd.DataFrame(columns=self.plan_df.columns)
         | 
| 377 410 |  | 
| 378 | 
            -
                    logging.info(f"Found {len(hdf_entries)} HDF entries.")
         | 
| 379 411 | 
             
                    return hdf_entries
         | 
| 380 412 |  | 
| 413 | 
            +
                
         | 
| 414 | 
            +
                @log_call
         | 
| 381 415 | 
             
                def print_data(self):
         | 
| 382 416 | 
             
                    """Print all RAS Object data for this instance."""
         | 
| 383 417 | 
             
                    self.check_initialized()
         | 
| 384 | 
            -
                     | 
| 385 | 
            -
                     | 
| 386 | 
            -
                     | 
| 387 | 
            -
                     | 
| 388 | 
            -
                     | 
| 389 | 
            -
                     | 
| 390 | 
            -
                     | 
| 391 | 
            -
                     | 
| 392 | 
            -
                     | 
| 393 | 
            -
                     | 
| 394 | 
            -
                     | 
| 395 | 
            -
                     | 
| 396 | 
            -
                     | 
| 397 | 
            -
                     | 
| 398 | 
            -
                     | 
| 399 | 
            -
                     | 
| 400 | 
            -
                     | 
| 401 | 
            -
             | 
| 418 | 
            +
                    logger.info(f"--- Data for {self.project_name} ---")
         | 
| 419 | 
            +
                    logger.info(f"Project folder: {self.project_folder}")
         | 
| 420 | 
            +
                    logger.info(f"PRJ file: {self.prj_file}")
         | 
| 421 | 
            +
                    logger.info(f"HEC-RAS executable: {self.ras_exe_path}")
         | 
| 422 | 
            +
                    logger.info("Plan files:")
         | 
| 423 | 
            +
                    logger.info(f"\n{self.plan_df}")
         | 
| 424 | 
            +
                    logger.info("Flow files:")
         | 
| 425 | 
            +
                    logger.info(f"\n{self.flow_df}")
         | 
| 426 | 
            +
                    logger.info("Unsteady flow files:")
         | 
| 427 | 
            +
                    logger.info(f"\n{self.unsteady_df}")
         | 
| 428 | 
            +
                    logger.info("Geometry files:")
         | 
| 429 | 
            +
                    logger.info(f"\n{self.geom_df}")
         | 
| 430 | 
            +
                    logger.info("HDF entries:")
         | 
| 431 | 
            +
                    logger.info(f"\n{self.get_hdf_entries()}")
         | 
| 432 | 
            +
                    logger.info("Boundary conditions:")
         | 
| 433 | 
            +
                    logger.info(f"\n{self.boundaries_df}")
         | 
| 434 | 
            +
                    logger.info("----------------------------")
         | 
| 402 435 |  | 
| 403 436 | 
             
                @staticmethod
         | 
| 437 | 
            +
                @log_call
         | 
| 404 438 | 
             
                def get_plan_value(
         | 
| 405 439 | 
             
                    plan_number_or_path: Union[str, Path],
         | 
| 406 440 | 
             
                    key: str,
         | 
| @@ -427,6 +461,7 @@ class RasPrj: | |
| 427 461 | 
             
                    >>> computation_interval = RasUtils.get_plan_value("01", "computation_interval")
         | 
| 428 462 | 
             
                    >>> print(f"Computation interval: {computation_interval}")
         | 
| 429 463 | 
             
                    """
         | 
| 464 | 
            +
                    logger = getLogger(__name__)
         | 
| 430 465 | 
             
                    ras_obj = ras_object or ras
         | 
| 431 466 | 
             
                    ras_obj.check_initialized()
         | 
| 432 467 |  | 
| @@ -439,19 +474,21 @@ class RasPrj: | |
| 439 474 | 
             
                    }
         | 
| 440 475 |  | 
| 441 476 | 
             
                    if key not in valid_keys:
         | 
| 477 | 
            +
                        logger.error(f"Invalid key: {key}")
         | 
| 442 478 | 
             
                        raise ValueError(f"Invalid key: {key}. Valid keys are: {', '.join(valid_keys)}")
         | 
| 443 479 |  | 
| 444 480 | 
             
                    plan_file_path = Path(plan_number_or_path)
         | 
| 445 481 | 
             
                    if not plan_file_path.is_file():
         | 
| 446 482 | 
             
                        plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
         | 
| 447 483 | 
             
                        if not plan_file_path.exists():
         | 
| 484 | 
            +
                            logger.error(f"Plan file not found: {plan_file_path}")
         | 
| 448 485 | 
             
                            raise ValueError(f"Plan file not found: {plan_file_path}")
         | 
| 449 486 |  | 
| 450 487 | 
             
                    try:
         | 
| 451 488 | 
             
                        with open(plan_file_path, 'r') as file:
         | 
| 452 489 | 
             
                            content = file.read()
         | 
| 453 490 | 
             
                    except IOError as e:
         | 
| 454 | 
            -
                         | 
| 491 | 
            +
                        logger.error(f"Error reading plan file {plan_file_path}: {e}")
         | 
| 455 492 | 
             
                        raise
         | 
| 456 493 |  | 
| 457 494 | 
             
                    if key == 'description':
         | 
| @@ -464,6 +501,7 @@ class RasPrj: | |
| 464 501 | 
             
                        match = re.search(pattern, content)
         | 
| 465 502 | 
             
                        return match.group(1).strip() if match else None
         | 
| 466 503 |  | 
| 504 | 
            +
                @log_call
         | 
| 467 505 | 
             
                def get_boundary_conditions(self) -> pd.DataFrame:
         | 
| 468 506 | 
             
                    """
         | 
| 469 507 | 
             
                    Extract boundary conditions from unsteady flow files and create a DataFrame.
         | 
| @@ -478,27 +516,39 @@ class RasPrj: | |
| 478 516 | 
             
                    is logged at the DEBUG level for each boundary condition. This feature is crucial
         | 
| 479 517 | 
             
                    for developers incorporating new boundary condition types or parameters, as it
         | 
| 480 518 | 
             
                    allows them to see what information might be missing from the current parsing logic.
         | 
| 519 | 
            +
                    If no unsteady flow files are present, it returns an empty DataFrame.
         | 
| 481 520 |  | 
| 482 521 | 
             
                    Returns:
         | 
| 483 522 | 
             
                        pd.DataFrame: A DataFrame containing detailed boundary condition information,
         | 
| 484 | 
            -
             | 
| 523 | 
            +
                                                  linked to the unsteady flow files.
         | 
| 485 524 |  | 
| 486 525 | 
             
                    Usage:
         | 
| 487 526 | 
             
                        To see the unparsed lines, set the logging level to DEBUG before calling this method:
         | 
| 488 527 |  | 
| 489 528 | 
             
                        import logging
         | 
| 490 | 
            -
                         | 
| 529 | 
            +
                        getLogger().setLevel(logging.DEBUG)
         | 
| 491 530 |  | 
| 492 531 | 
             
                        boundaries_df = ras_project.get_boundary_conditions()
         | 
| 532 | 
            +
                                      linked to the unsteady flow files. Returns an empty DataFrame if
         | 
| 533 | 
            +
                                      no unsteady flow files are present.
         | 
| 493 534 | 
             
                    """
         | 
| 494 535 | 
             
                    boundary_data = []
         | 
| 495 536 |  | 
| 537 | 
            +
                    # Check if unsteady_df is empty
         | 
| 538 | 
            +
                    if self.unsteady_df.empty:
         | 
| 539 | 
            +
                        logger.info("No unsteady flow files found in the project.")
         | 
| 540 | 
            +
                        return pd.DataFrame()  # Return an empty DataFrame
         | 
| 541 | 
            +
                    
         | 
| 496 542 | 
             
                    for _, row in self.unsteady_df.iterrows():
         | 
| 497 543 | 
             
                        unsteady_file_path = row['full_path']
         | 
| 498 544 | 
             
                        unsteady_number = row['unsteady_number']
         | 
| 499 545 |  | 
| 500 | 
            -
                         | 
| 501 | 
            -
                             | 
| 546 | 
            +
                        try:
         | 
| 547 | 
            +
                            with open(unsteady_file_path, 'r') as file:
         | 
| 548 | 
            +
                                content = file.read()
         | 
| 549 | 
            +
                        except IOError as e:
         | 
| 550 | 
            +
                            logger.error(f"Error reading unsteady file {unsteady_file_path}: {e}")
         | 
| 551 | 
            +
                            continue
         | 
| 502 552 |  | 
| 503 553 | 
             
                        bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
         | 
| 504 554 |  | 
| @@ -507,7 +557,11 @@ class RasPrj: | |
| 507 557 | 
             
                            boundary_data.append(bc_info)
         | 
| 508 558 |  | 
| 509 559 | 
             
                            if unparsed_lines:
         | 
| 510 | 
            -
                                 | 
| 560 | 
            +
                                logger.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
         | 
| 561 | 
            +
                    
         | 
| 562 | 
            +
                    if not boundary_data:
         | 
| 563 | 
            +
                        logger.info("No boundary conditions found in unsteady flow files.")
         | 
| 564 | 
            +
                        return pd.DataFrame()  # Return an empty DataFrame if no boundary conditions were found
         | 
| 511 565 |  | 
| 512 566 | 
             
                    boundaries_df = pd.DataFrame(boundary_data)
         | 
| 513 567 |  | 
| @@ -517,6 +571,7 @@ class RasPrj: | |
| 517 571 |  | 
| 518 572 | 
             
                    return merged_df
         | 
| 519 573 |  | 
| 574 | 
            +
                @log_call
         | 
| 520 575 | 
             
                def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
         | 
| 521 576 | 
             
                    lines = block.split('\n')
         | 
| 522 577 | 
             
                    bc_info = {
         | 
| @@ -588,21 +643,25 @@ class RasPrj: | |
| 588 643 | 
             
                    # Collect unparsed lines
         | 
| 589 644 | 
             
                    unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
         | 
| 590 645 |  | 
| 646 | 
            +
                    if unparsed_lines:
         | 
| 647 | 
            +
                        logger.debug(f"Unparsed lines for boundary condition {bc_number} in unsteady file {unsteady_number}:\n{unparsed_lines}")
         | 
| 648 | 
            +
                    
         | 
| 591 649 | 
             
                    return bc_info, unparsed_lines
         | 
| 592 650 |  | 
| 593 651 |  | 
| 594 652 | 
             
            # Create a global instance named 'ras'
         | 
| 653 | 
            +
            # Defining the global instance allows the init_ras_project function to initialize the project.
         | 
| 654 | 
            +
            # This only happens on the library initialization, not when the user calls init_ras_project.
         | 
| 595 655 | 
             
            ras = RasPrj()
         | 
| 596 656 |  | 
| 597 657 | 
             
            # END OF CLASS DEFINITION
         | 
| 598 658 |  | 
| 599 659 |  | 
| 600 | 
            -
             | 
| 601 | 
            -
             | 
| 602 660 | 
             
            # START OF FUNCTION DEFINITIONS
         | 
| 603 661 |  | 
| 604 662 |  | 
| 605 | 
            -
             | 
| 663 | 
            +
            @log_call
         | 
| 664 | 
            +
            def init_ras_project(ras_project_folder, ras_version=None, ras_instance=None):
         | 
| 606 665 | 
             
                """
         | 
| 607 666 | 
             
                Initialize a RAS project.
         | 
| 608 667 |  | 
| @@ -616,9 +675,13 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None): | |
| 616 675 | 
             
                -----------
         | 
| 617 676 | 
             
                ras_project_folder : str
         | 
| 618 677 | 
             
                    The path to the RAS project folder.
         | 
| 619 | 
            -
                ras_version : str
         | 
| 678 | 
            +
                ras_version : str, optional
         | 
| 620 679 | 
             
                    The version of RAS to use (e.g., "6.5").
         | 
| 621 680 | 
             
                    The version can also be a full path to the Ras.exe file. (Useful when calling ras objects for folder copies.)
         | 
| 681 | 
            +
                    If None, the function will attempt to use the version from the global 'ras' object or a default path.
         | 
| 682 | 
            +
                    You MUST specify a version number via init at some point or ras will not run.  
         | 
| 683 | 
            +
                    Once the ras_version is specified once it should auto-fill from the global 'ras' object.
         | 
| 684 | 
            +
                    The RAS Commander Library Assistant can ignore this argument since it does not have Ras.exe present, but all of other operations are fully working.
         | 
| 622 685 | 
             
                ras_instance : RasPrj, optional
         | 
| 623 686 | 
             
                    An instance of RasPrj to initialize. If None, the global 'ras' instance is used.
         | 
| 624 687 |  | 
| @@ -630,12 +693,12 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None): | |
| 630 693 | 
             
                Usage:
         | 
| 631 694 | 
             
                ------
         | 
| 632 695 | 
             
                1. For general use with a single project:
         | 
| 633 | 
            -
                    init_ras_project("/path/to/project" | 
| 696 | 
            +
                    init_ras_project("/path/to/project")
         | 
| 634 697 | 
             
                    # Use the global 'ras' object after initialization
         | 
| 635 698 |  | 
| 636 699 | 
             
                2. For managing multiple projects:
         | 
| 637 700 | 
             
                    project1 = init_ras_project("/path/to/project1", "6.5", ras_instance=RasPrj())
         | 
| 638 | 
            -
                    project2 = init_ras_project("/path/to/project2",  | 
| 701 | 
            +
                    project2 = init_ras_project("/path/to/project2", ras_instance=RasPrj())
         | 
| 639 702 |  | 
| 640 703 | 
             
                Notes:
         | 
| 641 704 | 
             
                ------
         | 
| @@ -644,51 +707,70 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None): | |
| 644 707 | 
             
                - Be consistent in your approach: stick to either the global 'ras' object
         | 
| 645 708 | 
             
                  or custom instances throughout your script or application.
         | 
| 646 709 | 
             
                - Document your choice of approach clearly in your code.
         | 
| 710 | 
            +
                - If ras_version is not provided, the function will attempt to use the version
         | 
| 711 | 
            +
                  from the global 'ras' object or a default path.
         | 
| 647 712 |  | 
| 648 713 | 
             
                Warnings:
         | 
| 649 714 | 
             
                ---------
         | 
| 650 715 | 
             
                Avoid mixing use of the global 'ras' object and custom instances to prevent
         | 
| 651 716 | 
             
                confusion and potential bugs.
         | 
| 652 717 | 
             
                """
         | 
| 653 | 
            -
                logging.info(f"Initializing project in folder: {ras_project_folder}")
         | 
| 654 | 
            -
                logging.info(f"Using ras_instance with id: {id(ras_instance)}")
         | 
| 655 | 
            -
                
         | 
| 656 | 
            -
             | 
| 657 | 
            -
             | 
| 658 718 | 
             
                if not Path(ras_project_folder).exists():
         | 
| 659 | 
            -
                     | 
| 719 | 
            +
                    logger.error(f"The specified RAS project folder does not exist: {ras_project_folder}")
         | 
| 660 720 | 
             
                    raise FileNotFoundError(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
         | 
| 661 721 |  | 
| 662 722 | 
             
                ras_exe_path = get_ras_exe(ras_version)
         | 
| 663 723 |  | 
| 664 724 | 
             
                if ras_instance is None:
         | 
| 665 | 
            -
                     | 
| 725 | 
            +
                    logger.info("Initializing global 'ras' object via init_ras_project function.")
         | 
| 666 726 | 
             
                    ras_instance = ras
         | 
| 667 727 | 
             
                elif not isinstance(ras_instance, RasPrj):
         | 
| 668 | 
            -
                     | 
| 728 | 
            +
                    logger.error("Provided ras_instance is not an instance of RasPrj.")
         | 
| 669 729 | 
             
                    raise TypeError("ras_instance must be an instance of RasPrj or None.")
         | 
| 670 730 |  | 
| 671 731 | 
             
                # Initialize the RasPrj instance
         | 
| 672 732 | 
             
                ras_instance.initialize(ras_project_folder, ras_exe_path)
         | 
| 673 733 |  | 
| 674 | 
            -
                 | 
| 734 | 
            +
                logger.info(f"Project initialized. ras_instance project folder: {ras_instance.project_folder}")
         | 
| 675 735 | 
             
                return ras_instance
         | 
| 676 736 |  | 
| 677 | 
            -
             | 
| 678 | 
            -
            def get_ras_exe(ras_version):
         | 
| 737 | 
            +
            @log_call
         | 
| 738 | 
            +
            def get_ras_exe(ras_version=None):
         | 
| 679 739 | 
             
                """
         | 
| 680 740 | 
             
                Determine the HEC-RAS executable path based on the input.
         | 
| 681 741 |  | 
| 682 742 | 
             
                Args:
         | 
| 683 | 
            -
                ras_version (str): Either a version number or a full path to the HEC-RAS executable.
         | 
| 743 | 
            +
                ras_version (str, optional): Either a version number or a full path to the HEC-RAS executable.
         | 
| 744 | 
            +
                                             If None, the function will attempt to use the version from the global 'ras' object
         | 
| 745 | 
            +
                                             or a default path.
         | 
| 684 746 |  | 
| 685 747 | 
             
                Returns:
         | 
| 686 748 | 
             
                str: The full path to the HEC-RAS executable.
         | 
| 687 749 |  | 
| 688 750 | 
             
                Raises:
         | 
| 689 751 | 
             
                ValueError: If the input is neither a valid version number nor a valid file path.
         | 
| 690 | 
            -
                 | 
| 752 | 
            +
                
         | 
| 753 | 
            +
                Notes:
         | 
| 754 | 
            +
                - If ras_version is not provided, the function will first check the global 'ras' object for a path.
         | 
| 755 | 
            +
                - If the global 'ras' object is not initialized or doesn't have a path, a default path will be used.
         | 
| 756 | 
            +
                - The default path allows the library to function in environments without HEC-RAS installed.
         | 
| 757 | 
            +
                - This enables the HEC-Commander GPT to operate without stopping, even if HEC-RAS is not present.
         | 
| 758 | 
            +
                - End users MUST use logging to check for operational errors, as full code stops prevent the GPT from running.
         | 
| 691 759 | 
             
                """
         | 
| 760 | 
            +
                # If ras_version is not provided, use the version of the global 'ras' object.
         | 
| 761 | 
            +
                # If the global 'ras' object is not initialized, use the default path: default_path = Path("/path/to/Ras.exe")
         | 
| 762 | 
            +
                # This default path allows the library to function in environments without HEC-RAS installed.
         | 
| 763 | 
            +
                # It enables our HEC-Commander GPT to operate without stopping, even though HEC-RAS is not present.
         | 
| 764 | 
            +
                # End users MUST use logging to check for operational errors, as full code stops prevent the GPT from running.
         | 
| 765 | 
            +
                if ras_version is None:
         | 
| 766 | 
            +
                    if hasattr(ras, 'ras_exe_path') and ras.ras_exe_path:
         | 
| 767 | 
            +
                        logger.info(f"Using HEC-RAS executable from global 'ras' object: {ras.ras_exe_path}")
         | 
| 768 | 
            +
                        return ras.ras_exe_path
         | 
| 769 | 
            +
                    else:
         | 
| 770 | 
            +
                        default_path = Path("Ras.exe")
         | 
| 771 | 
            +
                        logger.warning(f"No HEC-RAS version specified and global 'ras' object not initialized or missing ras_exe_path. Using default path: {default_path}. The RAS Commander (ras-commander) Library Assistant can ignore this error since it does not have HEC-RAS installed.")
         | 
| 772 | 
            +
                        return str(default_path)
         | 
| 773 | 
            +
                
         | 
| 692 774 | 
             
                ras_version_numbers = [
         | 
| 693 775 | 
             
                    "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
         | 
| 694 776 | 
             
                    "5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
         | 
| @@ -698,41 +780,30 @@ def get_ras_exe(ras_version): | |
| 698 780 | 
             
                hecras_path = Path(ras_version)
         | 
| 699 781 |  | 
| 700 782 | 
             
                if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
         | 
| 701 | 
            -
                     | 
| 783 | 
            +
                    logger.info(f"HEC-RAS executable found at specified path: {hecras_path}")
         | 
| 702 784 | 
             
                    return str(hecras_path)
         | 
| 703 785 |  | 
| 704 786 | 
             
                if ras_version in ras_version_numbers:
         | 
| 705 787 | 
             
                    default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
         | 
| 706 788 | 
             
                    if default_path.is_file():
         | 
| 707 | 
            -
                         | 
| 789 | 
            +
                        logger.info(f"HEC-RAS executable found at default path: {default_path}")
         | 
| 708 790 | 
             
                        return str(default_path)
         | 
| 709 791 | 
             
                    else:
         | 
| 710 | 
            -
                         | 
| 711 | 
            -
                        raise FileNotFoundError(f"HEC-RAS executable not found at the expected path: {default_path}")
         | 
| 792 | 
            +
                        logger.critical(f"HEC-RAS executable not found at the expected path: {default_path}")
         | 
| 712 793 |  | 
| 713 794 | 
             
                try:
         | 
| 714 795 | 
             
                    version_float = float(ras_version)
         | 
| 715 796 | 
             
                    if version_float > max(float(v) for v in ras_version_numbers):
         | 
| 716 797 | 
             
                        newer_version_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
         | 
| 717 798 | 
             
                        if newer_version_path.is_file():
         | 
| 718 | 
            -
                             | 
| 799 | 
            +
                            logger.info(f"Newer version of HEC-RAS executable found at: {newer_version_path}")
         | 
| 719 800 | 
             
                            return str(newer_version_path)
         | 
| 720 801 | 
             
                        else:
         | 
| 721 | 
            -
                             | 
| 722 | 
            -
                            raise FileNotFoundError(
         | 
| 723 | 
            -
                                f"Newer version of HEC-RAS was specified. Check the version number or pass the full Ras.exe path as the function argument instead of the version number. The script looked for the executable at: {newer_version_path}"
         | 
| 724 | 
            -
                            )
         | 
| 802 | 
            +
                            logger.critical("Newer version of HEC-RAS was specified, but the executable was not found.")
         | 
| 725 803 | 
             
                except ValueError:
         | 
| 726 804 | 
             
                    pass
         | 
| 727 805 |  | 
| 728 | 
            -
                 | 
| 729 | 
            -
             | 
| 730 | 
            -
             | 
| 731 | 
            -
             | 
| 732 | 
            -
                )
         | 
| 733 | 
            -
                raise ValueError(
         | 
| 734 | 
            -
                    f"Invalid HEC-RAS version or path: {ras_version}. "
         | 
| 735 | 
            -
                    f"Please provide a valid version number from {ras_version_numbers} "
         | 
| 736 | 
            -
                    "or a full path to the HEC-RAS executable."
         | 
| 737 | 
            -
                )
         | 
| 738 | 
            -
                
         | 
| 806 | 
            +
                logger.error(f"Invalid HEC-RAS version or path: {ras_version}, returning default path: {default_path}")
         | 
| 807 | 
            +
                #raise ValueError(f"Invalid HEC-RAS version or path: {ras_version}") # don't raise an error here, just return the default path
         | 
| 808 | 
            +
                return str(default_path)
         | 
| 809 | 
            +
                
         |