flood-adapt 0.3.9__py3-none-any.whl → 0.3.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flood_adapt/__init__.py +26 -22
- flood_adapt/adapter/__init__.py +9 -9
- flood_adapt/adapter/fiat_adapter.py +1541 -1541
- flood_adapt/adapter/interface/hazard_adapter.py +70 -70
- flood_adapt/adapter/interface/impact_adapter.py +36 -36
- flood_adapt/adapter/interface/model_adapter.py +89 -89
- flood_adapt/adapter/interface/offshore.py +19 -19
- flood_adapt/adapter/sfincs_adapter.py +1848 -1848
- flood_adapt/adapter/sfincs_offshore.py +193 -193
- flood_adapt/config/config.py +248 -248
- flood_adapt/config/fiat.py +219 -219
- flood_adapt/config/gui.py +331 -331
- flood_adapt/config/sfincs.py +481 -336
- flood_adapt/config/site.py +129 -129
- flood_adapt/database_builder/database_builder.py +2210 -2210
- flood_adapt/database_builder/templates/default_units/imperial.toml +9 -9
- flood_adapt/database_builder/templates/default_units/metric.toml +9 -9
- flood_adapt/database_builder/templates/green_infra_table/green_infra_lookup_table.csv +10 -10
- flood_adapt/database_builder/templates/infographics/OSM/config_charts.toml +90 -90
- flood_adapt/database_builder/templates/infographics/OSM/config_people.toml +57 -57
- flood_adapt/database_builder/templates/infographics/OSM/config_risk_charts.toml +121 -121
- flood_adapt/database_builder/templates/infographics/OSM/config_roads.toml +65 -65
- flood_adapt/database_builder/templates/infographics/OSM/styles.css +45 -45
- flood_adapt/database_builder/templates/infographics/US_NSI/config_charts.toml +126 -126
- flood_adapt/database_builder/templates/infographics/US_NSI/config_people.toml +60 -60
- flood_adapt/database_builder/templates/infographics/US_NSI/config_risk_charts.toml +121 -121
- flood_adapt/database_builder/templates/infographics/US_NSI/config_roads.toml +65 -65
- flood_adapt/database_builder/templates/infographics/US_NSI/styles.css +45 -45
- flood_adapt/database_builder/templates/infometrics/OSM/metrics_additional_risk_configs.toml +4 -4
- flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config.toml +143 -143
- flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config_risk.toml +153 -153
- flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config.toml +127 -127
- flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config_risk.toml +57 -57
- flood_adapt/database_builder/templates/infometrics/US_NSI/metrics_additional_risk_configs.toml +4 -4
- flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config.toml +191 -191
- flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config_risk.toml +153 -153
- flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config.toml +178 -178
- flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config_risk.toml +57 -57
- flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config.toml +9 -9
- flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config_risk.toml +65 -65
- flood_adapt/database_builder/templates/output_layers/bin_colors.toml +5 -5
- flood_adapt/database_builder.py +16 -16
- flood_adapt/dbs_classes/__init__.py +21 -21
- flood_adapt/dbs_classes/database.py +495 -684
- flood_adapt/dbs_classes/dbs_benefit.py +77 -76
- flood_adapt/dbs_classes/dbs_event.py +61 -59
- flood_adapt/dbs_classes/dbs_measure.py +112 -111
- flood_adapt/dbs_classes/dbs_projection.py +34 -34
- flood_adapt/dbs_classes/dbs_scenario.py +137 -137
- flood_adapt/dbs_classes/dbs_static.py +274 -273
- flood_adapt/dbs_classes/dbs_strategy.py +130 -129
- flood_adapt/dbs_classes/dbs_template.py +279 -278
- flood_adapt/dbs_classes/interface/database.py +107 -139
- flood_adapt/dbs_classes/interface/element.py +121 -121
- flood_adapt/dbs_classes/interface/static.py +47 -47
- flood_adapt/flood_adapt.py +1207 -1178
- flood_adapt/misc/database_user.py +16 -16
- flood_adapt/misc/exceptions.py +22 -0
- flood_adapt/misc/log.py +183 -183
- flood_adapt/misc/path_builder.py +54 -54
- flood_adapt/misc/utils.py +185 -185
- flood_adapt/objects/__init__.py +82 -82
- flood_adapt/objects/benefits/benefits.py +61 -61
- flood_adapt/objects/events/event_factory.py +135 -135
- flood_adapt/objects/events/event_set.py +88 -84
- flood_adapt/objects/events/events.py +234 -234
- flood_adapt/objects/events/historical.py +58 -58
- flood_adapt/objects/events/hurricane.py +68 -67
- flood_adapt/objects/events/synthetic.py +46 -50
- flood_adapt/objects/forcing/__init__.py +92 -92
- flood_adapt/objects/forcing/csv.py +68 -68
- flood_adapt/objects/forcing/discharge.py +66 -66
- flood_adapt/objects/forcing/forcing.py +150 -150
- flood_adapt/objects/forcing/forcing_factory.py +182 -182
- flood_adapt/objects/forcing/meteo_handler.py +93 -93
- flood_adapt/objects/forcing/netcdf.py +40 -40
- flood_adapt/objects/forcing/plotting.py +453 -429
- flood_adapt/objects/forcing/rainfall.py +98 -98
- flood_adapt/objects/forcing/tide_gauge.py +191 -191
- flood_adapt/objects/forcing/time_frame.py +90 -90
- flood_adapt/objects/forcing/timeseries.py +564 -564
- flood_adapt/objects/forcing/unit_system.py +580 -580
- flood_adapt/objects/forcing/waterlevels.py +108 -108
- flood_adapt/objects/forcing/wind.py +124 -124
- flood_adapt/objects/measures/measure_factory.py +92 -92
- flood_adapt/objects/measures/measures.py +529 -529
- flood_adapt/objects/object_model.py +74 -68
- flood_adapt/objects/projections/projections.py +103 -103
- flood_adapt/objects/scenarios/scenarios.py +22 -22
- flood_adapt/objects/strategies/strategies.py +89 -89
- flood_adapt/workflows/benefit_runner.py +579 -554
- flood_adapt/workflows/floodmap.py +85 -85
- flood_adapt/workflows/impacts_integrator.py +85 -85
- flood_adapt/workflows/scenario_runner.py +70 -70
- {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.10.dist-info}/LICENSE +674 -674
- {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.10.dist-info}/METADATA +866 -865
- flood_adapt-0.3.10.dist-info/RECORD +140 -0
- flood_adapt-0.3.9.dist-info/RECORD +0 -139
- {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.10.dist-info}/WHEEL +0 -0
- {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.10.dist-info}/top_level.txt +0 -0
|
@@ -1,554 +1,579 @@
|
|
|
1
|
-
import shutil
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
import geopandas as gpd
|
|
5
|
-
import numpy as np
|
|
6
|
-
import numpy_financial as npf
|
|
7
|
-
import pandas as pd
|
|
8
|
-
import plotly.graph_objects as go
|
|
9
|
-
import tomli
|
|
10
|
-
import tomli_w
|
|
11
|
-
from fiat_toolbox.metrics_writer.fiat_read_metrics_file import MetricsFileReader
|
|
12
|
-
|
|
13
|
-
from flood_adapt.misc.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from flood_adapt.objects.
|
|
20
|
-
from flood_adapt.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class BenefitRunner:
|
|
24
|
-
"""Object holding all attributes and methods related to a benefit analysis."""
|
|
25
|
-
|
|
26
|
-
benefit: Benefit
|
|
27
|
-
_scenarios: pd.DataFrame
|
|
28
|
-
|
|
29
|
-
def __init__(self, database, benefit: Benefit):
|
|
30
|
-
"""Initialize function called when object is created through the load_file or load_dict methods."""
|
|
31
|
-
self.database = database
|
|
32
|
-
self.benefit = benefit
|
|
33
|
-
|
|
34
|
-
# Get output path based on database path
|
|
35
|
-
self.results_path = self.database.benefits.output_path.joinpath(
|
|
36
|
-
self.benefit.name
|
|
37
|
-
)
|
|
38
|
-
self.site_info = self.database.site
|
|
39
|
-
self.unit = self.site_info.fiat.config.damage_unit
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def scenarios(self) -> pd.DataFrame:
|
|
43
|
-
"""Get the scenarios of the benefit analysis.
|
|
44
|
-
|
|
45
|
-
Returns
|
|
46
|
-
-------
|
|
47
|
-
pd.DataFrame
|
|
48
|
-
a table with the scenarios of the Benefit analysis and their status
|
|
49
|
-
"""
|
|
50
|
-
self._scenarios = self.check_scenarios()
|
|
51
|
-
return self._scenarios
|
|
52
|
-
|
|
53
|
-
@property
|
|
54
|
-
def has_run(self):
|
|
55
|
-
return self.has_run_check()
|
|
56
|
-
|
|
57
|
-
@property
|
|
58
|
-
def results(self):
|
|
59
|
-
if hasattr(self, "_results"):
|
|
60
|
-
return self._results
|
|
61
|
-
self._results = self.get_output()
|
|
62
|
-
return self._results
|
|
63
|
-
|
|
64
|
-
def get_output(self) -> dict[str, Any]:
|
|
65
|
-
if not self.has_run:
|
|
66
|
-
raise RuntimeError(
|
|
67
|
-
f"Cannot read output since benefit analysis '{self.benefit.name}' has not been run yet."
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
results_toml = self.results_path.joinpath("results.toml")
|
|
71
|
-
results_html = self.results_path.joinpath("benefits.html")
|
|
72
|
-
with open(results_toml, mode="rb") as fp:
|
|
73
|
-
results = tomli.load(fp)
|
|
74
|
-
results["html"] = str(results_html)
|
|
75
|
-
self._results = results
|
|
76
|
-
return results
|
|
77
|
-
|
|
78
|
-
def has_run_check(self) -> bool:
|
|
79
|
-
"""Check if the benefit analysis has already been run.
|
|
80
|
-
|
|
81
|
-
Returns
|
|
82
|
-
-------
|
|
83
|
-
has_run : bool
|
|
84
|
-
True if the analysis has already been run, else False
|
|
85
|
-
"""
|
|
86
|
-
# Output files to check
|
|
87
|
-
results_toml = self.results_path.joinpath("results.toml")
|
|
88
|
-
results_csv = self.results_path.joinpath("time_series.csv")
|
|
89
|
-
results_html = self.results_path.joinpath("benefits.html")
|
|
90
|
-
|
|
91
|
-
check = all(
|
|
92
|
-
result.exists() for result in [results_toml, results_csv, results_html]
|
|
93
|
-
)
|
|
94
|
-
return check
|
|
95
|
-
|
|
96
|
-
def check_scenarios(self) -> pd.DataFrame:
|
|
97
|
-
"""Check which scenarios are needed for this benefit calculation and if they have already been created.
|
|
98
|
-
|
|
99
|
-
The scenarios attribute of the object is updated accordingly and the table of the scenarios is returned.
|
|
100
|
-
|
|
101
|
-
Returns
|
|
102
|
-
-------
|
|
103
|
-
pd.DataFrame
|
|
104
|
-
a table with the scenarios of the Benefit analysis and their status
|
|
105
|
-
"""
|
|
106
|
-
# Define names of scenarios
|
|
107
|
-
scenarios_calc = {
|
|
108
|
-
"current_no_measures": {},
|
|
109
|
-
"current_with_strategy": {},
|
|
110
|
-
"future_no_measures": {},
|
|
111
|
-
"future_with_strategy": {},
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
# Use the predefined names for the current projections and no measures strategy
|
|
115
|
-
for scenario in scenarios_calc.keys():
|
|
116
|
-
scenarios_calc[scenario]["event"] = self.benefit.event_set
|
|
117
|
-
|
|
118
|
-
if "current" in scenario:
|
|
119
|
-
scenarios_calc[scenario]["projection"] = (
|
|
120
|
-
self.benefit.current_situation.projection
|
|
121
|
-
)
|
|
122
|
-
elif "future" in scenario:
|
|
123
|
-
scenarios_calc[scenario]["projection"] = self.benefit.projection
|
|
124
|
-
|
|
125
|
-
if "no_measures" in scenario:
|
|
126
|
-
scenarios_calc[scenario]["strategy"] = self.benefit.baseline_strategy
|
|
127
|
-
else:
|
|
128
|
-
scenarios_calc[scenario]["strategy"] = self.benefit.strategy
|
|
129
|
-
|
|
130
|
-
# Get the available scenarios
|
|
131
|
-
scenarios_avail = [
|
|
132
|
-
self.database.scenarios.get(scn)
|
|
133
|
-
for scn in self.database.scenarios.summarize_objects()["name"]
|
|
134
|
-
]
|
|
135
|
-
|
|
136
|
-
# Check if any of the needed scenarios are already there
|
|
137
|
-
for scenario in scenarios_calc.keys():
|
|
138
|
-
scn_dict = scenarios_calc[scenario].copy()
|
|
139
|
-
scn_dict["name"] = scenario
|
|
140
|
-
scenario_obj = Scenario(**scn_dict)
|
|
141
|
-
created = [
|
|
142
|
-
scn_avl for scn_avl in scenarios_avail if scenario_obj == scn_avl
|
|
143
|
-
]
|
|
144
|
-
if len(created) > 0:
|
|
145
|
-
|
|
146
|
-
scenarios_calc[scenario]["scenario
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
scenarios_calc[scenario]["scenario
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
159
|
-
"scenario
|
|
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
|
-
# Save
|
|
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
|
-
benefits
|
|
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
|
-
benefits
|
|
484
|
-
benefits
|
|
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
|
-
|
|
1
|
+
import shutil
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy_financial as npf
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import plotly.graph_objects as go
|
|
9
|
+
import tomli
|
|
10
|
+
import tomli_w
|
|
11
|
+
from fiat_toolbox.metrics_writer.fiat_read_metrics_file import MetricsFileReader
|
|
12
|
+
|
|
13
|
+
from flood_adapt.misc.exceptions import DatabaseError
|
|
14
|
+
from flood_adapt.misc.path_builder import (
|
|
15
|
+
ObjectDir,
|
|
16
|
+
TopLevelDir,
|
|
17
|
+
db_path,
|
|
18
|
+
)
|
|
19
|
+
from flood_adapt.objects.benefits.benefits import Benefit
|
|
20
|
+
from flood_adapt.objects.scenarios.scenarios import Scenario
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BenefitRunner:
|
|
24
|
+
"""Object holding all attributes and methods related to a benefit analysis."""
|
|
25
|
+
|
|
26
|
+
benefit: Benefit
|
|
27
|
+
_scenarios: pd.DataFrame
|
|
28
|
+
|
|
29
|
+
def __init__(self, database, benefit: Benefit):
|
|
30
|
+
"""Initialize function called when object is created through the load_file or load_dict methods."""
|
|
31
|
+
self.database = database
|
|
32
|
+
self.benefit = benefit
|
|
33
|
+
|
|
34
|
+
# Get output path based on database path
|
|
35
|
+
self.results_path = self.database.benefits.output_path.joinpath(
|
|
36
|
+
self.benefit.name
|
|
37
|
+
)
|
|
38
|
+
self.site_info = self.database.site
|
|
39
|
+
self.unit = self.site_info.fiat.config.damage_unit
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def scenarios(self) -> pd.DataFrame:
|
|
43
|
+
"""Get the scenarios of the benefit analysis.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
pd.DataFrame
|
|
48
|
+
a table with the scenarios of the Benefit analysis and their status
|
|
49
|
+
"""
|
|
50
|
+
self._scenarios = self.check_scenarios()
|
|
51
|
+
return self._scenarios
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def has_run(self):
|
|
55
|
+
return self.has_run_check()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def results(self):
|
|
59
|
+
if hasattr(self, "_results"):
|
|
60
|
+
return self._results
|
|
61
|
+
self._results = self.get_output()
|
|
62
|
+
return self._results
|
|
63
|
+
|
|
64
|
+
def get_output(self) -> dict[str, Any]:
|
|
65
|
+
if not self.has_run:
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"Cannot read output since benefit analysis '{self.benefit.name}' has not been run yet."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
results_toml = self.results_path.joinpath("results.toml")
|
|
71
|
+
results_html = self.results_path.joinpath("benefits.html")
|
|
72
|
+
with open(results_toml, mode="rb") as fp:
|
|
73
|
+
results = tomli.load(fp)
|
|
74
|
+
results["html"] = str(results_html)
|
|
75
|
+
self._results = results
|
|
76
|
+
return results
|
|
77
|
+
|
|
78
|
+
def has_run_check(self) -> bool:
|
|
79
|
+
"""Check if the benefit analysis has already been run.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
has_run : bool
|
|
84
|
+
True if the analysis has already been run, else False
|
|
85
|
+
"""
|
|
86
|
+
# Output files to check
|
|
87
|
+
results_toml = self.results_path.joinpath("results.toml")
|
|
88
|
+
results_csv = self.results_path.joinpath("time_series.csv")
|
|
89
|
+
results_html = self.results_path.joinpath("benefits.html")
|
|
90
|
+
|
|
91
|
+
check = all(
|
|
92
|
+
result.exists() for result in [results_toml, results_csv, results_html]
|
|
93
|
+
)
|
|
94
|
+
return check
|
|
95
|
+
|
|
96
|
+
def check_scenarios(self) -> pd.DataFrame:
|
|
97
|
+
"""Check which scenarios are needed for this benefit calculation and if they have already been created.
|
|
98
|
+
|
|
99
|
+
The scenarios attribute of the object is updated accordingly and the table of the scenarios is returned.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
pd.DataFrame
|
|
104
|
+
a table with the scenarios of the Benefit analysis and their status
|
|
105
|
+
"""
|
|
106
|
+
# Define names of scenarios
|
|
107
|
+
scenarios_calc = {
|
|
108
|
+
"current_no_measures": {},
|
|
109
|
+
"current_with_strategy": {},
|
|
110
|
+
"future_no_measures": {},
|
|
111
|
+
"future_with_strategy": {},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Use the predefined names for the current projections and no measures strategy
|
|
115
|
+
for scenario in scenarios_calc.keys():
|
|
116
|
+
scenarios_calc[scenario]["event"] = self.benefit.event_set
|
|
117
|
+
|
|
118
|
+
if "current" in scenario:
|
|
119
|
+
scenarios_calc[scenario]["projection"] = (
|
|
120
|
+
self.benefit.current_situation.projection
|
|
121
|
+
)
|
|
122
|
+
elif "future" in scenario:
|
|
123
|
+
scenarios_calc[scenario]["projection"] = self.benefit.projection
|
|
124
|
+
|
|
125
|
+
if "no_measures" in scenario:
|
|
126
|
+
scenarios_calc[scenario]["strategy"] = self.benefit.baseline_strategy
|
|
127
|
+
else:
|
|
128
|
+
scenarios_calc[scenario]["strategy"] = self.benefit.strategy
|
|
129
|
+
|
|
130
|
+
# Get the available scenarios
|
|
131
|
+
scenarios_avail = [
|
|
132
|
+
self.database.scenarios.get(scn)
|
|
133
|
+
for scn in self.database.scenarios.summarize_objects()["name"]
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
# Check if any of the needed scenarios are already there
|
|
137
|
+
for scenario in scenarios_calc.keys():
|
|
138
|
+
scn_dict = scenarios_calc[scenario].copy()
|
|
139
|
+
scn_dict["name"] = scenario
|
|
140
|
+
scenario_obj = Scenario(**scn_dict)
|
|
141
|
+
created = [
|
|
142
|
+
scn_avl for scn_avl in scenarios_avail if scenario_obj == scn_avl
|
|
143
|
+
]
|
|
144
|
+
if len(created) > 0:
|
|
145
|
+
scenarios_calc[scenario]["scenario created"] = created[0].name
|
|
146
|
+
scenarios_calc[scenario]["scenario run"] = (
|
|
147
|
+
self.database.scenarios.has_run_check(created[0].name)
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
scenarios_calc[scenario]["scenario created"] = "No"
|
|
151
|
+
scenarios_calc[scenario]["scenario run"] = False
|
|
152
|
+
|
|
153
|
+
df = pd.DataFrame(scenarios_calc).T
|
|
154
|
+
scenarios = df.astype(
|
|
155
|
+
dtype={
|
|
156
|
+
"event": "str",
|
|
157
|
+
"projection": "str",
|
|
158
|
+
"strategy": "str",
|
|
159
|
+
"scenario created": "str",
|
|
160
|
+
"scenario run": bool,
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
return scenarios
|
|
164
|
+
|
|
165
|
+
def create_benefit_scenarios(self) -> None:
|
|
166
|
+
"""Create any scenarios that are needed for the (cost-)benefit assessment and are not there already.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
benefit : Benefit
|
|
171
|
+
"""
|
|
172
|
+
# Iterate through the scenarios needed and create them if not existing
|
|
173
|
+
for _, row in self.scenarios.iterrows():
|
|
174
|
+
if row["scenario created"] == "No":
|
|
175
|
+
name = "_".join([row["projection"], row["event"], row["strategy"]])
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
self.database.scenarios.get(name)
|
|
179
|
+
except DatabaseError:
|
|
180
|
+
# If the scenario does not exist, create it
|
|
181
|
+
scenario = Scenario(
|
|
182
|
+
name=name,
|
|
183
|
+
event=row["event"],
|
|
184
|
+
projection=row["projection"],
|
|
185
|
+
strategy=row["strategy"],
|
|
186
|
+
)
|
|
187
|
+
self.database.scenarios.save(scenario)
|
|
188
|
+
|
|
189
|
+
def ready_to_run(self) -> bool:
|
|
190
|
+
"""Check if all the required scenarios have already been run.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
bool
|
|
195
|
+
True if required scenarios have been already run
|
|
196
|
+
"""
|
|
197
|
+
check = all(self.scenarios["scenario run"])
|
|
198
|
+
|
|
199
|
+
return check
|
|
200
|
+
|
|
201
|
+
def run_cost_benefit(self):
|
|
202
|
+
"""Run the cost-benefit calculation for the total study area and the different aggregation levels."""
|
|
203
|
+
# Throw an error if not all runs are finished
|
|
204
|
+
if not self.ready_to_run():
|
|
205
|
+
# First check is scenarios are there
|
|
206
|
+
if "No" in self.scenarios["scenario created"].to_numpy():
|
|
207
|
+
raise RuntimeError("Necessary scenarios have not been created yet.")
|
|
208
|
+
scens = self.scenarios["scenario created"][~self.scenarios["scenario run"]]
|
|
209
|
+
raise RuntimeError(
|
|
210
|
+
f"Scenarios {', '.join(scens.values)} need to be run before the cost-benefit analysis can be performed"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# If path for results does not yet exist, make it, and if it does delete it and recreate it
|
|
214
|
+
if not self.results_path.is_dir():
|
|
215
|
+
self.results_path.mkdir(parents=True)
|
|
216
|
+
else:
|
|
217
|
+
shutil.rmtree(self.results_path)
|
|
218
|
+
self.results_path.mkdir(parents=True)
|
|
219
|
+
|
|
220
|
+
# Run the cost-benefit analysis
|
|
221
|
+
self.cba()
|
|
222
|
+
# Run aggregation benefits
|
|
223
|
+
self.cba_aggregation()
|
|
224
|
+
# Updates results
|
|
225
|
+
self.has_run_check()
|
|
226
|
+
|
|
227
|
+
# Cache results
|
|
228
|
+
self.results
|
|
229
|
+
|
|
230
|
+
def cba(self):
|
|
231
|
+
"""Cost-benefit analysis for the whole study area."""
|
|
232
|
+
# Get EAD for each scenario and save to new dataframe
|
|
233
|
+
scenarios = self.scenarios.copy(deep=True)
|
|
234
|
+
scenarios["EAD"] = None
|
|
235
|
+
|
|
236
|
+
scn_output_path = db_path(TopLevelDir.output, ObjectDir.scenario)
|
|
237
|
+
|
|
238
|
+
# Get metrics per scenario
|
|
239
|
+
for index, scenario in scenarios.iterrows():
|
|
240
|
+
scn_name = scenario["scenario created"]
|
|
241
|
+
collective_fn = scn_output_path.joinpath(
|
|
242
|
+
scn_name, f"Infometrics_{scn_name}.csv"
|
|
243
|
+
)
|
|
244
|
+
collective_metrics = MetricsFileReader(
|
|
245
|
+
collective_fn,
|
|
246
|
+
).read_metrics_from_file()
|
|
247
|
+
|
|
248
|
+
# Fill scenarios EAD column with values from metrics
|
|
249
|
+
scenarios.loc[index, "EAD"] = float(
|
|
250
|
+
collective_metrics["Value"]["ExpectedAnnualDamages"]
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Get years of interest
|
|
254
|
+
year_start = self.benefit.current_situation.year
|
|
255
|
+
year_end = self.benefit.future_year
|
|
256
|
+
|
|
257
|
+
# Calculate benefits
|
|
258
|
+
cba = self._calc_benefits(
|
|
259
|
+
years=[year_start, year_end],
|
|
260
|
+
risk_no_measures=[
|
|
261
|
+
scenarios.loc["current_no_measures", "EAD"],
|
|
262
|
+
scenarios.loc["future_no_measures", "EAD"],
|
|
263
|
+
],
|
|
264
|
+
risk_with_strategy=[
|
|
265
|
+
scenarios.loc["current_with_strategy", "EAD"],
|
|
266
|
+
scenarios.loc["future_with_strategy", "EAD"],
|
|
267
|
+
],
|
|
268
|
+
discount_rate=self.benefit.discount_rate,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Save indicators in dictionary
|
|
272
|
+
results = {}
|
|
273
|
+
# Get net present value of benefits
|
|
274
|
+
results["benefits"] = cba["benefits_discounted"].sum()
|
|
275
|
+
|
|
276
|
+
# Only if costs are provided do the full cost-benefit analysis
|
|
277
|
+
cost_calc = (self.benefit.implementation_cost is not None) and (
|
|
278
|
+
self.benefit.annual_maint_cost is not None
|
|
279
|
+
)
|
|
280
|
+
if cost_calc:
|
|
281
|
+
cba = self._calc_costs(
|
|
282
|
+
benefits=cba,
|
|
283
|
+
implementation_cost=self.benefit.implementation_cost,
|
|
284
|
+
annual_maint_cost=self.benefit.annual_maint_cost,
|
|
285
|
+
discount_rate=self.benefit.discount_rate,
|
|
286
|
+
)
|
|
287
|
+
# Calculate costs
|
|
288
|
+
results["costs"] = cba["costs_discounted"].sum()
|
|
289
|
+
# Benefit to Cost Ratio
|
|
290
|
+
results["BCR"] = np.round(results["benefits"] / results["costs"], 2)
|
|
291
|
+
# Net present value
|
|
292
|
+
results["NPV"] = cba["profits_discounted"].sum()
|
|
293
|
+
# Internal Rate of Return
|
|
294
|
+
results["IRR"] = np.round(npf.irr(cba["profits"]), 3)
|
|
295
|
+
|
|
296
|
+
# Save results
|
|
297
|
+
if not self.results_path.is_dir():
|
|
298
|
+
self.results_path.mkdir(parents=True)
|
|
299
|
+
|
|
300
|
+
# Save indicators in a toml file
|
|
301
|
+
indicators = self.results_path.joinpath("results.toml")
|
|
302
|
+
with open(indicators, "wb") as f:
|
|
303
|
+
tomli_w.dump(results, f)
|
|
304
|
+
|
|
305
|
+
# Save time-series in a csv
|
|
306
|
+
time_series = self.results_path.joinpath("time_series.csv")
|
|
307
|
+
cba.to_csv(time_series)
|
|
308
|
+
|
|
309
|
+
# Make html
|
|
310
|
+
self._make_html(cba)
|
|
311
|
+
|
|
312
|
+
def cba_aggregation(self):
|
|
313
|
+
"""Zonal Benefits analysis for the different aggregation areas."""
|
|
314
|
+
results_path = db_path(TopLevelDir.output, ObjectDir.scenario)
|
|
315
|
+
# Get years of interest
|
|
316
|
+
year_start = self.benefit.current_situation.year
|
|
317
|
+
year_end = self.benefit.future_year
|
|
318
|
+
|
|
319
|
+
# Get EAD for each scenario and save to new dataframe
|
|
320
|
+
scenarios = self.scenarios.copy(deep=True)
|
|
321
|
+
|
|
322
|
+
# Read in the names of the aggregation area types
|
|
323
|
+
aggregations = [aggr.name for aggr in self.site_info.fiat.config.aggregation]
|
|
324
|
+
|
|
325
|
+
# Check if equity information is available to define variables to use
|
|
326
|
+
vars = []
|
|
327
|
+
for i, aggr_name in enumerate(aggregations):
|
|
328
|
+
if self.site_info.fiat.config.aggregation[i].equity is not None:
|
|
329
|
+
vars.append(["EAD", "EWEAD"])
|
|
330
|
+
else:
|
|
331
|
+
vars.append(["EAD"])
|
|
332
|
+
|
|
333
|
+
# Define which names are used in the metric tables
|
|
334
|
+
var_metric = {"EAD": "ExpectedAnnualDamages", "EWEAD": "EWEAD"}
|
|
335
|
+
|
|
336
|
+
# Prepare dictionary to save values
|
|
337
|
+
risk = {}
|
|
338
|
+
|
|
339
|
+
# Fill in the dictionary
|
|
340
|
+
for i, aggr_name in enumerate(aggregations):
|
|
341
|
+
risk[aggr_name] = {}
|
|
342
|
+
values = {}
|
|
343
|
+
for var in vars[i]:
|
|
344
|
+
values[var] = []
|
|
345
|
+
for index, scenario in scenarios.iterrows():
|
|
346
|
+
scn_name = scenario["scenario created"]
|
|
347
|
+
# Get available aggregation levels
|
|
348
|
+
aggregation_fn = results_path.joinpath(
|
|
349
|
+
scn_name, f"Infometrics_{scn_name}_{aggr_name}.csv"
|
|
350
|
+
)
|
|
351
|
+
for var in vars[i]:
|
|
352
|
+
# Get metrics per scenario and per aggregation
|
|
353
|
+
aggregated_metrics = MetricsFileReader(
|
|
354
|
+
aggregation_fn,
|
|
355
|
+
).read_aggregated_metric_from_file(var_metric[var])[2:]
|
|
356
|
+
aggregated_metrics = aggregated_metrics.loc[
|
|
357
|
+
aggregated_metrics.index.dropna()
|
|
358
|
+
]
|
|
359
|
+
aggregated_metrics.name = scenario.name
|
|
360
|
+
values[var].append(aggregated_metrics)
|
|
361
|
+
|
|
362
|
+
# Combine values in a single dataframe
|
|
363
|
+
for var in vars[i]:
|
|
364
|
+
risk[aggr_name][var] = pd.DataFrame(values[var]).T.astype(float)
|
|
365
|
+
|
|
366
|
+
var_output = {"EAD": "Benefits", "EWEAD": "Equity Weighted Benefits"}
|
|
367
|
+
|
|
368
|
+
# Calculate benefits
|
|
369
|
+
benefits = {}
|
|
370
|
+
for i, aggr_name in enumerate(aggregations):
|
|
371
|
+
benefits[aggr_name] = pd.DataFrame()
|
|
372
|
+
benefits[aggr_name].index = risk[aggr_name]["EAD"].index
|
|
373
|
+
for var in vars[i]:
|
|
374
|
+
for index, row in risk[aggr_name][var].iterrows():
|
|
375
|
+
cba = self._calc_benefits(
|
|
376
|
+
years=[year_start, year_end],
|
|
377
|
+
risk_no_measures=[
|
|
378
|
+
row["current_no_measures"],
|
|
379
|
+
row["future_no_measures"],
|
|
380
|
+
],
|
|
381
|
+
risk_with_strategy=[
|
|
382
|
+
row["current_with_strategy"],
|
|
383
|
+
row["future_with_strategy"],
|
|
384
|
+
],
|
|
385
|
+
discount_rate=self.benefit.discount_rate,
|
|
386
|
+
)
|
|
387
|
+
benefits[aggr_name].loc[row.name, var_output[var]] = cba[
|
|
388
|
+
"benefits_discounted"
|
|
389
|
+
].sum()
|
|
390
|
+
|
|
391
|
+
# Save results
|
|
392
|
+
if not self.results_path.is_dir():
|
|
393
|
+
self.results_path.mkdir(parents=True)
|
|
394
|
+
|
|
395
|
+
# Save benefits per aggregation area (csv and gpkg)
|
|
396
|
+
for i, aggr_name in enumerate(aggregations):
|
|
397
|
+
csv_filename = self.results_path.joinpath(f"benefits_{aggr_name}.csv")
|
|
398
|
+
benefits[aggr_name].to_csv(csv_filename, index=True)
|
|
399
|
+
|
|
400
|
+
# Load aggregation areas
|
|
401
|
+
ind = [
|
|
402
|
+
i
|
|
403
|
+
for i, n in enumerate(self.site_info.fiat.config.aggregation)
|
|
404
|
+
if n.name == aggr_name
|
|
405
|
+
][0]
|
|
406
|
+
aggr_areas_path = (
|
|
407
|
+
db_path(TopLevelDir.static)
|
|
408
|
+
/ self.site_info.fiat.config.aggregation[ind].file
|
|
409
|
+
)
|
|
410
|
+
aggr_areas = gpd.read_file(aggr_areas_path, engine="pyogrio")
|
|
411
|
+
# Define output path
|
|
412
|
+
outpath = self.results_path.joinpath(f"benefits_{aggr_name}.gpkg")
|
|
413
|
+
# Save file
|
|
414
|
+
aggr_areas = aggr_areas.join(
|
|
415
|
+
benefits[aggr_name],
|
|
416
|
+
on=self.site_info.fiat.config.aggregation[ind].field_name,
|
|
417
|
+
)
|
|
418
|
+
aggr_areas.to_file(outpath, driver="GPKG")
|
|
419
|
+
|
|
420
|
+
@staticmethod
|
|
421
|
+
def _calc_benefits(
|
|
422
|
+
years: list[int, int],
|
|
423
|
+
risk_no_measures: list[float, float],
|
|
424
|
+
risk_with_strategy: list[float, float],
|
|
425
|
+
discount_rate: float,
|
|
426
|
+
) -> pd.DataFrame:
|
|
427
|
+
"""Calculate per year benefits and discounted benefits.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
years : list[int, int]
|
|
432
|
+
the current and future year for the analysis
|
|
433
|
+
risk_no_measures : list[float, float]
|
|
434
|
+
the current and future risk value without any measures
|
|
435
|
+
risk_with_strategy : list[float, float]
|
|
436
|
+
the current and future risk value with the strategy under investigation
|
|
437
|
+
discount_rate : float
|
|
438
|
+
the yearly discount rate used to calculated the total benefit
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
pd.DataFrame
|
|
443
|
+
Dataframe containing the time-series of risks and benefits per year
|
|
444
|
+
"""
|
|
445
|
+
benefits = pd.DataFrame(
|
|
446
|
+
data={"risk_no_measures": np.nan, "risk_with_strategy": np.nan},
|
|
447
|
+
index=np.arange(years[0], years[1] + 1),
|
|
448
|
+
)
|
|
449
|
+
benefits.index.names = ["year"]
|
|
450
|
+
|
|
451
|
+
# Fill in dataframe
|
|
452
|
+
for strat, risk in zip(
|
|
453
|
+
["no_measures", "with_strategy"], [risk_no_measures, risk_with_strategy]
|
|
454
|
+
):
|
|
455
|
+
benefits.loc[years[0], f"risk_{strat}"] = risk[0]
|
|
456
|
+
benefits.loc[years[1], f"risk_{strat}"] = risk[1]
|
|
457
|
+
|
|
458
|
+
# Assume linear trend between current and future
|
|
459
|
+
benefits = benefits.interpolate(method="linear")
|
|
460
|
+
|
|
461
|
+
# Calculate benefits
|
|
462
|
+
benefits["benefits"] = (
|
|
463
|
+
benefits["risk_no_measures"] - benefits["risk_with_strategy"]
|
|
464
|
+
)
|
|
465
|
+
# Calculate discounted benefits using the provided discount rate
|
|
466
|
+
benefits["benefits_discounted"] = benefits["benefits"] / (
|
|
467
|
+
1 + discount_rate
|
|
468
|
+
) ** (benefits.index - benefits.index[0])
|
|
469
|
+
|
|
470
|
+
return benefits
|
|
471
|
+
|
|
472
|
+
@staticmethod
|
|
473
|
+
def _calc_costs(
|
|
474
|
+
benefits: pd.DataFrame,
|
|
475
|
+
implementation_cost: float,
|
|
476
|
+
annual_maint_cost: float,
|
|
477
|
+
discount_rate: float,
|
|
478
|
+
) -> pd.DataFrame:
|
|
479
|
+
"""Calculate per year costs and discounted costs.
|
|
480
|
+
|
|
481
|
+
Parameters
|
|
482
|
+
----------
|
|
483
|
+
benefits : pd.DataFrame
|
|
484
|
+
a time series of benefits per year (produced with __calc_benefits method)
|
|
485
|
+
implementation_cost : float
|
|
486
|
+
initial costs of implementing the adaptation strategy
|
|
487
|
+
annual_maint_cost : float
|
|
488
|
+
annual maintenance cost of the adaptation strategy
|
|
489
|
+
discount_rate : float
|
|
490
|
+
yearly discount rate
|
|
491
|
+
|
|
492
|
+
Returns
|
|
493
|
+
-------
|
|
494
|
+
pd.DataFrame
|
|
495
|
+
Dataframe containing the time-series of benefits, costs and profits per year
|
|
496
|
+
"""
|
|
497
|
+
benefits = benefits.copy()
|
|
498
|
+
benefits["costs"] = np.nan
|
|
499
|
+
# implementations costs at current year and maintenance from year 1
|
|
500
|
+
benefits.loc[benefits.index[0], "costs"] = implementation_cost
|
|
501
|
+
benefits.loc[benefits.index[1:], "costs"] = annual_maint_cost
|
|
502
|
+
benefits["costs_discounted"] = benefits["costs"] / (1 + discount_rate) ** (
|
|
503
|
+
benefits.index - benefits.index[0]
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Benefit to Cost Ratio
|
|
507
|
+
benefits["profits"] = benefits["benefits"] - benefits["costs"]
|
|
508
|
+
benefits["profits_discounted"] = benefits["profits"] / (1 + discount_rate) ** (
|
|
509
|
+
benefits.index - benefits.index[0]
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
return benefits
|
|
513
|
+
|
|
514
|
+
def _make_html(self, cba):
|
|
515
|
+
"""Make an html with the time-series of the benefits and discounted benefits."""
|
|
516
|
+
# Save a plotly graph in an html
|
|
517
|
+
fig = go.Figure()
|
|
518
|
+
|
|
519
|
+
# Get only endpoints
|
|
520
|
+
cba2 = cba.iloc[[0, -1]]
|
|
521
|
+
|
|
522
|
+
# Add graph with benefits
|
|
523
|
+
fig.add_trace(
|
|
524
|
+
go.Scatter(
|
|
525
|
+
x=cba.index,
|
|
526
|
+
y=cba["benefits"],
|
|
527
|
+
mode="lines",
|
|
528
|
+
line_color="black",
|
|
529
|
+
name="Interpolated benefits",
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
fig.add_trace(
|
|
533
|
+
go.Scatter(
|
|
534
|
+
x=cba2.index,
|
|
535
|
+
y=cba2["benefits"],
|
|
536
|
+
mode="markers",
|
|
537
|
+
marker_size=10,
|
|
538
|
+
marker_color="rgba(53,217,44,1)",
|
|
539
|
+
name="Calculated benefits",
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
fig.add_trace(
|
|
544
|
+
go.Scatter(
|
|
545
|
+
x=cba.index,
|
|
546
|
+
y=cba["benefits_discounted"],
|
|
547
|
+
mode="lines",
|
|
548
|
+
line_color="rgba(35,150,29,1)",
|
|
549
|
+
name="Discounted benefits",
|
|
550
|
+
),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
fig.add_trace(
|
|
554
|
+
go.Scatter(
|
|
555
|
+
x=cba.index,
|
|
556
|
+
y=cba["benefits_discounted"],
|
|
557
|
+
mode="none",
|
|
558
|
+
fill="tozeroy",
|
|
559
|
+
fillcolor="rgba(35,150,29,0.5)",
|
|
560
|
+
name="Benefits",
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Update xaxis properties
|
|
565
|
+
fig.update_xaxes(title_text="Year")
|
|
566
|
+
# Update yaxis properties
|
|
567
|
+
fig.update_yaxes(title_text=f"Annual Benefits ({self.unit})")
|
|
568
|
+
|
|
569
|
+
fig.update_layout(
|
|
570
|
+
autosize=False,
|
|
571
|
+
height=400,
|
|
572
|
+
width=800,
|
|
573
|
+
margin={"r": 0, "l": 0, "b": 0, "t": 0},
|
|
574
|
+
font={"size": 12, "color": "black", "family": "Arial"},
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# write html to results folder
|
|
578
|
+
html = self.results_path.joinpath("benefits.html")
|
|
579
|
+
fig.write_html(html)
|