ras-commander 0.72.0__py3-none-any.whl → 0.73.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -83,34 +83,6 @@ class HdfInfiltration:
83
83
  def __init__(self):
84
84
  self.logger = logging.getLogger(__name__)
85
85
 
86
- @staticmethod
87
- def _get_table_info(hdf_file: h5py.File, table_path: str) -> Tuple[List[str], List[str], List[str]]:
88
- """Get column names and types from HDF table
89
-
90
- Args:
91
- hdf_file: Open HDF file object
92
- table_path: Path to table in HDF file
93
-
94
- Returns:
95
- Tuple of (column names, numpy dtypes, column descriptions)
96
- """
97
- if table_path not in hdf_file:
98
- return [], [], []
99
-
100
- dataset = hdf_file[table_path]
101
- dtype = dataset.dtype
102
-
103
- # Extract column names and types
104
- col_names = []
105
- col_types = []
106
- col_descs = []
107
-
108
- for name in dtype.names:
109
- col_names.append(name)
110
- col_types.append(dtype[name].str)
111
- col_descs.append(name) # Could be enhanced to get actual descriptions
112
-
113
- return col_names, col_types, col_descs
114
86
 
115
87
  @staticmethod
116
88
  @log_call
@@ -160,6 +132,15 @@ class HdfInfiltration:
160
132
  logger.error(f"Error reading infiltration data from {hdf_path}: {str(e)}")
161
133
  return None
162
134
 
135
+
136
+
137
+ # set_infiltration_baseoverrides goes here, once finalized tested and fixed.
138
+
139
+
140
+
141
+ # Since the infiltration base overrides are in the geometry file, the above functions work on the geometry files
142
+ # The below functions work on the infiltration layer HDF files. Changes only take effect if no base overrides are present.
143
+
163
144
  @staticmethod
164
145
  @log_call
165
146
  def get_infiltration_layer_data(hdf_path: Path) -> Optional[pd.DataFrame]:
@@ -201,6 +182,7 @@ class HdfInfiltration:
201
182
  except Exception as e:
202
183
  logger.error(f"Error reading infiltration layer data from {hdf_path}: {str(e)}")
203
184
  return None
185
+
204
186
 
205
187
  @staticmethod
206
188
  @log_call
@@ -276,6 +258,10 @@ class HdfInfiltration:
276
258
  except Exception as e:
277
259
  logger.error(f"Error setting infiltration layer data in {hdf_path}: {str(e)}")
278
260
  return None
261
+
262
+
263
+
264
+
279
265
  @staticmethod
280
266
  @standardize_input(file_type='geom_hdf')
281
267
  @log_call
@@ -320,9 +306,694 @@ class HdfInfiltration:
320
306
  logger.error(f"Error scaling infiltration data in {hdf_path}: {str(e)}")
321
307
  return None
322
308
 
309
+
310
+
311
+ # Need to reorganize these soil staatistics functions so they are more straightforward.
312
+
313
+
323
314
  @staticmethod
324
315
  @log_call
