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.
- ras_commander/HdfInfiltration.py +910 -76
- ras_commander/HdfResultsMesh.py +245 -0
- ras_commander/RasGeo.py +537 -523
- ras_commander/__init__.py +3 -2
- {ras_commander-0.72.0.dist-info → ras_commander-0.73.0.dist-info}/METADATA +21 -2
- {ras_commander-0.72.0.dist-info → ras_commander-0.73.0.dist-info}/RECORD +9 -9
- {ras_commander-0.72.0.dist-info → ras_commander-0.73.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.72.0.dist-info → ras_commander-0.73.0.dist-info}/licenses/LICENSE +0 -0
- {ras_commander-0.72.0.dist-info → ras_commander-0.73.0.dist-info}/top_level.txt +0 -0
ras_commander/HdfInfiltration.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
|