ras-commander 0.70.0__py3-none-any.whl → 0.71.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ras_commander/RasCmdr.py CHANGED
@@ -99,23 +99,66 @@ class RasCmdr:
99
99
  overwrite_dest=False
100
100
  ):
101
101
  """
102
- Execute a HEC-RAS plan.
102
+ Execute a single HEC-RAS plan in a specified location.
103
+
104
+ This function runs a HEC-RAS plan by launching the HEC-RAS executable through command line,
105
+ allowing for destination folder specification, core count control, and geometry preprocessor management.
103
106
 
104
107
  Args:
105
108
  plan_number (str, Path): The plan number to execute (e.g., "01", "02") or the full path to the plan file.
109
+ Recommended to use two-digit strings for plan numbers for consistency (e.g., "01" instead of 1).
106
110
  dest_folder (str, Path, optional): Name of the folder or full path for computation.
107
111
  If a string is provided, it will be created in the same parent directory as the project folder.
108
112
  If a full path is provided, it will be used as is.
113
+ If None, computation occurs in the original project folder, modifying the original project.
109
114
  ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
115
+ Useful when working with multiple projects simultaneously.
110
116
  clear_geompre (bool, optional): Whether to clear geometry preprocessor files. Defaults to False.
111
- num_cores (int, optional): Number of cores to use for the plan execution. If None, the current setting is not changed.
117
+ Set to True when geometry has been modified to force recomputation of preprocessor files.
118
+ num_cores (int, optional): Number of cores to use for the plan execution.
119
+ If None, the current setting in the plan file is not changed.
120
+ Generally, 2-4 cores provides good performance for most models.
112
121
  overwrite_dest (bool, optional): If True, overwrite the destination folder if it exists. Defaults to False.
122
+ Set to True to replace an existing destination folder with the same name.
113
123
 
114
124
  Returns:
115
125
  bool: True if the execution was successful, False otherwise.
116
126
 
117
127
  Raises:
118
128
  ValueError: If the specified dest_folder already exists and is not empty, and overwrite_dest is False.
129
+ FileNotFoundError: If the plan file or project file cannot be found.
130
+ PermissionError: If there are issues accessing or writing to the destination folder.
131
+ subprocess.CalledProcessError: If the HEC-RAS execution fails.
132
+
133
+ Examples:
134
+ # Run a plan in the original project folder
135
+ RasCmdr.compute_plan("01")
136
+
137
+ # Run a plan in a separate folder
138
+ RasCmdr.compute_plan("01", dest_folder="computation_folder")
139
+
140
+ # Run a plan with a specific number of cores
141
+ RasCmdr.compute_plan("01", num_cores=4)
142
+
143
+ # Run a plan in a specific folder, overwriting if it exists
144
+ RasCmdr.compute_plan("01", dest_folder="computation_folder", overwrite_dest=True)
145
+
146
+ # Run a plan in a specific folder with multiple options
147
+ RasCmdr.compute_plan(
148
+ "01",
149
+ dest_folder="computation_folder",
150
+ num_cores=2,
151
+ clear_geompre=True,
152
+ overwrite_dest=True
153
+ )
154
+
155
+ Notes:
156
+ - For executing multiple plans, consider using compute_parallel() or compute_test_mode().
157
+ - Setting num_cores appropriately is important for performance:
158
+ * 1-2 cores: Highest efficiency per core, good for small models
159
+ * 3-8 cores: Good balance for most models
160
+ * >8 cores: May have diminishing returns due to overhead
161
+ - This function updates the RAS object's dataframes (plan_df, geom_df, etc.) after execution.
119
162
  """
120
163
  try:
121
164
  ras_obj = ras_object if ras_object is not None else ras
@@ -202,8 +245,6 @@ class RasCmdr:
202
245
 
203
246
 
204
247
 
205
- @staticmethod
206
- @log_call
207
248
  @staticmethod
208
249
  @log_call
209
250
  def compute_parallel(
@@ -216,19 +257,83 @@ class RasCmdr:
216
257
  overwrite_dest: bool = False
217
258
  ) -> Dict[str, bool]:
218
259
  """
219
- Compute multiple HEC-RAS plans in parallel.
260
+ Execute multiple HEC-RAS plans in parallel using multiple worker instances.
261
+
262
+ This method creates separate worker folders for each parallel process, runs plans
263
+ in those folders, and then consolidates results to a final destination folder.
264
+ It's ideal for running independent plans simultaneously to make better use of system resources.
220
265
 
221
266
  Args:
222
- plan_number (Union[str, List[str], None]): Plan number(s) to compute. If None, all plans are computed.
223
- max_workers (int): Maximum number of parallel workers.
267
+ plan_number (Union[str, List[str], None]): Plan number(s) to compute.
268
+ If None, all plans in the project are computed.
269
+ If string, only that plan will be computed.
270
+ If list, all specified plans will be computed.
271
+ Recommended to use two-digit strings for plan numbers for consistency (e.g., "01" instead of 1).
272
+ max_workers (int): Maximum number of parallel workers (separate HEC-RAS instances).
273
+ Each worker gets a separate folder with a copy of the project.
274
+ Optimal value depends on CPU cores and memory available.
275
+ A good starting point is: max_workers = floor(physical_cores / num_cores).
224
276
  num_cores (int): Number of cores to use per plan computation.
225
- clear_geompre (bool): Whether to clear geometry preprocessor files.
226
- ras_object (Optional[RasPrj]): RAS project object. If None, uses global instance.
277
+ Controls computational resources allocated to each individual HEC-RAS instance.
278
+ For parallel execution, 2-4 cores per worker often provides the best balance.
279
+ clear_geompre (bool): Whether to clear geometry preprocessor files before computation.
280
+ Set to True when geometry has been modified to force recomputation.
281
+ ras_object (Optional[RasPrj]): RAS project object. If None, uses global 'ras' instance.
282
+ Useful when working with multiple projects simultaneously.
227
283
  dest_folder (Union[str, Path, None]): Destination folder for computed results.
284
+ If None, creates a "[Computed]" folder adjacent to the project folder.
285
+ If string, creates folder in the project's parent directory.
286
+ If Path, uses the exact path provided.
228
287
  overwrite_dest (bool): Whether to overwrite existing destination folder.
288
+ Set to True to replace an existing destination folder with the same name.
229
289
 
230
290
  Returns:
231
291
  Dict[str, bool]: Dictionary of plan numbers and their execution success status.
292
+ Keys are plan numbers and values are boolean success indicators.
293
+
294
+ Raises:
295
+ ValueError: If the destination folder already exists, is not empty, and overwrite_dest is False.
296
+ FileNotFoundError: If project files cannot be found.
297
+ PermissionError: If there are issues accessing or writing to folders.
298
+ RuntimeError: If worker initialization fails.
299
+
300
+ Examples:
301
+ # Run all plans in parallel with default settings
302
+ RasCmdr.compute_parallel()
303
+
304
+ # Run all plans with 4 workers, 2 cores per worker
305
+ RasCmdr.compute_parallel(max_workers=4, num_cores=2)
306
+
307
+ # Run specific plans in parallel
308
+ RasCmdr.compute_parallel(plan_number=["01", "03"], max_workers=2)
309
+
310
+ # Run all plans with dynamic worker allocation based on system resources
311
+ import psutil
312
+ physical_cores = psutil.cpu_count(logical=False)
313
+ cores_per_worker = 2
314
+ max_workers = max(1, physical_cores // cores_per_worker)
315
+ RasCmdr.compute_parallel(max_workers=max_workers, num_cores=cores_per_worker)
316
+
317
+ # Run all plans in a specific destination folder
318
+ RasCmdr.compute_parallel(dest_folder="parallel_results", overwrite_dest=True)
319
+
320
+ Notes:
321
+ - Worker Assignment: Plans are assigned to workers in a round-robin fashion.
322
+ For example, with 3 workers and 5 plans, assignment would be:
323
+ Worker 1: Plans 1 & 4, Worker 2: Plans 2 & 5, Worker 3: Plan 3.
324
+
325
+ - Resource Management: Each HEC-RAS instance (worker) typically requires:
326
+ * 2-4 GB of RAM
327
+ * 2-4 cores for optimal performance
328
+
329
+ - When to use parallel vs. sequential:
330
+ * Parallel: For independent plans, faster overall completion
331
+ * Sequential: For dependent plans, consistent resource usage, easier debugging
332
+
333
+ - The function creates worker folders during execution and consolidates results
334
+ to the destination folder upon completion.
335
+
336
+ - This function updates the RAS object's dataframes (plan_df, geom_df, etc.) after execution.
232
337
  """
233
338
  try:
234
339
  ras_obj = ras_object or ras
@@ -251,11 +356,17 @@ class RasCmdr:
251
356
  logger.info(f"Copied project folder to destination: {dest_folder_path}")
252
357
  project_folder = dest_folder_path
253
358
 
359
+ # Store filtered plan numbers separately to ensure only these are executed
360
+ filtered_plan_numbers = []
361
+
254
362
  if plan_number:
255
363
  if isinstance(plan_number, str):
256
364
  plan_number = [plan_number]
257
365
  ras_obj.plan_df = ras_obj.plan_df[ras_obj.plan_df['plan_number'].isin(plan_number)]