325
- @standardize_input
316
+ @standardize_input(file_type='geom_hdf')
317
+ def get_soils_raster_stats(
318
+ geom_hdf_path: Path,
319
+ soil_hdf_path: Path = None,
320
+ ras_object: Any = None
321
+ ) -> pd.DataFrame:
322
+ """
323
+ Calculate soil group statistics for each 2D flow area using the area's perimeter.
324
+
325
+ Parameters
326
+ ----------
327
+ geom_hdf_path : Path
328
+ Path to the HEC-RAS geometry HDF file containing the 2D flow areas
329
+ soil_hdf_path : Path, optional
330
+ Path to the soil HDF file. If None, uses soil_layer_path from rasmap_df
331
+ ras_object : Any, optional
332
+ Optional RAS object. If not provided, uses global ras instance
333
+
334
+ Returns
335
+ -------
336
+ pd.DataFrame
337
+ DataFrame with soil statistics for each 2D flow area, including:
338
+ - mesh_name: Name of the 2D flow area
339
+ - mukey: Soil mukey identifier
340
+ - percentage: Percentage of 2D flow area covered by this soil type
341
+ - area_sqm: Area in square meters
342
+ - area_acres: Area in acres
343
+ - area_sqmiles: Area in square miles
344
+
345
+ Notes
346
+ -----
347
+ Requires the rasterstats package to be installed.
348
+ """
349
+ try:
350
+ from rasterstats import zonal_stats
351
+ import shapely
352
+ import geopandas as gpd
353
+ import numpy as np
354
+ import tempfile
355
+ import os
356
+ except ImportError as e:
357
+ logger.error(f"Failed to import required package: {e}. Please run 'pip install rasterstats shapely geopandas'")
358
+ raise e
359
+
360
+ # Import here to avoid circular imports
361
+ from .HdfMesh import HdfMesh
362
+
363
+ # Get the soil HDF path
364
+ if soil_hdf_path is None:
365
+ if ras_object is None:
366
+ from .RasPrj import ras
367
+ ras_object = ras
368
+
369
+ # Try to get soil_layer_path from rasmap_df
370
+ try:
371
+ soil_hdf_path = Path(ras_object.rasmap_df.loc[0, 'soil_layer_path'][0])
372
+ if not soil_hdf_path.exists():
373
+ logger.warning(f"Soil HDF path from rasmap_df does not exist: {soil_hdf_path}")
374
+ return pd.DataFrame()
375
+ except (KeyError, IndexError, AttributeError, TypeError) as e:
376
+ logger.error(f"Error retrieving soil_layer_path from rasmap_df: {str(e)}")
377
+ return pd.DataFrame()
378
+
379
+ # Get infiltration map - pass as hdf_path to ensure standardize_input works correctly
380
+ try:
381
+ raster_map = HdfInfiltration.get_infiltration_map(hdf_path=soil_hdf_path, ras_object=ras_object)
382
+ if not raster_map:
383
+ logger.error(f"No infiltration map found in {soil_hdf_path}")
384
+ return pd.DataFrame()
385
+ except Exception as e:
386
+ logger.error(f"Error getting infiltration map: {str(e)}")
387
+ return pd.DataFrame()
388
+
389
+ # Get 2D flow areas
390
+ mesh_areas = HdfMesh.get_mesh_areas(geom_hdf_path)
391
+ if mesh_areas.empty:
392
+ logger.warning(f"No 2D flow areas found in {geom_hdf_path}")
393
+ return pd.DataFrame()
394
+
395
+ # Extract the raster data for analysis
396
+ tif_path = soil_hdf_path.with_suffix('.tif')
397
+ if not tif_path.exists():
398
+ logger.error(f"No raster file found at {tif_path}")
399
+ return pd.DataFrame()
400
+
401
+ # Read the raster data and info
402
+ import rasterio
403
+ with rasterio.open(tif_path) as src:
404
+ grid_data = src.read(1)
405
+
406
+ # Get transform directly from rasterio
407
+ transform = src.transform
408
+ no_data = src.nodata if src.nodata is not None else -9999
409
+
410
+ # List to store all results
411
+ all_results = []
412
+
413
+ # Calculate zonal statistics for each 2D flow area
414
+ for _, mesh_row in mesh_areas.iterrows():
415
+ mesh_name = mesh_row['mesh_name']
416
+ mesh_geom = mesh_row['geometry']
417
+
418
+ # Get zonal statistics directly using numpy array
419
+ try:
420
+ stats = zonal_stats(
421
+ mesh_geom,
422
+ grid_data,
423
+ affine=transform,
424
+ categorical=True,
425
+ nodata=no_data
426
+ )[0]
427
+
428
+ # Skip if no stats
429
+ if not stats:
430
+ logger.warning(f"No soil data found for 2D flow area: {mesh_name}")
431
+ continue
432
+
433
+ # Calculate total area and percentages
434
+ total_area_sqm = sum(stats.values())
435
+
436
+ # Process each mukey
437
+ for raster_val, area_sqm in stats.items():
438
+ # Skip NoData values
439
+ if raster_val is None or raster_val == no_data:
440
+ continue
441
+
442
+ try:
443
+ mukey = raster_map.get(int(raster_val), f"Unknown-{raster_val}")
444
+ except (ValueError, TypeError):
445
+ mukey = f"Unknown-{raster_val}"
446
+
447
+ percentage = (area_sqm / total_area_sqm) * 100 if total_area_sqm > 0 else 0
448
+
449
+ all_results.append({
450
+ 'mesh_name': mesh_name,
451
+ 'mukey': mukey,
452
+ 'percentage': percentage,
453
+ 'area_sqm': area_sqm,
454
+ 'area_acres': area_sqm * HdfInfiltration.SQM_TO_ACRE,
455
+ 'area_sqmiles': area_sqm * HdfInfiltration.SQM_TO_SQMILE
456
+ })
457
+ except Exception as e:
458
+ logger.error(f"Error calculating statistics for mesh {mesh_name}: {str(e)}")
459
+ continue
460
+
461
+ # Create DataFrame with results
462
+ results_df = pd.DataFrame(all_results)
463
+
464
+ # Sort by mesh_name and percentage (descending)
465
+ if not results_df.empty:
466
+ results_df = results_df.sort_values(['mesh_name', 'percentage'], ascending=[True, False])
467
+
468
+ return results_df
469
+
470
+
471
+
472
+
473
+
474
+
475
+ @staticmethod
476
+ @log_call
477
+ @standardize_input(file_type='geom_hdf')
478
+ def get_soil_raster_stats(
479
+ geom_hdf_path: Path,
480
+ landcover_hdf_path: Path = None,
481
+ soil_hdf_path: Path = None,
482
+ ras_object: Any = None
483
+ ) -> pd.DataFrame:
484
+ """
485
+ Calculate combined land cover and soil infiltration statistics for each 2D flow area.
486
+
487
+ This function processes both land cover and soil data to calculate statistics
488
+ for each combination (Land Cover : Soil Type) within each 2D flow area.
489
+
490
+ Parameters
491
+ ----------
492
+ geom_hdf_path : Path
493
+ Path to the HEC-RAS geometry HDF file containing the 2D flow areas
494
+ landcover_hdf_path : Path, optional
495
+ Path to the land cover HDF file. If None, uses landcover_hdf_path from rasmap_df
496
+ soil_hdf_path : Path, optional
497
+ Path to the soil HDF file. If None, uses soil_layer_path from rasmap_df
498
+ ras_object : Any, optional
499
+ Optional RAS object. If not provided, uses global ras instance
500
+
501
+ Returns
502
+ -------
503
+ pd.DataFrame
504
+ DataFrame with combined statistics for each 2D flow area, including:
505
+ - mesh_name: Name of the 2D flow area
506
+ - combined_type: Combined land cover and soil type (e.g. "Mixed Forest : B")
507
+ - percentage: Percentage of 2D flow area covered by this combination
508
+ - area_sqm: Area in square meters
509
+ - area_acres: Area in acres
510
+ - area_sqmiles: Area in square miles
511
+ - curve_number: Curve number for this combination
512
+ - abstraction_ratio: Abstraction ratio for this combination
513
+ - min_infiltration_rate: Minimum infiltration rate for this combination
514
+
515
+ Notes
516
+ -----
517
+ Requires the rasterstats package to be installed.
518
+ """
519
+ try:
520
+ from rasterstats import zonal_stats
521
+ import shapely
522
+ import geopandas as gpd
523
+ import numpy as np
524
+ import tempfile
525
+ import os
526
+ import rasterio
527
+ from rasterio.merge import merge
528
+ except ImportError as e:
529
+ logger.error(f"Failed to import required package: {e}. Please run 'pip install rasterstats shapely geopandas rasterio'")
530
+ raise e
531
+
532
+ # Import here to avoid circular imports
533
+ from .HdfMesh import HdfMesh
534
+
535
+ # Get RAS object
536
+ if ras_object is None:
537
+ from .RasPrj import ras
538
+ ras_object = ras
539
+
540
+ # Get the landcover HDF path
541
+ if landcover_hdf_path is None:
542
+ try:
543
+ landcover_hdf_path = Path(ras_object.rasmap_df.loc[0, 'landcover_hdf_path'][0])
544
+ if not landcover_hdf_path.exists():
545
+ logger.warning(f"Land cover HDF path from rasmap_df does not exist: {landcover_hdf_path}")
546
+ return pd.DataFrame()
547
+ except (KeyError, IndexError, AttributeError, TypeError) as e:
548
+ logger.error(f"Error retrieving landcover_hdf_path from rasmap_df: {str(e)}")
549
+ return pd.DataFrame()
550
+
551
+ # Get the soil HDF path
552
+ if soil_hdf_path is None:
553
+ try:
554
+ soil_hdf_path = Path(ras_object.rasmap_df.loc[0, 'soil_layer_path'][0])
555
+ if not soil_hdf_path.exists():
556
+ logger.warning(f"Soil HDF path from rasmap_df does not exist: {soil_hdf_path}")
557
+ return pd.DataFrame()
558
+ except (KeyError, IndexError, AttributeError, TypeError) as e:
559
+ logger.error(f"Error retrieving soil_layer_path from rasmap_df: {str(e)}")
560
+ return pd.DataFrame()
561
+
562
+ # Get land cover map (raster to ID mapping)
563
+ try:
564
+ with h5py.File(landcover_hdf_path, 'r') as hdf:
565
+ if '//Raster Map' not in hdf:
566
+ logger.error(f"No Raster Map found in {landcover_hdf_path}")
567
+ return pd.DataFrame()
568
+
569
+ landcover_map_data = hdf['//Raster Map'][()]
570
+ landcover_map = {int(item[0]): item[1].decode('utf-8').strip() for item in landcover_map_data}
571
+ except Exception as e:
572
+ logger.error(f"Error reading land cover data from HDF: {str(e)}")
573
+ return pd.DataFrame()
574
+
575
+ # Get soil map (raster to ID mapping)
576
+ try:
577
+ soil_map = HdfInfiltration.get_infiltration_map(hdf_path=soil_hdf_path, ras_object=ras_object)
578
+ if not soil_map:
579
+ logger.error(f"No soil map found in {soil_hdf_path}")
580
+ return pd.DataFrame()
581
+ except Exception as e:
582
+ logger.error(f"Error getting soil map: {str(e)}")
583
+ return pd.DataFrame()
584
+
585
+ # Get infiltration parameters
586
+ try:
587
+ infiltration_params = HdfInfiltration.get_infiltration_layer_data(soil_hdf_path)
588
+ if infiltration_params is None or infiltration_params.empty:
589
+ logger.warning(f"No infiltration parameters found in {soil_hdf_path}")
590
+ infiltration_params = pd.DataFrame(columns=['Name', 'Curve Number', 'Abstraction Ratio', 'Minimum Infiltration Rate'])
591
+ except Exception as e:
592
+ logger.error(f"Error getting infiltration parameters: {str(e)}")
593
+ infiltration_params = pd.DataFrame(columns=['Name', 'Curve Number', 'Abstraction Ratio', 'Minimum Infiltration Rate'])
594
+
595
+ # Get 2D flow areas
596
+ mesh_areas = HdfMesh.get_mesh_areas(geom_hdf_path)
597
+ if mesh_areas.empty:
598
+ logger.warning(f"No 2D flow areas found in {geom_hdf_path}")
599
+ return pd.DataFrame()
600
+
601
+ # Check for the TIF files with same name as HDF
602
+ landcover_tif_path = landcover_hdf_path.with_suffix('.tif')
603
+ soil_tif_path = soil_hdf_path.with_suffix('.tif')
604
+
605
+ if not landcover_tif_path.exists():
606
+ logger.error(f"No land cover raster file found at {landcover_tif_path}")
607
+ return pd.DataFrame()
608
+
609
+ if not soil_tif_path.exists():
610
+ logger.error(f"No soil raster file found at {soil_tif_path}")
611
+ return pd.DataFrame()
612
+
613
+ # List to store all results
614
+ all_results = []
615
+
616
+ # Read the raster data
617
+ try:
618
+ with rasterio.open(landcover_tif_path) as landcover_src, rasterio.open(soil_tif_path) as soil_src:
619
+ landcover_nodata = landcover_src.nodata if landcover_src.nodata is not None else -9999
620
+ soil_nodata = soil_src.nodata if soil_src.nodata is not None else -9999
621
+
622
+ # Calculate zonal statistics for each 2D flow area
623
+ for _, mesh_row in mesh_areas.iterrows():
624
+ mesh_name = mesh_row['mesh_name']
625
+ mesh_geom = mesh_row['geometry']
626
+
627
+ # Get zonal statistics for land cover
628
+ try:
629
+ landcover_stats = zonal_stats(
630
+ mesh_geom,
631
+ landcover_tif_path,
632
+ categorical=True,
633
+ nodata=landcover_nodata
634
+ )[0]
635
+
636
+ # Get zonal statistics for soil
637
+ soil_stats = zonal_stats(
638
+ mesh_geom,
639
+ soil_tif_path,
640
+ categorical=True,
641
+ nodata=soil_nodata
642
+ )[0]
643
+
644
+ # Skip if no stats
645
+ if not landcover_stats or not soil_stats:
646
+ logger.warning(f"No land cover or soil data found for 2D flow area: {mesh_name}")
647
+ continue
648
+
649
+ # Calculate total area
650
+ landcover_total = sum(landcover_stats.values())
651
+ soil_total = sum(soil_stats.values())
652
+
653
+ # Create a cross-tabulation of land cover and soil types
654
+ # This is an approximation since we don't have the exact pixel-by-pixel overlap
655
+ mesh_area_sqm = mesh_row['geometry'].area
656
+
657
+ # Calculate percentage of each land cover type
658
+ landcover_pct = {k: v/landcover_total for k, v in landcover_stats.items() if k is not None and k != landcover_nodata}
659
+
660
+ # Calculate percentage of each soil type
661
+ soil_pct = {k: v/soil_total for k, v in soil_stats.items() if k is not None and k != soil_nodata}
662
+
663
+ # Generate combinations
664
+ for lc_id, lc_pct in landcover_pct.items():
665
+ lc_name = landcover_map.get(int(lc_id), f"Unknown-{lc_id}")
666
+
667
+ for soil_id, soil_pct in soil_pct.items():
668
+ try:
669
+ soil_name = soil_map.get(int(soil_id), f"Unknown-{soil_id}")
670
+ except (ValueError, TypeError):
671
+ soil_name = f"Unknown-{soil_id}"
672
+
673
+ # Calculate combined percentage (approximate)
674
+ # This is a simplification; actual overlap would require pixel-by-pixel analysis
675
+ combined_pct = lc_pct * soil_pct * 100
676
+ combined_area_sqm = mesh_area_sqm * (combined_pct / 100)
677
+
678
+ # Create combined name
679
+ combined_name = f"{lc_name} : {soil_name}"
680
+
681
+ # Look up infiltration parameters
682
+ param_row = infiltration_params[infiltration_params['Name'] == combined_name]
683
+ if param_row.empty:
684
+ # Try with NoData for soil type
685
+ param_row = infiltration_params[infiltration_params['Name'] == f"{lc_name} : NoData"]
686
+
687
+ if not param_row.empty:
688
+ curve_number = param_row.iloc[0]['Curve Number']
689
+ abstraction_ratio = param_row.iloc[0]['Abstraction Ratio']
690
+ min_infiltration_rate = param_row.iloc[0]['Minimum Infiltration Rate']
691
+ else:
692
+ curve_number = None
693
+ abstraction_ratio = None
694
+ min_infiltration_rate = None
695
+
696
+ all_results.append({
697
+ 'mesh_name': mesh_name,
698
+ 'combined_type': combined_name,
699
+ 'percentage': combined_pct,
700
+ 'area_sqm': combined_area_sqm,
701
+ 'area_acres': combined_area_sqm * HdfInfiltration.SQM_TO_ACRE,
702
+ 'area_sqmiles': combined_area_sqm * HdfInfiltration.SQM_TO_SQMILE,
703
+ 'curve_number': curve_number,
704
+ 'abstraction_ratio': abstraction_ratio,
705
+ 'min_infiltration_rate': min_infiltration_rate
706
+ })
707
+ except Exception as e:
708
+ logger.error(f"Error calculating statistics for mesh {mesh_name}: {str(e)}")
709
+ continue
710
+ except Exception as e:
711
+ logger.error(f"Error opening raster files: {str(e)}")
712
+ return pd.DataFrame()
713
+
714
+ # Create DataFrame with results
715
+ results_df = pd.DataFrame(all_results)
716
+
717
+ # Sort by mesh_name, percentage (descending)
718
+ if not results_df.empty:
719
+ results_df = results_df.sort_values(['mesh_name', 'percentage'], ascending=[True, False])
720
+
721
+ return results_df
722
+
723
+
724
+
725
+
726
+
727
+
728
+ @staticmethod
729
+ @log_call
730
+ @standardize_input(file_type='geom_hdf')
731
+ def get_infiltration_stats(
732
+ geom_hdf_path: Path,
733
+ landcover_hdf_path: Path = None,
734
+ soil_hdf_path: Path = None,
735
+ ras_object: Any = None
736
+ ) -> pd.DataFrame:
737
+ """
738
+ Calculate combined land cover and soil infiltration statistics for each 2D flow area.
739
+
740
+ This function processes both land cover and soil data to calculate statistics
741
+ for each combination (Land Cover : Soil Type) within each 2D flow area.
742
+
743
+ Parameters
744
+ ----------
745
+ geom_hdf_path : Path
746
+ Path to the HEC-RAS geometry HDF file containing the 2D flow areas
747
+ landcover_hdf_path : Path, optional
748
+ Path to the land cover HDF file. If None, uses landcover_hdf_path from rasmap_df
749
+ soil_hdf_path : Path, optional
750
+ Path to the soil HDF file. If None, uses soil_layer_path from rasmap_df
751
+ ras_object : Any, optional
752
+ Optional RAS object. If not provided, uses global ras instance
753
+
754
+ Returns
755
+ -------
756
+ pd.DataFrame
757
+ DataFrame with combined statistics for each 2D flow area, including:
758
+ - mesh_name: Name of the 2D flow area
759
+ - combined_type: Combined land cover and soil type (e.g. "Mixed Forest : B")
760
+ - percentage: Percentage of 2D flow area covered by this combination
761
+ - area_sqm: Area in square meters
762
+ - area_acres: Area in acres
763
+ - area_sqmiles: Area in square miles
764
+ - curve_number: Curve number for this combination
765
+ - abstraction_ratio: Abstraction ratio for this combination
766
+ - min_infiltration_rate: Minimum infiltration rate for this combination
767
+
768
+ Notes
769
+ -----
770
+ Requires the rasterstats package to be installed.
771
+ """
772
+ try:
773
+ from rasterstats import zonal_stats
774
+ import shapely
775
+ import geopandas as gpd
776
+ import numpy as np
777
+ import tempfile
778
+ import os
779
+ import rasterio
780
+ from rasterio.merge import merge
781
+ except ImportError as e:
782
+ logger.error(f"Failed to import required package: {e}. Please run 'pip install rasterstats shapely geopandas rasterio'")
783
+ raise e
784
+
785
+ # Import here to avoid circular imports
786
+ from .HdfMesh import HdfMesh
787
+
788
+ # Get RAS object
789
+ if ras_object is None:
790
+ from .RasPrj import ras
791
+ ras_object = ras
792
+
793
+ # Get the landcover HDF path
794
+ if landcover_hdf_path is None:
795
+ try:
796
+ landcover_hdf_path = Path(ras_object.rasmap_df.loc[0, 'landcover_hdf_path'][0])
797
+ if not landcover_hdf_path.exists():
798
+ logger.warning(f"Land cover HDF path from rasmap_df does not exist: {landcover_hdf_path}")
799
+ return pd.DataFrame()
800
+ except (KeyError, IndexError, AttributeError, TypeError) as e:
801
+ logger.error(f"Error retrieving landcover_hdf_path from rasmap_df: {str(e)}")
802
+ return pd.DataFrame()
803
+
804
+ # Get the soil HDF path
805
+ if soil_hdf_path is None:
806
+ try:
807
+ soil_hdf_path = Path(ras_object.rasmap_df.loc[0, 'soil_layer_path'][0])
808
+ if not soil_hdf_path.exists():
809
+ logger.warning(f"Soil HDF path from rasmap_df does not exist: {soil_hdf_path}")
810
+ return pd.DataFrame()
811
+ except (KeyError, IndexError, AttributeError, TypeError) as e:
812
+ logger.error(f"Error retrieving soil_layer_path from rasmap_df: {str(e)}")
813
+ return pd.DataFrame()
814
+
815
+ # Get land cover map (raster to ID mapping)
816
+ try:
817
+ with h5py.File(landcover_hdf_path, 'r') as hdf:
818
+ if '//Raster Map' not in hdf:
819
+ logger.error(f"No Raster Map found in {landcover_hdf_path}")
820
+ return pd.DataFrame()
821
+
822
+ landcover_map_data = hdf['//Raster Map'][()]
823
+ landcover_map = {int(item[0]): item[1].decode('utf-8').strip() for item in landcover_map_data}
824
+ except Exception as e:
825
+ logger.error(f"Error reading land cover data from HDF: {str(e)}")
826
+ return pd.DataFrame()
827
+
828
+ # Get soil map (raster to ID mapping)
829
+ try:
830
+ soil_map = HdfInfiltration.get_infiltration_map(hdf_path=soil_hdf_path, ras_object=ras_object)
831
+ if not soil_map:
832
+ logger.error(f"No soil map found in {soil_hdf_path}")
833
+ return pd.DataFrame()
834
+ except Exception as e:
835
+ logger.error(f"Error getting soil map: {str(e)}")
836
+ return pd.DataFrame()
837
+
838
+ # Get infiltration parameters
839
+ try:
840
+ infiltration_params = HdfInfiltration.get_infiltration_layer_data(soil_hdf_path)
841
+ if infiltration_params is None or infiltration_params.empty:
842
+ logger.warning(f"No infiltration parameters found in {soil_hdf_path}")
843
+ infiltration_params = pd.DataFrame(columns=['Name', 'Curve Number', 'Abstraction Ratio', 'Minimum Infiltration Rate'])
844
+ except Exception as e:
845
+ logger.error(f"Error getting infiltration parameters: {str(e)}")
846
+ infiltration_params = pd.DataFrame(columns=['Name', 'Curve Number', 'Abstraction Ratio', 'Minimum Infiltration Rate'])
847
+
848
+ # Get 2D flow areas
849
+ mesh_areas = HdfMesh.get_mesh_areas(geom_hdf_path)
850
+ if mesh_areas.empty:
851
+ logger.warning(f"No 2D flow areas found in {geom_hdf_path}")
852
+ return pd.DataFrame()
853
+
854
+ # Check for the TIF files with same name as HDF
855
+ landcover_tif_path = landcover_hdf_path.with_suffix('.tif')
856
+ soil_tif_path = soil_hdf_path.with_suffix('.tif')
857
+
858
+ if not landcover_tif_path.exists():
859
+ logger.error(f"No land cover raster file found at {landcover_tif_path}")
860
+ return pd.DataFrame()
861
+
862
+ if not soil_tif_path.exists():
863
+ logger.error(f"No soil raster file found at {soil_tif_path}")
864
+ return pd.DataFrame()
865
+
866
+ # List to store all results
867
+ all_results = []
868
+
869
+ # Read the raster data
870
+ try:
871
+ with rasterio.open(landcover_tif_path) as landcover_src, rasterio.open(soil_tif_path) as soil_src:
872
+ landcover_nodata = landcover_src.nodata if landcover_src.nodata is not None else -9999
873
+ soil_nodata = soil_src.nodata if soil_src.nodata is not None else -9999
874
+
875
+ # Calculate zonal statistics for each 2D flow area
876
+ for _, mesh_row in mesh_areas.iterrows():
877
+ mesh_name = mesh_row['mesh_name']
878
+ mesh_geom = mesh_row['geometry']
879
+
880
+ # Get zonal statistics for land cover
881
+ try:
882
+ landcover_stats = zonal_stats(
883
+ mesh_geom,
884
+ landcover_tif_path,
885
+ categorical=True,
886
+ nodata=landcover_nodata
887
+ )[0]
888
+
889
+ # Get zonal statistics for soil
890
+ soil_stats = zonal_stats(
891
+ mesh_geom,
892
+ soil_tif_path,
893
+ categorical=True,
894
+ nodata=soil_nodata
895
+ )[0]
896
+
897
+ # Skip if no stats
898
+ if not landcover_stats or not soil_stats:
899
+ logger.warning(f"No land cover or soil data found for 2D flow area: {mesh_name}")
900
+ continue
901
+
902
+ # Calculate total area
903
+ landcover_total = sum(landcover_stats.values())
904
+ soil_total = sum(soil_stats.values())
905
+
906
+ # Create a cross-tabulation of land cover and soil types
907
+ # This is an approximation since we don't have the exact pixel-by-pixel overlap
908
+ mesh_area_sqm = mesh_row['geometry'].area
909
+
910
+ # Calculate percentage of each land cover type
911
+ landcover_pct = {k: v/landcover_total for k, v in landcover_stats.items() if k is not None and k != landcover_nodata}
912
+
913
+ # Calculate percentage of each soil type
914
+ soil_pct = {k: v/soil_total for k, v in soil_stats.items() if k is not None and k != soil_nodata}
915
+
916
+ # Generate combinations
917
+ for lc_id, lc_pct in landcover_pct.items():
918
+ lc_name = landcover_map.get(int(lc_id), f"Unknown-{lc_id}")
919
+
920
+ for soil_id, soil_pct in soil_pct.items():
921
+ try:
922
+ soil_name = soil_map.get(int(soil_id), f"Unknown-{soil_id}")
923
+ except (ValueError, TypeError):
924
+ soil_name = f"Unknown-{soil_id}"
925
+
926
+ # Calculate combined percentage (approximate)
927
+ # This is a simplification; actual overlap would require pixel-by-pixel analysis
928
+ combined_pct = lc_pct * soil_pct * 100
929
+ combined_area_sqm = mesh_area_sqm * (combined_pct / 100)
930
+
931
+ # Create combined name
932
+ combined_name = f"{lc_name} : {soil_name}"
933
+
934
+ # Look up infiltration parameters
935
+ param_row = infiltration_params[infiltration_params['Name'] == combined_name]
936
+ if param_row.empty:
937
+ # Try with NoData for soil type
938
+ param_row = infiltration_params[infiltration_params['Name'] == f"{lc_name} : NoData"]
939
+
940
+ if not param_row.empty:
941
+ curve_number = param_row.iloc[0]['Curve Number']
942
+ abstraction_ratio = param_row.iloc[0]['Abstraction Ratio']
943
+ min_infiltration_rate = param_row.iloc[0]['Minimum Infiltration Rate']
944
+ else:
945
+ curve_number = None
946
+ abstraction_ratio = None
947
+ min_infiltration_rate = None
948
+
949
+ all_results.append({
950
+ 'mesh_name': mesh_name,
951
+ 'combined_type': combined_name,
952
+ 'percentage': combined_pct,
953
+ 'area_sqm': combined_area_sqm,
954
+ 'area_acres': combined_area_sqm * HdfInfiltration.SQM_TO_ACRE,
955
+ 'area_sqmiles': combined_area_sqm * HdfInfiltration.SQM_TO_SQMILE,
956
+ 'curve_number': curve_number,
957
+ 'abstraction_ratio': abstraction_ratio,
958
+ 'min_infiltration_rate': min_infiltration_rate
959
+ })
960
+ except Exception as e:
961
+ logger.error(f"Error calculating statistics for mesh {mesh_name}: {str(e)}")
962
+ continue
963
+ except Exception as e:
964
+ logger.error(f"Error opening raster files: {str(e)}")
965
+ return pd.DataFrame()
966
+
967
+ # Create DataFrame with results
968
+ results_df = pd.DataFrame(all_results)
969
+
970
+ # Sort by mesh_name, percentage (descending)
971
+ if not results_df.empty:
972
+ results_df = results_df.sort_values(['mesh_name', 'percentage'], ascending=[True, False])
973
+
974
+ return results_df
975
+
976
+
977
+
978
+
979
+
980
+
981
+
982
+
983
+
984
+
985
+
986
+
987
+
988
+
989
+
990
+
991
+
992
+
993
+
994
+ @staticmethod
995
+ @log_call
996
+ @standardize_input(file_type='geom_hdf')
326
997
  def get_infiltration_map(hdf_path: Path = None, ras_object: Any = None) -> dict:
