ras-commander 0.51.0__py3-none-any.whl → 0.53.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/Decorators.py +137 -127
- ras_commander/HdfBase.py +359 -307
- ras_commander/HdfFluvialPluvial.py +304 -59
- ras_commander/HdfMesh.py +461 -461
- ras_commander/HdfResultsPlan.py +192 -84
- ras_commander/HdfStruc.py +1 -1
- ras_commander/HdfUtils.py +434 -434
- ras_commander/HdfXsec.py +58 -40
- ras_commander/LoggingConfig.py +2 -1
- ras_commander/RasCmdr.py +45 -20
- ras_commander/RasPlan.py +74 -65
- ras_commander/RasPrj.py +934 -879
- ras_commander/RasUnsteady.py +38 -19
- {ras_commander-0.51.0.dist-info → ras_commander-0.53.0.dist-info}/METADATA +99 -53
- ras_commander-0.53.0.dist-info/RECORD +34 -0
- {ras_commander-0.51.0.dist-info → ras_commander-0.53.0.dist-info}/WHEEL +1 -1
- ras_commander-0.51.0.dist-info/RECORD +0 -34
- {ras_commander-0.51.0.dist-info → ras_commander-0.53.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.51.0.dist-info → ras_commander-0.53.0.dist-info}/top_level.txt +0 -0
ras_commander/RasPrj.py
CHANGED
@@ -1,879 +1,934 @@
|
|
1
|
-
"""
|
2
|
-
RasPrj.py - Manages HEC-RAS projects within the ras-commander library
|
3
|
-
|
4
|
-
This module provides a class for managing HEC-RAS projects.
|
5
|
-
|
6
|
-
Classes:
|
7
|
-
RasPrj: A class for managing HEC-RAS projects.
|
8
|
-
|
9
|
-
Functions:
|
10
|
-
init_ras_project: Initialize a RAS project.
|
11
|
-
get_ras_exe: Determine the HEC-RAS executable path based on the input.
|
12
|
-
|
13
|
-
DEVELOPER NOTE:
|
14
|
-
This class is used to initialize a RAS project and is used in conjunction with the RasCmdr class to manage the execution of RAS plans.
|
15
|
-
By default, the RasPrj class is initialized with the global 'ras' object.
|
16
|
-
However, you can create multiple RasPrj instances to manage multiple projects.
|
17
|
-
Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
|
18
|
-
|
19
|
-
This module is part of the ras-commander library and uses a centralized logging configuration.
|
20
|
-
|
21
|
-
Logging Configuration:
|
22
|
-
- The logging is set up in the logging_config.py file.
|
23
|
-
- A @log_call decorator is available to automatically log function calls.
|
24
|
-
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
25
|
-
- Logs are written to both console and a rotating file handler.
|
26
|
-
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
27
|
-
- The default log level is INFO.
|
28
|
-
|
29
|
-
To use logging in this module:
|
30
|
-
1. Use the @log_call decorator for automatic function call logging.
|
31
|
-
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
32
|
-
|
33
|
-
|
34
|
-
Example:
|
35
|
-
@log_call
|
36
|
-
def my_function():
|
37
|
-
|
38
|
-
logger.debug("Additional debug information")
|
39
|
-
# Function logic here
|
40
|
-
|
41
|
-
-----
|
42
|
-
|
43
|
-
All of the methods in this class are class methods and are designed to be used with instances of the class.
|
44
|
-
|
45
|
-
List of Functions in RasPrj:
|
46
|
-
- initialize()
|
47
|
-
- _load_project_data()
|
48
|
-
- _get_geom_file_for_plan()
|
49
|
-
- _parse_plan_file()
|
50
|
-
- _parse_unsteady_file()
|
51
|
-
- _get_prj_entries()
|
52
|
-
- _parse_boundary_condition()
|
53
|
-
- is_initialized (property)
|
54
|
-
- check_initialized()
|
55
|
-
- find_ras_prj()
|
56
|
-
- get_project_name()
|
57
|
-
- get_prj_entries()
|
58
|
-
- get_plan_entries()
|
59
|
-
- get_flow_entries()
|
60
|
-
- get_unsteady_entries()
|
61
|
-
- get_geom_entries()
|
62
|
-
- get_hdf_entries()
|
63
|
-
- print_data()
|
64
|
-
- get_plan_value()
|
65
|
-
- get_boundary_conditions()
|
66
|
-
|
67
|
-
Functions in RasPrj that are not part of the class:
|
68
|
-
- init_ras_project()
|
69
|
-
- get_ras_exe()
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
"""
|
75
|
-
import os
|
76
|
-
import re
|
77
|
-
from pathlib import Path
|
78
|
-
import pandas as pd
|
79
|
-
from typing import Union, Any, List, Dict, Tuple
|
80
|
-
import logging
|
81
|
-
from ras_commander.LoggingConfig import get_logger
|
82
|
-
from ras_commander.Decorators import log_call
|
83
|
-
|
84
|
-
logger = get_logger(__name__)
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
self.
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
self.
|
139
|
-
self.
|
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
|
-
|
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
|
-
for
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
logger.
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
@
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
}
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
#
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
#
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
1
|
+
"""
|
2
|
+
RasPrj.py - Manages HEC-RAS projects within the ras-commander library
|
3
|
+
|
4
|
+
This module provides a class for managing HEC-RAS projects.
|
5
|
+
|
6
|
+
Classes:
|
7
|
+
RasPrj: A class for managing HEC-RAS projects.
|
8
|
+
|
9
|
+
Functions:
|
10
|
+
init_ras_project: Initialize a RAS project.
|
11
|
+
get_ras_exe: Determine the HEC-RAS executable path based on the input.
|
12
|
+
|
13
|
+
DEVELOPER NOTE:
|
14
|
+
This class is used to initialize a RAS project and is used in conjunction with the RasCmdr class to manage the execution of RAS plans.
|
15
|
+
By default, the RasPrj class is initialized with the global 'ras' object.
|
16
|
+
However, you can create multiple RasPrj instances to manage multiple projects.
|
17
|
+
Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
|
18
|
+
|
19
|
+
This module is part of the ras-commander library and uses a centralized logging configuration.
|
20
|
+
|
21
|
+
Logging Configuration:
|
22
|
+
- The logging is set up in the logging_config.py file.
|
23
|
+
- A @log_call decorator is available to automatically log function calls.
|
24
|
+
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
25
|
+
- Logs are written to both console and a rotating file handler.
|
26
|
+
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
27
|
+
- The default log level is INFO.
|
28
|
+
|
29
|
+
To use logging in this module:
|
30
|
+
1. Use the @log_call decorator for automatic function call logging.
|
31
|
+
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
32
|
+
|
33
|
+
|
34
|
+
Example:
|
35
|
+
@log_call
|
36
|
+
def my_function():
|
37
|
+
|
38
|
+
logger.debug("Additional debug information")
|
39
|
+
# Function logic here
|
40
|
+
|
41
|
+
-----
|
42
|
+
|
43
|
+
All of the methods in this class are class methods and are designed to be used with instances of the class.
|
44
|
+
|
45
|
+
List of Functions in RasPrj:
|
46
|
+
- initialize()
|
47
|
+
- _load_project_data()
|
48
|
+
- _get_geom_file_for_plan()
|
49
|
+
- _parse_plan_file()
|
50
|
+
- _parse_unsteady_file()
|
51
|
+
- _get_prj_entries()
|
52
|
+
- _parse_boundary_condition()
|
53
|
+
- is_initialized (property)
|
54
|
+
- check_initialized()
|
55
|
+
- find_ras_prj()
|
56
|
+
- get_project_name()
|
57
|
+
- get_prj_entries()
|
58
|
+
- get_plan_entries()
|
59
|
+
- get_flow_entries()
|
60
|
+
- get_unsteady_entries()
|
61
|
+
- get_geom_entries()
|
62
|
+
- get_hdf_entries()
|
63
|
+
- print_data()
|
64
|
+
- get_plan_value()
|
65
|
+
- get_boundary_conditions()
|
66
|
+
|
67
|
+
Functions in RasPrj that are not part of the class:
|
68
|
+
- init_ras_project()
|
69
|
+
- get_ras_exe()
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
"""
|
75
|
+
import os
|
76
|
+
import re
|
77
|
+
from pathlib import Path
|
78
|
+
import pandas as pd
|
79
|
+
from typing import Union, Any, List, Dict, Tuple
|
80
|
+
import logging
|
81
|
+
from ras_commander.LoggingConfig import get_logger
|
82
|
+
from ras_commander.Decorators import log_call
|
83
|
+
|
84
|
+
logger = get_logger(__name__)
|
85
|
+
|
86
|
+
def read_file_with_fallback_encoding(file_path, encodings=['utf-8', 'latin1', 'cp1252', 'iso-8859-1']):
|
87
|
+
"""
|
88
|
+
Attempt to read a file using multiple encodings.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
file_path (str or Path): Path to the file to read
|
92
|
+
encodings (list): List of encodings to try, in order of preference
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
tuple: (content, encoding) or (None, None) if all encodings fail
|
96
|
+
"""
|
97
|
+
for encoding in encodings:
|
98
|
+
try:
|
99
|
+
with open(file_path, 'r', encoding=encoding) as file:
|
100
|
+
content = file.read()
|
101
|
+
return content, encoding
|
102
|
+
except UnicodeDecodeError:
|
103
|
+
continue
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Error reading file {file_path} with {encoding} encoding: {e}")
|
106
|
+
continue
|
107
|
+
|
108
|
+
logger.error(f"Failed to read file {file_path} with any of the attempted encodings: {encodings}")
|
109
|
+
return None, None
|
110
|
+
|
111
|
+
class RasPrj:
|
112
|
+
|
113
|
+
def __init__(self):
|
114
|
+
self.initialized = False
|
115
|
+
self.boundaries_df = None # New attribute to store boundary conditions
|
116
|
+
self.suppress_logging = False # Add suppress_logging as instance variable
|
117
|
+
|
118
|
+
@log_call
|
119
|
+
def initialize(self, project_folder, ras_exe_path, suppress_logging=True):
|
120
|
+
"""
|
121
|
+
Initialize a RasPrj instance.
|
122
|
+
|
123
|
+
This method sets up the RasPrj instance with the given project folder and RAS executable path.
|
124
|
+
It finds the project file, loads project data, sets the initialization flag, and now also
|
125
|
+
extracts boundary conditions.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
project_folder (str or Path): Path to the HEC-RAS project folder.
|
129
|
+
ras_exe_path (str or Path): Path to the HEC-RAS executable.
|
130
|
+
suppress_logging (bool): If True, suppresses initialization logging messages.
|
131
|
+
|
132
|
+
Raises:
|
133
|
+
ValueError: If no HEC-RAS project file is found in the specified folder.
|
134
|
+
|
135
|
+
Note:
|
136
|
+
This method is intended for internal use. External users should use the init_ras_project function instead.
|
137
|
+
"""
|
138
|
+
self.suppress_logging = suppress_logging # Store suppress_logging state
|
139
|
+
self.project_folder = Path(project_folder)
|
140
|
+
self.prj_file = self.find_ras_prj(self.project_folder)
|
141
|
+
if self.prj_file is None:
|
142
|
+
logger.error(f"No HEC-RAS project file found in {self.project_folder}")
|
143
|
+
raise ValueError(f"No HEC-RAS project file found in {self.project_folder}")
|
144
|
+
self.project_name = Path(self.prj_file).stem
|
145
|
+
self.ras_exe_path = ras_exe_path
|
146
|
+
self._load_project_data()
|
147
|
+
self.boundaries_df = self.get_boundary_conditions() # Extract boundary conditions
|
148
|
+
self.initialized = True
|
149
|
+
|
150
|
+
if not suppress_logging:
|
151
|
+
logger.info(f"Initialization complete for project: {self.project_name}")
|
152
|
+
logger.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
|
153
|
+
f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
|
154
|
+
f"Boundary conditions: {len(self.boundaries_df)}")
|
155
|
+
logger.info(f"Geometry HDF files found: {self.plan_df['Geom_File'].notna().sum()}")
|
156
|
+
|
157
|
+
@log_call
|
158
|
+
def _load_project_data(self):
|
159
|
+
"""
|
160
|
+
Load project data from the HEC-RAS project file.
|
161
|
+
|
162
|
+
This method initializes DataFrames for plan, flow, unsteady, and geometry entries
|
163
|
+
by calling the _get_prj_entries method for each entry type.
|
164
|
+
"""
|
165
|
+
# Initialize DataFrames
|
166
|
+
self.plan_df = self._get_prj_entries('Plan')
|
167
|
+
self.flow_df = self._get_prj_entries('Flow')
|
168
|
+
self.unsteady_df = self._get_prj_entries('Unsteady')
|
169
|
+
self.geom_df = self.get_geom_entries() # Use get_geom_entries instead of _get_prj_entries
|
170
|
+
|
171
|
+
# Add Geom_File to plan_df
|
172
|
+
self.plan_df['Geom_File'] = self.plan_df.apply(lambda row: self._get_geom_file_for_plan(row['plan_number']), axis=1)
|
173
|
+
|
174
|
+
|
175
|
+
def _get_geom_file_for_plan(self, plan_number):
|
176
|
+
"""
|
177
|
+
Get the geometry file path for a given plan number.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
plan_number (str): The plan number to find the geometry file for.
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
str: The full path to the geometry HDF file, or None if not found.
|
184
|
+
"""
|
185
|
+
plan_file_path = self.project_folder / f"{self.project_name}.p{plan_number}"
|
186
|
+
content, encoding = read_file_with_fallback_encoding(plan_file_path)
|
187
|
+
|
188
|
+
if content is None:
|
189
|
+
return None
|
190
|
+
|
191
|
+
try:
|
192
|
+
for line in content.splitlines():
|
193
|
+
if line.startswith("Geom File="):
|
194
|
+
geom_file = line.strip().split('=')[1]
|
195
|
+
geom_hdf_path = self.project_folder / f"{self.project_name}.{geom_file}.hdf"
|
196
|
+
if geom_hdf_path.exists():
|
197
|
+
return str(geom_hdf_path)
|
198
|
+
else:
|
199
|
+
return None
|
200
|
+
except Exception as e:
|
201
|
+
logger.error(f"Error reading plan file for geometry: {e}")
|
202
|
+
return None
|
203
|
+
|
204
|
+
|
205
|
+
@staticmethod
|
206
|
+
@log_call
|
207
|
+
def get_plan_value(
|
208
|
+
plan_number_or_path: Union[str, Path],
|
209
|
+
key: str,
|
210
|
+
ras_object=None
|
211
|
+
) -> Any:
|
212
|
+
"""
|
213
|
+
Retrieve a specific value from a HEC-RAS plan file.
|
214
|
+
|
215
|
+
Parameters:
|
216
|
+
plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
|
217
|
+
key (str): The key to retrieve from the plan file
|
218
|
+
ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
Any: The value associated with the specified key
|
222
|
+
|
223
|
+
Raises:
|
224
|
+
ValueError: If the plan file is not found
|
225
|
+
IOError: If there's an error reading the plan file
|
226
|
+
"""
|
227
|
+
logger = get_logger(__name__)
|
228
|
+
ras_obj = ras_object or ras
|
229
|
+
ras_obj.check_initialized()
|
230
|
+
|
231
|
+
# These must exactly match the keys in supported_plan_keys from _parse_plan_file
|
232
|
+
valid_keys = {
|
233
|
+
'Computation Interval',
|
234
|
+
'DSS File',
|
235
|
+
'Flow File',
|
236
|
+
'Friction Slope Method',
|
237
|
+
'Geom File',
|
238
|
+
'Mapping Interval',
|
239
|
+
'Plan Title',
|
240
|
+
'Program Version',
|
241
|
+
'Run HTab',
|
242
|
+
'Run PostProcess',
|
243
|
+
'Run Sediment',
|
244
|
+
'Run UNet',
|
245
|
+
'Run WQNet',
|
246
|
+
'Short Identifier',
|
247
|
+
'Simulation Date',
|
248
|
+
'UNET D1 Cores',
|
249
|
+
'UNET D2 Cores',
|
250
|
+
'PS Cores',
|
251
|
+
'UNET Use Existing IB Tables',
|
252
|
+
'UNET 1D Methodology',
|
253
|
+
'UNET D2 SolverType',
|
254
|
+
'UNET D2 Name',
|
255
|
+
'description' # Special case for description block
|
256
|
+
}
|
257
|
+
|
258
|
+
if key not in valid_keys:
|
259
|
+
logger.warning(f"Unknown key: {key}. Valid keys are: {', '.join(sorted(valid_keys))}")
|
260
|
+
return None
|
261
|
+
|
262
|
+
plan_file_path = Path(plan_number_or_path)
|
263
|
+
if not plan_file_path.is_file():
|
264
|
+
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
|
265
|
+
if not plan_file_path.exists():
|
266
|
+
logger.error(f"Plan file not found: {plan_file_path}")
|
267
|
+
raise ValueError(f"Plan file not found: {plan_file_path}")
|
268
|
+
|
269
|
+
try:
|
270
|
+
with open(plan_file_path, 'r') as file:
|
271
|
+
content = file.read()
|
272
|
+
except IOError as e:
|
273
|
+
logger.error(f"Error reading plan file {plan_file_path}: {e}")
|
274
|
+
raise
|
275
|
+
|
276
|
+
if key == 'description':
|
277
|
+
match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
278
|
+
return match.group(1).strip() if match else None
|
279
|
+
else:
|
280
|
+
pattern = f"{key}=(.*)"
|
281
|
+
match = re.search(pattern, content)
|
282
|
+
if match:
|
283
|
+
value = match.group(1).strip()
|
284
|
+
# Convert core values to integers
|
285
|
+
if key in ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']:
|
286
|
+
try:
|
287
|
+
return int(value)
|
288
|
+
except ValueError:
|
289
|
+
logger.warning(f"Could not convert {key} value '{value}' to integer")
|
290
|
+
return None
|
291
|
+
return value
|
292
|
+
|
293
|
+
# Use DEBUG level for missing core values, ERROR for other missing keys
|
294
|
+
if key in ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']:
|
295
|
+
logger.debug(f"Core setting '{key}' not found in plan file")
|
296
|
+
else:
|
297
|
+
logger.error(f"Key '{key}' not found in the plan file")
|
298
|
+
return None
|
299
|
+
|
300
|
+
def _parse_plan_file(self, plan_file_path):
|
301
|
+
"""
|
302
|
+
Parse a plan file and extract critical information.
|
303
|
+
|
304
|
+
Args:
|
305
|
+
plan_file_path (Path): Path to the plan file.
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
dict: Dictionary containing extracted plan information.
|
309
|
+
"""
|
310
|
+
plan_info = {}
|
311
|
+
content, encoding = read_file_with_fallback_encoding(plan_file_path)
|
312
|
+
|
313
|
+
if content is None:
|
314
|
+
logger.error(f"Could not read plan file {plan_file_path} with any supported encoding")
|
315
|
+
return plan_info
|
316
|
+
|
317
|
+
try:
|
318
|
+
# Extract description
|
319
|
+
description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
|
320
|
+
if description_match:
|
321
|
+
plan_info['description'] = description_match.group(1).strip()
|
322
|
+
|
323
|
+
# BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the plan file keys.
|
324
|
+
|
325
|
+
# Extract other critical information
|
326
|
+
supported_plan_keys = {
|
327
|
+
'Computation Interval': r'Computation Interval=(.+)',
|
328
|
+
'DSS File': r'DSS File=(.+)',
|
329
|
+
'Flow File': r'Flow File=(.+)',
|
330
|
+
'Friction Slope Method': r'Friction Slope Method=(.+)',
|
331
|
+
'Geom File': r'Geom File=(.+)',
|
332
|
+
'Mapping Interval': r'Mapping Interval=(.+)',
|
333
|
+
'Plan Title': r'Plan Title=(.+)',
|
334
|
+
'Program Version': r'Program Version=(.+)',
|
335
|
+
'Run HTab': r'Run HTab=(.+)',
|
336
|
+
'Run PostProcess': r'Run PostProcess=(.+)',
|
337
|
+
'Run Sediment': r'Run Sediment=(.+)',
|
338
|
+
'Run UNet': r'Run UNet=(.+)',
|
339
|
+
'Run WQNet': r'Run WQNet=(.+)',
|
340
|
+
'Short Identifier': r'Short Identifier=(.+)',
|
341
|
+
'Simulation Date': r'Simulation Date=(.+)',
|
342
|
+
'UNET D1 Cores': r'UNET D1 Cores=(.+)',
|
343
|
+
'UNET D2 Cores': r'UNET D2 Cores=(.+)',
|
344
|
+
'PS Cores': r'PS Cores=(.+)',
|
345
|
+
'UNET Use Existing IB Tables': r'UNET Use Existing IB Tables=(.+)',
|
346
|
+
'UNET 1D Methodology': r'UNET 1D Methodology=(.+)',
|
347
|
+
'UNET D2 SolverType': r'UNET D2 SolverType=(.+)',
|
348
|
+
'UNET D2 Name': r'UNET D2 Name=(.+)'
|
349
|
+
}
|
350
|
+
|
351
|
+
# END Exception to Style Guide
|
352
|
+
|
353
|
+
# First, explicitly set None for core values
|
354
|
+
core_keys = ['UNET D1 Cores', 'UNET D2 Cores', 'PS Cores']
|
355
|
+
for key in core_keys:
|
356
|
+
plan_info[key] = None
|
357
|
+
|
358
|
+
for key, pattern in supported_plan_keys.items():
|
359
|
+
match = re.search(pattern, content)
|
360
|
+
if match:
|
361
|
+
value = match.group(1).strip()
|
362
|
+
# Convert core values to integers if they exist
|
363
|
+
if key in core_keys and value:
|
364
|
+
try:
|
365
|
+
value = int(value)
|
366
|
+
except ValueError:
|
367
|
+
logger.warning(f"Could not convert {key} value '{value}' to integer in plan file {plan_file_path}")
|
368
|
+
value = None
|
369
|
+
plan_info[key] = value
|
370
|
+
elif key in core_keys:
|
371
|
+
logger.debug(f"Core setting '{key}' not found in plan file {plan_file_path}")
|
372
|
+
|
373
|
+
logger.debug(f"Parsed plan file: {plan_file_path} using {encoding} encoding")
|
374
|
+
except Exception as e:
|
375
|
+
logger.error(f"Error parsing plan file {plan_file_path}: {e}")
|
376
|
+
|
377
|
+
return plan_info
|
378
|
+
|
379
|
+
def _get_prj_entries(self, entry_type):
|
380
|
+
"""
|
381
|
+
Extract entries of a specific type from the HEC-RAS project file.
|
382
|
+
|
383
|
+
Args:
|
384
|
+
entry_type (str): The type of entry to extract (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
|
385
|
+
|
386
|
+
Returns:
|
387
|
+
pd.DataFrame: A DataFrame containing the extracted entries.
|
388
|
+
|
389
|
+
Note:
|
390
|
+
This method reads the project file and extracts entries matching the specified type.
|
391
|
+
For 'Unsteady' entries, it parses additional information from the unsteady file.
|
392
|
+
"""
|
393
|
+
entries = []
|
394
|
+
pattern = re.compile(rf"{entry_type} File=(\w+)")
|
395
|
+
|
396
|
+
try:
|
397
|
+
with open(self.prj_file, 'r') as file:
|
398
|
+
for line in file:
|
399
|
+
match = pattern.match(line.strip())
|
400
|
+
if match:
|
401
|
+
file_name = match.group(1)
|
402
|
+
full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
|
403
|
+
entry = {
|
404
|
+
f'{entry_type.lower()}_number': file_name[1:],
|
405
|
+
'full_path': full_path
|
406
|
+
}
|
407
|
+
|
408
|
+
if entry_type == 'Plan':
|
409
|
+
plan_info = self._parse_plan_file(Path(full_path))
|
410
|
+
entry.update(plan_info)
|
411
|
+
|
412
|
+
hdf_results_path = self.project_folder / f"{self.project_name}.p{file_name[1:]}.hdf"
|
413
|
+
entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
|
414
|
+
|
415
|
+
if entry_type == 'Unsteady':
|
416
|
+
unsteady_info = self._parse_unsteady_file(Path(full_path))
|
417
|
+
entry.update(unsteady_info)
|
418
|
+
|
419
|
+
entries.append(entry)
|
420
|
+
except Exception as e:
|
421
|
+
raise
|
422
|
+
|
423
|
+
return pd.DataFrame(entries)
|
424
|
+
|
425
|
+
def _parse_unsteady_file(self, unsteady_file_path):
|
426
|
+
"""
|
427
|
+
Parse an unsteady flow file and extract critical information.
|
428
|
+
|
429
|
+
Args:
|
430
|
+
unsteady_file_path (Path): Path to the unsteady flow file.
|
431
|
+
|
432
|
+
Returns:
|
433
|
+
dict: Dictionary containing extracted unsteady flow information.
|
434
|
+
"""
|
435
|
+
unsteady_info = {}
|
436
|
+
content, encoding = read_file_with_fallback_encoding(unsteady_file_path)
|
437
|
+
|
438
|
+
if content is None:
|
439
|
+
return unsteady_info
|
440
|
+
|
441
|
+
try:
|
442
|
+
# BEGIN Exception to Style Guide, this is needed to keep the key names consistent with the unsteady file keys.
|
443
|
+
|
444
|
+
supported_unsteady_keys = {
|
445
|
+
'Flow Title': r'Flow Title=(.+)',
|
446
|
+
'Program Version': r'Program Version=(.+)',
|
447
|
+
'Use Restart': r'Use Restart=(.+)',
|
448
|
+
'Precipitation Mode': r'Precipitation Mode=(.+)',
|
449
|
+
'Wind Mode': r'Wind Mode=(.+)',
|
450
|
+
'Met BC=Precipitation|Mode': r'Met BC=Precipitation\|Mode=(.+)',
|
451
|
+
'Met BC=Evapotranspiration|Mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
|
452
|
+
'Met BC=Precipitation|Expanded View': r'Met BC=Precipitation\|Expanded View=(.+)',
|
453
|
+
'Met BC=Precipitation|Constant Units': r'Met BC=Precipitation\|Constant Units=(.+)',
|
454
|
+
'Met BC=Precipitation|Gridded Source': r'Met BC=Precipitation\|Gridded Source=(.+)'
|
455
|
+
}
|
456
|
+
|
457
|
+
# END Exception to Style Guide
|
458
|
+
|
459
|
+
for key, pattern in supported_unsteady_keys.items():
|
460
|
+
match = re.search(pattern, content)
|
461
|
+
if match:
|
462
|
+
unsteady_info[key] = match.group(1).strip()
|
463
|
+
|
464
|
+
except Exception as e:
|
465
|
+
logger.error(f"Error parsing unsteady file {unsteady_file_path}: {e}")
|
466
|
+
|
467
|
+
return unsteady_info
|
468
|
+
|
469
|
+
@property
|
470
|
+
def is_initialized(self):
|
471
|
+
"""
|
472
|
+
Check if the RasPrj instance has been initialized.
|
473
|
+
|
474
|
+
Returns:
|
475
|
+
bool: True if the instance has been initialized, False otherwise.
|
476
|
+
"""
|
477
|
+
return self.initialized
|
478
|
+
|
479
|
+
@log_call
|
480
|
+
def check_initialized(self):
|
481
|
+
"""
|
482
|
+
Ensure that the RasPrj instance has been initialized.
|
483
|
+
|
484
|
+
Raises:
|
485
|
+
RuntimeError: If the project has not been initialized.
|
486
|
+
"""
|
487
|
+
if not self.initialized:
|
488
|
+
raise RuntimeError("Project not initialized. Call init_ras_project() first.")
|
489
|
+
|
490
|
+
@staticmethod
|
491
|
+
@log_call
|
492
|
+
def find_ras_prj(folder_path):
|
493
|
+
"""
|
494
|
+
Find the appropriate HEC-RAS project file (.prj) in the given folder.
|
495
|
+
|
496
|
+
Parameters:
|
497
|
+
folder_path (str or Path): Path to the folder containing HEC-RAS files.
|
498
|
+
|
499
|
+
Returns:
|
500
|
+
Path: The full path of the selected .prj file or None if no suitable file is found.
|
501
|
+
"""
|
502
|
+
folder_path = Path(folder_path)
|
503
|
+
prj_files = list(folder_path.glob("*.prj"))
|
504
|
+
rasmap_files = list(folder_path.glob("*.rasmap"))
|
505
|
+
if len(prj_files) == 1:
|
506
|
+
return prj_files[0].resolve()
|
507
|
+
if len(prj_files) > 1:
|
508
|
+
if len(rasmap_files) == 1:
|
509
|
+
base_filename = rasmap_files[0].stem
|
510
|
+
prj_file = folder_path / f"{base_filename}.prj"
|
511
|
+
if prj_file.exists():
|
512
|
+
return prj_file.resolve()
|
513
|
+
for prj_file in prj_files:
|
514
|
+
try:
|
515
|
+
with open(prj_file, 'r') as file:
|
516
|
+
content = file.read()
|
517
|
+
if "Proj Title=" in content:
|
518
|
+
return prj_file.resolve()
|
519
|
+
except Exception:
|
520
|
+
continue
|
521
|
+
return None
|
522
|
+
|
523
|
+
|
524
|
+
@log_call
|
525
|
+
def get_project_name(self):
|
526
|
+
"""
|
527
|
+
Get the name of the HEC-RAS project.
|
528
|
+
|
529
|
+
Returns:
|
530
|
+
str: The name of the project.
|
531
|
+
|
532
|
+
Raises:
|
533
|
+
RuntimeError: If the project has not been initialized.
|
534
|
+
"""
|
535
|
+
self.check_initialized()
|
536
|
+
return self.project_name
|
537
|
+
|
538
|
+
@log_call
|
539
|
+
def get_prj_entries(self, entry_type):
|
540
|
+
"""
|
541
|
+
Get entries of a specific type from the HEC-RAS project.
|
542
|
+
|
543
|
+
Args:
|
544
|
+
entry_type (str): The type of entry to retrieve (e.g., 'Plan', 'Flow', 'Unsteady', 'Geom').
|
545
|
+
|
546
|
+
Returns:
|
547
|
+
pd.DataFrame: A DataFrame containing the requested entries.
|
548
|
+
|
549
|
+
Raises:
|
550
|
+
RuntimeError: If the project has not been initialized.
|
551
|
+
"""
|
552
|
+
self.check_initialized()
|
553
|
+
return self._get_prj_entries(entry_type)
|
554
|
+
|
555
|
+
@log_call
|
556
|
+
def get_plan_entries(self):
|
557
|
+
"""
|
558
|
+
Get all plan entries from the HEC-RAS project.
|
559
|
+
|
560
|
+
Returns:
|
561
|
+
pd.DataFrame: A DataFrame containing all plan entries.
|
562
|
+
|
563
|
+
Raises:
|
564
|
+
RuntimeError: If the project has not been initialized.
|
565
|
+
"""
|
566
|
+
self.check_initialized()
|
567
|
+
return self._get_prj_entries('Plan')
|
568
|
+
|
569
|
+
@log_call
|
570
|
+
def get_flow_entries(self):
|
571
|
+
"""
|
572
|
+
Get all flow entries from the HEC-RAS project.
|
573
|
+
|
574
|
+
Returns:
|
575
|
+
pd.DataFrame: A DataFrame containing all flow entries.
|
576
|
+
|
577
|
+
Raises:
|
578
|
+
RuntimeError: If the project has not been initialized.
|
579
|
+
"""
|
580
|
+
self.check_initialized()
|
581
|
+
return self._get_prj_entries('Flow')
|
582
|
+
|
583
|
+
@log_call
|
584
|
+
def get_unsteady_entries(self):
|
585
|
+
"""
|
586
|
+
Get all unsteady flow entries from the HEC-RAS project.
|
587
|
+
|
588
|
+
Returns:
|
589
|
+
pd.DataFrame: A DataFrame containing all unsteady flow entries.
|
590
|
+
|
591
|
+
Raises:
|
592
|
+
RuntimeError: If the project has not been initialized.
|
593
|
+
"""
|
594
|
+
self.check_initialized()
|
595
|
+
return self._get_prj_entries('Unsteady')
|
596
|
+
|
597
|
+
@log_call
|
598
|
+
def get_geom_entries(self):
|
599
|
+
"""
|
600
|
+
Get geometry entries from the project file.
|
601
|
+
|
602
|
+
Returns:
|
603
|
+
pd.DataFrame: DataFrame containing geometry entries.
|
604
|
+
"""
|
605
|
+
geom_pattern = re.compile(r'Geom File=(\w+)')
|
606
|
+
geom_entries = []
|
607
|
+
|
608
|
+
try:
|
609
|
+
with open(self.prj_file, 'r') as f:
|
610
|
+
for line in f:
|
611
|
+
match = geom_pattern.search(line)
|
612
|
+
if match:
|
613
|
+
geom_entries.append(match.group(1))
|
614
|
+
|
615
|
+
geom_df = pd.DataFrame({'geom_file': geom_entries})
|
616
|
+
geom_df['geom_number'] = geom_df['geom_file'].str.extract(r'(\d+)$')
|
617
|
+
geom_df['full_path'] = geom_df['geom_file'].apply(lambda x: str(self.project_folder / f"{self.project_name}.{x}"))
|
618
|
+
geom_df['hdf_path'] = geom_df['full_path'] + ".hdf"
|
619
|
+
|
620
|
+
if not self.suppress_logging: # Only log if suppress_logging is False
|
621
|
+
logger.info(f"Found {len(geom_df)} geometry entries")
|
622
|
+
return geom_df
|
623
|
+
except Exception as e:
|
624
|
+
logger.error(f"Error reading geometry entries from project file: {e}")
|
625
|
+
raise
|
626
|
+
|
627
|
+
@log_call
|
628
|
+
def get_hdf_entries(self):
|
629
|
+
"""
|
630
|
+
Get HDF entries for plans that have results.
|
631
|
+
|
632
|
+
Returns:
|
633
|
+
pd.DataFrame: A DataFrame containing plan entries with HDF results.
|
634
|
+
Returns an empty DataFrame if no HDF entries are found.
|
635
|
+
"""
|
636
|
+
self.check_initialized()
|
637
|
+
|
638
|
+
hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
|
639
|
+
|
640
|
+
if hdf_entries.empty:
|
641
|
+
return pd.DataFrame(columns=self.plan_df.columns)
|
642
|
+
|
643
|
+
return hdf_entries
|
644
|
+
|
645
|
+
|
646
|
+
@log_call
|
647
|
+
def print_data(self):
|
648
|
+
"""Print all RAS Object data for this instance."""
|
649
|
+
self.check_initialized()
|
650
|
+
logger.info(f"--- Data for {self.project_name} ---")
|
651
|
+
logger.info(f"Project folder: {self.project_folder}")
|
652
|
+
logger.info(f"PRJ file: {self.prj_file}")
|
653
|
+
logger.info(f"HEC-RAS executable: {self.ras_exe_path}")
|
654
|
+
logger.info("Plan files:")
|
655
|
+
logger.info(f"\n{self.plan_df}")
|
656
|
+
logger.info("Flow files:")
|
657
|
+
logger.info(f"\n{self.flow_df}")
|
658
|
+
logger.info("Unsteady flow files:")
|
659
|
+
logger.info(f"\n{self.unsteady_df}")
|
660
|
+
logger.info("Geometry files:")
|
661
|
+
logger.info(f"\n{self.geom_df}")
|
662
|
+
logger.info("HDF entries:")
|
663
|
+
logger.info(f"\n{self.get_hdf_entries()}")
|
664
|
+
logger.info("Boundary conditions:")
|
665
|
+
logger.info(f"\n{self.boundaries_df}")
|
666
|
+
logger.info("----------------------------")
|
667
|
+
|
668
|
+
@log_call
|
669
|
+
def get_boundary_conditions(self) -> pd.DataFrame:
|
670
|
+
"""
|
671
|
+
Extract boundary conditions from unsteady flow files and create a DataFrame.
|
672
|
+
|
673
|
+
This method parses unsteady flow files to extract boundary condition information.
|
674
|
+
It creates a DataFrame with structured data for known boundary condition types
|
675
|
+
and parameters, and associates this information with the corresponding unsteady flow file.
|
676
|
+
|
677
|
+
Note:
|
678
|
+
Any lines in the boundary condition blocks that are not explicitly parsed and
|
679
|
+
incorporated into the DataFrame are captured in a multi-line string. This string
|
680
|
+
is logged at the DEBUG level for each boundary condition. This feature is crucial
|
681
|
+
for developers incorporating new boundary condition types or parameters, as it
|
682
|
+
allows them to see what information might be missing from the current parsing logic.
|
683
|
+
If no unsteady flow files are present, it returns an empty DataFrame.
|
684
|
+
|
685
|
+
Returns:
|
686
|
+
pd.DataFrame: A DataFrame containing detailed boundary condition information,
|
687
|
+
linked to the unsteady flow files.
|
688
|
+
|
689
|
+
Usage:
|
690
|
+
To see the unparsed lines, set the logging level to DEBUG before calling this method:
|
691
|
+
|
692
|
+
import logging
|
693
|
+
getLogger().setLevel(logging.DEBUG)
|
694
|
+
|
695
|
+
boundaries_df = ras_project.get_boundary_conditions()
|
696
|
+
linked to the unsteady flow files. Returns an empty DataFrame if
|
697
|
+
no unsteady flow files are present.
|
698
|
+
"""
|
699
|
+
boundary_data = []
|
700
|
+
|
701
|
+
# Check if unsteady_df is empty
|
702
|
+
if self.unsteady_df.empty:
|
703
|
+
logger.info("No unsteady flow files found in the project.")
|
704
|
+
return pd.DataFrame() # Return an empty DataFrame
|
705
|
+
|
706
|
+
for _, row in self.unsteady_df.iterrows():
|
707
|
+
unsteady_file_path = row['full_path']
|
708
|
+
unsteady_number = row['unsteady_number']
|
709
|
+
|
710
|
+
try:
|
711
|
+
with open(unsteady_file_path, 'r') as file:
|
712
|
+
content = file.read()
|
713
|
+
except IOError as e:
|
714
|
+
logger.error(f"Error reading unsteady file {unsteady_file_path}: {e}")
|
715
|
+
continue
|
716
|
+
|
717
|
+
bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
|
718
|
+
|
719
|
+
for i, block in enumerate(bc_blocks, 1):
|
720
|
+
bc_info, unparsed_lines = self._parse_boundary_condition(block, unsteady_number, i)
|
721
|
+
boundary_data.append(bc_info)
|
722
|
+
|
723
|
+
if unparsed_lines:
|
724
|
+
logger.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
725
|
+
|
726
|
+
if not boundary_data:
|
727
|
+
logger.info("No boundary conditions found in unsteady flow files.")
|
728
|
+
return pd.DataFrame() # Return an empty DataFrame if no boundary conditions were found
|
729
|
+
|
730
|
+
boundaries_df = pd.DataFrame(boundary_data)
|
731
|
+
|
732
|
+
# Merge with unsteady_df to get relevant unsteady flow file information
|
733
|
+
merged_df = pd.merge(boundaries_df, self.unsteady_df,
|
734
|
+
left_on='unsteady_number', right_on='unsteady_number', how='left')
|
735
|
+
|
736
|
+
return merged_df
|
737
|
+
|
738
|
+
def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
|
739
|
+
lines = block.split('\n')
|
740
|
+
bc_info = {
|
741
|
+
'unsteady_number': unsteady_number,
|
742
|
+
'boundary_condition_number': bc_number
|
743
|
+
}
|
744
|
+
|
745
|
+
parsed_lines = set()
|
746
|
+
|
747
|
+
# Parse Boundary Location
|
748
|
+
boundary_location = lines[0].split('=')[1].strip()
|
749
|
+
fields = [field.strip() for field in boundary_location.split(',')]
|
750
|
+
bc_info.update({
|
751
|
+
'river_reach_name': fields[0] if len(fields) > 0 else '',
|
752
|
+
'river_station': fields[1] if len(fields) > 1 else '',
|
753
|
+
'storage_area_name': fields[2] if len(fields) > 2 else '',
|
754
|
+
'pump_station_name': fields[3] if len(fields) > 3 else ''
|
755
|
+
})
|
756
|
+
parsed_lines.add(0)
|
757
|
+
|
758
|
+
# Determine BC Type
|
759
|
+
bc_types = {
|
760
|
+
'Flow Hydrograph=': 'Flow Hydrograph',
|
761
|
+
'Lateral Inflow Hydrograph=': 'Lateral Inflow Hydrograph',
|
762
|
+
'Uniform Lateral Inflow Hydrograph=': 'Uniform Lateral Inflow Hydrograph',
|
763
|
+
'Stage Hydrograph=': 'Stage Hydrograph',
|
764
|
+
'Friction Slope=': 'Normal Depth',
|
765
|
+
'Gate Name=': 'Gate Opening'
|
766
|
+
}
|
767
|
+
|
768
|
+
bc_info['bc_type'] = 'Unknown'
|
769
|
+
bc_info['hydrograph_type'] = None
|
770
|
+
for i, line in enumerate(lines[1:], 1):
|
771
|
+
for key, bc_type in bc_types.items():
|
772
|
+
if line.startswith(key):
|
773
|
+
bc_info['bc_type'] = bc_type
|
774
|
+
if 'Hydrograph' in bc_type:
|
775
|
+
bc_info['hydrograph_type'] = bc_type
|
776
|
+
parsed_lines.add(i)
|
777
|
+
break
|
778
|
+
if bc_info['bc_type'] != 'Unknown':
|
779
|
+
break
|
780
|
+
|
781
|
+
# Parse other fields
|
782
|
+
known_fields = ['Interval', 'DSS Path', 'Use DSS', 'Use Fixed Start Time', 'Fixed Start Date/Time',
|
783
|
+
'Is Critical Boundary', 'Critical Boundary Flow', 'DSS File']
|
784
|
+
for i, line in enumerate(lines):
|
785
|
+
if '=' in line:
|
786
|
+
key, value = line.split('=', 1)
|
787
|
+
key = key.strip()
|
788
|
+
if key in known_fields:
|
789
|
+
bc_info[key] = value.strip()
|
790
|
+
parsed_lines.add(i)
|
791
|
+
|
792
|
+
# Handle hydrograph values
|
793
|
+
bc_info['hydrograph_num_values'] = 0
|
794
|
+
if bc_info['hydrograph_type']:
|
795
|
+
hydrograph_key = f"{bc_info['hydrograph_type']}="
|
796
|
+
hydrograph_line = next((line for i, line in enumerate(lines) if line.startswith(hydrograph_key)), None)
|
797
|
+
if hydrograph_line:
|
798
|
+
hydrograph_index = lines.index(hydrograph_line)
|
799
|
+
values_count = int(hydrograph_line.split('=')[1].strip())
|
800
|
+
bc_info['hydrograph_num_values'] = values_count
|
801
|
+
if values_count > 0:
|
802
|
+
values = ' '.join(lines[hydrograph_index + 1:]).split()[:values_count]
|
803
|
+
bc_info['hydrograph_values'] = values
|
804
|
+
parsed_lines.update(range(hydrograph_index, hydrograph_index + (values_count // 5) + 2))
|
805
|
+
|
806
|
+
# Collect unparsed lines
|
807
|
+
unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
|
808
|
+
|
809
|
+
if unparsed_lines:
|
810
|
+
logger.debug(f"Unparsed lines for boundary condition {bc_number} in unsteady file {unsteady_number}:\n{unparsed_lines}")
|
811
|
+
|
812
|
+
return bc_info, unparsed_lines
|
813
|
+
|
814
|
+
|
815
|
+
# Create a global instance named 'ras'
|
816
|
+
# Defining the global instance allows the init_ras_project function to initialize the project.
|
817
|
+
# This only happens on the library initialization, not when the user calls init_ras_project.
|
818
|
+
ras = RasPrj()
|
819
|
+
|
820
|
+
# END OF CLASS DEFINITION
|
821
|
+
|
822
|
+
|
823
|
+
# START OF FUNCTION DEFINITIONS
|
824
|
+
|
825
|
+
@log_call
|
826
|
+
def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
|
827
|
+
"""
|
828
|
+
Initialize a RAS project.
|
829
|
+
|
830
|
+
USE THIS FUNCTION TO INITIALIZE A RAS PROJECT, NOT THE INITIALIZE METHOD OF THE RasPrj CLASS.
|
831
|
+
The initialize method of the RasPrj class only modifies the global 'ras' object.
|
832
|
+
|
833
|
+
Parameters:
|
834
|
+
-----------
|
835
|
+
ras_project_folder : str
|
836
|
+
The path to the RAS project folder.
|
837
|
+
ras_version : str, optional
|
838
|
+
The version of RAS to use (e.g., "6.6").
|
839
|
+
The version can also be a full path to the Ras.exe file.
|
840
|
+
If None, the function will attempt to use the version from the global 'ras' object or a default path.
|
841
|
+
ras_object : RasPrj, optional
|
842
|
+
An instance of RasPrj to initialize. If None, the global 'ras' object is used.
|
843
|
+
|
844
|
+
Returns:
|
845
|
+
--------
|
846
|
+
RasPrj
|
847
|
+
An initialized RasPrj instance.
|
848
|
+
"""
|
849
|
+
if not Path(ras_project_folder).exists():
|
850
|
+
logger.error(f"The specified RAS project folder does not exist: {ras_project_folder}")
|
851
|
+
raise FileNotFoundError(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
|
852
|
+
|
853
|
+
ras_exe_path = get_ras_exe(ras_version)
|
854
|
+
|
855
|
+
if ras_object is None:
|
856
|
+
logger.info("Initializing global 'ras' object via init_ras_project function.")
|
857
|
+
ras_object = ras
|
858
|
+
elif not isinstance(ras_object, RasPrj):
|
859
|
+
logger.error("Provided ras_object is not an instance of RasPrj.")
|
860
|
+
raise TypeError("ras_object must be an instance of RasPrj or None.")
|
861
|
+
|
862
|
+
# Initialize the RasPrj instance
|
863
|
+
ras_object.initialize(ras_project_folder, ras_exe_path)
|
864
|
+
|
865
|
+
logger.info(f"Project initialized. ras_object project folder: {ras_object.project_folder}")
|
866
|
+
return ras_object
|
867
|
+
|
868
|
+
@log_call
|
869
|
+
def get_ras_exe(ras_version=None):
|
870
|
+
"""
|
871
|
+
Determine the HEC-RAS executable path based on the input.
|
872
|
+
|
873
|
+
Args:
|
874
|
+
ras_version (str, optional): Either a version number or a full path to the HEC-RAS executable.
|
875
|
+
If None, the function will attempt to use the version from the global 'ras' object
|
876
|
+
or a default path.
|
877
|
+
|
878
|
+
Returns:
|
879
|
+
str: The full path to the HEC-RAS executable.
|
880
|
+
|
881
|
+
Raises:
|
882
|
+
ValueError: If the input is neither a valid version number nor a valid file path.
|
883
|
+
|
884
|
+
Notes:
|
885
|
+
- If ras_version is not provided, the function will first check the global 'ras' object for a path.
|
886
|
+
- If the global 'ras' object is not initialized or doesn't have a path, a default path will be used.
|
887
|
+
- The default path allows the library to function in environments without HEC-RAS installed.
|
888
|
+
- This enables the HEC-Commander GPT to operate without stopping, even if HEC-RAS is not present.
|
889
|
+
- End users MUST use logging to check for operational errors, as full code stops prevent the GPT from running.
|
890
|
+
"""
|
891
|
+
if ras_version is None:
|
892
|
+
if hasattr(ras, 'ras_exe_path') and ras.ras_exe_path:
|
893
|
+
logger.debug(f"Using HEC-RAS executable from global 'ras' object: {ras.ras_exe_path}")
|
894
|
+
return ras.ras_exe_path
|
895
|
+
else:
|
896
|
+
default_path = Path("Ras.exe")
|
897
|
+
logger.warning(f"No HEC-RAS version specified and global 'ras' object not initialized or missing ras_exe_path. Using default path: {default_path}. The RAS Commander (ras-commander) Library Assistant can ignore this error since it does not have HEC-RAS installed.")
|
898
|
+
return str(default_path)
|
899
|
+
|
900
|
+
ras_version_numbers = [
|
901
|
+
"6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
|
902
|
+
"5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
|
903
|
+
"4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
|
904
|
+
]
|
905
|
+
|
906
|
+
hecras_path = Path(ras_version)
|
907
|
+
|
908
|
+
if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
|
909
|
+
logger.debug(f"HEC-RAS executable found at specified path: {hecras_path}")
|
910
|
+
return str(hecras_path)
|
911
|
+
|
912
|
+
if ras_version in ras_version_numbers:
|
913
|
+
default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
|
914
|
+
if default_path.is_file():
|
915
|
+
logger.debug(f"HEC-RAS executable found at default path: {default_path}")
|
916
|
+
return str(default_path)
|
917
|
+
else:
|
918
|
+
logger.critical(f"HEC-RAS executable not found at the expected path: {default_path}")
|
919
|
+
|
920
|
+
try:
|
921
|
+
version_float = float(ras_version)
|
922
|
+
if version_float > max(float(v) for v in ras_version_numbers):
|
923
|
+
newer_version_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
|
924
|
+
if newer_version_path.is_file():
|
925
|
+
logger.debug(f"Newer version of HEC-RAS executable found at: {newer_version_path}")
|
926
|
+
return str(newer_version_path)
|
927
|
+
else:
|
928
|
+
logger.critical("Newer version of HEC-RAS was specified, but the executable was not found.")
|
929
|
+
except ValueError:
|
930
|
+
pass
|
931
|
+
|
932
|
+
logger.error(f"Invalid HEC-RAS version or path: {ras_version}, returning default path: {default_path}")
|
933
|
+
return str(default_path)
|
934
|
+
|