├── CATRiskModellingSandbox_codes.py ├── CATRiskModellingSandbox_tutorial.html ├── CATRiskModellingSandbox_tutorial.ipynb ├── CAT_StarterKit.R ├── CAT_StarterKit.py ├── LICENSE ├── README.md ├── figures ├── fig3_10_sandbox_env_COLOR.jpg ├── fig3_11_sandbox_fp_COLOR.jpg └── movie_LS.gif ├── inputs ├── grid.json ├── layer_landuse_S.npy ├── layer_roadNetwork.npy ├── layer_soil_hs.npy ├── layer_topo_z.npy └── src.json ├── plots_template_Python.pdf └── plots_template_R.pdf /CATRiskModellingSandbox_codes.py: -------------------------------------------------------------------------------- 1 | # Title: CAT Risk Modelling Sandbox 2 | # Author: Arnaud Mignan 3 | # Date: 17.05.2024 4 | # Description: Functions associated to the notebook CATRiskModellingSandbox_tutorial.ipynb to model risks in a virtual environment. 5 | # License: MIT 6 | # Version: 1.1 7 | # Dependencies: numpy, pandas, matplotlib, json, networkx, os, scipy, re, imageio, skimage 8 | # Contact: arnaud@mignanriskanalytics.com 9 | # Citation: Mignan, A. (2025), Introduction to Catastrophe Risk Modelling – A Physics-based Approach. Cambridge University Press, DOI: 10.1017/9781009437370 10 | 11 | #Copyright 2024 A. Mignan 12 | # 13 | #Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), 14 | #to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | #and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | # 17 | #The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | # 19 | #THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | #IN THE SOFTWARE. 23 | 24 | 25 | # Equation and section numbers provided below are from the textbook. 26 | 27 | import json 28 | import matplotlib.pyplot as plt 29 | import matplotlib.colors as plt_col 30 | from matplotlib.patches import Patch 31 | from matplotlib.lines import Line2D 32 | import networkx as netx 33 | import numpy as np 34 | import os 35 | import pandas as pd 36 | import scipy 37 | import re 38 | import imageio 39 | from skimage import measure 40 | 41 | wd = os.getcwd() 42 | 43 | ################## 44 | ## INPUT/OUTPUT ## 45 | ################## 46 | def load_json2dict(filename): 47 | file = open(filename, 'rb') 48 | data = json.load(file) 49 | return data 50 | 51 | 52 | ############### 53 | ## UTILITIES ## 54 | ############### 55 | def partitioning(IDs, w, n): 56 | ''' 57 | Return a 1D array of length n of IDs based on their weights w 58 | ''' 59 | indsort = np.argsort(w) 60 | cumPr = np.cumsum(w[indsort]) 61 | midpt = np.arange(1/n, 1+1/n, 1/n) - 1/n/2 62 | vec_IDs = np.zeros(n).astype(int) 63 | for i in range(n): 64 | vec_IDs[i] = IDs[indsort][np.argwhere(cumPr > midpt[i])[0]] 65 | return np.sort(vec_IDs) 66 | 67 | def fetch_A0(region): 68 | R_earth = 6371. # [km] 69 | A_earth = 4 * np.pi * R_earth**2 # [km^2] 70 | A_CONUS = 8080464. # [km^2] 71 | regions = ['global', 'CONUS'] 72 | areas = [A_earth, A_CONUS] 73 | return areas[region == regions] 74 | 75 | def zero_boundary_2d(arr, nx, ny): 76 | arr[:nx,:] = 0 77 | arr[-nx:,:] = 0 78 | arr[:,:ny] = 0 79 | arr[:,-ny:] = 0 80 | return arr 81 | 82 | 83 | ############################# 84 | ## ENVIRONMENTAL FUNCTIONS ## 85 | ############################# 86 | class RasterGrid: 87 | """Define the coordinates (x,y) of the square-pixels of a 2D raster grid. 88 | 89 | Notes: 90 | If x0, xbuffer, ybuffer and/or lat_deg are not provided by the user, 91 | they are fixed to xmin, 0, 0 and 45, respectively. 92 | 93 | Attributes: 94 | par (dict): Input, dictionary with keys ['w', 'xmin', 'x0' (opt.), 95 | 'xmax', 'ymin', 'ymax', 'xbuffer' (opt.), 'ybuffer' (opt.)] 96 | w (float): Pixel width in km 97 | xmin (float): Minimum abcissa of buffer box 98 | xmax (float): Maximum abcissa of buffer box 99 | ymin (float): Minimum ordinate of buffer box 100 | ymax (float): Maximum ordinate of buffer box 101 | xbuffer (float): Buffer width in the x direction (default is 0.) 102 | ybuffer (float): Buffer width in the y direction (default is 0.) 103 | lat_deg (float): Latitude at center of the grid (default is 45.) 104 | x0 (float): Abscissa of reference N-S coastline (default is xmin) 105 | x (ndarray(dtype=float, ndim=1)): 1D array of unique abscissas 106 | y (ndarray(dtype=float, ndim=1)): 1D array of unique ordinates 107 | xx (ndarray(dtype=float, ndim=2)): 2D array of grid abscissas 108 | yy (ndarray(dtype=float, ndim=2)): 2D array of grid ordinates 109 | nx (int): Length of x 110 | ny (int): Length of y 111 | 112 | Returns: 113 | class instance: A new instance of class RasterGrid 114 | 115 | Example: 116 | Create a grid 117 | 118 | >>> grid = RasterGrid({'w': 1, 'xmin': 0, 'xmax': 2, 'ymin': 0, 'ymax': 3}) 119 | >>> grid.x 120 | array([0., 1., 2.]) 121 | >>> grid.y 122 | array([0., 1., 2., 3.]) 123 | >>> grid.xx 124 | array([[0., 0., 0., 0.], 125 | [1., 1., 1., 1.], 126 | [2., 2., 2., 2.]]) 127 | >>> grid.yy 128 | array([[0., 1., 2., 3.], 129 | [0., 1., 2., 3.], 130 | [0., 1., 2., 3.]]) 131 | """ 132 | 133 | def __init__(self, par): 134 | """ 135 | Initialize RasterGrid 136 | 137 | Args: 138 | par (dict): Dictionary of input parameters with the following keys: 139 | w (float): Pixel width in km 140 | xmin (float): Minimum abcissa of buffer box 141 | xmax (float): Maximum abcissa of buffer box 142 | ymin (float): Minimum ordinate of buffer box 143 | ymax (float): Maximum ordinate of buffer box 144 | xbuffer (float, optional): Buffer width in the x direction (default is 0) 145 | ybuffer (float, optional): Buffer width in the y direction (default is 0) 146 | x0 (float, optional): Abscissa of reference N-S coastline (default is xmin) 147 | """ 148 | self.par = par 149 | self.w = par['w'] 150 | self.xmin = par['xmin'] 151 | self.xmax = par['xmax'] 152 | self.ymin = par['ymin'] 153 | self.ymax = par['ymax'] 154 | if 'xbuffer' in par.keys(): 155 | self.xbuffer = par['xbuffer'] 156 | else: 157 | self.xbuffer = 0. 158 | if 'ybuffer' in par.keys(): 159 | self.ybuffer = par['ybuffer'] 160 | else: 161 | self.ybuffer = 0. 162 | if 'x0' in par.keys(): 163 | self.x0 = par['x0'] 164 | else: 165 | self.x0 = self.xmin 166 | if 'lat_deg' in par.keys(): 167 | self.lat_deg = par['lat_deg'] 168 | else: 169 | self.lat_deg = 45. 170 | self.x = np.arange(self.xmin - self.w/2, self.xmax + self.w/2, self.w) + self.w/2 171 | self.y = np.arange(self.ymin - self.w/2, self.ymax + self.w/2, self.w) + self.w/2 172 | self.xx, self.yy = np.meshgrid(self.x, self.y, indexing='ij') 173 | self.nx = len(self.x) 174 | self.ny = len(self.y) 175 | 176 | def __repr__(self): 177 | return 'RasterGrid({})'.format(self.par) 178 | 179 | 180 | def calc_coord_river_dampedsine(grid, par, z = ''): 181 | ''' 182 | Calculate the (x,y,z) coordinates of the river(s) defined from a damped sine wave. 183 | ''' 184 | nriv = len(par['riv_y0']) 185 | river_xi = np.array([]) 186 | river_id = np.array([]) 187 | river_yi = np.array([]) 188 | river_zi = np.array([]) 189 | for riv in range(nriv): 190 | expdecay = par['riv_A_km'][riv] * np.exp(-par['riv_lbd'][riv] * grid.x) 191 | yrv_0 = expdecay * np.cos(par['riv_ome'][riv] * grid.x) + par['riv_y0'][riv] 192 | indy = np.where(grid.y > par['riv_y0'][riv] - 1e-6)[0][0] 193 | if len(z) != 0: 194 | zrv_0 = z[:,indy] 195 | indland = np.where(zrv_0 >= 0) 196 | else: 197 | zrv_0 = np.zeros(grid.nx) 198 | indland = np.arange(grid.nx) 199 | river_xi = np.append(river_xi, grid.x[indland]) 200 | river_yi = np.append(river_yi, yrv_0[indland]) 201 | river_zi = np.append(river_zi, zrv_0[indland]) 202 | river_id = np.append(river_id, np.repeat(riv, grid.nx)[indland]) 203 | return river_xi, river_yi, river_zi, river_id 204 | 205 | 206 | def gen_rdmcoord_tracks(N, grid, npt, max_deviation): 207 | ''' 208 | Return coordinates of N storm tracks, defined as straight lines 209 | subject to random deviation (below max_deviation) along y at npt points. 210 | ''' 211 | ID = np.repeat(np.arange(N)+1, npt) 212 | x = np.tile(np.linspace(grid.xmin, grid.xmax, npt), N) 213 | ystart = grid.ymin + np.random.random(N) * (grid.ymax - grid.ymin) 214 | yend = grid.ymin + np.random.random(N) * (grid.ymax - grid.ymin) 215 | y = np.linspace(ystart, yend, npt, axis = 1).flatten() 216 | deviation = np.random.uniform(-max_deviation, max_deviation, size = N*npt) 217 | y += deviation 218 | return x, y, ID 219 | 220 | 221 | ###################### 222 | ## HAZARD FUNCTIONS ## 223 | ###################### 224 | 225 | ## SOURCE DEF. & EVENT RATE ## 226 | def incrementing(xmin, xmax, xbin, scale): 227 | ''' 228 | Return evenly spaced values within a given interval in linear or log scale 229 | ''' 230 | if scale == 'linear': 231 | xi = np.arange(xmin, xmax + xbin, xbin) 232 | if scale == 'log': 233 | xi = 10**np.arange(np.log10(xmin), np.log10(xmax) + xbin, xbin) 234 | return xi 235 | 236 | def get_peril_evID(evID): 237 | ''' 238 | Return the peril identifiers for an array of event identifiers 239 | ''' 240 | return np.array([evID[k][0:2] for k in range(len(evID))]) 241 | 242 | def calc_Lbd_powerlaw(S, a, b): 243 | ''' 244 | Calculate the cumulative rate Lbd according to a power-law (Eq. 2.38) given event size S 245 | ''' 246 | Lbd = 10**(a - b * np.log10(S)) 247 | return Lbd 248 | 249 | def calc_Lbd_exponential(S, a, b): 250 | ''' 251 | Calculate the cumulative rate Lbd according to an exponential law (Eq. 2.39) given event size S 252 | ''' 253 | Lbd = 10**(a - b * S) 254 | return Lbd 255 | 256 | def calc_Lbd_GPD(S, mu, xi, sigma, Lbdmin): 257 | ''' 258 | Calculate the cumulative rate Lbd according to the Generalised Pareto Distribution (Eq. 2.50) given event size S 259 | ''' 260 | Lbd = Lbdmin * (1 + xi * (S - mu) / sigma)**(-1 / xi) 261 | return Lbd 262 | 263 | 264 | def transform_cum2noncum(S, par): 265 | ''' 266 | Transform the rate from cumulative (Lbd) to non-cumulative (lbd) (e.g., Eq. 2.65) 267 | ''' 268 | if par['Sscale'] == 'linear': 269 | S_lo = S - par['Sbin']/2 270 | S_hi = S + par['Sbin']/2 271 | elif par['Sscale'] == 'log': 272 | S_lo = 10**(np.log10(S) - par['Sbin']/2) 273 | S_hi = 10**(np.log10(S) + par['Sbin']/2) 274 | if par['distr'] == 'powerlaw': 275 | Lbd_lo = calc_Lbd_powerlaw(S_lo, par['a'], par['b']) 276 | Lbd_hi = calc_Lbd_powerlaw(S_hi, par['a'], par['b']) 277 | if par['distr'] == 'exponential': 278 | Lbd_lo = calc_Lbd_exponential(S_lo, par['a'], par['b']) 279 | Lbd_hi = calc_Lbd_exponential(S_hi, par['a'], par['b']) 280 | if par['distr'] == 'GPD': 281 | Lbd_lo = calc_Lbd_GPD(S_lo, par['mu'], par['xi'], par['sigma'], par['Lbdmin']) 282 | Lbd_hi = calc_Lbd_GPD(S_hi, par['mu'], par['xi'], par['sigma'], par['Lbdmin']) 283 | lbd = Lbd_lo - Lbd_hi 284 | return lbd 285 | 286 | 287 | ## EQ CASE ## 288 | def get_char_srcLine(par): 289 | ''' 290 | Calculate coordinates of fault sources based on their extrema and the provided 291 | resolution, as well as lenghts and strikes of faults and fault segments. 292 | ''' 293 | src_xi = np.array([]) 294 | src_yi = np.array([]) 295 | src_id = np.array([]) 296 | src_L = np.array([]) 297 | seg_id = np.array([]) 298 | seg_strike = np.array([]) 299 | seg_L = np.array([]) 300 | seg = 0 301 | for src_i in range(len(par['x'])): 302 | Lsum = 0 303 | for seg_i in range(len(par['x'][src_i]) - 1): 304 | dx = par['x'][src_i][seg_i+1] - par['x'][src_i][seg_i] 305 | dy = par['y'][src_i][seg_i+1] - par['y'][src_i][seg_i] 306 | sign1 = dx / np.abs(dx) 307 | sign2 = dy / np.abs(dy) 308 | L = np.sqrt(dx**2 + dy**2) 309 | strike = np.arctan(dx/dy) * 180 / np.pi 310 | sign3 = np.sin(strike * np.pi / 180) / np.abs(np.sin(strike * np.pi / 180)) 311 | npt = int(np.round(L / par['bin_km'])) 312 | seg_xi = np.zeros(npt) 313 | seg_yi = np.zeros(npt) 314 | seg_xi[0] = par['x'][src_i][seg_i] 315 | seg_yi[0] = par['y'][src_i][seg_i] 316 | for k in range(1, npt): 317 | seg_xi[k] = seg_xi[k-1] + sign1 * sign3 * par['bin_km'] * np.sin(strike * np.pi / 180) 318 | seg_yi[k] = seg_yi[k-1] + sign2 * par['bin_km'] * np.cos(strike * np.pi / 180) 319 | src_xi = np.append(src_xi, np.append(seg_xi, par['x'][src_i][seg_i+1])) 320 | src_yi = np.append(src_yi, np.append(seg_yi, par['y'][src_i][seg_i+1])) 321 | src_id = np.append(src_id, np.repeat(src_i, len(seg_xi)+1)) 322 | seg_id = np.append(seg_id, np.repeat(seg, len(seg_xi)+1)) 323 | seg_strike = np.append(seg_strike, strike) 324 | seg_L = np.append(seg_L, L) 325 | seg += 1 326 | Lsum += L 327 | src_L = np.append(src_L, Lsum) 328 | return {'x': src_xi, 'y': src_yi, 'srcID': src_id, 'srcL': src_L, 'segID': seg_id, 'strike': seg_strike, 'segL': seg_L} 329 | 330 | def calc_EQ_length2magnitude(L): 331 | ''' 332 | Given the earthquake rupture L [km], calculate the magnitude M 333 | ''' 334 | c1, c2 = [5., 1.22] # reverse case, Fig. 2.6b, Wells and Coppersmith (1994) 335 | M = c1 + c2 * np.log10(L) 336 | return np.round(M, decimals = 1) 337 | 338 | def calc_EQ_magnitude2length(M): 339 | ''' 340 | Given the earthquake magnitude M, calculate the rupture length L [km] 341 | (for floating rupture computations) 342 | ''' 343 | c1, c2 = [5., 1.22] # reverse case, Fig. 2.6b, Wells and Coppersmith (1994) 344 | L = 10**((M - c1)/c2) 345 | return L 346 | 347 | def pop_EQ_floatingRupture(evIDi, Si, src, srcEQ_char): 348 | ''' 349 | ''' 350 | nRup = len(Si) 351 | li = calc_EQ_magnitude2length(Si) 352 | flt_x = srcEQ_char['x'] 353 | flt_y = srcEQ_char['y'] 354 | flt_L = srcEQ_char['srcL'] 355 | flt_id = srcEQ_char['srcID'] 356 | indflt = partitioning(np.arange(len(flt_L)), flt_L / np.sum(flt_L), nRup) # longer faults visited more often 357 | Rup_loc = np.zeros(nRup, dtype = object) 358 | Rup_coord = pd.DataFrame(columns = ['evID', 'x', 'y', 'loc']) 359 | i = 0 360 | while i < nRup: 361 | flt_target = np.random.choice(indflt, 1) 362 | indID = flt_id == flt_target 363 | src_x = flt_x[indID] 364 | src_y = flt_y[indID] 365 | src_L = flt_L[flt_target] 366 | init = np.floor((src_L - li[i]) / src['EQ']['bin_km']) 367 | if src_L >= li[i]: 368 | u = np.ceil(np.random.random(1) * init).astype(int)[0] # random rupture start loc 369 | Rup_x = src_x[u:(u + li[i] / src['EQ']['bin_km']).astype(int)] 370 | Rup_y = src_y[u:(u + li[i] / src['EQ']['bin_km']).astype(int)] 371 | Rup_loc[i] = src['EQ']['object'] + str(flt_target[0] + 1) 372 | Rup_coord = pd.concat([Rup_coord, pd.DataFrame(data = {'evID': np.repeat(evIDi[i], len(Rup_x)), \ 373 | 'x': Rup_x, 'y': Rup_y, \ 374 | 'loc': np.repeat(Rup_loc[i], len(Rup_x))})], ignore_index=True) 375 | i += 1 376 | 377 | return Rup_coord 378 | 379 | 380 | ## TC CASE ## 381 | def get_TCtrack_highres(x0, y0, id0, bin_km): 382 | x_hires = np.array([]) 383 | y_hires = np.array([]) 384 | id_hires = np.array([]) 385 | evID = np.unique(id0) 386 | for i in range(len(evID)): 387 | indev = np.where(id0 == evID[i])[0] 388 | x_ev = x0[indev] 389 | y_ev = y0[indev] 390 | for seg in range(len(x_ev) - 1): 391 | dx = x_ev[seg + 1] - x_ev[seg] 392 | dy = y_ev[seg + 1] - y_ev[seg] 393 | sign1 = dx / np.abs(dx) 394 | sign2 = dy / np.abs(dy) 395 | L = np.sqrt(dx**2 + dy**2) 396 | strike = np.arctan(dx/dy) * 180 / np.pi 397 | npt = int(np.round(L / bin_km)) 398 | seg_xi = np.zeros(npt) 399 | seg_yi = np.zeros(npt) 400 | seg_xi[0] = x_ev[seg] 401 | seg_yi[0] = y_ev[seg] 402 | for k in range(1, npt): 403 | seg_xi[k] = seg_xi[k-1] + sign1 * sign2 * bin_km * np.sin(strike * np.pi / 180) 404 | seg_yi[k] = seg_yi[k-1] + sign1 * sign2 * bin_km * np.cos(strike * np.pi / 180) 405 | x_hires = np.append(x_hires, np.append(seg_xi, x_ev[seg + 1])) 406 | y_hires = np.append(y_hires, np.append(seg_yi, y_ev[seg + 1])) 407 | id_hires = np.append(id_hires, np.repeat(evID[i], len(seg_xi)+1)) 408 | 409 | Track_coord = pd.DataFrame({'x': x_hires, 'y': y_hires, 'ID': id_hires}) 410 | return Track_coord 411 | 412 | def calc_S_track(stochset, src, Track_coord): 413 | indperil = np.where(stochset['ID'] == 'TC')[0] 414 | evIDs = stochset['evID'][indperil].values 415 | vmax_start = stochset['S'][indperil].values 416 | S_alongtrack = {} 417 | for i in range(src['TC']['N']): 418 | indtrack = np.where(Track_coord['ID'] == i+1)[0] 419 | track_x = Track_coord['x'][indtrack].values 420 | track_y = Track_coord['y'][indtrack].values 421 | 422 | npt = len(indtrack) 423 | track_vmax = np.repeat(vmax_start[i], npt) # track over ocean at vmax_start 424 | 425 | # find inland section & reduce vmax 426 | d = [np.min(np.sqrt((track_x[j] - src['SS']['x'])**2 + \ 427 | (track_y[j] - src['SS']['y'])**2)) for j in range(npt)] 428 | indcoast = np.where(d == np.min(d))[0] 429 | d2coast = track_x[indcoast[0]:] - track_x[indcoast[0]] 430 | # ad-hoc decay relationship: 431 | track_vmax[indcoast[0]:] = vmax_start[i] * np.exp(-.1 / src['TC']['vforward_m/s'] * d2coast) 432 | 433 | S_alongtrack[evIDs[i]] = track_vmax 434 | return S_alongtrack 435 | 436 | 437 | ## STOCHASTIC EVENT SET ## 438 | def gen_eventset(src, sizeDistr): 439 | ev_stoch = pd.DataFrame(columns = ['ID', 'evID', 'S', 'lbd']) 440 | srcEQ_char = get_char_srcLine(src['EQ']) 441 | Mmax2 = calc_EQ_length2magnitude(srcEQ_char['srcL'][1]) # smaller of 2 hardcoded faults in src 442 | 443 | for ID in src['perils']: 444 | if ID in sizeDistr['primary']: 445 | # event ID definition # 446 | evID = [ID + str(i+1) for i in range(src[ID]['N'])] 447 | 448 | # size incrementation # 449 | Si = incrementing(sizeDistr[ID]['Smin'], sizeDistr[ID]['Smax'], sizeDistr[ID]['Sbin'], sizeDistr[ID]['Sscale']) 450 | 451 | # weighting how Si size distributed over N event sources 452 | Si_n = len(Si) 453 | Si_ind = np.arange(Si_n) 454 | if ID == 'EQ': 455 | # smaller events more often to test more spatial combinations on fault segments 456 | qi = np.linspace(1,11,Si_n) 457 | qi /= np.sum(qi) 458 | qi = np.sort(qi)[::-1] 459 | else: 460 | # equal weight 461 | qi = np.repeat(1./Si_n, Si_n) 462 | Si_ind_vec = partitioning(Si_ind, qi, src[ID]['N']) # distribute Si sizes into N event sources 463 | Si_vec = Si[Si_ind_vec] 464 | wi = 1 / np.array([np.count_nonzero(Si_ind_vec == i) for i in Si_ind]) 465 | wi_vec = [wi[Si_ind == i][0] for i in Si_ind_vec] # weight of rate(Si) at each of N locations 466 | 467 | # rate calculation # 468 | # calibrate event productivity 469 | if sizeDistr[ID]['distr'] == 'powerlaw': 470 | if 'a' not in sizeDistr[ID].keys(): 471 | rescaled = src['grid_A_km2'] / fetch_A0(sizeDistr[ID]['region']) 472 | sizeDistr[ID]['a'] = sizeDistr[ID]['a0'] + np.log10(rescaled) 473 | if sizeDistr[ID]['distr'] == 'GPD': 474 | if 'Lbdmin' not in sizeDistr[ID].keys(): 475 | rescaled = src['grid_A_km2'] / fetch_A0(sizeDistr[ID]['region']) 476 | sizeDistr[ID]['Lbdmin'] = sizeDistr[ID]['Lbdmin0'] * rescaled 477 | # calculate event rate (weighted) 478 | lbdi = transform_cum2noncum(Si_vec, sizeDistr[ID]) 479 | lbdi = lbdi * wi_vec 480 | ev_stoch = pd.concat([ev_stoch, pd.DataFrame(data = {'ID': np.repeat(ID, src[ID]['N']), 'evID': evID, 'S': Si_vec, 'lbd': lbdi})], ignore_index=True) 481 | 482 | if ID in sizeDistr['secondary']: 483 | trigger = sizeDistr[ID]['trigger'] 484 | evID = [ID + '_from' + trigger + str(i+1) for i in range(src[trigger]['N'])] 485 | Si_vec = np.repeat(np.nan, src[trigger]['N']) 486 | lbdi = np.repeat(np.nan, src[trigger]['N']) 487 | ev_stoch = pd.concat([ev_stoch, pd.DataFrame(data = {'ID': np.repeat(ID, src[trigger]['N']), 'evID': evID, 'S': Si_vec, 'lbd': lbdi})], ignore_index=True) 488 | 489 | return ev_stoch.reset_index(drop = True) 490 | 491 | 492 | ## HAZARD FOOTPRINT MODELS ## 493 | # analytical 494 | def calc_I_shaking_ms2(S, r): 495 | PGA_g = 10**(-1.34 + .23*S - np.log10(r)) # size = magnitude 496 | g_earth = 9.81 # [m/s^2] 497 | PGA_ms2 = PGA_g * g_earth 498 | return PGA_ms2 499 | 500 | def calc_I_blast_kPa(S, r): 501 | Z = r * 1e3 / (S * 1e6)**(1/3) # size = energy in kton TNT 502 | p_kPa = (1772/Z**3 - 114/Z**2 + 108/Z) 503 | return p_kPa 504 | 505 | def calc_I_ash_m(S, r): 506 | # assumes h0 proportional to V - e.g h0 = 1e-3 km for V=3 km3 (1980 Mt. St. Helens) 507 | h0 = 1e-3 /3 * S # size = volume in km3 508 | r_half = np.sqrt(S * np.log(2)**2 / (2* np.pi * h0) ) 509 | h_m = ( h0 * np.exp (-np.log(2) * r / r_half) ) * 1e3 # m 510 | return h_m 511 | 512 | def calc_I_v_ms(S, r, par): 513 | ''' 514 | Eq. 2.24 515 | ''' 516 | rho_atm = 1.15 # air density [kg/m3] 517 | Omega = 7.2921e-5 # [rad/s] 518 | f = 2 * Omega * np.sin(par['lat_deg'] * np.pi/180) # Coriolis parameter 519 | 520 | pn = par['pn_mbar'] * 100 # [Pa] 521 | B = par['B_Holland'] 522 | 523 | R = 51.6 * np.exp(-.0223 * S + .0281 * par['lat_deg']) # see caption of Fig. 2.19 524 | pc = pn - 1 / B * (rho_atm * np.exp(1) * S**2) 525 | 526 | v_ms = ( B * R**B * (pn - pc) * np.exp(-(R/r)**B) / (rho_atm * r**B) + r**2 * f**2 / 4 )**.5 - r*f/2 527 | return v_ms 528 | 529 | def add_v_forward(vf, vtan, track_x, track_y, grid, t_i): 530 | # components of forward motion vector 531 | if t_i < len(track_x)-1: 532 | dx = track_x[t_i+1]-track_x[t_i] 533 | dy = track_y[t_i+1]-track_y[t_i] 534 | if dx == 0 and dy == 0: 535 | dx = track_x[t_i]-track_x[t_i-1] 536 | dy = track_y[t_i]-track_y[t_i-1] 537 | else: 538 | # assumes same future direction 539 | dx = track_x[t_i]-track_x[t_i-1] 540 | dy = track_y[t_i]-track_y[t_i-1] 541 | 542 | 543 | beta = np.arctan(dy/dx) 544 | if dx > 0: 545 | vf_x = vf * np.cos(beta) 546 | vf_y = vf * np.sin(beta) 547 | else: 548 | vf_x = -vf * np.cos(beta) 549 | vf_y = -vf * np.sin(beta) 550 | 551 | # components of gradient-based azimuthal wind vector 552 | dx = grid.xx - track_x[t_i] 553 | dy = grid.yy - track_y[t_i] 554 | alpha = np.arctan(dy/dx) 555 | # if x > x0 556 | vtan_x = -vtan * np.sin(alpha) 557 | vtan_y = vtan * np.cos(alpha) 558 | # if x < x0 559 | indneg = np.where(grid.xx < track_x[t_i]) 560 | vtan_x[indneg] = vtan[indneg] * np.sin(alpha[indneg]) 561 | vtan_y[indneg] = -vtan[indneg] * np.cos(alpha[indneg]) 562 | 563 | vtot_x = vtan_x + vf_x 564 | vtot_y = vtan_y + vf_y 565 | vtot = np.sqrt(vtot_x**2 + vtot_y**2) 566 | return vtot, vtot_x, vtot_y, vtan_x, vtan_y 567 | 568 | 569 | # threshold model 570 | def calc_S_TC2SS(v_max, relationship = 'generic'): 571 | ''' 572 | Empirical relationships according to the Saffir-Simpson scale (generic) or 573 | from Lin et al. (2010) (New York harbor). 574 | vmax: max wind speed [m/s] during storm passage 575 | S_SS: storm surge size at the source (coastline) 576 | ''' 577 | if relationship == 'generic': 578 | S_SS = .0011 * v_max**2 579 | if relationship == 'New York harbor': 580 | S_SS = .031641 * v_max - .00075537 * v_max**2 + 3.1941e-5 * v_max**3 581 | return np.round(S_SS, decimals = 3) 582 | 583 | def model_SS_Bathtub(I_trigger, src_SS, grid, topo_z): 584 | vmax_coastline = np.zeros(grid.ny) 585 | for j in range(grid.ny): 586 | indx = np.where(grid.x > src_SS['x'][j]-1e-6)[0][0] 587 | vmax_coastline[j] = I_trigger[indx,j] 588 | S_SS = calc_S_TC2SS(vmax_coastline, src_SS['bathy']) 589 | I_SS = np.zeros((grid.nx, grid.ny)) 590 | for j in range(grid.ny): 591 | I_alongx = S_SS[j] - topo_z[:,j] 592 | I_alongx[I_alongx < 0] = 0 593 | I_alongx[grid.x < src_SS['x'][j]] = 0 594 | I_SS[:,j] = I_alongx 595 | return I_SS 596 | 597 | 598 | def gen_hazFootprints(stochset, src, grid, topo_z): 599 | catalog_hazFootprints = {} 600 | print('generating footprints for:') 601 | for ID in src['perils']: 602 | indperil = np.where(stochset['ID'] == ID)[0] 603 | Nev_peril = len(indperil) 604 | 605 | if ID == 'AI': 606 | print(ID) 607 | for i in range(Nev_peril): 608 | evID = stochset['evID'][indperil].values[i] 609 | S = stochset['S'][indperil].values[i] 610 | r = np.sqrt((grid.xx - src['AI']['x'][i])**2 + (grid.yy - src['AI']['y'][i])**2) # point source 611 | catalog_hazFootprints[evID] = calc_I_ash_m(S, r) 612 | 613 | if ID == 'VE': 614 | print(ID) 615 | for i in range(Nev_peril): 616 | evID = stochset['evID'][indperil].values[i] 617 | S = stochset['S'][indperil].values[i] 618 | r = np.sqrt((grid.xx - src['VE']['x'][0])**2 + (grid.yy - src['VE']['y'][0])**2) # point source 619 | catalog_hazFootprints[evID] = calc_I_blast_kPa(S, r) 620 | 621 | if ID == 'EQ': 622 | print(ID) 623 | EQ_coords = src['EQ']['rup_coords'] 624 | for i in range(Nev_peril): 625 | evID = stochset['evID'][indperil].values[i] 626 | S = stochset['S'][indperil].values[i] 627 | evID_coords = EQ_coords[EQ_coords['evID'] == evID] 628 | npt = len(evID_coords) 629 | d2rupt = np.zeros((grid.nx, grid.ny, npt)) 630 | for k in range(npt): 631 | d2rupt[:,:,k] = np.sqrt((grid.xx - evID_coords['x'].values[k])**2 + (grid.yy - evID_coords['y'].values[k])**2) 632 | dmin = d2rupt.min(axis = 2) 633 | z = src['EQ']['z_km'][int(evID_coords['loc'].iloc[0][-1])-1] 634 | r = np.sqrt(dmin**2 + z**2) # line source 635 | catalog_hazFootprints[evID] = calc_I_shaking_ms2(S, r) 636 | 637 | if ID == 'TC': 638 | print(ID) 639 | Track_coord = get_TCtrack_highres(src['TC']['x'], src['TC']['y'], src['TC']['ID'], src['TC']['bin_km']) 640 | S_alongtrack = calc_S_track(stochset, src, Track_coord) # ad-hoc inland decay of windspeed 641 | for i in range(Nev_peril): 642 | evID = stochset['evID'][indperil].values[i] 643 | indtrack = np.where(Track_coord['ID'] == i+1)[0] 644 | track_x = Track_coord['x'][indtrack].values 645 | track_y = Track_coord['y'][indtrack].values 646 | track_S = S_alongtrack[evID] 647 | npt = len(indtrack) 648 | I_t = np.zeros((grid.nx, grid.ny, npt)) 649 | for j in range(npt): 650 | r = np.sqrt((grid.xx - track_x[j])**2 + (grid.yy - track_y[j])**2) # point source at time t 651 | I_sym_t = calc_I_v_ms(track_S[j], r, src['TC']) 652 | I_t[:,:,j], _, _, _, _ = \ 653 | add_v_forward(src['TC']['vforward_m/s'], I_sym_t, track_x, track_y, grid, j) 654 | catalog_hazFootprints[evID] = np.nanmax(I_t, axis = 2) # track source 655 | 656 | if ID == 'SS': 657 | print(ID) 658 | pattern = re.compile(r'TC(\d+)') # match "TC" followed by numbers 659 | for i in range(Nev_peril): 660 | evID = stochset['evID'][indperil].values[i] 661 | evID_trigger = re.search(pattern, evID).group() 662 | I_trigger = catalog_hazFootprints[evID_trigger] 663 | catalog_hazFootprints[evID] = model_SS_Bathtub(I_trigger, src['SS'], grid, topo_z) 664 | 665 | print('... catalogue completed') 666 | return catalog_hazFootprints 667 | 668 | 669 | ########################### 670 | ## dynamic hazard models ## 671 | ########################### 672 | 673 | ## LANDSLIDE CASE ## 674 | def calc_topo_attributes(z, w): 675 | z = np.pad(z*1e-3, 1, 'edge') # from m to km 676 | # 3x3 kernel method to get dz/dx, dz/dy 677 | dz_dy, dz_dx = np.gradient(z) 678 | dz_dx = dz_dx[1:-1,1:-1] / w 679 | dz_dy = (dz_dy[1:-1,1:-1] / w) * (-1) 680 | tan_slope = np.sqrt(dz_dx**2 + dz_dy**2) 681 | slope = np.arctan(tan_slope) * 180 / np.pi 682 | aspect = 180 - np.arctan(dz_dy/dz_dx)*180/np.pi + 90 * (dz_dx + 1e-6) / (np.abs(dz_dx) + 1e-6) 683 | return tan_slope, aspect 684 | 685 | def calc_FS(slope, h, w, par): 686 | ''' 687 | Calculates the factor of safety using Eq. 3 of Pack et al. (1998). 688 | 689 | Reference: 690 | Pack RT, Tarboton DG, Goodwin CN (1998), The SINMAP Approach to Terrain Stability Mapping. 691 | Proceedings of the 8th Congress of the International Association of Engineering Geology, Vancouver, BC, 692 | Canada, 21 September 1998 693 | ''' 694 | g_earth = 9.81 # [m/s^2] 695 | rho_wat = 1000. # [kg/m^3] 696 | FS = (par['Ceff_Pa'] / (par['rho_kg/m3'] * g_earth * h) + np.cos(slope * np.pi/180) * \ 697 | (1 - w * rho_wat / par['rho_kg/m3']) * np.tan(par['phieff_deg'] * np.pi/180)) / \ 698 | np.sin(slope * np.pi/180) 699 | return FS 700 | 701 | def get_neighborhood_ind(i, j, grid_shape, r_v, method): 702 | ''' 703 | Get the indices of the neighboring cells, depending on method and radius of vision 704 | ''' 705 | nx, ny = grid_shape 706 | # rv_box neighborhood 707 | indx = range(i - r_v, i + r_v + 1) 708 | indy = range(j - r_v, j + r_v + 1) 709 | # cut at grid borders 710 | indx_k = np.array([np.nan if (k < 0 or k > (nx - 1)) else k for k in indx]) 711 | indy_k = np.array([np.nan if (k < 0 or k > (ny - 1)) else k for k in indy]) 712 | indx_cut = ~np.isnan(indx_k) 713 | indy_cut = ~np.isnan(indy_k) 714 | ik, jk = [indx_k[indx_cut].astype('int'), indy_k[indy_cut].astype('int')] 715 | # mask 716 | mask = np.ones((2*r_v + 1, 2*r_v + 1), dtype = bool) 717 | nx_mask, ny_mask = mask.shape 718 | i0 = int(np.floor(nx_mask/2)) 719 | j0 = int(np.floor(ny_mask/2)) 720 | mask[i0,j0] = 0 721 | if method == 'Moore': 722 | mask_cut = mask[np.ix_(indx_cut, indy_cut)] 723 | if method == 'vonNeumann': 724 | mask = np.zeros((nx_mask, ny_mask), dtype = bool) 725 | mask[i0,:] = 1 726 | mask[:,j0] = 1 727 | mask[i0,j0] = 0 728 | mask_cut = mask[np.ix_(indx_cut, indy_cut)] 729 | return [np.meshgrid(ik,jk)[i].flatten()[mask_cut.flatten()] for i in range(2)] 730 | 731 | def get_ind_aspect2moore(ind_old): 732 | ''' 733 | Return Moore indices from indices defined from the aspect angle. 734 | 735 | Note: 736 | The aspect angle directs towards index np.round(aspect*8/360).astype('int'). 737 | It therefore takes the form: 765 738 | 0 4 739 | 123 740 | while Moore indices take the form: 012 (see get_neighborhood_ind() function). 741 | 3 4 742 | 567 743 | ''' 744 | ind_new = np.array([3,5,6,7,4,2,1,0,3]) 745 | return ind_new[ind_old] 746 | 747 | def calc_stableSlope(h, w, par): 748 | slope_i = np.arange(1, 50, .1) 749 | FS_i = calc_FS(slope_i, h, w, par) 750 | slope_stable = slope_i[FS_i > 1.5][-1] 751 | return slope_stable 752 | 753 | def run_CellularAutomaton_LS(LSfootprint, hs, topo_z, grid, par, w, movie): 754 | h0 = np.copy(hs) # original soil depth 755 | h = np.copy(hs) # updating soil depth 756 | z = np.copy(topo_z) # altitude changes over time 757 | 758 | if movie['create'] and not os.path.exists(movie['path']): 759 | os.makedirs(movie['path']) 760 | 761 | LSfootprint_hmax = np.zeros((grid.nx, grid.ny)) 762 | kmax = 20 763 | k = 1 764 | while k <= kmax: 765 | print('iteration', k, '/', kmax) 766 | indmov = np.where(np.logical_and(LSfootprint == 1, h > 0)) 767 | for kk in range(len(indmov[0])): 768 | i, j = [indmov[0][kk], indmov[1][kk]] 769 | z_pad = np.pad(z, 1, 'edge') 770 | tan_slope, aspect = calc_topo_attributes(z_pad[i:i+3, j:j+3], grid.w) 771 | slope = np.arctan(tan_slope[1,1]) * 180 / np.pi 772 | steepestdir = np.round(aspect[1,1] * 7 / 360).astype('int') 773 | slope_stable = calc_stableSlope(h[i,j], w[i,j], par) 774 | if slope > slope_stable: 775 | i_nbor, j_nbor = get_neighborhood_ind(i, j, (grid.nx, grid.ny), 1, method = 'Moore') 776 | steepestdir_rfmt = get_ind_aspect2moore(steepestdir) 777 | i1, j1 = [i_nbor[steepestdir_rfmt], j_nbor[steepestdir_rfmt]] 778 | if steepestdir % 2 == 0: # perpendicular 779 | dh_stable = grid.w*1e3 * np.tan(slope_stable * np.pi/180) 780 | dz = (grid.w*1e3 * np.tan(slope*np.pi/180) - dh_stable)/2 781 | else: # diagonal 782 | dh_stable = grid.w*1e3 * np.sqrt(2) * np.tan(slope_stable * np.pi/180) 783 | dz = (grid.w*1e3 * np.sqrt(2) * np.tan(slope*np.pi/180) - dh_stable)/2 784 | if dz > h[i,j]: 785 | dz = h[i,j] 786 | z[i,j] = z[i,j] - dz 787 | z[i1,j1] = z[i1,j1] + dz 788 | h[i,j] = h[i,j] - dz 789 | h[i1,j1] = h[i1,j1] + dz 790 | LSfootprint[i1,j1] = 1 791 | 792 | LSfootprint_hmax = np.maximum(LSfootprint_hmax, h - h0) 793 | 794 | # plot 795 | if movie['create']: 796 | plt.rcParams['font.size'] = '20' 797 | fig, ax = plt.subplots(1, 1, figsize=(10,10), facecolor = 'white') 798 | h_plot = h_code(h, h0) 799 | ax.contourf(grid.xx, grid.yy, h_plot, cmap = h_col, vmin = 1, vmax = 6) 800 | ax.contourf(grid.xx, grid.yy, ls.hillshade(topo_z, vert_exag=.1), cmap='gray', alpha = .1) 801 | ax.set_xlabel('x [km]') 802 | ax.set_ylabel('y [km]') 803 | ax.set_title('LS iteration'+str(k), pad = 10) 804 | ax.set_aspect(1) 805 | ax.set_xlim(movie['xmin'], movie['xmax']) 806 | ax.set_ylim(movie['ymin'], movie['ymax']) 807 | if k < 10: 808 | k_str = '0' + str(k) 809 | else: 810 | k_str = str(k) 811 | plt.savefig(movie['path']+'iter' + k_str + '.png', dpi = 300, bbox_inches='tight') 812 | plt.close() 813 | 814 | k += 1 815 | 816 | if movie['create']: 817 | fd = movie['path'] 818 | img = [] 819 | filenames = [filename for filename in os.listdir(fd) if filename.startswith('iter')] 820 | filenames.sort() 821 | for filename in filenames: 822 | img.append(imageio.imread(fd + filename)) 823 | imageio.mimsave(wd + '/figures/movie_LS.gif', img, duration = 500, loop = 0) 824 | 825 | return LSfootprint_hmax 826 | 827 | 828 | def gen_hazFootprints_LS(stochset, src, grid, topo_z, soil_hs): 829 | catalog_hazFootprints_LS = {} 830 | ID = 'LS' 831 | pattern = re.compile(r'RS(\d+)') 832 | indperil = np.where(stochset['ID'] == ID)[0] 833 | Nev_peril = len(indperil) 834 | tan_slope, _ = calc_topo_attributes(topo_z, grid.w) 835 | slope = np.arctan(tan_slope) * 180 / np.pi 836 | nx, ny = int(grid.xbuffer/grid.w), int(grid.ybuffer/grid.w) 837 | 838 | for i in range(Nev_peril): 839 | evID = stochset['evID'][indperil].values[i] 840 | print(evID) 841 | evID_trigger = re.search(pattern, evID).group() 842 | FS_state = np.zeros((grid.nx,grid.ny)) 843 | LSfootprint_seed = np.zeros((grid.nx, grid.ny)) 844 | S_trigger = stochset['S'][stochset['evID'] == evID_trigger].values 845 | hw = S_trigger * 1e-3 * src['RS']['duration'] # water column [m] 846 | wetness = hw / soil_hs 847 | wetness[wetness > 1] = 1 # max possible saturation 848 | wetness[soil_hs == 0] = 0 # no soil case 849 | FS = calc_FS(slope, soil_hs, wetness, src['LS']) 850 | FS_state = get_FS_state(FS) 851 | LSfootprint_seed[FS_state == 2] = 1 # initiates landslide where slope is unstable 852 | LSfootprint_seed = zero_boundary_2d(LSfootprint_seed, nx, ny) # no landslide in buffer zone 853 | 854 | movie = {'create': False} 855 | catalog_hazFootprints_LS[evID] = run_CellularAutomaton_LS(LSfootprint_seed, soil_hs, topo_z, grid, src['LS'], wetness, movie) 856 | print('... catalogue completed') 857 | return catalog_hazFootprints_LS 858 | 859 | 860 | ## WILDFIRE CASE ## 861 | def run_CellularAutomaton_WF(landuseLayer_state, src, grid, topoLayer_z): 862 | landuseLayer_state4WF = np.copy(landuseLayer_state) 863 | indForest = np.where(landuseLayer_state.flatten() == 1)[0] 864 | indForest2Grass = np.random.choice(indForest, size = int(len(indForest) * src['WF']['ratio_grass']), replace=False) 865 | landuseLayer_state4WF = landuseLayer_state4WF.flatten() 866 | landuseLayer_state4WF[indForest2Grass] = 0 867 | landuseLayer_state4WF = landuseLayer_state4WF.reshape(landuseLayer_state.shape) 868 | 869 | if not os.path.exists(src['WF']['path']): 870 | os.makedirs(src['WF']['path']) 871 | 872 | S = np.zeros(landuseLayer_state4WF.shape) # 0: no tree 873 | S[landuseLayer_state4WF == 1] = 1 # 1: tree 874 | for i in range(src['WF']['nsim']): 875 | if i%1000 == 0: 876 | print(i, '/', src['WF']['nsim']) 877 | 878 | # LOADING (long-term tree growth) 879 | indForest_notree = indForest[np.where(S.flatten()[indForest] == 0)[0]] 880 | new_tree_xy = np.random.choice(indForest_notree, size = src['WF']['rate_newtrees']) 881 | S_flat = S.flatten() 882 | S_flat[new_tree_xy] = 1 883 | landuseLayer_state4WF_flat = landuseLayer_state4WF.flatten() 884 | landuseLayer_state4WF_flat[new_tree_xy] = 1 885 | 886 | if np.random.random(1) <= src['WF']['p_lightning']: 887 | # TRIGGER (lightning) 888 | lightning_xy = np.random.choice(indForest, size=1) 889 | WF_fp = np.zeros(S.shape) 890 | WF_fp[:,:] = np.nan 891 | if S_flat[lightning_xy] == 1: 892 | S = S_flat.reshape(S.shape) 893 | landuseLayer_state4WF = landuseLayer_state4WF_flat.reshape(S.shape) 894 | S_clumps = measure.label(S, connectivity = 1) # von Neumann neighbourhood 895 | clump_WF = S_clumps.flatten()[lightning_xy] 896 | indWF = S_clumps == clump_WF 897 | WF_fp[indWF] = 5 # S = 5 for wildfire footprint in land use classes 898 | S[indWF] = 0 # burned = no tree 899 | landuseLayer_state4WF[indWF] = 0 900 | 901 | if np.sum(WF_fp == 5) >= src['WF']['Smin_plot']: 902 | plt.rcParams['font.size'] = '14' 903 | fig, ax = plt.subplots(1, 1, figsize=(7,7)) 904 | ax.contourf(grid.xx, grid.yy, ls.hillshade(topoLayer_z, vert_exag=.1), cmap='gray', alpha = .1) 905 | ax.pcolormesh(grid.xx, grid.yy, landuseLayer_state4WF, cmap = col_S, vmin=-1, vmax=5, alpha = .5) 906 | ax.pcolormesh(grid.xx, grid.yy, WF_fp, cmap = col_S, vmin=-1, vmax=5) 907 | plt.savefig(src['WF']['path'] + 'iter' + str(i) + '.jpg', dpi = 300) 908 | plt.close() 909 | WF_fp_saved = WF_fp 910 | 911 | return landuseLayer_state4WF, WF_fp_saved 912 | 913 | 914 | ## FLUVIAL FLOOD CASE ## 915 | def run_CellularAutomaton_FF(I_RS, src, grid, topoLayer_z): 916 | A_catchment = (100e3 * 100e3) # [m2] ad-hoc area east of the virtual region 917 | Qp = I_RS * A_catchment # [m3/s] 918 | river_xi, river_yi, _, _ = calc_coord_river_dampedsine(grid, src['FF']) 919 | src_indx = np.where(grid.x > river_xi[-1] - 1e-6)[0][0] 920 | src_indy = np.where(grid.y > river_yi[-1] - 1e-6)[0][0] 921 | 922 | l_src_max = Qp / (2 *(grid.w * 1e3)**2) # [m/s] 923 | tmax = src['RS']['duration'] * 3600 # [s] 924 | 925 | # make offshore mask 926 | mask_offshore = np.zeros((grid.nx, grid.ny), dtype = bool) 927 | for j in range(grid.ny): 928 | mask_offshore[grid.x >= src['SS']['x'][j], j] = True 929 | 930 | FFfootprint_t = np.zeros((grid.nx,grid.ny)) # flood footprint at time t 931 | FFfootprint = np.zeros((grid.nx,grid.ny)) # static flood footprint I(x,y) = max_t I(x,y,t) 932 | c = .5 933 | for t in range(tmax): 934 | if t%3600 == 0: 935 | print(t/3600, 'hr. /', tmax/3600) 936 | 937 | # water flowing at source grid cells (channel composed of two cells) 938 | FFfootprint_t[src_indx,src_indy] = l_src_max # should consider a hydrograph (with gradual increase to hW_src_max instead) 939 | FFfootprint_t[src_indx,src_indy-1] = l_src_max 940 | 941 | l = topoLayer_z + FFfootprint_t 942 | 943 | dl_a = np.pad((l[:,1:] - l[:,:-1]), [(0, 0), (1, 0)]) # left see fig. 4.5 of the textbook 944 | dl_b = np.pad((l[:-1,:] - l[1:,:]), [(0, 1), (0, 0)]) # bottom 945 | dl_c = np.pad((l[:,:-1] - l[:,1:]), [(0, 0), (0, 1)]) # right 946 | dl_d = np.pad((l[1:,:] - l[:-1,:]), [(1, 0), (0, 0)]) # top 947 | 948 | dl_all = np.stack((dl_a, dl_b, dl_c, dl_d)) 949 | dl_all[dl_all < 0] = 0 # water only going down, 0 for sum below 950 | dl_sum = np.sum(dl_all, axis = 0) 951 | dl_sum[dl_sum == 0] = np.inf # inf for 0 weight below 952 | weight_dl = dl_all / dl_sum 953 | 954 | lmax = np.minimum(FFfootprint_t, np.amax(c * dl_all, axis = 0)) 955 | lmov_all = weight_dl * lmax 956 | 957 | lIN_a = np.pad(lmov_all[0,:,1:], [(0, 0), (0, 1)]) 958 | lIN_b = np.pad(lmov_all[1,:-1,:], [(1, 0), (0, 0)]) 959 | lIN_c = np.pad(lmov_all[2,:,:-1], [(0, 0), (1, 0)]) 960 | lIN_d = np.pad(lmov_all[3,1:,:], [(0, 1), (0, 0)]) 961 | lOUT_0 = np.sum(lmov_all, axis = 0) 962 | 963 | FFfootprint_t = FFfootprint_t + (lIN_a + lIN_b + lIN_c + lIN_d) - lOUT_0 # simplified eq. 4.6 of textbook 964 | FFfootprint_t = FFfootprint_t * mask_offshore 965 | FFfootprint = np.maximum(FFfootprint, FFfootprint_t) 966 | 967 | return FFfootprint 968 | 969 | 970 | #################### 971 | ## RISK FUNCTIONS ## 972 | #################### 973 | def f_D(I, peril): 974 | if peril == 'AI' or peril == 'Ex': # I = overpressure [kPa] 975 | mu = np.log(20) 976 | sig = .4 977 | MDR = .5 * (1 + scipy.special.erf((np.log(I) - mu)/(sig * np.sqrt(2)))) 978 | if peril == 'EQ': # I = peak ground acceleration [m/s2] 979 | mu = np.log(6) 980 | sig = .6 981 | MDR = .5 * (1 + scipy.special.erf((np.log(I) - mu)/(sig * np.sqrt(2)))) 982 | if peril == 'FF' or peril == 'SS': # I = inundation depth [m] 983 | c = .45 984 | MDR = c * np.sqrt(I) 985 | MDR[MDR > 1] = 1 986 | if peril == 'LS': # I = landslide thickness [m] 987 | c1 = -1.671 988 | c2 = 3.189 989 | c3 = 1.746 990 | MDR = 1 - np.exp(c1*((I+c2)/c2 - 1)**c3) 991 | if peril == 'VE': # I = ash thickness [m] 992 | g_earth = 9.81 # [m/s^2] 993 | rho_ash = 900 # [kg/m3] (dry ash) 994 | I_kPa = rho_ash * g_earth * I * 1e-3 995 | mu = 1.6 996 | sig = .4 997 | MDR = .5 * (1 + scipy.special.erf((np.log(I_kPa) - mu)/(sig * np.sqrt(2)))) 998 | if peril == 'WS' or peril == 'TC': # I = wind speed [m/s] 999 | v_thresh = 25.7 # 50 kts 1000 | v_half = 74.7 1001 | vn = (I - v_thresh) / (v_half - v_thresh) 1002 | vn[vn < 0] = 0 1003 | MDR = vn**3 / (1+vn**3) 1004 | return MDR 1005 | 1006 | 1007 | def gen_lossFootprints(catalog_hazFootprints, expoFootprint, stochset): 1008 | ELT = pd.DataFrame(columns = ['ID', 'evID', 'lbd', 'L']) 1009 | catalog_MDR = {} 1010 | catalog_lossFootprints = {} 1011 | n_fp = len(catalog_hazFootprints) 1012 | evIDs = np.array(list(catalog_hazFootprints.keys())) 1013 | perils = get_peril_evID(evIDs) 1014 | print('generating footprints for:') 1015 | peril_check = '' 1016 | delete_TC = False 1017 | for i in range(n_fp): 1018 | evID = evIDs[i] 1019 | peril = perils[i] 1020 | if peril != peril_check: 1021 | print(peril) 1022 | peril_check = peril 1023 | 1024 | hazFootprint = catalog_hazFootprints[evID] 1025 | if evIDs[i][2] != '_': 1026 | # primary events 1027 | evID_trigger = evID 1028 | MDR = f_D(hazFootprint, peril) 1029 | lossFootprint = MDR * expoFootprint 1030 | Ltot = np.nansum(lossFootprint) 1031 | else: 1032 | # secondary events 1033 | if peril == 'LS': 1034 | MDR = f_D(hazFootprint, peril) 1035 | evID_trigger = evID[7:] # according to secondary ID format ID_fromIDx 1036 | peril = 'RS+LS' 1037 | evID = evID_trigger + '+LS' 1038 | lossFootprint = MDR * expoFootprint # no direct RS loss (i.e., invisible event) 1039 | Ltot = np.nansum(lossFootprint) 1040 | if peril == 'SS': 1041 | MDR = f_D(hazFootprint, peril) 1042 | # combine TC+SS 1043 | evID_trigger = evID[7:] # according to secondary ID format ID_fromIDx 1044 | peril = 'TC+SS' 1045 | evID = evID_trigger + '+SS' 1046 | lossFootprint_TC = catalog_lossFootprints[evID_trigger] 1047 | lossFootprint = lossFootprint_TC + MDR * (expoFootprint - lossFootprint_TC) # assumes SS only damages what is not yet damaged by TC 1048 | Ltot = np.nansum(lossFootprint) 1049 | delete_TC = True # delete TC from ELT since now combined in TC+SS 1050 | 1051 | catalog_MDR[evID] = MDR 1052 | catalog_lossFootprints[evID] = lossFootprint 1053 | ELT = pd.concat([ELT, pd.DataFrame({'ID': peril, 'evID': evID, 'lbd': stochset[stochset['evID'] == evID_trigger]['lbd'], 'L': Ltot})]) 1054 | 1055 | if delete_TC: 1056 | ELT = ELT[ELT['ID'] != 'TC'] 1057 | 1058 | return catalog_lossFootprints, ELT 1059 | 1060 | 1061 | def gen_YLT(ELT, Nsim): 1062 | # 1. calculate the overall ELT frequency 1063 | lbd = np.sum(ELT['lbd']) 1064 | 1065 | # 2. simulate the number of events each year 1066 | k = np.random.poisson(lbd, int(Nsim)) 1067 | 1068 | # 3. define a simulation ID for each simulated event 1069 | simIDs = [] 1070 | simID = 1 1071 | for val in k: 1072 | simIDs.append(np.repeat(simID, val)) 1073 | simID += 1 1074 | simIDs = np.concatenate(simIDs) 1075 | 1076 | # 4. sample events according to the ELT rates 1077 | n = np.sum(k) # tot. number of events 1078 | u = np.random.random(n) # random numbers for sampling 1079 | EF_norm = ELT['EF'] / lbd # normalised exceedance frequency 1080 | IDs = [ELT['evID'][EF_norm > u[i]].iloc[0] for i in range(n)] 1081 | 1082 | # 5. use ELT as lookup table to add losses 1083 | YLT = pd.DataFrame(data = {'simID': simIDs, 'evID': IDs}) 1084 | YLT = YLT.merge(ELT[['evID', 'L']], on='evID', how='left') 1085 | 1086 | return YLT 1087 | 1088 | 1089 | def calc_EP(lbd): 1090 | nev = len(lbd) 1091 | EFi = np.zeros(nev) 1092 | for i in range(nev): 1093 | EFi[i] = np.sum(lbd[0:i+1]) 1094 | EPi = 1 - np.exp(- EFi) # Eq. 3.22 1095 | return EFi, EPi 1096 | 1097 | def calc_riskmetrics_fromELT(ELT, q_VAR): 1098 | AAL = np.sum(ELT['lbd'] * ELT['L']) # Eq. 3.18 1099 | ELT = ELT.sort_values(by = 'L', ascending = False) # losses in descending order 1100 | EFi, EPi = calc_EP(ELT['lbd'].values) 1101 | ELT['EF'], ELT['EP'] = [EFi, EPi] 1102 | # VaR_q and TVaR_q 1103 | p = 1 - q_VAR 1104 | ELT_asc = ELT.sort_values(by = 'L') # losses in ascending order 1105 | VaRq = ELT_asc['L'][ELT_asc['EP'] < p].iloc[0] # Eq. 3.23 1106 | TVaRq = np.sum(ELT_asc['L'][ELT_asc['L'] > VaRq]) / len(ELT_asc['L'][ELT_asc['L'] > VaRq]) # derived from Eq. 3.24 1107 | 1108 | L_hires = 10**np.linspace(np.log10(ELT_asc['L'].min()+1e-6), np.log10(ELT_asc['L'].max()), num = 1000) 1109 | EP_hires = np.interp(L_hires, ELT_asc['L'], ELT_asc['EP']) 1110 | VaRq_interp = L_hires[EP_hires < p][0] 1111 | TVaRq_interp = np.sum(L_hires[L_hires > VaRq_interp]) / len(L_hires[L_hires > VaRq_interp]) 1112 | 1113 | return ELT, AAL, VaRq_interp, TVaRq_interp, VaRq, TVaRq 1114 | 1115 | def calc_riskmetrics_fromYLT(YLT, Nsim, q_VAR): 1116 | AAL = np.sum(YLT['L']) / Nsim # Eq. 3.19 1117 | YLT_asc = YLT.sort_values(by = 'L') 1118 | # VaR_q and TVaR_q 1119 | n = len(YLT) 1120 | VaRq = YLT_asc['L'].iloc[int(q_VAR*n+1)] # Sec. 3.3.2.3 1121 | TVaRq = 1/(n - (q_VAR*n+1) + 1) * np.sum(YLT_asc['L'].iloc[int(q_VAR*n+1):]) # Eq. 3.25 1122 | return AAL, VaRq, TVaRq 1123 | 1124 | def calc_EPfromYLT(YLT, Nsim): 1125 | ''' 1126 | ''' 1127 | EPi = (Nsim - np.arange(Nsim)) / Nsim 1128 | simIDs = np.unique(YLT['simID']) 1129 | n = len(simIDs) 1130 | Lmax = [np.max(YLT['L'][YLT['simID'] == simID]) for simID in simIDs] 1131 | Lagg = [np.sum(YLT['L'][YLT['simID'] == simID]) for simID in simIDs] 1132 | Li_max = np.concatenate((np.zeros(int(Nsim) - n), np.sort(Lmax))) 1133 | Li_agg = np.concatenate((np.zeros(int(Nsim) - n), np.sort(Lagg))) 1134 | return [Li_max, Li_agg, EPi] 1135 | 1136 | 1137 | ############## 1138 | ## PLOTTING ## 1139 | ############## 1140 | def col_peril(peril): 1141 | col_peril_extra = '#663399' # Rebeccapurple 1142 | col_peril_geophys = "#CD853F" # Peru 1143 | col_peril_hydro = "#20B2AA" # MediumSeaGreen 1144 | col_peril_meteo = "#4169E1" # RoyalBlue 1145 | col_peril_clim = '#8B0000' # DarkRed 1146 | col_peril_tech = '#708090' # SlateGrey 1147 | if peril == 'AI': 1148 | col = col_peril_extra 1149 | if peril == 'EQ' or peril == 'LS' or peril == 'VE': 1150 | col = col_peril_geophys 1151 | if peril == 'FF' or peril == 'SS': 1152 | col = col_peril_hydro 1153 | if peril == 'RS' or peril == 'WS' or peril == 'TC': 1154 | col = col_peril_meteo 1155 | if peril == 'WF': 1156 | col = col_peril_clim 1157 | if peril == 'Ex': 1158 | col = col_peril_tech 1159 | return col 1160 | 1161 | ls = plt_col.LightSource(azdeg = 45, altdeg = 45) 1162 | 1163 | # terrain color scheme 1164 | n_water, n_land = [50,200] 1165 | col_water = plt.cm.terrain(np.linspace(0, 0.17, n_water)) 1166 | col_land = plt.cm.terrain(np.linspace(0.25, 1, n_land)) 1167 | col_terrain = np.vstack((col_water, col_land)) 1168 | cmap_z = plt_col.LinearSegmentedColormap.from_list('cmap_z', col_terrain) 1169 | class norm_z(plt_col.Normalize): 1170 | # from https://stackoverflow.com/questions/40895021/python-equivalent-for-matlabs-demcmap-elevation-appropriate-colormap 1171 | # col_val = n_water/(n_water+n_land) 1172 | def __init__(self, vmin=None, vmax=None, sealevel=0, col_val = 0.2, clip=False): 1173 | # sealevel is the fix point of the colormap (in data units) 1174 | self.sealevel = sealevel 1175 | # col_val is the color value in the range [0,1] that should represent the sealevel 1176 | self.col_val = col_val 1177 | plt_col.Normalize.__init__(self, vmin, vmax, clip) 1178 | def __call__(self, value, clip=None): 1179 | x, y = [self.vmin, self.sealevel, self.vmax], [0, self.col_val, 1] 1180 | return np.ma.masked_array(np.interp(value, x, y)) 1181 | 1182 | # land use state color scheme 1183 | colors = [(0/255.,127/255.,191/255.), # -1 - water mask 1184 | (236/255., 235/255., 189/255.), # 0 - grassland (fall green) 1185 | (34/255.,139/255.,34/255.), # 1 - forest 1186 | (131/255.,137/255.,150/255.), # 2 - built, residential 1187 | (10/255.,10/255.,10/255.), # 3 - built, industry 1188 | (230/255.,230/255.,230/255.), # 4 - built, commercial 1189 | (255/255.,0/255.,0/255.)] # 5 - wildfire 1190 | col_S = plt_col.LinearSegmentedColormap.from_list('col_S', colors, N = 7) 1191 | 1192 | # soil color scheme 1193 | def col_state_h(h, h0): 1194 | h_plot = np.copy(h) 1195 | h_plot[h == 0] = 0 # erosion +++ (scarp) 1196 | h_plot[h == h0] = 1 # intact 1197 | h_plot[np.logical_and(h > 0, h <= h0/2)] = 2 # erosion ++ 1198 | h_plot[np.logical_and(h > h0/2, h < h0)] = 3 # erosion + 1199 | h_plot[np.logical_and(h > h0, h <= 2*h0)] = 4 # landslide + 1200 | h_plot[h > 2*h0] = 5 # landslide ++ 1201 | return h_plot 1202 | colors = [(105/255,105/255,105/255), # 0 - scarp / erosion +++ (dimgrey) 1203 | (236/255,235/255,189/255), # 1 - intact (fall green) 1204 | (195/255,176/255,145/255), # 2 - erosion ++ (khaki) 1205 | (186/255,135/255,89/255), # 3 - erosion + (deer) 1206 | (155/255,118/255,83/255), # 4 - landslide + (dirt) 1207 | (131/255,105/255,83/255)] # 5 - landslide ++ (pastel brown) 1208 | col_h = plt_col.LinearSegmentedColormap.from_list('col_h', colors, N = 6) 1209 | 1210 | 1211 | def plot_envLayers(grid, src, topoLayer_z, soilLayer_hs, landuseLayer_S, roadNetwork): 1212 | plt.rcParams['font.size'] = '18' 1213 | fig, ax = plt.subplots(2, 2, figsize=(20, 16)) 1214 | ax[0,0].contourf(grid.xx, grid.yy, ls.hillshade(topoLayer_z, vert_exag=.1), cmap='gray', alpha = .1) 1215 | if 'EQ' in src['perils']: 1216 | for src_i in range(len(src['EQ']['x'])): 1217 | if src_i == 1: 1218 | ax[0,0].plot(src['EQ']['x'][src_i], src['EQ']['y'][src_i], color = col_peril('EQ'), label = 'faults (EQ)') 1219 | else: 1220 | ax[0,0].plot(src['EQ']['x'][src_i], src['EQ']['y'][src_i], color = col_peril('EQ')) 1221 | if 'FF' in src['perils']: 1222 | river_xi, river_yi, _, river_id = calc_coord_river_dampedsine(grid, src['FF']) 1223 | ax[0,0].plot(river_xi, river_yi, color = col_peril('FF'), label = 'river bed') 1224 | ax[0,0].scatter(np.max(river_xi), src['FF']['riv_y0'][0], s=100, marker = 's', color = col_peril('FF'), label = 'upstream point (FF)') 1225 | if 'VE' in src['perils']: 1226 | ax[0,0].scatter(src['VE']['x'], src['VE']['x'], color = col_peril('VE'), s=100, marker = '^', label = 'volcano (VE)') 1227 | ax[0,0].plot([grid.xmin + grid.xbuffer, grid.xmax - grid.xbuffer, grid.xmax - grid.xbuffer, \ 1228 | grid.xmin + grid.xbuffer, grid.xmin + grid.xbuffer], 1229 | [grid.ymin + grid.ybuffer, grid.ymin + grid.ybuffer, grid.ymax - grid.ybuffer, \ 1230 | grid.ymax - grid.ybuffer, grid.ymin + grid.ybuffer], linestyle = 'dotted', color = 'black') 1231 | ax[0,0].set_xlim(grid.xmin, grid.xmax) 1232 | ax[0,0].set_ylim(grid.ymin, grid.ymax) 1233 | ax[0,0].set_xlabel('x [km]') 1234 | ax[0,0].set_ylabel('y [km]') 1235 | ax[0,0].set_title('Peril source coordinates', pad = 20) 1236 | ax[0,0].legend(loc = 'upper left', fontsize = 16) 1237 | ax[0,0].set_aspect(1) 1238 | 1239 | plt_zmin_m = -500 1240 | plt_zmax_m = 4500 1241 | ax[0,1].contourf(grid.xx, grid.yy, ls.hillshade(topoLayer_z, vert_exag=.1), cmap='gray') 1242 | z_plot = np.copy(topoLayer_z) 1243 | z_plot[z_plot < plt_zmin_m] = plt_zmin_m 1244 | z_plot[z_plot > plt_zmax_m] = plt_zmax_m 1245 | img = ax[0,1].contourf(grid.xx, grid.yy, z_plot, norm = norm_z(sealevel = 0, vmax = plt_zmax_m), \ 1246 | cmap = cmap_z, levels = np.arange(plt_zmin_m, plt_zmax_m+100, 100), alpha = .8) 1247 | fig.colorbar(img, ax = ax[0,1], fraction = .04, pad = .04, label = 'z [m]') 1248 | ax[0,1].plot([grid.xmin + grid.xbuffer, grid.xmax - grid.xbuffer, grid.xmax - grid.xbuffer, \ 1249 | grid.xmin + grid.xbuffer, grid.xmin + grid.xbuffer], 1250 | [grid.ymin + grid.ybuffer, grid.ymin + grid.ybuffer, grid.ymax - grid.ybuffer, \ 1251 | grid.ymax - grid.ybuffer, grid.ymin + grid.ybuffer], linestyle = 'dotted', color = 'black') 1252 | ax[0,1].set_xlim(grid.xmin, grid.xmax) 1253 | ax[0,1].set_ylim(grid.ymin, grid.ymax) 1254 | ax[0,1].set_xlabel('x [km]') 1255 | ax[0,1].set_ylabel('y [km]') 1256 | ax[0,1].set_title('Topography', pad = 20) 1257 | ax[0,1].set_aspect(1) 1258 | 1259 | legend_h = [Patch(facecolor=(105/255,105/255,105/255, .5), edgecolor='black', label='h=0 (scarp)'), 1260 | Patch(facecolor=(216/255,228/255,188/255, .5), edgecolor='black', label='h=$h_0$ (soil)')] 1261 | h0_m = 10 1262 | h_state = col_state_h(soilLayer_hs, h0_m) 1263 | 1264 | ax[1,0].contourf(grid.xx, grid.yy, ls.hillshade(topoLayer_z, vert_exag=.1), cmap='gray') 1265 | ax[1,0].pcolormesh(grid.xx, grid.yy, h_state, cmap = col_h, vmin=0, vmax=5, alpha = .5) 1266 | ax[1,0].plot([grid.xmin + grid.xbuffer, grid.xmax - grid.xbuffer, grid.xmax - grid.xbuffer, \ 1267 | grid.xmin + grid.xbuffer, grid.xmin + grid.xbuffer], 1268 | [grid.ymin + grid.ybuffer, grid.ymin + grid.ybuffer, grid.ymax - grid.ybuffer, \ 1269 | grid.ymax - grid.ybuffer, grid.ymin + grid.ybuffer], linestyle = 'dotted', color = 'black') 1270 | ax[1,0].set_xlim(grid.xmin, grid.xmax) 1271 | ax[1,0].set_ylim(grid.ymin, grid.ymax) 1272 | ax[1,0].set_xlabel('x [km]') 1273 | ax[1,0].set_ylabel('y [km]') 1274 | ax[1,0].set_title('Soil depth', pad = 20) 1275 | ax[1,0].set_aspect(1) 1276 | ax[1,0].legend(handles=legend_h, loc='upper left', fontsize = 16) 1277 | 1278 | legend_S = [Patch(facecolor=(0/255.,127/255.,191/255.,.5), edgecolor='black', label='water ($\mathcal{S}$ = -1)'), 1279 | Patch(facecolor=(236/255., 235/255., 189/255.,.5), edgecolor='black', label='grassland ($\mathcal{S}$ = 0)'), 1280 | Patch(facecolor=(34/255.,139/255.,34/255.,.5), edgecolor='black', label='forest ($\mathcal{S}$ = 1)'), 1281 | Patch(facecolor=(131/255.,137/255.,150/255.,.5), edgecolor='black', label='residential ($\mathcal{S}$ = 2)'), 1282 | Patch(facecolor=(10/255.,10/255.,10/255.,.5), edgecolor='black', label='industrial ($\mathcal{S}$ = 3)'), 1283 | Patch(facecolor=(230/255.,230/255.,230/255.,.5), edgecolor='black', label='commercial ($\mathcal{S}$ = 4)'), 1284 | Line2D([0], [0], color='yellow', linewidth=1, label='road network')] 1285 | ax[1,1].contourf(grid.xx, grid.yy, ls.hillshade(topoLayer_z, vert_exag=.1), cmap='gray') 1286 | ax[1,1].pcolormesh(grid.xx, grid.yy, landuseLayer_S, cmap = col_S, vmin=-1, vmax=5, alpha = .5) 1287 | ax[1,1].plot(roadNetwork[0], roadNetwork[1], color='yellow', lw = .5) 1288 | ax[1,1].plot([grid.xmin + grid.xbuffer, grid.xmax - grid.xbuffer, grid.xmax - grid.xbuffer, \ 1289 | grid.xmin + grid.xbuffer, grid.xmin + grid.xbuffer], 1290 | [grid.ymin + grid.ybuffer, grid.ymin + grid.ybuffer, grid.ymax - grid.ybuffer, \ 1291 | grid.ymax - grid.ybuffer, grid.ymin + grid.ybuffer], linestyle = 'dotted', color = 'black') 1292 | ax[1,1].set_xlim(grid.xmin, grid.xmax) 1293 | ax[1,1].set_ylim(grid.ymin, grid.ymax) 1294 | ax[1,1].set_xlabel('x [km]') 1295 | ax[1,1].set_ylabel('y [km]') 1296 | ax[1,1].set_title('Land use', pad = 20) 1297 | ax[1,1].set_aspect(1) 1298 | ax[1,1].legend(handles=legend_S, loc='upper right', fontsize = 16) 1299 | plt.tight_layout() 1300 | plt.savefig(wd + '/figures/envLayers.jpg', dpi = 300) 1301 | plt.pause(1) 1302 | plt.show() 1303 | 1304 | 1305 | def plot_hazFootprints(catalog_hazFootprints, grid, src, topoLayer_z, plot_Imax, nstoch = 5): 1306 | evIDs = np.array(list(catalog_hazFootprints.keys())) 1307 | ev_peril = get_peril_evID(evIDs) 1308 | perils = np.unique(ev_peril) 1309 | nperil = len(perils) 1310 | 1311 | plt.rcParams['font.size'] = '18' 1312 | fig, ax = plt.subplots(nperil, nstoch, figsize=(20, nperil*20/nstoch)) 1313 | 1314 | for i in range(nperil): 1315 | indperil = np.where(ev_peril == perils[i])[0] 1316 | nev = len(indperil) 1317 | nplot = np.min([nstoch, nev]) 1318 | evID_shuffled = evIDs[indperil] 1319 | if nev > nplot: 1320 | np.random.shuffle(evID_shuffled) 1321 | Imax = plot_Imax[perils[i]] 1322 | for j in range(nplot): 1323 | I_plt = np.copy(catalog_hazFootprints[evID_shuffled[j]]) 1324 | I_plt[I_plt >= Imax] = Imax 1325 | ax[i,j].contourf(grid.xx, grid.yy, I_plt, cmap = 'Reds', levels = np.linspace(0, Imax, 100)) 1326 | ax[i,j].contourf(grid.xx, grid.yy, ls.hillshade(topoLayer_z, vert_exag=.1), cmap='gray', alpha = .1) 1327 | ax[i,j].set_xlim(grid.xmin, grid.xmax) 1328 | ax[i,j].set_ylim(grid.ymin, grid.ymax) 1329 | ax[i,j].set_xlabel('x [km]') 1330 | ax[i,j].set_ylabel('y [km]') 1331 | ax[i,j].set_title(evID_shuffled[j], pad = 10) 1332 | ax[i,j].set_aspect(1) 1333 | if nplot < nstoch: 1334 | for j in np.arange(nplot, nstoch): 1335 | ax[i,j].set_axis_off() 1336 | plt.tight_layout() 1337 | plt.savefig(wd + '/figures/hazFootprints.jpg', dpi = 300) 1338 | plt.pause(1) 1339 | plt.show() 1340 | 1341 | 1342 | def plot_lossFootprints(catalog_lossFootprints, ELT, grid, topoLayer_z, Lmax, nstoch = 5): 1343 | evIDs = ELT['evID'].values 1344 | ev_peril = ELT['ID'].values 1345 | perils = np.unique(ev_peril) 1346 | nperil = len(perils) 1347 | 1348 | plt.rcParams['font.size'] = '18' 1349 | fig, ax = plt.subplots(nperil, nstoch, figsize=(20, nperil*20/nstoch)) 1350 | 1351 | for i in range(nperil): 1352 | indperil = np.where(ev_peril == perils[i])[0] 1353 | nev = len(indperil) 1354 | nplot = np.min([nstoch, nev]) 1355 | evID_shuffled = evIDs[indperil] 1356 | if nev > nplot: 1357 | np.random.shuffle(evID_shuffled) 1358 | for j in range(nplot): 1359 | L_plt = np.copy(catalog_lossFootprints[evID_shuffled[j]]) 1360 | L_plt[L_plt >= Lmax] = Lmax 1361 | ax[i,j].contourf(grid.xx, grid.yy, L_plt, cmap = 'Reds', levels = np.linspace(0, Lmax, 100)) 1362 | ax[i,j].contourf(grid.xx, grid.yy, ls.hillshade(topoLayer_z, vert_exag=.1), cmap='gray', alpha = .1) 1363 | ax[i,j].set_xlim(grid.xmin, grid.xmax) 1364 | ax[i,j].set_ylim(grid.ymin, grid.ymax) 1365 | ax[i,j].set_xlabel('x [km]') 1366 | ax[i,j].set_ylabel('y [km]') 1367 | ax[i,j].set_title(evID_shuffled[j], pad = 10) 1368 | ax[i,j].set_aspect(1) 1369 | if nplot < nstoch: 1370 | for j in np.arange(nplot, nstoch): 1371 | ax[i,j].set_axis_off() 1372 | plt.tight_layout() 1373 | plt.savefig(wd + '/figures/lossFootprints.jpg', dpi = 300) 1374 | plt.pause(1) 1375 | plt.show() 1376 | 1377 | 1378 | def plot_vulnFunctions(): 1379 | pi_kPa = np.linspace(0, 50, 100) 1380 | MDR_blast = f_D(pi_kPa, 'AI') # or 'Ex' 1381 | PGAi = np.linspace(0, 15, 100) # m/s2 1382 | MDR_EQ = f_D(PGAi, 'EQ') 1383 | hwi = np.linspace(0, 7, 1000) # m 1384 | MDR_flood = f_D(hwi, 'FF') # or 'SS' 1385 | hsi = np.linspace(0, 7, 100) # m 1386 | MDR_LS = f_D(hsi, 'LS') 1387 | hai = np.linspace(0, 2, 100) # m 1388 | MDR_VE = f_D(hai, 'VE') 1389 | g_earth = 9.81 # [m/s^2] 1390 | rho_ash = 900 # [kg/m3] (dry ash) 1391 | pi_VE_kPa = rho_ash * g_earth * hai * 1e-3 1392 | vi = np.linspace(0, 100, 100) # m/s 1393 | MDR_WS = f_D(vi, 'WS') 1394 | 1395 | plt.rcParams['font.size'] = '18' 1396 | fig, ax = plt.subplots(2,3, figsize = (20,12)) 1397 | ax[0,0].plot(pi_kPa, MDR_blast, color = 'black') 1398 | ax[0,0].set_title('Blast (AI, Ex)', pad = 20) 1399 | ax[0,0].set_xlabel('Overpressure $P$ [kPa]') 1400 | ax[0,0].set_ylabel('MDR') 1401 | ax[0,0].set_ylim(0,1.01) 1402 | ax[0,0].spines['right'].set_visible(False) 1403 | ax[0,0].spines['top'].set_visible(False) 1404 | 1405 | ax[0,1].plot(PGAi, MDR_EQ, color = 'black') 1406 | ax[0,1].set_title('Earthquake (EQ)', pad = 20) 1407 | ax[0,1].set_xlabel('PGA [m/s$^2$]') 1408 | ax[0,1].set_ylabel('MDR') 1409 | ax[0,1].set_ylim(0,1.01) 1410 | ax[0,1].spines['right'].set_visible(False) 1411 | ax[0,1].spines['top'].set_visible(False) 1412 | 1413 | ax[0,2].plot(hwi, MDR_flood, color = 'black') 1414 | ax[0,2].set_title('Flooding (FF, SS)', pad = 20) 1415 | ax[0,2].set_xlabel('Inundation depth $h$ [m]') 1416 | ax[0,2].set_ylabel('MDR') 1417 | ax[0,2].set_ylim(0,1.01) 1418 | ax[0,2].spines['right'].set_visible(False) 1419 | ax[0,2].spines['top'].set_visible(False) 1420 | 1421 | ax[1,0].plot(hsi, MDR_LS, color = 'black') 1422 | ax[1,0].set_title('Landslide (LS)', pad = 20) 1423 | ax[1,0].set_xlabel('Deposited height $h$ [m]') 1424 | ax[1,0].set_ylabel('MDR') 1425 | ax[1,0].set_ylim(0,1.01) 1426 | ax[1,0].spines['right'].set_visible(False) 1427 | ax[1,0].spines['top'].set_visible(False) 1428 | 1429 | ax[1,1].plot(pi_VE_kPa, MDR_VE, color = 'black') 1430 | ax[1,1].set_title('Volcanic eruption (VE)', pad = 20) 1431 | ax[1,1].set_xlabel('Ash load $P$ [kPa]') 1432 | ax[1,1].set_ylabel('MDR') 1433 | ax[1,1].set_ylim(0,1.01) 1434 | ax[1,1].spines['right'].set_visible(False) 1435 | ax[1,1].spines['top'].set_visible(False) 1436 | ax2 = ax[1,1].twiny() 1437 | ax2.set_xlabel('Ash thickness $h$ [m]') 1438 | ax2.plot(hai, MDR_VE, color = 'white', alpha = 0) 1439 | ax2.spines['right'].set_visible(False) 1440 | 1441 | ax[1,2].plot(vi, MDR_WS, color = 'black') 1442 | ax[1,2].set_title('Windstorm (WS)', pad = 20) 1443 | ax[1,2].set_xlabel('Maximum wind speed $v_{max}$ [m/s]') 1444 | ax[1,2].set_ylabel('MDR') 1445 | ax[1,2].set_ylim(0,1.01) 1446 | ax[1,2].spines['right'].set_visible(False) 1447 | ax[1,2].spines['top'].set_visible(False) 1448 | 1449 | fig.tight_layout() 1450 | plt.savefig(wd + '/figures/vulnFunctions.jpg', dpi = 300) 1451 | plt.pause(1) 1452 | plt.show() 1453 | 1454 | 1455 | ## landslide plotting ## 1456 | def get_FS_state(FS_value): 1457 | FS_code = np.copy(FS_value) 1458 | FS_code[FS_value > 1.5] = 0 # stable 1459 | FS_code[np.logical_and(FS_value > 1, FS_value <= 1.5)] = 1 # critical 1460 | FS_code[FS_value <= 1] = 2 # unstable 1461 | return FS_code 1462 | 1463 | colors = [(0, 100/255., 0), # 0 - stable FS (dark green) 1464 | (255/255.,215/255.,0/255.), # 1 - critical FS (gold) 1465 | (178/255.,34/255.,34/255.)] # 2 - unstable FS (darkred) 1466 | col_FS = plt_col.LinearSegmentedColormap.from_list('col_FS', colors, N = 3) 1467 | 1468 | legend_FS = [Patch(facecolor=(0, 100/255., 0, .5), edgecolor='black', label='>1.5 (stable)'), 1469 | Patch(facecolor=(255/255.,215/255.,0/255., .5), edgecolor='black',label='[1,1.5] (critical)'), 1470 | Patch(facecolor=(178/255.,34/255.,34/255., .5), edgecolor='black', label='<1 (unstable)')] 1471 | 1472 | def h_code(h, h0): 1473 | h_plot = np.copy(h) 1474 | h_plot[h == 0] = 1 # erosion +++ (scarp) 1475 | h_plot[h == h0] = 2 # intact 1476 | h_plot[np.logical_and(h > 0, h <= h0/2)] = 3 # erosion ++ 1477 | h_plot[np.logical_and(h > h0/2, h < h0)] = 4 # erosion + 1478 | h_plot[np.logical_and(h > h0, h <= 2*h0)] = 5 # landslide + 1479 | h_plot[h > 2*h0] = 6 # landslide ++ 1480 | return h_plot 1481 | 1482 | #dimgrey, gin, khaki, deer, dirt, pastel brown 1483 | colors = [(105/255,105/255,105/255), 1484 | (216/255,228/255,188/255), 1485 | (195/255,176/255,145/255), 1486 | (186/255,135/255,89/255), 1487 | (155/255,118/255,83/255), 1488 | (131/255,105/255,83/255)] 1489 | h_col = plt_col.LinearSegmentedColormap.from_list('h_col', colors, N=6) 1490 | -------------------------------------------------------------------------------- /CAT_StarterKit.R: -------------------------------------------------------------------------------- 1 | # Title: CAT Risk Modelling Starter-Kit (in R) 2 | # Author: Arnaud Mignan 3 | # Date: 01.02.2024 4 | # Description: A basic template to develop a catastrophe (CAT) risk model (here with ad-hoc parameters & models). 5 | # License: MIT 6 | # Version: 1.1 7 | # Dependencies: ggplot2, gridExtra, lattice 8 | # Contact: arnaud@mignanriskanalytics.com 9 | # Citation: Mignan, A. (2025), Introduction to Catastrophe Risk Modelling – A Physics-based Approach. Cambridge University Press, DOI: 10.1017/9781009437370 10 | 11 | #Copyright 2024 A. Mignan 12 | # 13 | #Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), 14 | #to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | #and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | # 17 | #The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | # 19 | #THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | #IN THE SOFTWARE. 23 | 24 | 25 | library(ggplot2) 26 | library(gridExtra) 27 | library(lattice) 28 | 29 | 30 | # ad-hoc function 31 | func_src2ev <- function(src_S) { 32 | # The maximum size S of an event (ev_Smax) is constrained by the source from which it originates, 33 | # with src_S the source characteristic from which ev_Smax can be inferred - See model examples in Section 2.2. 34 | c <- 0.1 35 | k <- 2 36 | ev_Smax <- c * src_S^k # ad-hoc model 37 | return(ev_Smax) # maximum-size event (or characteristic event for the source) 38 | } 39 | 40 | func_intensity <- function(S, r) { 41 | # The impact of each event on the environment is assessed by the hazard intensity I(x,y) across a 42 | # geographical grid (x,y). A static footprint is defined as a function of distance from source r(x,y) 43 | # and event size S - See model examples in Section 2.4. 44 | c0 <- -1 45 | c1 <- 0.1 46 | c2 <- 1 47 | c3 <- 5 48 | I <- exp(c0 + c1 * S - c2 * log(r + c3)) # ad-hoc model 49 | return(I) 50 | } 51 | 52 | erf <- function(x) return(2 * pnorm(x * sqrt(2)) - 1) 53 | func_vulnerability <- function(I, Theta_nu) { 54 | # The vulnerability curve expresses the damage D (or Mean Damage Ratio, MDR) expected on an asset of 55 | # characteristics Theta_nu given a hazard intensity load I - See model examples in Section 3.2.5. 56 | MDR <- 0.5 + 0.5 * erf((log(I) - Theta_nu$mu) / sqrt(2 * Theta_nu$sigma^2)) # here cum. lognormal distr. 57 | return(MDR) 58 | } 59 | 60 | 61 | # ad-hoc parameters (to be replaced by peril- & region-specific values) 62 | xmin <- 0 # [km] 63 | xmax <- 100 64 | dx <- 1 65 | ymin <- 0 # [km] 66 | ymax <- 100 67 | dy <- 1 68 | src1_x0 <- 25 # source 'src1' of coordinates (x0,y0) and size S 69 | src1_y0 <- 25 70 | src1_S <- 5 71 | src2_x0 <- 75 72 | src2_y0 <- 50 73 | src2_S <- 8 74 | Smin <- 0.1 # minimum event size [ad hoc unit] (e.g., energy) 75 | dS_log10 <- 0.1 # event size increment (in log10 scale) 76 | a <- 0.5 # for Eq. 2.38: log10(rate_cum(S)) = a - b log10(S); see some values in Tab. 2.5 77 | b <- 1 78 | Theta_nu <- list(mu = log(.04), sigma = .1) # for Eq. 3.4: cum. lognormal distr.; see some values in Section 3.2.5 79 | 80 | # define environment (i.e., grid for footprints) 81 | x = seq(xmin, xmax, dx) 82 | y = seq(ymin, ymax, dy) 83 | grid <- expand.grid(x = x, y = y) 84 | 85 | # exposure footprint defined below as a square town of uniform asset value 1 86 | padW <- 30 87 | padE <- 25 88 | padN <- 40 89 | padS <- 20 90 | nu_grid <- array(1, dim = c(length(grid$x))) 91 | for (i in 1:length(grid$x)) { 92 | if (grid$x[i] < padW){nu_grid[i] = 0} 93 | if (grid$x[i] >= length(x)-padE){nu_grid[i] = 0} 94 | if (grid$y[i] < padS){nu_grid[i] = 0} 95 | if (grid$y[i] >= length(y)-padN){nu_grid[i] = 0} 96 | } 97 | 98 | 99 | ## HAZARD ASSESSMENT ## 100 | # define source model (here, 2 point sources) 101 | Src <- data.frame(ID = c('src1', 'src2'), x0 = c(src1_x0, src2_x0), y0 = c(src1_y0, src2_y0), S = c(src1_S, src2_S)) 102 | 103 | 104 | # define size distribution 105 | Smax1 <- func_src2ev(src1_S) 106 | Smax2 <- func_src2ev(src2_S) 107 | Smax <- max(Smax1, Smax2) 108 | Si <- 10^seq(log10(Smin), log10(Smax), dS_log10) 109 | Si1 <- 10^seq(log10(Smin), log10(Smax1), dS_log10) 110 | Si2 <- 10^seq(log10(Smin), log10(Smax2), dS_log10) 111 | N1 <- length(Si1) 112 | N2 <- length(Si2) 113 | ratei <- 10^(a - b * (log10(Si) - dS_log10 / 2)) - 10^(a - b * (log10(Si) + dS_log10 / 2)) # e.g., Eq. 2.65 114 | 115 | 116 | # define event table 117 | # peril ID: e.g., EQ, VE, AI... Tab. 1.7 118 | EventTable <- data.frame(ID = paste0('ID', 1:(N1 + N2)), Src = rep(c('src1', 'src2'), c(N1, N2)), S = c(Si1, Si2),rate = c(ratei[1:N1], ratei[1:N2])) 119 | 120 | # correct rate, which is function of the stochastic set definition: 121 | # in the present case, we have two sources with equal share of the overall event activity defined by (a,b) 122 | Nevent_perSi = c(rep(2, 2 * N1), rep(1, N2-N1)) # if N1 < N2 (i.e., src1_S < src2_S) 123 | EventTable$rate = EventTable$rate / Nevent_perSi 124 | # Whichever stochastic construct, we must have EventTable['rate'].sum() = np.sum(ratei) 125 | 126 | 127 | # define intensity I grid footprint catalog 128 | I_grid <- array(0, dim = c(N1 + N2, length(grid$x))) 129 | for (i in 1:(N1 + N2)) { 130 | ind <- which(Src$ID == EventTable$Src[i])[1] 131 | for (j in 1:length(grid$x)){ 132 | r_grid <- sqrt((grid$x[j] - Src$x0[ind])^2 + (grid$y[j] - Src$y0[ind])^2) 133 | I_grid[i,j] <- func_intensity(EventTable$S[i], r_grid) 134 | } 135 | } 136 | 137 | # -> calculate hazard metrics (see solution to exercise #2.4) 138 | 139 | 140 | ## RISK ASSESSMENT ## 141 | # calculate damage D grid & loss L grid footprints 142 | D_grid <- array(0, dim = c(N1 + N2, length(grid$x))) 143 | L_grid <- array(0, dim = c(N1 + N2, length(grid$x))) 144 | for (i in 1:(N1 + N2)) { 145 | D_grid[i,] <- func_vulnerability(I_grid[i,], Theta_nu) 146 | L_grid[i,] <- D_grid[i,] * nu_grid 147 | } 148 | 149 | # update event table as loss table 150 | ELT <- EventTable 151 | ELT$loss <- sapply(1:(N1 + N2), function(i) sum(L_grid[i,])) 152 | 153 | # -> calculate risk metrics (see solution to exercise #3.2) 154 | 155 | 156 | ## plot templates ## 157 | pdf('plots_template_R.pdf', width = 20, height = 15) 158 | 159 | df_plot <- data.frame(src_Si = seq(0.1, 10, length.out = 100), func_src2ev = func_src2ev(seq(0.1, 10, length.out = 100))) 160 | plot1 <- ggplot(df_plot, aes(x = src_Si, y = func_src2ev)) + 161 | geom_line(color = 'black') + 162 | geom_hline(yintercept = Smin, linetype = 'dashed', color = 'black') + 163 | geom_vline(xintercept = src1_S, linetype = 'dashed', color = 'orange') + 164 | geom_vline(xintercept = src2_S, linetype = 'dashed', color = 'red') + 165 | labs(x = 'Source size src_S', y = 'Max. event size Smax', title = 'Characteristic event size') + 166 | theme_minimal() 167 | 168 | df_rate <- data.frame(Si = Si, ratei = ratei) 169 | ind1 <- which(EventTable$Src == 'src1') 170 | ind2 <- which(EventTable$Src == 'src2') 171 | 172 | df_src1 <- EventTable[ind1, ] 173 | df_src2 <- EventTable[ind2, ] 174 | offset <- 1.1 # to avoid overlap 175 | df_src2$rate <- df_src2$rate * offset 176 | plot2 <- ggplot() + 177 | geom_line(data = df_rate, aes(x = Si, y = ratei), color = 'black') + 178 | geom_point(data = df_src1, aes(x = S, y = rate), color = 'orange') + 179 | geom_point(data = df_src2, aes(x = S, y = rate), color = 'red') + 180 | scale_x_log10() + 181 | scale_y_log10() + 182 | labs(x = 'Event size S', y = 'Rate', title = 'Stochastic set distribution') + 183 | theme_minimal() + 184 | theme(legend.position = 'top') 185 | 186 | df_intensity <- data.frame( 187 | ri = seq(0, 50, 0.1), 188 | intensity_src1 = func_intensity(Smax1, seq(0, 50, 0.1)), 189 | intensity_src2 = func_intensity(Smax2, seq(0, 50, 0.1)) 190 | ) 191 | plot3 <- ggplot(df_intensity, aes(x = ri)) + 192 | geom_line(aes(y = intensity_src1), color = 'orange') + 193 | geom_line(aes(y = intensity_src2), color = 'red') + 194 | labs(x = 'Distance from source r', y = 'Intensity I', title = 'Hazard intensity model') + 195 | theme_minimal() + 196 | theme(legend.position = 'top') 197 | 198 | Ii = seq(.001, .1, .001) 199 | df_vulnerability <- data.frame(Ii = Ii, mean_damage_ratio = func_vulnerability(Ii, Theta_nu)) 200 | plot4 <- ggplot(df_vulnerability, aes(x = Ii, y = mean_damage_ratio)) + 201 | geom_line(color = 'black') + 202 | labs(x = 'Intensity I', y = 'Mean damage ratio', title = 'Vulnerability curve') + 203 | theme_minimal() 204 | 205 | df_nu = data.frame(nu = nu_grid, x = grid$x, y = grid$y) 206 | plot5 <- ggplot() + 207 | geom_raster(data = df_nu, aes(x = x, y = y, fill = nu)) + 208 | scale_fill_gradient(low = "white", high = "darkgreen") + 209 | labs(x = 'x', y = 'y', title = 'Exposure footprint') + 210 | theme_minimal() + 211 | theme(legend.position="none") 212 | 213 | df_I = data.frame(nu = I_grid[tail(ind1, 1),], x = grid$x, y = grid$y) 214 | plot6 <- ggplot() + 215 | geom_raster(data = df_I, aes(x = x, y = y, fill = nu)) + 216 | geom_point(data = Src[Src$ID == 'src1',], aes(x = x0, y = y0), pch = 3, col = 'black') + 217 | scale_fill_gradient(low = "white", high = "red") + 218 | labs(x = 'x', y = 'y', title = 'Hazard intensity footprint (Smax1)') + 219 | theme_minimal() + 220 | theme(legend.position="none") 221 | 222 | 223 | df_D = data.frame(nu = D_grid[tail(ind1, 1),], x = grid$x, y = grid$y) 224 | plot7 <- ggplot() + 225 | geom_raster(data = df_D, aes(x = x, y = y, fill = nu)) + 226 | geom_point(data = Src[Src$ID == 'src1',], aes(x = x0, y = y0), pch = 3, col = 'black') + 227 | scale_fill_gradient(low = "white", high = "blue") + 228 | labs(x = 'x', y = 'y', title = 'Expected damage footprint (Smax1)') + 229 | theme_minimal() + 230 | theme(legend.position="none") 231 | 232 | df_L = data.frame(nu = L_grid[tail(ind1, 1),], x = grid$x, y = grid$y) 233 | plot8 <- ggplot() + 234 | geom_raster(data = df_L, aes(x = x, y = y, fill = nu)) + 235 | geom_point(data = Src[Src$ID == 'src1',], aes(x = x0, y = y0), pch = 3, col = 'black') + 236 | scale_fill_gradient(low = "white", high = "purple") + 237 | labs(x = 'x', y = 'y', title = 'Loss footprint (Smax1)') + 238 | theme_minimal() + 239 | theme(legend.position="none") 240 | 241 | plot9 <- ggplot() + 242 | geom_raster(data = df_nu, aes(x = x, y = y, fill = nu)) + 243 | scale_fill_gradient(low = "white", high = "darkgreen") + 244 | labs(x = 'x', y = 'y', title = 'Exposure footprint') + 245 | theme_minimal() + 246 | theme(legend.position="none") 247 | 248 | df_I = data.frame(nu = I_grid[tail(ind2, 1),], x = grid$x, y = grid$y) 249 | plot10 <- ggplot() + 250 | geom_raster(data = df_I, aes(x = x, y = y, fill = nu)) + 251 | geom_point(data = Src[Src$ID == 'src2',], aes(x = x0, y = y0), pch = 3, col = 'black') + 252 | scale_fill_gradient(low = "white", high = "red") + 253 | labs(x = 'x', y = 'y', title = 'Hazard intensity footprint (Smax2)') + 254 | theme_minimal() + 255 | theme(legend.position="none") + 256 | coord_fixed() 257 | 258 | df_D = data.frame(nu = D_grid[tail(ind2, 1),], x = grid$x, y = grid$y) 259 | plot11 <- ggplot() + 260 | geom_raster(data = df_D, aes(x = x, y = y, fill = nu)) + 261 | geom_point(data = Src[Src$ID == 'src2',], aes(x = x0, y = y0), pch = 3, col = 'black') + 262 | scale_fill_gradient(low = "white", high = "blue") + 263 | labs(x = 'x', y = 'y', title = 'Expected damage footprint (Smax2)') + 264 | theme_minimal() + 265 | theme(legend.position="none") + 266 | coord_fixed() 267 | 268 | df_L = data.frame(nu = L_grid[tail(ind2, 1),], x = grid$x, y = grid$y) 269 | plot12 <- ggplot() + 270 | geom_raster(data = df_L, aes(x = x, y = y, fill = nu)) + 271 | geom_point(data = Src[Src$ID == 'src2',], aes(x = x0, y = y0), pch = 3, col = 'black') + 272 | scale_fill_gradient(low = "white", high = "purple") + 273 | labs(x = 'x', y = 'y', title = 'Loss footprint (Smax2)') + 274 | theme_minimal() + 275 | theme(legend.position="none") + 276 | coord_fixed() 277 | 278 | fig <- grid.arrange(plot1, plot2, plot3, plot4, plot5, plot6, plot7, plot8, plot9, plot10, plot11, plot12, ncol = 4, nrow = 3) 279 | dev.off() 280 | 281 | 282 | -------------------------------------------------------------------------------- /CAT_StarterKit.py: -------------------------------------------------------------------------------- 1 | # Title: CAT Risk Modelling Starter-Kit (in Python) 2 | # Author: Arnaud Mignan 3 | # Date: 01.02.2024 4 | # Description: A basic template to develop a catastrophe (CAT) risk model (here with ad-hoc parameters & models). 5 | # License: MIT 6 | # Version: 1.1 7 | # Dependencies: numpy, pandas, scipy, matplotlib 8 | # Contact: arnaud@mignanriskanalytics.com 9 | # Citation: Mignan, A. (2025), Introduction to Catastrophe Risk Modelling – A Physics-based Approach. Cambridge University Press, DOI: 10.1017/9781009437370 10 | 11 | #Copyright 2024 A. Mignan 12 | # 13 | #Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), 14 | #to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | #and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 16 | # 17 | #The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 18 | # 19 | #THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22 | #IN THE SOFTWARE. 23 | 24 | 25 | import numpy as np 26 | import pandas as pd 27 | from scipy.special import erf 28 | import matplotlib.pyplot as plt 29 | 30 | 31 | # ad-hoc functions 32 | def func_src2ev(src_S): 33 | ''' 34 | The maximum size S of an event (ev_Smax) is constrained by the source from which it originates, 35 | with src_S the source characteristic from which ev_Smax can be inferred - See model examples in Section 2.2. 36 | ''' 37 | c = .1 38 | k = 2 39 | ev_Smax = c * src_S**k # ad-hoc model 40 | return ev_Smax # maximum-size event (or characteristic event for the source) 41 | 42 | def func_intensity(S, r): 43 | ''' 44 | The impact of each event on the environment is assessed by the hazard intensity I(x,y) across a 45 | geographical grid (x,y). A static footprint is defined as function of distance from source r(x,y) 46 | and event size S - See model examples in Section 2.4. 47 | ''' 48 | c0 = -1 49 | c1 = .1 50 | c2 = 1 51 | c3 = 5 52 | I = np.exp(c0 + c1 * S - c2 * np.log(r+c3)) # ad-hoc model 53 | return I 54 | 55 | def func_vulnerability(I, Theta_nu): 56 | ''' 57 | The vulnerability curve expresses the damage D (or Mean Damage Ratio, MDR) expected on an asset of 58 | characteristics Theta_nu given a hazard intensity load I - See model examples in Section 3.2.5. 59 | ''' 60 | MDR = 1/2 + 1/2 * erf((np.log(I) - Theta_nu['mu']) / np.sqrt(2 * Theta_nu['sigma']**2)) # here cum. lognormal distr. 61 | return MDR 62 | 63 | 64 | # ad-hoc parameters (to be replaced by peril- & region-specific values) 65 | xmin, xmax, dx = [0, 100, 1] # [km] 66 | ymin, ymax, dy = [0, 100, 1] # [km] 67 | src1_x0, src1_y0, src1_S = [25, 25, 5] # source 'src1' of coordinates (x0,y0) and size S 68 | src2_x0, src2_y0, src2_S = [75, 50, 8] # 69 | Smin = .1 # minimum event size [ad hoc unit] (e.g., energy) 70 | dS_log10 = .1 # event size increment (in log10 scale) 71 | a, b = [.5, 1] # for Eq. 2.38: log10(rate_cum(S)) = a - b log10(S); see some values in Tab. 2.5 72 | Theta_nu = {'mu':np.log(.04), 'sigma':.1} # for Eq. 3.4: cum. lognormal distr.; see some values in Section 3.2.5 73 | 74 | # define environment (i.e., grid for footprints) 75 | x = np.arange(xmin, xmax, dx) 76 | y = np.arange(ymin, ymax, dy) 77 | grid_x, grid_y = np.meshgrid(x, y) 78 | 79 | # exposure footprint defined below as a square town of uniform asset value 1 80 | padW, padE, padN, padS = [30, 25, 40, 20] 81 | padding = ((padS, padN),(padW, padE)) 82 | nu_grid = np.ones((len(x)-(padN+padS), len(y)-(padW+padE))) 83 | nu_grid = np.pad(nu_grid, padding) 84 | 85 | 86 | ## HAZARD ASSESSMENT ## 87 | # define source model (here, 2 point sources) 88 | Src = pd.DataFrame({'ID': ['src1', 'src2'], 'x0': [src1_x0, src2_x0], 'y0': [src1_y0, src2_y0], 'S': [src1_S, src2_S]}) 89 | 90 | 91 | # define size distribution 92 | Smax1 = func_src2ev(src1_S) 93 | Smax2 = func_src2ev(src2_S) 94 | Smax = np.max([Smax1, Smax2]) 95 | Si = 10**np.arange(np.log10(Smin), np.log10(Smax), dS_log10) 96 | Si1 = 10**np.arange(np.log10(Smin), np.log10(Smax1), dS_log10) 97 | Si2 = 10**np.arange(np.log10(Smin), np.log10(Smax2), dS_log10) 98 | N1, N2 = [len(Si1), len(Si2)] 99 | ratei = 10**(a - b * (np.log10(Si) - dS_log10 / 2)) - 10**(a - b * (np.log10(Si) + dS_log10 / 2)) # e.g., Eq. 2.65 100 | 101 | 102 | # define event table 103 | # peril ID: e.g., EQ, VE, AI... Tab. 1.7 104 | EventTable = pd.DataFrame({'ID': ['ID' + str(i+1) for i in range(N1+N2)],\ 105 | 'Src': np.concatenate([np.repeat('src1', N1), np.repeat('src2', N2)]),\ 106 | 'S': np.concatenate([Si1, Si2]),\ 107 | 'rate': np.concatenate([ratei[0:N1], ratei[0:N2]])}) 108 | 109 | # correct rate, which is function of the stochastic set definition: 110 | # in the present case, we have two sources with equal share of the overall event activity defined by (a,b) 111 | Nevent_perSi = np.concatenate([np.repeat(2, 2 * N1), np.repeat(1, N2-N1)]) # if N1 < N2 (i.e., src1_S < src2_S) 112 | EventTable['rate'] = EventTable['rate'] / Nevent_perSi 113 | # Whichever stochastic construct, we must have EventTable['rate'].sum() = np.sum(ratei) 114 | 115 | 116 | # define intensity I grid footprint catalog 117 | I_grid = np.zeros((N1+N2, len(x), len(y))) 118 | for i in range(N1+N2): 119 | ind = np.where(Src['ID'] == EventTable['Src'][i])[0][0] 120 | r_grid = np.sqrt((grid_x - Src['x0'][ind])**2 + (grid_y - Src['y0'][ind])**2) 121 | I_grid[i,:,:] = func_intensity(EventTable['S'][i], r_grid) 122 | 123 | # -> calculate hazard metrics (see solution to exercise #2.4) 124 | 125 | 126 | ## RISK ASSESSMENT ## 127 | # calculate damage D grid & loss L grid footprints 128 | D_grid = np.zeros((N1+N2, len(x), len(y))) 129 | L_grid = np.zeros((N1+N2, len(x), len(y))) 130 | for i in range(N1+N2): 131 | D_grid[i,:,:] = func_vulnerability(I_grid[i,:,:], Theta_nu) 132 | L_grid[i,:,:] = D_grid[i,:,:] * nu_grid 133 | 134 | # update event table as loss table 135 | ELT = EventTable 136 | ELT['loss'] = [np.sum(L_grid[i,:,:]) for i in range(N1+N2)] 137 | 138 | # -> calculate risk metrics (see solution to exercise #3.2) 139 | 140 | 141 | 142 | 143 | ## plot templates ## 144 | fig, ax = plt.subplots(3,4, figsize = (20,15)) 145 | src_Si = np.arange(.1,10) 146 | ax[0,0].plot(src_Si, func_src2ev(src_Si), color = 'black') 147 | ax[0,0].axhline(Smin, color = 'black', linestyle = 'dashed') 148 | ax[0,0].axvline(src1_S, color = 'orange', linestyle = 'dashed') 149 | ax[0,0].axvline(src2_S, color = 'red', linestyle = 'dashed') 150 | ax[0,0].set_xlabel('Source size src_S') 151 | ax[0,0].set_ylabel('Max. event size Smax') 152 | ax[0,0].set_title('Characteristic event size') 153 | 154 | ax[0,1].plot(Si, ratei, color = 'black') 155 | ind1 = np.where(EventTable['Src'] == 'src1')[0] 156 | ax[0,1].scatter(EventTable['S'][ind1], EventTable['rate'][ind1], color = 'orange') 157 | ind2 = np.where(EventTable['Src'] == 'src2')[0] 158 | offset = 1.1 # to avoid overlap 159 | ax[0,1].scatter(EventTable['S'][ind2], EventTable['rate'][ind2]*offset, color = 'red') 160 | ax[0,1].set_xscale('log') 161 | ax[0,1].set_yscale('log') 162 | ax[0,1].set_xlabel('Event size S') 163 | ax[0,1].set_ylabel('Rate') 164 | ax[0,1].set_title('Stochastic set distribution') 165 | 166 | ri = np.arange(0,50,.1) 167 | ax[0,2].plot(ri, func_intensity(Smax1, ri), color = 'orange') 168 | ax[0,2].plot(ri, func_intensity(Smax2, ri), color = 'red') 169 | ax[0,2].set_xlabel('Distance from source r') 170 | ax[0,2].set_ylabel('Intensity I') 171 | ax[0,2].set_title('Hazard intensity model') 172 | 173 | Ii = np.arange(.001,.1,.001) 174 | ax[0,3].plot(Ii, func_vulnerability(Ii, Theta_nu), color = 'black') 175 | ax[0,3].set_xlabel('Intensity I') 176 | ax[0,3].set_ylabel('Mean damage ratio') 177 | ax[0,3].set_title('Vulnerability curve') 178 | 179 | ax[1,0].contourf(x, y, np.ma.masked_where(nu_grid == 0, nu_grid), cmap = 'Greens', vmin = 0, vmax = 1) 180 | ax[1,0].set_xlabel('x') 181 | ax[1,0].set_ylabel('y') 182 | ax[1,0].set_title('Exposure footprint') 183 | 184 | ax[1,1].contourf(x, y, I_grid[ind1[-1],:,:], cmap = 'Reds', vmin = 0, vmax = np.max(I_grid)) 185 | ax[1,1].scatter(src1_x0, src1_y0, color = 'black', marker = '+') 186 | ax[1,1].set_xlabel('x') 187 | ax[1,1].set_ylabel('y') 188 | ax[1,1].set_title('Hazard intensity footprint (Smax1)') 189 | 190 | ax[1,2].contourf(x, y, D_grid[ind1[-1],:,:], cmap = 'Blues', vmin = 0, vmax = 1) 191 | ax[1,2].scatter(src1_x0, src1_y0, color = 'black', marker = '+') 192 | ax[1,2].set_xlabel('x') 193 | ax[1,2].set_ylabel('y') 194 | ax[1,2].set_title('Expected damage footprint (Smax1)') 195 | 196 | ax[1,3].contourf(x, y, np.ma.masked_where(L_grid[ind1[-1],:,:] == 0, L_grid[ind1[-1],:,:]), cmap = 'Purples', vmin = 0, vmax = 1) 197 | ax[1,3].scatter(src1_x0, src1_y0, color = 'black', marker = '+') 198 | ax[1,3].set_xlabel('x') 199 | ax[1,3].set_ylabel('y') 200 | ax[1,3].set_title('Loss footprint (Smax1)') 201 | 202 | ax[2,0].contourf(x, y, np.ma.masked_where(nu_grid == 0, nu_grid), cmap = 'Greens', vmin = 0, vmax = 1) 203 | ax[2,0].set_xlabel('x') 204 | ax[2,0].set_ylabel('y') 205 | ax[2,0].set_title('Exposure footprint') 206 | 207 | ax[2,1].contourf(x, y, I_grid[ind2[-1],:,:], cmap = 'Reds', vmin = 0, vmax = np.max(I_grid)) 208 | ax[2,1].scatter(src2_x0, src2_y0, color = 'black', marker = '+') 209 | ax[2,1].set_xlabel('x') 210 | ax[2,1].set_ylabel('y') 211 | ax[2,1].set_title('Hazard intensity footprint (Smax2)') 212 | 213 | ax[2,2].contourf(x, y, D_grid[ind2[-1],:,:], cmap = 'Blues', vmin = 0, vmax = 1) 214 | ax[2,2].scatter(src2_x0, src2_y0, color = 'black', marker = '+') 215 | ax[2,2].set_xlabel('x') 216 | ax[2,2].set_ylabel('y') 217 | ax[2,2].set_title('Expected damage footprint (Smax2)') 218 | 219 | ax[2,3].contourf(x, y, np.ma.masked_where(L_grid[ind2[-1],:,:] == 0, L_grid[ind2[-1],:,:]), cmap = 'Purples', vmin = 0, vmax = 1) 220 | ax[2,3].scatter(src2_x0, src2_y0, color = 'black', marker = '+') 221 | ax[2,3].set_xlabel('x') 222 | ax[2,3].set_ylabel('y') 223 | ax[2,3].set_title('Loss footprint (Smax2)') 224 | 225 | fig.tight_layout() 226 | plt.savefig('plots_template_Python.pdf') -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Arnaud Mignan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction to Catastrophe Risk Modelling: A Physics-based Approach 2 | 3 | This repository is a clone of the unlocked section of the [online resources page](https://www.cambridge.org/highereducation/books/introduction-to-catastrophe-risk-modelling/A3A5B5FB990921422BFEBB07734BF869/resources/instructor-resources/96CEE02A01B26976178E856371C5B11F) associated with the textbook '_Introduction to Catastrophe Risk Modelling: A Physics-based Approach_'. 4 | 5 | The textbook code can be considered version 1.0. Any future minor updates, which may not be implemented on the Cambridge University Press textbook page, will also be available here. 6 | 7 | **Citation:** Mignan, A. (2025), Introduction to Catastrophe Risk Modelling. A Physics-based Approach. Cambridge University Press, doi: [10.1017/9781009437370](https://www.cambridge.org/highereducation/books/introduction-to-catastrophe-risk-modelling/A3A5B5FB990921422BFEBB07734BF869#overview) 8 | 9 | 10 | ## CAT Starter Kit v1.0 11 | 12 | A basic template to develop a catastrophe (CAT) risk model (here with ad-hoc parameters & models). Available in both R (`CAT_StarterKit.R`) and Python (`CAT_StarterKit.py`). 13 | 14 | 15 | ## CAT Risk Modelling Sandbox v1.0 16 | 17 | This tutorial acts as a guide to catastrophe (CAT) risk modelling, streamlining the intricate processes typically associated with developing a CAT model while retaining a realistic context. This is achieved by implementing various perils in a virtual environment, as represented in the next figure (Mignan, 2025:fig. 3.10): 18 | 19 |
20 |
21 |
Instance of the virtual region generated for the CAT Risk Modelling Sandbox, here illustrating the topography and land use layers (the latter consisting of a water mass, built area, and road network) – Simulation rendered with Rayshader (Morgan-Wall, 2023).
23 | 24 | All code is available in `CATRiskModellingSandbox_codes.py`. Learn more by running the tutorial `CATRiskModellingSandbox_tutorial.ipynb`. Please note that the code is for educational purposes only, with models kept as simple as possible for ease of understanding. 25 | -------------------------------------------------------------------------------- /figures/fig3_10_sandbox_env_COLOR.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/figures/fig3_10_sandbox_env_COLOR.jpg -------------------------------------------------------------------------------- /figures/fig3_11_sandbox_fp_COLOR.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/figures/fig3_11_sandbox_fp_COLOR.jpg -------------------------------------------------------------------------------- /figures/movie_LS.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/figures/movie_LS.gif -------------------------------------------------------------------------------- /inputs/grid.json: -------------------------------------------------------------------------------- 1 | {"w": 0.1, "xmin": -20, "x0": 0, "xmax": 120, "ymin": -10, "ymax": 110, "xbuffer": 20, "ybuffer": 10, "lat_deg": 30} -------------------------------------------------------------------------------- /inputs/layer_landuse_S.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/inputs/layer_landuse_S.npy -------------------------------------------------------------------------------- /inputs/layer_roadNetwork.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/inputs/layer_roadNetwork.npy -------------------------------------------------------------------------------- /inputs/layer_soil_hs.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/inputs/layer_soil_hs.npy -------------------------------------------------------------------------------- /inputs/layer_topo_z.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/inputs/layer_topo_z.npy -------------------------------------------------------------------------------- /inputs/src.json: -------------------------------------------------------------------------------- 1 | {"perils": ["EQ", "FF", "VE"], "EQ": {"object": "fault", "x": [[20, 40, 70], [40, 60]], "y": [[60, 75, 80], [25, 30]], "w_km": [5, 5], "dip_deg": [45, 45], "z_km": [2, 2], "mec": ["R", "R"], "bin_km": 1}, "FF": {"object": "river", "riv_A_km": [10], "riv_lbd": [0.03], "riv_ome": [0.1], "riv_y0": [20], "Q_m3/s": [100000.0], "A_km2": 10000}, "VE": {"object": "volcano", "x": [90], "y": [90]}} -------------------------------------------------------------------------------- /plots_template_Python.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/plots_template_Python.pdf -------------------------------------------------------------------------------- /plots_template_R.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amignan/Intro2CATriskModelling/d15d86a47776d167e2a4d3ec08978cdcde4982ae/plots_template_R.pdf --------------------------------------------------------------------------------