258
- logger.info(f"Filtered plans to execute: {plan_number}")
366
+ filtered_plan_numbers = list(ras_obj.plan_df['plan_number'])
367
+ logger.info(f"Filtered plans to execute: {filtered_plan_numbers}")
368
+ else:
369
+ filtered_plan_numbers = list(ras_obj.plan_df['plan_number'])
259
370
 
260
371
  num_plans = len(ras_obj.plan_df)
261
372
  max_workers = min(max_workers, num_plans) if num_plans > 0 else 1
@@ -282,8 +393,9 @@ class RasCmdr:
282
393
  logger.critical(f"Failed to initialize RAS project for worker {worker_id}: {str(e)}")
283
394
  worker_ras_objects[worker_id] = None
284
395
 
396
+ # Explicitly use the filtered plan numbers for assignments
285
397
  worker_cycle = cycle(range(1, max_workers + 1))
286
- plan_assignments = [(next(worker_cycle), plan_num) for plan_num in ras_obj.plan_df['plan_number']]
398
+ plan_assignments = [(next(worker_cycle), plan_num) for plan_num in filtered_plan_numbers]
287
399
 
288
400
  execution_results: Dict[str, bool] = {}
289
401
 
@@ -397,49 +509,86 @@ class RasCmdr:
397
509
  overwrite_dest=False
398
510
  ):
399
511
  """
400
- Execute HEC-RAS plans in test mode. This is a re-creation of the HEC-RAS command line -test flag,
401
- which does not work in recent versions of HEC-RAS.
402
-
403
- As a special-purpose function that emulates the original -test flag, it operates differently than the
404
- other two compute_ functions. Per the original HEC-RAS test flag, it creates a separate test folder,
405
- copies the project there, and executes the specified plans in sequential order.
406
-
407
- For most purposes, just copying the project folder, initing that new folder, then running each plan
408
- with compute_plan is a simpler and more flexible approach. This is shown in the examples provided
409
- in the ras-commander library.
512
+ Execute HEC-RAS plans sequentially in a separate test folder.
513
+
514
+ This function creates a separate test folder, copies the project there, and executes
515
+ the specified plans in sequential order. It's useful for batch processing plans that
516
+ need to be run in a specific order or when you want to ensure consistent resource usage.
410
517
 
411
518
  Args:
412
519
  plan_number (str, list[str], optional): Plan number or list of plan numbers to execute.
413
520
  If None, all plans will be executed. Default is None.
414
- dest_folder_suffix (str, optional): Suffix to append to the test folder name to create dest_folder.
521
+ Recommended to use two-digit strings for plan numbers for consistency (e.g., "01" instead of 1).
522
+ dest_folder_suffix (str, optional): Suffix to append to the test folder name.
415
523
  Defaults to "[Test]".
416
- dest_folder is always created in the project folder's parent directory.
524
+ The test folder is always created in the project folder's parent directory.
417
525
  clear_geompre (bool, optional): Whether to clear geometry preprocessor files.
418
526
  Defaults to False.
419
- num_cores (int, optional): Maximum number of cores to use for each plan.
420
- If None, the current setting is not changed. Default is None.
527
+ Set to True when geometry has been modified to force recomputation.
528
+ num_cores (int, optional): Number of cores to use for each plan.
529
+ If None, the current setting in the plan file is not changed. Default is None.
530
+ For sequential execution, 4-8 cores often provides good performance.
421
531
  ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
422
- overwrite_dest (bool, optional): If True, overwrite the destination folder if it exists. Defaults to False.
532
+ Useful when working with multiple projects simultaneously.
533
+ overwrite_dest (bool, optional): If True, overwrite the destination folder if it exists.
534
+ Defaults to False.
535
+ Set to True to replace an existing test folder with the same name.
423
536
 
424
537
  Returns:
425
538
  Dict[str, bool]: Dictionary of plan numbers and their execution success status.
539
+ Keys are plan numbers and values are boolean success indicators.
540
+
541
+ Raises:
542
+ ValueError: If the destination folder already exists, is not empty, and overwrite_dest is False.
543
+ FileNotFoundError: If project files cannot be found.
544
+ PermissionError: If there are issues accessing or writing to folders.
426
545
 
427
- Example:
428
- Run all plans: RasCommander.compute_test_mode()
429
- Run a specific plan: RasCommander.compute_test_mode(plan_number="01")
430
- Run multiple plans: RasCommander.compute_test_mode(plan_number=["01", "03", "05"])
431
- Run plans with a custom folder suffix: RasCommander.compute_test_mode(dest_folder_suffix="[TestRun]")
432
- Run plans and clear geometry preprocessor files: RasCommander.compute_test_mode(clear_geompre=True)
433
- Run plans with a specific number of cores: RasCommander.compute_test_mode(num_cores=4)
546
+ Examples:
547
+ # Run all plans sequentially
548
+ RasCmdr.compute_test_mode()
549
+
550
+ # Run a specific plan
551
+ RasCmdr.compute_test_mode(plan_number="01")
552
+
553
+ # Run multiple specific plans
554
+ RasCmdr.compute_test_mode(plan_number=["01", "03", "05"])
555
+
556
+ # Run plans with a custom folder suffix
557
+ RasCmdr.compute_test_mode(dest_folder_suffix="[SequentialRun]")
558
+
559
+ # Run plans with a specific number of cores
560
+ RasCmdr.compute_test_mode(num_cores=4)
434
561
 
562
+ # Run specific plans with multiple options
563
+ RasCmdr.compute_test_mode(
564
+ plan_number=["01", "02"],
565
+ dest_folder_suffix="[SpecificSequential]",
566
+ clear_geompre=True,
567
+ num_cores=6,
568
+ overwrite_dest=True
569
+ )
570
+
435
571
  Notes:
436
- - This function executes plans in a separate folder for isolated testing.
437
- - If plan_number is not provided, all plans in the project will be executed.
438
- - The function does not change the geometry preprocessor and IB tables settings.
439
- - To force recomputing of geometry preprocessor and IB tables, use the clear_geompre=True option.
440
- - Plans are executed sequentially.
441
- - Because copying the project is implicit, only a dest_folder_suffix option is provided.
442
- - For more flexible run management, use the compute_parallel or compute_sequential functions.
572
+ - This function was created to replicate the original HEC-RAS command line -test flag,
573
+ which does not work in recent versions of HEC-RAS.
574
+
575
+ - Key differences from other compute functions:
576
+ * compute_plan: Runs a single plan, with option for destination folder
577
+ * compute_parallel: Runs multiple plans simultaneously in worker folders
578
+ * compute_test_mode: Runs multiple plans sequentially in a single test folder
579
+
580
+ - Use cases:
581
+ * Running plans in a specific order
582
+ * Ensuring consistent resource usage
583
+ * Easier debugging (one plan at a time)
584
+ * Isolated test environment
585
+
586
+ - Performance considerations:
587
+ * Sequential execution is generally slower overall than parallel execution
588
+ * Each plan gets consistent resource usage
589
+ * Execution time scales linearly with the number of plans
590
+
591
+ - This function updates the RAS object's dataframes (plan_df, geom_df, etc.) after execution.
443
592
  """
