-
Notifications
You must be signed in to change notification settings - Fork 154
Expand file tree
/
Copy pathbase.py
More file actions
executable file
·570 lines (493 loc) · 20.2 KB
/
base.py
File metadata and controls
executable file
·570 lines (493 loc) · 20.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
"""
This file is part of CLIMADA.
Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.
CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free
Software Foundation, version 3.
CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.
---
Define Measure class.
"""
__all__ = ["Measure"]
import copy
import logging
from pathlib import Path
from typing import Optional, Tuple
import numpy as np
import pandas as pd
from geopandas import GeoDataFrame
import climada.util.checker as u_check
from climada.entity.exposures.base import INDICATOR_CENTR, INDICATOR_IMPF, Exposures
from climada.hazard.base import Hazard
LOGGER = logging.getLogger(__name__)
IMPF_ID_FACT = 1000
"""Factor internally used as id for impact functions when region selected."""
NULL_STR = "nil"
"""String considered as no path in measures exposures_set and hazard_set or
no string in imp_fun_map"""
class Measure:
"""
Contains the definition of one measure.
Attributes
----------
name : str
name of the measure
haz_type : str
related hazard type (peril), e.g. TC
color_rgb : np.array
integer array of size 3. Color code of this measure in RGB
cost : float
discounted cost (in same units as assets)
hazard_set : str
file name of hazard to use (in h5 format)
hazard_freq_cutoff : float
hazard frequency cutoff
exposures_set : str or climada.entity.Exposure
file name of exposure to use (in h5 format) or Exposure instance
imp_fun_map : str
change of impact function id of exposures, e.g. '1to3'
hazard_inten_imp : tuple(float, float)
parameter a and b of hazard intensity change
mdd_impact : tuple(float, float)
parameter a and b of the impact over the mean damage degree
paa_impact : tuple(float, float)
parameter a and b of the impact over the percentage of affected assets
exp_region_id : int
region id of the selected exposures to consider ALL the previous
parameters
risk_transf_attach : float
risk transfer attachment
risk_transf_cover : float
risk transfer cover
risk_transf_cost_factor : float
factor to multiply to resulting insurance layer to get the total
cost of risk transfer
"""
def __init__(
self,
name: str = "",
haz_type: str = "",
cost: float = 0,
hazard_set: str = NULL_STR,
hazard_freq_cutoff: float = 0,
exposures_set: str = NULL_STR,
imp_fun_map: str = NULL_STR,
hazard_inten_imp: Tuple[float, float] = (1, 0),
mdd_impact: Tuple[float, float] = (1, 0),
paa_impact: Tuple[float, float] = (1, 0),
exp_region_id: Optional[list] = None,
risk_transf_attach: float = 0,
risk_transf_cover: float = 0,
risk_transf_cost_factor: float = 1,
color_rgb: Optional[np.ndarray] = None,
):
"""Initialize a Measure object with given values.
Parameters
----------
name : str, optional
name of the measure
haz_type : str, optional
related hazard type (peril), e.g. TC
cost : float, optional
discounted cost (in same units as assets)
hazard_set : str, optional
file name of hazard to use (in h5 format)
hazard_freq_cutoff : float, optional
hazard frequency cutoff
exposures_set : str or climada.entity.Exposure, optional
file name of exposure to use (in h5 format) or Exposure instance
imp_fun_map : str, optional
change of impact function id of exposures, e.g. '1to3'
hazard_inten_imp : tuple(float, float), optional
parameter a and b of hazard intensity change
mdd_impact : tuple(float, float), optional
parameter a and b of the impact over the mean damage degree
paa_impact : tuple(float, float), optional
parameter a and b of the impact over the percentage of affected assets
exp_region_id : int, optional
region id of the selected exposures to consider ALL the previous
parameters
risk_transf_attach : float, optional
risk transfer attachment
risk_transf_cover : float, optional
risk transfer cover
risk_transf_cost_factor : float, optional
factor to multiply to resulting insurance layer to get the total
cost of risk transfer
color_rgb : np.array, optional
integer array of size 3. Color code of this measure in RGB.
Default is None (corresponds to black).
"""
self.name = name
self.haz_type = haz_type
self.color_rgb = np.array([0, 0, 0]) if color_rgb is None else color_rgb
self.cost = cost
# related to change in hazard
self.hazard_set = hazard_set
self.hazard_freq_cutoff = hazard_freq_cutoff
# related to change in exposures
self.exposures_set = exposures_set
self.imp_fun_map = imp_fun_map
# related to change in impact functions
self.hazard_inten_imp = hazard_inten_imp
self.mdd_impact = mdd_impact
self.paa_impact = paa_impact
# related to change in region
self.exp_region_id = [] if exp_region_id is None else exp_region_id
# risk transfer
self.risk_transf_attach = risk_transf_attach
self.risk_transf_cover = risk_transf_cover
self.risk_transf_cost_factor = risk_transf_cost_factor
def check(self):
"""
Check consistent instance data.
Raises
------
ValueError
"""
u_check.size([3, 4], self.color_rgb, "Measure.color_rgb")
u_check.size(2, self.hazard_inten_imp, "Measure.hazard_inten_imp")
u_check.size(2, self.mdd_impact, "Measure.mdd_impact")
u_check.size(2, self.paa_impact, "Measure.paa_impact")
def calc_impact(self, exposures, imp_fun_set, hazard):
"""
Apply measure and compute impact and risk transfer of measure
implemented over inputs.
Parameters
----------
exposures : climada.entity.Exposures
exposures instance
imp_fun_set : climada.entity.ImpactFuncSet
impact function set instance
hazard : climada.hazard.Hazard
hazard instance
Returns
-------
climada.engine.Impact
resulting impact and risk transfer of measure
"""
new_exp, new_impfs, new_haz = self.apply(exposures, imp_fun_set, hazard)
# assign centroids if missing
if new_haz.centr_exp_col not in new_exp.gdf.columns:
LOGGER.warning(
"No assigned hazard centroids in exposure object after the "
"application of the measure. The centroids will be assigned during impact "
"calculation. This is potentiall costly. To silence this warning, make sure "
"that centroids are assigned to all exposures."
)
new_exp.assign_centroids(new_haz)
return self._calc_impact(new_exp, new_impfs, new_haz)
def apply(self, exposures, imp_fun_set, hazard):
"""
Implement measure with all its defined parameters.
Parameters
----------
exposures : climada.entity.Exposures
exposures instance
imp_fun_set : climada.entity.ImpactFuncSet
impact function set instance
hazard : climada.hazard.Hazard
hazard instance
Returns
-------
new_exp : climada.entity.Exposure
Exposure with implemented measure with all defined parameters
new_ifs : climada.entity.ImpactFuncSet
Impact function set with implemented measure with all defined parameters
new_haz : climada.hazard.Hazard
Hazard with implemented measure with all defined parameters
"""
# change hazard
new_haz = self._change_all_hazard(hazard)
# change exposures
new_exp = self._change_all_exposures(exposures)
new_exp = self._change_exposures_impf(new_exp)
# change impact functions
new_impfs = self._change_imp_func(imp_fun_set)
# cutoff events whose damage happen with high frequency (in region impf specified)
new_haz = self._cutoff_hazard_damage(new_exp, new_impfs, new_haz)
# apply all previous changes only to the selected exposures
new_exp, new_impfs, new_haz = self._filter_exposures(
exposures, imp_fun_set, hazard, new_exp, new_impfs, new_haz
)
return new_exp, new_impfs, new_haz
def _calc_impact(self, new_exp, new_impfs, new_haz):
"""Compute impact and risk transfer of measure implemented over inputs.
Parameters
----------
new_exp : climada.entity.Exposures
exposures once measure applied
new_ifs : climada.entity.ImpactFuncSet
impact function set once measure applied
new_haz : climada.hazard.Hazard
hazard once measure applied
Returns
-------
climada.engine.Impact
"""
from climada.engine.impact_calc import (
ImpactCalc, # pylint: disable=import-outside-toplevel
)
imp = ImpactCalc(new_exp, new_impfs, new_haz).impact(
save_mat=False, assign_centroids=False
)
return imp.calc_risk_transfer(self.risk_transf_attach, self.risk_transf_cover)
def _change_all_hazard(self, hazard):
"""
Change hazard to provided hazard_set.
Parameters
----------
hazard : climada.hazard.Hazard
hazard instance
Returns
-------
new_haz : climada.hazard.Hazard
Hazard
"""
if self.hazard_set == NULL_STR:
return hazard
LOGGER.debug("Setting new hazard %s", self.hazard_set)
new_haz = Hazard.from_hdf5(self.hazard_set)
new_haz.check()
return new_haz
def _change_all_exposures(self, exposures):
"""
Change exposures to provided exposures_set.
Parameters
----------
exposures : climada.entity.Exposures
exposures instance
Returns
-------
new_exp : climada.entity.Exposures()
Exposures
"""
if isinstance(self.exposures_set, str) and self.exposures_set == NULL_STR:
return exposures
if isinstance(self.exposures_set, (str, Path)):
LOGGER.debug("Setting new exposures %s", self.exposures_set)
new_exp = Exposures.from_hdf5(self.exposures_set)
new_exp.check()
elif isinstance(self.exposures_set, Exposures):
LOGGER.debug("Setting new exposures. ")
new_exp = self.exposures_set.copy(deep=True)
new_exp.check()
else:
raise ValueError(
f"{self.exposures_set} is neither a string nor an Exposures object"
)
if not np.array_equal(
np.unique(exposures.latitude), np.unique(new_exp.latitude)
) or not np.array_equal(
np.unique(exposures.longitude), np.unique(new_exp.longitude)
):
LOGGER.warning("Exposures locations have changed.")
return new_exp
def _change_exposures_impf(self, exposures):
"""Change exposures impact functions ids according to imp_fun_map.
Parameters
----------
exposures : climada.entity.Exposures
exposures instance
Returns
-------
new_exp : climada.entity.Exposure
Exposure with updated impact functions ids accordgin to
impf_fun_map
"""
if self.imp_fun_map == NULL_STR:
return exposures
LOGGER.debug("Setting new exposures impact functions%s", self.imp_fun_map)
new_exp = exposures.copy(deep=True)
from_id = int(self.imp_fun_map[0 : self.imp_fun_map.find("to")])
to_id = int(self.imp_fun_map[self.imp_fun_map.find("to") + 2 :])
try:
exp_change = np.argwhere(
new_exp.gdf[INDICATOR_IMPF + self.haz_type].values == from_id
).reshape(-1)
new_exp.gdf[INDICATOR_IMPF + self.haz_type].values[exp_change] = to_id
except KeyError:
exp_change = np.argwhere(
new_exp.gdf[INDICATOR_IMPF].values == from_id
).reshape(-1)
new_exp.gdf[INDICATOR_IMPF].values[exp_change] = to_id
return new_exp
def _change_imp_func(self, imp_set):
"""
Apply measure to impact functions of the same hazard type.
Parameters
----------
imp_set : climada.entity.ImpactFuncSet
impact function set instance to be modified
Returns
-------
new_imp_set : climada.entity.ImpactFuncSet
ImpactFuncSet with measure applied to each impact function
according to the defined hazard type
"""
if (
self.hazard_inten_imp == (1, 0)
and self.mdd_impact == (1, 0)
and self.paa_impact == (1, 0)
):
return imp_set
new_imp_set = copy.deepcopy(imp_set)
for imp_fun in new_imp_set.get_func(self.haz_type):
LOGGER.debug("Transforming impact functions.")
imp_fun.intensity = np.maximum(
imp_fun.intensity * self.hazard_inten_imp[0] - self.hazard_inten_imp[1],
0.0,
)
imp_fun.mdd = np.maximum(
imp_fun.mdd * self.mdd_impact[0] + self.mdd_impact[1], 0.0
)
imp_fun.paa = np.maximum(
imp_fun.paa * self.paa_impact[0] + self.paa_impact[1], 0.0
)
if not new_imp_set.size():
LOGGER.info("No impact function of hazard %s found.", self.haz_type)
return new_imp_set
def _cutoff_hazard_damage(self, exposures, impf_set, hazard):
"""Cutoff of hazard events which generate damage with a frequency higher
than hazard_freq_cutoff.
Parameters
----------
exposures : climada.entity.Exposures
exposures instance
imp_set : climada.entity.ImpactFuncSet
impact function set instance
hazard : climada.hazard.Hazard
hazard instance
Returns
-------
new_haz : climada.hazard.Hazard
Hazard without events which generate damage with a frequency
higher than hazard_freq_cutoff
"""
if self.hazard_freq_cutoff == 0:
return hazard
if self.exp_region_id:
# compute impact only in selected region
in_reg = np.logical_or.reduce(
[exposures.region_id == reg for reg in self.exp_region_id]
)
exp_imp = Exposures(exposures.gdf[in_reg], crs=exposures.crs)
else:
exp_imp = exposures
from climada.engine.impact_calc import (
ImpactCalc, # pylint: disable=import-outside-toplevel
)
imp = ImpactCalc(exp_imp, impf_set, hazard).impact(
assign_centroids=hazard.centr_exp_col not in exp_imp.gdf
)
LOGGER.debug(
"Cutting events whose damage have a frequency > %s.",
self.hazard_freq_cutoff,
)
new_haz = copy.deepcopy(hazard)
sort_idxs = np.argsort(imp.at_event)[::-1]
exceed_freq = np.cumsum(imp.frequency[sort_idxs])
cutoff = exceed_freq > self.hazard_freq_cutoff
sel_haz = sort_idxs[cutoff]
for row in sel_haz:
new_haz.intensity.data[
new_haz.intensity.indptr[row] : new_haz.intensity.indptr[row + 1]
] = 0
new_haz.intensity.eliminate_zeros()
return new_haz
def _filter_exposures(
self, exposures, imp_set, hazard, new_exp, new_impfs, new_haz
):
"""
Incorporate changes of new elements to previous ones only for the
selected exp_region_id. If exp_region_id is [], all new changes
will be accepted.
Parameters
----------
exposures : climada.entity.Exposures
old exposures instance
imp_set :climada.entity.ImpactFuncSet
old impact function set instance
hazard : climada.hazard.Hazard
old hazard instance
new_exp : climada.entity.Exposures
new exposures instance
new_ifs : climada.entity.ImpactFuncSet
new impact functions instance
new_haz : climada.hazard.Hazard
new hazard instance
Returns
-------
new_exp,new_ifs, new_haz : climada.entity.Exposures,
climada.entity.ImpactFuncSet,
climada.hazard.Hazard
Exposures, ImpactFuncSet, Hazard with incoporated elements
for the selected exp_region_id.
"""
if not self.exp_region_id:
return new_exp, new_impfs, new_haz
if exposures is new_exp:
new_exp = exposures.copy(deep=True)
if imp_set is not new_impfs:
# provide new impact functions ids to changed impact functions
fun_ids = list(new_impfs.get_func()[self.haz_type].keys())
for key in fun_ids:
new_impfs.get_func()[self.haz_type][key].id = key + IMPF_ID_FACT
new_impfs.get_func()[self.haz_type][
key + IMPF_ID_FACT
] = new_impfs.get_func()[self.haz_type][key]
try:
new_exp.gdf[INDICATOR_IMPF + self.haz_type] += IMPF_ID_FACT
except KeyError:
new_exp.gdf[INDICATOR_IMPF] += IMPF_ID_FACT
# collect old impact functions as well (used by exposures)
new_impfs.get_func()[self.haz_type].update(
imp_set.get_func()[self.haz_type]
)
# get the indices for changing and inert regions
chg_reg = exposures.gdf["region_id"].isin(self.exp_region_id)
no_chg_reg = ~chg_reg
LOGGER.debug("Number of changed exposures: %s", chg_reg.sum())
# concatenate previous and new exposures
new_exp.set_gdf(
GeoDataFrame(
pd.concat(
[
exposures.gdf[no_chg_reg], # old values for inert regions
new_exp.gdf[chg_reg], # new values for changing regions
]
).loc[
exposures.gdf.index, :
], # re-establish old order
),
crs=exposures.crs,
)
# set missing values of centr_
if (
INDICATOR_CENTR + self.haz_type in new_exp.gdf.columns
and np.isnan(new_exp.gdf[INDICATOR_CENTR + self.haz_type].values).any()
):
new_exp.gdf.drop(columns=INDICATOR_CENTR + self.haz_type, inplace=True)
elif (
INDICATOR_CENTR in new_exp.gdf.columns
and np.isnan(new_exp.gdf[INDICATOR_CENTR].values).any()
):
new_exp.gdf.drop(columns=INDICATOR_CENTR, inplace=True)
# put hazard intensities outside region to previous intensities
if hazard is not new_haz:
if INDICATOR_CENTR + self.haz_type in exposures.gdf.columns:
centr = exposures.gdf[INDICATOR_CENTR + self.haz_type].values[chg_reg]
elif INDICATOR_CENTR in exposures.gdf.columns:
centr = exposures.gdf[INDICATOR_CENTR].values[chg_reg]
else:
exposures.assign_centroids(hazard)
centr = exposures.gdf[INDICATOR_CENTR + self.haz_type].values[chg_reg]
centr = np.delete(np.arange(hazard.intensity.shape[1]), np.unique(centr))
new_haz_inten = new_haz.intensity.tolil()
new_haz_inten[:, centr] = hazard.intensity[:, centr]
new_haz.intensity = new_haz_inten.tocsr()
return new_exp, new_impfs, new_haz