ras-commander 0.1.6__py2.py3-none-any.whl → 0.20.dev0__py2.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.
@@ -0,0 +1,1216 @@
1
+ """
2
+ Operations for modifying and updating HEC-RAS plan files.
3
+
4
+ """
5
+ import re
6
+ from pathlib import Path
7
+ import shutil
8
+ from typing import Union, Optional
9
+ import pandas as pd
10
+ from .RasPrj import RasPrj, ras
11
+ from .RasUtils import RasUtils
12
+
13
+ class RasPlan:
14
+ """
15
+ A class for operations on HEC-RAS plan files.
16
+ """
17
+
18
+ @staticmethod
19
+ def set_geom(self, new_geom: Union[str, int], ras_object=None) -> pd.DataFrame:
20
+ """
21
+ Set the geometry for the current plan.
22
+
23
+ Parameters:
24
+ new_geom (Union[str, int]): The new geometry number to set.
25
+ ras_object: An optional RAS object instance.
26
+
27
+ Returns:
28
+ pd.DataFrame: The updated geometry DataFrame.
29
+
30
+ Example:
31
+ plan = RasPlan('path/to/plan.p01')
32
+ updated_geom_df = plan.set_geom('G01')
33
+
34
+ Note:
35
+ This function updates the ras object's dataframes after modifying the project structure.
36
+ """
37
+ ras_obj = ras_object or ras
38
+ ras_obj.check_initialized()
39
+
40
+ geom_df = ras_obj.geom_df
41
+
42
+ if new_geom not in geom_df['geom_number']:
43
+ raise ValueError(f"Geometry {new_geom} not found in project.")
44
+
45
+ self.geom = new_geom
46
+ geom_df.loc[geom_df['plan_number'] == self.plan_number, 'geom_number'] = new_geom
47
+
48
+ print(f"Geometry for plan {self.plan_number} set to {new_geom}")
49
+ print("Updated geometry DataFrame:")
50
+ display(geom_df)
51
+
52
+ ras_obj.plan_df = ras_obj.get_plan_entries()
53
+ ras_obj.geom_df = ras_obj.get_geom_entries()
54
+ ras_obj.flow_df = ras_obj.get_flow_entries()
55
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
56
+
57
+ return geom_df
58
+
59
+ @staticmethod
60
+ def set_steady(plan_number: str, new_steady_flow_number: str, ras_object=None):
61
+ """
62
+ Apply a steady flow file to a plan file.
63
+
64
+ Parameters:
65
+ plan_number (str): Plan number (e.g., '02')
66
+ new_steady_flow_number (str): Steady flow number to apply (e.g., '01')
67
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
68
+
69
+ Returns:
70
+ None
71
+
72
+ Raises:
73
+ ValueError: If the specified steady flow number is not found in the project file
74
+ FileNotFoundError: If the specified plan file is not found
75
+
76
+ Example:
77
+ >>> RasPlan.set_steady('02', '01')
78
+
79
+ Note:
80
+ This function updates the ras object's dataframes after modifying the project structure.
81
+ """
82
+ logging.info(f"Setting steady flow file to {new_steady_flow_number} in Plan {plan_number}")
83
+ ras_obj = ras_object or ras
84
+ ras_obj.check_initialized()
85
+
86
+ # Update the flow dataframe in the ras instance to ensure it is current
87
+ ras_obj.flow_df = ras_obj.get_flow_entries()
88
+
89
+ if new_steady_flow_number not in ras_obj.flow_df['flow_number'].values:
90
+ raise ValueError(f"Steady flow number {new_steady_flow_number} not found in project file.")
91
+
92
+ # Resolve the full path of the plan file
93
+ plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
94
+ if not plan_file_path:
95
+ raise FileNotFoundError(f"Plan file not found: {plan_number}")
96
+
97
+ with open(plan_file_path, 'r') as f:
98
+ lines = f.readlines()
99
+ with open(plan_file_path, 'w') as f:
100
+ for line in lines:
101
+ if line.startswith("Flow File=f"):
102
+ f.write(f"Flow File=f{new_steady_flow_number}\n")
103
+ logging.info(f"Updated Flow File in {plan_file_path} to f{new_steady_flow_number}")
104
+ else:
105
+ f.write(line)
106
+
107
+ ras_obj.plan_df = ras_obj.get_plan_entries()
108
+ ras_obj.geom_df = ras_obj.get_geom_entries()
109
+ ras_obj.flow_df = ras_obj.get_flow_entries()
110
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
111
+
112
+ @staticmethod
113
+ def set_unsteady(plan_number: str, new_unsteady_flow_number: str, ras_object=None):
114
+ """
115
+ Apply an unsteady flow file to a plan file.
116
+
117
+ Parameters:
118
+ plan_number (str): Plan number (e.g., '04')
119
+ new_unsteady_flow_number (str): Unsteady flow number to apply (e.g., '01')
120
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
121
+
122
+ Returns:
123
+ None
124
+
125
+ Raises:
126
+ ValueError: If the specified unsteady number is not found in the project file
127
+ FileNotFoundError: If the specified plan file is not found
128
+
129
+ Example:
130
+ >>> RasPlan.set_unsteady('04', '01')
131
+
132
+ Note:
133
+ This function updates the ras object's dataframes after modifying the project structure.
134
+ """
135
+ print(f"Setting unsteady flow file from {new_unsteady_flow_number} to {plan_number}")
136
+
137
+ ras_obj = ras_object or ras
138
+ ras_obj.check_initialized()
139
+
140
+ # Update the unsteady dataframe in the ras instance to ensure it is current
141
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
142
+
143
+ if new_unsteady_flow_number not in ras_obj.unsteady_df['unsteady_number'].values:
144
+ raise ValueError(f"Unsteady number {new_unsteady_flow_number} not found in project file.")
145
+
146
+ # Get the full path of the plan file
147
+ plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
148
+ if not plan_file_path:
149
+ raise FileNotFoundError(f"Plan file not found: {plan_number}")
150
+
151
+ RasPlan.update_plan_file(plan_file_path, 'Unsteady', new_unsteady_flow_number)
152
+ print(f"Updated unsteady flow file in {plan_file_path} to u{new_unsteady_flow_number}")
153
+
154
+ ras_obj.plan_df = ras_obj.get_plan_entries()
155
+ ras_obj.geom_df = ras_obj.get_geom_entries()
156
+ ras_obj.flow_df = ras_obj.get_flow_entries()
157
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
158
+
159
+ @staticmethod
160
+ def set_num_cores(plan_number, num_cores, ras_object=None):
161
+ """
162
+ Update the maximum number of cores to use in the HEC-RAS plan file.
163
+
164
+ Parameters:
165
+ plan_number (str): Plan number (e.g., '02') or full path to the plan file
166
+ num_cores (int): Maximum number of cores to use
167
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
168
+
169
+ Returns:
170
+ None
171
+
172
+ Notes on setting num_cores in HEC-RAS:
173
+ The recommended setting for num_cores is 2 (most efficient) to 8 (most performant)
174
+ More details in the HEC-Commander Repository Blog "Benchmarking is All You Need"
175
+ https://github.com/billk-FM/HEC-Commander/blob/main/Blog/7._Benchmarking_Is_All_You_Need.md
176
+
177
+ Microsoft Windows has a maximum of 64 cores that can be allocated to a single Ras.exe process.
178
+
179
+ Example:
180
+ >>> # Using plan number
181
+ >>> RasPlan.set_num_cores('02', 4)
182
+ >>> # Using full path to plan file
183
+ >>> RasPlan.set_num_cores('/path/to/project.p02', 4)
184
+
185
+ Note:
186
+ This function updates the ras object's dataframes after modifying the project structure.
187
+ """
188
+ print(f"Setting num_cores to {num_cores} in Plan {plan_number}")
189
+
190
+ ras_obj = ras_object or ras
191
+ ras_obj.check_initialized()
192
+
193
+ # Determine if plan_number is a path or a plan number
194
+ if Path(plan_number).is_file():
195
+ plan_file_path = Path(plan_number)
196
+ if not plan_file_path.exists():
197
+ raise FileNotFoundError(f"Plan file not found: {plan_file_path}. Please provide a valid plan number or path.")
198
+ else:
199
+ # Update the plan dataframe in the ras instance to ensure it is current
200
+ ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
201
+
202
+ # Get the full path of the plan file
203
+ plan_file_path = RasPlan.get_plan_path(plan_number, ras_obj)
204
+ if not plan_file_path:
205
+ raise FileNotFoundError(f"Plan file not found: {plan_number}. Please provide a valid plan number or path.")
206
+
207
+ cores_pattern = re.compile(r"(UNET D1 Cores= )\d+")
208
+ with open(plan_file_path, 'r') as file:
209
+ content = file.read()
210
+ new_content = cores_pattern.sub(rf"\g<1>{num_cores}", content)
211
+ with open(plan_file_path, 'w') as file:
212
+ file.write(new_content)
213
+ print(f"Updated {plan_file_path} with {num_cores} cores.")
214
+
215
+ ras_obj.plan_df = ras_obj.get_plan_entries()
216
+ ras_obj.geom_df = ras_obj.get_geom_entries()
217
+ ras_obj.flow_df = ras_obj.get_flow_entries()
218
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
219
+
220
+
221
+ @staticmethod
222
+ def set_geom_preprocessor(file_path, run_htab, use_ib_tables, ras_object=None):
223
+ """
224
+ Update the simulation plan file to modify the `Run HTab` and `UNET Use Existing IB Tables` settings.
225
+
226
+ Parameters:
227
+ file_path (str): Path to the simulation plan file (.p06 or similar) that you want to modify.
228
+ run_htab (int): Value for the `Run HTab` setting:
229
+ - `0` : Do not run the geometry preprocessor, use existing geometry tables.
230
+ - `-1` : Run the geometry preprocessor, forcing a recomputation of the geometry tables.
231
+ use_ib_tables (int): Value for the `UNET Use Existing IB Tables` setting:
232
+ - `0` : Use existing interpolation/boundary (IB) tables without recomputing them.
233
+ - `-1` : Do not use existing IB tables, force a recomputation.
234
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
235
+
236
+ Returns:
237
+ None
238
+
239
+ Raises:
240
+ ValueError: If `run_htab` or `use_ib_tables` are not integers or not within the accepted values (`0` or `-1`).
241
+ FileNotFoundError: If the specified file does not exist.
242
+ IOError: If there is an error reading or writing the file.
243
+
244
+ Example:
245
+ >>> RasPlan.set_geom_preprocessor('/path/to/project.p06', run_htab=-1, use_ib_tables=0)
246
+
247
+ Note:
248
+ This function updates the ras object's dataframes after modifying the project structure.
249
+ """
250
+ ras_obj = ras_object or ras
251
+ ras_obj.check_initialized()
252
+
253
+ if run_htab not in [-1, 0]:
254
+ raise ValueError("Invalid value for `Run HTab`. Expected `0` or `-1`.")
255
+ if use_ib_tables not in [-1, 0]:
256
+ raise ValueError("Invalid value for `UNET Use Existing IB Tables`. Expected `0` or `-1`.")
257
+ try:
258
+ print(f"Reading the file: {file_path}")
259
+ with open(file_path, 'r') as file:
260
+ lines = file.readlines()
261
+ print("Updating the file with new settings...")
262
+ updated_lines = []
263
+ for line in lines:
264
+ if line.lstrip().startswith("Run HTab="):
265
+ updated_line = f"Run HTab= {run_htab} \n"
266
+ updated_lines.append(updated_line)
267
+ print(f"Updated 'Run HTab' to {run_htab}")
268
+ elif line.lstrip().startswith("UNET Use Existing IB Tables="):
269
+ updated_line = f"UNET Use Existing IB Tables= {use_ib_tables} \n"
270
+ updated_lines.append(updated_line)
271
+ print(f"Updated 'UNET Use Existing IB Tables' to {use_ib_tables}")
272
+ else:
273
+ updated_lines.append(line)
274
+ print(f"Writing the updated settings back to the file: {file_path}")
275
+ with open(file_path, 'w') as file:
276
+ file.writelines(updated_lines)
277
+ print("File update completed successfully.")
278
+ except FileNotFoundError:
279
+ raise FileNotFoundError(f"The file '{file_path}' does not exist.")
280
+ except IOError as e:
281
+ raise IOError(f"An error occurred while reading or writing the file: {e}")
282
+
283
+ ras_obj.plan_df = ras_obj.get_plan_entries()
284
+ ras_obj.geom_df = ras_obj.get_geom_entries()
285
+ ras_obj.flow_df = ras_obj.get_flow_entries()
286
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
287
+
288
+ # Get Functions to retrieve file paths for plan, flow, unsteady, geometry and results files
289
+
290
+ @staticmethod
291
+ def get_results_path(plan_number: str, ras_object=None) -> Optional[str]:
292
+ """
293
+ Retrieve the results file path for a given HEC-RAS plan number.
294
+
295
+ Args:
296
+ plan_number (str): The HEC-RAS plan number for which to find the results path.
297
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
298
+
299
+ Returns:
300
+ Optional[str]: The full path to the results file if found and the file exists, or None if not found.
301
+
302
+ Raises:
303
+ RuntimeError: If the project is not initialized.
304
+
305
+ Example:
306
+ >>> ras_plan = RasPlan()
307
+ >>> results_path = ras_plan.get_results_path('01')
308
+ >>> if results_path:
309
+ ... print(f"Results file found at: {results_path}")
310
+ ... else:
311
+ ... print("Results file not found.")
312
+ """
313
+ ras_obj = ras_object or ras
314
+ ras_obj.check_initialized()
315
+
316
+ # Update the plan dataframe in the ras instance to ensure it is current
317
+ ras_obj.plan_df = ras_obj.get_plan_entries()
318
+
319
+ plan_entry = ras_obj.plan_df[ras_obj.plan_df['plan_number'] == plan_number]
320
+ if not plan_entry.empty:
321
+ results_path = plan_entry['HDF_Results_Path'].iloc[0]
322
+ if results_path:
323
+ print(f"Results file for Plan number {plan_number} exists at: {results_path}")
324
+ return results_path
325
+ else:
326
+ print(f"Results file for Plan number {plan_number} does not exist.")
327
+ return None
328
+ else:
329
+ print(f"Plan number {plan_number} not found in the entries.")
330
+ return None
331
+
332
+ @staticmethod
333
+ def get_plan_path(plan_number: str, ras_object=None) -> Optional[str]:
334
+ """
335
+ Return the full path for a given plan number.
336
+
337
+ This method ensures that the latest plan entries are included by refreshing
338
+ the plan dataframe before searching for the requested plan number.
339
+
340
+ Args:
341
+ plan_number (str): The plan number to search for.
342
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
343
+
344
+ Returns:
345
+ Optional[str]: The full path of the plan file if found, None otherwise.
346
+
347
+ Raises:
348
+ RuntimeError: If the project is not initialized.
349
+
350
+ Example:
351
+ >>> ras_plan = RasPlan()
352
+ >>> plan_path = ras_plan.get_plan_path('01')
353
+ >>> if plan_path:
354
+ ... print(f"Plan file found at: {plan_path}")
355
+ ... else:
356
+ ... print("Plan file not found.")
357
+ """
358
+ ras_obj = ras_object or ras
359
+ ras_obj.check_initialized()
360
+
361
+ project_name = ras_obj.project_name
362
+
363
+ # Use updated plan dataframe
364
+ plan_df = ras_obj.get_plan_entries()
365
+
366
+ plan_path = plan_df[plan_df['plan_number'] == plan_number]
367
+
368
+ if not plan_path.empty:
369
+ full_path = plan_path['full_path'].iloc[0]
370
+ return full_path
371
+ else:
372
+ print(f"Plan number {plan_number} not found in the updated plan entries.")
373
+ return None
374
+
375
+ @staticmethod
376
+ def get_flow_path(flow_number: str, ras_object=None) -> Optional[str]:
377
+ """
378
+ Return the full path for a given flow number.
379
+
380
+ Args:
381
+ flow_number (str): The flow number to search for.
382
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
383
+
384
+ Returns:
385
+ Optional[str]: The full path of the flow file if found, None otherwise.
386
+
387
+ Raises:
388
+ RuntimeError: If the project is not initialized.
389
+
390
+ Example:
391
+ >>> ras_plan = RasPlan()
392
+ >>> flow_path = ras_plan.get_flow_path('01')
393
+ >>> if flow_path:
394
+ ... print(f"Flow file found at: {flow_path}")
395
+ ... else:
396
+ ... print("Flow file not found.")
397
+ """
398
+ ras_obj = ras_object or ras
399
+ ras_obj.check_initialized()
400
+
401
+ # Use updated flow dataframe
402
+ ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
403
+
404
+ flow_path = ras_obj.flow_df[ras_obj.flow_df['flow_number'] == flow_number]
405
+ if not flow_path.empty:
406
+ full_path = flow_path['full_path'].iloc[0]
407
+ return full_path
408
+ else:
409
+ print(f"Flow number {flow_number} not found in the updated flow entries.")
410
+ return None
411
+
412
+ @staticmethod
413
+ def get_unsteady_path(unsteady_number: str, ras_object=None) -> Optional[str]:
414
+ """
415
+ Return the full path for a given unsteady number.
416
+
417
+ Args:
418
+ unsteady_number (str): The unsteady number to search for.
419
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
420
+
421
+ Returns:
422
+ Optional[str]: The full path of the unsteady file if found, None otherwise.
423
+
424
+ Raises:
425
+ RuntimeError: If the project is not initialized.
426
+
427
+ Example:
428
+ >>> ras_plan = RasPlan()
429
+ >>> unsteady_path = ras_plan.get_unsteady_path('01')
430
+ >>> if unsteady_path:
431
+ ... print(f"Unsteady file found at: {unsteady_path}")
432
+ ... else:
433
+ ... print("Unsteady file not found.")
434
+ """
435
+ ras_obj = ras_object or ras
436
+ ras_obj.check_initialized()
437
+
438
+ # Use updated unsteady dataframe
439
+ ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
440
+
441
+ unsteady_path = ras_obj.unsteady_df[ras_obj.unsteady_df['unsteady_number'] == unsteady_number]
442
+ if not unsteady_path.empty:
443
+ full_path = unsteady_path['full_path'].iloc[0]
444
+ return full_path
445
+ else:
446
+ print(f"Unsteady number {unsteady_number} not found in the updated unsteady entries.")
447
+ return None
448
+
449
+ @staticmethod
450
+ def get_geom_path(geom_number: str, ras_object=None) -> Optional[str]:
451
+ """
452
+ Return the full path for a given geometry number.
453
+
454
+ Args:
455
+ geom_number (str): The geometry number to search for.
456
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
457
+
458
+ Returns:
459
+ Optional[str]: The full path of the geometry file if found, None otherwise.
460
+
461
+ Raises:
462
+ RuntimeError: If the project is not initialized.
463
+
464
+ Example:
465
+ >>> ras_plan = RasPlan()
466
+ >>> geom_path = ras_plan.get_geom_path('01')
467
+ >>> if geom_path:
468
+ ... print(f"Geometry file found at: {geom_path}")
469
+ ... else:
470
+ ... print("Geometry file not found.")
471
+ """
472
+ ras_obj = ras_object or ras
473
+ ras_obj.check_initialized()
474
+
475
+ # Use updated geom dataframe
476
+ ras_obj.geom_df = ras_obj.get_prj_entries('Geom')
477
+
478
+ geom_path = ras_obj.geom_df[ras_obj.geom_df['geom_number'] == geom_number]
479
+ if not geom_path.empty:
480
+ full_path = geom_path['full_path'].iloc[0]
481
+ return full_path
482
+ else:
483
+ print(f"Geometry number {geom_number} not found in the updated geometry entries.")
484
+ return None
485
+ # Clone Functions to copy unsteady, flow, and geometry files from templates
486
+
487
+ @staticmethod
488
+ def clone_plan(template_plan, new_plan_shortid=None, ras_object=None):
489
+ """
490
+ Create a new plan file based on a template and update the project file.
491
+
492
+ Parameters:
493
+ template_plan (str): Plan number to use as template (e.g., '01')
494
+ new_plan_shortid (str, optional): New short identifier for the plan file
495
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
496
+
497
+ Returns:
498
+ str: New plan number
499
+
500
+ Example:
501
+ >>> ras_plan = RasPlan()
502
+ >>> new_plan_number = ras_plan.clone_plan('01', new_plan_shortid='New Plan')
503
+ >>> print(f"New plan created with number: {new_plan_number}")
504
+
505
+ Note:
506
+ This function updates the ras object's dataframes after modifying the project structure.
507
+ """
508
+ ras_obj = ras_object or ras
509
+ ras_obj.check_initialized()
510
+
511
+ # Update plan entries without reinitializing the entire project
512
+ ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
513
+
514
+ new_plan_num = RasPlan.get_next_number(ras_obj.plan_df['plan_number'])
515
+ template_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{template_plan}"
516
+ new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
517
+
518
+ if not template_plan_path.exists():
519
+ raise FileNotFoundError(f"Template plan file '{template_plan_path}' does not exist.")
520
+
521
+ shutil.copy(template_plan_path, new_plan_path)
522
+ print(f"Copied {template_plan_path} to {new_plan_path}")
523
+
524
+ with open(new_plan_path, 'r') as f:
525
+ plan_lines = f.readlines()
526
+
527
+ shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
528
+ for i, line in enumerate(plan_lines):
529
+ match = shortid_pattern.match(line.strip())
530
+ if match:
531
+ current_shortid = match.group(1)
532
+ if new_plan_shortid is None:
533
+ new_shortid = (current_shortid + "_copy")[:24]
534
+ else:
535
+ new_shortid = new_plan_shortid[:24]
536
+ plan_lines[i] = f"Short Identifier={new_shortid}\n"
537
+ break
538
+
539
+ with open(new_plan_path, 'w') as f:
540
+ f.writelines(plan_lines)
541
+
542
+ print(f"Updated short identifier in {new_plan_path}")
543
+
544
+ with open(ras_obj.prj_file, 'r') as f:
545
+ lines = f.readlines()
546
+
547
+ # Prepare the new Plan File entry line
548
+ new_plan_line = f"Plan File=p{new_plan_num}\n"
549
+
550
+ # Find the correct insertion point for the new Plan File entry
551
+ plan_file_pattern = re.compile(r'^Plan File=p(\d+)', re.IGNORECASE)
552
+ insertion_index = None
553
+ for i, line in enumerate(lines):
554
+ match = plan_file_pattern.match(line.strip())
555
+ if match:
556
+ current_number = int(match.group(1))
557
+ if current_number < int(new_plan_num):
558
+ continue
559
+ else:
560
+ insertion_index = i
561
+ break
562
+
563
+ if insertion_index is not None:
564
+ lines.insert(insertion_index, new_plan_line)
565
+ else:
566
+ # Try to insert after the last Plan File entry
567
+ plan_indices = [i for i, line in enumerate(lines) if plan_file_pattern.match(line.strip())]
568
+ if plan_indices:
569
+ last_plan_index = plan_indices[-1]
570
+ lines.insert(last_plan_index + 1, new_plan_line)
571
+ else:
572
+ # Append at the end if no Plan File entries exist
573
+ lines.append(new_plan_line)
574
+
575
+ # Write the updated lines back to the project file
576
+ with open(ras_obj.prj_file, 'w') as f:
577
+ f.writelines(lines)
578
+
579
+ print(f"Updated {ras_obj.prj_file} with new plan p{new_plan_num}")
580
+ new_plan = new_plan_num
581
+
582
+ # Store the project folder path
583
+ project_folder = ras_obj.project_folder
584
+
585
+ # Re-initialize the ras global object
586
+ ras_obj.initialize(project_folder, ras_obj.ras_exe_path)
587
+
588
+ ras_obj.plan_df = ras_obj.get_plan_entries()
589
+ ras_obj.geom_df = ras_obj.get_geom_entries()
590
+ ras_obj.flow_df = ras_obj.get_flow_entries()
591
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
592
+
593
+ return new_plan
594
+
595
+
596
+ @staticmethod
597
+ def clone_unsteady(template_unsteady, ras_object=None):
598
+ """
599
+ Copy unsteady flow files from a template, find the next unsteady number,
600
+ and update the project file accordingly.
601
+
602
+ Parameters:
603
+ template_unsteady (str): Unsteady flow number to be used as a template (e.g., '01')
604
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
605
+
606
+ Returns:
607
+ str: New unsteady flow number (e.g., '03')
608
+
609
+ Example:
610
+ >>> ras_plan = RasPlan()
611
+ >>> new_unsteady_num = ras_plan.clone_unsteady('01')
612
+ >>> print(f"New unsteady flow file created: u{new_unsteady_num}")
613
+
614
+ Note:
615
+ This function updates the ras object's dataframes after modifying the project structure.
616
+ """
617
+ ras_obj = ras_object or ras
618
+ ras_obj.check_initialized()
619
+
620
+ # Update unsteady entries without reinitializing the entire project
621
+ ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
622
+
623
+ new_unsteady_num = RasPlan.get_next_number(ras_obj.unsteady_df['unsteady_number'])
624
+ template_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}"
625
+ new_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}"
626
+
627
+ if not template_unsteady_path.exists():
628
+ raise FileNotFoundError(f"Template unsteady file '{template_unsteady_path}' does not exist.")
629
+
630
+ shutil.copy(template_unsteady_path, new_unsteady_path)
631
+ print(f"Copied {template_unsteady_path} to {new_unsteady_path}")
632
+
633
+ # Copy the corresponding .hdf file if it exists
634
+ template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}.hdf"
635
+ new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}.hdf"
636
+ if template_hdf_path.exists():
637
+ shutil.copy(template_hdf_path, new_hdf_path)
638
+ print(f"Copied {template_hdf_path} to {new_hdf_path}")
639
+ else:
640
+ print(f"No corresponding .hdf file found for '{template_unsteady_path}'. Skipping '.hdf' copy.")
641
+
642
+ with open(ras_obj.prj_file, 'r') as f:
643
+ lines = f.readlines()
644
+
645
+ # Prepare the new Unsteady Flow File entry line
646
+ new_unsteady_line = f"Unsteady File=u{new_unsteady_num}\n"
647
+
648
+ # Find the correct insertion point for the new Unsteady Flow File entry
649
+ unsteady_file_pattern = re.compile(r'^Unsteady File=u(\d+)', re.IGNORECASE)
650
+ insertion_index = None
651
+ for i, line in enumerate(lines):
652
+ match = unsteady_file_pattern.match(line.strip())
653
+ if match:
654
+ current_number = int(match.group(1))
655
+ if current_number < int(new_unsteady_num):
656
+ continue
657
+ else:
658
+ insertion_index = i
659
+ break
660
+
661
+ if insertion_index is not None:
662
+ lines.insert(insertion_index, new_unsteady_line)
663
+ else:
664
+ # Try to insert after the last Unsteady Flow File entry
665
+ unsteady_indices = [i for i, line in enumerate(lines) if unsteady_file_pattern.match(line.strip())]
666
+ if unsteady_indices:
667
+ last_unsteady_index = unsteady_indices[-1]
668
+ lines.insert(last_unsteady_index + 1, new_unsteady_line)
669
+ else:
670
+ # Append at the end if no Unsteady Flow File entries exist
671
+ lines.append(new_unsteady_line)
672
+
673
+ # Write the updated lines back to the project file
674
+ with open(ras_obj.prj_file, 'w') as f:
675
+ f.writelines(lines)
676
+
677
+ print(f"Updated {ras_obj.prj_file} with new unsteady flow file u{new_unsteady_num}")
678
+ new_unsteady = new_unsteady_num
679
+
680
+ # Store the project folder path
681
+ project_folder = ras_obj.project_folder
682
+ hecras_path = ras_obj.ras_exe_path
683
+
684
+ # Re-initialize the ras global object
685
+ ras_obj.initialize(project_folder, hecras_path)
686
+
687
+ ras_obj.plan_df = ras_obj.get_plan_entries()
688
+ ras_obj.geom_df = ras_obj.get_geom_entries()
689
+ ras_obj.flow_df = ras_obj.get_flow_entries()
690
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
691
+
692
+ return new_unsteady
693
+
694
+ @staticmethod
695
+ def clone_steady(template_flow, ras_object=None):
696
+ """
697
+ Copy steady flow files from a template, find the next flow number,
698
+ and update the project file accordingly.
699
+
700
+ Parameters:
701
+ template_flow (str): Flow number to be used as a template (e.g., '01')
702
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
703
+
704
+ Returns:
705
+ str: New flow number (e.g., '03')
706
+
707
+ Example:
708
+ >>> ras_plan = RasPlan()
709
+ >>> new_flow_num = ras_plan.clone_steady('01')
710
+ >>> print(f"New steady flow file created: f{new_flow_num}")
711
+
712
+ Note:
713
+ This function updates the ras object's dataframes after modifying the project structure.
714
+ """
715
+ ras_obj = ras_object or ras
716
+ ras_obj.check_initialized()
717
+
718
+ # Update flow entries without reinitializing the entire project
719
+ ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
720
+
721
+ new_flow_num = RasPlan.get_next_number(ras_obj.flow_df['flow_number'])
722
+ template_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{template_flow}"
723
+ new_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_flow_num}"
724
+
725
+ if not template_flow_path.exists():
726
+ raise FileNotFoundError(f"Template steady flow file '{template_flow_path}' does not exist.")
727
+
728
+ shutil.copy(template_flow_path, new_flow_path)
729
+ print(f"Copied {template_flow_path} to {new_flow_path}")
730
+
731
+ # Read the contents of the project file
732
+ with open(ras_obj.prj_file, 'r') as f:
733
+ lines = f.readlines()
734
+
735
+ # Prepare the new Steady Flow File entry line
736
+ new_flow_line = f"Flow File=f{new_flow_num}\n"
737
+
738
+ # Find the correct insertion point for the new Steady Flow File entry
739
+ flow_file_pattern = re.compile(r'^Flow File=f(\d+)', re.IGNORECASE)
740
+ insertion_index = None
741
+ for i, line in enumerate(lines):
742
+ match = flow_file_pattern.match(line.strip())
743
+ if match:
744
+ current_number = int(match.group(1))
745
+ if current_number < int(new_flow_num):
746
+ continue
747
+ else:
748
+ insertion_index = i
749
+ break
750
+
751
+ if insertion_index is not None:
752
+ lines.insert(insertion_index, new_flow_line)
753
+ else:
754
+ # Try to insert after the last Steady Flow File entry
755
+ flow_indices = [i for i, line in enumerate(lines) if flow_file_pattern.match(line.strip())]
756
+ if flow_indices:
757
+ last_flow_index = flow_indices[-1]
758
+ lines.insert(last_flow_index + 1, new_flow_line)
759
+ else:
760
+ # Append at the end if no Steady Flow File entries exist
761
+ lines.append(new_flow_line)
762
+
763
+ # Write the updated lines back to the project file
764
+ with open(ras_obj.prj_file, 'w') as f:
765
+ f.writelines(lines)
766
+
767
+ print(f"Updated {ras_obj.prj_file} with new steady flow file f{new_flow_num}")
768
+ new_steady = new_flow_num
769
+
770
+ # Store the project folder path
771
+ project_folder = ras_obj.project_folder
772
+
773
+ # Re-initialize the ras global object
774
+ ras_obj.initialize(project_folder, ras_obj.ras_exe_path)
775
+
776
+ ras_obj.plan_df = ras_obj.get_plan_entries()
777
+ ras_obj.geom_df = ras_obj.get_geom_entries()
778
+ ras_obj.flow_df = ras_obj.get_flow_entries()
779
+ ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
780
+
781
+ return new_steady
782
+
783
+
784
+ @staticmethod
785
+ def clone_geom(template_geom, ras_object=None):
786
+ """
787
+ Copy geometry files from a template, find the next geometry number,
788
+ and update the project file accordingly.
789
+
790
+ Parameters:
791
+ template_geom (str): Geometry number to be used as a template (e.g., '01')
792
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
793
+
794
+ Returns:
795
+ str: New geometry number (e.g., '03')
796
+
797
+ Note:
798
+ This function updates the ras object's dataframes after modifying the project structure.
799
+ """
800
+ ras_obj = ras_object or ras
801
+ ras_obj.check_initialized()
802
+
803
+ # Update geometry entries without reinitializing the entire project
804
+ ras_obj.geom_df = ras_obj.get_prj_entries('Geom') # Call the correct function to get updated geometry entries
805
+ print(f"Updated geometry entries:\n{ras_obj.geom_df}")
806
+
807
+ # Clone Functions to copy unsteady, flow, and geometry files from templates
808
+
809
+ @staticmethod
810
+ def clone_plan(template_plan, new_plan_shortid=None, ras_object=None):
811
+ """
812
+ Create a new plan file based on a template and update the project file.
813
+
814
+ Parameters:
815
+ template_plan (str): Plan number to use as template (e.g., '01')
816
+ new_plan_shortid (str, optional): New short identifier for the plan file
817
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
818
+
819
+ Returns:
820
+ str: New plan number
821
+
822
+ Revision Notes:
823
+ - Updated to insert new plan entry in the correct position
824
+ - Improved error handling and logging
825
+ - Updated to use get_prj_entries('Plan') for the latest entries
826
+ - Added print statements for progress tracking
827
+
828
+ Example:
829
+ >>> ras_plan = RasPlan()
830
+ >>> new_plan_number = ras_plan.clone_plan('01', new_plan_shortid='New Plan')
831
+ >>> print(f"New plan created with number: {new_plan_number}")
832
+ """
833
+ ras_obj = ras_object or ras
834
+ ras_obj.check_initialized()
835
+
836
+ # Update plan entries without reinitializing the entire project
837
+ ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
838
+
839
+ new_plan_num = RasPlan.get_next_number(ras_obj.plan_df['plan_number'])
840
+ template_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{template_plan}"
841
+ new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
842
+
843
+ if not template_plan_path.exists():
844
+ raise FileNotFoundError(f"Template plan file '{template_plan_path}' does not exist.")
845
+
846
+ shutil.copy(template_plan_path, new_plan_path)
847
+ print(f"Copied {template_plan_path} to {new_plan_path}")
848
+
849
+ with open(new_plan_path, 'r') as f:
850
+ plan_lines = f.readlines()
851
+
852
+ shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
853
+ for i, line in enumerate(plan_lines):
854
+ match = shortid_pattern.match(line.strip())
855
+ if match:
856
+ current_shortid = match.group(1)
857
+ if new_plan_shortid is None:
858
+ new_shortid = (current_shortid + "_copy")[:24]
859
+ else:
860
+ new_shortid = new_plan_shortid[:24]
861
+ plan_lines[i] = f"Short Identifier={new_shortid}\n"
862
+ break
863
+
864
+ with open(new_plan_path, 'w') as f:
865
+ f.writelines(plan_lines)
866
+
867
+ print(f"Updated short identifier in {new_plan_path}")
868
+
869
+ with open(ras_obj.prj_file, 'r') as f:
870
+ lines = f.readlines()
871
+
872
+ # Prepare the new Plan File entry line
873
+ new_plan_line = f"Plan File=p{new_plan_num}\n"
874
+
875
+ # Find the correct insertion point for the new Plan File entry
876
+ plan_file_pattern = re.compile(r'^Plan File=p(\d+)', re.IGNORECASE)
877
+ insertion_index = None
878
+ for i, line in enumerate(lines):
879
+ match = plan_file_pattern.match(line.strip())
880
+ if match:
881
+ current_number = int(match.group(1))
882
+ if current_number < int(new_plan_num):
883
+ continue
884
+ else:
885
+ insertion_index = i
886
+ break
887
+
888
+ if insertion_index is not None:
889
+ lines.insert(insertion_index, new_plan_line)
890
+ else:
891
+ # Try to insert after the last Plan File entry
892
+ plan_indices = [i for i, line in enumerate(lines) if plan_file_pattern.match(line.strip())]
893
+ if plan_indices:
894
+ last_plan_index = plan_indices[-1]
895
+ lines.insert(last_plan_index + 1, new_plan_line)
896
+ else:
897
+ # Append at the end if no Plan File entries exist
898
+ lines.append(new_plan_line)
899
+
900
+ # Write the updated lines back to the project file
901
+ with open(ras_obj.prj_file, 'w') as f:
902
+ f.writelines(lines)
903
+
904
+ print(f"Updated {ras_obj.prj_file} with new plan p{new_plan_num}")
905
+ new_plan = new_plan_num
906
+
907
+ # Store the project folder path
908
+ project_folder = ras_obj.project_folder
909
+
910
+ # Re-initialize the ras global object
911
+ ras_obj.initialize(project_folder, ras_obj.ras_exe_path)
912
+ return new_plan
913
+
914
+
915
+ @staticmethod
916
+ def clone_unsteady(template_unsteady, ras_object=None):
917
+ """
918
+ Copy unsteady flow files from a template, find the next unsteady number,
919
+ and update the project file accordingly.
920
+
921
+ Parameters:
922
+ template_unsteady (str): Unsteady flow number to be used as a template (e.g., '01')
923
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
924
+
925
+ Returns:
926
+ str: New unsteady flow number (e.g., '03')
927
+
928
+ Example:
929
+ >>> ras_plan = RasPlan()
930
+ >>> new_unsteady_num = ras_plan.clone_unsteady('01')
931
+ >>> print(f"New unsteady flow file created: u{new_unsteady_num}")
932
+
933
+ Revision Notes:
934
+ - Updated to insert new unsteady flow entry in the correct position
935
+ - Improved error handling and logging
936
+ - Removed dst_folder parameter as it's not needed (using project folder)
937
+ - Added handling for corresponding .hdf files
938
+ - Updated to use get_prj_entries('Unsteady') for the latest entries
939
+ """
940
+ ras_obj = ras_object or ras
941
+ ras_obj.check_initialized()
942
+
943
+ # Update unsteady entries without reinitializing the entire project
944
+ ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
945
+
946
+ new_unsteady_num = RasPlan.get_next_number(ras_obj.unsteady_df['unsteady_number'])
947
+ template_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}"
948
+ new_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}"
949
+
950
+ if not template_unsteady_path.exists():
951
+ raise FileNotFoundError(f"Template unsteady file '{template_unsteady_path}' does not exist.")
952
+
953
+ shutil.copy(template_unsteady_path, new_unsteady_path)
954
+ print(f"Copied {template_unsteady_path} to {new_unsteady_path}")
955
+
956
+ # Copy the corresponding .hdf file if it exists
957
+ template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}.hdf"
958
+ new_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}.hdf"
959
+ if template_hdf_path.exists():
960
+ shutil.copy(template_hdf_path, new_hdf_path)
961
+ print(f"Copied {template_hdf_path} to {new_hdf_path}")
962
+ else:
963
+ print(f"No corresponding .hdf file found for '{template_unsteady_path}'. Skipping '.hdf' copy.")
964
+
965
+ with open(ras_obj.prj_file, 'r') as f:
966
+ lines = f.readlines()
967
+
968
+ # Prepare the new Unsteady Flow File entry line
969
+ new_unsteady_line = f"Unsteady File=u{new_unsteady_num}\n"
970
+
971
+ # Find the correct insertion point for the new Unsteady Flow File entry
972
+ unsteady_file_pattern = re.compile(r'^Unsteady File=u(\d+)', re.IGNORECASE)
973
+ insertion_index = None
974
+ for i, line in enumerate(lines):
975
+ match = unsteady_file_pattern.match(line.strip())
976
+ if match:
977
+ current_number = int(match.group(1))
978
+ if current_number < int(new_unsteady_num):
979
+ continue
980
+ else:
981
+ insertion_index = i
982
+ break
983
+
984
+ if insertion_index is not None:
985
+ lines.insert(insertion_index, new_unsteady_line)
986
+ else:
987
+ # Try to insert after the last Unsteady Flow File entry
988
+ unsteady_indices = [i for i, line in enumerate(lines) if unsteady_file_pattern.match(line.strip())]
989
+ if unsteady_indices:
990
+ last_unsteady_index = unsteady_indices[-1]
991
+ lines.insert(last_unsteady_index + 1, new_unsteady_line)
992
+ else:
993
+ # Append at the end if no Unsteady Flow File entries exist
994
+ lines.append(new_unsteady_line)
995
+
996
+ # Write the updated lines back to the project file
997
+ with open(ras_obj.prj_file, 'w') as f:
998
+ f.writelines(lines)
999
+
1000
+ print(f"Updated {ras_obj.prj_file} with new unsteady flow file u{new_unsteady_num}")
1001
+ new_unsteady = new_unsteady_num
1002
+
1003
+ # Store the project folder path
1004
+ project_folder = ras_obj.project_folder
1005
+ hecras_path = ras_obj.ras_exe_path
1006
+
1007
+ # Re-initialize the ras global object
1008
+ ras_obj.initialize(project_folder, hecras_path)
1009
+
1010
+ return new_unsteady
1011
+
1012
+ @staticmethod
1013
+ def clone_steady(template_flow, ras_object=None):
1014
+ """
1015
+ Copy steady flow files from a template, find the next flow number,
1016
+ and update the project file accordingly.
1017
+
1018
+ Parameters:
1019
+ template_flow (str): Flow number to be used as a template (e.g., '01')
1020
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
1021
+
1022
+ Returns:
1023
+ str: New flow number (e.g., '03')
1024
+
1025
+ Example:
1026
+ >>> ras_plan = RasPlan()
1027
+ >>> new_flow_num = ras_plan.clone_steady('01')
1028
+ >>> print(f"New steady flow file created: f{new_flow_num}")
1029
+
1030
+ Revision Notes:
1031
+ - Updated to insert new steady flow entry in the correct position
1032
+ - Improved error handling and logging
1033
+ - Added handling for corresponding .hdf files
1034
+ - Updated to use get_prj_entries('Flow') for the latest entries
1035
+ """
1036
+ ras_obj = ras_object or ras
1037
+ ras_obj.check_initialized()
1038
+
1039
+ # Update flow entries without reinitializing the entire project
1040
+ ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
1041
+
1042
+ new_flow_num = RasPlan.get_next_number(ras_obj.flow_df['flow_number'])
1043
+ template_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{template_flow}"
1044
+ new_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_flow_num}"
1045
+
1046
+ if not template_flow_path.exists():
1047
+ raise FileNotFoundError(f"Template steady flow file '{template_flow_path}' does not exist.")
1048
+
1049
+ shutil.copy(template_flow_path, new_flow_path)
1050
+ print(f"Copied {template_flow_path} to {new_flow_path}")
1051
+
1052
+ # Read the contents of the project file
1053
+ with open(ras_obj.prj_file, 'r') as f:
1054
+ lines = f.readlines()
1055
+
1056
+ # Prepare the new Steady Flow File entry line
1057
+ new_flow_line = f"Flow File=f{new_flow_num}\n"
1058
+
1059
+ # Find the correct insertion point for the new Steady Flow File entry
1060
+ flow_file_pattern = re.compile(r'^Flow File=f(\d+)', re.IGNORECASE)
1061
+ insertion_index = None
1062
+ for i, line in enumerate(lines):
1063
+ match = flow_file_pattern.match(line.strip())
1064
+ if match:
1065
+ current_number = int(match.group(1))
1066
+ if current_number < int(new_flow_num):
1067
+ continue
1068
+ else:
1069
+ insertion_index = i
1070
+ break
1071
+
1072
+ if insertion_index is not None:
1073
+ lines.insert(insertion_index, new_flow_line)
1074
+ else:
1075
+ # Try to insert after the last Steady Flow File entry
1076
+ flow_indices = [i for i, line in enumerate(lines) if flow_file_pattern.match(line.strip())]
1077
+ if flow_indices:
1078
+ last_flow_index = flow_indices[-1]
1079
+ lines.insert(last_flow_index + 1, new_flow_line)
1080
+ else:
1081
+ # Append at the end if no Steady Flow File entries exist
1082
+ lines.append(new_flow_line)
1083
+
1084
+ # Write the updated lines back to the project file
1085
+ with open(ras_obj.prj_file, 'w') as f:
1086
+ f.writelines(lines)
1087
+
1088
+ print(f"Updated {ras_obj.prj_file} with new steady flow file f{new_flow_num}")
1089
+ new_steady = new_flow_num
1090
+
1091
+ # Store the project folder path
1092
+ project_folder = ras_obj.project_folder
1093
+
1094
+ # Re-initialize the ras global object
1095
+ ras_obj.initialize(project_folder, ras_obj.ras_exe_path)
1096
+
1097
+ return new_steady
1098
+
1099
+
1100
+ @staticmethod
1101
+ def clone_geom(template_geom, ras_object=None):
1102
+ """
1103
+ Copy geometry files from a template, find the next geometry number,
1104
+ and update the project file accordingly.
1105
+
1106
+ Parameters:
1107
+ template_geom (str): Geometry number to be used as a template (e.g., '01')
1108
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
1109
+
1110
+ Returns:
1111
+ str: New geometry number (e.g., '03')
1112
+ """
1113
+ ras_obj = ras_object or ras
1114
+ ras_obj.check_initialized()
1115
+
1116
+ # Update geometry entries without reinitializing the entire project
1117
+ ras_obj.geom_df = ras_obj.get_prj_entries('Geom') # Call the correct function to get updated geometry entries
1118
+ #print(f"Updated geometry entries:\n{ras_obj.geom_df}")
1119
+
1120
+ template_geom_filename = f"{ras_obj.project_name}.g{template_geom}"
1121
+ template_geom_path = ras_obj.project_folder / template_geom_filename
1122
+
1123
+ if not template_geom_path.is_file():
1124
+ raise FileNotFoundError(f"Template geometry file '{template_geom_path}' does not exist.")
1125
+
1126
+ next_geom_number = RasPlan.get_next_number(ras_obj.geom_df['geom_number'])
1127
+
1128
+ new_geom_filename = f"{ras_obj.project_name}.g{next_geom_number}"
1129
+ new_geom_path = ras_obj.project_folder / new_geom_filename
1130
+
1131
+ shutil.copyfile(template_geom_path, new_geom_path)
1132
+ print(f"Copied '{template_geom_path}' to '{new_geom_path}'.")
1133
+
1134
+ # Handle HDF file copy
1135
+ template_hdf_path = template_geom_path.with_suffix('.g' + template_geom + '.hdf')
1136
+ new_hdf_path = new_geom_path.with_suffix('.g' + next_geom_number + '.hdf')
1137
+ if template_hdf_path.is_file():
1138
+ shutil.copyfile(template_hdf_path, new_hdf_path)
1139
+ print(f"Copied '{template_hdf_path}' to '{new_hdf_path}'.")
1140
+ else:
1141
+ print(f"Warning: Template geometry HDF file '{template_hdf_path}' does not exist. This is common, and not critical. Continuing without it.")
1142
+
1143
+ with open(ras_obj.prj_file, 'r') as file:
1144
+ lines = file.readlines()
1145
+
1146
+ # Prepare the new Geometry File entry line
1147
+ new_geom_line = f"Geom File=g{next_geom_number}\n"
1148
+
1149
+ # Find the correct insertion point for the new Geometry File entry
1150
+ geom_file_pattern = re.compile(r'^Geom File=g(\d+)', re.IGNORECASE)
1151
+ insertion_index = None
1152
+ for i, line in enumerate(lines):
1153
+ match = geom_file_pattern.match(line.strip())
1154
+ if match:
1155
+ current_number = int(match.group(1))
1156
+ if current_number < int(next_geom_number):
1157
+ continue
1158
+ else:
1159
+ insertion_index = i
1160
+ break
1161
+
1162
+ if insertion_index is not None:
1163
+ lines.insert(insertion_index, new_geom_line)
1164
+ else:
1165
+ # Try to insert after the last Geometry File entry
1166
+ geom_indices = [i for i, line in enumerate(lines) if geom_file_pattern.match(line.strip())]
1167
+ if geom_indices:
1168
+ last_geom_index = geom_indices[-1]
1169
+ lines.insert(last_geom_index + 1, new_geom_line)
1170
+ else:
1171
+ # Append at the end if no Geometry File entries exist
1172
+ lines.append(new_geom_line)
1173
+
1174
+ # Write the updated lines back to the project file
1175
+ with open(ras_obj.prj_file, 'w') as file:
1176
+ file.writelines(lines)
1177
+
1178
+ print(f"Updated {ras_obj.prj_file} with new geometry file g{next_geom_number}")
1179
+ new_geom = next_geom_number
1180
+
1181
+ # Update the geometry entries in the ras object
1182
+ ras_obj.geom_df = ras_obj.get_geom_entries()
1183
+ print(f"Updated geometry entries:\n{ras_obj.geom_df}")
1184
+
1185
+ return new_geom
1186
+
1187
+
1188
+
1189
+
1190
+ @staticmethod
1191
+ def get_next_number(existing_numbers):
1192
+ """
1193
+ Determine the next available number from a list of existing numbers.
1194
+
1195
+ Parameters:
1196
+ existing_numbers (list): List of existing numbers as strings
1197
+
1198
+ Returns:
1199
+ str: Next available number as a zero-padded string
1200
+
1201
+ Example:
1202
+ >>> existing_numbers = ['01', '02', '04']
1203
+ >>> RasPlan.get_next_number(existing_numbers)
1204
+ '03'
1205
+ >>> existing_numbers = ['01', '02', '03']
1206
+ >>> RasPlan.get_next_number(existing_numbers)
1207
+ '04'
1208
+ """
1209
+ existing_numbers = sorted(int(num) for num in existing_numbers)
1210
+ next_number = 1
1211
+ for num in existing_numbers:
1212
+ if num == next_number:
1213
+ next_number += 1
1214
+ else:
1215
+ break
1216
+ return f"{next_number:02d}"