444
593
  try:
445
594
  ras_obj = ras_object or ras
ras_commander/RasGeo.py CHANGED
@@ -37,6 +37,7 @@ List of Functions in RasGeo:
37
37
  import os
38
38
  from pathlib import Path
39
39
  from typing import List, Union
40
+ import pandas as pd # Added pandas import
40
41
  from .RasPlan import RasPlan
41
42
  from .RasPrj import ras
42
43
  from .LoggingConfig import get_logger
@@ -56,13 +57,18 @@ class RasGeo:
56
57
  ras_object = None
57
58
  ) -> None:
58
59
  """
59
- Clear HEC-RAS geometry preprocessor files for specified plan files or all plan files in the project directory.
60
-
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
+
61
66
  Limitations/Future Work:
62
67
  - This function only deletes the geometry preprocessor file.
63
68
  - It does not clear the IB tables.
64
69
  - It also does not clear geometry preprocessor tables from the geometry HDF.
65
- - All of these features will need to be added to reliably remove geometry preprocessor files for 1D and 2D projects.
70
+ - All of these features will need to be added to reliably remove geometry preprocessor
71
+ files for 1D and 2D projects.
66
72
 
67
73
  Parameters:
68
74
  plan_files (Union[str, Path, List[Union[str, Path]]], optional):
@@ -71,20 +77,20 @@ class RasGeo:
71
77
  ras_object: An optional RAS object instance.
72
78
 
73
79
  Returns:
74
- None
75
-
76
- Examples:
77
- # Clear all geometry preprocessor files in the project directory
78
- RasGeo.clear_geompre_files()
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")
79
86
 
80
- # Clear a single plan file
81
- RasGeo.clear_geompre_files(r'path/to/plan.p01')
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)
82
90
 
83
- # Clear multiple plan files
84
- RasGeo.clear_geompre_files([r'path/to/plan1.p01', r'path/to/plan2.p02'])
85
-
86
- Note:
87
- This function updates the ras object's geometry dataframe after clearing the preprocessor files.
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}")
88
94
  """
89
95
  ras_obj = ras_object or ras
90
96
  ras_obj.check_initialized()
@@ -131,8 +137,244 @@ class RasGeo:
131
137
 
132
138
 
133
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
134
227
 
135
228
 
136
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
+
137
379
 
138
380