Source code for climada.util.lines_polys_handler

"""
This file is part of CLIMADA.

Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.

CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free
Software Foundation, version 3.

CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.

"""
import logging
import copy
from enum import Enum

import cartopy.crs as ccrs
import geopandas as gpd
import numpy as np
import pandas as pd
import pyproj
import scipy as sp
import shapely as sh
import shapely.geometry as shgeom

from climada.engine import ImpactCalc
from climada.util import coordinates as u_coord

LOGGER = logging.getLogger(__name__)


[docs] class DisaggMethod(Enum): """ Disaggregation Method for the ... function DIV : the geometry's distributed to equal parts over all its interpolated points FIX : the geometry's value is replicated over all its interpolated points """ DIV = 'div' FIX = 'fix'
[docs] class AggMethod(Enum): """ Aggregation Method for the aggregate_impact_mat function SUM : the impact is summed over all points in the polygon/line """ SUM = 'sum'
[docs] def calc_geom_impact( exp, impf_set, haz, res, to_meters=False, disagg_met=DisaggMethod.DIV, disagg_val=None, agg_met=AggMethod.SUM): """ Compute impact for exposure with (multi-)polygons and/or (multi-)lines. Lat/Lon values in exp.gdf are ignored, only exp.gdf.geometry is considered. The geometries are first disaggregated to points. Polygons: grid with resolution res*res. Lines: points along the line separated by distance res. The impact per point is then re-aggregated for each geometry. Parameters ---------- exp : Exposures The exposure instance with exp.gdf.geometry containing (multi-)polygons and/or (multi-)lines impf_set : ImpactFuncSet The set of impact functions. haz : Hazard The hazard instance. res : float Resolution of the disaggregation grid (polygon) or line (lines). to_meters : bool, optional If True, res is interpreted as meters, and geometries are projected to an equal area projection for disaggregation. The exposures are then projected back to the original projections before impact calculation. The default is False. disagg_met : DisaggMethod Disaggregation method of the shapes's original value onto its inter- polated points. 'DIV': Divide the value evenly over all the new points; 'FIX': Replicate the value onto all the new points. Default is 'DIV'. Works in combination with the kwarg 'disagg_val'. disagg_val: float, optional Specifies what number should be taken as the value, which is to be disaggregated according to the method provided in disagg_met. None: The shape's value is taken from the exp.gdf.value column. float: This given number will be disaggregated according to the method. In case exp.gdf.value column exists, original values in there will be ignored. The default is None. agg_met : AggMethod Aggregation method of the point impacts into impact for respective parent-geometry. If 'SUM', the impact is summed over all points in each geometry. The default is 'SUM'. Returns ------- Impact Impact object with the impact per geometry (rows of exp.gdf). Contains two additional attributes 'geom_exp' and 'coord_exp', the first one being the origninal line or polygon geometries for which impact was computed. See Also -------- exp_geom_to_pnt: disaggregate exposures """ # disaggregate exposure exp_pnt = exp_geom_to_pnt( exp=exp, res=res, to_meters=to_meters, disagg_met=disagg_met, disagg_val=disagg_val ) exp_pnt.assign_centroids(haz) # compute point impact calc = ImpactCalc(exp_pnt, impf_set, haz) impact_pnt = calc.impact(save_mat=True, assign_centroids=False) # re-aggregate impact to original exposure geometry impact_agg = impact_pnt_agg(impact_pnt, exp_pnt.gdf, agg_met) return impact_agg
[docs] def impact_pnt_agg(impact_pnt, exp_pnt_gdf, agg_met): """ Aggregate the impact per geometry. The output Impact object contains an extra attribute 'geom_exp' containing the geometries. Parameters ---------- impact_pnt : Impact Impact object with impact per exposure point (lines of exp_pnt) exp_pnt_gdf : gpd.GeoDataFrame Geodataframe of an exposures featuring a multi-index. First level indicating membership of original geometries, second level the disaggregated points. The exposure is obtained for instance with the disaggregation method exp_geom_to_pnt(). agg_met : AggMethod Aggregation method of the point impacts into impact for respective parent-geometry. If 'SUM', the impact is summed over all points in each geometry. The default is 'SUM'. Returns ------- impact_agg : Impact Impact object with the impact per original geometry. Original geometry additionally stored in attribute 'geom_exp'; coord_exp contains only representative points (lat/lon) of those geometries. See also -------- exp_geom_to_pnt: exposures disaggregation method """ # aggregate impact mat_agg = _aggregate_impact_mat(impact_pnt, exp_pnt_gdf, agg_met) # write to impact obj impact_agg = set_imp_mat(impact_pnt, mat_agg) # add exposure representation points as coordinates repr_pnts = gpd.GeoSeries( exp_pnt_gdf['geometry_orig'][:,0].apply( lambda x: x.representative_point())) impact_agg.coord_exp = np.array([repr_pnts.y, repr_pnts.x]).transpose() # Add original geometries for plotting impact_agg.geom_exp = exp_pnt_gdf.xs(0, level=1)\ .set_geometry('geometry_orig')\ .geometry.rename('geometry') return impact_agg
def _aggregate_impact_mat(imp_pnt, gdf_pnt, agg_met): """ Aggregate point impact matrix given the geodataframe of disaggregated geometries. Parameters ---------- imp_pnt : Impact Impact object with impact per point (rows of gdf_pnt) gdf_pnt : gpd.GeoDataFrame Exposures geodataframe with a multi-index, as obtained from disaggregation method exp_geom_to_pnt(). First level indicating membership of original geometries, second level the disaggregated points agg_met : AggMethod Aggregation method of the point impacts into impact for respective parent-geometry. If 'SUM', the impact is summed over all points in each geometry. The default is 'SUM'. Returns ------- sparse.csr_matrix matrix of shape #events x #original geometries with impacts. """ col_geom = gdf_pnt.index.get_level_values(level=0) # Converts string multi-index level 0 to integer index col_geom = np.sort(np.unique(col_geom, return_inverse=True)[1]) row_pnt = np.arange(len(col_geom)) if agg_met is AggMethod.SUM: mask = np.ones(len(col_geom)) else: raise NotImplementedError( f'The available aggregation methods are {AggMethod._member_names_}') # pylint: disable=no-member, protected-access csr_mask = sp.sparse.csr_matrix( (mask, (row_pnt, col_geom)), shape=(len(row_pnt), len(np.unique(col_geom))) ) return imp_pnt.imp_mat.dot(csr_mask)
[docs] def calc_grid_impact( exp, impf_set, haz, grid, disagg_met=DisaggMethod.DIV, disagg_val=None, agg_met=AggMethod.SUM): """ Compute impact for exposure with (multi-)polygons and/or (multi-)lines. Lat/Lon values in exp.gdf are ignored, only exp.gdf.geometry is considered. The geometries are first disaggregated to points. Polygons: grid with resolution res*res. Lines: points along the line separated by distance res. The impact per point is then re-aggregated for each geometry. Parameters ---------- exp : Exposures The exposure instance with exp.gdf.geometry containing (multi-)polygons and/or (multi-)lines impf_set : ImpactFuncSet The set of impact functions. haz : Hazard The hazard instance. grid : np.array() Grid on which to disaggregate the exposures. Provided as two vectors [x_grid, y_grid]. disagg_met : DisaggMethod Disaggregation method of the shapes's original value onto its inter- polated points. 'DIV': Divide the value evenly over all the new points; 'FIX': Replicate the value onto all the new points. Default is 'DIV'. Works in combination with the kwarg 'disagg_val'. disagg_val: float, optional Specifies what number should be taken as the value, which is to be disaggregated according to the method provided in disagg_met. None: The shape's value is taken from the exp.gdf.value column. float: This given number will be disaggregated according to the method. In case exp.gdf.value column exists, original values in there will be ignored The default is None. agg_met : AggMethod Aggregation method of the point impacts into impact for respective parent-geometry. If 'SUM', the impact is summed over all points in each geometry. The default is 'SUM'. Returns ------- Impact Impact object with the impact per geometry (rows of exp.gdf). Contains two additional attributes 'geom_exp' and 'coord_exp', the first one being the origninal line or polygon geometries for which impact was computed. See Also -------- exp_geom_to_pnt: disaggregate exposures """ # disaggregate exposure exp_pnt = exp_geom_to_grid( exp=exp, grid= grid, disagg_met=disagg_met, disagg_val=disagg_val ) exp_pnt.assign_centroids(haz) # compute point impact impact_pnt = ImpactCalc(exp_pnt, impf_set, haz).impact(save_mat=True, assign_centroids=False) # re-aggregate impact to original exposure geometry impact_agg = impact_pnt_agg(impact_pnt, exp_pnt.gdf, agg_met) return impact_agg
[docs] def plot_eai_exp_geom(imp_geom, centered=False, figsize=(9, 13), **kwargs): """ Plot the average impact per exposure polygon. Parameters ---------- imp_geom : Impact Impact instance with imp_geom set (i.e. computed from exposures with polygons) centered : bool, optional Center the plot. The default is False. figsize : (float, float), optional Figure size. The default is (9, 13). **kwargs : dict Keyword arguments for GeoDataFrame.plot() Returns ------- ax: matplotlib axes instance """ kwargs['figsize'] = figsize if 'legend_kwds' not in kwargs: kwargs['legend_kwds'] = { 'label': f"Impact [{imp_geom.unit}]", 'orientation': "horizontal" } if 'legend' not in kwargs: kwargs['legend'] = True gdf_plot = gpd.GeoDataFrame(imp_geom.geom_exp) gdf_plot['impact'] = imp_geom.eai_exp if centered: # pylint: disable=abstract-class-instantiated xmin, xmax = u_coord.lon_bounds(imp_geom.coord_exp[:,1]) proj_plot = ccrs.PlateCarree(central_longitude=0.5 * (xmin + xmax)) gdf_plot = gdf_plot.to_crs(proj_plot) return gdf_plot.plot(column = 'impact', **kwargs)
[docs] def exp_geom_to_pnt(exp, res, to_meters, disagg_met, disagg_val): """ Disaggregate exposures with (multi-)polygons and/or (multi-)lines geometries to points based on a given resolution. Parameters ---------- exp : Exposures The exposure instance with exp.gdf.geometry containing lines or polygons res : float Resolution of the disaggregation grid / distance. Can also be a tuple of [x_grid, y_grid] numpy arrays. In this case, to_meters is ignored. This is only possible for Polygon-only exposures. to_meters : bool If True, res is interpreted as meters, and geometries are projected to an equal area projection for disaggregation. The exposures are then projected back to the original projections before impact calculation. The default is False. disagg_met : DisaggMethod Disaggregation method of the shapes's original value onto its inter- polated points. 'DIV': Divide the value evenly over all the new points; 'FIX': Replicate the value onto all the new points. Default is 'DIV'. Works in combination with the kwarg 'disagg_val'. disagg_val: float, optional Specifies what number should be taken as the value, which is to be disaggregated according to the method provided in disagg_met. None: The shape's value is taken from the exp.gdf.value column. float: This given number will be disaggregated according to the method. In case exp.gdf.value column exists, original values in there will be ignored The default is None. Returns ------- exp_pnt : Exposures Exposures with a double index geodataframe, first level indicating membership of the original geometries of exp, second for the point disaggregation within each geometries. """ if disagg_val is not None: exp = exp.copy() exp.gdf['value'] = disagg_val if ((disagg_val is None) and ('value' not in exp.gdf.columns)): raise ValueError('There is no value column in the exposure gdf to'+ ' disaggregate from. Please set disagg_val explicitly.') gdf_pnt = gdf_to_pnts(exp.gdf, res, to_meters) # disaggregate value column if disagg_met is DisaggMethod.DIV: gdf_pnt = _disagg_values_div(gdf_pnt) # set lat lon and centroids exp_pnt = exp.copy(deep=False) exp_pnt.set_gdf(gdf_pnt) exp_pnt.set_lat_lon() return exp_pnt
[docs] def exp_geom_to_grid(exp, grid, disagg_met, disagg_val): """ Disaggregate exposures with (multi-)polygon geometries to points based on a pre-defined grid. Parameters ---------- exp : Exposures The exposure instance with exp.gdf.geometry containing polygons grid : np.array() Grid on which to disaggregate the exposures. Provided as two vectors [x_grid, y_grid]. disagg_met : DisaggMethod Disaggregation method of the shapes's original value onto its inter- polated points. 'DIV': Divide the value evenly over all the new points; 'FIX': Replicate the value onto all the new points. Default is 'DIV'. Works in combination with the kwarg 'disagg_val'. disagg_val: float, optional Specifies what number should be taken as the value, which is to be disaggregated according to the method provided in disagg_met. None: The shape's value is taken from the exp.gdf.value column. float: This given number will be disaggregated according to the method. In case exp.gdf.value column exists, original values in there will be ignored The default is None. Returns ------- exp_pnt : Exposures Exposures with a double index geodataframe, first level indicating membership of the original geometries of exp, second for the point disaggregation within each geometries. Note ---- Works with polygon geometries only. No points or lines are allowed. """ if disagg_val is not None: exp = exp.copy() exp.gdf.value = disagg_val if ((disagg_val is None) and ('value' not in exp.gdf.columns)): raise ValueError('There is no value column in the exposure gdf to'+ ' disaggregate from. Please set disagg_val explicitly.') gdf_pnt = gdf_to_grid(exp.gdf, grid) # disaggregate value column if disagg_met is DisaggMethod.DIV: gdf_pnt = _disagg_values_div(gdf_pnt) # set lat lon and centroids exp_pnt = exp.copy(deep=False) exp_pnt.set_gdf(gdf_pnt) exp_pnt.set_lat_lon() return exp_pnt
def _pnt_line_poly_mask(gdf): """ Mask for points, lines and polygons Parameters ---------- gdf : gpd.GeoDataFrame Feodataframe instance with gdf.geometry containing (multi)-lines or (multi-)polygons. Points are ignored. Returns ------- pnt_mask, line_mask, poly_mask : """ pnt_mask = gdf.geometry.apply(lambda x: isinstance(x, shgeom.Point)) line_mask = gdf.geometry.apply(lambda x: isinstance(x, shgeom.LineString)) line_mask |= gdf.geometry.apply(lambda x: isinstance(x, shgeom.MultiLineString)) poly_mask = gdf.geometry.apply(lambda x: isinstance(x, shgeom.Polygon)) poly_mask |= gdf.geometry.apply(lambda x: isinstance(x, shgeom.MultiPolygon)) return pnt_mask, line_mask, poly_mask
[docs] def gdf_to_pnts(gdf, res, to_meters): """ Disaggregate geodataframe with (multi-)polygons geometries to points. Parameters ---------- gdf : gpd.GeoDataFrame Feodataframe instance with gdf.geometry containing (multi)-lines or (multi-)polygons. Points are ignored. res : float Resolution of the disaggregation grid. Can also be a tuple of [x_grid, y_grid] numpy arrays. In this case, to_meters is ignored. to_meters : bool If True, the geometries are projected to an equal area projection before the disaggregation. res is then in meters. The exposures are then reprojected into the original projections before the impact calculation. Returns ------- gdf_pnt : gpd.GeoDataFrame with a double index, first for the geometries of exp, second for the point disaggregation of the geometries. """ if gdf.empty: return gdf pnt_mask, line_mask, poly_mask = _pnt_line_poly_mask(gdf) # Concatenating an empty dataframe with an index together with # a dataframe with a multi-index breaks the multi-index gdf_pnt = gpd.GeoDataFrame([]) if pnt_mask.any(): gdf_pnt_only = gdf[pnt_mask] gdf_pnt_only['geometry_orig'] = gdf_pnt_only['geometry'].copy() index = gdf_pnt_only.index.values gdf_pnt_only.index = pd.MultiIndex.from_arrays([index, np.zeros(len(index))]) gdf_pnt = gpd.GeoDataFrame(pd.concat([ gdf_pnt, gdf_pnt_only ])) if line_mask.any(): gdf_pnt = gpd.GeoDataFrame(pd.concat([ gdf_pnt, _line_to_pnts(gdf[line_mask], res, to_meters) ])) if poly_mask.any(): gdf_pnt = gpd.GeoDataFrame(pd.concat([ gdf_pnt, _poly_to_pnts(gdf[poly_mask], res, to_meters) ])) return gdf_pnt
[docs] def gdf_to_grid(gdf, grid): """ Disaggregate geodataframe with (multi-)polygons geometries to points based on a pre-defined grid. Parameters ---------- gdf : gpd.GeoDataFrame Geodataframe instance with gdf.geometry containing (multi-)polygons. grid : np.array() Grid on which to disaggregate the exposures. Provided as two vectors [x_grid, y_grid]. Returns ------- gdf_pnt : gpd.GeoDataFrame with a double index, first for the geometries of exp, second for the point disaggregation of the geometries. Note ---- Works only with polygon geometries. No mixed inputs (with lines or points) are allowed Raises ------ AttributeError : if other geometry types than polygons are contained in the dataframe """ if gdf.empty: return gdf pnt_mask, line_mask, poly_mask = _pnt_line_poly_mask(gdf) # Concatenating an empty dataframe with an index together with # a dataframe with a multi-index breaks the multi-index if (line_mask.any() or pnt_mask.any()): raise AttributeError("The dataframe contains lines and/or polygons." "Currently only polygon dataframes can be " "disaggregated onto a fixed grid.") if poly_mask.any(): return _poly_to_grid(gdf[poly_mask], grid) return gpd.GeoDataFrame([])
def _disagg_values_div(gdf_pnts): """ Disaggregate value column of original gdf to disaggregated point gdf by dividing value from geometry equally on points. Parameters ---------- gdf_pnts : gpd.GeoDataFrame Geodataframe with a double index, first for geometries (lines, polygons), second for the point disaggregation of the polygons. The value column is assumed to represent values per polygon / line (first index). Returns ------- gdf_disagg : gpd.GeoDataFrame The value per geometry are evenly distributed over the points per geometry. """ gdf_disagg = gdf_pnts.copy(deep=False) group = gdf_pnts.groupby(axis=0, level=0) vals = group.value.mean() / group.value.count() vals = vals.reindex(gdf_pnts.index, level=0) gdf_disagg['value'] = vals return gdf_disagg def _poly_to_pnts(gdf, res, to_meters): """ Disaggregate (multi-)polygons geodataframe to points. Note: If polygon is smaller than specified resolution, a representative point within the polygon will be chosen, nevertheless. This may lead to inaccuracies during value assignments / disaggregations. Parameters ---------- gdf : gpd.GeoDataFrame Can have any CRS res : float Resolution (same units as gdf crs) to_meters : bool If True, res is interpreted as meters, and geometries are projected to an equal area projection for disaggregation. Returns ------- gpd.GeoDataFrame Geodataframe with a double index, first for polygon geometries, second for the point disaggregation of the polygons. """ if gdf.empty: return gdf # Needed because gdf.explode(index_parts=True) requires numeric index idx = gdf.index.to_list() #To restore the naming of the index gdf_points = gdf.copy().reset_index(drop=True) # Check if we need to reproject if to_meters and not gdf.geometry.crs.is_projected: gdf_points['geometry_pnt'] = gdf_points.apply( lambda row: _interp_one_poly_m(row.geometry, res, gdf.crs), axis=1) else: gdf_points['geometry_pnt'] = gdf_points.apply( lambda row: _interp_one_poly(row.geometry, res), axis=1) gdf_points = _swap_geom_cols( gdf_points, geom_to='geometry_orig', new_geom='geometry_pnt') gdf_points = gdf_points.explode(index_parts=True) gdf_points.index = gdf_points.index.set_levels(idx, level=0) return gdf_points def _poly_to_grid(gdf, grid): """ Disaggregate (multi-)polygons geodataframe to points. Note: If polygon is smaller than specified resolution, a representative point within the polygon will be chosen, nevertheless. This may lead to inaccuracies during value assignments / disaggregations. Parameters ---------- gdf : gpd.GeoDataFrame Can have any CRS grid : np.array() Grid on which to disaggregate the exposures. Provided as two vectors [x_grid, y_grid]. Returns ------- gpd.GeoDataFrame Geodataframe with a double index, first for polygon geometries, second for the point disaggregation of the polygons. """ if gdf.empty: return gdf # Needed because gdf.explode(index_parts=True) requires numeric index idx = gdf.index.to_list() #To restore the naming of the index gdf_points = gdf.copy().reset_index(drop=True) x_grid, y_grid = grid gdf_points['geometry_pnt'] = gdf_points.apply( lambda row: _interp_one_poly_grid(row.geometry, x_grid, y_grid), axis=1) gdf_points = _swap_geom_cols( gdf_points, geom_to='geometry_orig', new_geom='geometry_pnt') gdf_points = gdf_points.explode(index_parts=True) gdf_points.index = gdf_points.index.set_levels(idx, level=0) return gdf_points def _interp_one_poly_grid(poly, x_grid, y_grid): """ Disaggregate a single polygon to points on grid (does not have to be a regular raster) Parameters ---------- poly : shapely Polygon Polygon x_grid : np.array 1D array of x-coordinates of grid points. y_grid : np.array 1D array of y-coordinates of grid points. Returns ------- shapely multipoint Grid of points inside the polygon """ if poly.is_empty: return shgeom.MultiPoint([]) in_geom = sh.vectorized.contains(poly, x_grid, y_grid) if sum(in_geom.flatten()) > 1: return shgeom.MultiPoint(list(zip(x_grid[in_geom], y_grid[in_geom]))) LOGGER.warning('Polygon smaller than resolution. Setting a representative point.') return shgeom.MultiPoint([poly.representative_point()]) def _interp_one_poly(poly, res): """ Disaggregate a single polygon to points Parameters ---------- poly : shapely Polygon Polygon res : float Resolution (same units as gdf crs) Returns ------- shapely multipoint Grid of points rasterizing the polygon """ if poly.is_empty: return shgeom.MultiPoint([]) height, width, trafo = u_coord.pts_to_raster_meta(poly.bounds, (res, res)) x_grid, y_grid = u_coord.raster_to_meshgrid(trafo, width, height) in_geom = sh.vectorized.contains(poly, x_grid, y_grid) if sum(in_geom.flatten()) > 1: return shgeom.MultiPoint(list(zip(x_grid[in_geom], y_grid[in_geom]))) LOGGER.warning('Polygon smaller than resolution. Setting a representative point.') return shgeom.MultiPoint([poly.representative_point()]) def _interp_one_poly_m(poly, res, orig_crs): """ Disaggregate a single polygon to points for resolution given in meters. Transforms coordinates into an adequate projected equal-area crs for this. Parameters ---------- poly : shapely Polygon Polygon res : float Resolution in meters orig_crs: pyproj.CRS CRS of the polygon Returns ------- shapely multipoint Grid of points rasterizing the polygon """ if poly.is_empty: return shgeom.MultiPoint([]) m_crs = _get_equalarea_proj(poly) poly_m = reproject_poly(poly, orig_crs, m_crs) height, width, trafo = u_coord.pts_to_raster_meta(poly_m.bounds, (res, res)) x_grid, y_grid = u_coord.raster_to_meshgrid(trafo, width, height) in_geom = sh.vectorized.contains(poly_m, x_grid, y_grid) if sum(in_geom.flatten()) > 1: x_poly, y_poly = reproject_grid( x_grid[in_geom], y_grid[in_geom], m_crs, orig_crs) return shgeom.MultiPoint(list(zip(x_poly, y_poly))) LOGGER.warning('Polygon smaller than resolution. Setting a representative point.') return shgeom.MultiPoint([poly.representative_point()]) def _get_equalarea_proj(poly): """ Find an adequate Equal Area Cylindrical projection using a representative point as lat/lon reference. https://proj.org/operations/projections/cea.html """ repr_pnt = poly.representative_point() lon_0, lat_0 = repr_pnt.x, repr_pnt.y return f"+proj=cea +lat_0={lat_0:.6f} +lon_0={lon_0:.6f} +units=m" def _get_pyproj_trafo(orig_crs, dest_crs): """ Get pyproj projection from orig_crs to dest_crs """ return pyproj.Transformer.from_proj(pyproj.Proj(orig_crs), pyproj.Proj(dest_crs), always_xy=True)
[docs] def reproject_grid(x_grid, y_grid, orig_crs, dest_crs): """ Reproject a grid from one crs to another Parameters ---------- x_grid : x-coordinates y_grid : y-coordinates orig_crs: pyproj.CRS original CRS of the grid dest_crs: pyproj.CRS CRS of the grid to be reprojected to Returns ------- x_trafo, y_trafo : Grid coordinates in reprojected crs """ project = _get_pyproj_trafo(orig_crs, dest_crs) x_trafo, y_trafo = project.transform(x_grid, y_grid) return x_trafo, y_trafo
[docs] def reproject_poly(poly, orig_crs, dest_crs): """ Reproject a polygon from one crs to another Parameters ---------- poly : shapely Polygon Polygon orig_crs: pyproj.CRS original CRS of the polygon dest_crs: pyproj.CRS CRS of the polygon to be reprojected to Returns ------- poly : shapely Polygon Polygon in desired projection """ project = _get_pyproj_trafo(orig_crs, dest_crs) return sh.ops.transform(project.transform, poly)
def _line_to_pnts(gdf_lines, res, to_meters): """ Convert a GeoDataFrame with LineString geometries to Point geometries, where Points are placed at a specified distance (in meters, if applicable) along the original LineString. Each line is reduced to at least two points. Parameters ---------- gdf_lines : gpd.GeoDataFrame Geodataframe with line geometries res : float Resolution (distance) apart from which the generated Points should be approximately placed. to_meters : bool If True, res is interpreted as meters, and geometries are projected to an equal area projection for disaggregation. Returns ------- gdf_points : gpd.GeoDataFrame Geodataframe with a double index, first for line geometries, second for the point disaggregation of the lines (i.e. one Point per row). See also -------- climada.util.coordinates.compute_geodesic_lengths """ if gdf_lines.empty: return gdf_lines # Needed because gdf.explode(index_parts=True) requires numeric index idx = gdf_lines.index.to_list() #To restore the naming of the index gdf_points = gdf_lines.copy().reset_index(drop=True) if to_meters: line_lengths = u_coord.compute_geodesic_lengths(gdf_points) else: line_lengths = gdf_lines.length # Add warning if lines are too short w.r.t. resolution failing_res_check_count = len(line_lengths[line_lengths > 10*res]) if failing_res_check_count > 0: LOGGER.warning( "%d lines with a length < 10*resolution were found. " "Each of these lines is disaggregate to one point. " "Reaggregatint values will thus likely lead to overestimattion. " "Consider chosing a smaller resolution or filter out the short lines. ", failing_res_check_count ) line_fractions = [ _line_fraction(length, res) for length in line_lengths ] gdf_points['geometry_pnt'] = [ shgeom.MultiPoint([ line.interpolate(dist, normalized=True) for dist in fractions ]) for line, fractions in zip(gdf_points.geometry, line_fractions) ] gdf_points = _swap_geom_cols( gdf_points, geom_to='geometry_orig', new_geom='geometry_pnt') gdf_points = gdf_points.explode(index_parts=True) gdf_points.index = gdf_points.index.set_levels(idx, level=0) return gdf_points def _line_fraction(length, res): """ Compute the fraction in which to divide a line of given length at given resolution Parameters ---------- length : float Length of a string res : float Resolution (length of a string element) Returns ------- np.ndarray Array of the fraction at which to divide the string of given length into points at the chosen resolution. """ nb_points = _pnts_per_line(length, res) eff_res = 1 / nb_points start = eff_res / 2 return np.arange(start, 1, eff_res) def _pnts_per_line(length, res): """Calculate number of points fitting along a line, given a certain resolution (spacing) res between points. Parameters ---------- length : float res : float Returns -------- int Number of points along line """ return int(max(np.round(length / res), 1)) def _swap_geom_cols(gdf, geom_to, new_geom): """ Change which column is the geometry column. Conserves the crs of the original geometry column. Parameters ---------- gdf : gpd.GeoDataFrame Input geodatafram geom_to : string New name of the current 'geometry' column new_geom : string Column that should be set as the 'geometry' column. The column new_geom is renamed to 'geometry' Returns ------- gdf_swap : gpd.GeoDataFrame Copy of gdf with the new geometry column """ gdf_swap = gdf.rename(columns = {'geometry': geom_to}) gdf_swap.rename(columns = {new_geom: 'geometry'}, inplace=True) gdf_swap.set_geometry('geometry', inplace=True, crs=gdf.crs) return gdf_swap # TODO: To be removed in a future iteration and included directly into the impact class
[docs] def set_imp_mat(impact, imp_mat): """ Set Impact attributes from the impact matrix. Returns a copy. Overwrites eai_exp, at_event, aai_agg, imp_mat. Parameters ---------- impact : Impact Impact instance. imp_mat : sparse.csr_matrix matrix num_events x num_exp with impacts. Returns ------- imp : Impact Copy of impact with eai_exp, at_event, aai_agg, imp_mat set. """ imp = copy.deepcopy(impact) imp.eai_exp = eai_exp_from_mat(imp_mat, imp.frequency) imp.at_event = at_event_from_mat(imp_mat) imp.aai_agg = aai_agg_from_at_event(imp.at_event, imp.frequency) imp.imp_mat = imp_mat return imp
[docs] def eai_exp_from_mat(imp_mat, freq): """ Compute impact for each exposures from the total impact matrix Parameters ---------- imp_mat : sparse.csr_matrix matrix num_events x num_exp with impacts. frequency : np.array frequency of events Returns ------- eai_exp : np.array expected impact for each exposure within a period of 1/frequency_unit """ freq_mat = freq.reshape(len(freq), 1) return imp_mat.multiply(freq_mat).sum(axis=0).A1
[docs] def at_event_from_mat(imp_mat): """ Compute impact for each hazard event from the total impact matrix Parameters ---------- imp_mat : sparse.csr_matrix matrix num_events x num_exp with impacts. Returns ------- at_event : np.array impact for each hazard event """ return np.squeeze(np.asarray(np.sum(imp_mat, axis=1)))
[docs] def aai_agg_from_at_event(at_event, freq): """ Aggregate impact.at_event Parameters ---------- at_event : np.array impact for each hazard event frequency : np.array frequency of event Returns ------- float average impact within a period of 1/frequency_unit, aggregated """ return sum(at_event * freq)