ras-commander 0.71.0__py3-none-any.whl → 0.73.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/RasGeo.py CHANGED
@@ -1,380 +1,538 @@
1
- """
2
- RasGeo - Operations for handling geometry files in HEC-RAS projects
3
-
4
- This module is part of the ras-commander library and uses a centralized logging configuration.
5
-
6
- Logging Configuration:
7
- - The logging is set up in the logging_config.py file.
8
- - A @log_call decorator is available to automatically log function calls.
9
- - Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
10
- - Logs are written to both console and a rotating file handler.
11
- - The default log file is 'ras_commander.log' in the 'logs' directory.
12
- - The default log level is INFO.
13
-
14
- To use logging in this module:
15
- 1. Use the @log_call decorator for automatic function call logging.
16
- 2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
17
- 3. Obtain the logger using: logger = logging.getLogger(__name__)
18
-
19
- Example:
20
- @log_call
21
- def my_function():
22
- logger = logging.getLogger(__name__)
23
- logger.debug("Additional debug information")
24
- # Function logic here
25
-
26
-
27
-
28
- -----
29
-
30
- All of the methods in this class are static and are designed to be used without instantiation.
31
-
32
- List of Functions in RasGeo:
33
- - clear_geompre_files()
34
-
35
-
36
- """
37
- import os
38
- from pathlib import Path
39
- from typing import List, Union
40
- import pandas as pd # Added pandas import
41
- from .RasPlan import RasPlan
42
- from .RasPrj import ras
43
- from .LoggingConfig import get_logger
44
- from .Decorators import log_call
45
-
46
- logger = get_logger(__name__)
47
-
48
- class RasGeo:
49
- """
50
- A class for operations on HEC-RAS geometry files.
51
- """
52
-
53
- @staticmethod
54
- @log_call
55
- def clear_geompre_files(
56
- plan_files: Union[str, Path, List[Union[str, Path]]] = None,
57
- ras_object = None
58
- ) -> None:
59
- """
60
- Clear HEC-RAS geometry preprocessor files for specified plan files.
61
-
62
- Geometry preprocessor files (.c* extension) contain computed hydraulic properties derived
63
- from the geometry. These should be cleared when the geometry changes to ensure that
64
- HEC-RAS recomputes all hydraulic tables with updated geometry information.
65
-
66
- Limitations/Future Work:
67
- - This function only deletes the geometry preprocessor file.
68
- - It does not clear the IB tables.
69
- - It also does not clear geometry preprocessor tables from the geometry HDF.
70
- - All of these features will need to be added to reliably remove geometry preprocessor
71
- files for 1D and 2D projects.
72
-
73
- Parameters:
74
- plan_files (Union[str, Path, List[Union[str, Path]]], optional):
75
- Full path(s) to the HEC-RAS plan file(s) (.p*).
76
- If None, clears all plan files in the project directory.
77
- ras_object: An optional RAS object instance.
78
-
79
- Returns:
80
- None: The function deletes files and updates the ras object's geometry dataframe
81
-
82
- Example:
83
- # Clone a plan and geometry
84
- new_plan_number = RasPlan.clone_plan("01")
85
- new_geom_number = RasPlan.clone_geom("01")
86
-
87
- # Set the new geometry for the cloned plan
88
- RasPlan.set_geom(new_plan_number, new_geom_number)
89
- plan_path = RasPlan.get_plan_path(new_plan_number)
90
-
91
- # Clear geometry preprocessor files to ensure clean results
92
- RasGeo.clear_geompre_files(plan_path)
93
- print(f"Cleared geometry preprocessor files for plan {new_plan_number}")
94
- """
95
- ras_obj = ras_object or ras
96
- ras_obj.check_initialized()
97
-
98
- def clear_single_file(plan_file: Union[str, Path], ras_obj) -> None:
99
- plan_path = Path(plan_file)
100
- geom_preprocessor_suffix = '.c' + ''.join(plan_path.suffixes[1:]) if plan_path.suffixes else '.c'
101
- geom_preprocessor_file = plan_path.with_suffix(geom_preprocessor_suffix)
102
- if geom_preprocessor_file.exists():
103
- try:
104
- geom_preprocessor_file.unlink()
105
- logger.info(f"Deleted geometry preprocessor file: {geom_preprocessor_file}")
106
- except PermissionError:
107
- logger.error(f"Permission denied: Unable to delete geometry preprocessor file: {geom_preprocessor_file}")
108
- raise PermissionError(f"Unable to delete geometry preprocessor file: {geom_preprocessor_file}. Permission denied.")
109
- except OSError as e:
110
- logger.error(f"Error deleting geometry preprocessor file: {geom_preprocessor_file}. {str(e)}")
111
- raise OSError(f"Error deleting geometry preprocessor file: {geom_preprocessor_file}. {str(e)}")
112
- else:
113
- logger.warning(f"No geometry preprocessor file found for: {plan_file}")
114
-
115
- if plan_files is None:
116
- logger.info("Clearing all geometry preprocessor files in the project directory.")
117
- plan_files_to_clear = list(ras_obj.project_folder.glob(r'*.p*'))
118
- elif isinstance(plan_files, (str, Path)):
119
- plan_files_to_clear = [plan_files]
120
- logger.info(f"Clearing geometry preprocessor file for single plan: {plan_files}")
121
- elif isinstance(plan_files, list):
122
- plan_files_to_clear = plan_files
123
- logger.info(f"Clearing geometry preprocessor files for multiple plans: {plan_files}")
124
- else:
125
- logger.error("Invalid input type for plan_files.")
126
- raise ValueError("Invalid input. Please provide a string, Path, list of paths, or None.")
127
-
128
- for plan_file in plan_files_to_clear:
129
- clear_single_file(plan_file, ras_obj)
130
-
131
- try:
132
- ras_obj.geom_df = ras_obj.get_geom_entries()
133
- logger.info("Geometry dataframe updated successfully.")
134
- except Exception as e:
135
- logger.error(f"Failed to update geometry dataframe: {str(e)}")
136
- raise
137
-
138
-
139
-
140
- @staticmethod
141
- def get_mannings_override_tables(geom_file_path, ras_object=None):
142
- """
143
- Extracts Manning's override region tables from a HEC-RAS geometry file.
144
-
145
- Args:
146
- geom_file_path (str or Path): Geometry file path or geometry number (e.g., "01").
147
- ras_object (RasPrj, optional): RAS project object for context. Defaults to global 'ras'.
148
-
149
- Returns:
150
- pd.DataFrame: DataFrame containing Manning's override region tables with columns:
151
- - Region Name: Name of the override region
152
- - Land Use Type: Land use type or description
153
- - Mannings N Value: Manning's n value for the land use type
154
- - Polygon Value: Polygon value or ID associated with the region
155
-
156
- Raises:
157
- FileNotFoundError: If the geometry file doesn't exist.
158
- ValueError: If the geometry file number is invalid.
159
- """
160
- # Get the full path to the geometry file if a number was provided
161
- if isinstance(geom_file_path, (str, int)) and not str(geom_file_path).endswith('.g'):
162
- ras_obj = ras_object or ras
163
- ras_obj.check_initialized()
164
- geom_file_path = RasPlan.get_geom_path(str(geom_file_path), ras_object=ras_obj)
165
- if geom_file_path is None:
166
- raise ValueError(f"Geometry file number '{geom_file_path}' not found in project")
167
-
168
- geom_file_path = Path(geom_file_path)
169
- if not geom_file_path.exists():
170
- raise FileNotFoundError(f"Geometry file not found: {geom_file_path}")
171
-
172
- # Lists for storing data
173
- region_names, land_use_types, mannings_values, polygon_values = [], [], [], []
174
-
175
- region_name, table_value, polygon_value = "", 0, ""
176
-
177
- with open(geom_file_path, 'r') as file:
178
- lines = file.readlines()
179
-
180
- i = 0 # Initialize line counter
181
- while i < len(lines):
182
- line = lines[i].strip()
183
-
184
- if "LCMann Region Name=" in line:
185
- region_name = line.split("=")[1]
186
- i += 1 # Move to the next line
187
- continue
188
-
189
- if "LCMann Region Table=" in line:
190
- table_value = int(line.split("=")[1])
191
- i += 1 # Skip to the next line which starts the table entries
192
- for j in range(table_value):
193
- if i+j < len(lines):
194
- # Handle multiple commas by splitting from the right
195
- parts = lines[i+j].strip().rsplit(",", 1)
196
- if len(parts) == 2:
197
- land_use, mannings = parts
198
- try:
199
- mannings_float = float(mannings)
200
- region_names.append(region_name)
201
- land_use_types.append(land_use)
202
- mannings_values.append(mannings_float)
203
- polygon_values.append(polygon_value) # This will repeat the last polygon_value
204
- except ValueError:
205
- # Skip if Manning's value is not a valid float
206
- pass
207
-
208
- i += table_value # Skip past the table entries
209
- continue
210
-
211
- if "LCMann Region Polygon=" in line:
212
- polygon_value = line.split("=")[1]
213
- i += 1 # Move to the next line
214
- continue
215
-
216
- i += 1 # Move to the next line if none of the conditions above are met
217
-
218
- # Create DataFrame
219
- mannings_tables = pd.DataFrame({
220
- "Region Name": region_names,
221
- "Land Use Type": land_use_types,
222
- "Mannings N Value": mannings_values,
223
- "Polygon Value": polygon_values
224
- })
225
-
226
- return mannings_tables
227
-
228
-
229
-
230
- @staticmethod
231
- @log_call
232
- def set_mannings_override_tables(geom_file_path, mannings_df, ras_object=None):
233
- """
234
- Updates Manning's override region tables in a HEC-RAS geometry file based on provided dataframe.
235
-
236
- This function takes a dataframe of Manning's values (similar to the one returned by
237
- extract_mannings_override_tables) and updates the corresponding values in the geometry file.
238
- If Region Name is specified in the dataframe, only updates that specific region.
239
- If no Region Name is given for a row, it updates all instances of the Land Use Type
240
- across all regions in the geometry file.
241
-
242
- Args:
243
- geom_file_path (str or Path): Geometry file path or geometry number (e.g., "01").
244
- mannings_df (pd.DataFrame): DataFrame containing Manning's override values with columns:
245
- - Land Use Type: Land use type or description (required)
246
- - Mannings N Value: Manning's n value for the land use type (required)
247
- - Region Name: Name of the override region (optional)
248
- ras_object (RasPrj, optional): RAS project object for context. Defaults to global 'ras'.
249
-
250
- Returns:
251
- bool: True if successful, False otherwise.
252
-
253
- Raises:
254
- FileNotFoundError: If the geometry file doesn't exist.
255
- ValueError: If the geometry file number is invalid or required columns are missing.
256
-
257
- Example:
258
- # Get existing Manning's tables
259
- mannings_tables = RasGeo.extract_mannings_override_tables("01")
260
-
261
- # Update specific values
262
- mannings_tables.loc[mannings_tables['Land Use Type'] == 'Open Water', 'Mannings N Value'] = 0.030
263
-
264
- # Update all forest types in all regions
265
- forest_updates = pd.DataFrame({
266
- 'Land Use Type': ['Mixed Forest', 'Deciduous Forest', 'Evergreen Forest'],
267
- 'Mannings N Value': [0.040, 0.042, 0.045]
268
- })
269
-
270
- # Apply the changes
271
- RasGeo.set_mannings_override_tables("01", mannings_tables)
272
- # Or apply just the forest updates to all regions
273
- RasGeo.set_mannings_override_tables("01", forest_updates)
274
- """
275
- # Get the full path to the geometry file if a number was provided
276
- if isinstance(geom_file_path, (str, int)) and not str(geom_file_path).endswith('.g'):
277
- ras_obj = ras_object or ras
278
- ras_obj.check_initialized()
279
- geom_file_path = RasPlan.get_geom_path(str(geom_file_path), ras_object=ras_obj)
280
- if geom_file_path is None:
281
- raise ValueError(f"Geometry file number '{geom_file_path}' not found in project")
282
-
283
- geom_file_path = Path(geom_file_path)
284
- if not geom_file_path.exists():
285
- raise FileNotFoundError(f"Geometry file not found: {geom_file_path}")
286
-
287
- # Verify required columns exist
288
- required_columns = ['Land Use Type', 'Mannings N Value']
289
- if not all(col in mannings_df.columns for col in required_columns):
290
- raise ValueError(f"DataFrame must contain columns: {required_columns}")
291
-
292
- # Create a dictionary for easier lookups
293
- update_dict = {}
294
- for _, row in mannings_df.iterrows():
295
- land_use = row['Land Use Type']
296
- manning_value = row['Mannings N Value']
297
- region_name = row.get('Region Name', None) # Optional column
298
-
299
- if region_name:
300
- if region_name not in update_dict:
301
- update_dict[region_name] = {}
302
- update_dict[region_name][land_use] = manning_value
303
- else:
304
- # Special key for updates that apply to all regions
305
- if 'ALL_REGIONS' not in update_dict:
306
- update_dict['ALL_REGIONS'] = {}
307
- update_dict['ALL_REGIONS'][land_use] = manning_value
308
-
309
- logger.info(f"Updating Manning's n values in geometry file: {geom_file_path}")
310
-
311
- # Read the entire file
312
- with open(geom_file_path, 'r') as file:
313
- lines = file.readlines()
314
-
315
- # Process the file line by line
316
- modified_lines = []
317
- current_region = None
318
- in_table = False
319
- table_start_index = -1
320
- table_size = 0
321
-
322
- i = 0
323
- while i < len(lines):
324
- line = lines[i]
325
- modified_lines.append(line) # Add line by default, may modify later
326
-
327
- if "LCMann Region Name=" in line:
328
- current_region = line.split("=")[1].strip()
329
- in_table = False
330
-
331
- elif "LCMann Region Table=" in line:
332
- in_table = True
333
- table_start_index = len(modified_lines)
334
- try:
335
- table_size = int(line.split("=")[1].strip())
336
- except ValueError:
337
- logger.warning(f"Invalid table size at line: {line}")
338
- table_size = 0
339
-
340
- elif in_table and table_size > 0:
341
- # We're inside a Manning's table
342
- land_use_entry = line.strip()
343
- if "," in land_use_entry:
344
- parts = land_use_entry.rsplit(",", 1)
345
- if len(parts) == 2:
346
- land_use, _ = parts
347
-
348
- # Check if we should update this entry
349
- update_value = None
350
-
351
- # First check region-specific updates
352
- if current_region in update_dict and land_use in update_dict[current_region]:
353
- update_value = update_dict[current_region][land_use]
354
-
355
- # Then check global updates (ALL_REGIONS)
356
- elif 'ALL_REGIONS' in update_dict and land_use in update_dict['ALL_REGIONS']:
357
- update_value = update_dict['ALL_REGIONS'][land_use]
358
-
359
- if update_value is not None:
360
- # Replace the last entry in modified_lines with updated Manning's value
361
- modified_lines[-1] = f"{land_use},{update_value}\n"
362
- logger.debug(f"Updated '{land_use}' in region '{current_region}' to {update_value}")
363
-
364
- # Decrement counter for table entries
365
- table_size -= 1
366
- if table_size == 0:
367
- in_table = False
368
-
369
- i += 1
370
-
371
- # Write the file back
372
- with open(geom_file_path, 'w') as file:
373
- file.writelines(modified_lines)
374
-
375
- logger.info(f"Successfully updated Manning's n values in geometry file: {geom_file_path}")
376
- return True
377
-
378
-
379
-
380
-
1
+ """
2
+ RasGeo - Operations for handling geometry files in HEC-RAS projects
3
+
4
+ This module is part of the ras-commander library and uses a centralized logging configuration.
5
+
6
+ Logging Configuration:
7
+ - The logging is set up in the logging_config.py file.
8
+ - A @log_call decorator is available to automatically log function calls.
9
+ - Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
10
+ - Logs are written to both console and a rotating file handler.
11
+ - The default log file is 'ras_commander.log' in the 'logs' directory.
12
+ - The default log level is INFO.
13
+
14
+ To use logging in this module:
15
+ 1. Use the @log_call decorator for automatic function call logging.
16
+ 2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
17
+ 3. Obtain the logger using: logger = logging.getLogger(__name__)
18
+
19
+ Example:
20
+ @log_call
21
+ def my_function():
22
+ logger = logging.getLogger(__name__)
23
+ logger.debug("Additional debug information")
24
+ # Function logic here
25
+
26
+
27
+ All of the methods in this class are static and are designed to be used without instantiation.
28
+
29
+ List of Functions in RasGeo:
30
+ - clear_geompre_files(): Clears geometry preprocessor files for specified plan files
31
+ - get_mannings_baseoverrides(): Reads base Manning's n table from a geometry file
32
+ - get_mannings_regionoverrides(): Reads Manning's n region overrides from a geometry file
33
+ - set_mannings_baseoverrides(): Writes base Manning's n values to a geometry file
34
+ - set_mannings_regionoverrides(): Writes regional Manning's n overrides to a geometry file
35
+ """
36
+ import os
37
+ from pathlib import Path
38
+ from typing import List, Union
39
+ import pandas as pd # Added pandas import
40
+ from .RasPlan import RasPlan
41
+ from .RasPrj import ras
42
+ from .LoggingConfig import get_logger
43
+ from .Decorators import log_call
44
+
45
+ logger = get_logger(__name__)
46
+
47
+ class RasGeo:
48
+ """
49
+ A class for operations on HEC-RAS geometry files.
50
+ """
51
+
52
+ @staticmethod
53
+ @log_call
54
+ def clear_geompre_files(
55
+ plan_files: Union[str, Path, List[Union[str, Path]]] = None,
56
+ ras_object = None
57
+ ) -> None:
58
+ """
59
+ Clear HEC-RAS geometry preprocessor files for specified plan files.
60
+
61
+ Geometry preprocessor files (.c* extension) contain computed hydraulic properties derived
62
+ from the geometry. These should be cleared when the geometry changes to ensure that
63
+ HEC-RAS recomputes all hydraulic tables with updated geometry information.
64
+
65
+ Limitations/Future Work:
66
+ - This function only deletes the geometry preprocessor file.
67
+ - It does not clear the IB tables.
68
+ - It also does not clear geometry preprocessor tables from the geometry HDF.
69
+ - All of these features will need to be added to reliably remove geometry preprocessor
70
+ files for 1D and 2D projects.
71
+
72
+ Parameters:
73
+ plan_files (Union[str, Path, List[Union[str, Path]]], optional):
74
+ Full path(s) to the HEC-RAS plan file(s) (.p*).
75
+ If None, clears all plan files in the project directory.
76
+ ras_object: An optional RAS object instance.
77
+
78
+ Returns:
79
+ None: The function deletes files and updates the ras object's geometry dataframe
80
+
81
+ Example:
82
+ # Clone a plan and geometry
83
+ new_plan_number = RasPlan.clone_plan("01")
84
+ new_geom_number = RasPlan.clone_geom("01")
85
+
86
+ # Set the new geometry for the cloned plan
87
+ RasPlan.set_geom(new_plan_number, new_geom_number)
88
+ plan_path = RasPlan.get_plan_path(new_plan_number)
89
+
90
+ # Clear geometry preprocessor files to ensure clean results
91
+ RasGeo.clear_geompre_files(plan_path)
92
+ print(f"Cleared geometry preprocessor files for plan {new_plan_number}")
93
+ """
94
+ ras_obj = ras_object or ras
95
+ ras_obj.check_initialized()
96
+
97
+ def clear_single_file(plan_file: Union[str, Path], ras_obj) -> None:
98
+ plan_path = Path(plan_file)
99
+ geom_preprocessor_suffix = '.c' + ''.join(plan_path.suffixes[1:]) if plan_path.suffixes else '.c'
100
+ geom_preprocessor_file = plan_path.with_suffix(geom_preprocessor_suffix)
101
+ if geom_preprocessor_file.exists():
102
+ try:
103
+ geom_preprocessor_file.unlink()
104
+ logger.info(f"Deleted geometry preprocessor file: {geom_preprocessor_file}")
105
+ except PermissionError:
106
+ logger.error(f"Permission denied: Unable to delete geometry preprocessor file: {geom_preprocessor_file}")
107
+ raise PermissionError(f"Unable to delete geometry preprocessor file: {geom_preprocessor_file}. Permission denied.")
108
+ except OSError as e:
109
+ logger.error(f"Error deleting geometry preprocessor file: {geom_preprocessor_file}. {str(e)}")
110
+ raise OSError(f"Error deleting geometry preprocessor file: {geom_preprocessor_file}. {str(e)}")
111
+ else:
112
+ logger.warning(f"No geometry preprocessor file found for: {plan_file}")
113
+
114
+ if plan_files is None:
115
+ logger.info("Clearing all geometry preprocessor files in the project directory.")
116
+ plan_files_to_clear = list(ras_obj.project_folder.glob(r'*.p*'))
117
+ elif isinstance(plan_files, (str, Path)):
118
+ plan_files_to_clear = [plan_files]
119
+ logger.info(f"Clearing geometry preprocessor file for single plan: {plan_files}")
120
+ elif isinstance(plan_files, list):
121
+ plan_files_to_clear = plan_files
122
+ logger.info(f"Clearing geometry preprocessor files for multiple plans: {plan_files}")
123
+ else:
124
+ logger.error("Invalid input type for plan_files.")
125
+ raise ValueError("Invalid input. Please provide a string, Path, list of paths, or None.")
126
+
127
+ for plan_file in plan_files_to_clear:
128
+ clear_single_file(plan_file, ras_obj)
129
+
130
+ try:
131
+ ras_obj.geom_df = ras_obj.get_geom_entries()
132
+ logger.info("Geometry dataframe updated successfully.")
133
+ except Exception as e:
134
+ logger.error(f"Failed to update geometry dataframe: {str(e)}")
135
+ raise
136
+
137
+ @staticmethod
138
+ @log_call
139
+ def get_mannings_baseoverrides(geom_file_path):
140
+ """
141
+ Reads the base Manning's n table from a HEC-RAS geometry file.
142
+
143
+ Parameters:
144
+ -----------
145
+ geom_file_path : str or Path
146
+ Path to the geometry file (.g##)
147
+
148
+ Returns:
149
+ --------
150
+ pandas.DataFrame
151
+ DataFrame with Table Number, Land Cover Name, and Base Manning's n Value
152
+
153
+ Example:
154
+ --------
155
+ >>> geom_path = RasPlan.get_geom_path("01")
156
+ >>> mannings_df = RasGeo.get_mannings_baseoverrides(geom_path)
157
+ >>> print(mannings_df)
158
+ """
159
+ import pandas as pd
160
+ from pathlib import Path
161
+
162
+ # Convert to Path object if it's a string
163
+ if isinstance(geom_file_path, str):
164
+ geom_file_path = Path(geom_file_path)
165
+
166
+ base_table_rows = []
167
+ table_number = None
168
+
169
+ # Read the geometry file
170
+ with open(geom_file_path, 'r') as f:
171
+ lines = f.readlines()
172
+
173
+ # Parse the file
174
+ reading_base_table = False
175
+ for line in lines:
176
+ line = line.strip()
177
+
178
+ # Find the table number
179
+ if line.startswith('LCMann Table='):
180
+ table_number = line.split('=')[1]
181
+ reading_base_table = True
182
+ continue
183
+
184
+ # Stop reading when we hit a line without a comma or starting with LCMann
185
+ if reading_base_table and (not ',' in line or line.startswith('LCMann')):
186
+ reading_base_table = False
187
+ continue
188
+
189
+ # Parse data rows in base table
190
+ if reading_base_table and ',' in line:
191
+ # Check if there are multiple commas in the line
192
+ parts = line.split(',')
193
+ if len(parts) > 2:
194
+ # Handle case where land cover name contains commas
195
+ name = ','.join(parts[:-1])
196
+ value = parts[-1]
197
+ else:
198
+ name, value = parts
199
+
200
+ try:
201
+ base_table_rows.append([table_number, name, float(value)])
202
+ except ValueError:
203
+ # Log the error and continue
204
+ print(f"Error parsing line: {line}")
205
+ continue
206
+
207
+ # Create DataFrame
208
+ if base_table_rows:
209
+ df = pd.DataFrame(base_table_rows, columns=['Table Number', 'Land Cover Name', 'Base Manning\'s n Value'])
210
+ return df
211
+ else:
212
+ return pd.DataFrame(columns=['Table Number', 'Land Cover Name', 'Base Manning\'s n Value'])
213
+
214
+
215
+ @staticmethod
216
+ @log_call
217
+ def get_mannings_regionoverrides(geom_file_path):
218
+ """
219
+ Reads the Manning's n region overrides from a HEC-RAS geometry file.
220
+
221
+ Parameters:
222
+ -----------
223
+ geom_file_path : str or Path
224
+ Path to the geometry file (.g##)
225
+
226
+ Returns:
227
+ --------
228
+ pandas.DataFrame
229
+ DataFrame with Table Number, Land Cover Name, MainChannel value, and Region Name
230
+
231
+ Example:
232
+ --------
233
+ >>> geom_path = RasPlan.get_geom_path("01")
234
+ >>> region_overrides_df = RasGeo.get_mannings_regionoverrides(geom_path)
235
+ >>> print(region_overrides_df)
236
+ """
237
+ import pandas as pd
238
+ from pathlib import Path
239
+
240
+ # Convert to Path object if it's a string
241
+ if isinstance(geom_file_path, str):
242
+ geom_file_path = Path(geom_file_path)
243
+
244
+ region_rows = []
245
+ current_region = None
246
+ current_table = None
247
+
248
+ # Read the geometry file
249
+ with open(geom_file_path, 'r') as f:
250
+ lines = f.readlines()
251
+
252
+ # Parse the file
253
+ reading_region_table = False
254
+ for line in lines:
255
+ line = line.strip()
256
+
257
+ # Find region name
258
+ if line.startswith('LCMann Region Name='):
259
+ current_region = line.split('=')[1]
260
+ continue
261
+
262
+ # Find region table number
263
+ if line.startswith('LCMann Region Table='):
264
+ current_table = line.split('=')[1]
265
+ reading_region_table = True
266
+ continue
267
+
268
+ # Stop reading when we hit a line without a comma or starting with LCMann
269
+ if reading_region_table and (not ',' in line or line.startswith('LCMann')):
270
+ reading_region_table = False
271
+ continue
272
+
273
+ # Parse data rows in region table
274
+ if reading_region_table and ',' in line and current_region is not None:
275
+ # Check if there are multiple commas in the line
276
+ parts = line.split(',')
277
+ if len(parts) > 2:
278
+ # Handle case where land cover name contains commas
279
+ name = ','.join(parts[:-1])
280
+ value = parts[-1]
281
+ else:
282
+ name, value = parts
283
+
284
+ try:
285
+ region_rows.append([current_table, name, float(value), current_region])
286
+ except ValueError:
287
+ # Log the error and continue
288
+ print(f"Error parsing line: {line}")
289
+ continue
290
+
291
+ # Create DataFrame
292
+ if region_rows:
293
+ return pd.DataFrame(region_rows, columns=['Table Number', 'Land Cover Name', 'MainChannel', 'Region Name'])
294
+ else:
295
+ return pd.DataFrame(columns=['Table Number', 'Land Cover Name', 'MainChannel', 'Region Name'])
296
+
297
+
298
+
299
+ @staticmethod
300
+ @log_call
301
+ def set_mannings_baseoverrides(geom_file_path, mannings_data):
302
+ """
303
+ Writes base Manning's n values to a HEC-RAS geometry file.
304
+
305
+ Parameters:
306
+ -----------
307
+ geom_file_path : str or Path
308
+ Path to the geometry file (.g##)
309
+ mannings_data : DataFrame
310
+ DataFrame with columns 'Table Number', 'Land Cover Name', and 'Base Manning\'s n Value'
311
+
312
+ Returns:
313
+ --------
314
+ bool
315
+ True if successful
316
+ """
317
+ from pathlib import Path
318
+ import shutil
319
+ import pandas as pd
320
+ import datetime
321
+
322
+ # Convert to Path object if it's a string
323
+ if isinstance(geom_file_path, str):
324
+ geom_file_path = Path(geom_file_path)
325
+
326
+ # Create backup
327
+ backup_path = geom_file_path.with_suffix(geom_file_path.suffix + '.bak')
328
+ shutil.copy2(geom_file_path, backup_path)
329
+
330
+ # Read the entire file
331
+ with open(geom_file_path, 'r') as f:
332
+ lines = f.readlines()
333
+
334
+ # Find the Manning's table section
335
+ table_number = str(mannings_data['Table Number'].iloc[0])
336
+ start_idx = None
337
+ end_idx = None
338
+
339
+ for i, line in enumerate(lines):
340
+ if line.strip() == f"LCMann Table={table_number}":
341
+ start_idx = i
342
+ # Find the end of this table (next LCMann directive or end of file)
343
+ for j in range(i+1, len(lines)):
344
+ if lines[j].strip().startswith('LCMann'):
345
+ end_idx = j
346
+ break
347
+ if end_idx is None: # If we reached the end of the file
348
+ end_idx = len(lines)
349
+ break
350
+
351
+ if start_idx is None:
352
+ raise ValueError(f"Manning's table {table_number} not found in the geometry file")
353
+
354
+ # Extract existing land cover names from the file
355
+ existing_landcover = []
356
+ for i in range(start_idx+1, end_idx):
357
+ line = lines[i].strip()
358
+ if ',' in line:
359
+ parts = line.split(',')
360
+ if len(parts) > 2:
361
+ # Handle case where land cover name contains commas
362
+ name = ','.join(parts[:-1])
363
+ else:
364
+ name = parts[0]
365
+ existing_landcover.append(name)
366
+
367
+ # Check if all land cover names in the dataframe match the file
368
+ df_landcover = mannings_data['Land Cover Name'].tolist()
369
+ if set(df_landcover) != set(existing_landcover):
370
+ missing = set(existing_landcover) - set(df_landcover)
371
+ extra = set(df_landcover) - set(existing_landcover)
372
+ error_msg = "Land cover names don't match between file and dataframe.\n"
373
+ if missing:
374
+ error_msg += f"Missing in dataframe: {missing}\n"
375
+ if extra:
376
+ error_msg += f"Extra in dataframe: {extra}"
377
+ raise ValueError(error_msg)
378
+
379
+ # Create new content for the table
380
+ new_content = [f"LCMann Table={table_number}\n"]
381
+
382
+ # Add base table entries
383
+ for _, row in mannings_data.iterrows():
384
+ new_content.append(f"{row['Land Cover Name']},{row['Base Manning\'s n Value']}\n")
385
+
386
+ # Replace the section in the original file
387
+ updated_lines = lines[:start_idx] + new_content + lines[end_idx:]
388
+
389
+ # Update the time stamp
390
+ current_time = datetime.datetime.now().strftime("%b/%d/%Y %H:%M:%S")
391
+ for i, line in enumerate(updated_lines):
392
+ if line.strip().startswith("LCMann Time="):
393
+ updated_lines[i] = f"LCMann Time={current_time}\n"
394
+ break
395
+
396
+ # Write the updated file
397
+ with open(geom_file_path, 'w') as f:
398
+ f.writelines(updated_lines)
399
+
400
+ return True
401
+
402
+
403
+
404
+
405
+
406
+
407
+
408
+ @staticmethod
409
+ @log_call
410
+ def set_mannings_regionoverrides(geom_file_path, mannings_data):
411
+ """
412
+ Writes regional Manning's n overrides to a HEC-RAS geometry file.
413
+
414
+ Parameters:
415
+ -----------
416
+ geom_file_path : str or Path
417
+ Path to the geometry file (.g##)
418
+ mannings_data : DataFrame
419
+ DataFrame with columns 'Table Number', 'Land Cover Name', 'MainChannel', and 'Region Name'
420
+
421
+ Returns:
422
+ --------
423
+ bool
424
+ True if successful
425
+ """
426
+ from pathlib import Path
427
+ import shutil
428
+ import pandas as pd
429
+ import datetime
430
+
431
+ # Convert to Path object if it's a string
432
+ if isinstance(geom_file_path, str):
433
+ geom_file_path = Path(geom_file_path)
434
+
435
+ # Create backup
436
+ backup_path = geom_file_path.with_suffix(geom_file_path.suffix + '.bak')
437
+ shutil.copy2(geom_file_path, backup_path)
438
+
439
+ # Read the entire file
440
+ with open(geom_file_path, 'r') as f:
441
+ lines = f.readlines()
442
+
443
+ # Group data by region
444
+ regions = mannings_data.groupby('Region Name')
445
+
446
+ # Find the Manning's region sections
447
+ for region_name, region_data in regions:
448
+ table_number = str(region_data['Table Number'].iloc[0])
449
+
450
+ # Find the region section
451
+ region_start_idx = None
452
+ region_table_idx = None
453
+ region_end_idx = None
454
+ region_polygon_line = None
455
+
456
+ for i, line in enumerate(lines):
457
+ if line.strip() == f"LCMann Region Name={region_name}":
458
+ region_start_idx = i
459
+
460
+ if region_start_idx is not None and line.strip() == f"LCMann Region Table={table_number}":
461
+ region_table_idx = i
462
+
463
+ # Find the end of this region (next LCMann Region or end of file)
464
+ for j in range(i+1, len(lines)):
465
+ if lines[j].strip().startswith('LCMann Region Name=') or lines[j].strip().startswith('LCMann Region Polygon='):
466
+ if lines[j].strip().startswith('LCMann Region Polygon='):
467
+ region_polygon_line = lines[j]
468
+ region_end_idx = j
469
+ break
470
+ if region_end_idx is None: # If we reached the end of the file
471
+ region_end_idx = len(lines)
472
+ break
473
+
474
+ if region_start_idx is None or region_table_idx is None:
475
+ raise ValueError(f"Region {region_name} with table {table_number} not found in the geometry file")
476
+
477
+ # Extract existing land cover names from the file
478
+ existing_landcover = []
479
+ for i in range(region_table_idx+1, region_end_idx):
480
+ line = lines[i].strip()
481
+ if ',' in line and not line.startswith('LCMann'):
482
+ parts = line.split(',')
483
+ if len(parts) > 2:
484
+ # Handle case where land cover name contains commas
485
+ name = ','.join(parts[:-1])
486
+ else:
487
+ name = parts[0]
488
+ existing_landcover.append(name)
489
+
490
+ # Check if all land cover names in the dataframe match the file
491
+ df_landcover = region_data['Land Cover Name'].tolist()
492
+ if set(df_landcover) != set(existing_landcover):
493
+ missing = set(existing_landcover) - set(df_landcover)
494
+ extra = set(df_landcover) - set(existing_landcover)
495
+ error_msg = f"Land cover names for region {region_name} don't match between file and dataframe.\n"
496
+ if missing:
497
+ error_msg += f"Missing in dataframe: {missing}\n"
498
+ if extra:
499
+ error_msg += f"Extra in dataframe: {extra}"
500
+ raise ValueError(error_msg)
501
+
502
+ # Create new content for the region
503
+ new_content = [
504
+ f"LCMann Region Name={region_name}\n",
505
+ f"LCMann Region Table={table_number}\n"
506
+ ]
507
+
508
+ # Add region table entries
509
+ for _, row in region_data.iterrows():
510
+ new_content.append(f"{row['Land Cover Name']},{row['MainChannel']}\n")
511
+
512
+ # Add the region polygon line if it exists
513
+ if region_polygon_line:
514
+ new_content.append(region_polygon_line)
515
+
516
+ # Replace the section in the original file
517
+ if region_polygon_line:
518
+ # If we have a polygon line, include it in the replacement
519
+ updated_lines = lines[:region_start_idx] + new_content + lines[region_end_idx+1:]
520
+ else:
521
+ # If no polygon line, just replace up to the end index
522
+ updated_lines = lines[:region_start_idx] + new_content + lines[region_end_idx:]
523
+
524
+ # Update the lines for the next region
525
+ lines = updated_lines
526
+
527
+ # Update the time stamp
528
+ current_time = datetime.datetime.now().strftime("%b/%d/%Y %H:%M:%S")
529
+ for i, line in enumerate(lines):
530
+ if line.strip().startswith("LCMann Region Time="):
531
+ lines[i] = f"LCMann Region Time={current_time}\n"
532
+ break
533
+
534
+ # Write the updated file
535
+ with open(geom_file_path, 'w') as f:
536
+ f.writelines(lines)
537
+
538
+ return True