Skip to content

Commit d67c804

Browse files
Merge pull request #898 from CLIMADA-project/feature/write_haz_rp_maps2
Add functions to compute, plot, store the local hazard exceedence intensity and RP maps (new branch)
2 parents 9538bb5 + 7f61c52 commit d67c804

File tree

6 files changed

+309
-165
lines changed

6 files changed

+309
-165
lines changed

AUTHORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@
3535
* Timo Schmid
3636
* Luca Severino
3737
* Samuel Juhel
38+
* Valentin Gebhart

climada/hazard/base.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from typing import Optional,List
2828
import warnings
2929

30+
import geopandas as gpd
3031
import numpy as np
3132
from pathos.pools import ProcessPool as Pool
3233
from scipy import sparse
@@ -450,6 +451,75 @@ def sanitize_event_ids(self):
450451
LOGGER.debug('Resetting event_id.')
451452
self.event_id = np.arange(1, self.event_id.size + 1)
452453

454+
def local_return_period(self, threshold_intensities=(5., 10., 20.)):
455+
"""Compute local return periods for given hazard intensities. The used method
456+
is fitting the ordered intensitites per centroid to the corresponding cummulated
457+
frequency with a step function.
458+
459+
Parameters
460+
----------
461+
threshold_intensities : np.array
462+
User-specified hazard intensities for which the return period should be calculated
463+
locally (at each centroid). Defaults to (5, 10, 20)
464+
465+
Returns
466+
-------
467+
gdf : gpd.GeoDataFrame
468+
GeoDataFrame containing return periods for given threshold intensities. Each column
469+
corresponds to a threshold_intensity value, each row corresponds to a centroid. Values
470+
in the gdf correspond to the return period for the given centroid and
471+
threshold_intensity value
472+
label : str
473+
GeoDataFrame label, for reporting and plotting
474+
column_label : function
475+
Column-label-generating function, for reporting and plotting
476+
"""
477+
#check frequency unit
478+
if self.frequency_unit in ['1/year', 'annual', '1/y', '1/a']:
479+
rp_unit = 'Years'
480+
elif self.frequency_unit in ['1/month', 'monthly', '1/m']:
481+
rp_unit = 'Months'
482+
elif self.frequency_unit in ['1/week', 'weekly', '1/w']:
483+
rp_unit = 'Weeks'
484+
else:
485+
LOGGER.warning("Hazard's frequency unit %s is not known, "
486+
"years will be used as return period unit.", self.frequency_unit)
487+
rp_unit = 'Years'
488+
489+
# Ensure threshold_intensities is a numpy array
490+
threshold_intensities = np.array(threshold_intensities)
491+
492+
num_cen = self.intensity.shape[1]
493+
return_periods = np.zeros((len(threshold_intensities), num_cen))
494+
495+
# batch_centroids = number of centroids that are handled in parallel:
496+
# batch_centroids = maximal matrix size // number of events
497+
batch_centroids = CONFIG.max_matrix_size.int() // self.intensity.shape[0]
498+
if batch_centroids < 1:
499+
raise ValueError('Increase max_matrix_size configuration parameter to >'
500+
f'{self.intensity.shape[0]}')
501+
502+
# Process the intensities in chunks of centroids
503+
for start_col in range(0, num_cen, batch_centroids):
504+
end_col = min(start_col + batch_centroids, num_cen)
505+
return_periods[:, start_col:end_col] = self._loc_return_period(
506+
threshold_intensities,
507+
self.intensity[:, start_col:end_col].toarray()
508+
)
509+
510+
# create the output GeoDataFrame
511+
gdf = gpd.GeoDataFrame(geometry = self.centroids.gdf['geometry'],
512+
crs = self.centroids.gdf.crs)
513+
col_names = [f'{tresh_inten}' for tresh_inten in threshold_intensities]
514+
gdf[col_names] = return_periods.T
515+
516+
# create label and column_label
517+
label = f'Return Periods ({rp_unit})'
518+
column_label = lambda column_names: [f'Threshold Intensity: {col} {self.units}'
519+
for col in column_names]
520+
521+
return gdf, label, column_label
522+
453523
def get_event_id(self, event_name):
454524
"""Get an event id from its name. Several events might have the same
455525
name.
@@ -614,6 +684,52 @@ def _loc_return_inten(self, return_periods, inten, exc_inten):
614684
inten_sort[:, cen_idx], freq_sort[:, cen_idx],
615685
self.intensity_thres, return_periods)
616686

687+
def _loc_return_period(self, threshold_intensities, inten):
688+
"""Compute local return periods for user-specified threshold intensities
689+
for a subset of hazard centroids
690+
691+
Parameters
692+
----------
693+
threshold_intensities: np.array
694+
User-specified hazard intensities for which the return period should be calculated
695+
locally (at each centroid).
696+
inten: np.array
697+
subarray of full hazard intensities corresponding to a subset of the centroids
698+
(rows corresponds to events, columns correspond to centroids)
699+
700+
Returns
701+
-------
702+
np.array
703+
(rows corresponds to threshold_intensities, columns correspond to centroids)
704+
"""
705+
# Assuming inten is sorted and calculating cumulative frequency
706+
sort_pos = np.argsort(inten, axis=0)[::-1, :]
707+
inten_sort = inten[sort_pos, np.arange(inten.shape[1])]
708+
freq_sort = self.frequency[sort_pos]
709+
freq_sort = np.cumsum(freq_sort, axis=0)
710+
return_periods = np.zeros((len(threshold_intensities), inten.shape[1]))
711+
712+
for cen_idx in range(inten.shape[1]):
713+
sorted_inten_cen = inten_sort[:, cen_idx]
714+
cum_freq_cen = freq_sort[:, cen_idx]
715+
716+
for i, intensity in enumerate(threshold_intensities):
717+
# Find the first occurrence where the intensity is less than the sorted intensities
718+
exceedance_index = np.searchsorted(sorted_inten_cen[::-1], intensity, side='left')
719+
720+
# Calculate exceedance probability
721+
if exceedance_index < len(cum_freq_cen):
722+
exceedance_probability = cum_freq_cen[-exceedance_index - 1]
723+
else:
724+
exceedance_probability = 0 # Or set a default minimal probability
725+
726+
# Calculate and store return period
727+
if exceedance_probability > 0:
728+
return_periods[i, cen_idx] = 1 / exceedance_probability
729+
else:
730+
return_periods[i, cen_idx] = np.nan
731+
return return_periods
732+
617733
def _check_events(self):
618734
"""Check that all attributes but centroids contain consistent data.
619735
Put default date, event_name and orig if not provided. Check not

