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/HdfInfiltration.py +1530 -416
- ras_commander/HdfResultsMesh.py +245 -0
- ras_commander/RasGeo.py +538 -380
- ras_commander/RasMap.py +252 -0
- ras_commander/RasPrj.py +19 -1
- ras_commander/__init__.py +3 -2
- {ras_commander-0.71.0.dist-info → ras_commander-0.73.0.dist-info}/METADATA +22 -3
- {ras_commander-0.71.0.dist-info → ras_commander-0.73.0.dist-info}/RECORD +11 -10
- {ras_commander-0.71.0.dist-info → ras_commander-0.73.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.71.0.dist-info → ras_commander-0.73.0.dist-info}/licenses/LICENSE +0 -0
- {ras_commander-0.71.0.dist-info → ras_commander-0.73.0.dist-info}/top_level.txt +0 -0
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
import
|
38
|
-
from
|
39
|
-
|
40
|
-
|
41
|
-
from .
|
42
|
-
from .
|
43
|
-
from .
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
@
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
-
|
68
|
-
- It does not clear the
|
69
|
-
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
RasPlan.
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
ras_obj
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
geom_preprocessor_file
|
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
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
geom_file_path =
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
""
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
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
|