def combined_fis(database: str, label: str, veg_type: str, max_drainage_area: float): """ Combined beaver dam capacity FIS :param network: Shapefile path containing necessary FIS inputs :param label: Plain English label identifying vegetation type ("Existing" or "Historical") :param veg_type: Vegetation type suffix added to end of output ShapeFile fields :param max_drainage_area: Max drainage above which features are not processed. :return: None """ log = Logger('Combined FIS') log.info('Processing {} vegetation'.format(label)) veg_fis_field = 'oVC_{}'.format(veg_type) capacity_field = 'oCC_{}'.format(veg_type) dam_count_field = 'mCC_{}_CT'.format(veg_type) fields = [ veg_fis_field, 'iGeo_Slope', 'iGeo_DA', 'iHyd_SP2', 'iHyd_SPLow', 'iGeo_Len' ] reaches = load_attributes( database, fields, ' AND '.join(['({} IS NOT NULL)'.format(f) for f in fields])) calculate_combined_fis(reaches, veg_fis_field, capacity_field, dam_count_field, max_drainage_area) write_db_attributes(database, reaches, [capacity_field, dam_count_field], log) log.info('Process completed successfully.')
def vegetation_fis(database: str, label: str, veg_type: str): """Calculate vegetation suitability for each reach in a BRAT SQLite database Arguments: database {str} -- Path to BRAT SQLite database label {str} -- Either 'historic' or 'existing'. Only used for lof messages. veg_type {str} -- Prefix either 'EX' for existing or 'HPE' for historic """ log = Logger('Vegetation FIS') log.info('Processing {} vegetation'.format(label)) streamside_field = 'iVeg_30{}'.format(veg_type) riparian_field = 'iVeg100{}'.format(veg_type) out_field = 'oVC_{}'.format(veg_type) feature_values = load_attributes( database, [streamside_field, riparian_field], '({} IS NOT NULL) AND ({} IS NOT NULL)'.format(streamside_field, riparian_field)) calculate_vegegtation_fis(feature_values, streamside_field, riparian_field, out_field) write_db_attributes(database, feature_values, [out_field]) log.info('Process completed successfully.')
def conflict_attributes(output_gpkg: str, flowlines_path: str, valley_bottom: str, roads: str, rail: str, canals: str, ownership: str, buffer_distance_metres: float, cell_size_meters: float, epsg: int, canal_codes: List[int], intermediates_gpkg_path: str): """Calculate conflict attributes and write them back to a BRAT database Arguments: database {str} -- Path to BRAT database valley_bottom {str} -- Path to valley bottom polygon ShapeFile roads {str} -- Path to roads polyline ShapeFile rail {str} -- Path to railway polyline ShapeFile canals {str} -- Path to canals polyline ShapeFile ownership {str} -- Path to land ownership polygon ShapeFile buffer_distance_metres {float} -- Distance (meters) to buffer reaches when calculating distance to conflict cell_size_meters {float} -- Size of cells (meters) when rasterize Euclidean distance to conflict features epsg {str} -- Spatial reference in which to perform the analysis """ # Calculate conflict attributes values = calc_conflict_attributes(flowlines_path, valley_bottom, roads, rail, canals, ownership, buffer_distance_metres, cell_size_meters, epsg, canal_codes, intermediates_gpkg_path) # Write float and string fields separately with log summary enabled write_db_attributes(output_gpkg, values, [ 'iPC_Road', 'iPC_RoadVB', 'iPC_Rail', 'iPC_RailVB', 'iPC_Canal', 'iPC_DivPts', 'iPC_RoadX', 'iPC_Privat', 'oPC_Dist' ]) write_db_attributes(output_gpkg, values, ['AgencyID'], summarize=False)
def land_use(database: str, buffer: float): """Calculate land use intensity for each reach in BRAT database Arguments: database {str} -- Path to BRAT SQLite database buffer {float} -- Distance (meters) to buffer reach to obtain land use """ reaches = calculate_land_use(database, buffer) write_db_attributes( database, reaches, ['iPC_LU', 'iPC_VLowLU', 'iPC_LowLU', 'iPC_ModLU', 'iPC_HighLU'])
def conservation(database: str): """Calculate conservation fields for an existing BRAT database Assumes that conflict attributes and the dam capacity model have already been run for the database Arguments: database {str} -- Path to BRAT database """ results = calculate_conservation(database) write_db_attributes(database, results, ['OpportunityID', 'LimitationID', 'RiskID'])
def vegetation_suitability(gpkg_path: str, buffer: float, prefix: str, ecoregion: str): """Calculate vegetation suitability for each reach in a BRAT SQLite database Arguments: database {str} -- Path to BRAT SQLite database buffer {float} -- Distance to buffer reach polyline to obtain vegetation prefix {str} -- Either 'EX' for existing or 'HPE' for historic. ecoregion {int} -- Database ID of the ecoregion associated with the watershed """ veg_col = 'iVeg{}{}{}'.format('_' if len(str(int(buffer))) < 3 else '', int(buffer), prefix) reaches = calculate_vegetation_suitability(gpkg_path, buffer, prefix, veg_col, ecoregion) write_db_attributes(gpkg_path, reaches, [veg_col])
def rvd(huc: int, flowlines_orig: Path, existing_veg_orig: Path, historic_veg_orig: Path, valley_bottom_orig: Path, output_folder: Path, reach_codes: List[str], flow_areas_orig: Path, waterbodies_orig: Path, meta=None): """[Generate segmented reaches on flowline network and calculate RVD from historic and existing vegetation rasters Args: huc (integer): Watershed ID flowlines_orig (Path): Segmented flowlines feature layer existing_veg_orig (Path): LANDFIRE version 2.00 evt raster, with adjacent xml metadata file historic_veg_orig (Path): LANDFIRE version 2.00 bps raster, with adjacent xml metadata file valley_bottom_orig (Path): Vbet polygon feature layer output_folder (Path): destination folder for project output reach_codes (List[int]): NHD reach codes for features to include in outputs flow_areas_orig (Path): NHD flow area polygon feature layer waterbodies (Path): NHD waterbodies polygon feature layer meta (Dict[str,str]): dictionary of riverscapes metadata key: value pairs """ log = Logger("RVD") log.info('RVD v.{}'.format(cfg.version)) try: int(huc) except ValueError: raise Exception('Invalid HUC identifier "{}". Must be an integer'.format(huc)) if not (len(huc) == 4 or len(huc) == 8): raise Exception('Invalid HUC identifier. Must be four digit integer') safe_makedirs(output_folder) project, _realization, proj_nodes = create_project(huc, output_folder) # Incorporate project metadata to the riverscapes project if meta is not None: project.add_metadata(meta) log.info('Adding inputs to project') _prj_existing_path_node, prj_existing_path = project.add_project_raster(proj_nodes['Inputs'], LayerTypes['EXVEG'], existing_veg_orig) _prj_historic_path_node, prj_historic_path = project.add_project_raster(proj_nodes['Inputs'], LayerTypes['HISTVEG'], historic_veg_orig) # TODO: Don't forget the att_filter # _prj_flowlines_node, prj_flowlines = project.add_project_geopackage(proj_nodes['Inputs'], LayerTypes['INPUTS'], flowlines, att_filter="\"ReachCode\" Like '{}%'".format(huc)) # Copy in the vectors we need inputs_gpkg_path = os.path.join(output_folder, LayerTypes['INPUTS'].rel_path) intermediates_gpkg_path = os.path.join(output_folder, LayerTypes['INTERMEDIATES'].rel_path) outputs_gpkg_path = os.path.join(output_folder, LayerTypes['OUTPUTS'].rel_path) # Make sure we're starting with empty/fresh geopackages GeopackageLayer.delete(inputs_gpkg_path) GeopackageLayer.delete(intermediates_gpkg_path) GeopackageLayer.delete(outputs_gpkg_path) # Copy our input layers and also find the difference in the geometry for the valley bottom flowlines_path = os.path.join(inputs_gpkg_path, LayerTypes['INPUTS'].sub_layers['FLOWLINES'].rel_path) vbottom_path = os.path.join(inputs_gpkg_path, LayerTypes['INPUTS'].sub_layers['VALLEY_BOTTOM'].rel_path) copy_feature_class(flowlines_orig, flowlines_path, epsg=cfg.OUTPUT_EPSG) copy_feature_class(valley_bottom_orig, vbottom_path, epsg=cfg.OUTPUT_EPSG) with GeopackageLayer(flowlines_path) as flow_lyr: # Set the output spatial ref as this for the whole project out_srs = flow_lyr.spatial_ref meter_conversion = flow_lyr.rough_convert_metres_to_vector_units(1) distance_buffer = flow_lyr.rough_convert_metres_to_vector_units(1) # Transform issues reading 102003 as espg id. Using sr wkt seems to work, however arcgis has problems loading feature classes with this method... raster_srs = ogr.osr.SpatialReference() ds = gdal.Open(prj_existing_path, 0) raster_srs.ImportFromWkt(ds.GetProjectionRef()) raster_srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER) transform_shp_to_raster = VectorBase.get_transform(out_srs, raster_srs) gt = ds.GetGeoTransform() cell_area = ((gt[1] / meter_conversion) * (-gt[5] / meter_conversion)) # Create the output feature class fields with GeopackageLayer(outputs_gpkg_path, layer_name='ReachGeometry', delete_dataset=True) as out_lyr: out_lyr.create_layer(ogr.wkbMultiLineString, spatial_ref=out_srs, options=['FID=ReachID'], fields={ 'GNIS_NAME': ogr.OFTString, 'ReachCode': ogr.OFTString, 'TotDASqKm': ogr.OFTReal, 'NHDPlusID': ogr.OFTReal, 'WatershedID': ogr.OFTInteger }) metadata = { 'RVD_DateTime': datetime.datetime.now().isoformat(), 'Reach_Codes': reach_codes } # Execute the SQL to create the lookup tables in the RVD geopackage SQLite database watershed_name = create_database(huc, outputs_gpkg_path, metadata, cfg.OUTPUT_EPSG, os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'database', 'rvd_schema.sql')) project.add_metadata({'Watershed': watershed_name}) geom_vbottom = get_geometry_unary_union(vbottom_path, spatial_ref=raster_srs) flowareas_path = None if flow_areas_orig: flowareas_path = os.path.join(inputs_gpkg_path, LayerTypes['INPUTS'].sub_layers['FLOW_AREA'].rel_path) copy_feature_class(flow_areas_orig, flowareas_path, epsg=cfg.OUTPUT_EPSG) geom_flow_areas = get_geometry_unary_union(flowareas_path) # Difference with existing vbottom geom_vbottom = geom_vbottom.difference(geom_flow_areas) else: del LayerTypes['INPUTS'].sub_layers['FLOW_AREA'] waterbodies_path = None if waterbodies_orig: waterbodies_path = os.path.join(inputs_gpkg_path, LayerTypes['INPUTS'].sub_layers['WATERBODIES'].rel_path) copy_feature_class(waterbodies_orig, waterbodies_path, epsg=cfg.OUTPUT_EPSG) geom_waterbodies = get_geometry_unary_union(waterbodies_path) # Difference with existing vbottom geom_vbottom = geom_vbottom.difference(geom_waterbodies) else: del LayerTypes['INPUTS'].sub_layers['WATERBODIES'] # Add the inputs to the XML _nd, _in_gpkg_path, _sublayers = project.add_project_geopackage(proj_nodes['Inputs'], LayerTypes['INPUTS']) # Filter the flow lines to just the required features and then segment to desired length # TODO: These are brat methods that need to be refactored to use VectorBase layers cleaned_path = os.path.join(outputs_gpkg_path, 'ReachGeometry') build_network(flowlines_path, flowareas_path, cleaned_path, waterbodies_path=waterbodies_path, epsg=cfg.OUTPUT_EPSG, reach_codes=reach_codes, create_layer=False) # Generate Voroni polygons log.info("Calculating Voronoi Polygons...") # Add all the points (including islands) to the list flowline_thiessen_points_groups = centerline_points(cleaned_path, distance_buffer, transform_shp_to_raster) flowline_thiessen_points = [pt for group in flowline_thiessen_points_groups.values() for pt in group] simple_save([pt.point for pt in flowline_thiessen_points], ogr.wkbPoint, raster_srs, "Thiessen_Points", intermediates_gpkg_path) # Exterior is the shell and there is only ever 1 myVorL = NARVoronoi(flowline_thiessen_points) # Generate Thiessen Polys myVorL.createshapes() # Dissolve by flowlines log.info("Dissolving Thiessen Polygons") dissolved_polys = myVorL.dissolve_by_property('fid') # Clip Thiessen Polys log.info("Clipping Thiessen Polygons to Valley Bottom") clipped_thiessen = clip_polygons(geom_vbottom, dissolved_polys) # Save Intermediates simple_save(clipped_thiessen.values(), ogr.wkbPolygon, raster_srs, "Thiessen", intermediates_gpkg_path) simple_save(dissolved_polys.values(), ogr.wkbPolygon, raster_srs, "ThiessenPolygonsDissolved", intermediates_gpkg_path) simple_save(myVorL.polys, ogr.wkbPolygon, raster_srs, "ThiessenPolygonsRaw", intermediates_gpkg_path) _nd, _inter_gpkg_path, _sublayers = project.add_project_geopackage(proj_nodes['Intermediates'], LayerTypes['INTERMEDIATES']) # OLD METHOD FOR AUDIT # dissolved_polys2 = dissolve_by_points(flowline_thiessen_points_groups, myVorL.polys) # simple_save(dissolved_polys2.values(), ogr.wkbPolygon, out_srs, "ThiessenPolygonsDissolved_OLD", intermediates_gpkg_path) # Load Vegetation Rasters log.info(f"Loading Existing and Historic Vegetation Rasters") vegetation = {} vegetation["EXISTING"] = load_vegetation_raster(prj_existing_path, outputs_gpkg_path, True, output_folder=os.path.join(output_folder, 'Intermediates')) vegetation["HISTORIC"] = load_vegetation_raster(prj_historic_path, outputs_gpkg_path, False, output_folder=os.path.join(output_folder, 'Intermediates')) for epoch in vegetation.keys(): for name in vegetation[epoch].keys(): if not f"{epoch}_{name}" == "HISTORIC_LUI": project.add_project_raster(proj_nodes['Intermediates'], LayerTypes[f"{epoch}_{name}"]) if vegetation["EXISTING"]["RAW"].shape != vegetation["HISTORIC"]["RAW"].shape: raise Exception('Vegetation raster shapes are not equal Existing={} Historic={}. Cannot continue'.format(vegetation["EXISTING"]["RAW"].shape, vegetation["HISTORIC"]["RAW"].shape)) # Vegetation zone calculations riparian_zone_arrays = {} riparian_zone_arrays["RIPARIAN_ZONES"] = ((vegetation["EXISTING"]["RIPARIAN"] + vegetation["HISTORIC"]["RIPARIAN"]) > 0) * 1 riparian_zone_arrays["NATIVE_RIPARIAN_ZONES"] = ((vegetation["EXISTING"]["NATIVE_RIPARIAN"] + vegetation["HISTORIC"]["NATIVE_RIPARIAN"]) > 0) * 1 riparian_zone_arrays["VEGETATION_ZONES"] = ((vegetation["EXISTING"]["VEGETATED"] + vegetation["HISTORIC"]["VEGETATED"]) > 0) * 1 # Save Intermediate Rasters for name, raster in riparian_zone_arrays.items(): save_intarr_to_geotiff(raster, os.path.join(output_folder, "Intermediates", f"{name}.tif"), prj_existing_path) project.add_project_raster(proj_nodes['Intermediates'], LayerTypes[name]) # Calculate Riparian Departure per Reach riparian_arrays = {f"{epoch.capitalize()}{(name.capitalize()).replace('Native_riparian', 'NativeRiparian')}Mean": array for epoch, arrays in vegetation.items() for name, array in arrays.items() if name in ["RIPARIAN", "NATIVE_RIPARIAN"]} # Vegetation Cell Counts raw_arrays = {f"{epoch}": array for epoch, arrays in vegetation.items() for name, array in arrays.items() if name == "RAW"} # Generate Vegetation Conversions vegetation_change = (vegetation["HISTORIC"]["CONVERSION"] - vegetation["EXISTING"]["CONVERSION"]) save_intarr_to_geotiff(vegetation_change, os.path.join(output_folder, "Intermediates", "Conversion_Raster.tif"), prj_existing_path) project.add_project_raster(proj_nodes['Intermediates'], LayerTypes['VEGETATION_CONVERSION']) # load conversion types dictionary from database conn = sqlite3.connect(outputs_gpkg_path) conn.row_factory = dict_factory curs = conn.cursor() curs.execute('SELECT * FROM ConversionTypes') conversion_classifications = curs.fetchall() curs.execute('SELECT * FROM vwConversions') conversion_ids = curs.fetchall() # Split vegetation change classes into binary arrays vegetation_change_arrays = { c['FieldName']: (vegetation_change == int(c["TypeValue"])) * 1 if int(c["TypeValue"]) in np.unique(vegetation_change) else None for c in conversion_classifications } # Calcuate average and unique cell counts per reach progbar = ProgressBar(len(clipped_thiessen.keys()), 50, "Extracting array values by reach...") counter = 0 discarded = 0 with rasterio.open(prj_existing_path) as dataset: unique_vegetation_counts = {} reach_average_riparian = {} reach_average_change = {} for reachid, poly in clipped_thiessen.items(): counter += 1 progbar.update(counter) # we can discount a lot of shapes here. if not poly.is_valid or poly.is_empty or poly.area == 0 or poly.geom_type not in ["Polygon", "MultiPolygon"]: discarded += 1 continue raw_values_unique = {} change_values_mean = {} riparian_values_mean = {} reach_raster = np.ma.masked_invalid( features.rasterize( [poly], out_shape=dataset.shape, transform=dataset.transform, all_touched=True, fill=np.nan)) for raster_name, raster in raw_arrays.items(): if raster is not None: current_raster = np.ma.masked_array(raster, mask=reach_raster.mask) raw_values_unique[raster_name] = np.unique(np.ma.filled(current_raster, fill_value=0), return_counts=True) else: raw_values_unique[raster_name] = [] for raster_name, raster in riparian_arrays.items(): if raster is not None: current_raster = np.ma.masked_array(raster, mask=reach_raster.mask) riparian_values_mean[raster_name] = np.ma.mean(current_raster) else: riparian_values_mean[raster_name] = 0.0 for raster_name, raster in vegetation_change_arrays.items(): if raster is not None: current_raster = np.ma.masked_array(raster, mask=reach_raster.mask) change_values_mean[raster_name] = np.ma.mean(current_raster) else: change_values_mean[raster_name] = 0.0 unique_vegetation_counts[reachid] = raw_values_unique reach_average_riparian[reachid] = riparian_values_mean reach_average_change[reachid] = change_values_mean progbar.finish() with SQLiteCon(outputs_gpkg_path) as gpkg: # Ensure all reaches are present in the ReachAttributes table before storing RVD output values gpkg.curs.execute('INSERT INTO ReachAttributes (ReachID) SELECT ReachID FROM ReachGeometry;') errs = 0 for reachid, epochs in unique_vegetation_counts.items(): for epoch in epochs.values(): insert_values = [[reachid, int(vegetationid), float(count * cell_area), int(count)] for vegetationid, count in zip(epoch[0], epoch[1]) if vegetationid != 0] try: gpkg.curs.executemany('''INSERT INTO ReachVegetation ( ReachID, VegetationID, Area, CellCount) VALUES (?,?,?,?)''', insert_values) # Sqlite can't report on SQL errors so we have to print good log messages to help intuit what the problem is except sqlite3.IntegrityError as err: # THis is likely a constraint error. errstr = "Integrity Error when inserting records: ReachID: {} VegetationIDs: {}".format(reachid, str(list(epoch[0]))) log.error(errstr) errs += 1 except sqlite3.Error as err: # This is any other kind of error errstr = "SQL Error when inserting records: ReachID: {} VegetationIDs: {} ERROR: {}".format(reachid, str(list(epoch[0])), str(err)) log.error(errstr) errs += 1 if errs > 0: raise Exception('Errors were found inserting records into the database. Cannot continue.') gpkg.conn.commit() # load RVD departure levels from DepartureLevels database table with SQLiteCon(outputs_gpkg_path) as gpkg: gpkg.curs.execute('SELECT LevelID, MaxRVD FROM DepartureLevels ORDER BY MaxRVD ASC') departure_levels = gpkg.curs.fetchall() # Calcuate Average Departure for Riparian and Native Riparian riparian_departure_values = riparian_departure(reach_average_riparian, departure_levels) write_db_attributes(outputs_gpkg_path, riparian_departure_values, departure_type_columns) # Add Conversion Code, Type to Vegetation Conversion with SQLiteCon(outputs_gpkg_path) as gpkg: gpkg.curs.execute('SELECT LevelID, MaxValue, NAME FROM ConversionLevels ORDER BY MaxValue ASC') conversion_levels = gpkg.curs.fetchall() reach_values_with_conversion_codes = classify_conversions(reach_average_change, conversion_ids, conversion_levels) write_db_attributes(outputs_gpkg_path, reach_values_with_conversion_codes, rvd_columns) # # Write Output to GPKG table # log.info('Insert values to GPKG tables') # # TODO move this to write_attirubtes method # with get_shp_or_gpkg(outputs_gpkg_path, layer_name='ReachAttributes', write=True, ) as in_layer: # # Create each field and store the name and index in a list of tuples # field_indices = [(field, in_layer.create_field(field, field_type)) for field, field_type in { # "FromConifer": ogr.OFTReal, # "FromDevegetated": ogr.OFTReal, # "FromGrassShrubland": ogr.OFTReal, # "FromDeciduous": ogr.OFTReal, # "NoChange": ogr.OFTReal, # "Deciduous": ogr.OFTReal, # "GrassShrubland": ogr.OFTReal, # "Devegetation": ogr.OFTReal, # "Conifer": ogr.OFTReal, # "Invasive": ogr.OFTReal, # "Development": ogr.OFTReal, # "Agriculture": ogr.OFTReal, # "ConversionCode": ogr.OFTInteger, # "ConversionType": ogr.OFTString}.items()] # for feature, _counter, _progbar in in_layer.iterate_features("Writing Attributes", write_layers=[in_layer]): # reach = feature.GetFID() # if reach not in reach_values_with_conversion_codes: # continue # # Set all the field values and then store the feature # for field, _idx in field_indices: # if field in reach_values_with_conversion_codes[reach]: # if not reach_values_with_conversion_codes[reach][field]: # feature.SetField(field, None) # else: # feature.SetField(field, reach_values_with_conversion_codes[reach][field]) # in_layer.ogr_layer.SetFeature(feature) # # Create each field and store the name and index in a list of tuples # field_indices = [(field, in_layer.create_field(field, field_type)) for field, field_type in { # "EXISTING_RIPARIAN_MEAN": ogr.OFTReal, # "HISTORIC_RIPARIAN_MEAN": ogr.OFTReal, # "RIPARIAN_DEPARTURE": ogr.OFTReal, # "EXISTING_NATIVE_RIPARIAN_MEAN": ogr.OFTReal, # "HISTORIC_NATIVE_RIPARIAN_MEAN": ogr.OFTReal, # "NATIVE_RIPARIAN_DEPARTURE": ogr.OFTReal, }.items()] # for feature, _counter, _progbar in in_layer.iterate_features("Writing Attributes", write_layers=[in_layer]): # reach = feature.GetFID() # if reach not in riparian_departure_values: # continue # # Set all the field values and then store the feature # for field, _idx in field_indices: # if field in riparian_departure_values[reach]: # if not riparian_departure_values[reach][field]: # feature.SetField(field, None) # else: # feature.SetField(field, riparian_departure_values[reach][field]) # in_layer.ogr_layer.SetFeature(feature) # with sqlite3.connect(outputs_gpkg_path) as conn: # cursor = conn.cursor() # errs = 0 # for reachid, epochs in unique_vegetation_counts.items(): # for epoch in epochs.values(): # insert_values = [[reachid, int(vegetationid), float(count * cell_area), int(count)] for vegetationid, count in zip(epoch[0], epoch[1]) if vegetationid != 0] # try: # cursor.executemany('''INSERT INTO ReachVegetation ( # ReachID, # VegetationID, # Area, # CellCount) # VALUES (?,?,?,?)''', insert_values) # # Sqlite can't report on SQL errors so we have to print good log messages to help intuit what the problem is # except sqlite3.IntegrityError as err: # # THis is likely a constraint error. # errstr = "Integrity Error when inserting records: ReachID: {} VegetationIDs: {}".format(reachid, str(list(epoch[0]))) # log.error(errstr) # errs += 1 # except sqlite3.Error as err: # # This is any other kind of error # errstr = "SQL Error when inserting records: ReachID: {} VegetationIDs: {} ERROR: {}".format(reachid, str(list(epoch[0])), str(err)) # log.error(errstr) # errs += 1 # if errs > 0: # raise Exception('Errors were found inserting records into the database. Cannot continue.') # conn.commit() # Add intermediates and the report to the XML # project.add_project_geopackage(proj_nodes['Intermediates'], LayerTypes['INTERMEDIATES']) already # added above project.add_project_geopackage(proj_nodes['Outputs'], LayerTypes['OUTPUTS']) # Add the report to the XML report_path = os.path.join(project.project_dir, LayerTypes['REPORT'].rel_path) project.add_report(proj_nodes['Outputs'], LayerTypes['REPORT'], replace=True) report = RVDReport(report_path, project) report.write() log.info('RVD complete')
def reach_geometry(flow_lines: Path, dem_path: Path, buffer_distance: float): """ Calculate reach geometry BRAT attributes Args: flow_lines (Path): [description] dem_path (Path): [description] buffer_distance (float): [description] """ log = Logger('Reach Geometry') # Determine the best projected coordinate system based on the raster dataset = gdal.Open(dem_path) geo_transform = dataset.GetGeoTransform() xcentre = geo_transform[0] + (dataset.RasterXSize * geo_transform[1]) / 2.0 epsg = get_utm_zone_epsg(xcentre) with rasterio.open(dem_path) as raster: bounds = raster.bounds extent = box(*bounds) # Buffer the start and end point of each reach line_start_polygons = {} line_end_polygons = {} reaches = {} with get_shp_or_gpkg(flow_lines) as lyr: # Transformations from original flow line features to metric EPSG, and to raster spatial reference _srs, transform_to_metres = VectorBase.get_transform_from_epsg(lyr.spatial_ref, epsg) _srs, transform_to_raster = VectorBase.get_transform_from_raster(lyr.spatial_ref, dem_path) # Buffer distance converted to the units of the raster spatial reference vector_buffer = VectorBase.rough_convert_metres_to_raster_units(dem_path, buffer_distance) for feature, _counter, _progbar in lyr.iterate_features("Processing reaches"): reach_id = feature.GetFID() geom = feature.GetGeometryRef() geom_clone = geom.Clone() # Calculate the reach length in the output spatial reference if transform_to_metres is not None: geom.Transform(transform_to_metres) reaches[reach_id] = {'iGeo_Len': geom.Length(), 'iGeo_Slope': 0.0, 'iGeo_ElMin': None, 'IGeo_ElMax': None} if transform_to_raster is not None: geom_clone.Transform(transform_to_raster) # Buffer the ends of the reach polyline in the raster spatial reference pt_start = Point(VectorBase.ogr2shapely(geom_clone, transform_to_raster).coords[0]) pt_end = Point(VectorBase.ogr2shapely(geom_clone, transform_to_raster).coords[-1]) if extent.contains(pt_start) and extent.contains(pt_end): line_start_polygons[reach_id] = pt_start.buffer(vector_buffer) line_end_polygons[reach_id] = pt_end.buffer(vector_buffer) # Retrieve the mean elevation of start and end of point line_start_elevations = raster_buffer_stats2(line_start_polygons, dem_path) line_end_elevations = raster_buffer_stats2(line_end_polygons, dem_path) for reach_id, data in reaches.items(): if reach_id in line_start_elevations and reach_id in line_end_elevations: sta_data = line_start_elevations[reach_id] end_data = line_end_elevations[reach_id] data['iGeo_ElMax'] = _max_ignore_none(sta_data['Maximum'], end_data['Maximum']) data['iGeo_ElMin'] = _min_ignore_none(sta_data['Minimum'], end_data['Minimum']) if sta_data['Mean'] is not None and end_data['Mean'] is not None and sta_data['Mean'] != end_data['Mean']: data['iGeo_Slope'] = abs(sta_data['Mean'] - end_data['Mean']) / data['iGeo_Len'] else: log.warning('{:,} features skipped because one or both ends of polyline not on DEM raster'.format(reach_id)) write_db_attributes(os.path.dirname(flow_lines), reaches, ['iGeo_Len', 'iGeo_ElMax', 'iGeo_ElMin', 'iGeo_Slope'])
def hydrology(gpkg_path: str, prefix: str, huc: str): """Calculate low flow, peak flow discharges for each reach in a BRAT database Arguments: database {str} -- Path to BRAT SQLite database prefix {str} -- Q2 or QLow identifying which discharge to calculate huc {str} -- watershed identifier Raises: Exception: When the watershed is missing the regional discharge equation """ hydrology_field = 'iHyd_Q{}'.format(prefix) streampower_field = 'iHyd_SP{}'.format(prefix) log = Logger('Hydrology') log.info('Calculating Q{} hydrology for HUC {}'.format(prefix, huc)) log.info('Discharge field: {}'.format(hydrology_field)) log.info('Stream power field: {}'.format(streampower_field)) # Load the hydrology equation for the HUC with SQLiteCon(gpkg_path) as database: database.curs.execute( 'SELECT Q{} As Q FROM Watersheds WHERE WatershedID = ?'.format( prefix), [huc]) equation = database.curs.fetchone()['Q'] equation = equation.replace('^', '**') if not equation: raise Exception('Missing {} hydrology formula for HUC {}'.format( prefix, huc)) log.info('Regional curve: {}'.format(equation)) # Load the hydrology CONVERTED parameters for the HUC (the values will be in the same units as used in the regional equations) database.curs.execute( 'SELECT Parameter, ConvertedValue FROM vwHydroParams WHERE WatershedID = ?', [huc]) params = { row['Parameter']: row['ConvertedValue'] for row in database.curs.fetchall() } [ log.info('Param: {} = {:.2f}'.format(key, value)) for key, value in params.items() ] # Load the conversion factor for converting reach attribute drainage areas to the values used in the regional equations database.curs.execute( 'SELECT Conversion FROM HydroParams WHERE Name = ?', [DRNAREA_PARAM]) drainage_conversion_factor = database.curs.fetchone()['Conversion'] log.info('Reach drainage area attribute conversion factor = {}'.format( drainage_conversion_factor)) # Load the discharges for each reach reaches = load_attributes(gpkg_path, ['iGeo_DA'], '(iGeo_DA IS NOT NULL)') log.info('{:,} reaches loaded with valid drainage area values'.format( len(reaches))) # Calculate the discharges for each reach results = calculate_hydrology(reaches, equation, params, drainage_conversion_factor, hydrology_field) log.info('{:,} reach hydrology values calculated.'.format(len(results))) # Write the discharges to the database write_db_attributes(gpkg_path, results, [hydrology_field]) # Convert discharges to stream power with SQLiteCon(gpkg_path) as database: database.curs.execute( 'UPDATE ReachAttributes SET {0} = ROUND((1000 * 9.80665) * iGeo_Slope * ({1} * 0.028316846592), 2)' ' WHERE ({1} IS NOT NULL) AND (iGeo_Slope IS NOT NULL)'.format( streampower_field, hydrology_field)) database.conn.commit() log.info('Hydrology calculation complete')