ras-commander 0.72.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,524 +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
- 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
- @log_call
138
- def get_mannings_baseoverrides(geom_file_path):
139
- """
140
- Reads the base Manning's n table from a HEC-RAS geometry file.
141
-
142
- Parameters:
143
- -----------
144
- geom_file_path : str or Path
145
- Path to the geometry file (.g##)
146
-
147
- Returns:
148
- --------
149
- pandas.DataFrame
150
- DataFrame with Table Number, Land Cover Name, and Base Manning's n Value
151
- """
152
- import pandas as pd
153
- from pathlib import Path
154
-
155
- # Convert to Path object if it's a string
156
- if isinstance(geom_file_path, str):
157
- geom_file_path = Path(geom_file_path)
158
-
159
- base_table_rows = []
160
- table_number = None
161
-
162
- # Read the geometry file
163
- with open(geom_file_path, 'r') as f:
164
- lines = f.readlines()
165
-
166
- # Parse the file
167
- reading_base_table = False
168
- for line in lines:
169
- line = line.strip()
170
-
171
- # Find the table number
172
- if line.startswith('LCMann Table='):
173
- table_number = line.split('=')[1]
174
- reading_base_table = True
175
- continue
176
-
177
- # Stop reading when we hit a line without a comma or starting with LCMann
178
- if reading_base_table and (not ',' in line or line.startswith('LCMann')):
179
- reading_base_table = False
180
- continue
181
-
182
- # Parse data rows in base table
183
- if reading_base_table and ',' in line:
184
- # Check if there are multiple commas in the line
185
- parts = line.split(',')
186
- if len(parts) > 2:
187
- # Handle case where land cover name contains commas
188
- name = ','.join(parts[:-1])
189
- value = parts[-1]
190
- else:
191
- name, value = parts
192
-
193
- try:
194
- base_table_rows.append([table_number, name, float(value)])
195
- except ValueError:
196
- # Log the error and continue
197
- print(f"Error parsing line: {line}")
198
- continue
199
-
200
- # Create DataFrame
201
- if base_table_rows:
202
- df = pd.DataFrame(base_table_rows, columns=['Table Number', 'Land Cover Name', 'Base Manning\'s n Value'])
203
- return df
204
- else:
205
- return pd.DataFrame(columns=['Table Number', 'Land Cover Name', 'Base Manning\'s n Value'])
206
-
207
-
208
- @log_call
209
- def get_mannings_regionoverrides(geom_file_path):
210
- """
211
- Reads the Manning's n region overrides from a HEC-RAS geometry file.
212
-
213
- Parameters:
214
- -----------
215
- geom_file_path : str or Path
216
- Path to the geometry file (.g##)
217
-
218
- Returns:
219
- --------
220
- pandas.DataFrame
221
- DataFrame with Table Number, Land Cover Name, MainChannel value, and region name
222
- """
223
- import pandas as pd
224
- from pathlib import Path
225
-
226
- # Convert to Path object if it's a string
227
- if isinstance(geom_file_path, str):
228
- geom_file_path = Path(geom_file_path)
229
-
230
- region_rows = []
231
- current_region = None
232
- current_table = None
233
-
234
- # Read the geometry file
235
- with open(geom_file_path, 'r') as f:
236
- lines = f.readlines()
237
-
238
- # Parse the file
239
- reading_region_table = False
240
- for line in lines:
241
- line = line.strip()
242
-
243
- # Find region name
244
- if line.startswith('LCMann Region Name='):
245
- current_region = line.split('=')[1]
246
- continue
247
-
248
- # Find region table number
249
- if line.startswith('LCMann Region Table='):
250
- current_table = line.split('=')[1]
251
- reading_region_table = True
252
- continue
253
-
254
- # Stop reading when we hit a line without a comma or starting with LCMann
255
- if reading_region_table and (not ',' in line or line.startswith('LCMann')):
256
- reading_region_table = False
257
- continue
258
-
259
- # Parse data rows in region table
260
- if reading_region_table and ',' in line and current_region is not None:
261
- # Check if there are multiple commas in the line
262
- parts = line.split(',')
263
- if len(parts) > 2:
264
- # Handle case where land cover name contains commas
265
- name = ','.join(parts[:-1])
266
- value = parts[-1]
267
- else:
268
- name, value = parts
269
-
270
- try:
271
- region_rows.append([current_table, name, float(value), current_region])
272
- except ValueError:
273
- # Log the error and continue
274
- print(f"Error parsing line: {line}")
275
- continue
276
-
277
- # Create DataFrame
278
- if region_rows:
279
- return pd.DataFrame(region_rows, columns=['Table Number', 'Land Cover Name', 'MainChannel', 'Region Name'])
280
- else:
281
- return pd.DataFrame(columns=['Table Number', 'Land Cover Name', 'MainChannel', 'Region Name'])
282
-
283
-
284
-
285
- @staticmethod
286
- @log_call
287
- def set_mannings_baseoverrides(geom_file_path, mannings_data):
288
- """
289
- Writes base Manning's n values to a HEC-RAS geometry file.
290
-
291
- Parameters:
292
- -----------
293
- geom_file_path : str or Path
294
- Path to the geometry file (.g##)
295
- mannings_data : DataFrame
296
- DataFrame with columns 'Table Number', 'Land Cover Name', and 'Base Manning\'s n Value'
297
-
298
- Returns:
299
- --------
300
- bool
301
- True if successful
302
- """
303
- from pathlib import Path
304
- import shutil
305
- import pandas as pd
306
- import datetime
307
-
308
- # Convert to Path object if it's a string
309
- if isinstance(geom_file_path, str):
310
- geom_file_path = Path(geom_file_path)
311
-
312
- # Create backup
313
- backup_path = geom_file_path.with_suffix(geom_file_path.suffix + '.bak')
314
- shutil.copy2(geom_file_path, backup_path)
315
-
316
- # Read the entire file
317
- with open(geom_file_path, 'r') as f:
318
- lines = f.readlines()
319
-
320
- # Find the Manning's table section
321
- table_number = str(mannings_data['Table Number'].iloc[0])
322
- start_idx = None
323
- end_idx = None
324
-
325
- for i, line in enumerate(lines):
326
- if line.strip() == f"LCMann Table={table_number}":
327
- start_idx = i
328
- # Find the end of this table (next LCMann directive or end of file)
329
- for j in range(i+1, len(lines)):
330
- if lines[j].strip().startswith('LCMann'):
331
- end_idx = j
332
- break
333
- if end_idx is None: # If we reached the end of the file
334
- end_idx = len(lines)
335
- break
336
-
337
- if start_idx is None:
338
- raise ValueError(f"Manning's table {table_number} not found in the geometry file")
339
-
340
- # Extract existing land cover names from the file
341
- existing_landcover = []
342
- for i in range(start_idx+1, end_idx):
343
- line = lines[i].strip()
344
- if ',' in line:
345
- parts = line.split(',')
346
- if len(parts) > 2:
347
- # Handle case where land cover name contains commas
348
- name = ','.join(parts[:-1])
349
- else:
350
- name = parts[0]
351
- existing_landcover.append(name)
352
-
353
- # Check if all land cover names in the dataframe match the file
354
- df_landcover = mannings_data['Land Cover Name'].tolist()
355
- if set(df_landcover) != set(existing_landcover):
356
- missing = set(existing_landcover) - set(df_landcover)
357
- extra = set(df_landcover) - set(existing_landcover)
358
- error_msg = "Land cover names don't match between file and dataframe.\n"
359
- if missing:
360
- error_msg += f"Missing in dataframe: {missing}\n"
361
- if extra:
362
- error_msg += f"Extra in dataframe: {extra}"
363
- raise ValueError(error_msg)
364
-
365
- # Create new content for the table
366
- new_content = [f"LCMann Table={table_number}\n"]
367
-
368
- # Add base table entries
369
- for _, row in mannings_data.iterrows():
370
- new_content.append(f"{row['Land Cover Name']},{row['Base Manning\'s n Value']}\n")
371
-
372
- # Replace the section in the original file
373
- updated_lines = lines[:start_idx] + new_content + lines[end_idx:]
374
-
375
- # Update the time stamp
376
- current_time = datetime.datetime.now().strftime("%b/%d/%Y %H:%M:%S")
377
- for i, line in enumerate(updated_lines):
378
- if line.strip().startswith("LCMann Time="):
379
- updated_lines[i] = f"LCMann Time={current_time}\n"
380
- break
381
-
382
- # Write the updated file
383
- with open(geom_file_path, 'w') as f:
384
- f.writelines(updated_lines)
385
-
386
- return True
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
- @staticmethod
395
- @log_call
396
- def set_mannings_regionoverrides(geom_file_path, mannings_data):
397
- """
398
- Writes regional Manning's n overrides to a HEC-RAS geometry file.
399
-
400
- Parameters:
401
- -----------
402
- geom_file_path : str or Path
403
- Path to the geometry file (.g##)
404
- mannings_data : DataFrame
405
- DataFrame with columns 'Table Number', 'Land Cover Name', 'MainChannel', and 'Region Name'
406
-
407
- Returns:
408
- --------
409
- bool
410
- True if successful
411
- """
412
- from pathlib import Path
413
- import shutil
414
- import pandas as pd
415
- import datetime
416
-
417
- # Convert to Path object if it's a string
418
- if isinstance(geom_file_path, str):
419
- geom_file_path = Path(geom_file_path)
420
-
421
- # Create backup
422
- backup_path = geom_file_path.with_suffix(geom_file_path.suffix + '.bak')
423
- shutil.copy2(geom_file_path, backup_path)
424
-
425
- # Read the entire file
426
- with open(geom_file_path, 'r') as f:
427
- lines = f.readlines()
428
-
429
- # Group data by region
430
- regions = mannings_data.groupby('Region Name')
431
-
432
- # Find the Manning's region sections
433
- for region_name, region_data in regions:
434
- table_number = str(region_data['Table Number'].iloc[0])
435
-
436
- # Find the region section
437
- region_start_idx = None
438
- region_table_idx = None
439
- region_end_idx = None
440
- region_polygon_line = None
441
-
442
- for i, line in enumerate(lines):
443
- if line.strip() == f"LCMann Region Name={region_name}":
444
- region_start_idx = i
445
-
446
- if region_start_idx is not None and line.strip() == f"LCMann Region Table={table_number}":
447
- region_table_idx = i
448
-
449
- # Find the end of this region (next LCMann Region or end of file)
450
- for j in range(i+1, len(lines)):
451
- if lines[j].strip().startswith('LCMann Region Name=') or lines[j].strip().startswith('LCMann Region Polygon='):
452
- if lines[j].strip().startswith('LCMann Region Polygon='):
453
- region_polygon_line = lines[j]
454
- region_end_idx = j
455
- break
456
- if region_end_idx is None: # If we reached the end of the file
457
- region_end_idx = len(lines)
458
- break
459
-
460
- if region_start_idx is None or region_table_idx is None:
461
- raise ValueError(f"Region {region_name} with table {table_number} not found in the geometry file")
462
-
463
- # Extract existing land cover names from the file
464
- existing_landcover = []
465
- for i in range(region_table_idx+1, region_end_idx):
466
- line = lines[i].strip()
467
- if ',' in line and not line.startswith('LCMann'):
468
- parts = line.split(',')
469
- if len(parts) > 2:
470
- # Handle case where land cover name contains commas
471
- name = ','.join(parts[:-1])
472
- else:
473
- name = parts[0]
474
- existing_landcover.append(name)
475
-
476
- # Check if all land cover names in the dataframe match the file
477
- df_landcover = region_data['Land Cover Name'].tolist()
478
- if set(df_landcover) != set(existing_landcover):
479
- missing = set(existing_landcover) - set(df_landcover)
480
- extra = set(df_landcover) - set(existing_landcover)
481
- error_msg = f"Land cover names for region {region_name} don't match between file and dataframe.\n"
482
- if missing:
483
- error_msg += f"Missing in dataframe: {missing}\n"
484
- if extra:
485
- error_msg += f"Extra in dataframe: {extra}"
486
- raise ValueError(error_msg)
487
-
488
- # Create new content for the region
489
- new_content = [
490
- f"LCMann Region Name={region_name}\n",
491
- f"LCMann Region Table={table_number}\n"
492
- ]
493
-
494
- # Add region table entries
495
- for _, row in region_data.iterrows():
496
- new_content.append(f"{row['Land Cover Name']},{row['MainChannel']}\n")
497
-
498
- # Add the region polygon line if it exists
499
- if region_polygon_line:
500
- new_content.append(region_polygon_line)
501
-
502
- # Replace the section in the original file
503
- if region_polygon_line:
504
- # If we have a polygon line, include it in the replacement
505
- updated_lines = lines[:region_start_idx] + new_content + lines[region_end_idx+1:]
506
- else:
507
- # If no polygon line, just replace up to the end index
508
- updated_lines = lines[:region_start_idx] + new_content + lines[region_end_idx:]
509
-
510
- # Update the lines for the next region
511
- lines = updated_lines
512
-
513
- # Update the time stamp
514
- current_time = datetime.datetime.now().strftime("%b/%d/%Y %H:%M:%S")
515
- for i, line in enumerate(lines):
516
- if line.strip().startswith("LCMann Region Time="):
517
- lines[i] = f"LCMann Region Time={current_time}\n"
518
- break
519
-
520
- # Write the updated file
521
- with open(geom_file_path, 'w') as f:
522
- f.writelines(lines)
523
-
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
+
524
538
  return True