climada/hazard/test/test_base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,25 @@ def test_ref_all_pass(self):
10221022
self.assertAlmostEqual(inten_stats[1][66], 70.608592953031405)
10231023
self.assertAlmostEqual(inten_stats[3][33], 88.510983305123631)
10241024
self.assertAlmostEqual(inten_stats[2][99], 79.717518054203623)
1025+
1026+
def test_local_return_period(self):
1027+
"""Compare local return periods against reference."""
1028+
haz = dummy_hazard()
1029+
haz.intensity = sparse.csr_matrix([
1030+
[1., 5., 1.],
1031+
[2., 2., 0.]
1032+
])
1033+
haz.frequency = np.full(4, 1.)
1034+
threshold_intensities = np.array([1., 2., 3.])
1035+
return_stats, _, _ = haz.local_return_period(threshold_intensities)
1036+
np.testing.assert_allclose(
1037+
return_stats[return_stats.columns[1:]].values.T,
1038+
np.array([
1039+
[0.5, 0.5, 1.],
1040+
[1., 0.5, np.nan],
1041+
[np.nan, 1., np.nan]
1042+
])
1043+
)
10251044

10261045

10271046
class TestYearset(unittest.TestCase):

climada/util/plot.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
4545
from rasterio.crs import CRS
4646
import requests
47+
import geopandas as gpd
4748