327
998
  """Read the infiltration raster map from HDF file
328
999
 
@@ -502,55 +1173,218 @@ class HdfInfiltration:
502
1173
  return weighted_params
503
1174
 
504
1175
 
1176
+ @staticmethod
1177
+ def _get_table_info(hdf_file: h5py.File, table_path: str) -> Tuple[List[str], List[str], List[str]]:
1178
+ """Get column names and types from HDF table
1179
+
1180
+ Args:
1181
+ hdf_file: Open HDF file object
1182
+ table_path: Path to table in HDF file
1183
+
1184
+ Returns:
1185
+ Tuple of (column names, numpy dtypes, column descriptions)
1186
+ """
1187
+ if table_path not in hdf_file:
1188
+ return [], [], []
1189
+
1190
+ dataset = hdf_file[table_path]
1191
+ dtype = dataset.dtype
1192
+
1193
+ # Extract column names and types
1194
+ col_names = []
1195
+ col_types = []
1196
+ col_descs = []
1197
+
1198
+ for name in dtype.names:
1199
+ col_names.append(name)
1200
+ col_types.append(dtype[name].str)
1201
+ col_descs.append(name) # Could be enhanced to get actual descriptions
1202
+
1203
+ return col_names, col_types, col_descs
505
1204
 
506
1205
 
507
-
508
-
509
- # Example usage:
510
- """
511
- from pathlib import Path
512
-
513
- # Initialize paths
514
- raster_path = Path('input_files/gSSURGO_InfiltrationDC.tif')
515
- boundary_path = Path('input_files/WF_Boundary_Simple.shp')
516
- hdf_path = raster_path.with_suffix('.hdf')
517
-
518
- # Get infiltration mapping
519
- infil_map = HdfInfiltration.get_infiltration_map(hdf_path)
520
-
521
- # Get zonal statistics (using RasMapper class)
522
- clipped_data, transform, nodata = RasMapper.clip_raster_with_boundary(
523
- raster_path, boundary_path)
524
- stats = RasMapper.calculate_zonal_stats(
525
- boundary_path, clipped_data, transform, nodata)
526
-
527
- # Calculate soil statistics
528
- soil_stats = HdfInfiltration.calculate_soil_statistics(stats, infil_map)
529
-
530
- # Get significant mukeys (>1%)
531
- significant = HdfInfiltration.get_significant_mukeys(soil_stats, threshold=1.0)
532
-
533
- # Calculate total percentage of significant mukeys
534
- total_significant = HdfInfiltration.calculate_total_significant_percentage(significant)
535
- print(f"Total percentage of significant mukeys: {total_significant}%")
536
-
537
- # Get infiltration parameters for each significant mukey
538
- infiltration_params = {}
539
- for mukey in significant['mukey']:
540
- params = HdfInfiltration.get_infiltration_parameters(hdf_path, mukey)
541
- if params:
542
- infiltration_params[mukey] = params
543
-
544
- # Calculate weighted parameters
545
- weighted_params = HdfInfiltration.calculate_weighted_parameters(
546
- significant, infiltration_params)
547
- print("Weighted infiltration parameters:", weighted_params)
548
-
549
- # Save results
550
- HdfInfiltration.save_statistics(soil_stats, Path('soil_statistics.csv'))
551
- """
552
-
553
-
1206
+ @staticmethod
1207
+ @log_call
1208
+ @standardize_input(file_type='geom_hdf')
1209
+ def get_landcover_raster_stats(
1210
+ geom_hdf_path: Path,
1211
+ landcover_hdf_path: Path = None,
1212
+ ras_object: Any = None
1213
+ ) -> pd.DataFrame:
1214
+ """
1215
+ Calculate land cover statistics for each 2D flow area using the area's perimeter.
1216
+
1217
+ Parameters
1218
+ ----------
1219
+ geom_hdf_path : Path
1220
+ Path to the HEC-RAS geometry HDF file containing the 2D flow areas
1221
+ landcover_hdf_path : Path, optional
1222
+ Path to the land cover HDF file. If None, uses landcover_hdf_path from rasmap_df
1223
+ ras_object : Any, optional
1224
+ Optional RAS object. If not provided, uses global ras instance
1225
+
1226
+ Returns
1227
+ -------
1228
+ pd.DataFrame
1229
+ DataFrame with land cover statistics for each 2D flow area, including:
1230
+ - mesh_name: Name of the 2D flow area
1231
+ - land_cover: Land cover classification name
1232
+ - percentage: Percentage of 2D flow area covered by this land cover type
1233
+ - area_sqm: Area in square meters
1234
+ - area_acres: Area in acres
1235
+ - area_sqmiles: Area in square miles
1236
+ - mannings_n: Manning's n value for this land cover type
1237
+ - percent_impervious: Percent impervious for this land cover type
1238
+
1239
+ Notes
1240
+ -----
1241
+ Requires the rasterstats package to be installed.
1242
+ """
1243
+ try:
1244
+ from rasterstats import zonal_stats
1245
+ import shapely
1246
+ import geopandas as gpd
1247
+ import numpy as np
1248
+ import tempfile
1249
+ import os
1250
+ import rasterio
1251
+ except ImportError as e:
1252
+ logger.error(f"Failed to import required package: {e}. Please run 'pip install rasterstats shapely geopandas rasterio'")
1253
+ raise e
1254
+
1255
+ # Import here to avoid circular imports
1256
+ from .HdfMesh import HdfMesh
1257
+
1258
+ # Get the landcover HDF path
1259
+ if landcover_hdf_path is None:
1260
+ if ras_object is None:
1261
+ from .RasPrj import ras
1262
+ ras_object = ras
1263
+
1264
+ # Try to get landcover_hdf_path from rasmap_df
1265
+ try:
1266
+ landcover_hdf_path = Path(ras_object.rasmap_df.loc[0, 'landcover_hdf_path'][0])
1267
+ if not landcover_hdf_path.exists():
1268
+ logger.warning(f"Land cover HDF path from rasmap_df does not exist: {landcover_hdf_path}")
1269
+ return pd.DataFrame()
1270
+ except (KeyError, IndexError, AttributeError, TypeError) as e:
1271
+ logger.error(f"Error retrieving landcover_hdf_path from rasmap_df: {str(e)}")
1272
+ return pd.DataFrame()
1273
+
1274
+ # Get land cover map (raster to ID mapping)
1275
+ try:
1276
+ with h5py.File(landcover_hdf_path, 'r') as hdf:
1277
+ if '//Raster Map' not in hdf:
1278
+ logger.error(f"No Raster Map found in {landcover_hdf_path}")
1279
+ return pd.DataFrame()
1280
+
1281
+ raster_map_data = hdf['//Raster Map'][()]
1282
+ raster_map = {int(item[0]): item[1].decode('utf-8').strip() for item in raster_map_data}
1283
+
1284
+ # Get land cover variables (mannings_n and percent_impervious)
1285
+ variables = {}
1286
+ if '//Variables' in hdf:
1287
+ var_data = hdf['//Variables'][()]
1288
+ for row in var_data:
1289
+ name = row[0].decode('utf-8').strip()
1290
+ mannings_n = float(row[1])
1291
+ percent_impervious = float(row[2])
1292
+ variables[name] = {
1293
+ 'mannings_n': mannings_n,
1294
+ 'percent_impervious': percent_impervious
1295
+ }
1296
+ except Exception as e:
1297
+ logger.error(f"Error reading land cover data from HDF: {str(e)}")
1298
+ return pd.DataFrame()
1299
+
1300
+ # Get 2D flow areas
1301
+ mesh_areas = HdfMesh.get_mesh_areas(geom_hdf_path)
1302
+ if mesh_areas.empty:
1303
+ logger.warning(f"No 2D flow areas found in {geom_hdf_path}")
1304
+ return pd.DataFrame()
1305
+
1306
+ # Check for the TIF file with same name as HDF
1307
+ tif_path = landcover_hdf_path.with_suffix('.tif')
1308
+ if not tif_path.exists():
1309
+ logger.error(f"No raster file found at {tif_path}")
1310
+ return pd.DataFrame()
1311
+
1312
+ # List to store all results
1313
+ all_results = []
1314
+
1315
+ # Read the raster data and info
1316
+ try:
1317
+ with rasterio.open(tif_path) as src:
1318
+ # Get transform directly from rasterio
1319
+ transform = src.transform
1320
+ no_data = src.nodata if src.nodata is not None else -9999
1321
+
1322
+ # Calculate zonal statistics for each 2D flow area
1323
+ for _, mesh_row in mesh_areas.iterrows():
1324
+ mesh_name = mesh_row['mesh_name']
1325
+ mesh_geom = mesh_row['geometry']
1326
+
1327
+ # Get zonal statistics directly using rasterio grid
1328
+ try:
1329
+ stats = zonal_stats(
1330
+ mesh_geom,
1331
+ tif_path,
1332
+ categorical=True,
1333
+ nodata=no_data
1334
+ )[0]
1335
+
1336
+ # Skip if no stats
1337
+ if not stats:
1338
+ logger.warning(f"No land cover data found for 2D flow area: {mesh_name}")
1339
+ continue
1340
+
1341
+ # Calculate total area and percentages
1342
+ total_area_sqm = sum(stats.values())
1343
+
1344
+ # Process each land cover type
1345
+ for raster_val, area_sqm in stats.items():
1346
+ # Skip NoData values
1347
+ if raster_val is None or raster_val == no_data:
1348
+ continue
1349
+
1350
+ try:
1351
+ # Get land cover name from raster map
1352
+ land_cover = raster_map.get(int(raster_val), f"Unknown-{raster_val}")
1353
+
1354
+ # Get Manning's n and percent impervious
1355
+ mannings_n = variables.get(land_cover, {}).get('mannings_n', None)
1356
+ percent_impervious = variables.get(land_cover, {}).get('percent_impervious', None)
1357
+
1358
+ percentage = (area_sqm / total_area_sqm) * 100 if total_area_sqm > 0 else 0
1359
+
1360
+ all_results.append({
1361
+ 'mesh_name': mesh_name,
1362
+ 'land_cover': land_cover,
1363
+ 'percentage': percentage,
1364
+ 'area_sqm': area_sqm,
1365
+ 'area_acres': area_sqm * HdfInfiltration.SQM_TO_ACRE,
1366
+ 'area_sqmiles': area_sqm * HdfInfiltration.SQM_TO_SQMILE,
1367
+ 'mannings_n': mannings_n,
1368
+ 'percent_impervious': percent_impervious
1369
+ })
1370
+ except Exception as e:
1371
+ logger.warning(f"Error processing raster value {raster_val}: {e}")
1372
+ continue
1373
+ except Exception as e:
1374
+ logger.error(f"Error calculating statistics for mesh {mesh_name}: {str(e)}")
1375
+ continue
1376
+ except Exception as e:
1377
+ logger.error(f"Error opening raster file {tif_path}: {str(e)}")
1378
+ return pd.DataFrame()
1379
+
1380
+ # Create DataFrame with results
1381
+ results_df = pd.DataFrame(all_results)
1382
+
1383
+ # Sort by mesh_name, percentage (descending)
1384
+ if not results_df.empty:
1385
+ results_df = results_df.sort_values(['mesh_name', 'percentage'], ascending=[True, False])
1386
+
1387
+ return results_df
554
1388
 
555
1389
 
556
1390