4849
from climada.util.constants import CMAP_EXPOSURES, CMAP_CAT, CMAP_RASTER
4950
from climada.util.files_handler import to_list
@@ -874,3 +875,80 @@ def multibar_plot(ax, data, colors=None, total_width=0.8, single_width=1,
874875
# Draw legend if we need
875876
if legend:
876877
ax.legend(bars, data.keys())
878+
879+
def subplots_from_gdf(
880+
gdf: gpd.GeoDataFrame,
881+
colorbar_name: str = None,
882+
title_subplots: callable = None,
883+
smooth=True,
884+
axis=None,
885+
figsize=(9, 13),
886+
adapt_fontsize=True,
887+
**kwargs
888+
):
889+
"""Plot several subplots from different columns of a GeoDataFrame, e.g., for
890+
plotting local return periods or local exceedance intensities.
891+
892+
Parameters
893+
----------
894+
gdf: gpd.GeoDataFrame
895+
return periods per threshold intensity
896+
colorbar_name: str
897+
title of the subplots' colorbars
898+
title_subplots: function
899+
function that generates the titles of the different subplots using the columns' names
900+
smooth: bool, optional
901+
Smooth plot to plot.RESOLUTION x plot.RESOLUTION. Default is True
902+
axis: matplotlib.axes._subplots.AxesSubplot, optional
903+
Axis to use. Default is None
904+
figsize: tuple, optional
905+
Figure size for plt.subplots. Default is (9, 13)
906+
adapt_fontsize: bool, optional
907+
If set to true, the size of the fonts will be adapted to the size of the figure.
908+
Otherwise the default matplotlib font size is used. Default is True.
909+
kwargs: optional
910+
Arguments for pcolormesh matplotlib function used in event plots.
911+
912+
Returns
913+
-------
914+
axis: matplotlib.axes._subplots.AxesSubplot
915+
Matplotlib axis with the plot.
916+
"""
917+
# check if inputs are correct types
918+
if not isinstance(gdf, gpd.GeoDataFrame):
919+
raise ValueError("gdf is not a GeoDataFrame")
920+
gdf = gdf[['geometry', *[col for col in gdf.columns if col != 'geometry']]]
921+
922+
# read meta data for fig and axis labels
923+
if not isinstance(colorbar_name, str):
924+
print("Unknown colorbar name. Colorbar label will be missing.")
925+
colorbar_name = ''
926+
if not callable(title_subplots):
927+
print("Unknown subplot-title-generation function. Subplot titles will be column names.")
928+
title_subplots = lambda cols: [f"{col}" for col in cols]
929+
930+
# change default plot kwargs if plotting return periods
931+
if colorbar_name.strip().startswith('Return Period'):
932+
if 'cmap' not in kwargs.keys():
933+
kwargs.update({'cmap': 'viridis_r'})
934+
if 'norm' not in kwargs.keys():
935+
kwargs.update(
936+
{'norm': mpl.colors.LogNorm(
937+
vmin=gdf.values[:,1:].min(), vmax=gdf.values[:,1:].max()
938+
),
939+
'vmin': None, 'vmax': None}
940+
)
941+
942+
axis = geo_im_from_array(
943+
gdf.values[:,1:].T,
944+
gdf.geometry.get_coordinates().values[:,::-1],
945+
colorbar_name,
946+
title_subplots(gdf.columns[1:]),
947+
smooth=smooth,
948+
axes=axis,
949+
figsize=figsize,
950+
adapt_fontsize=adapt_fontsize,
951+
**kwargs
952+
)
953+
954+
return axis

climada/util/test/test_plot.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import matplotlib.pyplot as plt
2626
from matplotlib import colormaps as cm
2727
import cartopy.crs as ccrs
28+
import geopandas as gpd
29+
from shapely import Point
2830

2931
import climada.util.plot as u_plot
3032

@@ -150,6 +152,21 @@ def test_geo_im_from_array(self):
150152
self.assertEqual(cmap, ax.collections[0].cmap.name)
151153
plt.close()
152154

155+
def test_subplots_from_gdf(self):
156+
return_periods = gpd.GeoDataFrame(
157+
data = ((2., 5.), (3., 6.), (None, 2.), (1., 7.)),
158+
columns = ('10.0', '20.0')
159+
)
160+
return_periods['geometry'] = (Point(45., 26.), Point(46., 26.), Point(45., 27.), Point(46., 27.))
161+
colorbar_name = 'Return Periods (Years)'
162+
title_subplots = lambda cols: [f'Threshold Intensity: {col} m/s' for col in cols]
163+
(axis1, axis2) = u_plot.subplots_from_gdf(
164+
return_periods,
165+
colorbar_name=colorbar_name,
166+
title_subplots=title_subplots)
167+
self.assertEqual('Threshold Intensity: 10.0 m/s', axis1.get_title())
168+
self.assertEqual('Threshold Intensity: 20.0 m/s', axis2.get_title())
169+
plt.close()
153170

154171
# Execute Tests
155172
if __name__ == "__main__":

doc/tutorial/climada_hazard_Hazard.ipynb

Lines changed: 78 additions & 165 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)